./ct_report/coverage/mod_smart_markers.COVER.html

1 %%%----------------------------------------------------------------------------
2 %%% @copyright (C) 2020, Erlang Solutions Ltd.
3 %%% @doc
4 %%% This module implements storage of the latest chat markers
5 %%% sent by the users. This can be used to optimize mod_offline
6 %%% functionality, or to implement custom fetching protocol and
7 %%% avoid storage of chat markers in MAM.
8 %%%
9 %%% Please be aware of the next implementation details:
10 %%%
11 %%% 1) Current implementation is based on user_send_packet hook.
12 %%% It doesn't work for s2s connections, but usage of another
13 %%% hook (e.g. filter_local_packet) makes implementation harder
14 %%% and results in multiple processing of one and the same
15 %%% chat marker notification (sent to different users by MUC).
16 %%% However that is the only possible way to deal with group
17 %%% chat messages sent from the room to the user over s2s.
18 %%%
19 %%% ```
20 %%% S2S
21 %%% +
22 %%% |
23 %%% +--------------------+ |
24 %%% | | | filter
25 %%% | +--------------->
26 %%% send | | | filter
27 %%% +------->+ ROOM +--------------->
28 %%% | | | filter
29 %%% | +--------------->
30 %%% | | |
31 %%% +--------------------+ |
32 %%% |
33 %%% +
34 %%% '''
35 %%%
36 %%% 2) DB backend requires us to provide host information, and
37 %%% the host is always the recipient's server in case one2one
38 %%% messages, and a master domain of the MUC service in case
39 %%% of groupchat.
40 %%%
41 %%% 3) It is the client application's responsibility to ensure that
42 %%% chat markers move only forward. There is no verification of
43 %%% chat markers in this module, it just stores the latest chat
44 %%% marker information sent by the user.
45 %%%
46 %%% 4) MUC light doesn't have message serialization! So it doesn't
47 %%% guarantee one and the same message order for different users.
48 %%% This can result in a race condition situation when different
49 %%% users track (and mark) different messages as the last in a
50 %%% chat history. However, this is a rare situation, and it self
51 %%% recovers on the next message in the room. Anyway storing chat
52 %%% markers in MAM doesn't fix this problem.
53 %%%
54 %%% @end
55 %%%----------------------------------------------------------------------------
56 -module(mod_smart_markers).
57
58 -include("jlib.hrl").
59
60 -xep([{xep, 333}, {version, "0.3"}]).
61 -behaviour(gen_mod).
62
63 %% gen_mod API
64 -export([start/2]).
65 -export([stop/1]).
66 -export([supported_features/0]).
67
68 %% gen_mod API
69 -export([get_chat_markers/3]).
70
71 %% Hook handlers
72 -export([user_send_packet/4]).
73
74 -ignore_xref([
75 behaviour_info/1, user_send_packet/4
76 ]).
77
78 %%--------------------------------------------------------------------
79 %% Type declarations
80 %%--------------------------------------------------------------------
81 -type maybe_thread() :: undefined | binary().
82 -type chat_type() :: one2one | groupchat.
83
84 -type chat_marker() :: #{from := jid:jid(),
85 to := jid:jid(),
86 thread := maybe_thread(), % it is not optional!
87 type := mongoose_chat_markers:chat_marker_type(),
88 timestamp := integer(), % microsecond
89 id := binary()}.
90
91 -export_type([chat_marker/0]).
92
93 %%--------------------------------------------------------------------
94 %% gen_mod API
95 %%--------------------------------------------------------------------
96 -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> any().
97 start(HostType, Opts) ->
98 1 mod_smart_markers_backend:init(HostType, Opts),
99 1 ejabberd_hooks:add(hooks(HostType)).
100
101 -spec stop(mongooseim:host_type()) -> ok.
102 stop(HostType) ->
103 1 ejabberd_hooks:delete(hooks(HostType)).
104
105 -spec supported_features() -> [atom()].
106 supported_features() ->
107 1 [dynamic_domains].
108
109 %%--------------------------------------------------------------------
110 %% Hook handlers
111 %%--------------------------------------------------------------------
112 -spec hooks(mongooseim:host_type()) -> [ejabberd_hooks:hook()].
113 hooks(HostType) ->
114 2 [{user_send_packet, HostType, ?MODULE, user_send_packet, 90}].
115
116 -spec user_send_packet(mongoose_acc:t(), jid:jid(), jid:jid(), exml:element()) ->
117 mongoose_acc:t().
118 user_send_packet(Acc, From, To, Packet = #xmlel{name = <<"message">>}) ->
119 12 case has_valid_markers(Acc, From, To, Packet) of
120 {true, HostType, Markers} ->
121 10 update_chat_markers(Acc, HostType, Markers);
122 2 false -> Acc
123 end;
124 user_send_packet(Acc, _From, _To, _Packet) ->
125 7 Acc.
126
127 %%--------------------------------------------------------------------
128 %% Other API
129 %%--------------------------------------------------------------------
130 -spec get_chat_markers(jid:jid(), maybe_thread(), integer()) ->
131 [chat_marker()].
132 get_chat_markers(#jid{lserver = LServer} = To, Thread, TS) ->
133 %% internal API, no room access rights verification here!
134 4 {ok, HostType} = mongoose_domain_api:get_host_type(LServer),
135 4 mod_smart_markers_backend:get_chat_markers(HostType, To, Thread, TS).
136
137 %%--------------------------------------------------------------------
138 %% Local functions
139 %%--------------------------------------------------------------------
140 -spec update_chat_markers(mongoose_acc:t(), mongooseim:host_type(), [chat_marker()]) ->
141 mongoose_acc:t().
142 update_chat_markers(Acc, HostType, Markers) ->
143 10 TS = mongoose_acc:timestamp(Acc),
144 10 [mod_smart_markers_backend:update_chat_marker(HostType, CM) || CM <- Markers],
145 10 mongoose_acc:set_permanent(?MODULE, timestamp, TS, Acc).
146
147 -spec has_valid_markers(mongoose_acc:t(), jid:jid(), jid:jid(), exml:element()) ->
148 false | {true, mongooseim:host_type(), Markers :: [chat_marker()]}.
149 has_valid_markers(Acc, From, To, Packet) ->
150 12 case extract_chat_markers(Acc, From, To, Packet) of
151 2 [] -> false;
152 Markers ->
153 10 case is_valid_host(Acc, From, To) of
154
:-(
false -> false;
155 10 {true, HostType} -> {true, HostType, Markers}
156 end
157 end.
158
159 -spec is_valid_host(mongoose_acc:t(), jid:jid(), jid:jid()) ->
160 false | {true, mongooseim:host_type()}.
161 is_valid_host(Acc, From, To) ->
162 10 case mongoose_acc:stanza_type(Acc) of
163 5 <<"groupchat">> -> get_host(Acc, From, To, groupchat);
164 5 _ -> get_host(Acc, From, To, one2one)
165 end.
166
167 -spec extract_chat_markers(mongoose_acc:t(), jid:jid(), jid:jid(), exml:element()) ->
168 [chat_marker()].
169 extract_chat_markers(Acc, From, To, Packet) ->
170 12 TS = mongoose_acc:timestamp(Acc),
171 12 case mongoose_chat_markers:list_chat_markers(Packet) of
172 2 [] -> [];
173 ChatMarkers ->
174 10 CM = #{from => From, to => To, thread => get_thread(Packet), timestamp => TS},
175 10 [CM#{type => Type, id => Id} || {Type, Id} <- ChatMarkers]
176 end.
177
178 -spec get_thread(exml:element()) -> maybe_thread().
179 get_thread(El) ->
180 10 case exml_query:path(El, [{element, <<"thread">>}, cdata]) of
181 10 Thread when Thread =/= <<>> -> Thread;
182
:-(
_ -> undefined
183 end.
184
185 -spec get_host(mongoose_acc:t(), jid:jid(), jid:jid(), chat_type()) ->
186 false | {true, mongooseim:host_type()}.
187 get_host(Acc, From, To, groupchat) ->
188 5 HostType = mod_muc_light_utils:room_jid_to_host_type(To),
189 5 can_access_room(HostType, Acc, From, To) andalso {true, HostType};
190 get_host(_Acc, _From, To, one2one) ->
191 5 LServer = To#jid.lserver,
192 5 case mongoose_domain_api:get_domain_host_type(LServer) of
193 5 {ok, HostType} -> {true, HostType};
194
:-(
{error, not_found} -> false
195 end.
196
197 -spec can_access_room(HostType :: mongooseim:host_type(),
198 Acc :: mongoose_acc:t(),
199 User :: jid:jid(),
200 Room :: jid:jid()) -> boolean().
201 can_access_room(HostType, Acc, User, Room) ->
202 5 mongoose_hooks:can_access_room(HostType, Acc, Room, User).
Line Hits Source