diff --git a/big_tests/default.spec b/big_tests/default.spec index 956670df95a..4bb8b9f247e 100644 --- a/big_tests/default.spec +++ b/big_tests/default.spec @@ -85,6 +85,7 @@ {suites, "tests", service_domain_db_SUITE}. {suites, "tests", domain_isolation_SUITE}. {suites, "tests", domain_removal_SUITE}. +{suites, "tests", mam_send_message_SUITE}. {config, ["test.config"]}. {logdir, "ct_report"}. diff --git a/big_tests/tests/mam_send_message_SUITE.erl b/big_tests/tests/mam_send_message_SUITE.erl new file mode 100644 index 00000000000..784c5af1242 --- /dev/null +++ b/big_tests/tests/mam_send_message_SUITE.erl @@ -0,0 +1,142 @@ +-module(mam_send_message_SUITE). + +%% API +-export([all/0, + groups/0, + init_per_suite/1, + end_per_suite/1, + init_per_group/2, + end_per_group/2, + init_per_testcase/2, + end_per_testcase/2]). + +-export([mam_muc_send_message/1, + mam_pm_send_message/1]). + +-import(mam_helper, + [stanza_archive_request/2, + wait_archive_respond/1, + assert_respond_size/2, + respond_messages/1, + parse_forwarded_message/1]). + +-import(distributed_helper, [mim/0, + require_rpc_nodes/1, + rpc/4]). + +-include("mam_helper.hrl"). +-include_lib("escalus/include/escalus.hrl"). +-include_lib("escalus/include/escalus_xmlns.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("exml/include/exml_stream.hrl"). + +all() -> + [{group, send_message}]. + +groups() -> + [ + {send_message, [], [mam_pm_send_message, + mam_muc_send_message]} + ]. + +domain() -> + ct:get_config({hosts, mim, domain}). + +%%%=================================================================== +%%% Overall setup/teardown +%%%=================================================================== +init_per_suite(Config) -> + escalus:init_per_suite(Config). + +end_per_suite(Config) -> + escalus:end_per_suite(Config). + +%%%=================================================================== +%%% Group specific setup/teardown +%%%=================================================================== +init_per_group(Group, Config) -> + case mongoose_helper:is_rdbms_enabled(domain()) of + true -> + load_custom_module(), + Config2 = dynamic_modules:save_modules(domain(), Config), + rpc(mim(), gen_mod_deps, start_modules, [domain(), group_to_modules(Group)]), + Config2; + false -> + {skip, require_rdbms} + end. + +end_per_group(_Groupname, Config) -> + case mongoose_helper:is_rdbms_enabled(domain()) of + true -> + dynamic_modules:restore_modules(domain(), Config); + false -> + ok + end, + ok. + +group_to_modules(send_message) -> + MH = muc_light_helper:muc_host(), + [{mod_mam_meta, [{backend, rdbms}, {pm, []}, {muc, [{host, MH}]}, + {send_message, mam_send_message_example}]}, + {mod_muc_light, []}, + {mam_send_message_example, []}]. + +load_custom_module() -> + mam_send_message_example:module_info(), + {Mod, Code, File} = code:get_object_code(mam_send_message_example), + rpc(mim(), code, load_binary, [Mod, File, Code]). + +%%%=================================================================== +%%% Testcase specific setup/teardown +%%%=================================================================== + +init_per_testcase(TestCase, Config) -> + escalus:init_per_testcase(TestCase, Config). + +end_per_testcase(TestCase, Config) -> + escalus:end_per_testcase(TestCase, Config). + +%%%=================================================================== +%%% Test Cases +%%%=================================================================== + +mam_pm_send_message(Config) -> + P = ?config(props, Config), + F = fun(Alice, Bob) -> + escalus:send(Alice, escalus_stanza:chat_to(Bob, <<"OH, HAI!">>)), + escalus:wait_for_stanza(Bob), + mam_helper:wait_for_archive_size(Alice, 1), + mam_helper:wait_for_archive_size(Bob, 1), + escalus:send(Alice, stanza_archive_request(P, <<"q1">>)), + Res = wait_archive_respond(Alice), + assert_respond_size(1, Res), + [Msg] = respond_messages(Res), + verify_has_some_hash(Msg) + end, + escalus_fresh:story(Config, [{alice, 1}, {bob, 1}], F). + +mam_muc_send_message(Config0) -> + F = fun(Config, Alice) -> + P = ?config(props, Config), + Room = muc_helper:fresh_room_name(), + MucHost = muc_light_helper:muc_host(), + muc_light_helper:create_room(Room, MucHost, alice, + [], Config, muc_light_helper:ver(1)), + escalus_assert:has_no_stanzas(Alice), + RoomAddr = <>, + escalus:send(Alice, escalus_stanza:groupchat_to(RoomAddr, <<"text">>)), + M = escalus:wait_for_stanza(Alice), + escalus:assert(is_message, M), + escalus_assert:has_no_stanzas(Alice), + mam_helper:wait_for_room_archive_size(MucHost, Room, 1), + escalus:send(Alice, escalus_stanza:to(stanza_archive_request(P, <<"q1">>), RoomAddr)), + [Msg] = respond_messages(assert_respond_size(1, wait_archive_respond(Alice))), + verify_has_some_hash(Msg) + end, + escalus_fresh:story_with_config(Config0, [{alice, 1}], F). + +verify_has_some_hash(Msg) -> + Hash = exml_query:path(Msg, [{element, <<"result">>}, + {element, <<"some_hash">>}, + {attr, <<"value">>}]), + binary_to_integer(Hash). %% is integer diff --git a/big_tests/tests/mam_send_message_SUITE_data/mam_send_message_example.erl b/big_tests/tests/mam_send_message_SUITE_data/mam_send_message_example.erl new file mode 100644 index 00000000000..f5fc28acce6 --- /dev/null +++ b/big_tests/tests/mam_send_message_SUITE_data/mam_send_message_example.erl @@ -0,0 +1,46 @@ +%% Adds some_hash element to each extracted message result. +%% +%% An example module for extending MAM lookup results. +%% Defines a callback for send_message callback. +%% Handles lookup messages hooks to extend message rows with extra info. +-module(mam_send_message_example). +-behaviour(gen_mod). +-behaviour(mongoose_module_metrics). +-include_lib("exml/include/exml.hrl"). + +-export([start/2, + stop/1, + lookup_messages/3, + send_message/4]). + + +start(Host, _Opts) -> + ejabberd_hooks:add(hooks(Host)). + +stop(Host) -> + ejabberd_hooks:delete(hooks(Host)). + +hooks(Host) -> + [{mam_lookup_messages, Host, ?MODULE, lookup_messages, 60}, + {mam_muc_lookup_messages, Host, ?MODULE, lookup_messages, 60}]. + +lookup_messages({error, _Reason} = Result, _Host, _Params) -> + Result; +lookup_messages({ok, {TotalCount, Offset, MessageRows}}, + Host, Params = #{owner_jid := ArcJID}) -> + MessageRows2 = [extend_message(Host, ArcJID, Row) || Row <- MessageRows], + {ok, {TotalCount, Offset, MessageRows2}}. + +extend_message(_Host, _ArcJID, Row = #{}) -> + %% Extend a message with a new field + %% Usually extracted from a DB + Row#{some_hash => erlang:phash2(Row, 32)}. + +send_message(Row, From, To, Mess) -> + Res = xml:get_subtag(Mess, <<"result">>), + Res2 = xml:append_subtags(Res, [new_subelem(Row)]), + Mess2 = xml:replace_subelement(Mess, Res2), + mod_mam_utils:send_message(Row, From, To, Mess2). + +new_subelem(#{some_hash := SomeHash}) -> + #xmlel{name = <<"some_hash">>, attrs = [{<<"value">>, integer_to_binary(SomeHash)}]}. diff --git a/doc/modules/mod_mam.md b/doc/modules/mod_mam.md index e789f7ebad4..af5ac231db1 100644 --- a/doc/modules/mod_mam.md +++ b/doc/modules/mod_mam.md @@ -74,6 +74,9 @@ Name of a module implementing [`is_archivable_message/3` callback](#is_archivabl Name of a module implementing `send_message/4` callback that routes a message during lookup operation. Consult with `mod_mam_utils:send_message/4` code for more information. +Check `big_tests/tests/mam_send_message_SUITE_data/mam_send_message_example.erl` file +in the MongooseIM repository for the usage example. + ### `modules.mod_mam_meta.archive_chat_markers` * **Syntax:** boolean * **Default:** `false` diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl index 355801e630a..a94625adfd4 100644 --- a/src/mod_muc_room.erl +++ b/src/mod_muc_room.erl @@ -4319,18 +4319,10 @@ send_decline_invitation({Packet, XEl, DEl, ToJID}, RoomJID, FromJID) -> DAttrs2 = lists:keydelete(<<"to">>, 1, DAttrs), DAttrs3 = [{<<"from">>, FromString} | DAttrs2], DEl2 = #xmlel{name = <<"decline">>, attrs = DAttrs3, children = DEls}, - XEl2 = replace_subelement(XEl, DEl2), - Packet2 = replace_subelement(Packet, XEl2), + XEl2 = xml:replace_subelement(XEl, DEl2), + Packet2 = xml:replace_subelement(Packet, XEl2), ejabberd_router:route(RoomJID, ToJID, Packet2). -%% @doc Given an element and a new subelement, -%% replace the instance of the subelement in element with the new subelement. --spec replace_subelement(exml:element(), exml:element()) -> exml:element(). -replace_subelement(XE = #xmlel{children = SubEls}, NewSubEl) -> - {_, NameNewSubEl, _, _} = NewSubEl, - SubEls2 = lists:keyreplace(NameNewSubEl, 2, SubEls, NewSubEl), - XE#xmlel{children = SubEls2}. - -spec send_error_only_occupants(binary(), exml:element(), binary() | nonempty_string(), jid:jid(), jid:jid()) -> mongoose_acc:t(). diff --git a/src/xml.erl b/src/xml.erl index c5c1c8444cc..3ab7c3d036c 100644 --- a/src/xml.erl +++ b/src/xml.erl @@ -33,7 +33,8 @@ get_subtag/2, append_subtags/2, get_path_s/2, - replace_tag_attr/3]). + replace_tag_attr/3, + replace_subelement/2]). -include("mongoose.hrl"). -include("jlib.hrl"). @@ -144,6 +145,13 @@ replace_tag_attr(Attr, Value, XE = #xmlel{attrs = Attrs}) -> Attrs2 = [{Attr, Value} | Attrs1], XE#xmlel{attrs = Attrs2}. +%% @doc Given an element and a new subelement, +%% replace the instance of the subelement in element with the new subelement. +-spec replace_subelement(exml:element(), exml:element()) -> exml:element(). +replace_subelement(XE = #xmlel{children = SubEls}, NewSubEl) -> + {_, NameNewSubEl, _, _} = NewSubEl, + SubEls2 = lists:keyreplace(NameNewSubEl, 2, SubEls, NewSubEl), + XE#xmlel{children = SubEls2}. -spec context_default(binary() | string()) -> <<>> | []. context_default(Attr) when is_list(Attr) ->