./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 150 [{<<"roomname">>, <<"Untitled">>, roomname, binary},
95 {<<"subject">>, <<>>, subject, binary}].
96
97 -spec default_host() -> mongoose_subdomain_utils:subdomain_pattern().
98 default_host() ->
99 146 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 461 HostType = mod_muc_light_utils:muc_host_to_host_type(MUCServer),
104 461 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 711 gen_mod:get_module_opt(HostType, ?MODULE, config_schema).
111
112 force_clear_from_ct(HostType) ->
113 103 catch mod_muc_light_cache:force_clear(HostType),
114 103 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 250 RoomUS = jid:to_lus(RoomJID),
126 250 HostType = mod_muc_light_utils:room_jid_to_host_type(RoomJID),
127 250 CfgRes = prepare_config(HostType, RawConfig),
128 250 AffRes = prepare_affs(HostType, CreatorJid, RoomUS, CreationCfg),
129 250 case {CfgRes, AffRes} of
130 {{ok, Config0}, {ok, FinalAffUsers}} ->
131 248 Version = mongoose_bin:gen_from_timestamp(),
132 248 case mod_muc_light_db_backend:create_room(
133 HostType, RoomUS, lists:sort(Config0), FinalAffUsers, Version) of
134 {ok, {FinalU, FinalS}} ->
135 240 {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 216 [{mod_muc_light_cache, CacheOpts, hard}];
194 deps(_HostType, #{}) ->
195 583 [].
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 146 #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 146 #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 752 HostType = mod_muc_light_utils:acc_to_host_type(Acc),
315 752 DecodedPacket = mod_muc_light_codec_backend:decode(From, To, El, Acc),
316 752 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 109 case not mod_muc_light_utils:room_limit_reached(From, HostType) of
328 108 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 29 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 29 _ -> Acc
354 end;
355 process_decoded_packet(HostType, From, To, Acc, El,
356 {ok, RequestToRoom})
357 when To#jid.luser =/= <<>> ->
358 547 case mongoose_hooks:room_exists(HostType, To) of
359 547 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 949 Version = mongoose_bin:gen_from_timestamp(),
417 949 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 949 bcast_removed_user(Acc, UserJid, AffectedRooms, Version),
424 949 maybe_forget_rooms(Acc, AffectedRooms, Version),
425 949 {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 :: mongoose_acc:t(),
442 Params :: map(),
443 Extra :: gen_hook:extra().
444 add_rooms_to_roster(Acc, #{jid := UserJID}, _Extra) ->
445 16 Items = mongoose_acc:get(roster, items, [], Acc),
446 16 HostType = mongoose_acc:host_type(Acc),
447 16 RoomList = mod_muc_light_db_backend:get_user_rooms(HostType, jid:to_lus(UserJID), undefined),
448 16 Info = get_rooms_info(HostType, lists:sort(RoomList)),
449 16 NewItems = [make_roster_item(Item) || Item <- Info] ++ Items,
450 16 {ok, mongoose_acc:set(roster, items, NewItems, Acc)}.
451
452 make_roster_item({{RoomU, RoomS}, RoomName, RoomVersion}) ->
453 5 JID = jid:make_noprep(RoomU, RoomS, <<>>),
454 5 VerEl = #xmlel{ name = <<"version">>,
455 children = [#xmlcdata{ content = RoomVersion }] },
456 5 #roster{usj = {RoomU, RoomS, jid:to_lower(JID)},
457 us = {RoomU, RoomS},
458 jid = jid:to_lower(JID),
459 name = RoomName,
460 subscription = to,
461 groups = [?NS_MUC_LIGHT],
462 xs = [VerEl] }.
463
464 -spec process_iq_get(Acc, Params, Extra) -> {ok | stop, Acc} when
465 Acc :: mongoose_acc:t(),
466 Params :: map(),
467 Extra :: gen_hook:extra().
468 process_iq_get(Acc, #{from := #jid{lserver = FromS} = From, to := To, iq := IQ}, #{host_type := HostType}) ->
469
:-(
MUCHost = server_host_to_muc_host(HostType, FromS),
470
:-(
case {mod_muc_light_codec_backend:decode(From, To, IQ, Acc),
471 gen_mod:get_module_opt(HostType, ?MODULE, blocking)} of
472 {{ok, {get, #blocking{} = Blocking}}, true} ->
473
:-(
Items = mod_muc_light_db_backend:get_blocking(HostType, jid:to_lus(From), MUCHost),
474
:-(
mod_muc_light_codec_backend:encode(
475 {get, Blocking#blocking{ items = Items }}, From, To,
476
:-(
fun(_, _, Packet) -> put(encode_res, Packet) end,
477 Acc),
478
:-(
#xmlel{ children = ResponseChildren } = erase(encode_res),
479
:-(
Result = {result, ResponseChildren},
480
:-(
{stop, mongoose_acc:set(hook, result, Result, Acc)};
481 {{ok, {get, #blocking{}}}, false} ->
482
:-(
Result = {error, mongoose_xmpp_errors:bad_request()},
483
:-(
{stop, mongoose_acc:set(hook, result, Result, Acc)};
484 _ ->
485
:-(
Result = {error, mongoose_xmpp_errors:bad_request()},
486
:-(
{ok, mongoose_acc:set(hook, result, Result, Acc)}
487 end.
488
489 %% Blocking is done using your local domain
490 -spec process_iq_set(Acc, Params, Extra) -> {ok | stop, Acc} when
491 Acc :: mongoose_acc:t(),
492 Params :: map(),
493 Extra :: gen_hook:extra().
494 process_iq_set(Acc, #{from := #jid{ lserver = FromS } = From, to := To, iq := IQ}, _Extra) ->
495
:-(
HostType = mod_muc_light_utils:acc_to_host_type(Acc),
496
:-(
MUCHost = server_host_to_muc_host(HostType, FromS),
497
:-(
case {mod_muc_light_codec_backend:decode(From, To, IQ, Acc),
498 gen_mod:get_module_opt(HostType, ?MODULE, blocking)} of
499 {{ok, {set, #blocking{ items = Items }} = Blocking}, true} ->
500
:-(
RouteFun = fun(_, _, Packet) -> put(encode_res, Packet) end,
501
:-(
ConditionFun = fun({_, _, {WhoU, WhoS}}) -> WhoU =:= <<>> orelse WhoS =:= <<>> end,
502
:-(
case lists:any(ConditionFun, Items) of
503 true ->
504
:-(
{stop, mongoose_acc:set(hook, result,
505 {error, mongoose_xmpp_errors:bad_request()}, Acc)};
506 false ->
507
:-(
ok = mod_muc_light_db_backend:set_blocking(HostType, jid:to_lus(From), MUCHost, Items),
508
:-(
mod_muc_light_codec_backend:encode(Blocking, From, To, RouteFun, Acc),
509
:-(
#xmlel{ children = ResponseChildren } = erase(encode_res),
510
:-(
{stop, mongoose_acc:set(hook, result, {result, ResponseChildren}, Acc)}
511 end;
512 {{ok, {set, #blocking{}}}, false} ->
513
:-(
{stop, mongoose_acc:set(hook, result,
514 {error, mongoose_xmpp_errors:bad_request()}, Acc)};
515 _ ->
516
:-(
{ok, mongoose_acc:set(hook, result, {error, mongoose_xmpp_errors:bad_request()}, Acc)}
517 end.
518
519 -spec is_muc_room_owner(Acc, Params, Extra) -> {ok, Acc} when
520 Acc :: boolean(),
521 Params :: map(),
522 Extra :: gen_hook:extra().
523 is_muc_room_owner(true, _Params, _Extra) ->
524
:-(
{ok, true};
525 is_muc_room_owner(_, #{acc := Acc, room := Room, user := User}, _Extra) ->
526
:-(
{ok, owner == get_affiliation(Acc, Room, User)}.
527
528 -spec can_access_room(Acc, Params, Extra) -> {ok, Acc} when
529 Acc :: boolean(),
530 Params :: map(),
531 Extra :: gen_hook:extra().
532 can_access_room(true, _Params, _Extra) ->
533
:-(
{ok, true};
534 can_access_room(_, #{acc := Acc, room := Room, user := User}, _Extra) ->
535 46 {ok, none =/= get_affiliation(Acc, Room, User)}.
536
537 -spec acc_room_affiliations(Acc, Params, Extra) -> {ok, Acc} when
538 Acc :: mongoose_acc:t(),
539 Params :: map(),
540 Extra :: gen_hook:extra().
541 acc_room_affiliations(Acc1, #{room := RoomJid}, _Extra) ->
542 618 case get_room_affs_from_acc(Acc1, RoomJid) of
543 {error, _} ->
544 590 HostType = mongoose_acc:host_type(Acc1),
545 590 case mod_muc_light_db_backend:get_aff_users(HostType, jid:to_lus(RoomJid)) of
546 {error, not_exists} ->
547 5 {ok, Acc1};
548 {ok, _Affs, _Version} = Res ->
549 585 {ok, set_room_affs_from_acc(Acc1, RoomJid, Res)}
550 end;
551 _Affs ->
552 28 {ok, Acc1}
553 end.
554
555 -spec room_exists(Acc, Params, Extra) -> {ok, Acc} when
556 Acc :: boolean(),
557 Params :: map(),
558 Extra :: gen_hook:extra().
559 room_exists(_, #{room := RoomJid}, #{host_type := HostType}) ->
560 519 {ok, mod_muc_light_db_backend:room_exists(HostType, jid:to_lus(RoomJid))}.
561
562 -spec get_acc_room_affiliations(mongoose_acc:t(), jid:jid()) ->
563 {mongoose_acc:t(), versioned_affs() | {error, not_exists}}.
564 get_acc_room_affiliations(Acc1, RoomJid) ->
565 618 case get_room_affs_from_acc(Acc1, RoomJid) of
566 {error, not_exists} ->
567 618 Acc2 = mongoose_hooks:acc_room_affiliations(Acc1, RoomJid),
568 618 {Acc2, get_room_affs_from_acc(Acc2, RoomJid)};
569 Res ->
570
:-(
{Acc1, Res}
571 end.
572
573 -spec get_room_affs_from_acc(mongoose_acc:t(), jid:jid()) -> versioned_affs() | {error, not_exists}.
574 get_room_affs_from_acc(Acc, RoomJid) ->
575 1964 mongoose_acc:get(?MODULE, {affiliations, RoomJid}, {error, not_exists}, Acc).
576
577 -spec set_room_affs_from_acc(mongoose_acc:t(), jid:jid(), versioned_affs()) -> mongoose_acc:t().
578 set_room_affs_from_acc(Acc, RoomJid, Affs) ->
579 613 mongoose_acc:set(?MODULE, {affiliations, RoomJid}, Affs, Acc).
580
581 -spec can_access_identity(Acc, Params, Extra) -> {ok, Acc} when
582 Acc :: boolean(),
583 Params :: map(),
584 Extra :: gen_hook:extra().
585 can_access_identity(true, _Params, _Extra) ->
586
:-(
{ok, true};
587 can_access_identity(_Acc, _Params, _Extra) ->
588 %% User JIDs are explicit in MUC Light but this hook is about appending
589 %% 0045 MUC element with user identity and we don't want it
590 29 {ok, false}.
591
592 %%====================================================================
593 %% Internal functions
594 %%====================================================================
595
596 prepare_config(HostType, RawConfig) ->
597 250 Schema = config_schema_for_host_type(HostType),
598 250 mod_muc_light_room_config:from_binary_kv(RawConfig, Schema).
599
600 prepare_affs(HostType, CreatorJid, RoomUS, #create{aff_users = AffUsers}) ->
601 250 CreatorUS = jid:to_lus(CreatorJid),
602 250 InitialAffUsers = mod_muc_light_utils:filter_out_prevented(HostType,
603 CreatorUS, RoomUS, AffUsers),
604 250 Res = process_create_aff_users_if_valid(HostType, CreatorUS, InitialAffUsers),
605 250 MaxOccupants = max_occupants(HostType),
606 250 case Res of
607 {ok, FinalAffUsers} when length(FinalAffUsers) > MaxOccupants ->
608 1 {error, max_occupants_reached};
609 _ ->
610 249 Res
611 end.
612
613 max_occupants(HostType) ->
614 250 gen_mod:get_module_opt(HostType, ?MODULE, max_occupants).
615
616 get_affiliation(Acc, Room, User) ->
617 46 case get_acc_room_affiliations(Acc, Room) of
618 {_, {ok, AffUsers, _}} ->
619 46 case lists:keyfind(jid:to_lus(User), 1, AffUsers) of
620 46 {_, Aff} -> Aff;
621
:-(
_ -> none
622 end;
623 _ ->
624
:-(
none
625 end.
626
627 -spec create_room(mongoose_acc:t(),
628 jid:jid(),
629 jid:jid(),
630 create_req_props(),
631 exml:element()) ->
632 mongoose_acc:t().
633 create_room(Acc, From, To, Create0, OrigPacket) ->
634 108 case try_to_create_room(From, To, Create0) of
635 {ok, FinalRoomJid, Details} ->
636 105 mod_muc_light_codec_backend:encode({set, Details, To#jid.luser == <<>>}, From,
637 FinalRoomJid, make_handler_fun(Acc), Acc);
638 {error, exists} ->
639 2 mod_muc_light_codec_backend:encode_error({error, {conflict, <<"Room already exists">>}},
640 From, To, OrigPacket,
641 Acc);
642 {error, bad_request} ->
643
:-(
mod_muc_light_codec_backend:encode_error({error, bad_request}, From, To, OrigPacket,
644 Acc);
645 {error, {_, _} = Error} ->
646
:-(
ErrorText = io_lib:format("~s:~p", tuple_to_list(Error)),
647
:-(
mod_muc_light_codec_backend:encode_error(
648 {error, bad_request, ErrorText}, From, To, OrigPacket, Acc);
649 {error, Error} ->
650 1 ErrorText = io_lib:format("~p", [Error]),
651 1 mod_muc_light_codec_backend:encode_error(
652 {error, bad_request, ErrorText}, From, To, OrigPacket, Acc)
653 end.
654
655 -spec process_create_aff_users_if_valid(HostType :: host_type(),
656 Creator :: jid:simple_bare_jid(),
657 AffUsers :: aff_users()) ->
658 {ok, aff_users()} | {error, bad_request}.
659 process_create_aff_users_if_valid(HostType, Creator, AffUsers) ->
660 250 case lists:any(fun ({User, _}) when User =:= Creator -> true;
661 151 ({_, Aff}) -> Aff =:= none end, AffUsers) of
662 false ->
663 250 process_create_aff_users(Creator, AffUsers, equal_occupants(HostType));
664 true ->
665
:-(
{error, bad_request}
666 end.
667
668 equal_occupants(HostType) ->
669 250 gen_mod:get_module_opt(HostType, ?MODULE, equal_occupants).
670
671 -spec process_create_aff_users(Creator :: jid:simple_bare_jid(), AffUsers :: aff_users(),
672 EqualOccupants :: boolean()) ->
673 {ok, aff_users()} | {error, bad_request}.
674 process_create_aff_users(Creator, AffUsers, EqualOccupants) ->
675 250 case mod_muc_light_utils:change_aff_users([{Creator, creator_aff(EqualOccupants)}], AffUsers) of
676 250 {ok, FinalAffUsers, _ChangedAffUsers, _JoiningUsers, _LeavingUsers} -> {ok, FinalAffUsers};
677
:-(
Error -> Error
678 end.
679
680 -spec creator_aff(EqualOccupants :: boolean()) -> owner | member.
681 2 creator_aff(true) -> member;
682 248 creator_aff(false) -> owner.
683
684 -spec handle_disco_info_get(From ::jid:jid(),
685 To :: jid:jid(),
686 DiscoInfo :: disco_info_req_props(),
687 Acc :: mongoose_acc:t()) ->
688 mongoose_acc:t().
689 handle_disco_info_get(From, To, DiscoInfo, Acc) ->
690 8 mod_muc_light_codec_backend:encode({get, DiscoInfo}, From, To,
691 make_handler_fun(Acc), Acc).
692
693 -spec handle_disco_items_get(HostType :: host_type(),
694 Acc :: mongoose_acc:t(),
695 From ::jid:jid(), To ::jid:jid(),
696 DiscoItems :: disco_items_req_props(),
697 OrigPacket :: exml:element()) ->
698 mongoose_acc:t().
699 handle_disco_items_get(HostType, Acc, From, To, DiscoItems0, OrigPacket) ->
700 21 case catch mod_muc_light_db_backend:get_user_rooms(HostType, jid:to_lus(From), To#jid.lserver) of
701 {error, Error} ->
702
:-(
?LOG_ERROR(#{what => muc_get_user_rooms_failed,
703 text => <<"Couldn't get room list for user">>,
704
:-(
from_jid => From, reason => Error}),
705
:-(
mod_muc_light_codec_backend:encode_error(
706 {error, internal_server_error}, From, To, OrigPacket, Acc);
707 Rooms ->
708 21 RoomsInfo = get_rooms_info(HostType, lists:sort(Rooms)),
709 21 RouteFun = make_handler_fun(Acc),
710 21 RoomsPerPage = gen_mod:get_module_opt(HostType, ?MODULE, rooms_per_page),
711 21 case apply_rsm(RoomsInfo, length(RoomsInfo),
712 page_service_limit(DiscoItems0#disco_items.rsm, RoomsPerPage)) of
713 {ok, RoomsInfoSlice, RSMOut} ->
714 19 DiscoItems = DiscoItems0#disco_items{ rooms = RoomsInfoSlice, rsm = RSMOut },
715 19 mod_muc_light_codec_backend:encode({get, DiscoItems},
716 From, To,
717 RouteFun, Acc);
718 {error, item_not_found} ->
719 2 mod_muc_light_codec_backend:encode_error({error, item_not_found},
720 From, To, OrigPacket, Acc)
721 end
722 end.
723
724 -spec get_rooms_info(HostType :: mongooseim:host_type(),
725 Rooms :: [jid:simple_bare_jid()]) -> [disco_room_info()].
726 get_rooms_info(_HostType, []) ->
727 37 [];
728 get_rooms_info(HostType, [{RoomU, _} = RoomUS | RRooms]) ->
729 27 {ok, Config, Version} = mod_muc_light_db_backend:get_config(HostType, RoomUS),
730 27 RoomName = case lists:keyfind(roomname, 1, Config) of
731 2 false -> RoomU;
732 25 {_, RoomName0} -> RoomName0
733 end,
734 27 [{RoomUS, RoomName, Version} | get_rooms_info(HostType, RRooms)].
735
736 -spec apply_rsm(RoomsInfo :: [disco_room_info()], RoomsInfoLen :: non_neg_integer(),
737 RSMIn :: jlib:rsm_in()) ->
738 {ok, RoomsInfoSlice :: [disco_room_info()], RSMOut :: jlib:rsm_out()} | {error, item_not_found}.
739 apply_rsm(RoomsInfo, _RoomsInfoLen, none) ->
740 2 {ok, RoomsInfo, none};
741 apply_rsm(_RoomsInfo, _RoomsInfoLen, #rsm_in{ max = Max }) when Max < 0 ->
742
:-(
{error, item_not_found};
743 apply_rsm(_RoomsInfo, RoomsInfoLen, #rsm_in{ max = 0 }) ->
744
:-(
{ok, [], #rsm_out{ count = RoomsInfoLen }};
745 apply_rsm([], 0, #rsm_in{ direction = undefined, id = undefined, index = undefined } = _RSMIn) ->
746 4 {ok, [], none};
747 apply_rsm(RoomsInfo, RoomsInfoLen, #rsm_in{ direction = undefined, id = undefined,
748 index = undefined } = RSMIn) ->
749 11 apply_rsm(RoomsInfo, RoomsInfoLen, RSMIn#rsm_in{ index = 0 });
750 apply_rsm(RoomsInfo, RoomsInfoLen, #rsm_in{ direction = before, id = <<>>, max = Max }) ->
751 2 apply_rsm(RoomsInfo, RoomsInfoLen, #rsm_in{ max = Max, index = RoomsInfoLen - Max });
752 apply_rsm(RoomsInfo, RoomsInfoLen, #rsm_in{ index = undefined, direction = Direction,
753 id = RoomUSBin, max = Max }) ->
754 2 case find_room_pos(RoomUSBin, RoomsInfo) of
755 {error, item_not_found} ->
756 2 {error, item_not_found};
757 RoomPos ->
758
:-(
FirstPos = case {Direction, RoomPos - Max} of
759
:-(
{aft, _} -> RoomPos + 1;
760
:-(
{before, TooLow} when TooLow < 1 -> 1;
761
:-(
{before, FirstPos0} -> FirstPos0
762 end,
763
:-(
[{FirstRoomUS, _, _} | _] = RoomsInfoSlice = lists:sublist(RoomsInfo, FirstPos, Max),
764
:-(
{LastRoomUS, _, _} = lists:last(RoomsInfoSlice),
765
:-(
{ok, RoomsInfoSlice, #rsm_out{ count = RoomsInfoLen,
766 index = FirstPos - 1,
767 first = jid:to_binary(FirstRoomUS),
768 last = jid:to_binary(LastRoomUS) }}
769 end;
770 apply_rsm(RoomsInfo, RoomsInfoLen, #rsm_in{ max = Max, index = Index}) when Index < RoomsInfoLen ->
771 13 [{FirstRoomUS, _, _} | _] = RoomsInfoSlice = lists:sublist(RoomsInfo, Index + 1, Max),
772 13 {LastRoomUS, _, _} = lists:last(RoomsInfoSlice),
773 13 {ok, RoomsInfoSlice, #rsm_out{ count = RoomsInfoLen,
774 index = Index,
775 first = jid:to_binary(FirstRoomUS),
776 last = jid:to_binary(LastRoomUS) }};
777 apply_rsm(_RoomsInfo, _RoomsInfoLen, _RSMIn) ->
778
:-(
{error, item_not_found}.
779
780 -spec page_service_limit(RSMIn :: jlib:rsm_in() | undefined, ServiceMax :: integer()) ->
781 jlib:rsm_in() | none.
782 2 page_service_limit(none, infinity) -> none;
783 15 page_service_limit(none, ServiceMax) -> #rsm_in{ max = ServiceMax };
784
:-(
page_service_limit(#rsm_in{ max = Max } = RSMIn, ServiceMax) when Max =< ServiceMax -> RSMIn;
785 4 page_service_limit(RSMIn, ServiceMax) -> RSMIn#rsm_in{ max = ServiceMax }.
786
787 -spec find_room_pos(RoomUSBin :: binary(), RoomsInfo :: [disco_room_info()]) ->
788 pos_integer() | {error, item_not_found}.
789 find_room_pos(RoomUSBin, RoomsInfo) ->
790 2 case jid:from_binary(RoomUSBin) of
791
:-(
error -> {error, item_not_found};
792 2 #jid{ luser = RoomU, lserver = RoomS } -> find_room_pos({RoomU, RoomS}, RoomsInfo, 1)
793 end.
794
795 -spec find_room_pos(RoomUS :: jid:simple_bare_jid(), RoomsInfo :: [disco_room_info()],
796 Pos :: pos_integer()) -> pos_integer() | {error, item_not_found}.
797
:-(
find_room_pos(RoomUS, [{RoomUS, _, _} | _], Pos) -> Pos;
798 4 find_room_pos(RoomUS, [_ | RRooms], Pos) -> find_room_pos(RoomUS, RRooms, Pos + 1);
799 2 find_room_pos(_, [], _) -> {error, item_not_found}.
800
801 -spec handle_blocking(Acc :: mongoose_acc:t(), From :: jid:jid(), To :: jid:jid(),
802 BlockingReq :: {get | set, blocking_req_props()}) ->
803 {error, bad_request} | ok.
804 handle_blocking(Acc, From, To, {get, #blocking{} = Blocking}) ->
805 6 HostType = mongoose_acc:host_type(Acc),
806 6 BlockingItems = mod_muc_light_db_backend:get_blocking(HostType, jid:to_lus(From), To#jid.lserver),
807 6 mod_muc_light_codec_backend:encode({get, Blocking#blocking{ items = BlockingItems }},
808 From, To, make_handler_fun(Acc), Acc);
809 handle_blocking(Acc, From, To, {set, #blocking{ items = Items }} = BlockingReq) ->
810 21 case lists:any(fun({_, _, {WhoU, WhoS}}) -> WhoU =:= <<>> orelse WhoS =:= <<>> end, Items) of
811 true ->
812
:-(
{error, bad_request};
813 false ->
814 21 HostType = mongoose_acc:host_type(Acc),
815 21 ok = mod_muc_light_db_backend:set_blocking(HostType, jid:to_lus(From), To#jid.lserver, Items),
816 21 mod_muc_light_codec_backend:encode(
817 BlockingReq, From, To, make_handler_fun(Acc), Acc),
818 21 ok
819 end.
820
821 -spec bcast_removed_user(Acc :: mongoose_acc:t(), UserJid :: jid:jid(),
822 AffectedRooms :: mod_muc_light_db_backend:remove_user_return(),
823 Version :: binary()) -> ok.
824 bcast_removed_user(Acc, UserJid, AffectedRooms, Version) ->
825 949 bcast_removed_user(Acc, UserJid, AffectedRooms,
826 Version, mongoose_bin:gen_from_timestamp()).
827
828 -spec bcast_removed_user(Acc :: mongoose_acc:t(), UserJID :: jid:jid(),
829 AffectedRooms :: mod_muc_light_db_backend:remove_user_return(),
830 Version :: binary(),
831 PacketID :: binary()) -> ok.
832 bcast_removed_user(_Acc, _UserJID, [], _Version, _ID) ->
833 949 ok;
834 bcast_removed_user(Acc, UserJID,
835 [{{RoomU, RoomS}, {ok, OldAffUsers, NewAffUsers, AffUsersChanged, PrevVersion}}
836 | RAffected], Version, ID) ->
837 221 Affiliations = #affiliations{
838 id = ID,
839 prev_version = PrevVersion,
840 version = Version,
841 aff_users = AffUsersChanged
842 },
843 221 Cmd = {set, Affiliations, OldAffUsers, NewAffUsers},
844 221 RoomJid = jid:make_noprep(RoomU, RoomS, <<>>),
845 221 mod_muc_light_codec_backend:encode(Cmd, UserJID, RoomJid, make_handler_fun(Acc), Acc),
846 221 bcast_removed_user(Acc, UserJID, RAffected, Version, ID);
847 bcast_removed_user(Acc, UserJID, [{{RoomU, RoomS} = _RoomUS, Error} | RAffected], Version, ID) ->
848
:-(
?LOG_ERROR(#{what => muc_remove_user_failed,
849 user_jid => jid:to_binary(UserJID), room => RoomU, sub_host => RoomS,
850
:-(
reason => Error}),
851
:-(
bcast_removed_user(Acc, UserJID, RAffected, Version, ID).
852
853 -spec maybe_forget_rooms(Acc :: mongoose_acc:t(),
854 AffectedRooms :: mod_muc_light_db_backend:remove_user_return(),
855 Version :: binary()) -> ok.
856 maybe_forget_rooms(_Acc, [], _) ->
857 949 ok;
858 maybe_forget_rooms(Acc, [{RoomUS, {ok, _, NewAffUsers, _, _}} | RAffectedRooms], Version) ->
859 221 mod_muc_light_room:maybe_forget(Acc, RoomUS, NewAffUsers, Version),
860 221 maybe_forget_rooms(Acc, RAffectedRooms, Version).
861
862 make_handler_fun(Acc) ->
863 382 fun(From, To, Packet) -> ejabberd_router:route(From, To, Acc, Packet) end.
864
865 -spec config_metrics(mongooseim:host_type()) -> [{gen_mod:opt_key(), gen_mod:opt_value()}].
866 config_metrics(HostType) ->
867 24 mongoose_module_metrics:opts_for_module(HostType, ?MODULE, [backend]).
Line Hits Source