1 |
|
%%%------------------------------------------------------------------- |
2 |
|
%%% @author Uvarov Michael <arcusfelis@gmail.com> |
3 |
|
%%% @copyright (C) 2013, Uvarov Michael |
4 |
|
%%% @doc XEP-0313: Message Archive Management |
5 |
|
%%% |
6 |
|
%%% The module uses several backend modules: |
7 |
|
%%% |
8 |
|
%%% <ul> |
9 |
|
%%% <li>Preference manager ({@link mod_mam_muc_rdbms_prefs});</li> |
10 |
|
%%% <li>Writer ({@link mod_mam_muc_rdbms_arch} or {@link mod_mam_muc_rdbms_async_pool_writer});</li> |
11 |
|
%%% <li>Archive manager ({@link mod_mam_muc_rdbms_arch});</li> |
12 |
|
%%% <li>User's ID generator ({@link mod_mam_muc_user}).</li> |
13 |
|
%%% </ul> |
14 |
|
%%% |
15 |
|
%%% Preferences can be also stored in Mnesia ({@link mod_mam_mnesia_prefs}). |
16 |
|
%%% This module handles MUC archives. |
17 |
|
%%% |
18 |
|
%%% This module should be started for each host. |
19 |
|
%%% Message archivation is not shaped here (use standard support for this). |
20 |
|
%%% MAM's IQs are shaped inside {@link opuntia_srv}. |
21 |
|
%%% |
22 |
|
%%% Message identifiers (or UIDs in the spec) are generated based on: |
23 |
|
%%% |
24 |
|
%%% <ul> |
25 |
|
%%% <li>date (using `timestamp()');</li> |
26 |
|
%%% <li>node number (using {@link mongoose_node_num}).</li> |
27 |
|
%%% </ul> |
28 |
|
%%% @end |
29 |
|
%%%------------------------------------------------------------------- |
30 |
|
-module(mod_mam_muc). |
31 |
|
%% ---------------------------------------------------------------------- |
32 |
|
%% Exports |
33 |
|
|
34 |
|
%% Client API |
35 |
|
-export([delete_archive/2, |
36 |
|
archive_size/2, |
37 |
|
archive_id/2]). |
38 |
|
|
39 |
|
%% gen_mod handlers |
40 |
|
-export([start/2, stop/1, supported_features/0]). |
41 |
|
|
42 |
|
%% ejabberd room handlers |
43 |
|
-export([disco_muc_features/3, |
44 |
|
filter_room_packet/3, |
45 |
|
forget_room/3]). |
46 |
|
|
47 |
|
-export([room_process_mam_iq/5]). |
48 |
|
|
49 |
|
%% gdpr callback |
50 |
|
-export([get_personal_data/3]). |
51 |
|
|
52 |
|
%% private |
53 |
|
-export([archive_message_for_ct/1]). |
54 |
|
-export([lookup_messages/2]). |
55 |
|
-export([archive_id_int/2]). |
56 |
|
|
57 |
|
-ignore_xref([archive_id/2, archive_message_for_ct/1, archive_size/2, delete_archive/2, |
58 |
|
start/2, stop/1, supported_features/0]). |
59 |
|
|
60 |
|
-include_lib("mongoose.hrl"). |
61 |
|
-include_lib("jlib.hrl"). |
62 |
|
-include_lib("exml/include/exml.hrl"). |
63 |
|
|
64 |
|
-callback is_complete_message(Module :: atom(), Dir :: atom(), Packet :: any()) -> |
65 |
|
boolean(). |
66 |
|
|
67 |
|
%% ---------------------------------------------------------------------- |
68 |
|
%% Other types |
69 |
|
-type packet() :: any(). |
70 |
|
-type row_batch() :: {TotalCount :: non_neg_integer(), |
71 |
|
Offset :: non_neg_integer(), |
72 |
|
MessageRows :: [row()]}. |
73 |
|
-type row() :: mod_mam:message_row(). |
74 |
|
-type host_type() :: mongooseim:host_type(). |
75 |
|
-type muc_action() :: atom(). |
76 |
|
|
77 |
|
-export_type([row/0, row_batch/0]). |
78 |
|
|
79 |
|
%% ---------------------------------------------------------------------- |
80 |
|
%% API |
81 |
|
|
82 |
|
-spec get_personal_data(Acc, Params, Extra) -> {ok, Acc} when |
83 |
|
Acc :: gdpr:personal_data(), |
84 |
|
Params :: #{jid := jid:jid()}, |
85 |
|
Extra :: gen_hook:extra(). |
86 |
|
get_personal_data(Acc, #{jid := ArcJID}, #{host_type := HostType}) -> |
87 |
51 |
Schema = ["id", "message"], |
88 |
51 |
Entries = mongoose_hooks:get_mam_muc_gdpr_data(HostType, ArcJID), |
89 |
51 |
{ok, [{mam_muc, Schema, Entries} | Acc]}. |
90 |
|
|
91 |
|
-spec delete_archive(jid:server(), jid:user()) -> ok. |
92 |
|
delete_archive(MucHost, RoomName) when is_binary(MucHost), is_binary(RoomName) -> |
93 |
138 |
?LOG_DEBUG(#{what => mam_delete_room, room => RoomName, sub_host => MucHost}), |
94 |
138 |
ArcJID = jid:make_bare(RoomName, MucHost), |
95 |
138 |
HostType = mod_muc_light_utils:room_jid_to_host_type(ArcJID), |
96 |
138 |
ArcID = archive_id_int(HostType, ArcJID), |
97 |
138 |
remove_archive(HostType, ArcID, ArcJID), |
98 |
138 |
ok. |
99 |
|
|
100 |
|
-spec archive_size(jid:server(), jid:user()) -> integer(). |
101 |
|
archive_size(MucHost, RoomName) when is_binary(MucHost), is_binary(RoomName) -> |
102 |
257 |
ArcJID = jid:make_bare(RoomName, MucHost), |
103 |
257 |
HostType = mod_muc_light_utils:room_jid_to_host_type(ArcJID), |
104 |
257 |
ArcID = archive_id_int(HostType, ArcJID), |
105 |
257 |
archive_size(HostType, ArcID, ArcJID). |
106 |
|
|
107 |
|
-spec archive_id(jid:server(), jid:user()) -> integer(). |
108 |
|
archive_id(MucHost, RoomName) when is_binary(MucHost), is_binary(RoomName) -> |
109 |
16 |
ArcJID = jid:make_bare(RoomName, MucHost), |
110 |
16 |
HostType = mod_muc_light_utils:room_jid_to_host_type(ArcJID), |
111 |
16 |
archive_id_int(HostType, ArcJID). |
112 |
|
|
113 |
|
%% ---------------------------------------------------------------------- |
114 |
|
%% gen_mod callbacks |
115 |
|
%% Starting and stopping functions for MUC archives |
116 |
|
|
117 |
|
-spec start(host_type(), gen_mod:module_opts()) -> any(). |
118 |
|
start(HostType, Opts) -> |
119 |
24 |
?LOG_DEBUG(#{what => mam_muc_starting}), |
120 |
24 |
ensure_metrics(HostType), |
121 |
24 |
gen_hook:add_handlers(hooks(HostType)), |
122 |
24 |
add_iq_handlers(HostType, Opts), |
123 |
24 |
ok. |
124 |
|
|
125 |
|
-spec stop(host_type()) -> any(). |
126 |
|
stop(HostType) -> |
127 |
24 |
?LOG_DEBUG(#{what => mam_muc_stopping}), |
128 |
24 |
gen_hook:delete_handlers(hooks(HostType)), |
129 |
24 |
remove_iq_handlers(HostType), |
130 |
24 |
ok. |
131 |
|
|
132 |
|
-spec supported_features() -> [atom()]. |
133 |
|
supported_features() -> |
134 |
:-( |
[dynamic_domains]. |
135 |
|
|
136 |
|
%% ---------------------------------------------------------------------- |
137 |
|
%% hooks and handlers for MUC |
138 |
|
|
139 |
|
-spec disco_muc_features(Acc, Params, Extra) -> {ok, Acc} when |
140 |
|
Acc :: mongoose_disco:feature_acc(), |
141 |
|
Params :: map(), |
142 |
|
Extra :: gen_hook:extra(). |
143 |
|
disco_muc_features(Acc = #{host_type := HostType, node := <<>>}, _Params, _Extra) -> |
144 |
4 |
{ok, mongoose_disco:add_features(mod_mam_utils:features(?MODULE, HostType), Acc)}; |
145 |
|
disco_muc_features(Acc, _Params, _Extra) -> |
146 |
:-( |
{ok, Acc}. |
147 |
|
|
148 |
|
%% @doc Handle public MUC-message. |
149 |
|
-spec filter_room_packet(Packet, EventData, Extra) -> {ok, Packet} when |
150 |
|
Packet :: exml:element(), |
151 |
|
EventData :: mod_muc:room_event_data(), |
152 |
|
Extra :: gen_hook:extra(). |
153 |
|
filter_room_packet(Packet, EventData, #{host_type := HostType}) -> |
154 |
146 |
?LOG_DEBUG(#{what => mam_room_packet, text => <<"Incoming room packet">>, |
155 |
146 |
packet => Packet, event_data => EventData}), |
156 |
146 |
IsArchivable = is_archivable_message(HostType, incoming, Packet), |
157 |
146 |
case IsArchivable of |
158 |
|
true -> |
159 |
140 |
#{from_nick := FromNick, from_jid := FromJID, room_jid := RoomJID, |
160 |
|
role := Role, affiliation := Affiliation, timestamp := TS} = EventData, |
161 |
140 |
{ok, archive_room_packet(HostType, Packet, FromNick, FromJID, |
162 |
|
RoomJID, Role, Affiliation, TS)}; |
163 |
|
false -> |
164 |
6 |
{ok, Packet} |
165 |
|
end. |
166 |
|
|
167 |
|
%% @doc Archive without validation. |
168 |
|
-spec archive_room_packet(HostType :: host_type(), |
169 |
|
Packet :: packet(), FromNick :: jid:user(), |
170 |
|
FromJID :: jid:jid(), RoomJID :: jid:jid(), |
171 |
|
Role :: mod_muc:role(), Affiliation :: mod_muc:affiliation(), |
172 |
|
TS :: integer()) -> packet(). |
173 |
|
archive_room_packet(HostType, Packet, FromNick, FromJID = #jid{}, |
174 |
|
RoomJID = #jid{}, Role, Affiliation, TS) -> |
175 |
140 |
ArcID = archive_id_int(HostType, RoomJID), |
176 |
|
%% Occupant JID <room@service/nick> |
177 |
140 |
SrcJID = jid:replace_resource(RoomJID, FromNick), |
178 |
140 |
IsMamMucEnabled = mod_mam_utils:is_mam_muc_enabled(RoomJID#jid.lserver, HostType), |
179 |
140 |
IsInteresting = |
180 |
|
case get_behaviour(HostType, ArcID, RoomJID, SrcJID) of |
181 |
140 |
always -> true; |
182 |
:-( |
never -> false; |
183 |
:-( |
roster -> true |
184 |
|
end, |
185 |
140 |
case IsInteresting andalso IsMamMucEnabled of |
186 |
|
true -> |
187 |
140 |
MessID = mod_mam_utils:generate_message_id(TS), |
188 |
140 |
Packet1 = mod_mam_utils:replace_x_user_element(FromJID, Role, Affiliation, Packet), |
189 |
140 |
OriginID = mod_mam_utils:get_origin_id(Packet), |
190 |
140 |
Params = #{message_id => MessID, |
191 |
|
archive_id => ArcID, |
192 |
|
local_jid => RoomJID, |
193 |
|
remote_jid => FromJID, |
194 |
|
source_jid => SrcJID, |
195 |
|
origin_id => OriginID, |
196 |
|
direction => incoming, |
197 |
|
packet => Packet1}, |
198 |
|
%% Packet to be broadcasted and packet to be archived are |
199 |
|
%% not 100% the same |
200 |
140 |
Result = archive_message(HostType, Params), |
201 |
140 |
case Result of |
202 |
|
ok -> |
203 |
140 |
ExtID = mod_mam_utils:mess_id_to_external_binary(MessID), |
204 |
140 |
ShouldAdd = mod_mam_params:add_stanzaid_element(?MODULE, HostType), |
205 |
140 |
mod_mam_utils:maybe_add_arcid_elems(RoomJID, ExtID, Packet, ShouldAdd); |
206 |
:-( |
{error, _} -> Packet |
207 |
|
end; |
208 |
:-( |
false -> Packet |
209 |
|
end. |
210 |
|
|
211 |
|
%% @doc `To' is an account or server entity hosting the archive. |
212 |
|
%% Servers that archive messages on behalf of local users SHOULD expose archives |
213 |
|
%% to the user on their bare JID (i.e. `From.luser'), |
214 |
|
%% while a MUC service might allow MAM queries to be sent to the room's bare JID |
215 |
|
%% (i.e `To.luser'). |
216 |
|
-spec room_process_mam_iq(Acc :: mongoose_acc:t(), |
217 |
|
From :: jid:jid(), |
218 |
|
To :: jid:jid(), |
219 |
|
IQ :: jlib:iq(), |
220 |
|
Extra :: gen_hook:extra()) -> {mongoose_acc:t(), jlib:iq() | ignore}. |
221 |
|
room_process_mam_iq(Acc, From, To, IQ, #{host_type := HostType}) -> |
222 |
120 |
mod_mam_utils:maybe_log_deprecation(IQ), |
223 |
120 |
Action = mam_iq:action(IQ), |
224 |
120 |
MucAction = action_to_muc_action(Action), |
225 |
120 |
case check_action_allowed(HostType, Acc, To#jid.lserver, Action, MucAction, From, To) of |
226 |
|
ok -> |
227 |
116 |
case mod_mam_utils:wait_shaper(HostType, To#jid.lserver, MucAction, From) of |
228 |
|
continue -> |
229 |
116 |
handle_error_iq(Acc, HostType, To, Action, |
230 |
|
handle_mam_iq(HostType, Action, From, To, IQ)); |
231 |
|
{error, max_delay_reached} -> |
232 |
:-( |
mongoose_metrics:update(HostType, modMucMamDroppedIQ, 1), |
233 |
:-( |
{Acc, return_max_delay_reached_error_iq(IQ)} |
234 |
|
end; |
235 |
|
{error, Reason} -> |
236 |
4 |
?LOG_WARNING(#{what => action_not_allowed, |
237 |
|
action => Action, acc => Acc, reason => Reason, |
238 |
:-( |
can_access_room => can_access_room(HostType, Acc, From, To)}), |
239 |
4 |
{Acc, return_action_not_allowed_error_iq(Reason, IQ)} |
240 |
|
end. |
241 |
|
|
242 |
|
-spec forget_room(Acc, Params, Extra) -> {ok, Acc} when |
243 |
|
Acc :: term(), |
244 |
|
Params :: #{muc_host := jid:server(), room := jid:luser()}, |
245 |
|
Extra :: gen_hook:extra(). |
246 |
|
forget_room(Acc, #{muc_host := MucServer, room := RoomName}, _Extra) -> |
247 |
73 |
delete_archive(MucServer, RoomName), |
248 |
73 |
{ok, Acc}. |
249 |
|
|
250 |
|
%% ---------------------------------------------------------------------- |
251 |
|
%% Internal functions |
252 |
|
|
253 |
|
-spec check_action_allowed(host_type(), mongoose_acc:t(), jid:lserver(), mam_iq:action(), muc_action(), |
254 |
|
jid:jid(), jid:jid()) -> ok | {error, binary()}. |
255 |
|
check_action_allowed(HostType, Acc, Domain, Action, MucAction, From, To) -> |
256 |
120 |
case acl:match_rule(HostType, Domain, MucAction, From, default) of |
257 |
:-( |
allow -> ok; |
258 |
:-( |
deny -> {false, <<"Blocked by service policy.">>}; |
259 |
120 |
default -> check_room_action_allowed_by_default(HostType, Acc, Action, From, To) |
260 |
|
end. |
261 |
|
|
262 |
|
-spec action_to_muc_action(mam_iq:action()) -> atom(). |
263 |
|
action_to_muc_action(Action) -> |
264 |
120 |
list_to_atom("muc_" ++ atom_to_list(Action)). |
265 |
|
|
266 |
|
-spec check_room_action_allowed_by_default(HostType :: host_type(), |
267 |
|
Acc :: mongoose_acc:t(), |
268 |
|
Action :: mam_iq:action(), |
269 |
|
From :: jid:jid(), |
270 |
|
To :: jid:jid()) -> ok | {error, binary()}. |
271 |
|
check_room_action_allowed_by_default(HostType, Acc, Action, From, To) -> |
272 |
120 |
case mam_iq:action_type(Action) of |
273 |
|
set -> |
274 |
:-( |
case is_room_owner(HostType, Acc, From, To) of |
275 |
:-( |
true -> ok; |
276 |
:-( |
false -> {error, <<"Not a room owner.">>} |
277 |
|
end; |
278 |
|
get -> |
279 |
120 |
case can_access_room(HostType, Acc, From, To) of |
280 |
116 |
true -> ok; |
281 |
4 |
false -> {error, <<"Not allowed to enter the room.">>} |
282 |
|
end |
283 |
|
end. |
284 |
|
|
285 |
|
-spec is_room_owner(HostType :: host_type(), |
286 |
|
Acc :: mongoose_acc:t(), |
287 |
|
UserJid :: jid:jid(), |
288 |
|
RoomJid :: jid:jid()) -> boolean(). |
289 |
|
is_room_owner(HostType, Acc, UserJid, RoomJid) -> |
290 |
:-( |
mongoose_hooks:is_muc_room_owner(HostType, Acc, UserJid, RoomJid). |
291 |
|
|
292 |
|
%% @doc Return true if user element should be removed from results |
293 |
|
-spec is_user_identity_hidden(HostType :: host_type(), |
294 |
|
UserJid :: jid:jid(), |
295 |
|
RoomJid :: jid:jid()) -> boolean(). |
296 |
|
is_user_identity_hidden(HostType, UserJid, RoomJid) -> |
297 |
82 |
case mongoose_hooks:can_access_identity(HostType, RoomJid, UserJid) of |
298 |
82 |
CanAccess when is_boolean(CanAccess) -> not CanAccess |
299 |
|
end. |
300 |
|
|
301 |
|
-spec can_access_room(HostType :: host_type(), |
302 |
|
Acc :: mongoose_acc:t(), |
303 |
|
UserJid :: jid:jid(), |
304 |
|
RoomJid :: jid:jid()) -> boolean(). |
305 |
|
can_access_room(HostType, Acc, UserJid, RoomJid) -> |
306 |
124 |
mongoose_hooks:can_access_room(HostType, Acc, RoomJid, UserJid). |
307 |
|
|
308 |
|
-spec handle_mam_iq(HostType :: host_type(), mam_iq:action(), |
309 |
|
From :: jid:jid(), jid:jid(), jlib:iq()) -> |
310 |
|
jlib:iq() | {error, any(), jlib:iq()} | ignore. |
311 |
|
handle_mam_iq(HostType, Action, From, To, IQ) -> |
312 |
116 |
case Action of |
313 |
|
mam_get_prefs -> |
314 |
:-( |
handle_get_prefs(HostType, To, IQ); |
315 |
|
mam_set_prefs -> |
316 |
:-( |
handle_set_prefs(HostType, To, IQ); |
317 |
|
mam_set_message_form -> |
318 |
110 |
handle_set_message_form(HostType, From, To, IQ); |
319 |
|
mam_get_message_form -> |
320 |
:-( |
handle_get_message_form(HostType, From, To, IQ); |
321 |
|
mam_get_metadata -> |
322 |
6 |
handle_get_metadata(HostType, From, To, IQ) |
323 |
|
end. |
324 |
|
|
325 |
|
-spec handle_set_prefs(host_type(), jid:jid(), jlib:iq()) -> |
326 |
|
jlib:iq() | {error, any(), jlib:iq()}. |
327 |
|
handle_set_prefs(HostType, ArcJID = #jid{}, |
328 |
|
IQ = #iq{sub_el = PrefsEl}) -> |
329 |
:-( |
{DefaultMode, AlwaysJIDs, NeverJIDs} = mod_mam_utils:parse_prefs(PrefsEl), |
330 |
:-( |
?LOG_DEBUG(#{what => mam_muc_set_prefs, archive_jid => ArcJID, |
331 |
|
default_mode => DefaultMode, |
332 |
:-( |
always_jids => AlwaysJIDs, never_jids => NeverJIDs, iq => IQ}), |
333 |
:-( |
ArcID = archive_id_int(HostType, ArcJID), |
334 |
:-( |
Res = set_prefs(HostType, ArcID, ArcJID, DefaultMode, AlwaysJIDs, NeverJIDs), |
335 |
:-( |
handle_set_prefs_result(Res, DefaultMode, AlwaysJIDs, NeverJIDs, IQ). |
336 |
|
|
337 |
|
handle_set_prefs_result(ok, DefaultMode, AlwaysJIDs, NeverJIDs, IQ) -> |
338 |
:-( |
ResultPrefsEl = mod_mam_utils:result_prefs(DefaultMode, AlwaysJIDs, NeverJIDs, IQ#iq.xmlns), |
339 |
:-( |
IQ#iq{type = result, sub_el = [ResultPrefsEl]}; |
340 |
|
handle_set_prefs_result({error, Reason}, |
341 |
|
_DefaultMode, _AlwaysJIDs, _NeverJIDs, IQ) -> |
342 |
:-( |
return_error_iq(IQ, Reason). |
343 |
|
|
344 |
|
-spec handle_get_prefs(host_type(), jid:jid(), jlib:iq()) -> |
345 |
|
jlib:iq() | {error, any(), jlib:iq()}. |
346 |
|
handle_get_prefs(HostType, ArcJID=#jid{}, IQ=#iq{}) -> |
347 |
:-( |
ArcID = archive_id_int(HostType, ArcJID), |
348 |
:-( |
Res = get_prefs(HostType, ArcID, ArcJID, always), |
349 |
:-( |
handle_get_prefs_result(ArcJID, Res, IQ). |
350 |
|
|
351 |
|
handle_get_prefs_result(ArcJID, {DefaultMode, AlwaysJIDs, NeverJIDs}, IQ) -> |
352 |
:-( |
?LOG_DEBUG(#{what => mam_muc_get_prefs_result, archive_jid => ArcJID, |
353 |
|
default_mode => DefaultMode, |
354 |
:-( |
always_jids => AlwaysJIDs, never_jids => NeverJIDs, iq => IQ}), |
355 |
:-( |
ResultPrefsEl = mod_mam_utils:result_prefs(DefaultMode, AlwaysJIDs, NeverJIDs, IQ#iq.xmlns), |
356 |
:-( |
IQ#iq{type = result, sub_el = [ResultPrefsEl]}; |
357 |
|
handle_get_prefs_result(_ArcJID, {error, Reason}, IQ) -> |
358 |
:-( |
return_error_iq(IQ, Reason). |
359 |
|
|
360 |
|
-spec handle_set_message_form(HostType :: host_type(), |
361 |
|
From :: jid:jid(), ArcJID :: jid:jid(), |
362 |
|
IQ :: jlib:iq()) -> |
363 |
|
jlib:iq() | ignore | {error, term(), jlib:iq()}. |
364 |
|
handle_set_message_form(HostType, #jid{} = From, #jid{} = ArcJID, IQ) -> |
365 |
110 |
ArcID = archive_id_int(HostType, ArcJID), |
366 |
110 |
ResLimit = mod_mam_params:max_result_limit(?MODULE, HostType), |
367 |
110 |
DefLimit = mod_mam_params:default_result_limit(?MODULE, HostType), |
368 |
110 |
ExtMod = mod_mam_params:extra_params_module(?MODULE, HostType), |
369 |
110 |
Sim = mod_mam_params:enforce_simple_queries(?MODULE, HostType), |
370 |
110 |
try mam_iq:form_to_lookup_params(IQ, ResLimit, DefLimit, ExtMod, Sim) of |
371 |
|
Params0 -> |
372 |
108 |
do_handle_set_message_form(HostType, From, ArcID, ArcJID, IQ, Params0) |
373 |
|
catch _C:R:S -> |
374 |
2 |
report_issue({R, S}, mam_lookup_failed, ArcJID, IQ), |
375 |
2 |
return_error_iq(IQ, R) |
376 |
|
end. |
377 |
|
|
378 |
|
|
379 |
|
-spec do_handle_set_message_form(HostType :: mongooseim:host_type(), |
380 |
|
From :: jid:jid(), |
381 |
|
ArcId :: mod_mam:archive_id(), |
382 |
|
ArcJID :: jid:jid(), |
383 |
|
IQ :: jlib:iq(), |
384 |
|
Params :: mam_iq:lookup_params()) -> |
385 |
|
jlib:iq() | ignore | {error, term(), jlib:iq()}. |
386 |
|
do_handle_set_message_form(HostType, From, ArcID, ArcJID, IQ, Params0) -> |
387 |
108 |
Params = mam_iq:lookup_params_with_archive_details(Params0, ArcID, ArcJID, From), |
388 |
108 |
Result = mod_mam_utils:lookup(HostType, Params, fun lookup_messages/2), |
389 |
108 |
handle_lookup_result(Result, HostType, From, IQ, Params). |
390 |
|
|
391 |
|
-spec handle_lookup_result({ok, mod_mam:lookup_result()} | {error, term()}, |
392 |
|
host_type(), jid:jid(), jlib:iq(), map()) -> |
393 |
|
jlib:iq() | ignore | {error, term(), jlib:iq()}. |
394 |
|
handle_lookup_result(Result, HostType, From, IQ, #{owner_jid := ArcJID} = Params) -> |
395 |
108 |
case Result of |
396 |
|
{error, Reason} -> |
397 |
6 |
report_issue(Reason, mam_muc_lookup_failed, ArcJID, IQ), |
398 |
6 |
return_error_iq(IQ, Reason); |
399 |
|
{ok, Res} -> |
400 |
102 |
send_messages_and_iq_result(Res, HostType, From, IQ, Params) |
401 |
|
end. |
402 |
|
|
403 |
|
send_messages_and_iq_result(#{total_count := TotalCount, offset := Offset, |
404 |
|
messages := MessageRows, is_complete := IsComplete}, |
405 |
|
HostType, From, |
406 |
|
#iq{xmlns = MamNs, sub_el = QueryEl} = IQ, |
407 |
|
#{owner_jid := ArcJID} = Params) -> |
408 |
|
%% Reverse order of messages if the client requested it |
409 |
102 |
MessageRows1 = mod_mam_utils:maybe_reverse_messages(Params, MessageRows), |
410 |
|
%% Forward messages |
411 |
102 |
QueryID = exml_query:attr(QueryEl, <<"queryid">>, <<>>), |
412 |
102 |
{FirstMessID, LastMessID} = forward_messages(HostType, From, ArcJID, MamNs, |
413 |
|
QueryID, MessageRows1, true), |
414 |
|
%% Make fin iq |
415 |
102 |
IsStable = true, |
416 |
102 |
ResultSetEl = mod_mam_utils:result_set(FirstMessID, LastMessID, Offset, TotalCount), |
417 |
102 |
ExtFinMod = mod_mam_params:extra_fin_element_module(?MODULE, HostType), |
418 |
102 |
FinElem = mod_mam_utils:make_fin_element(HostType, Params, IQ#iq.xmlns, |
419 |
|
IsComplete, IsStable, |
420 |
|
ResultSetEl, ExtFinMod), |
421 |
102 |
IQ#iq{type = result, sub_el = [FinElem]}. |
422 |
|
|
423 |
|
forward_messages(HostType, From, ArcJID, MamNs, QueryID, MessageRows, SetClientNs) -> |
424 |
|
%% Forward messages |
425 |
102 |
{FirstMessID, LastMessID, HideUser} = |
426 |
|
case MessageRows of |
427 |
20 |
[] -> {undefined, undefined, undefined}; |
428 |
82 |
[_ | _] -> {message_row_to_ext_id(hd(MessageRows)), |
429 |
|
message_row_to_ext_id(lists:last(MessageRows)), |
430 |
|
is_user_identity_hidden(HostType, From, ArcJID)} |
431 |
|
end, |
432 |
102 |
SendModule = mod_mam_params:send_message_mod(?MODULE, HostType), |
433 |
102 |
[send_message(SendModule, Row, ArcJID, From, |
434 |
|
message_row_to_xml(MamNs, From, HideUser, SetClientNs, Row, |
435 |
|
QueryID)) |
436 |
102 |
|| Row <- MessageRows], |
437 |
102 |
{FirstMessID, LastMessID}. |
438 |
|
|
439 |
|
send_message(SendModule, Row, ArcJID, From, Packet) -> |
440 |
449 |
mam_send_message:call_send_message(SendModule, Row, ArcJID, From, Packet). |
441 |
|
|
442 |
|
-spec handle_get_message_form(host_type(), jid:jid(), jid:jid(), jlib:iq()) -> |
443 |
|
jlib:iq(). |
444 |
|
handle_get_message_form(HostType, |
445 |
|
_From = #jid{}, _ArcJID = #jid{}, IQ = #iq{}) -> |
446 |
:-( |
return_message_form_iq(HostType, IQ). |
447 |
|
|
448 |
|
-spec handle_get_metadata(host_type(), jid:jid(), jid:jid(), jlib:iq()) -> |
449 |
|
jlib:iq() | {error, term(), jlib:iq()}. |
450 |
|
handle_get_metadata(HostType, #jid{} = From, #jid{} = ArcJID, IQ) -> |
451 |
6 |
ArcID = archive_id_int(HostType, ArcJID), |
452 |
6 |
case mod_mam_utils:lookup_first_and_last_messages(HostType, ArcID, From, |
453 |
|
ArcJID, fun lookup_messages/2) of |
454 |
|
{error, Reason} -> |
455 |
:-( |
report_issue(Reason, mam_lookup_failed, ArcJID, IQ), |
456 |
:-( |
return_error_iq(IQ, Reason); |
457 |
|
{FirstMsg, LastMsg} -> |
458 |
4 |
{FirstMsgID, FirstMsgTS} = mod_mam_utils:get_msg_id_and_timestamp(FirstMsg), |
459 |
4 |
{LastMsgID, LastMsgTS} = mod_mam_utils:get_msg_id_and_timestamp(LastMsg), |
460 |
4 |
MetadataElement = |
461 |
|
mod_mam_utils:make_metadata_element(FirstMsgID, FirstMsgTS, LastMsgID, LastMsgTS), |
462 |
4 |
IQ#iq{type = result, sub_el = [MetadataElement]}; |
463 |
|
empty_archive -> |
464 |
2 |
MetadataElement = mod_mam_utils:make_metadata_element(), |
465 |
2 |
IQ#iq{type = result, sub_el = [MetadataElement]} |
466 |
|
end. |
467 |
|
|
468 |
|
%% ---------------------------------------------------------------------- |
469 |
|
%% Backend wrappers |
470 |
|
|
471 |
|
-spec archive_id_int(HostType :: host_type(), ArcJID :: jid:jid()) -> |
472 |
|
integer() | undefined. |
473 |
|
archive_id_int(HostType, ArcJID = #jid{}) -> |
474 |
667 |
mongoose_hooks:mam_muc_archive_id(HostType, ArcJID). |
475 |
|
|
476 |
|
-spec archive_size(HostType :: host_type(), ArcID :: mod_mam:archive_id(), |
477 |
|
ArcJID ::jid:jid()) -> non_neg_integer(). |
478 |
|
archive_size(HostType, ArcID, ArcJID = #jid{}) -> |
479 |
257 |
mongoose_hooks:mam_muc_archive_size(HostType, ArcID, ArcJID). |
480 |
|
|
481 |
|
-spec get_behaviour(HostType :: host_type(), ArcID :: mod_mam:archive_id(), |
482 |
|
LocJID :: jid:jid(), RemJID :: jid:jid()) -> any(). |
483 |
|
get_behaviour(HostType, ArcID, LocJID = #jid{}, RemJID = #jid{}) -> |
484 |
140 |
mongoose_hooks:mam_muc_get_behaviour(HostType, ArcID, LocJID, RemJID). |
485 |
|
|
486 |
|
-spec set_prefs(HostType :: host_type(), ArcID :: mod_mam:archive_id(), |
487 |
|
ArcJID :: jid:jid(), DefaultMode :: mod_mam:archive_behaviour(), |
488 |
|
AlwaysJIDs :: [jid:literal_jid()], |
489 |
|
NeverJIDs :: [jid:literal_jid()]) -> any(). |
490 |
|
set_prefs(HostType, ArcID, ArcJID, DefaultMode, AlwaysJIDs, NeverJIDs) -> |
491 |
:-( |
mongoose_hooks:mam_muc_set_prefs(HostType, ArcID, ArcJID, DefaultMode, |
492 |
|
AlwaysJIDs, NeverJIDs). |
493 |
|
|
494 |
|
%% @doc Load settings from the database. |
495 |
|
-spec get_prefs(HostType :: host_type(), ArcID :: mod_mam:archive_id(), |
496 |
|
ArcJID :: jid:jid(), GlobalDefaultMode :: mod_mam:archive_behaviour()) |
497 |
|
-> mod_mam:preference() | {error, Reason :: term()}. |
498 |
|
get_prefs(HostType, ArcID, ArcJID, GlobalDefaultMode) -> |
499 |
:-( |
mongoose_hooks:mam_muc_get_prefs(HostType, GlobalDefaultMode, ArcID, ArcJID). |
500 |
|
|
501 |
|
-spec remove_archive(host_type(), mod_mam:archive_id() | undefined, |
502 |
|
jid:jid()) -> ok. |
503 |
|
remove_archive(HostType, ArcID, ArcJID = #jid{}) -> |
504 |
138 |
mongoose_hooks:mam_muc_remove_archive(HostType, ArcID, ArcJID), |
505 |
138 |
ok. |
506 |
|
|
507 |
|
%% See description in mod_mam_pm. |
508 |
|
-spec lookup_messages(HostType :: host_type(), Params :: map()) -> |
509 |
|
{ok, mod_mam:lookup_result()} |
510 |
|
| {error, 'policy-violation'} |
511 |
|
| {error, Reason :: term()}. |
512 |
|
lookup_messages(HostType, Params) -> |
513 |
118 |
Result = lookup_messages_without_policy_violation_check(HostType, Params), |
514 |
|
%% If a query returns a number of stanzas greater than this limit and the |
515 |
|
%% client did not specify a limit using RSM then the server should return |
516 |
|
%% a policy-violation error to the client. |
517 |
118 |
mod_mam_utils:check_result_for_policy_violation(Params, Result). |
518 |
|
|
519 |
|
lookup_messages_without_policy_violation_check(HostType, |
520 |
|
#{search_text := SearchText} = Params) -> |
521 |
118 |
case SearchText /= undefined andalso |
522 |
1 |
not mod_mam_params:has_full_text_search(?MODULE, HostType) of |
523 |
|
true -> %% Use of disabled full text search |
524 |
:-( |
{error, 'not-supported'}; |
525 |
|
false -> |
526 |
118 |
StartT = erlang:monotonic_time(microsecond), |
527 |
118 |
R = case maps:get(message_ids, Params, undefined) of |
528 |
|
undefined -> |
529 |
112 |
mongoose_hooks:mam_muc_lookup_messages(HostType, |
530 |
|
Params#{message_id => undefined}); |
531 |
|
IDs -> |
532 |
6 |
mod_mam_utils:lookup_specific_messages(HostType, Params, IDs, |
533 |
|
fun mongoose_hooks:mam_muc_lookup_messages/2) |
534 |
|
end, |
535 |
118 |
Diff = erlang:monotonic_time(microsecond) - StartT, |
536 |
118 |
mongoose_metrics:update(HostType, [backends, ?MODULE, lookup], Diff), |
537 |
118 |
R |
538 |
|
end. |
539 |
|
|
540 |
|
archive_message_for_ct(Params = #{local_jid := RoomJid}) -> |
541 |
544 |
HostType = mod_muc_light_utils:room_jid_to_host_type(RoomJid), |
542 |
544 |
archive_message(HostType, Params). |
543 |
|
|
544 |
|
-spec archive_message(host_type(), mod_mam:archive_message_params()) -> ok | {error, timeout}. |
545 |
|
archive_message(HostType, Params) -> |
546 |
684 |
mongoose_hooks:mam_muc_archive_message(HostType, Params). |
547 |
|
|
548 |
|
%% ---------------------------------------------------------------------- |
549 |
|
%% Helpers |
550 |
|
|
551 |
|
-spec message_row_to_xml(binary(), jid:jid(), boolean(), boolean(), row(), binary() | undefined) -> |
552 |
|
exml:element(). |
553 |
|
message_row_to_xml(MamNs, ReceiverJID, HideUser, SetClientNs, |
554 |
|
#{id := MessID, jid := SrcJID, packet := Packet}, QueryID) -> |
555 |
449 |
{Microseconds, _NodeMessID} = mod_mam_utils:decode_compact_uuid(MessID), |
556 |
449 |
TS = calendar:system_time_to_rfc3339(Microseconds, [{offset, "Z"}, {unit, microsecond}]), |
557 |
449 |
BExtMessID = mod_mam_utils:mess_id_to_external_binary(MessID), |
558 |
449 |
Packet1 = maybe_delete_x_user_element(HideUser, ReceiverJID, Packet), |
559 |
449 |
Packet2 = mod_mam_utils:maybe_set_client_xmlns(SetClientNs, Packet1), |
560 |
449 |
Packet3 = replace_from_to_attributes(SrcJID, Packet2), |
561 |
449 |
mod_mam_utils:wrap_message(MamNs, Packet3, QueryID, BExtMessID, TS, SrcJID). |
562 |
|
|
563 |
|
maybe_delete_x_user_element(true, ReceiverJID, Packet) -> |
564 |
32 |
PacketJID = mod_mam_utils:packet_to_x_user_jid(Packet), |
565 |
32 |
case jid:are_bare_equal(ReceiverJID, PacketJID) of |
566 |
|
false -> |
567 |
26 |
mod_mam_utils:delete_x_user_element(Packet); |
568 |
|
true -> %% expose identity for user's own messages |
569 |
6 |
Packet |
570 |
|
end; |
571 |
|
maybe_delete_x_user_element(false, _ReceiverJID, Packet) -> |
572 |
417 |
Packet. |
573 |
|
|
574 |
|
%% From XEP-0313: |
575 |
|
%% When sending out the archives to a requesting client, |
576 |
|
%% the forwarded stanza MUST NOT have a 'to' attribute, and |
577 |
|
%% the 'from' MUST be the occupant JID of the sender of the archived message. |
578 |
|
replace_from_to_attributes(SrcJID, Packet = #xmlel{attrs = Attrs}) -> |
579 |
449 |
NewAttrs = jlib:replace_from_to_attrs(jid:to_binary(SrcJID), undefined, Attrs), |
580 |
449 |
Packet#xmlel{attrs = NewAttrs}. |
581 |
|
|
582 |
|
-spec message_row_to_ext_id(row()) -> binary(). |
583 |
|
message_row_to_ext_id(#{id := MessID}) -> |
584 |
164 |
mod_mam_utils:mess_id_to_external_binary(MessID). |
585 |
|
|
586 |
|
-spec handle_error_iq(mongoose_acc:t(), host_type(), jid:jid(), atom(), |
587 |
|
{error, term(), jlib:iq()} | jlib:iq() | ignore) -> {mongoose_acc:t(), jlib:iq() | ignore}. |
588 |
|
handle_error_iq(Acc, HostType, _To, _Action, {error, _Reason, IQ}) -> |
589 |
8 |
mongoose_metrics:update(HostType, modMucMamDroppedIQ, 1), |
590 |
8 |
{Acc, IQ}; |
591 |
|
handle_error_iq(Acc, _HostType, _To, _Action, IQ) -> |
592 |
108 |
{Acc, IQ}. |
593 |
|
|
594 |
|
return_error_iq(IQ, {Reason, {stacktrace, _Stacktrace}}) -> |
595 |
:-( |
return_error_iq(IQ, Reason); |
596 |
|
return_error_iq(IQ, timeout) -> |
597 |
:-( |
{error, timeout, IQ#iq{type = error, sub_el = [mongoose_xmpp_errors:service_unavailable(<<"en">>, <<"Timeout in mod_mam_muc">>)]}}; |
598 |
|
return_error_iq(IQ, invalid_stanza_id) -> |
599 |
2 |
Text = mongoose_xmpp_errors:not_acceptable(<<"en">>, <<"Invalid stanza id provided">>), |
600 |
2 |
{error, invalid_stanza_id, IQ#iq{type = error, sub_el = [Text]}}; |
601 |
|
return_error_iq(IQ, item_not_found) -> |
602 |
6 |
{error, item_not_found, IQ#iq{type = error, sub_el = [mongoose_xmpp_errors:item_not_found()]}}; |
603 |
|
return_error_iq(IQ, not_implemented) -> |
604 |
:-( |
{error, not_implemented, IQ#iq{type = error, sub_el = [mongoose_xmpp_errors:feature_not_implemented(<<"en">>, <<"From mod_mam_muc">>)]}}; |
605 |
|
return_error_iq(IQ, missing_with_jid) -> |
606 |
:-( |
Error = mongoose_xmpp_errors:bad_request(<<"en">>, |
607 |
|
<<"Limited set of queries allowed in the conversation mode.", |
608 |
|
"Missing with_jid filter">>), |
609 |
:-( |
{error, bad_request, IQ#iq{type = error, sub_el = [Error]}}; |
610 |
|
return_error_iq(IQ, Reason) -> |
611 |
:-( |
{error, Reason, IQ#iq{type = error, sub_el = [mongoose_xmpp_errors:internal_server_error()]}}. |
612 |
|
|
613 |
|
-spec return_action_not_allowed_error_iq(Reason :: binary(), jlib:iq()) -> jlib:iq(). |
614 |
|
return_action_not_allowed_error_iq(Reason, IQ) -> |
615 |
4 |
ErrorEl = jlib:stanza_errort(<<"">>, <<"cancel">>, <<"not-allowed">>, |
616 |
|
<<"en">>, <<"The action is not allowed. ", Reason/binary>>), |
617 |
4 |
IQ#iq{type = error, sub_el = [ErrorEl]}. |
618 |
|
|
619 |
|
-spec return_max_delay_reached_error_iq(jlib:iq()) -> jlib:iq(). |
620 |
|
return_max_delay_reached_error_iq(IQ) -> |
621 |
|
%% Message not found. |
622 |
:-( |
ErrorEl = mongoose_xmpp_errors:resource_constraint( |
623 |
|
<<"en">>, <<"The action is cancelled because of flooding.">>), |
624 |
:-( |
IQ#iq{type = error, sub_el = [ErrorEl]}. |
625 |
|
|
626 |
|
return_message_form_iq(HostType, IQ) -> |
627 |
:-( |
Form = mod_mam_utils:message_form(?MODULE, HostType, IQ#iq.xmlns), |
628 |
:-( |
IQ#iq{type = result, sub_el = [Form]}. |
629 |
|
|
630 |
|
% the stacktrace is a big lie |
631 |
|
report_issue({Reason, {stacktrace, Stacktrace}}, Issue, ArcJID, IQ) -> |
632 |
:-( |
report_issue(Reason, Stacktrace, Issue, ArcJID, IQ); |
633 |
|
report_issue(Reason, Issue, ArcJID, IQ) -> |
634 |
8 |
report_issue(Reason, [], Issue, ArcJID, IQ). |
635 |
|
|
636 |
|
report_issue(invalid_stanza_id, _Stacktrace, _Issue, _ArcJID, _IQ) -> |
637 |
:-( |
expected; |
638 |
|
report_issue(item_not_found, _Stacktrace, _Issue, _ArcJID, _IQ) -> |
639 |
6 |
expected; |
640 |
|
report_issue(missing_with_jid, _Stacktrace, _Issue, _ArcJID, _IQ) -> |
641 |
:-( |
expected; |
642 |
|
report_issue(not_implemented, _Stacktrace, _Issue, _ArcJID, _IQ) -> |
643 |
:-( |
expected; |
644 |
|
report_issue(timeout, _Stacktrace, _Issue, _ArcJID, _IQ) -> |
645 |
:-( |
expected; |
646 |
|
report_issue(Reason, Stacktrace, Issue, #jid{lserver = LServer, luser = LUser}, IQ) -> |
647 |
2 |
?LOG_ERROR(#{what => mam_muc_error, issue => Issue, reason => Reason, |
648 |
:-( |
user => LUser, server => LServer, iq => IQ, stacktrace => Stacktrace}). |
649 |
|
|
650 |
|
-spec is_archivable_message(HostType :: host_type(), |
651 |
|
Dir :: incoming | outgoing, |
652 |
|
Packet :: exml:element()) -> boolean(). |
653 |
|
is_archivable_message(HostType, Dir, Packet) -> |
654 |
146 |
M = mod_mam_params:is_archivable_message_module(?MODULE, HostType), |
655 |
146 |
ArchiveChatMarkers = mod_mam_params:archive_chat_markers(?MODULE, HostType), |
656 |
146 |
erlang:apply(M, is_archivable_message, [?MODULE, Dir, Packet, ArchiveChatMarkers]). |
657 |
|
|
658 |
|
-spec hooks(mongooseim:host_type()) -> gen_hook:hook_list(). |
659 |
|
hooks(HostType) -> |
660 |
48 |
[{disco_muc_features, HostType, fun ?MODULE:disco_muc_features/3, #{}, 99}, |
661 |
|
{filter_room_packet, HostType, fun ?MODULE:filter_room_packet/3, #{}, 60}, |
662 |
|
{forget_room, HostType, fun ?MODULE:forget_room/3, #{}, 90}, |
663 |
|
{get_personal_data, HostType, fun ?MODULE:get_personal_data/3, #{}, 50} |
664 |
|
| mongoose_metrics_mam_hooks:get_mam_muc_hooks(HostType)]. |
665 |
|
|
666 |
|
add_iq_handlers(HostType, Opts) -> |
667 |
24 |
IQDisc = gen_mod:get_opt(iqdisc, Opts, parallel), |
668 |
24 |
MUCSubdomainPattern = gen_mod:get_module_opt(HostType, ?MODULE, host), |
669 |
|
|
670 |
24 |
gen_iq_handler:add_iq_handler_for_subdomain(HostType, MUCSubdomainPattern, |
671 |
|
?NS_MAM_04, mod_muc_iq, |
672 |
|
fun ?MODULE:room_process_mam_iq/5, |
673 |
|
#{}, IQDisc), |
674 |
24 |
gen_iq_handler:add_iq_handler_for_subdomain(HostType, MUCSubdomainPattern, |
675 |
|
?NS_MAM_06, mod_muc_iq, |
676 |
|
fun ?MODULE:room_process_mam_iq/5, |
677 |
|
#{}, IQDisc), |
678 |
24 |
ok. |
679 |
|
|
680 |
|
remove_iq_handlers(HostType) -> |
681 |
24 |
MUCSubdomainPattern = gen_mod:get_module_opt(HostType, ?MODULE, host), |
682 |
24 |
gen_iq_handler:remove_iq_handler_for_subdomain(HostType, MUCSubdomainPattern, |
683 |
|
?NS_MAM_04, mod_muc_iq), |
684 |
24 |
gen_iq_handler:remove_iq_handler_for_subdomain(HostType, MUCSubdomainPattern, |
685 |
|
?NS_MAM_06, mod_muc_iq), |
686 |
24 |
ok. |
687 |
|
|
688 |
|
ensure_metrics(HostType) -> |
689 |
24 |
mongoose_metrics:ensure_metric(HostType, [backends, ?MODULE, lookup], histogram), |
690 |
24 |
lists:foreach(fun(Name) -> |
691 |
216 |
mongoose_metrics:ensure_metric(HostType, Name, spiral) |
692 |
|
end, |
693 |
|
spirals()). |
694 |
|
|
695 |
|
spirals() -> |
696 |
24 |
[modMucMamPrefsSets, |
697 |
|
modMucMamPrefsGets, |
698 |
|
modMucMamArchiveRemoved, |
699 |
|
modMucMamLookups, |
700 |
|
modMucMamForwarded, |
701 |
|
modMucMamArchived, |
702 |
|
modMucMamFlushed, |
703 |
|
modMucMamDropped, |
704 |
|
modMucMamDroppedIQ]. |