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