diff --git a/big_tests/tests/smart_markers_SUITE.erl b/big_tests/tests/smart_markers_SUITE.erl index 068e8843a8..82c911326d 100644 --- a/big_tests/tests/smart_markers_SUITE.erl +++ b/big_tests/tests/smart_markers_SUITE.erl @@ -1,16 +1,21 @@ -module(smart_markers_SUITE). -compile([export_all, nowarn_export_all]). +-include_lib("stdlib/include/assert.hrl"). +-include_lib("escalus/include/escalus_xmlns.hrl"). +-include_lib("exml/include/exml.hrl"). -include("muc_light.hrl"). +-define(NS_ESL_SMART_MARKERS, <<"esl:xmpp:smart-markers:0">>). +-define(NS_STANZAID, <<"urn:xmpp:sid:0">>). -import(distributed_helper, [mim/0, rpc/4, subhost_pattern/1]). -import(domain_helper, [host_type/0]). --import(config_parser_helper, [mod_config/2]). +-import(config_parser_helper, [default_mod_config/1, mod_config/2]). %%% Suite configuration all() -> case (not ct_helper:is_ct_running()) - orelse mongoose_helper:is_rdbms_enabled(host_type()) of + orelse mongoose_helper:is_rdbms_enabled(host_type()) of true -> all_cases(); false -> {skip, require_rdbms} end. @@ -18,21 +23,36 @@ all() -> all_cases() -> [ {group, one2one}, - {group, muclight} + {group, muclight}, + {group, keep_private} ]. groups() -> [ - {one2one, [], + {one2one, [parallel], [ + error_set_iq, + error_bad_peer, + error_no_peer_given, + error_bad_timestamp, marker_is_stored, + marker_can_be_fetched, + marker_for_thread_can_be_fetched, + marker_after_timestamp_can_be_fetched, + marker_after_timestamp_for_threadid_can_be_fetched, remove_markers_when_removed_user ]}, - {muclight, [], + {muclight, [parallel], [ marker_is_stored_for_room, + marker_can_be_fetched_for_room, marker_is_removed_when_user_leaves_room, markers_are_removed_when_room_is_removed + ]}, + {keep_private, [parallel], + [ + marker_is_not_routed_nor_fetchable, + fetching_room_answers_only_own_marker ]} ]. @@ -50,9 +70,12 @@ init_per_group(GroupName, Config) -> Config. group_to_module(one2one) -> - [{mod_smart_markers, [{backend, rdbms}]}]; + [{mod_smart_markers, default_mod_config(mod_smart_markers)}]; +group_to_module(keep_private) -> + [{mod_smart_markers, mod_config(mod_smart_markers, #{keep_private => true})}, + {mod_muc_light, mod_config(mod_muc_light, #{backend => rdbms})}]; group_to_module(muclight) -> - [{mod_smart_markers, [{backend, rdbms}]}, + [{mod_smart_markers, default_mod_config(mod_smart_markers)}, {mod_muc_light, mod_config(mod_muc_light, #{backend => rdbms})}]. end_per_group(muclight, Config) -> @@ -69,6 +92,40 @@ end_per_testcase(Name, Config) -> escalus:end_per_testcase(Name, Config). %%% tests +error_set_iq(Config) -> + escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> + Query = escalus_stanza:query_el(?NS_ESL_SMART_MARKERS, []), + Iq = escalus_stanza:iq(<<"set">>, [Query]), + escalus:send(Alice, Iq), + Response = escalus:wait_for_stanza(Alice), + escalus:assert(is_iq_error, [Iq], Response) + end). + +error_bad_peer(Config) -> + escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> + Iq = iq_fetch_marker([{<<"peer">>, <<"/@">>}]), + escalus:send(Alice, Iq), + Response = escalus:wait_for_stanza(Alice), + escalus:assert(is_iq_error, [Iq], Response) + end). + +error_no_peer_given(Config) -> + escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> + Iq = iq_fetch_marker([]), + escalus:send(Alice, Iq), + Response = escalus:wait_for_stanza(Alice), + escalus:assert(is_iq_error, [Iq], Response) + end). + +error_bad_timestamp(Config) -> + escalus:fresh_story(Config, [{alice, 1}], fun(Alice) -> + PeerJid = <<"peer@localhost">>, + Iq = iq_fetch_marker([{<<"peer">>, PeerJid}, {<<"after">>, <<"baddate">>}]), + escalus:send(Alice, Iq), + Response = escalus:wait_for_stanza(Alice), + escalus:assert(is_iq_error, [Iq], Response) + end). + marker_is_stored(Config) -> escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> send_message_respond_marker(Alice, Bob), @@ -78,6 +135,72 @@ marker_is_stored(Config) -> fun() -> length(fetch_markers_for_users(BobJid, AliceJid)) > 0 end, true) end). +marker_can_be_fetched(Config) -> + escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> + send_message_respond_marker(Alice, Bob), + send_message_respond_marker(Bob, Alice), + verify_marker_fetch(Bob, Alice), + verify_marker_fetch(Alice, Bob) + end). + +marker_is_not_routed_nor_fetchable(Config) -> + escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> + MsgId = escalus_stanza:id(), + Msg = escalus_stanza:set_id(escalus_stanza:chat_to(Bob, <<"Hello!">>), MsgId), + escalus:send(Alice, Msg), + escalus:wait_for_stanza(Bob), + ChatMarker = escalus_stanza:chat_marker(Alice, <<"displayed">>, MsgId), + escalus:send(Bob, ChatMarker), + escalus_assert:has_no_stanzas(Alice), %% Marker is filtered, Alice won't receive the marker + verify_marker_fetch_is_empty(Alice, Bob), %% Alice won't see Bob's marker + verify_marker_fetch(Bob, Alice) %% Bob will see his own marker + end). + +fetching_room_answers_only_own_marker(Config) -> + escalus:fresh_story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], fun(Alice, Bob, Kate) -> + Users = [Alice, Bob, Kate], + RoomId = create_room(Alice, [Bob, Kate], Config), + RoomBinJid = muc_light_helper:room_bin_jid(RoomId), + send_msg_to_room(Users, RoomBinJid, Alice, escalus_stanza:id()), + send_msg_to_room(Users, RoomBinJid, Bob, escalus_stanza:id()), + MsgId = send_msg_to_room(Users, RoomBinJid, Kate, escalus_stanza:id()), + ChatMarker = escalus_stanza:setattr( + escalus_stanza:chat_marker(RoomBinJid, <<"displayed">>, MsgId), + <<"type">>, <<"groupchat">>), + [ begin + escalus:send(User, ChatMarker), + MUser = verify_marker_fetch(User, RoomBinJid), + ?assertEqual(1, length(MUser)) + end || User <- [Alice, Bob] ] + end). + +marker_for_thread_can_be_fetched(Config) -> + escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> + ThreadId = <<"some-thread-id">>, + send_message_respond_marker(Alice, Bob), + send_message_respond_marker(Alice, Bob, ThreadId), + verify_marker_fetch(Bob, Alice, ThreadId, undefined) + end). + +marker_after_timestamp_can_be_fetched(Config) -> + escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> + TS = rpc(mim(), erlang, system_time, [microsecond]), + BinTS = list_to_binary(calendar:system_time_to_rfc3339(TS, [{offset, "Z"}, {unit, microsecond}])), + send_message_respond_marker(Alice, Bob), + send_message_respond_marker(Alice, Bob), + verify_marker_fetch(Bob, Alice, undefined, BinTS) + end). + +marker_after_timestamp_for_threadid_can_be_fetched(Config) -> + escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> + ThreadId = <<"some-thread-id">>, + TS = rpc(mim(), erlang, system_time, [microsecond]), + BinTS = list_to_binary(calendar:system_time_to_rfc3339(TS, [{offset, "Z"}, {unit, microsecond}])), + send_message_respond_marker(Alice, Bob), + send_message_respond_marker(Alice, Bob, ThreadId), + verify_marker_fetch(Bob, Alice, ThreadId, BinTS) + end). + remove_markers_when_removed_user(Config) -> escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> Body = <<"Hello Bob!">>, @@ -107,6 +230,16 @@ marker_is_stored_for_room(Config) -> fun() -> length(fetch_markers_for_users(BobJid, jid:from_binary(RoomBinJid))) > 0 end, true) end). +marker_can_be_fetched_for_room(Config) -> + escalus:fresh_story(Config, [{alice, 1}, {bob, 1}, {kate, 1}], + fun(Alice, Bob, Kate) -> + Users = [Alice, Bob, Kate], + RoomId = create_room(Alice, [Bob, Kate], Config), + RoomBinJid = muc_light_helper:room_bin_jid(RoomId), + one_marker_in_room(Users, RoomBinJid, Alice, Bob), + verify_marker_fetch(Bob, RoomBinJid) + end). + marker_is_removed_when_user_leaves_room(Config) -> escalus:fresh_story(Config, [{alice, 1}, {bob, 1}], fun(Alice, Bob) -> @@ -149,6 +282,10 @@ fetch_markers_for_users(From, To) -> [host_type(), To, undefined, 0]), [MR || #{from := FR} = MR <- MRs, jid:are_bare_equal(From, FR)]. +iq_fetch_marker(Attrs) -> + Query = escalus_stanza:query_el(?NS_ESL_SMART_MARKERS, Attrs, []), + escalus_stanza:iq(<<"get">>, [Query]). + create_room(Owner, Members, Config) -> RoomId = muc_helper:fresh_room_name(), MucHost = muc_light_helper:muc_host(), @@ -164,27 +301,91 @@ delete_room(Owner, Users, RoomBinJid) -> one_marker_in_room(Users, RoomBinJid, Writer, Marker) -> MsgId = escalus_stanza:id(), - Msg = escalus_stanza:set_id( - escalus_stanza:groupchat_to(RoomBinJid, <<"Hello">>), MsgId), + StzId = send_msg_to_room(Users, RoomBinJid, Writer, MsgId), + mark_msg(Users, RoomBinJid, Marker, StzId). + +send_msg_to_room(Users, RoomBinJid, Writer, MsgId) -> + Msg = escalus_stanza:set_id(escalus_stanza:groupchat_to(RoomBinJid, <<"Hello">>), MsgId), escalus:send(Writer, Msg), - [ escalus:wait_for_stanza(User) || User <- Users], + Msgs = [ escalus:wait_for_stanza(User) || User <- Users], + get_id(hd(Msgs), MsgId). + +mark_msg(Users, RoomBinJid, Marker, StzId) -> ChatMarker = escalus_stanza:setattr( - escalus_stanza:chat_marker(RoomBinJid, <<"displayed">>, MsgId), + escalus_stanza:chat_marker(RoomBinJid, <<"displayed">>, StzId), <<"type">>, <<"groupchat">>), escalus:send(Marker, ChatMarker), [ escalus:wait_for_stanza(User) || User <- Users], - MsgId. + StzId. send_message_respond_marker(MsgWriter, MarkerAnswerer) -> - Body = <<"Hello">>, + send_message_respond_marker(MsgWriter, MarkerAnswerer, undefined). + +send_message_respond_marker(MsgWriter, MarkerAnswerer, MaybeThread) -> MsgId = escalus_stanza:id(), - Msg = escalus_stanza:set_id(escalus_stanza:chat_to(MarkerAnswerer, Body), MsgId), + Msg = add_thread_id(escalus_stanza:set_id( + escalus_stanza:chat_to(MarkerAnswerer, <<"Hello!">>), + MsgId), + MaybeThread), escalus:send(MsgWriter, Msg), escalus:wait_for_stanza(MarkerAnswerer), - ChatMarker = escalus_stanza:chat_marker(MsgWriter, <<"displayed">>, MsgId), + ChatMarker = add_thread_id(escalus_stanza:chat_marker( + MsgWriter, <<"displayed">>, MsgId), + MaybeThread), escalus:send(MarkerAnswerer, ChatMarker), escalus:wait_for_stanza(MsgWriter). +verify_marker_fetch(MarkingUser, MarkedUser) -> + verify_marker_fetch(MarkingUser, MarkedUser, undefined, undefined). + +verify_marker_fetch(MarkingUser, MarkedUser, Thread, After) -> + MarkedUserBJid = case is_binary(MarkedUser) of + true -> [{<<"peer">>, MarkedUser}]; + false -> [{<<"peer">>, escalus_utils:jid_to_lower(escalus_client:short_jid(MarkedUser))}] + end, + MaybeThread = case Thread of + undefined -> []; + _ -> [{<<"thread">>, Thread}] + end, + MaybeAfter = case After of + undefined -> []; + _ -> [{<<"after">>, After}] + end, + Iq = iq_fetch_marker(MarkedUserBJid ++ MaybeThread ++ MaybeAfter), + escalus:send(MarkingUser, Iq), + Response = escalus:wait_for_stanza(MarkingUser), + escalus:assert(is_iq_result, [Iq], Response), + Markers = [Marker | _] = exml_query:paths( + Response, [{element_with_ns, <<"query">>, ?NS_ESL_SMART_MARKERS}, + {element, <<"marker">>}]), + ?assertNotEqual(undefined, Marker), + ?assertNotEqual(undefined, exml_query:attr(Marker, <<"timestamp">>)), + ?assertEqual(<<"displayed">>, exml_query:attr(Marker, <<"type">>)), + ?assertEqual(Thread, exml_query:attr(Marker, <<"thread">>)), + ?assertNotEqual(undefined, exml_query:attr(Marker, <<"id">>)), + lists:sort(Markers). + +verify_marker_fetch_is_empty(MarkingUser, MarkedUser) -> + MarkedUserBJid = escalus_utils:jid_to_lower(escalus_client:short_jid(MarkedUser)), + Iq = iq_fetch_marker([{<<"peer">>, MarkedUserBJid}]), + escalus:send(MarkingUser, Iq), + Response = escalus:wait_for_stanza(MarkingUser), + escalus:assert(is_iq_result, [Iq], Response), + Markers = exml_query:paths(Response, [{element_with_ns, <<"query">>, ?NS_ESL_SMART_MARKERS}, + {element, <<"marker">>}]), + ?assertEqual([], Markers). + +get_id(Packet, Def) -> + exml_query:path( + Packet, [{element_with_ns, <<"stanza-id">>, ?NS_STANZAID}, {attr, <<"id">>}], Def). + +add_thread_id(Msg, undefined) -> + Msg; +add_thread_id(#xmlel{children = Children} = Msg, ThreadID) -> + ThreadEl = #xmlel{name = <<"thread">>, + children = [#xmlcdata{content = ThreadID}]}, + Msg#xmlel{children = [ThreadEl | Children]}. + unregister_user(Client) -> Jid = jid:from_binary(escalus_client:short_jid(Client)), rpc(mim(), ejabberd_auth, remove_user, [Jid]). diff --git a/include/mongoose_ns.hrl b/include/mongoose_ns.hrl index 3da573ef43..49e11a0eeb 100644 --- a/include/mongoose_ns.hrl +++ b/include/mongoose_ns.hrl @@ -115,4 +115,7 @@ -define(NS_ESL_INBOX, <<"erlang-solutions.com:xmpp:inbox:0">>). -define(NS_ESL_INBOX_CONVERSATION, <<"erlang-solutions.com:xmpp:inbox:0#conversation">>). +%% Erlang Solutions custom extension - smart_markers feature +-define(NS_ESL_SMART_MARKERS, <<"esl:xmpp:smart-markers:0">>). + -endif. diff --git a/src/smart_markers/mod_smart_markers.erl b/src/smart_markers/mod_smart_markers.erl index 06ad1cfb42..aff4aa7b2c 100644 --- a/src/smart_markers/mod_smart_markers.erl +++ b/src/smart_markers/mod_smart_markers.erl @@ -59,27 +59,26 @@ -include("mod_muc_light.hrl"). -include("mongoose_config_spec.hrl"). --xep([{xep, 333}, {version, "0.3"}]). +-xep([{xep, 333}, {version, "0.4"}]). -behaviour(gen_mod). %% gen_mod API -export([start/2, stop/1, supported_features/0, config_spec/0]). -%% gen_mod API +%% Internal API -export([get_chat_markers/3]). %% Hook handlers --export([user_send_packet/4, remove_user/3, remove_domain/3, - forget_room/4, room_new_affiliations/4]). --ignore_xref([user_send_packet/4, remove_user/3, remove_domain/3, - forget_room/4, room_new_affiliations/4]). +-export([process_iq/5, user_send_packet/4, filter_local_packet/1, + remove_user/3, remove_domain/3, forget_room/4, room_new_affiliations/4]). +-ignore_xref([process_iq/5, user_send_packet/4, filter_local_packet/1, + remove_user/3, remove_domain/3, forget_room/4, room_new_affiliations/4]). %%-------------------------------------------------------------------- %% Type declarations %%-------------------------------------------------------------------- -type maybe_thread() :: undefined | binary(). -type chat_type() :: one2one | groupchat. - -type chat_marker() :: #{from := jid:jid(), to := jid:jid(), thread := maybe_thread(), % it is not optional! @@ -89,16 +88,18 @@ -export_type([chat_marker/0]). -%%-------------------------------------------------------------------- %% gen_mod API -%%-------------------------------------------------------------------- -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> any(). -start(HostType, #{iqdisc := IQDisc} = Opts) -> +start(HostType, #{iqdisc := IQDisc, keep_private := Private} = Opts) -> mod_smart_markers_backend:init(HostType, Opts), + gen_iq_handler:add_iq_handler_for_domain( + HostType, ?NS_ESL_SMART_MARKERS, ejabberd_sm, + fun ?MODULE:process_iq/5, #{keep_private => Private}, IQDisc), ejabberd_hooks:add(hooks(HostType, Opts)). -spec stop(mongooseim:host_type()) -> ok. stop(HostType) -> + gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_ESL_SMART_MARKERS, ejabberd_sm), Opts = gen_mod:get_module_opts(HostType, ?MODULE), ejabberd_hooks:delete(hooks(HostType, Opts)). @@ -118,9 +119,66 @@ config_spec() -> format_items = map }. -%%-------------------------------------------------------------------- -%% Hook handlers -%%-------------------------------------------------------------------- +%% IQ handlers +-spec process_iq(mongoose_acc:t(), jid:jid(), jid:jid(), jlib:iq(), map()) -> + {mongoose_acc:t(), jlib:iq()}. +process_iq(Acc, _From, _To, #iq{type = set, sub_el = SubEl} = IQ, _Extra) -> + {Acc, IQ#iq{type = error, sub_el = [SubEl, mongoose_xmpp_errors:not_allowed()]}}; +process_iq(Acc, From, _To, #iq{type = get, sub_el = SubEl} = IQ, #{keep_private := Private}) -> + Req = maps:from_list(SubEl#xmlel.attrs), + MaybePeer = jid:from_binary(maps:get(<<"peer">>, Req, undefined)), + MaybeAfter = parse_ts(maps:get(<<"after">>, Req, undefined)), + MaybeThread = maps:get(<<"thread">>, Req, undefined), + Res = fetch_markers(IQ, Acc, From, MaybePeer, MaybeThread, MaybeAfter, Private), + {Acc, Res}. + +-spec parse_ts(undefined | binary()) -> integer() | error. +parse_ts(undefined) -> + 0; +parse_ts(BinTS) -> + try calendar:rfc3339_to_system_time(binary_to_list(BinTS)) + catch error:_Error -> error + end. + +-spec fetch_markers(jlib:iq(), + mongoose_acc:t(), + From :: jid:jid(), + MaybePeer :: error | jid:jid(), + maybe_thread(), + MaybeTS :: error | integer(), + MaybePrivate :: boolean()) -> jlib:iq(). +fetch_markers(IQ, _, _, error, _, _, _) -> + IQ#iq{type = error, + sub_el = [mongoose_xmpp_errors:bad_request(<<"en">>, <<"invalid-peer">>)]}; +fetch_markers(IQ, _, _, _, _, error, _) -> + IQ#iq{type = error, + sub_el = [mongoose_xmpp_errors:bad_request(<<"en">>, <<"invalid-timestamp">>)]}; +fetch_markers(IQ, Acc, From, Peer, Thread, TS, Private) -> + HostType = mongoose_acc:host_type(Acc), + Markers = mod_smart_markers_backend:get_conv_chat_marker(HostType, From, Peer, Thread, TS, Private), + SubEl = #xmlel{name = <<"query">>, + attrs = [{<<"xmlns">>, ?NS_ESL_SMART_MARKERS}, + {<<"peer">>, jid:to_binary(jid:to_lus(Peer))}], + children = build_result(Markers)}, + IQ#iq{type = result, sub_el = SubEl}. + +build_result(Markers) -> + [ #xmlel{name = <<"marker">>, + attrs = [{<<"id">>, MsgId}, + {<<"from">>, jid:to_binary(From)}, + {<<"type">>, atom_to_binary(Type)}, + {<<"timestamp">>, ts_to_bin(MsgTS)} + | maybe_thread(MsgThread) ]} + || #{from := From, thread := MsgThread, type := Type, timestamp := MsgTS, id := MsgId} <- Markers ]. + +ts_to_bin(TS) -> + list_to_binary(calendar:system_time_to_rfc3339(TS, [{offset, "Z"}, {unit, microsecond}])). + +maybe_thread(undefined) -> + []; +maybe_thread(Bin) -> + [{<<"thread">>, Bin}]. + %% HOOKS -spec hooks(mongooseim:host_type(), gen_mod:module_opts()) -> [ejabberd_hooks:hook()]. hooks(HostType, #{keep_private := KeepPrivate}) -> @@ -144,11 +202,22 @@ user_send_packet(Acc, From, To, Packet = #xmlel{name = <<"message">>}) -> case has_valid_markers(Acc, From, To, Packet) of {true, HostType, Markers} -> update_chat_markers(Acc, HostType, Markers); - false -> Acc + _ -> + Acc end; user_send_packet(Acc, _From, _To, _Packet) -> Acc. +-spec filter_local_packet(mongoose_hooks:filter_packet_acc() | drop) -> + mongoose_hooks:filter_packet_acc() | drop. +filter_local_packet(Filter = {_From, _To, _Acc, Msg = #xmlel{name = <<"message">>}}) -> + case mongoose_chat_markers:has_chat_markers(Msg) of + false -> Filter; + true -> drop + end; +filter_local_packet(Filter) -> + Filter. + remove_user(Acc, User, Server) -> HostType = mongoose_acc:host_type(Acc), mod_smart_markers_backend:remove_user(HostType, jid:make_bare(User, Server)), diff --git a/src/smart_markers/mod_smart_markers_backend.erl b/src/smart_markers/mod_smart_markers_backend.erl index 40730b1224..ceeaa2f638 100644 --- a/src/smart_markers/mod_smart_markers_backend.erl +++ b/src/smart_markers/mod_smart_markers_backend.erl @@ -4,6 +4,7 @@ -export([init/2]). -export([update_chat_marker/2]). +-export([get_conv_chat_marker/6]). -export([get_chat_markers/4]). -export([remove_domain/2]). -export([remove_user/2]). @@ -23,6 +24,14 @@ %%% This function must return the latest chat markers sent to the %%% user/room (with or w/o thread) later than provided timestamp. +-callback get_conv_chat_marker(HostType :: mongooseim:host_type(), + From :: jid:jid(), + To :: jid:jid(), + Thread :: mod_smart_markers:maybe_thread(), + Timestamp :: integer(), + Private :: boolean()) -> + [mod_smart_markers:chat_marker()]. + -callback get_chat_markers(HostType :: mongooseim:host_type(), To :: jid:jid(), Thread :: mod_smart_markers:maybe_thread(), @@ -39,7 +48,7 @@ -spec init(mongooseim:host_type(), gen_mod:module_opts()) -> ok. init(HostType, Opts) -> - TrackedFuns = [get_chat_markers, update_chat_marker], + TrackedFuns = [get_chat_markers, get_conv_chat_marker, update_chat_marker], mongoose_backend:init(HostType, ?MAIN_MODULE, TrackedFuns, Opts), Args = [HostType, Opts], mongoose_backend:call(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, Args). @@ -50,6 +59,16 @@ update_chat_marker(HostType, ChatMarker) -> Args = [HostType, ChatMarker], mongoose_backend:call_tracked(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, Args). +-spec get_conv_chat_marker(HostType :: mongooseim:host_type(), + From :: jid:jid(), + To :: jid:jid(), + Thread :: mod_smart_markers:maybe_thread(), + Timestamp :: integer(), + Private :: boolean()) -> [mod_smart_markers:chat_marker()]. +get_conv_chat_marker(HostType, From, To, Thread, TS, Private) -> + Args = [HostType, From, To, Thread, TS, Private], + mongoose_backend:call_tracked(HostType, ?MAIN_MODULE, ?FUNCTION_NAME, Args). + -spec get_chat_markers(HostType :: mongooseim:host_type(), To :: jid:jid(), Thread :: mod_smart_markers:maybe_thread(), diff --git a/src/smart_markers/mod_smart_markers_rdbms.erl b/src/smart_markers/mod_smart_markers_rdbms.erl index 1c40925515..8a7f32eaa4 100644 --- a/src/smart_markers/mod_smart_markers_rdbms.erl +++ b/src/smart_markers/mod_smart_markers_rdbms.erl @@ -1,8 +1,8 @@ %%%---------------------------------------------------------------------------- -%%% @copyright (C) 2020, Erlang Solutions Ltd. %%% @doc %%% RDBMS backend for mod_smart_markers %%% @end +%%% @copyright (C) 2020-2022, Erlang Solutions Ltd. %%%---------------------------------------------------------------------------- -module(mod_smart_markers_rdbms). -author("denysgonchar"). @@ -11,6 +11,7 @@ -include("jlib.hrl"). -export([init/2, update_chat_marker/2, get_chat_markers/4]). +-export([get_conv_chat_marker/6]). -export([remove_domain/2, remove_user/2, remove_to/2, remove_to_for_user/3]). %%-------------------------------------------------------------------- @@ -23,6 +24,10 @@ init(HostType, _) -> InsertFields = KeyFields ++ UpdateFields, rdbms_queries:prepare_upsert(HostType, smart_markers_upsert, smart_markers, InsertFields, UpdateFields, KeyFields), + mongoose_rdbms:prepare(smart_markers_select_conv, smart_markers, + [lserver, luser, to_jid, thread, timestamp], + <<"SELECT lserver, luser, to_jid, thread, type, msg_id, timestamp FROM smart_markers " + "WHERE lserver = ? AND luser = ? AND to_jid = ? AND thread = ? AND timestamp >= ?">>), mongoose_rdbms:prepare(smart_markers_select, smart_markers, [to_jid, thread, timestamp], <<"SELECT lserver, luser, type, msg_id, timestamp FROM smart_markers " @@ -59,6 +64,46 @@ update_chat_marker(HostType, #{from := #jid{luser = LU, lserver = LS}, InsertValues, UpdateValues, KeyValues), ok = check_upsert_result(Res). +-spec get_conv_chat_marker(HostType :: mongooseim:host_type(), + From :: jid:jid(), + To :: jid:jid(), + Thread :: mod_smart_markers:maybe_thread(), + Timestamp :: integer(), + Private :: boolean()) -> [mod_smart_markers:chat_marker()]. +get_conv_chat_marker(HostType, From, To = #jid{lserver = ToLServer}, Thread, TS, Private) -> + % If To is a room, we'll want to check just the room + case mongoose_domain_api:get_subdomain_host_type(ToLServer) of + {error, not_found} -> + one2one_get_conv_chat_marker(HostType, From, To, Thread, TS, Private); + {ok, _} -> + groupchat_get_conv_chat_marker(HostType, From, To, Thread, TS, Private) + end. + +one2one_get_conv_chat_marker(HostType, + From = #jid{luser = FromLUser, lserver = FromLServer}, + To = #jid{luser = ToLUser, lserver = ToLServer}, + Thread, TS, Private) -> + {selected, ChatMarkersFrom} = mongoose_rdbms:execute_successfully( + HostType, smart_markers_select_conv, + [FromLServer, FromLUser, encode_jid(To), encode_thread(Thread), TS]), + ChatMarkers = case Private of + true -> ChatMarkersFrom; + false -> + {selected, ChatMarkersTo} = mongoose_rdbms:execute_successfully( + HostType, smart_markers_select_conv, + [ToLServer, ToLUser, encode_jid(From), encode_thread(Thread), TS]), + ChatMarkersFrom ++ ChatMarkersTo + end, + [ decode_chat_marker(Tuple) || Tuple <- ChatMarkers]. + +groupchat_get_conv_chat_marker(HostType, _From, To, Thread, TS, false) -> + get_chat_markers(HostType, To, Thread, TS); +groupchat_get_conv_chat_marker(HostType, #jid{luser = FromLUser, lserver = FromLServer}, To, Thread, TS, true) -> + {selected, ChatMarkers} = mongoose_rdbms:execute_successfully( + HostType, smart_markers_select_conv, + [FromLServer, FromLUser, encode_jid(To), encode_thread(Thread), TS]), + [ decode_chat_marker(Tuple) || Tuple <- ChatMarkers]. + %%% @doc %%% This function must return the latest chat markers sent to the %%% user/room (with or w/o thread) later than provided timestamp. @@ -116,6 +161,19 @@ check_upsert_result({updated, 2}) -> ok; check_upsert_result(Result) -> {error, {bad_result, Result}}. +decode_chat_marker({LS, LU, ToJid, MsgThread, Type, MsgId, MsgTS}) -> + #{from => jid:make_noprep(LU, LS, <<>>), + to => decode_jid(ToJid), + thread => decode_thread(MsgThread), + type => decode_type(Type), + timestamp => decode_timestamp(MsgTS), + id => MsgId}. + +decode_jid(EncodedJID) -> jid:from_binary(EncodedJID). + +decode_thread(<<>>) -> undefined; +decode_thread(Thread) -> Thread. + decode_type(<<"R">>) -> received; decode_type(<<"D">>) -> displayed; decode_type(<<"A">>) -> acknowledged.