• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

esl / MongooseIM / 16499779126

24 Jul 2025 02:03PM UTC coverage: 85.593% (-0.02%) from 85.614%
16499779126

Pull #4549

github

jacekwegr
Add TODO comments
Pull Request #4549: Support Erlang 28

50 of 68 new or added lines in 5 files covered. (73.53%)

289 existing lines in 7 files now uncovered.

28945 of 33817 relevant lines covered (85.59%)

51736.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.17
/src/muc/mod_muc.erl
1
%%%----------------------------------------------------------------------
2
%%% File    : mod_muc.erl
3
%%% Author  : Alexey Shchepin <alexey@process-one.net>
4
%%% Purpose : MUC support (XEP-0045)
5
%%% Created : 19 Mar 2003 by Alexey Shchepin <alexey@process-one.net>
6
%%%
7
%%%
8
%%% ejabberd, Copyright (C) 2002-2011   ProcessOne
9
%%%
10
%%% This program is free software; you can redistribute it and/or
11
%%% modify it under the terms of the GNU General Public License as
12
%%% published by the Free Software Foundation; either version 2 of the
13
%%% License, or (at your option) any later version.
14
%%%
15
%%% This program is distributed in the hope that it will be useful,
16
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
17
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18
%%% General Public License for more details.
19
%%%
20
%%% You should have received a copy of the GNU General Public License
21
%%% along with this program; if not, write to the Free Software
22
%%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
23
%%%
24
%%%----------------------------------------------------------------------
25

26
-module(mod_muc).
27
-author('alexey@process-one.net').
28
-xep([{xep, 45}, {version, "1.34.5"}]).
29
-xep([{xep, 249}, {version, "1.2"}]).
30
-behaviour(gen_server).
31
-behaviour(gen_mod).
32
-behaviour(mongoose_packet_handler).
33
-behaviour(mongoose_module_metrics).
34
-behaviour(mongoose_instrument_probe).
35

36
%% API
37
-export([start_link/2,
38
         start/2,
39
         stop/1,
40
         supported_features/0,
41
         config_spec/0,
42
         instrumentation/1,
43
         process_room_affiliation/1,
44
         room_destroyed/4,
45
         store_room/4,
46
         restore_room/3,
47
         forget_room/3,
48
         create_instant_room/6,
49
         broadcast_service_message/2,
50
         can_use_nick/4,
51
         room_jid_to_pid/1,
52
         get_vh_rooms/2,
53
         default_host/0]).
54
-export([server_host_to_muc_host/2]).
55

56
%% For testing purposes only
57
-export([register_room/4]).
58

59
%% gen_server callbacks
60
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
61
         terminate/2, code_change/3]).
62

63
%% packet handler callback
64
-export([process_packet/5]).
65

66
%% mongoose_instrument_probe callback
67
-export([probe/2]).
68

69
%% Hooks handlers
70
-export([is_muc_room_owner/3,
71
         can_access_room/3,
72
         remove_domain/3,
73
         acc_room_affiliations/3,
74
         can_access_identity/3,
75
         disco_local_items/3,
76
         node_cleanup_for_host_type/3]).
77

78
-export([config_metrics/1]).
79

80
-ignore_xref([broadcast_service_message/2, create_instant_room/6,
81
              register_room/4, restore_room/3, start_link/2]).
82

83
-include("mongoose.hrl").
84
-include("jlib.hrl").
85
-include("mongoose_rsm.hrl").
86
-include("mongoose_config_spec.hrl").
87
-include("mod_muc_room.hrl").
88

89
-export_type([access/0,
90
             room/0,
91
             nick/0,
92
             packet/0,
93
             role/0,
94
             affiliation/0
95
            ]).
96

97
-type role() :: moderator | participant | visitor | none.
98
-type affiliation() :: admin | owner | member | outcast | none.
99
-type room() :: binary().
100
-type nick() :: binary().
101
-type room_host() :: jid:simple_bare_jid().
102
-type packet() :: exml:element().
103
-type from_to_packet() ::
104
        {From :: jid:jid(), To :: jid:jid(), Acc :: mongoose_acc:t(),
105
         Packet :: packet()}.
106
-type access() :: {_AccessRoute, _AccessCreate, _AccessAdmin, _AccessPersistent}.
107
-type host_type() :: mongooseim:host_type().
108
-type muc_host() :: jid:lserver().
109

110
-include("mod_muc.hrl").
111

112
-type muc_room() :: #muc_room{
113
    name_host    :: room_host(),
114
    opts         :: list()
115
}.
116

117
-type muc_online_room() :: #muc_online_room{
118
    name_host :: room_host(),
119
    host_type :: host_type(),
120
    pid       :: pid()
121
}.
122
-export_type([muc_online_room/0]).
123

124
-type room_event_data() :: #{
125
                  from_nick := nick(),
126
                  from_jid := jid:jid(),
127
                  room_jid := jid:jid(),
128
                  affiliation := affiliation(),
129
                  role := role(),
130
                  timestamp := integer()
131
       }.
132
-export_type([room_event_data/0]).
133

134
-record(muc_state, {host_type           :: host_type(),
135
                    subdomain_pattern   :: mongoose_subdomain_utils:subdomain_pattern(),
136
                    access,
137
                    history_size        :: integer(),
138
                    default_room_opts   :: list(),
139
                    room_shaper         :: mongoose_shaper:shaper(),
140
                    http_auth_pool      :: mongoose_http_client:pool(),
141
                    hibernated_room_check_interval :: timeout(),
142
                    hibernated_room_timeout :: timeout() }).
143

144
-type state() :: #muc_state{}.
145

146
-export_type([muc_room/0]).
147

148
-define(PROCNAME, ejabberd_mod_muc).
149

150

151
%%====================================================================
152
%% API
153
%%====================================================================
154
%%--------------------------------------------------------------------
155
%% Function: start_link() -> {ok, Pid} | ignore | {error, Error}
156
%% Description: Starts the server
157
%%--------------------------------------------------------------------
158
-spec start_link(host_type(), map())
159
            -> ignore | {error, _} | {ok, pid()}.
160
start_link(HostType, Opts) ->
161
    Proc = gen_mod:get_module_proc(HostType, ?PROCNAME),
357✔
162
    gen_server:start_link({local, Proc}, ?MODULE, {HostType, Opts}, []).
357✔
163

164
-spec start(host_type(), _) -> ok.
165
start(HostType, Opts) when is_map(Opts) ->
166
    mod_muc_online_backend:start(HostType, Opts),
357✔
167
    start_supervisor(HostType),
357✔
168
    start_server(HostType, Opts),
357✔
169
    assert_server_running(HostType),
357✔
170
    ok.
357✔
171

172
-spec stop(host_type()) -> ok.
173
stop(HostType) ->
174
    stop_supervisor(HostType),
357✔
175
    stop_gen_server(HostType),
357✔
176
    mod_muc_online_backend:stop(HostType),
357✔
177
    ok.
357✔
178

179
-spec supported_features() -> [atom()].
180
supported_features() ->
181
    [dynamic_domains].
128✔
182

183
start_server(HostType, Opts) ->
184
    Proc = gen_mod:get_module_proc(HostType, ?PROCNAME),
357✔
185
    ChildSpec =
357✔
186
        {Proc,
187
         {?MODULE, start_link, [HostType, Opts]},
188
         temporary,
189
         1000,
190
         worker,
191
         [?MODULE]},
192
    {ok, _} = ejabberd_sup:start_child(ChildSpec).
357✔
193

194
assert_server_running(HostType) ->
195
    true = is_pid(whereis(gen_mod:get_module_proc(HostType, ?PROCNAME))).
357✔
196

197
-spec config_spec() -> mongoose_config_spec:config_section().
198
config_spec() ->
199
    #section{
15,224✔
200
       items = #{<<"backend">> => #option{type = atom,
201
                                          validate = {module, mod_muc}},
202
                 <<"online_backend">> => #option{type = atom,
203
                                                 validate = {module, mod_muc_online}},
204
                 <<"host">> => #option{type = string,
205
                                       validate = subdomain_template,
206
                                       process = fun mongoose_subdomain_utils:make_subdomain_pattern/1},
207
                 <<"access">> => #option{type = atom,
208
                                         validate = access_rule},
209
                 <<"access_create">> => #option{type = atom,
210
                                                validate = access_rule},
211
                 <<"access_admin">> => #option{type = atom,
212
                                               validate = access_rule},
213
                 <<"access_persistent">> => #option{type = atom,
214
                                                    validate = access_rule},
215
                 <<"history_size">> => #option{type = integer,
216
                                               validate = non_negative},
217
                 <<"room_shaper">> => #option{type = atom,
218
                                              validate = shaper},
219
                 <<"max_room_id">> => #option{type = int_or_infinity,
220
                                              validate = non_negative},
221
                 <<"max_room_name">> => #option{type = int_or_infinity,
222
                                                validate = non_negative},
223
                 <<"max_room_desc">> => #option{type = int_or_infinity,
224
                                                validate = non_negative},
225
                 <<"min_message_interval">> => #option{type = integer,
226
                                                       validate = non_negative},
227
                 <<"min_presence_interval">> => #option{type = integer,
228
                                                        validate = non_negative},
229
                 <<"max_users">> => #option{type = integer,
230
                                            validate = positive},
231
                 <<"max_users_admin_threshold">> => #option{type = integer,
232
                                                            validate = positive},
233
                 <<"user_message_shaper">> => #option{type = atom,
234
                                                      validate = shaper},
235
                 <<"user_presence_shaper">> => #option{type = atom,
236
                                                       validate = shaper},
237
                 <<"max_user_conferences">> => #option{type = integer,
238
                                                       validate = non_negative},
239
                 <<"http_auth_pool">> => #option{type = atom,
240
                                                 validate = pool_name},
241
                 <<"load_permanent_rooms_at_startup">> => #option{type = boolean},
242
                 <<"hibernate_timeout">> => #option{type = int_or_infinity,
243
                                                    validate = non_negative},
244
                 <<"hibernated_room_check_interval">> => #option{type = int_or_infinity,
245
                                                                 validate = non_negative},
246
                 <<"hibernated_room_timeout">> => #option{type = int_or_infinity,
247
                                                          validate = non_negative},
248
                 <<"default_room">> => default_room_config_spec()
249
                },
250
       defaults = defaults()
251
      }.
252

253
defaults() ->
254
    #{<<"backend">> => mnesia,
15,224✔
255
      <<"online_backend">> => mnesia,
256
      <<"host">> => default_host(),
257
      <<"access">> => all,
258
      <<"access_create">> => all,
259
      <<"access_admin">> => none,
260
      <<"access_persistent">> => all,
261
      <<"history_size">> => 20,
262
      <<"room_shaper">> => none,
263
      <<"max_room_id">> => infinity,
264
      <<"max_room_name">> => infinity,
265
      <<"max_room_desc">> => infinity,
266
      <<"min_message_interval">> => 0,
267
      <<"min_presence_interval">> => 0,
268
      <<"max_users">> => ?MAX_USERS_DEFAULT,
269
      <<"max_users_admin_threshold">> => 5,
270
      <<"user_message_shaper">> => none,
271
      <<"user_presence_shaper">> => none,
272
      <<"max_user_conferences">> => 10,
273
      <<"http_auth_pool">> => none,
274
      <<"load_permanent_rooms_at_startup">> => false,
275
      <<"hibernate_timeout">> => timer:seconds(90),
276
      <<"hibernated_room_check_interval">> => infinity,
277
      <<"hibernated_room_timeout">> => infinity,
278
      <<"default_room">> => keys_as_atoms(default_room_opts())}.
279

280
keys_as_atoms(Map) ->
281
    maps:from_list([{binary_to_atom(K), V} || {K, V} <- maps:to_list(Map)]).
15,224✔
282

283
default_room_config_spec() ->
284
    #section{
15,224✔
285
       items = #{<<"title">> => #option{type = binary},
286
                 <<"description">> => #option{type = binary},
287
                 <<"allow_change_subj">> => #option{type = boolean},
288
                 <<"allow_query_users">> => #option{type = boolean},
289
                 <<"allow_private_messages">> => #option{type = boolean},
290
                 <<"allow_visitor_status">> => #option{type = boolean},
291
                 <<"allow_visitor_nickchange">> => #option{type = boolean},
292
                 <<"public">> => #option{type = boolean},
293
                 <<"public_list">> => #option{type = boolean},
294
                 <<"persistent">> => #option{type = boolean},
295
                 <<"moderated">> => #option{type = boolean},
296
                 <<"members_by_default">> => #option{type = boolean},
297
                 <<"members_only">> => #option{type = boolean},
298
                 <<"allow_user_invites">> => #option{type = boolean},
299
                 <<"allow_multiple_sessions">> => #option{type = boolean},
300
                 <<"password_protected">> => #option{type = boolean},
301
                 <<"password">> => #option{type = binary},
302
                 <<"anonymous">> => #option{type = boolean},
303
                 <<"max_users">> => #option{type = integer,
304
                                            validate = positive},
305
                 <<"logging">> => #option{type = boolean},
306
                 <<"maygetmemberlist">> => #list{items = #option{type = atom,
307
                                                                 validate = non_empty}},
308
                 <<"affiliations">> => #list{items = default_room_affiliations_spec()},
309
                 <<"subject">> => #option{type = binary},
310
                 <<"subject_author">> => #option{type = binary}
311
                },
312
       defaults = default_room_opts()
313
      }.
314

315
default_room_opts() ->
316
    X = #config{},
30,448✔
317
    #{<<"title">> => X#config.title,
30,448✔
318
      <<"description">> => X#config.description,
319
      <<"allow_change_subj">> => X#config.allow_change_subj,
320
      <<"allow_query_users">> => X#config.allow_query_users,
321
      <<"allow_private_messages">> => X#config.allow_private_messages,
322
      <<"allow_visitor_status">> => X#config.allow_visitor_status,
323
      <<"allow_visitor_nickchange">> => X#config.allow_visitor_nickchange,
324
      <<"public">> => X#config.public,
325
      <<"public_list">> => X#config.public_list,
326
      <<"persistent">> => X#config.persistent,
327
      <<"moderated">> => X#config.moderated,
328
      <<"members_by_default">> => X#config.members_by_default,
329
      <<"members_only">> => X#config.members_only,
330
      <<"allow_user_invites">> => X#config.allow_user_invites,
331
      <<"allow_multiple_sessions">> => X#config.allow_multiple_sessions,
332
      <<"password_protected">> => X#config.password_protected,
333
      <<"password">> => X#config.password,
334
      <<"anonymous">> => X#config.anonymous,
335
      <<"max_users">> => X#config.max_users,
336
      <<"logging">> => X#config.logging,
337
      <<"maygetmemberlist">> => X#config.maygetmemberlist,
338
      <<"affiliations">> => [],
339
      <<"subject">> => <<>>,
340
      <<"subject_author">> => <<>>}.
341

342
default_room_affiliations_spec() ->
343
    #section{
15,224✔
344
       items = #{<<"user">> => #option{type = binary,
345
                                       validate = non_empty},
346
                 <<"server">> => #option{type = binary,
347
                                         validate = domain},
348
                 <<"resource">> => #option{type = binary},
349
                 <<"affiliation">> => #option{type = atom,
350
                                              validate = non_empty}},
351
       required = all,
352
       process = fun ?MODULE:process_room_affiliation/1
353
      }.
354

355
process_room_affiliation(#{user := User, server := Server, resource := Res, affiliation := Aff}) ->
356
    {{User, Server, Res}, Aff}.
8✔
357

358
stop_gen_server(HostType) ->
359
    Proc = gen_mod:get_module_proc(HostType, ?PROCNAME),
357✔
360
    gen_server:call(Proc, stop),
357✔
361
    %% Proc can still be alive because of a race condition
362
    ejabberd_sup:stop_child(Proc).
357✔
363

364
%% @doc This function is called by a room in three situations:
365
%% A) The owner of the room destroyed it
366
%% B) The only participant of a temporary room leaves it
367
%% C) mod_muc:stop was called, and each room is being terminated
368
%%    In this case, the mod_muc process died before the room processes
369
%%    So the message sending must be catched
370
-spec room_destroyed(host_type(), jid:server(), room(), pid()) -> 'ok'.
371
room_destroyed(HostType, MucHost, Room, Pid) ->
372
    mod_muc_online_backend:room_destroyed(HostType, MucHost, Room, Pid).
6,203✔
373

374
%% @doc Create a room.
375
%% If Opts = default, the default room options are used.
376
%% Else use the passed options as defined in mod_muc_room.
377
%% XXX Only used from tests.
378
-spec create_instant_room(jid:lserver(), MucHost :: jid:lserver(), Name :: room(),
379
    From :: jid:jid(), Nick :: nick(), Opts :: list()) -> any().
380
create_instant_room(ServerHost, MucHost, Name, From, Nick, Opts) ->
381
    {ok, HostType} = mongoose_domain_api:get_domain_host_type(ServerHost),
5,834✔
382
    Proc = gen_mod:get_module_proc(HostType, ?PROCNAME),
5,834✔
383
    gen_server:call(Proc, {create_instant, ServerHost, MucHost, Name, From, Nick, Opts}).
5,834✔
384

385
-spec store_room(host_type(), jid:server(), room(), list()) ->
386
    {error, _} | ok.
387
store_room(HostType, MucHost, Name, Opts) ->
388
    mod_muc_backend:store_room(HostType, MucHost, Name, Opts).
4,375✔
389

390
-spec restore_room(host_type(), muc_host(), room()) ->
391
        {error, _} | {ok, _}.
392
restore_room(HostType, MucHost, Name) ->
393
    mod_muc_backend:restore_room(HostType, MucHost, Name).
58✔
394

395
-spec forget_room(host_type(), jid:server(), room()) -> ok | {error, term()}.
396
forget_room(HostType, MucHost, Name) ->
397
    %% Removes room from DB, even if it's already removed.
398
    Result = mod_muc_backend:forget_room(HostType, MucHost, Name),
3,287✔
399
    case Result of
3,287✔
400
        ok ->
401
            %% TODO This hook should be renamed to forget_room_hook.
402
            %% We also need to think how to remove stopped rooms
403
            %% (i.e. in case we want to expose room removal over REST or SQS).
404
            %%
405
            %% In some _rare_ cases this hook can be called more than once for the same room.
406
            mongoose_hooks:forget_room(HostType, MucHost, Name);
3,287✔
407
        _ ->
408
            %% Room is not removed or we don't know.
409
            %% XXX Handle this case better.
410
            ok
×
411
    end,
412
    Result.
3,287✔
413

414
%% For rooms
415
-spec process_iq_disco_items(MucHost :: jid:server(), From :: jid:jid(),
416
        To :: jid:jid(), jlib:iq()) -> mongoose_acc:t().
417
process_iq_disco_items(MucHost, From, To, #iq{lang = Lang} = IQ) ->
418
    Rsm = jlib:rsm_decode(IQ),
220✔
419
    Res = IQ#iq{type = result,
220✔
420
                sub_el = [#xmlel{name = <<"query">>,
421
                                 attrs = #{<<"xmlns">> => ?NS_DISCO_ITEMS},
422
                                 children = iq_disco_items(MucHost, From, Lang, Rsm)}]},
423
    ejabberd_router:route(To, From, jlib:iq_to_xml(Res)).
220✔
424

425
-spec can_use_nick(host_type(), jid:server(), jid:jid(), nick()) -> boolean().
426
can_use_nick(_HostType, _Host, _JID, <<>>) ->
427
    false;
×
428
can_use_nick(HostType, MucHost, JID, Nick) ->
429
    mod_muc_backend:can_use_nick(HostType, MucHost, JID, Nick).
8,027✔
430

431
set_nick(_HostType, _MucHost, _From, <<>>) ->
432
    {error, should_not_be_empty};
×
433
set_nick(HostType, MucHost, From, Nick) ->
434
    mod_muc_backend:set_nick(HostType, MucHost, From, Nick).
134✔
435

436
unset_nick(HostType, MucHost, From) ->
437
    mod_muc_backend:unset_nick(HostType, MucHost, From).
54✔
438

439
get_nick(HostType, MucHost, From) ->
440
    mod_muc_backend:get_nick(HostType, MucHost, From).
198✔
441

442
%%====================================================================
443
%% gen_server callbacks
444
%%====================================================================
445

446
-spec init({host_type(), map()}) -> {ok, state()}.
447
init({HostType, Opts}) ->
448
    mod_muc_backend:init(HostType, Opts),
357✔
449
    catch ets:new(muc_online_users, [bag, named_table, public, {keypos, 2}]),
357✔
450
    #{access := Access,
357✔
451
      access_create := AccessCreate,
452
      access_admin := AccessAdmin,
453
      access_persistent := AccessPersistent,
454
      http_auth_pool := HttpAuthPool,
455
      history_size := HistorySize,
456
      default_room := DefRoomOpts,
457
      room_shaper := RoomShaper,
458
      hibernated_room_check_interval := CheckInterval,
459
      hibernated_room_timeout := HibernatedTimeout,
460
      host := SubdomainPattern,
461
      load_permanent_rooms_at_startup := LoadPermRoomsAtStartup} = Opts,
462
    State = #muc_state{host_type = HostType,
357✔
463
                       subdomain_pattern = SubdomainPattern,
464
                       access = {Access, AccessCreate, AccessAdmin, AccessPersistent},
465
                       default_room_opts = maps:to_list(DefRoomOpts),
466
                       history_size = HistorySize,
467
                       room_shaper = RoomShaper,
468
                       http_auth_pool = HttpAuthPool,
469
                       hibernated_room_check_interval = CheckInterval,
470
                       hibernated_room_timeout = HibernatedTimeout},
471
    %% Hooks
472
    gen_hook:add_handlers(hooks(HostType)),
357✔
473
    %% Handler
474
    PacketHandler = mongoose_packet_handler:new(?MODULE, #{state => State}),
357✔
475
    case SubdomainPattern of
357✔
476
       {prefix, _} -> ok;
357✔
477
       _ -> ?LOG_WARNING(#{what => muc_host_pattern_missing,
×
478
                           host_type => HostType,
479
                           subdomain_pattern => SubdomainPattern,
480
                           text => <<"Only one MUC domain would work with this host type">>})
×
481
    end,
482
    mongoose_domain_api:register_subdomain(HostType, SubdomainPattern, PacketHandler),
357✔
483
    %% Loading
484
    case LoadPermRoomsAtStartup of
357✔
485
        false ->
486
            ?LOG_INFO(#{what => load_permanent_rooms_at_startup, skip => true,
357✔
487
                        text => <<"Skip loading permanent rooms at startup. "
488
                                  "Each room is loaded when someone access the room">>});
357✔
489
        true ->
490
            ?LOG_WARNING(#{what => load_permanent_rooms_at_startup_is_deprecated, skip => false,
×
491
                           text => <<"Loading permanent rooms at startup is deprecated. "
492
                                     "The option is ignored.">>})
×
493
    end,
494
    set_persistent_rooms_timer(State),
357✔
495
    {ok, State}.
357✔
496

497
set_persistent_rooms_timer(#muc_state{hibernated_room_check_interval = infinity}) ->
498
    ok;
57✔
499
set_persistent_rooms_timer(#muc_state{hibernated_room_check_interval = Timeout}) ->
500
    timer:send_after(Timeout, stop_hibernated_persistent_rooms).
2,953✔
501

502
handle_call(stop, _From, State) ->
503
    gen_hook:delete_handlers(hooks(State#muc_state.host_type)),
357✔
504
    {stop, normal, ok, State};
357✔
505
handle_call({create_instant, ServerHost, MucHost, Room, From, Nick, Opts},
506
            _From,
507
            #muc_state{host_type = HostType,
508
                       access = Access,
509
                       default_room_opts = DefOpts,
510
                       history_size = HistorySize,
511
                       room_shaper = RoomShaper,
512
                       http_auth_pool = HttpAuthPool} = State) ->
513
    ?LOG_DEBUG(#{what => muc_create_instant, room => Room, sub_host => MucHost}),
5,834✔
514
    NewOpts = case Opts of
5,834✔
515
                  default -> DefOpts;
156✔
516
                  _ -> Opts
5,678✔
517
              end,
518
    try
5,834✔
519
        {ok, Pid} = mod_muc_room:start_new(HostType,
5,834✔
520
                                           MucHost, ServerHost, Access,
521
                                           Room, HistorySize,
522
                                           RoomShaper, HttpAuthPool, From,
523
                                           Nick, [{instant, true}|NewOpts]),
524
        register_room_or_stop_if_duplicate(HostType, MucHost, Room, Pid),
5,834✔
525
        {reply, ok, State}
5,834✔
526
    catch Class:Reason:Stacktrace ->
527
              Err = #{what => muc_create_instant_failed,
×
528
                      server => ServerHost, host_type => HostType,
529
                      room => Room, from_jid => From,
530
                      class => Class, reason => Reason,
531
                      stacktrace => Stacktrace},
532
              ?LOG_ERROR(Err),
×
533
              {reply, {error, Err}, State}
×
534
    end.
535

536
handle_cast(_Msg, State) ->
537
    {noreply, State}.
×
538

539
handle_info(stop_hibernated_persistent_rooms,
540
            #muc_state{host_type = HostType,
541
                       hibernated_room_timeout = Timeout} = State)
542
  when is_integer(Timeout) ->
543
    handle_stop_hibernated_persistent_rooms(HostType, Timeout),
2,653✔
544
    set_persistent_rooms_timer(State),
2,653✔
545
    {noreply, State};
2,653✔
546
handle_info(_Info, State) ->
547
    {noreply, State}.
×
548

549
handle_stop_hibernated_persistent_rooms(HostType, Timeout) ->
550
    ?LOG_INFO(#{what => muc_stop_hibernated_persistent_rooms, host_type => HostType,
2,653✔
551
               text => <<"Closing hibernated persistent rooms">>}),
2,653✔
552
    try
2,653✔
553
        Supervisor = gen_mod:get_module_proc(HostType, ejabberd_mod_muc_sup),
2,653✔
554
        Now = os:timestamp(),
2,653✔
555
        [stop_if_hibernated(Pid, Now, Timeout * 1000) ||
2,653✔
556
         {undefined, Pid, worker, _} <- supervisor:which_children(Supervisor)]
2,653✔
557
    catch Error:Reason:Stacktrace ->
558
              ?LOG_ERROR(#{what => stop_hibernated_persistent_rooms_failed,
×
559
                           error => Error, reason => Reason,
560
                           stacktrace => Stacktrace,
561
                           host_type => HostType})
×
562
    end.
563

564
stop_if_hibernated(Pid, Now, Timeout) ->
565
    stop_if_hibernated(Pid, Now, Timeout, erlang:process_info(Pid, current_function)).
7,115✔
566

567
stop_if_hibernated(Pid, Now, Timeout, {current_function, {erlang, hibernate, 3}}) ->
568
    {dictionary, Dictionary} = erlang:process_info(Pid, dictionary),
2,967✔
569
    LastHibernated = lists:keyfind(hibernated, 1, Dictionary),
2,967✔
570
    stop_if_hibernated_for_specified_time(Pid, Now, Timeout, LastHibernated),
2,967✔
571
    ok;
2,967✔
572
stop_if_hibernated(_, _, _, _) ->
573
    ok.
4,148✔
574

575
stop_if_hibernated_for_specified_time(_Pid, _, _, false) ->
576
    ok;
×
577
stop_if_hibernated_for_specified_time(Pid, Now, Timeout, {hibernated, LastHibernated}) ->
578
    TimeDiff = timer:now_diff(Now, LastHibernated),
2,967✔
579
    case TimeDiff >= Timeout of
2,967✔
580
        true ->
581
            Pid ! stop_persistent_room_process;
922✔
582
        _ ->
583
            ok
2,045✔
584
    end.
585

586
terminate(_Reason, #muc_state{host_type = HostType,
587
                              subdomain_pattern = SubdomainPattern}) ->
588
    mongoose_domain_api:unregister_subdomain(HostType, SubdomainPattern).
357✔
589

590
code_change(_OldVsn, State, _Extra) ->
591
    {ok, State}.
×
592

593
%%--------------------------------------------------------------------
594
%%% Internal functions
595
%%--------------------------------------------------------------------
596
-spec start_supervisor(host_type()) -> {error, _}
597
                                           | {ok, undefined | pid()}
598
                                           | {ok, undefined | pid(), _}.
599
start_supervisor(HostType) ->
600
    ChildSpec = sup_spec(HostType),
357✔
601
    ejabberd_sup:start_child(ChildSpec).
357✔
602

603
sup_spec(HostType) ->
604
    Proc = gen_mod:get_module_proc(HostType, ejabberd_mod_muc_sup),
357✔
605
    ejabberd_sup:template_supervisor_spec(Proc, mod_muc_room).
357✔
606

607
-spec stop_supervisor(jid:server()) -> ok | {error, Reason}
608
    when Reason :: not_found | restarting | running | simple_one_for_one.
609
stop_supervisor(HostType) ->
610
    Proc = gen_mod:get_module_proc(HostType, ejabberd_mod_muc_sup),
357✔
611
    ejabberd_sup:stop_child(Proc).
357✔
612

613
-spec process_packet(Acc :: mongoose_acc:t(),
614
                     From :: jid:jid(),
615
                     To :: jid:simple_jid() | jid:jid(),
616
                     El :: exml:element(),
617
                     #{state := state()}) -> mongoose_acc:t().
618
process_packet(Acc, From, To, El, #{state := State}) ->
619
    {AccessRoute, _, _, _} = State#muc_state.access,
32,518✔
620
    ServerHost = make_server_host(To, State),
32,518✔
621
    HostType = State#muc_state.host_type,
32,518✔
622
    case acl:match_rule(HostType, ServerHost, AccessRoute, From) of
32,518✔
623
        allow ->
624
            {Room, MucHost, _} = jid:to_lower(To),
32,518✔
625
            route_to_room(MucHost, Room, {From, To, Acc, El}, State),
32,518✔
626
            Acc;
32,485✔
627
        _ ->
628
            Lang = exml_query:attr(El, <<"xml:lang">>, <<>>),
×
629
            ErrText = <<"Access denied by service policy">>,
×
630
            ejabberd_router:route_error_reply(To, From, Acc,
×
631
                                              mongoose_xmpp_errors:forbidden(Lang, ErrText))
632
    end.
633

634

635
-spec route_to_room(jid:lserver(), room(), from_to_packet(), state()) -> ok | pid().
636
route_to_room(_MucHost, <<>>, {_, To, _Acc, _} = Routed, State) ->
637
    {_, _, Nick} = jid:to_lower(To),
833✔
638
    route_by_nick(Nick, Routed, State);
833✔
639
route_to_room(MucHost, Room, Routed, #muc_state{} = State) ->
640
    HostType = State#muc_state.host_type,
31,685✔
641
    case find_room_pid(HostType, MucHost, Room) of
31,685✔
642
        {error, not_found} ->
643
            case get_registered_room_or_route_error(MucHost, Room, Routed, State) of
618✔
644
                {ok, Pid} ->
645
                    route_to_online_room(Pid, Routed);
592✔
646
                {route_error, _ErrText} ->
647
                    ok
26✔
648
            end;
649
        {ok, Pid} ->
650
            route_to_online_room(Pid, Routed)
31,067✔
651
    end.
652

653
route_to_online_room(Pid, {From, To, Acc, Packet}) ->
654
    ?LOG_DEBUG(#{what => muc_route_to_online_room, room_pid => Pid, acc => Acc}),
31,659✔
655
    {_, _, Nick} = jid:to_lower(To),
31,659✔
656
    ok = mod_muc_room:route(Pid, From, Nick, Acc, Packet).
31,659✔
657

658
-spec get_registered_room_or_route_error(muc_host(), room(), from_to_packet(), state()) -> {ok, pid()} | {route_error, binary()}.
659
get_registered_room_or_route_error(MucHost, Room, {From, To, Acc, Packet}, State) ->
660
    case {Packet#xmlel.name, exml_query:attr(Packet, <<"type">>, <<>>)} of
618✔
661
        {<<"presence">>, <<>>} ->
662
            get_registered_room_or_route_error_from_presence(MucHost, Room, From, To, Acc, Packet, State);
571✔
663
        _ ->
664
            get_registered_room_or_route_error_from_packet(MucHost, Room, From, To, Acc, Packet, State)
47✔
665
    end.
666

667
get_registered_room_or_route_error_from_presence(MucHost, Room, From, To, Acc,
668
        Packet, #muc_state{host_type = HostType, access = Access} = State) ->
669
    {_, AccessCreate, _, _} = Access,
571✔
670
    ServerHost = make_server_host(To, State),
571✔
671
    case check_user_can_create_room(HostType, ServerHost, AccessCreate, From, Room) of
571✔
672
        ok ->
673
            #muc_state{history_size = HistorySize,
560✔
674
                       room_shaper = RoomShaper,
675
                       http_auth_pool = HttpAuthPool,
676
                       default_room_opts = DefRoomOpts} = State,
677
            {_, _, Nick} = jid:to_lower(To),
560✔
678
            ServerHost = make_server_host(To, State),
560✔
679
            Result = start_room(HostType, ServerHost, MucHost, Access, Room,
560✔
680
                                       HistorySize, RoomShaper, HttpAuthPool,
681
                                       From, Nick, DefRoomOpts, Acc),
682
            case Result of
560✔
683
                {ok, Pid} ->
684
                    register_room_or_stop_if_duplicate(HostType, MucHost, Room, Pid);
560✔
685
                {error, {failed_to_restore, Reason}} ->
686
                    %% Notify user about our backend module error
687
                    ?LOG_WARNING(#{what => muc_send_service_unavailable,
×
688
                                   text => <<"Failed to restore room">>,
689
                                   host_type => HostType,
690
                                   room => Room, sub_host => MucHost,
691
                                   reason => Reason, acc => Acc}),
×
692
                    Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>),
×
693
                    ErrText = <<"Service is temporary unavailable">>,
×
694
                    {Acc1, Err} = jlib:make_error_reply(
×
695
                            Acc, Packet, mongoose_xmpp_errors:service_unavailable(Lang, ErrText)),
696
                    ejabberd_router:route(To, From, Acc1, Err),
×
697
                    {route_error, ErrText}
×
698
            end;
699
        {error, Reason} ->
700
            Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>),
11✔
701
            Policy = iolist_to_binary(io_lib:format("~p", [Reason])),
11✔
702
            ErrText = <<"Room creation is denied by service policy: ", Policy/binary>>,
11✔
703
            {Acc1, Err} = jlib:make_error_reply(
11✔
704
                    Acc, Packet, mongoose_xmpp_errors:not_allowed(Lang, ErrText)),
705
            ejabberd_router:route(To, From, Acc1, Err),
11✔
706
            {route_error, ErrText}
11✔
707
    end.
708

709
get_registered_room_or_route_error_from_packet(MucHost, Room, From, To, Acc, Packet,
710
                                 #muc_state{host_type = HostType,
711
                                            access = Access} = State) ->
712
    ServerHost = make_server_host(To, State),
47✔
713
    case restore_room(HostType, MucHost, Room) of
47✔
714
        {error, room_not_found} ->
715
            Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>),
15✔
716
            ErrText = <<"Conference room does not exist">>,
15✔
717
            {Acc1, Err} = jlib:make_error_reply(
15✔
718
                    Acc, Packet, mongoose_xmpp_errors:item_not_found(Lang, ErrText)),
719
            ejabberd_router:route(To, From, Acc1, Err),
15✔
720
            {route_error, ErrText};
15✔
721
        {error, Reason} ->
722
            ?LOG_WARNING(#{what => muc_send_service_unavailable,
×
723
                           room => Room, host_type => HostType, sub_host => MucHost,
724
                           reason => Reason, acc => Acc}),
×
725
            Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>),
×
726
            ErrText = <<"Service is temporary unavailable">>,
×
727
            {Acc1, Err} = jlib:make_error_reply(
×
728
                    Acc, Packet, mongoose_xmpp_errors:service_unavailable(Lang, ErrText)),
729
            ejabberd_router:route(To, From, Acc1, Err),
×
730
            {route_error, ErrText};
×
731
        {ok, Opts} ->
732
            ?LOG_DEBUG(#{what => muc_restore_room, room => Room, room_opts => Opts}),
32✔
733
            #muc_state{history_size = HistorySize,
32✔
734
                       room_shaper = RoomShaper,
735
                       http_auth_pool = HttpAuthPool} = State,
736
            {ok, Pid} = mod_muc_room:start_restored(HostType,
32✔
737
                                           MucHost, ServerHost, Access,
738
                                           Room, HistorySize,
739
                                           RoomShaper, HttpAuthPool, Opts),
740
            register_room_or_stop_if_duplicate(HostType, MucHost, Room, Pid)
32✔
741
    end.
742

743
-spec route_by_nick(room(), from_to_packet(), state()) -> 'ok' | pid().
744
route_by_nick(<<>>, {_, _, _, Packet} = Routed, State) ->
745
    #xmlel{name = Name} = Packet,
833✔
746
    route_by_type(Name, Routed, State);
833✔
747
route_by_nick(_Nick, {From, To, Acc, Packet}, _State) ->
748
    case exml_query:attr(Packet, <<"type">>) of
×
749
        <<"error">> ->
750
            Acc;
×
751
        <<"result">> ->
752
            Acc;
×
753
        _ ->
754
            {Acc1, Err} = jlib:make_error_reply(Acc, Packet, mongoose_xmpp_errors:item_not_found()),
×
755
            ejabberd_router:route(To, From, Acc1, Err)
×
756
    end.
757

758
-spec route_by_type(binary(), from_to_packet(), state()) -> ok | pid().
759
route_by_type(<<"iq">>, {From, To, Acc, Packet}, #muc_state{} = State) ->
760
    HostType = State#muc_state.host_type,
833✔
761
    MucHost = To#jid.lserver,
833✔
762
    case jlib:iq_query_info(Packet) of
833✔
763
        #iq{type = get, xmlns = ?NS_DISCO_INFO = XMLNS, lang = Lang} = IQ ->
764
            IdentityXML = mongoose_disco:identities_to_xml([identity(Lang)]),
137✔
765
            FeatureXML =  mongoose_disco:get_muc_features(HostType, From, To, <<>>, Lang,
137✔
766
                                                          features()),
767
            InfoXML = mongoose_disco:get_info(HostType, ?MODULE, <<>>, Lang),
137✔
768
            Res = IQ#iq{type = result,
137✔
769
                        sub_el = [#xmlel{name = <<"query">>,
770
                                         attrs = #{<<"xmlns">> => XMLNS},
771
                                         children = IdentityXML ++ FeatureXML ++ InfoXML}]},
772
            ejabberd_router:route(To, From, jlib:iq_to_xml(Res));
137✔
773
        #iq{type = get, xmlns = ?NS_DISCO_ITEMS} = IQ ->
774
            proc_lib:spawn(fun() -> process_iq_disco_items(MucHost, From, To, IQ) end);
220✔
775
        #iq{type = get, xmlns = ?NS_REGISTER = XMLNS, lang = Lang} = IQ ->
776
            Result = iq_get_register_info(HostType, MucHost, From, Lang),
198✔
777
            Res = IQ#iq{type = result,
198✔
778
                        sub_el = [#xmlel{name = <<"query">>,
779
                                         attrs = #{<<"xmlns">> => XMLNS},
780
                                         children = Result}]},
781
            ejabberd_router:route(To, From, jlib:iq_to_xml(Res));
198✔
782
        #iq{type = set,
783
            xmlns = ?NS_REGISTER = XMLNS,
784
            lang = Lang,
785
            sub_el = SubEl} = IQ ->
786
            case process_iq_register_set(HostType, MucHost, From, SubEl, Lang) of
278✔
787
                {result, IQRes} ->
788
                    Res = IQ#iq{type = result,
206✔
789
                                sub_el = [#xmlel{name = <<"query">>,
790
                                                 attrs = #{<<"xmlns">> => XMLNS},
791
                                                 children = IQRes}]},
792
                    ejabberd_router:route(To, From, jlib:iq_to_xml(Res));
206✔
793
                {error, Error} ->
794
                    {Acc1, Err} = jlib:make_error_reply(Acc, Packet, Error),
72✔
795
                    ejabberd_router:route(To, From, Acc1, Err)
72✔
796
            end;
797
        #iq{type = get, xmlns = ?NS_VCARD = XMLNS, lang = Lang} = IQ ->
798
            Res = IQ#iq{type = result,
×
799
                        sub_el = [#xmlel{name = <<"vCard">>,
800
                                         attrs = #{<<"xmlns">> => XMLNS},
801
                                         children = iq_get_vcard(Lang)}]},
802
            ejabberd_router:route(To, From, jlib:iq_to_xml(Res));
×
803
        #iq{type = get, xmlns = ?NS_MUC_UNIQUE} = IQ ->
804
           Res = IQ#iq{type = result,
×
805
                       sub_el = [#xmlel{name = <<"unique">>,
806
                                        attrs = #{<<"xmlns">> => ?NS_MUC_UNIQUE},
807
                                        children = [iq_get_unique(From)]}]},
808
           ejabberd_router:route(To, From, jlib:iq_to_xml(Res));
×
809
        #iq{} ->
810
            ?LOG_INFO(#{what => muc_ignore_unknown_iq, acc => Acc}),
×
811
            {Acc1, Err} = jlib:make_error_reply(Acc, Packet,
×
812
                mongoose_xmpp_errors:feature_not_implemented(<<"en">>, <<"From mod_muc">>)),
813
            ejabberd_router:route(To, From, Acc1, Err);
×
814
        Other ->
815
            ?LOG_INFO(#{what => muc_failed_to_parse_iq, acc => Acc, reason => Other}),
×
816
            ok
×
817
    end;
818
route_by_type(<<"message">>, {From, To, Acc, Packet},
819
              #muc_state{host_type = HostType,
820
                         access = {_, _, AccessAdmin, _}} = State) ->
821
    MucHost = To#jid.lserver,
×
822
    ServerHost = make_server_host(To, State),
×
823
    case exml_query:attr(Packet, <<"type">>) of
×
824
        <<"error">> ->
825
            ok;
×
826
        _ ->
827
            case acl:match_rule(HostType, ServerHost, AccessAdmin, From) of
×
828
                allow ->
829
                    Msg = exml_query:path(Packet, [{element, <<"body">>}, cdata], <<>>),
×
830
                    broadcast_service_message(MucHost, Msg);
×
831
                _ ->
832
                    Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>),
×
833
                    ErrTxt = <<"Only service administrators are allowed to send service messages">>,
×
834
                    Err = mongoose_xmpp_errors:forbidden(Lang, ErrTxt),
×
835
                    {Acc1, ErrorReply} = jlib:make_error_reply(Acc, Packet, Err),
×
836
                    ejabberd_router:route(To, From, Acc1, ErrorReply)
×
837
            end
838
    end;
839
route_by_type(<<"presence">>, _Routed, _State) ->
840
    ok.
×
841

842
-spec check_user_can_create_room(host_type(), jid:lserver(),
843
        allow | atom(), jid:jid(), room()) -> ok | {error, term()}.
844
check_user_can_create_room(HostType, ServerHost, AccessCreate, From, RoomID) ->
845
    case acl:match_rule(HostType, ServerHost, AccessCreate, From) of
571✔
846
        allow ->
847
            MaxLen = gen_mod:get_module_opt(HostType, mod_muc, max_room_id),
560✔
848
            case (size(RoomID) =< MaxLen) of
560✔
849
                true -> ok;
560✔
850
                false -> {error, room_id_too_long}
×
851
            end;
852
        _ ->
853
            ?LOG_WARNING(#{what => check_user_can_create_room_failed,
11✔
854
                           host_type => HostType,
855
                           server => ServerHost,
856
                           access_create => AccessCreate,
857
                           from_jid => From,
858
                           room_id => RoomID}),
×
859
            {error, no_matching_acl_rule}
11✔
860
    end.
861

862
-spec start_room(HostType :: host_type(), ServerHost :: jid:lserver(),
863
        MucHost :: muc_host(), Access :: access(), room(),
864
        HistorySize :: undefined | integer(), RoomShaper :: mongoose_shaper:shaper(),
865
        HttpAuthPool :: none | mongoose_http_client:pool(), From :: jid:jid(), nick(),
866
        DefRoomOpts :: undefined | [any()], Acc :: mongoose_acc:t())
867
            -> {error, {failed_to_restore, Reason :: term()}} | {ok, pid()}.
868
start_room(HostType, ServerHost, MucHost, Access, Room,
869
               HistorySize, RoomShaper, HttpAuthPool, From,
870
               Nick, DefRoomOpts, Acc) ->
871
    case mod_muc_backend:restore_room(HostType, MucHost, Room) of
560✔
872
        {error, room_not_found} ->
873
            ?LOG_DEBUG(#{what => muc_start_new_room, acc => Acc,
527✔
874
                         room => Room, host_type => HostType, sub_host => MucHost}),
527✔
875
            mod_muc_room:start_new(HostType,
527✔
876
                               MucHost, ServerHost, Access,
877
                               Room, HistorySize,
878
                               RoomShaper, HttpAuthPool, From,
879
                               Nick, DefRoomOpts);
880
        {error, Reason} ->
881
            {error, {failed_to_restore, Reason}};
×
882
        {ok, Opts} ->
883
            ?LOG_DEBUG(#{what => muc_restore_room, acc => Acc, room => Room,
33✔
884
                         host_type => HostType, sub_host => MucHost, room_opts => Opts}),
33✔
885
            mod_muc_room:start_restored(HostType,
33✔
886
                               MucHost, ServerHost, Access,
887
                               Room, HistorySize,
888
                               RoomShaper, HttpAuthPool, Opts)
889
    end.
890

891
register_room_or_stop_if_duplicate(HostType, MucHost, Room, Pid) ->
892
    case register_room(HostType, MucHost, Room, Pid) of
6,426✔
893
        ok ->
894
            {ok, Pid};
5,921✔
895
        {exists, OldPid} ->
896
            mod_muc_room:stop(Pid),
505✔
897
            {ok, OldPid};
505✔
898
        {error, Reason} ->
899
            error({failed_to_register, MucHost, Room, Pid, Reason})
×
900
    end.
901

902
-spec register_room(HostType :: host_type(), jid:server(), room(),
903
                    pid()) -> ok | {exists, pid()} | {error, term()}.
904
register_room(HostType, MucHost, Room, Pid) ->
905
    mod_muc_online_backend:register_room(HostType, MucHost, Room, Pid).
6,459✔
906

907
-spec room_jid_to_pid(RoomJID :: jid:jid()) -> {ok, pid()} | {error, not_found}.
908
room_jid_to_pid(#jid{luser = Room, lserver = MucHost}) ->
909
    case mongoose_domain_api:get_subdomain_host_type(MucHost) of
13,433✔
910
        {ok, HostType} ->
911
            find_room_pid(HostType, MucHost, Room);
13,109✔
912
        _ ->
913
            {error, not_found}
324✔
914
    end.
915

916
find_room_pid(HostType, MucHost, Room) ->
917
    mod_muc_online_backend:find_room_pid(HostType, MucHost, Room).
44,794✔
918

919
-spec default_host() -> mongoose_subdomain_utils:subdomain_pattern().
920
default_host() ->
921
    mongoose_subdomain_utils:make_subdomain_pattern(<<"conference.@HOST@">>).
30,448✔
922

923
identity(Lang) ->
924
    #{category => <<"conference">>,
137✔
925
      type => <<"text">>,
926
      name => service_translations:do(Lang, <<"Chatrooms">>)}.
927

928
features() ->
929
    [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_MUC, ?NS_MUC_UNIQUE, ?NS_REGISTER, ?NS_RSM, ?NS_VCARD, ?NS_CONFERENCE].
137✔
930

931
%% Disco for rooms
932
-spec iq_disco_items(muc_host(), jid:jid(), ejabberd:lang(),
933
                     Rsm :: none | jlib:rsm_in()) -> any().
934
iq_disco_items(MucHost, From, Lang, none) ->
935
    AllRooms = get_vh_rooms(MucHost) ++ get_persistent_vh_rooms(MucHost),
66✔
936
    Rooms = lists:ukeysort(1, lists:map(fun record_to_simple/1, AllRooms)),
66✔
937
    BareRooms = lists:filtermap(fun(Room) -> room_to_item(Room, MucHost, From, Lang) end, Rooms),
66✔
938
    lists:ukeysort(3, BareRooms);
66✔
939
iq_disco_items(MucHost, From, Lang, Rsm) ->
940
    {Rooms, RsmO} = get_vh_rooms(MucHost, Rsm),
154✔
941
    RsmOut = jlib:rsm_encode(RsmO),
154✔
942
    lists:filtermap(fun(Room) -> room_to_item(Room, MucHost, From, Lang) end, Rooms) ++ RsmOut.
154✔
943

944
room_to_item({{Name, _}, Pid}, MucHost, From, Lang) when is_pid(Pid) ->
945
     case catch gen_fsm_compat:sync_send_all_state_event(
1,227✔
946
                  Pid, {get_disco_item, From, Lang}, 100) of
947
         {item, Desc} ->
948
             {true,
1,212✔
949
              #xmlel{name = <<"item">>,
950
                     attrs = #{<<"jid">> => jid:to_binary({Name, MucHost, <<>>}),
951
                               <<"name">> => Desc}}};
952
         _ ->
953
             false
15✔
954
     end;
955
room_to_item({{Name, _}, _}, MucHost, _, _) ->
956
     {true,
645✔
957
     #xmlel{name = <<"item">>,
958
            attrs = #{<<"jid">> => jid:to_binary({Name, MucHost, <<>>}),
959
                      <<"name">> => Name}}
960
     }.
961
record_to_simple(#muc_online_room{name_host = Room, pid = Pid}) ->
962
    {Room, Pid};
3,758✔
963
record_to_simple(#muc_room{name_host = Room, opts = Opts}) ->
964
    {Room, Opts}.
1,036✔
965

966
-spec get_vh_rooms(muc_host(), jlib:rsm_in()) -> {list(), jlib:rsm_out()}.
967
get_vh_rooms(MucHost, #rsm_in{max=Max, direction=Direction, id=I, index=Index}) ->
968
    NonUndefMax = case Max of
397✔
969
        undefined -> 134217728;
257✔
970
        _ -> Max
140✔
971
    end,
972
    Rooms = get_vh_rooms(MucHost) ++ get_persistent_vh_rooms(MucHost),
397✔
973
    BareSortedRooms = lists:ukeysort(1, lists:map(fun record_to_simple/1, Rooms)),
397✔
974
    Count = erlang:length(BareSortedRooms),
397✔
975
    L2 = case {Index, Direction} of
397✔
976
        {undefined, undefined} ->
977
            lists:sublist(BareSortedRooms, 1, NonUndefMax);
235✔
978
        {undefined, aft} ->
979
            lists:sublist(
22✔
980
                lists:dropwhile(
981
                    fun({{Id, _}, _}) -> Id =< I end,
253✔
982
                    BareSortedRooms),
983
                1,
984
                NonUndefMax);
985
        {undefined,before} when I == <<>> ->
986
            lists:reverse(
11✔
987
                lists:sublist(
988
                    lists:reverse(BareSortedRooms), 1, NonUndefMax));
989
        {undefined, before} ->
990
            L = lists:takewhile(
22✔
991
                fun({{Id, _}, _}) -> Id < I end,
231✔
992
                BareSortedRooms),
993
            lists:reverse(
22✔
994
                lists:sublist(
995
                    lists:reverse(L), 1, NonUndefMax));
996
        {Index, _} when Index < 0 orelse Index > Count -> [];
22✔
997
        {Index, _} ->
998
            lists:sublist(BareSortedRooms, Index + 1, NonUndefMax);
85✔
999
         Input ->
1000
             ?LOG_ERROR(#{what => muc_get_rooms_with_pagination_failed,
×
1001
                          text => <<"Unexpected result in get_rooms_with_pagination">>,
1002
                          reason => Input}),
×
1003
             []
×
1004
         end,
1005
    case L2 of
397✔
1006
        [] ->
1007
            {L2, #rsm_out{count=Count}};
74✔
1008
        _ ->
1009
            H = hd(L2),
323✔
1010
            NewIndex = get_room_pos(H, BareSortedRooms),
323✔
1011

1012
            {{F, _},_} = H,
323✔
1013
            {{Last, _}, _} = lists:last(L2),
323✔
1014
            {L2, #rsm_out{first=F, last=Last, count=Count, index=NewIndex}}
323✔
1015
    end.
1016

1017
%% @doc Return the position of desired room in the list of rooms.
1018
%% The room must exist in the list. The count starts in 0.
1019
-spec get_room_pos({{binary(), any()}, any()}, [{{binary(), any()}, any}]) -> non_neg_integer().
1020
get_room_pos(Desired, Rooms) ->
1021
    get_room_pos(Desired, Rooms, 0).
323✔
1022
get_room_pos({{NameHost, _}, _}, [{{NameHost, _}, _} | _], HeadPosition) ->
1023
    HeadPosition;
323✔
1024
get_room_pos(Desired, [_ | Rooms], HeadPosition) ->
1025
    get_room_pos(Desired, Rooms, HeadPosition + 1).
811✔
1026

1027
%% @doc Get a pseudo unique Room Name. The Room Name is generated as a hash of
1028
%%      the requester JID, the local time and a random salt.
1029
%%
1030
%%      `<<"pseudo">>' because we don't verify that there is not a room
1031
%%      with the returned Name already created, nor mark the generated Name
1032
%%      as `<<"already used">>'.  But in practice, it is unique enough. See
1033
%%      http://xmpp.org/extensions/xep-0045.html#createroom-unique
1034
-spec iq_get_unique(jid:jid()) -> exml:cdata().
1035
iq_get_unique(From) ->
1036
    Raw = [From, erlang:unique_integer(), mongoose_bin:gen_from_crypto()],
×
1037
    #xmlcdata{content = mongoose_bin:encode_crypto(term_to_binary(Raw))}.
×
1038

1039
-spec iq_get_register_info(host_type(), jid:server(),
1040
        jid:simple_jid() | jid:jid(), ejabberd:lang())
1041
            -> [exml:element(), ...].
1042
iq_get_register_info(HostType, MucHost, From, Lang) ->
1043
    {Nick, Registered} =
198✔
1044
        case catch get_nick(HostType, MucHost, From) of
1045
            {'EXIT', _Reason} ->
1046
                {<<>>, []};
×
1047
            {error, _} ->
1048
                {<<>>, []};
72✔
1049
            {ok, N} ->
1050
                {N, [#xmlel{name = <<"registered">>}]}
126✔
1051
        end,
1052
    ClientReqText = service_translations:do(
198✔
1053
                      Lang, <<"You need a client that supports x:data to register the nickname">>),
1054
    ClientReqEl = #xmlel{name = <<"instructions">>,
198✔
1055
                         children = [#xmlcdata{content = ClientReqText}]},
1056
    EnterNicknameText = service_translations:do(Lang, <<"Enter nickname you want to register">>),
198✔
1057
    TitleText = <<(service_translations:do(Lang, <<"Nickname Registration at ">>))/binary,
198✔
1058
                  MucHost/binary>>,
1059
    NickField = #{type => <<"text-single">>,
198✔
1060
                  label => service_translations:do(Lang, <<"Nickname">>),
1061
                  var => <<"nick">>,
1062
                  values => [Nick]},
1063
    Registered ++ [ClientReqEl, mongoose_data_forms:form(#{title => TitleText,
198✔
1064
                                                           instructions => EnterNicknameText,
1065
                                                           fields => [NickField]})].
1066

1067
-spec iq_set_register_info(host_type(), jid:server(),
1068
        jid:simple_jid() | jid:jid(), nick(), ejabberd:lang())
1069
            -> {'error', exml:element()} | {'result', []}.
1070
iq_set_register_info(HostType, MucHost, From, Nick, Lang) ->
1071
    case set_nick(HostType, MucHost, From, Nick) of
134✔
1072
        ok ->
1073
            {result, []};
134✔
1074
        {error, conflict} ->
1075
            ErrText = <<"That nickname is registered by another person">>,
×
1076
            {error, mongoose_xmpp_errors:conflict(Lang, ErrText)};
×
1077
        {error, should_not_be_empty} ->
1078
            ErrText = <<"You must fill in field \"Nickname\" in the form">>,
×
1079
            {error, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)};
×
1080
        {error, ErrorReason} ->
1081
            ?LOG_ERROR(#{what => muc_iq_set_register_info_failed,
×
1082
                         host_type => HostType, sub_host => MucHost,
1083
                         from_jid => jid:to_binary(From), nick => Nick,
1084
                         reason => ErrorReason}),
×
1085
            {error, mongoose_xmpp_errors:internal_server_error()}
×
1086
    end.
1087

1088
-spec iq_set_unregister_info(host_type(), jid:server(),
1089
        jid:simple_jid() | jid:jid(), ejabberd:lang())
1090
            -> {'error', exml:element()} | {'result', []}.
1091
iq_set_unregister_info(HostType, MucHost, From, _Lang) ->
1092
    case unset_nick(HostType, MucHost, From) of
54✔
1093
        ok ->
1094
            {result, []};
54✔
1095
        {error, ErrorReason} ->
1096
            ?LOG_ERROR(#{what => muc_iq_set_unregister_info_failed,
×
1097
                         host_type => HostType, sub_host => MucHost,
1098
                         from_jid => jid:to_binary(From), reason => ErrorReason}),
×
1099
            {error, mongoose_xmpp_errors:internal_server_error()}
×
1100
    end.
1101

1102
-spec process_iq_register_set(host_type(), jid:server(),
1103
                              jid:jid(), exml:element(), ejabberd:lang())
1104
            -> {'error', exml:element()} | {'result', []}.
1105
process_iq_register_set(HostType, MucHost, From, SubEl, Lang) ->
1106
    case exml_query:subelement(SubEl, <<"remove">>) of
278✔
1107
        undefined ->
1108
            case mongoose_data_forms:find_and_parse_form(SubEl) of
224✔
1109
                #{type := <<"cancel">>} ->
1110
                    {result, []};
18✔
1111
                #{type := <<"submit">>, kvs := KVs} ->
1112
                    process_register(HostType, MucHost, From, Lang, KVs);
152✔
1113
                {error, Msg} ->
1114
                    {error, mongoose_xmpp_errors:bad_request(Lang, Msg)};
36✔
1115
                _ ->
1116
                    {error, mongoose_xmpp_errors:bad_request(Lang, <<"Invalid form type">>)}
18✔
1117
            end;
1118
        _ ->
1119
            iq_set_unregister_info(HostType, MucHost, From, Lang)
54✔
1120
    end.
1121

1122
-spec process_register(HostType :: host_type(), MucHost :: jid:server(),
1123
                       From :: jid:jid(), Lang :: ejabberd:lang(),
1124
                       KVs :: mongoose_data_forms:kv_map()) ->
1125
    {error, exml:element()} | {result, []}.
1126
process_register(HostType, MucHost, From, Lang, #{<<"nick">> := [Nick]}) ->
1127
    iq_set_register_info(HostType, MucHost, From, Nick, Lang);
134✔
1128
process_register(_HostType, _MucHost, _From, Lang, #{}) ->
1129
    ErrText = <<"You must fill in field \"Nickname\" in the form">>,
18✔
1130
    {error, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)}.
18✔
1131

1132
-spec iq_get_vcard(ejabberd:lang()) -> [exml:element(), ...].
1133
iq_get_vcard(Lang) ->
1134
    [#xmlel{name = <<"FN">>,
×
1135
            children = [#xmlcdata{content = <<"ejabberd/mod_muc">>}]},
1136
     #xmlel{name = <<"URL">>, children = [#xmlcdata{content = ?MONGOOSE_URI}]},
1137
     #xmlel{name = <<"DESC">>,
1138
            children = [#xmlcdata{content =
1139
                                  <<(service_translations:do(Lang, <<"ejabberd MUC module">>))/binary,
1140
                                    "\nCopyright (c) 2003-2011 ProcessOne">>}]}].
1141

1142
-spec broadcast_service_message(muc_host(), binary() | string()) -> ok.
1143
broadcast_service_message(MucHost, Msg) ->
1144
    lists:foreach(
×
1145
      fun(#muc_online_room{pid = Pid}) ->
1146
              gen_fsm_compat:send_all_state_event(
×
1147
                Pid, {service_message, Msg})
1148
      end, get_vh_rooms(MucHost)).
1149

1150
-spec get_vh_rooms(muc_host()) -> [muc_online_room()].
1151
get_vh_rooms(MucHost) ->
1152
    {ok, HostType} = mongoose_domain_api:get_subdomain_host_type(MucHost),
463✔
1153
    mod_muc_online_backend:get_online_rooms(HostType, MucHost).
463✔
1154

1155
-spec get_persistent_vh_rooms(muc_host()) -> [muc_room()].
1156
get_persistent_vh_rooms(MucHost) ->
1157
    {ok, HostType} = mongoose_domain_api:get_subdomain_host_type(MucHost),
463✔
1158
    case mod_muc_backend:get_rooms(HostType, MucHost) of
463✔
1159
        {ok, List} ->
1160
            List;
463✔
1161
        {error, _} ->
1162
            []
×
1163
    end.
1164

1165
-spec node_cleanup(host_type(), node()) -> ok.
1166
node_cleanup(HostType, Node) ->
1167
    mod_muc_online_backend:node_cleanup(HostType, Node).
4✔
1168

1169
%%====================================================================
1170
%% Hooks handlers
1171
%%====================================================================
1172

1173
-spec is_muc_room_owner(Acc, Params, Extra) -> {ok, Acc} when
1174
    Acc :: boolean(),
1175
    Params :: #{room := jid:jid(), user := jid:jid()},
1176
    Extra :: gen_hook:extra().
1177
is_muc_room_owner(true, _, _) ->
1178
    {ok, true};
×
1179
is_muc_room_owner(_, #{room := Room, user := User}, _) ->
1180
    Result = mod_muc_room:is_room_owner(Room, User) =:= {ok, true},
1,416✔
1181
    {ok, Result}.
1,416✔
1182

1183
-spec can_access_room(Acc, Params, Extra) -> {ok, Acc} when
1184
    Acc :: boolean(),
1185
    Params :: #{room := jid:jid(), user := jid:jid()},
1186
    Extra :: gen_hook:extra().
1187
can_access_room(true, _, _) ->
1188
    {ok, true};
×
1189
can_access_room(_, #{room := Room, user := User}, _) ->
1190
    Result = case mod_muc_room:can_access_room(Room, User) of
4,485✔
1191
        {error, _} -> false;
350✔
1192
        {ok, CanAccess} -> CanAccess
4,135✔
1193
    end,
1194
    {ok, Result}.
4,485✔
1195

1196
-spec remove_domain(Acc, Params, Extra) -> {ok, Acc} when
1197
    Acc :: mongoose_hooks:simple_acc(),
1198
    Params :: #{domain := jid:lserver()},
1199
    Extra :: gen_hook:extra().
1200
 remove_domain(Acc, #{domain := Domain}, #{host_type := HostType}) ->
1201
    MUCHost = server_host_to_muc_host(HostType, Domain),
8✔
1202
    mod_muc_backend:remove_domain(HostType, MUCHost, Domain),
8✔
1203
    {ok, Acc}.
8✔
1204

1205
-spec acc_room_affiliations(Acc, Params, Extra) -> {ok, Acc} when
1206
    Acc :: mongoose_acc:t(),
1207
    Params :: #{room := jid:jid()},
1208
    Extra :: gen_hook:extra().
1209
acc_room_affiliations(Acc, #{room := Room}, _) ->
1210
    NewAcc = case mongoose_acc:get(?MODULE, {affiliations, Room}, {error, not_found}, Acc) of
1,405✔
1211
        {error, _} ->
1212
            case mod_muc_room:get_room_users(Room) of
1,405✔
1213
                {error, not_found} ->
1214
                    Acc;
1,405✔
1215
                {ok, _Affs} = Res ->
1216
                    mongoose_acc:set(?MODULE, {affiliations, Room}, Res, Acc)
×
1217
            end;
1218
        _Affs ->
1219
            Acc
×
1220
    end,
1221
    {ok, NewAcc}.
1,405✔
1222

1223
-spec can_access_identity(Acc, Params, Extra) -> {ok, Acc} when
1224
    Acc :: boolean(),
1225
    Params :: #{room := jid:jid(), user := jid:jid()},
1226
    Extra :: gen_hook:extra().
1227
can_access_identity(true, _, _) ->
1228
    {ok, true};
×
1229
can_access_identity(_, #{room := Room, user := User}, _) ->
1230
    Result = case mod_muc_room:can_access_identity(Room, User) of
2,895✔
1231
        {error, _} -> false;
294✔
1232
        {ok, CanAccess} -> CanAccess
2,601✔
1233
    end,
1234
    {ok, Result}.
2,895✔
1235

1236
-spec disco_local_items(Acc, Params, Extra) -> {ok, Acc} when
1237
    Acc :: mongoose_disco:item_acc(),
1238
    Params :: map(),
1239
    Extra :: gen_hook:extra().
1240
disco_local_items(Acc = #{host_type := HostType,
1241
                          to_jid := #jid{lserver = ServerHost},
1242
                          node := <<>>}, _, _) ->
1243
    MUCHost = server_host_to_muc_host(HostType, ServerHost),
136✔
1244
    Items = [#{jid => MUCHost, node => ?NS_MUC}],
136✔
1245
    {ok, mongoose_disco:add_items(Items, Acc)};
136✔
1246
disco_local_items(Acc, _, _) ->
1247
    {ok, Acc}.
×
1248

1249
-spec node_cleanup_for_host_type(Acc, Params, Extra) -> {ok, Acc} when
1250
    Acc :: mongoose_disco:item_acc(),
1251
    Params :: map(),
1252
    Extra :: gen_hook:extra().
1253
node_cleanup_for_host_type(Acc, #{node := Node}, #{host_type := HostType}) ->
1254
    node_cleanup(HostType, Node),
4✔
1255
    {ok, Acc}.
4✔
1256

1257
-spec count_rooms(mongooseim:host_type()) -> mongoose_instrument:measurements().
1258
count_rooms(HostType) ->
1259
    try all_room_pids(HostType) of
804✔
1260
        AllRooms ->
1261
            InitialCounts = #{online => 0, hibernated => 0},
804✔
1262
            lists:foldl(fun count_rooms/2, InitialCounts, AllRooms)
804✔
1263
    catch exit:{noproc, _} ->
UNCOV
1264
            #{}
×
1265
    end.
1266

1267
all_room_pids(HostType) ->
1268
    Supervisor = gen_mod:get_module_proc(HostType, ejabberd_mod_muc_sup),
804✔
1269
    [Pid || {undefined, Pid, worker, _} <- supervisor:which_children(Supervisor)].
804✔
1270

1271
count_rooms(Pid, Counts = #{online := Online, hibernated := Hibernated}) ->
1272
    case erlang:process_info(Pid, current_function) of
4,922✔
1273
        {current_function, {erlang, hibernate, _}} ->
1274
            #{online => Online + 1, hibernated => Hibernated + 1};
2,354✔
1275
        _ ->
1276
            Counts#{online := Online + 1}
2,568✔
1277
    end.
1278

1279
-spec config_metrics(mongooseim:host_type()) -> [{gen_mod:opt_key(), gen_mod:opt_value()}].
1280
config_metrics(HostType) ->
1281
    mongoose_module_metrics:opts_for_module(HostType, ?MODULE, [backend, online_backend]).
240✔
1282

1283
-spec hooks(mongooseim:host_type()) -> gen_hook:hook_list().
1284
hooks(HostType) ->
1285
    [{is_muc_room_owner, HostType, fun ?MODULE:is_muc_room_owner/3, #{}, 50},
714✔
1286
     {can_access_room, HostType, fun ?MODULE:can_access_room/3, #{}, 50},
1287
     {remove_domain, HostType, fun ?MODULE:remove_domain/3, #{}, 50},
1288
     {acc_room_affiliations, HostType, fun ?MODULE:acc_room_affiliations/3, #{}, 50},
1289
     {can_access_identity, HostType, fun ?MODULE:can_access_identity/3, #{}, 50},
1290
     {disco_local_items, HostType, fun ?MODULE:disco_local_items/3, #{}, 250},
1291
     {node_cleanup_for_host_type, HostType, fun ?MODULE:node_cleanup_for_host_type/3, #{}, 50}].
1292

1293
-spec instrumentation(host_type()) -> [mongoose_instrument:spec()].
1294
instrumentation(HostType) ->
1295
    [{mod_muc_deep_hibernations, #{host_type => HostType},
725✔
1296
      #{metrics => #{count => spiral}}},
1297
     {mod_muc_process_recreations, #{host_type => HostType},
1298
      #{metrics => #{count => spiral}}},
1299
     {mod_muc_hibernations, #{host_type => HostType},
1300
      #{metrics => #{count => spiral}}},
1301
     {mod_muc_rooms, #{host_type => HostType},
1302
      #{probe => #{module => ?MODULE}, metrics => #{online => gauge, hibernated => gauge}}}].
1303

1304
-spec probe(mongoose_instrument:event_name(), mongoose_instrument:labels()) ->
1305
          mongoose_instrument:measurements().
1306
probe(mod_muc_rooms, #{host_type := HostType}) ->
1307
    count_rooms(HostType).
804✔
1308

1309
subdomain_pattern(HostType) ->
1310
    gen_mod:get_module_opt(HostType, ?MODULE, host).
868✔
1311

1312
server_host_to_muc_host(HostType, ServerHost) ->
1313
    mongoose_subdomain_utils:get_fqdn(subdomain_pattern(HostType), ServerHost).
868✔
1314

1315
make_server_host(To, #muc_state{host_type = HostType,
1316
                                subdomain_pattern = SubdomainPattern}) ->
1317
    case SubdomainPattern of
33,696✔
1318
        {prefix, _} ->
1319
            mod_muc_light_utils:room_jid_to_server_host(To);
33,696✔
1320
        {fqdn, _} ->
1321
            HostType
×
1322
    end.
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc