./ct_report/coverage/mod_muc_light.COVER.html

1 %%%----------------------------------------------------------------------
2 %%% File : mod_muc_light.erl
3 %%% Author : Piotr Nosek <piotr.nosek@erlang-solutions.com>
4 %%% Purpose : MUC light support
5 %%% Created : 8 Sep 2014 by Piotr Nosek <piotr.nosek@erlang-solutions.com>
6 %%%
7 %%% Looking for documentation excerpt that was present here for 5 years?
8 %%% Now everything is moved to doc/modules/mod_muc_light.md
9 %%%
10 %%%----------------------------------------------------------------------
11
12 -module(mod_muc_light).
13 -author('piotr.nosek@erlang-solutions.com').
14
15 -include("mongoose.hrl").
16 -include("jlib.hrl").
17 -include("mod_muc_light.hrl").
18 -include("mod_roster.hrl").
19 -include("mongoose_rsm.hrl").
20 -include("mongoose_config_spec.hrl").
21
22 -behaviour(gen_mod).
23 -behaviour(mongoose_packet_handler).
24 -behaviour(mongoose_module_metrics).
25
26 %% API
27 -export([server_host_to_muc_host/2]).
28 -export([config_schema/1]).
29
30 %% For Administration API
31 -export([try_to_create_room/3,
32 change_room_config/5,
33 delete_room/1]).
34
35 %% gen_mod callbacks
36 -export([start/2, stop/1, hooks/1, config_spec/0, supported_features/0, deps/2]).
37
38 %% config processing callback
39 -export([process_config_schema/1]).
40
41 %% Packet handler export
42 -export([process_packet/5]).
43
44 %% Hook handlers
45 -export([prevent_service_unavailable/3,
46 disco_local_items/3,
47 remove_user/3,
48 remove_domain/3,
49 add_rooms_to_roster/3,
50 process_iq_get/3,
51 process_iq_set/3,
52 is_muc_room_owner/3,
53 can_access_room/3,
54 acc_room_affiliations/3,
55 room_exists/3,
56 can_access_identity/3]).
57
58 -export([get_acc_room_affiliations/2]).
59
60 %% For propEr
61 -export([apply_rsm/3]).
62
63 -export([config_metrics/1]).
64 %% for mod_muc_light_codec_legacy
65 -export([subdomain_pattern/1]).
66
67 -export([get_room_affs_from_acc/2, set_room_affs_from_acc/3]).
68
69 %% For tests
70 -export([default_schema/0,
71 force_clear_from_ct/1]).
72
73 -export_type([aff_users/0]).
74
75 -ignore_xref([
76 apply_rsm/3, default_schema/0, force_clear_from_ct/1, server_host_to_muc_host/2
77 ]).
78
79 -type muc_server() :: jid:lserver().
80 -type host_type() :: mongooseim:host_type().
81 -type versioned_affs() :: {ok, aff_users(), binary()}.
82
83 %%====================================================================
84 %% API
85 %%====================================================================
86
87 -spec supported_features() -> [atom()].
88 supported_features() ->
89 73 [dynamic_domains].
90
91 -spec default_schema() -> mod_muc_light_room_config:schema().
92 default_schema() ->
93 % This list needs to be sorted
94 190 [{<<"roomname">>, <<"Untitled">>, roomname, binary},
95 {<<"subject">>, <<>>, subject, binary}].
96
97 -spec default_host() -> mongoose_subdomain_utils:subdomain_pattern().
98 default_host() ->
99 186 mongoose_subdomain_utils:make_subdomain_pattern(<<"muclight.@HOST@">>).
100
101 -spec config_schema(MUCServer :: muc_server()) -> mod_muc_light_room_config:schema().
102 config_schema(MUCServer) ->
103 665 HostType = mod_muc_light_utils:muc_host_to_host_type(MUCServer),
104 665 config_schema_for_host_type(HostType).
105
106 %% Internals
107
108 -spec config_schema_for_host_type(host_type()) -> mod_muc_light_room_config:schema().
109 config_schema_for_host_type(HostType) ->
110 943 gen_mod:get_module_opt(HostType, ?MODULE, config_schema).
111
112 force_clear_from_ct(HostType) ->
113 104 catch mod_muc_light_cache:force_clear(HostType),
114 104 mod_muc_light_db_backend:force_clear(HostType).
115
116 %%====================================================================
117 %% Administration API
118 %%====================================================================
119
120 -spec try_to_create_room(CreatorJid :: jid:jid(), RoomJID :: jid:jid(),
121 CreationCfg :: create_req_props()) ->
122 {ok, jid:jid(), create_req_props()} | validation_error()
123 | {error, bad_request | exists | max_occupants_reached}.
124 try_to_create_room(CreatorJid, RoomJID, #create{raw_config = RawConfig} = CreationCfg) ->
125 278 RoomUS = jid:to_lus(RoomJID),
126 278 HostType = mod_muc_light_utils:room_jid_to_host_type(RoomJID),
127 278 CfgRes = prepare_config(HostType, RawConfig),
128 278 AffRes = prepare_affs(HostType, CreatorJid, RoomUS, CreationCfg),
129 278 case {CfgRes, AffRes} of
130 {{ok, Config0}, {ok, FinalAffUsers}} ->
131 276 Version = mongoose_bin:gen_from_timestamp(),
132 276 case mod_muc_light_db_backend:create_room(
133 HostType, RoomUS, lists:sort(Config0), FinalAffUsers, Version) of
134 {ok, {FinalU, FinalS}} ->
135 268 {ok, jid:make_noprep(FinalU, FinalS, <<>>), CreationCfg#create{
136 aff_users = FinalAffUsers, version = Version}};
137 Other ->
138 8 Other
139 end;
140 {{error, _} = Error, _} ->
141 1 Error;
142 {_, {error, _} = Error} ->
143 1 Error
144 end.
145
146 -spec change_room_config(UserJid :: jid:jid(), RoomID :: jid:user(),
147 MUCLightDomain :: jid:server(),
148 ConfigReq :: config_req_props(),
149 Acc :: mongoose_acc:t()) ->
150 {ok, jid:jid(), config_req_props()}
151 | {error, validation_error() | bad_request | not_allowed | not_exists | item_not_found}.
152 change_room_config(UserJid, RoomID, MUCLightDomain, ConfigReq, Acc1) ->
153 25 RoomJID = jid:make_bare(RoomID, MUCLightDomain),
154 25 {Acc2, AffUsersRes} = get_acc_room_affiliations(Acc1, RoomJID),
155 25 case mod_muc_light_room:process_request(UserJid, RoomJID, {set, ConfigReq},
156 AffUsersRes, Acc2) of
157 {set, ConfigResp, _} ->
158 9 {ok, RoomJID, ConfigResp};
159 {error, _Reason} = E ->
160 16 E
161 end.
162
163 -spec delete_room(RoomUS :: jid:simple_bare_jid()) -> ok | {error, not_exists}.
164 delete_room({_, RoomS} = RoomUS) ->
165 9 HostType = mod_muc_light_utils:muc_host_to_host_type(RoomS),
166 9 mod_muc_light_db_backend:destroy_room(HostType, RoomUS).
167
168 %%====================================================================
169 %% gen_mod callbacks
170 %%====================================================================
171
172 -spec start(host_type(), gen_mod:module_opts()) -> ok.
173 start(HostType, Opts) ->
174 71 Codec = host_type_to_codec(HostType),
175 71 mod_muc_light_db_backend:start(HostType, Opts),
176 71 mod_muc_light_codec_backend:start(HostType, #{backend => Codec}),
177 %% Handler
178 71 SubdomainPattern = subdomain_pattern(HostType),
179 71 PacketHandler = mongoose_packet_handler:new(?MODULE),
180 71 mongoose_domain_api:register_subdomain(HostType, SubdomainPattern, PacketHandler),
181 71 ok.
182
183 -spec stop(host_type()) -> ok.
184 stop(HostType) ->
185 71 SubdomainPattern = subdomain_pattern(HostType),
186 71 mongoose_domain_api:unregister_subdomain(HostType, SubdomainPattern),
187 71 mod_muc_light_codec_backend:stop(HostType),
188 71 mod_muc_light_db_backend:stop(HostType),
189 71 ok.
190
191 -spec deps(mongooseim:host_type(), gen_mod:module_opts()) -> gen_mod_deps:deps().
192 deps(_HostType, #{cache_affs := CacheOpts}) ->
193 220 [{mod_muc_light_cache, CacheOpts, hard}];
194 deps(_HostType, #{}) ->
195 667 [].
196
197 %% Init helpers
198 subdomain_pattern(HostType) ->
199 154 gen_mod:get_module_opt(HostType, ?MODULE, host).
200
201 server_host_to_muc_host(HostType, ServerHost) ->
202 9 mongoose_subdomain_utils:get_fqdn(subdomain_pattern(HostType), ServerHost).
203
204 host_type_to_codec(HostType) ->
205 213 case gen_mod:get_module_opt(HostType, ?MODULE, legacy_mode) of
206 true ->
207 27 legacy;
208 false ->
209 186 modern
210 end.
211
212 %% Config callbacks
213 -spec config_spec() -> mongoose_config_spec:config_section().
214 config_spec() ->
215 186 #section{
216 items = #{<<"backend">> => #option{type = atom,
217 validate = {module, mod_muc_light_db}},
218 <<"cache_affs">> => mod_muc_light_cache:config_spec(),
219 <<"host">> => #option{type = string,
220 validate = subdomain_template,
221 process = fun mongoose_subdomain_utils:make_subdomain_pattern/1},
222 <<"equal_occupants">> => #option{type = boolean},
223 <<"legacy_mode">> => #option{type = boolean},
224 <<"rooms_per_user">> => #option{type = int_or_infinity,
225 validate = positive},
226 <<"blocking">> => #option{type = boolean},
227 <<"all_can_configure">> => #option{type = boolean},
228 <<"all_can_invite">> => #option{type = boolean},
229 <<"max_occupants">> => #option{type = int_or_infinity,
230 validate = positive},
231 <<"rooms_per_page">> => #option{type = int_or_infinity,
232 validate = positive},
233 <<"rooms_in_rosters">> => #option{type = boolean},
234 <<"config_schema">> => #list{items = config_schema_spec(),
235 process = fun ?MODULE:process_config_schema/1}
236 },
237 defaults = #{<<"backend">> => mnesia,
238 <<"host">> => default_host(),
239 <<"equal_occupants">> => ?DEFAULT_EQUAL_OCCUPANTS,
240 <<"legacy_mode">> => ?DEFAULT_LEGACY_MODE,
241 <<"rooms_per_user">> => ?DEFAULT_ROOMS_PER_USER,
242 <<"blocking">> => ?DEFAULT_BLOCKING,
243 <<"all_can_configure">> => ?DEFAULT_ALL_CAN_CONFIGURE,
244 <<"all_can_invite">> => ?DEFAULT_ALL_CAN_INVITE,
245 <<"max_occupants">> => ?DEFAULT_MAX_OCCUPANTS,
246 <<"rooms_per_page">> => ?DEFAULT_ROOMS_PER_PAGE,
247 <<"rooms_in_rosters">> => ?DEFAULT_ROOMS_IN_ROSTERS,
248 <<"config_schema">> => default_schema()}
249 }.
250
251 config_schema_spec() ->
252 186 #section{
253 items = #{<<"field">> => #option{type = binary,
254 validate = non_empty},
255 <<"string_value">> => #option{type = binary},
256 <<"integer_value">> => #option{type = integer},
257 <<"float_value">> => #option{type = float},
258 <<"internal_key">> => #option{type = atom,
259 validate = non_empty}
260 },
261 required = [<<"field">>]
262 }.
263
264 -spec process_config_schema([map()]) -> mod_muc_light_room_config:schema().
265 process_config_schema(Items) ->
266
:-(
lists:ukeysort(1, lists:map(fun process_config_schema_item/1, Items)).
267
268 process_config_schema_item(#{field := FieldName} = FieldSpec) ->
269
:-(
InternalKey = maps:get(internal_key, FieldSpec, binary_to_atom(FieldName)),
270
:-(
FieldTypes = schema_field_types(),
271
:-(
case [K || K <- maps:keys(FieldTypes), maps:is_key(K, FieldSpec)] of
272 [Key] ->
273
:-(
{FieldName, maps:get(Key, FieldSpec), InternalKey, maps:get(Key, FieldTypes)};
274 _ ->
275
:-(
error(#{what => invalid_schema_field_specification, field_spec => FieldSpec})
276 end.
277
278 schema_field_types() ->
279
:-(
#{string_value => binary, integer_value => integer, float_value => float}.
280
281 -spec hooks(mongooseim:host_type()) -> gen_hook:hook_list().
282 hooks(HostType) ->
283 142 Codec = host_type_to_codec(HostType),
284 142 Roster = gen_mod:get_module_opt(HostType, ?MODULE, rooms_in_rosters),
285 [{is_muc_room_owner, HostType, fun ?MODULE:is_muc_room_owner/3, #{}, 50},
286 {can_access_room, HostType, fun ?MODULE:can_access_room/3, #{}, 50},
287 {acc_room_affiliations, HostType, fun ?MODULE:acc_room_affiliations/3, #{}, 50},
288 {room_exists, HostType, fun ?MODULE:room_exists/3, #{}, 50},
289 {can_access_identity, HostType, fun ?MODULE:can_access_identity/3, #{}, 50},
290 %% Prevent sending service-unavailable on groupchat messages
291 {offline_groupchat_message_hook, HostType, fun ?MODULE:prevent_service_unavailable/3, #{}, 90},
292 {remove_user, HostType, fun ?MODULE:remove_user/3, #{}, 50},
293 {remove_domain, HostType, fun ?MODULE:remove_domain/3, #{}, 50},
294 142 {disco_local_items, HostType, fun ?MODULE:disco_local_items/3, #{}, 50}] ++
295 case Codec of
296 legacy ->
297 18 [{privacy_iq_get, HostType, fun ?MODULE:process_iq_get/3, #{}, 1},
298 {privacy_iq_set, HostType, fun ?MODULE:process_iq_set/3, #{}, 1}];
299 _ ->
300 124 []
301 end ++
302 case Roster of
303 74 false -> [];
304 68 true -> [{roster_get, HostType, fun ?MODULE:add_rooms_to_roster/3, #{}, 50}]
305 end.
306
307 %%====================================================================
308 %% Routing
309 %%====================================================================
310
311 -spec process_packet(Acc :: mongoose_acc:t(), From ::jid:jid(), To ::jid:jid(),
312 El :: exml:element(), Extra :: gen_hook:extra()) -> mongoose_acc:t().
313 process_packet(Acc, From, To, El, _Extra) ->
314 825 HostType = mod_muc_light_utils:acc_to_host_type(Acc),
315 825 DecodedPacket = mod_muc_light_codec_backend:decode(From, To, El, Acc),
316 825 process_decoded_packet(HostType, From, To, Acc, El, DecodedPacket).
317
318 -spec process_decoded_packet(
319 HostType :: host_type(),
320 From :: jid:jid(), To :: jid:jid(),
321 Acc :: mongoose_acc:t(),
322 OrigPacket :: exml:element(),
323 DecodedPacket :: mod_muc_light_codec_backend:decode_result()) ->
324 mongoose_acc:t().
325 process_decoded_packet(HostType, From, To, Acc, El,
326 {ok, {set, #create{} = Create}}) ->
327 137 case not mod_muc_light_utils:room_limit_reached(From, HostType) of
328 136 true -> create_room(Acc, From, To, Create, El);
329 1 false -> make_err(From, To, El, Acc, {error, room_limit_reached})
330 end;
331 process_decoded_packet(_HostType, From, To, Acc, _El,
332 {ok, {get, #disco_info{} = DI}}) ->
333 8 handle_disco_info_get(From, To, DI, Acc);
334 process_decoded_packet(HostType, From, To, Acc, El,
335 {ok, {get, #disco_items{} = DI}}) ->
336 21 handle_disco_items_get(HostType, Acc, From, To, DI, El);
337 process_decoded_packet(HostType, From, To, Acc, El,
338 {ok, {_, #blocking{}} = Blocking}) ->
339 31 case gen_mod:get_module_opt(HostType, ?MODULE, blocking) of
340 true ->
341 27 case handle_blocking(Acc, From, To, Blocking) of
342
:-(
{error, _} = Res -> make_err(From, To, El, Acc, Res);
343 27 _ -> Acc
344 end;
345 4 false -> make_err(From, To, El, Acc, {error, blocking_disabled})
346 end;
347 process_decoded_packet(_HostType, From, To, Acc, El,
348 {ok, #iq{} = IQ}) ->
349 36 case mod_muc_iq:process_iq(To#jid.lserver, From, To, Acc, IQ) of
350 {Acc1, error} ->
351
:-(
E = {error, {feature_not_implemented, <<"mod_muc_iq returns error">>}},
352
:-(
make_err(From, To, El, Acc1, E);
353 36 _ -> Acc
354 end;
355 process_decoded_packet(HostType, From, To, Acc, El,
356 {ok, RequestToRoom})
357 when To#jid.luser =/= <<>> ->
358 585 case mongoose_hooks:room_exists(HostType, To) of
359 585 true -> mod_muc_light_room:handle_request(From, To, El, RequestToRoom, Acc);
360
:-(
false -> make_err(From, To, El, Acc, {error, item_not_found})
361 end;
362 process_decoded_packet(_HostType, From, To, Acc, El,
363 {error, _} = Err) ->
364 4 make_err(From, To, El, Acc, Err);
365 process_decoded_packet(_HostType, _From, _To, Acc, _El, ignore) ->
366 3 Acc;
367 process_decoded_packet(_HostType, From, To, Acc, El, InvalidReq) ->
368
:-(
?LOG_WARNING(#{what => muc_light_invalid_request,
369
:-(
acc => Acc, reason => InvalidReq}),
370
:-(
make_err(From, To, El, Acc, {error, bad_request}).
371
372 make_err(From, To, El, Acc, Reason) ->
373 9 mod_muc_light_codec_backend:encode_error(Reason, From, To, El, Acc).
374
375 %%====================================================================
376 %% Hook handlers
377 %%====================================================================
378
379 -spec prevent_service_unavailable(Acc, Params, Extra) -> {ok | stop, Acc} when
380 Acc :: mongoose_acc:t(),
381 Params :: map(),
382 Extra :: gen_hook:extra().
383 prevent_service_unavailable(Acc, #{packet := Packet}, _Extra) ->
384 286 case xml:get_tag_attr_s(<<"type">>, Packet) of
385 286 <<"groupchat">> -> {stop, Acc};
386
:-(
_Type -> {ok, Acc}
387 end.
388
389 -spec disco_local_items(Acc, Params, Extra) -> {ok, Acc} when
390 Acc :: mongoose_acc:t(),
391 Params :: map(),
392 Extra :: gen_hook:extra().
393 disco_local_items(Acc = #{host_type := HostType,
394 to_jid := #jid{lserver = ServerHost},
395 node := <<>>},
396 _Params,
397 _Extra) ->
398 2 XMLNS = case legacy_mode(HostType) of
399 1 true -> ?NS_MUC;
400 1 false -> ?NS_MUC_LIGHT
401 end,
402 2 MUCHost = server_host_to_muc_host(HostType, ServerHost),
403 2 Items = [#{jid => MUCHost, node => XMLNS}],
404 2 {ok, mongoose_disco:add_items(Items, Acc)};
405 disco_local_items(Acc, _Params, _Extra) ->
406
:-(
{ok, Acc}.
407
408 legacy_mode(HostType) ->
409 2 gen_mod:get_module_opt(HostType, ?MODULE, legacy_mode).
410
411 -spec remove_user(Acc, Params, Extra) -> {ok, Acc} when
412 Acc :: mongoose_acc:t(),
413 Params :: map(),
414 Extra :: gen_hook:extra().
415 remove_user(Acc, #{jid := UserJid}, #{host_type := HostType}) ->
416 951 Version = mongoose_bin:gen_from_timestamp(),
417 951 case mod_muc_light_db_backend:remove_user(HostType, jid:to_lus(UserJid), Version) of
418 {error, Reason} ->
419
:-(
?LOG_ERROR(#{what => muc_remove_user_failed,
420
:-(
reason => Reason, acc => Acc}),
421
:-(
{ok, Acc};
422 AffectedRooms ->
423 951 bcast_removed_user(Acc, UserJid, AffectedRooms, Version),
424 951 maybe_forget_rooms(Acc, AffectedRooms, Version),
425 951 {ok, Acc}
426 end.
427
428 -spec remove_domain(Acc, Params, Extra) -> {ok | stop, Acc} when
429 Acc :: mongoose_domain_api:remove_domain_acc(),
430 Params :: map(),
431 Extra :: gen_hook:extra().
432 remove_domain(Acc, #{domain := Domain}, #{host_type := HostType}) ->
433 7 F = fun() ->
434 7 MUCHost = server_host_to_muc_host(HostType, Domain),
435 7 mod_muc_light_db_backend:remove_domain(HostType, MUCHost, Domain),
436 7 Acc
437 end,
438 7 mongoose_domain_api:remove_domain_wrapper(Acc, F, ?MODULE).
439
440 -spec add_rooms_to_roster(Acc, Params, Extra) -> {ok, Acc} when
441 Acc :: [mod_roster:roster()],
442 Params :: #{jid := jid:jid()},
443 Extra :: gen_hook:extra().
444 add_rooms_to_roster(Items, #{jid := UserJID}, #{host_type := HostType}) ->
445 16 RoomList = mod_muc_light_db_backend:get_user_rooms(HostType, jid:to_lus(UserJID), undefined),
446 16 Info = get_rooms_info(HostType, lists:sort(RoomList)),
447 16 NewItems = [make_roster_item(Item) || Item <- Info],
448 16 {ok, NewItems ++ Items}.
449
450 make_roster_item({{RoomU, RoomS}, RoomName, RoomVersion}) ->
451 5 JID = jid:make_noprep(RoomU, RoomS, <<>>),
452 5 VerEl = #xmlel{ name = <<"version">>,
453 children = [#xmlcdata{ content = RoomVersion }] },
454 5 #roster{usj = {jid:to_lus(JID), jid:to_lower(JID)},
455 us = jid:to_lus(JID),
456 jid = jid:to_lower(JID),
457 name = RoomName,
458 subscription = to,
459 groups = [?NS_MUC_LIGHT],
460 xs = [VerEl] }.
461
462 -spec process_iq_get(Acc, Params, Extra) -> {ok | stop, Acc} when
463 Acc :: mongoose_acc:t(),
464 Params :: map(),
465 Extra :: gen_hook:extra().
466 process_iq_get(Acc, #{from := #jid{lserver = FromS} = From, to := To, iq := IQ}, #{host_type := HostType}) ->
467
:-(
MUCHost = server_host_to_muc_host(HostType, FromS),
468
:-(
case {mod_muc_light_codec_backend:decode(From, To, IQ, Acc),
469 gen_mod:get_module_opt(HostType, ?MODULE, blocking)} of
470 {{ok, {get, #blocking{} = Blocking}}, true} ->
471
:-(
Items = mod_muc_light_db_backend:get_blocking(HostType, jid:to_lus(From), MUCHost),
472
:-(
mod_muc_light_codec_backend:encode(
473 {get, Blocking#blocking{ items = Items }}, From, To,
474
:-(
fun(_, _, Packet) -> put(encode_res, Packet) end,
475 Acc),
476
:-(
#xmlel{ children = ResponseChildren } = erase(encode_res),
477
:-(
Result = {result, ResponseChildren},
478
:-(
{stop, mongoose_acc:set(hook, result, Result, Acc)};
479 {{ok, {get, #blocking{}}}, false} ->
480
:-(
Result = {error, mongoose_xmpp_errors:bad_request()},
481
:-(
{stop, mongoose_acc:set(hook, result, Result, Acc)};
482 _ ->
483
:-(
Result = {error, mongoose_xmpp_errors:bad_request()},
484
:-(
{ok, mongoose_acc:set(hook, result, Result, Acc)}
485 end.
486
487 %% Blocking is done using your local domain
488 -spec process_iq_set(Acc, Params, Extra) -> {ok | stop, Acc} when
489 Acc :: mongoose_acc:t(),
490 Params :: map(),
491 Extra :: gen_hook:extra().
492 process_iq_set(Acc, #{from := #jid{ lserver = FromS } = From, to := To, iq := IQ}, _Extra) ->
493
:-(
HostType = mod_muc_light_utils:acc_to_host_type(Acc),
494
:-(
MUCHost = server_host_to_muc_host(HostType, FromS),
495
:-(
case {mod_muc_light_codec_backend:decode(From, To, IQ, Acc),
496 gen_mod:get_module_opt(HostType, ?MODULE, blocking)} of
497 {{ok, {set, #blocking{ items = Items }} = Blocking}, true} ->
498
:-(
RouteFun = fun(_, _, Packet) -> put(encode_res, Packet) end,
499
:-(
ConditionFun = fun({_, _, {WhoU, WhoS}}) -> WhoU =:= <<>> orelse WhoS =:= <<>> end,
500
:-(
case lists:any(ConditionFun, Items) of
501 true ->
502
:-(
{stop, mongoose_acc:set(hook, result,
503 {error, mongoose_xmpp_errors:bad_request()}, Acc)};
504 false ->
505
:-(
ok = mod_muc_light_db_backend:set_blocking(HostType, jid:to_lus(From), MUCHost, Items),
506
:-(
mod_muc_light_codec_backend:encode(Blocking, From, To, RouteFun, Acc),
507
:-(
#xmlel{ children = ResponseChildren } = erase(encode_res),
508
:-(
{stop, mongoose_acc:set(hook, result, {result, ResponseChildren}, Acc)}
509 end;
510 {{ok, {set, #blocking{}}}, false} ->
511
:-(
{stop, mongoose_acc:set(hook, result,
512 {error, mongoose_xmpp_errors:bad_request()}, Acc)};
513 _ ->
514
:-(
{ok, mongoose_acc:set(hook, result, {error, mongoose_xmpp_errors:bad_request()}, Acc)}
515 end.
516
517 -spec is_muc_room_owner(Acc, Params, Extra) -> {ok, Acc} when
518 Acc :: boolean(),
519 Params :: map(),
520 Extra :: gen_hook:extra().
521 is_muc_room_owner(true, _Params, _Extra) ->
522
:-(
{ok, true};
523 is_muc_room_owner(_, #{acc := Acc, room := Room, user := User}, _Extra) ->
524
:-(
{ok, owner == get_affiliation(Acc, Room, User)}.
525
526 -spec can_access_room(Acc, Params, Extra) -> {ok, Acc} when
527 Acc :: boolean(),
528 Params :: map(),
529 Extra :: gen_hook:extra().
530 can_access_room(true, _Params, _Extra) ->
531
:-(
{ok, true};
532 can_access_room(_, #{acc := Acc, room := Room, user := User}, _Extra) ->
533 53 {ok, none =/= get_affiliation(Acc, Room, User)}.
534
535 -spec acc_room_affiliations(Acc, Params, Extra) -> {ok, Acc} when
536 Acc :: mongoose_acc:t(),
537 Params :: map(),
538 Extra :: gen_hook:extra().
539 acc_room_affiliations(Acc1, #{room := RoomJid}, _Extra) ->
540 663 case get_room_affs_from_acc(Acc1, RoomJid) of
541 {error, _} ->
542 633 HostType = mongoose_acc:host_type(Acc1),
543 633 case mod_muc_light_db_backend:get_aff_users(HostType, jid:to_lus(RoomJid)) of
544 {error, not_exists} ->
545 5 {ok, Acc1};
546 {ok, _Affs, _Version} = Res ->
547 628 {ok, set_room_affs_from_acc(Acc1, RoomJid, Res)}
548 end;
549 _Affs ->
550 30 {ok, Acc1}
551 end.
552
553 -spec room_exists(Acc, Params, Extra) -> {ok, Acc} when
554 Acc :: boolean(),
555 Params :: map(),
556 Extra :: gen_hook:extra().
557 room_exists(_, #{room := RoomJid}, #{host_type := HostType}) ->
558 555 {ok, mod_muc_light_db_backend:room_exists(HostType, jid:to_lus(RoomJid))}.
559
560 -spec get_acc_room_affiliations(mongoose_acc:t(), jid:jid()) ->
561 {mongoose_acc:t(), versioned_affs() | {error, not_exists}}.
562 get_acc_room_affiliations(Acc1, RoomJid) ->
563 663 case get_room_affs_from_acc(Acc1, RoomJid) of
564 {error, not_exists} ->
565 663 Acc2 = mongoose_hooks:acc_room_affiliations(Acc1, RoomJid),
566 663 {Acc2, get_room_affs_from_acc(Acc2, RoomJid)};
567 Res ->
568
:-(
{Acc1, Res}
569 end.
570
571 -spec get_room_affs_from_acc(mongoose_acc:t(), jid:jid()) -> versioned_affs() | {error, not_exists}.
572 get_room_affs_from_acc(Acc, RoomJid) ->
573 2105 mongoose_acc:get(?MODULE, {affiliations, RoomJid}, {error, not_exists}, Acc).
574
575 -spec set_room_affs_from_acc(mongoose_acc:t(), jid:jid(), versioned_affs()) -> mongoose_acc:t().
576 set_room_affs_from_acc(Acc, RoomJid, Affs) ->
577 658 mongoose_acc:set(?MODULE, {affiliations, RoomJid}, Affs, Acc).
578
579 -spec can_access_identity(Acc, Params, Extra) -> {ok, Acc} when
580 Acc :: boolean(),
581 Params :: map(),
582 Extra :: gen_hook:extra().
583 can_access_identity(true, _Params, _Extra) ->
584
:-(
{ok, true};
585 can_access_identity(_Acc, _Params, _Extra) ->
586 %% User JIDs are explicit in MUC Light but this hook is about appending
587 %% 0045 MUC element with user identity and we don't want it
588 36 {ok, false}.
589
590 %%====================================================================
591 %% Internal functions
592 %%====================================================================
593
594 prepare_config(HostType, RawConfig) ->
595 278 Schema = config_schema_for_host_type(HostType),
596 278 mod_muc_light_room_config:from_binary_kv(RawConfig, Schema).
597
598 prepare_affs(HostType, CreatorJid, RoomUS, #create{aff_users = AffUsers}) ->
599 278 CreatorUS = jid:to_lus(CreatorJid),
600 278 InitialAffUsers = mod_muc_light_utils:filter_out_prevented(HostType,
601 CreatorUS, RoomUS, AffUsers),
602 278 Res = process_create_aff_users_if_valid(HostType, CreatorUS, InitialAffUsers),
603 278 MaxOccupants = max_occupants(HostType),
604 278 case Res of
605 {ok, FinalAffUsers} when length(FinalAffUsers) > MaxOccupants ->
606 1 {error, max_occupants_reached};
607 _ ->
608 277 Res
609 end.
610
611 max_occupants(HostType) ->
612 278 gen_mod:get_module_opt(HostType, ?MODULE, max_occupants).
613
614 get_affiliation(Acc, Room, User) ->
615 53 case get_acc_room_affiliations(Acc, Room) of
616 {_, {ok, AffUsers, _}} ->
617 53 case lists:keyfind(jid:to_lus(User), 1, AffUsers) of
618 53 {_, Aff} -> Aff;
619
:-(
_ -> none
620 end;
621 _ ->
622
:-(
none
623 end.
624
625 -spec create_room(mongoose_acc:t(),
626 jid:jid(),
627 jid:jid(),
628 create_req_props(),
629 exml:element()) ->
630 mongoose_acc:t().
631 create_room(Acc, From, To, Create0, OrigPacket) ->
632 136 case try_to_create_room(From, To, Create0) of
633 {ok, FinalRoomJid, Details} ->
634 133 mod_muc_light_codec_backend:encode({set, Details, To#jid.luser == <<>>}, From,
635 FinalRoomJid, make_handler_fun(Acc), Acc);
636 {error, exists} ->
637 2 mod_muc_light_codec_backend:encode_error({error, {conflict, <<"Room already exists">>}},
638 From, To, OrigPacket,
639 Acc);
640 {error, bad_request} ->
641
:-(
mod_muc_light_codec_backend:encode_error({error, bad_request}, From, To, OrigPacket,
642 Acc);
643 {error, {_, _} = Error} ->
644
:-(
ErrorText = io_lib:format("~s:~p", tuple_to_list(Error)),
645
:-(
mod_muc_light_codec_backend:encode_error(
646 {error, bad_request, ErrorText}, From, To, OrigPacket, Acc);
647 {error, Error} ->
648 1 ErrorText = io_lib:format("~p", [Error]),
649 1 mod_muc_light_codec_backend:encode_error(
650 {error, bad_request, ErrorText}, From, To, OrigPacket, Acc)
651 end.
652
653 -spec process_create_aff_users_if_valid(HostType :: host_type(),
654 Creator :: jid:simple_bare_jid(),
655 AffUsers :: aff_users()) ->
656 {ok, aff_users()} | {error, bad_request}.
657 process_create_aff_users_if_valid(HostType, Creator, AffUsers) ->
658 278 case lists:any(fun ({User, _}) when User =:= Creator -> true;
659 172 ({_, Aff}) -> Aff =:= none end, AffUsers) of
660 false ->
661 278 process_create_aff_users(Creator, AffUsers, equal_occupants(HostType));
662 true ->
663
:-(
{error, bad_request}
664 end.
665
666 equal_occupants(HostType) ->
667 278 gen_mod:get_module_opt(HostType, ?MODULE, equal_occupants).
668
669 -spec process_create_aff_users(Creator :: jid:simple_bare_jid(), AffUsers :: aff_users(),
670 EqualOccupants :: boolean()) ->
671 {ok, aff_users()} | {error, bad_request}.
672 process_create_aff_users(Creator, AffUsers, EqualOccupants) ->
673 278 case mod_muc_light_utils:change_aff_users([{Creator, creator_aff(EqualOccupants)}], AffUsers) of
674 278 {ok, FinalAffUsers, _ChangedAffUsers, _JoiningUsers, _LeavingUsers} -> {ok, FinalAffUsers};
675
:-(
Error -> Error
676 end.
677
678 -spec creator_aff(EqualOccupants :: boolean()) -> owner | member.
679 2 creator_aff(true) -> member;
680 276 creator_aff(false) -> owner.
681
682 -spec handle_disco_info_get(From ::jid:jid(),
683 To :: jid:jid(),
684 DiscoInfo :: disco_info_req_props(),
685 Acc :: mongoose_acc:t()) ->
686 mongoose_acc:t().
687 handle_disco_info_get(From, To, DiscoInfo, Acc) ->
688 8 mod_muc_light_codec_backend:encode({get, DiscoInfo}, From, To,
689 make_handler_fun(Acc), Acc).
690
691 -spec handle_disco_items_get(HostType :: host_type(),
692 Acc :: mongoose_acc:t(),
693 From ::jid:jid(), To ::jid:jid(),
694 DiscoItems :: disco_items_req_props(),
695 OrigPacket :: exml:element()) ->
696 mongoose_acc:t().
697 handle_disco_items_get(HostType, Acc, From, To, DiscoItems0, OrigPacket) ->
698 21 case catch mod_muc_light_db_backend:get_user_rooms(HostType, jid:to_lus(From), To#jid.lserver) of
699 {error, Error} ->
700
:-(
?LOG_ERROR(#{what => muc_get_user_rooms_failed,
701 text => <<"Couldn't get room list for user">>,
702
:-(
from_jid => From, reason => Error}),
703
:-(
mod_muc_light_codec_backend:encode_error(
704 {error, internal_server_error}, From, To, OrigPacket, Acc);
705 Rooms ->
706 21 RoomsInfo = get_rooms_info(HostType, lists:sort(Rooms)),
707 21 RouteFun = make_handler_fun(Acc),
708 21 RoomsPerPage = gen_mod:get_module_opt(HostType, ?MODULE, rooms_per_page),
709 21 case apply_rsm(RoomsInfo, length(RoomsInfo),
710 page_service_limit(DiscoItems0#disco_items.rsm, RoomsPerPage)) of
711 {ok, RoomsInfoSlice, RSMOut} ->
712 19 DiscoItems = DiscoItems0#disco_items{ rooms = RoomsInfoSlice, rsm = RSMOut },
713 19 mod_muc_light_codec_backend:encode({get, DiscoItems},
714 From, To,
715 RouteFun, Acc);
716 {error, item_not_found} ->
717 2 mod_muc_light_codec_backend:encode_error({error, item_not_found},
718 From, To, OrigPacket, Acc)
719 end
720 end.
721
722 -spec get_rooms_info(HostType :: mongooseim:host_type(),
723 Rooms :: [jid:simple_bare_jid()]) -> [disco_room_info()].
724 get_rooms_info(_HostType, []) ->
725 37 [];
726 get_rooms_info(HostType, [{RoomU, _} = RoomUS | RRooms]) ->
727 27 {ok, Config, Version} = mod_muc_light_db_backend:get_config(HostType, RoomUS),
728 27 RoomName = case lists:keyfind(roomname, 1, Config) of
729 2 false -> RoomU;
730 25 {_, RoomName0} -> RoomName0
731 end,
732 27 [{RoomUS, RoomName, Version} | get_rooms_info(HostType, RRooms)].
733
734 -spec apply_rsm(RoomsInfo :: [disco_room_info()], RoomsInfoLen :: non_neg_integer(),
735 RSMIn :: jlib:rsm_in()) ->
736 {ok, RoomsInfoSlice :: [disco_room_info()], RSMOut :: jlib:rsm_out()} | {error, item_not_found}.
737 apply_rsm(RoomsInfo, _RoomsInfoLen, none) ->
738 2 {ok, RoomsInfo, none};
739 apply_rsm(_RoomsInfo, _RoomsInfoLen, #rsm_in{ max = Max }) when Max < 0 ->
740
:-(
{error, item_not_found};
741 apply_rsm(_RoomsInfo, RoomsInfoLen, #rsm_in{ max = 0 }) ->
742
:-(
{ok, [], #rsm_out{ count = RoomsInfoLen }};
743 apply_rsm([], 0, #rsm_in{ direction = undefined, id = undefined, index = undefined } = _RSMIn) ->
744 4 {ok, [], none};
745 apply_rsm(RoomsInfo, RoomsInfoLen, #rsm_in{ direction = undefined, id = undefined,
746 index = undefined } = RSMIn) ->
747 11 apply_rsm(RoomsInfo, RoomsInfoLen, RSMIn#rsm_in{ index = 0 });
748 apply_rsm(RoomsInfo, RoomsInfoLen, #rsm_in{ direction = before, id = <<>>, max = Max }) ->
749 2 apply_rsm(RoomsInfo, RoomsInfoLen, #rsm_in{ max = Max, index = RoomsInfoLen - Max });
750 apply_rsm(RoomsInfo, RoomsInfoLen, #rsm_in{ index = undefined, direction = Direction,
751 id = RoomUSBin, max = Max }) ->
752 2 case find_room_pos(RoomUSBin, RoomsInfo) of
753 {error, item_not_found} ->
754 2 {error, item_not_found};
755 RoomPos ->
756
:-(
FirstPos = case {Direction, RoomPos - Max} of
757
:-(
{aft, _} -> RoomPos + 1;
758
:-(
{before, TooLow} when TooLow < 1 -> 1;
759
:-(
{before, FirstPos0} -> FirstPos0
760 end,
761
:-(
[{FirstRoomUS, _, _} | _] = RoomsInfoSlice = lists:sublist(RoomsInfo, FirstPos, Max),
762
:-(
{LastRoomUS, _, _} = lists:last(RoomsInfoSlice),
763
:-(
{ok, RoomsInfoSlice, #rsm_out{ count = RoomsInfoLen,
764 index = FirstPos - 1,
765 first = jid:to_binary(FirstRoomUS),
766 last = jid:to_binary(LastRoomUS) }}
767 end;
768 apply_rsm(RoomsInfo, RoomsInfoLen, #rsm_in{ max = Max, index = Index}) when Index < RoomsInfoLen ->
769 13 [{FirstRoomUS, _, _} | _] = RoomsInfoSlice = lists:sublist(RoomsInfo, Index + 1, Max),
770 13 {LastRoomUS, _, _} = lists:last(RoomsInfoSlice),
771 13 {ok, RoomsInfoSlice, #rsm_out{ count = RoomsInfoLen,
772 index = Index,
773 first = jid:to_binary(FirstRoomUS),
774 last = jid:to_binary(LastRoomUS) }};
775 apply_rsm(_RoomsInfo, _RoomsInfoLen, _RSMIn) ->
776
:-(
{error, item_not_found}.
777
778 -spec page_service_limit(RSMIn :: jlib:rsm_in() | undefined, ServiceMax :: integer()) ->
779 jlib:rsm_in() | none.
780 2 page_service_limit(none, infinity) -> none;
781 15 page_service_limit(none, ServiceMax) -> #rsm_in{ max = ServiceMax };
782
:-(
page_service_limit(#rsm_in{ max = Max } = RSMIn, ServiceMax) when Max =< ServiceMax -> RSMIn;
783 4 page_service_limit(RSMIn, ServiceMax) -> RSMIn#rsm_in{ max = ServiceMax }.
784
785 -spec find_room_pos(RoomUSBin :: binary(), RoomsInfo :: [disco_room_info()]) ->
786 pos_integer() | {error, item_not_found}.
787 find_room_pos(RoomUSBin, RoomsInfo) ->
788 2 case jid:from_binary(RoomUSBin) of
789
:-(
error -> {error, item_not_found};
790 2 #jid{ luser = RoomU, lserver = RoomS } -> find_room_pos({RoomU, RoomS}, RoomsInfo, 1)
791 end.
792
793 -spec find_room_pos(RoomUS :: jid:simple_bare_jid(), RoomsInfo :: [disco_room_info()],
794 Pos :: pos_integer()) -> pos_integer() | {error, item_not_found}.
795
:-(
find_room_pos(RoomUS, [{RoomUS, _, _} | _], Pos) -> Pos;
796 4 find_room_pos(RoomUS, [_ | RRooms], Pos) -> find_room_pos(RoomUS, RRooms, Pos + 1);
797 2 find_room_pos(_, [], _) -> {error, item_not_found}.
798
799 -spec handle_blocking(Acc :: mongoose_acc:t(), From :: jid:jid(), To :: jid:jid(),
800 BlockingReq :: {get | set, blocking_req_props()}) ->
801 {error, bad_request} | ok.
802 handle_blocking(Acc, From, To, {get, #blocking{} = Blocking}) ->
803 6 HostType = mongoose_acc:host_type(Acc),
804 6 BlockingItems = mod_muc_light_db_backend:get_blocking(HostType, jid:to_lus(From), To#jid.lserver),
805 6 mod_muc_light_codec_backend:encode({get, Blocking#blocking{ items = BlockingItems }},
806 From, To, make_handler_fun(Acc), Acc);
807 handle_blocking(Acc, From, To, {set, #blocking{ items = Items }} = BlockingReq) ->
808 21 case lists:any(fun({_, _, {WhoU, WhoS}}) -> WhoU =:= <<>> orelse WhoS =:= <<>> end, Items) of
809 true ->
810
:-(
{error, bad_request};
811 false ->
812 21 HostType = mongoose_acc:host_type(Acc),
813 21 ok = mod_muc_light_db_backend:set_blocking(HostType, jid:to_lus(From), To#jid.lserver, Items),
814 21 mod_muc_light_codec_backend:encode(
815 BlockingReq, From, To, make_handler_fun(Acc), Acc),
816 21 ok
817 end.
818
819 -spec bcast_removed_user(Acc :: mongoose_acc:t(), UserJid :: jid:jid(),
820 AffectedRooms :: mod_muc_light_db_backend:remove_user_return(),
821 Version :: binary()) -> ok.
822 bcast_removed_user(Acc, UserJid, AffectedRooms, Version) ->
823 951 bcast_removed_user(Acc, UserJid, AffectedRooms,
824 Version, mongoose_bin:gen_from_timestamp()).
825
826 -spec bcast_removed_user(Acc :: mongoose_acc:t(), UserJID :: jid:jid(),
827 AffectedRooms :: mod_muc_light_db_backend:remove_user_return(),
828 Version :: binary(),
829 PacketID :: binary()) -> ok.
830 bcast_removed_user(_Acc, _UserJID, [], _Version, _ID) ->
831 951 ok;
832 bcast_removed_user(Acc, UserJID,
833 [{{RoomU, RoomS}, {ok, OldAffUsers, NewAffUsers, AffUsersChanged, PrevVersion}}
834 | RAffected], Version, ID) ->
835 221 Affiliations = #affiliations{
836 id = ID,
837 prev_version = PrevVersion,
838 version = Version,
839 aff_users = AffUsersChanged
840 },
841 221 Cmd = {set, Affiliations, OldAffUsers, NewAffUsers},
842 221 RoomJid = jid:make_noprep(RoomU, RoomS, <<>>),
843 221 mod_muc_light_codec_backend:encode(Cmd, UserJID, RoomJid, make_handler_fun(Acc), Acc),
844 221 bcast_removed_user(Acc, UserJID, RAffected, Version, ID);
845 bcast_removed_user(Acc, UserJID, [{{RoomU, RoomS} = _RoomUS, Error} | RAffected], Version, ID) ->
846
:-(
?LOG_ERROR(#{what => muc_remove_user_failed,
847 user_jid => jid:to_binary(UserJID), room => RoomU, sub_host => RoomS,
848
:-(
reason => Error}),
849
:-(
bcast_removed_user(Acc, UserJID, RAffected, Version, ID).
850
851 -spec maybe_forget_rooms(Acc :: mongoose_acc:t(),
852 AffectedRooms :: mod_muc_light_db_backend:remove_user_return(),
853 Version :: binary()) -> ok.
854 maybe_forget_rooms(_Acc, [], _) ->
855 951 ok;
856 maybe_forget_rooms(Acc, [{RoomUS, {ok, _, NewAffUsers, _, _}} | RAffectedRooms], Version) ->
857 221 mod_muc_light_room:maybe_forget(Acc, RoomUS, NewAffUsers, Version),
858 221 maybe_forget_rooms(Acc, RAffectedRooms, Version).
859
860 make_handler_fun(Acc) ->
861 410 fun(From, To, Packet) -> ejabberd_router:route(From, To, Acc, Packet) end.
862
863 -spec config_metrics(mongooseim:host_type()) -> [{gen_mod:opt_key(), gen_mod:opt_value()}].
864 config_metrics(HostType) ->
865 30 mongoose_module_metrics:opts_for_module(HostType, ?MODULE, [backend]).
Line Hits Source