• 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

79.28
/src/muc/mod_muc_room.erl
1
%%%----------------------------------------------------------------------
2
%%% File    : mod_muc_room.erl
3
%%% Author  : Alexey Shchepin <alexey@process-one.net>
4
%%% Purpose : MUC room stuff
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_room).
27
-author('alexey@process-one.net').
28
-behaviour(gen_fsm_compat).
29

30
%% External exports
31
-export([start_link/1,
32
         start_new/11,
33
         start_restored/9,
34
         route/5,
35
         stop/1]).
36

37
%% API exports
38
-export([get_room_users/1,
39
         get_room_affiliations/1,
40
         set_admin_items/3,
41
         get_room_config/1,
42
         change_room_config/2,
43
         delete_room/2,
44
         is_room_owner/2,
45
         can_access_room/2,
46
         can_access_identity/2]).
47

48
%% gen_fsm callbacks
49
-export([init/1,
50
         normal_state/2,
51
         normal_state/3,
52
         locked_state/2,
53
         initial_state/2,
54
         handle_event/3,
55
         handle_sync_event/4,
56
         handle_info/3,
57
         terminate/3,
58
         code_change/4]).
59

60
-ignore_xref([initial_state/2, locked_state/2, normal_state/2, normal_state/3, start_link/1]).
61

62
-import(mongoose_lib, [maps_append/3,
63
                       maps_foreach/2,
64
                       pairs_foreach/2,
65
                       maps_or_pairs_foreach/2]).
66

67
-include("mongoose.hrl").
68
-include("jlib.hrl").
69
-include("mod_muc_room.hrl").
70

71
-record(routed_message, {allowed,
72
                         type,
73
                         from,
74
                         packet,
75
                         lang
76
                        }).
77
-type routed_message() :: #routed_message{}.
78

79
-record(routed_nick_message, {allow_pm,
80
                              online,
81
                              type,
82
                              from,
83
                              nick,
84
                              lang,
85
                              packet,
86
                              decide,
87
                              jid
88
                            }).
89
-type routed_nick_message() :: #routed_nick_message{}.
90

91
-record(routed_iq, {iq,
92
                    from,
93
                    packet
94
                   }).
95
-type routed_iq() :: #routed_iq{}.
96

97
-record(routed_nick_iq, {allow_query,
98
                         online,
99
                         iq,
100
                         packet,
101
                         lang,
102
                         nick,
103
                         jid,
104
                         from,
105
                         stanza
106
                       }).
107
-type routed_nick_iq() :: #routed_nick_iq{}.
108

109
%%%----------------------------------------------------------------------
110
%%% Types
111
%%%----------------------------------------------------------------------
112
-export_type([config/0, user/0, activity/0]).
113

114
-type statename() :: 'locked_state' | 'normal_state'.
115
-type fsm_return() :: {'next_state', statename(), state()}
116
                    | {'next_state', statename(), state(), timeout() | hibernate}
117
                    | {'stop', any(), state()}.
118

119
-type lqueue() :: #lqueue{}.
120
-type state() :: #state{}.
121
-type config() :: #config{}.
122
-type user() :: #user{}.
123
-type activity() :: #activity{}.
124
-type stanzaid() :: {binary(), jid:resource()}.
125
-type new_user_strategy() :: 'allowed'
126
                           | 'conflict_registered'
127
                           | 'conflict_use'
128
                           | 'invalid_password'
129
                           | 'limit_reached'
130
                           | 'require_membership'
131
                           | 'require_password'
132
                           | 'user_banned'
133
                           | 'http_auth'.
134
-type users_map() :: #{jid:simple_jid() => user()}.
135
-type users_pairs() :: [{jid:simple_jid(), user()}].
136
-type sessions_map() :: #{mod_muc:nick() => jid:jid()}.
137
-type affiliations_map() :: #{jid:simple_jid() => mod_muc:affiliation()}.
138

139

140
-type update_inbox_for_muc_payload() :: #{
141
        host_type := mongooseim:host_type(),
142
        room_jid := jid:jid(),
143
        from_jid := jid:jid(),
144
        from_room_jid := jid:jid(),
145
        packet := exml:element(),
146
        affiliations_map := affiliations_map()
147
       }.
148
-export_type([update_inbox_for_muc_payload/0]).
149

150
-define(MAX_USERS_DEFAULT_LIST,
151
        [5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]).
152

153
%%%----------------------------------------------------------------------
154
%%% API
155
%%%----------------------------------------------------------------------
156

157
-spec start_new(HostType :: mongooseim:host_type(), Host :: jid:lserver(), ServerHost :: jid:lserver(),
158
            Access :: _, Room :: mod_muc:room(), HistorySize :: integer(),
159
            RoomShaper :: mongoose_shaper:shaper(), HttpAuthPool :: none | mongoose_http_client:pool(),
160
            Creator :: jid:jid(), Nick :: mod_muc:nick(),
161
            DefRoomOpts :: list()) -> {ok, pid()}.
162
start_new(HostType, Host, ServerHost, Access, Room,
163
          HistorySize, RoomShaper, HttpAuthPool, Creator, Nick, DefRoomOpts) ->
164
    Supervisor = gen_mod:get_module_proc(HostType, ejabberd_mod_muc_sup),
6,361✔
165
    Args = #{init_type => start_new, host_type => HostType,
6,361✔
166
             muc_host => Host, server_host => ServerHost, access => Access,
167
             room_name => Room, history_size => HistorySize,
168
             room_shaper => RoomShaper, http_auth_pool => HttpAuthPool,
169
             creator => Creator, nick => Nick, def_opts => DefRoomOpts},
170
    supervisor:start_child(Supervisor, [Args]).
6,361✔
171

172
-spec start_restored(HostType :: mongooseim:host_type(), Host :: jid:lserver(), ServerHost :: jid:lserver(),
173
            Access :: _, Room :: mod_muc:room(), HistorySize :: integer(),
174
            RoomShaper :: mongoose_shaper:shaper(), HttpAuthPool :: none | mongoose_http_client:pool(),
175
            Opts :: list()) -> {ok, pid()}.
176
start_restored(HostType, Host, ServerHost, Access, Room,
177
               HistorySize, RoomShaper, HttpAuthPool, Opts)
178
    when is_list(Opts) ->
179
    Supervisor = gen_mod:get_module_proc(HostType, ejabberd_mod_muc_sup),
65✔
180
    Args = #{init_type => start_restored, host_type => HostType,
65✔
181
             muc_host => Host, server_host => ServerHost,
182
             access => Access, room_name => Room, history_size => HistorySize,
183
             room_shaper => RoomShaper, http_auth_pool => HttpAuthPool,
184
             opts => Opts},
185
    supervisor:start_child(Supervisor, [Args]).
65✔
186

187
start_link(Args = #{}) ->
188
    gen_fsm_compat:start_link(?MODULE, Args, []).
6,426✔
189

190
stop(Pid) ->
191
    gen_fsm_compat:stop(Pid).
505✔
192

193
-spec get_room_users(RoomJID :: jid:jid()) ->
194
    {ok, [user()]} | {error, not_found}.
195
get_room_users(RoomJID) ->
196
    case mod_muc:room_jid_to_pid(RoomJID) of
2,275✔
197
        {ok, Pid} ->
198
            gen_fsm_compat:sync_send_all_state_event(Pid, get_room_users);
771✔
199
        {error, Reason} ->
200
            {error, Reason}
1,504✔
201
    end.
202

203
-spec get_room_affiliations(RoomJID :: jid:jid()) ->
204
    {ok, affiliations_map()} | {error, not_found}.
205
get_room_affiliations(RoomJID) ->
206
    case mod_muc:room_jid_to_pid(RoomJID) of
585✔
207
        {ok, Pid} ->
208
            gen_fsm_compat:sync_send_all_state_event(Pid, get_room_affiliations);
541✔
209
        {error, Reason} ->
210
            {error, Reason}
44✔
211
    end.
212

213
-spec is_room_owner(RoomJID :: jid:jid(), UserJID :: jid:jid()) ->
214
    {ok, boolean()} | {error, not_found}.
215
is_room_owner(RoomJID, UserJID) ->
216
    case mod_muc:room_jid_to_pid(RoomJID) of
1,515✔
217
        {ok, Pid} ->
218
            gen_fsm_compat:sync_send_all_state_event(Pid, {is_room_owner, UserJID});
1,482✔
219
        {error, Reason} ->
220
            {error, Reason}
33✔
221
    end.
222

223
-type error_xml() :: #xmlel{}.
224
-type item_xml() :: #xmlel{}.
225

226
-spec set_admin_items(jid:jid(), jid:jid(), [item_xml()]) ->
227
    ok | {error, not_found | error_xml()}.
228
set_admin_items(RoomJID, ModJID, Items) ->
229
    case mod_muc:room_jid_to_pid(RoomJID) of
586✔
230
        {ok, Pid} ->
231
            gen_fsm_compat:sync_send_event(Pid, {set_admin_items, ModJID, Items});
575✔
232
        {error, Reason} ->
233
            {error, Reason}
11✔
234
    end.
235

236
-spec get_room_config(jid:jid()) ->
237
    {ok, config()} | {error, not_found}.
238
get_room_config(RoomJID) ->
239
    case mod_muc:room_jid_to_pid(RoomJID) of
208✔
240
        {ok, Pid} ->
241
            gen_fsm_compat:sync_send_all_state_event(Pid, get_config);
186✔
242
        {error, Reason} ->
243
            {error, Reason}
22✔
244
    end.
245

246
-spec change_room_config(jid:jid(), config()) ->
247
    {ok, config()} | {error, not_found}.
248
change_room_config(RoomJID, NewConfig) ->
249
    case mod_muc:room_jid_to_pid(RoomJID) of
41✔
250
        {ok, Pid} ->
251
            gen_fsm_compat:sync_send_all_state_event(Pid, {change_config, NewConfig});
41✔
252
        {error, Reason} ->
253
            {error, Reason}
×
254
    end.
255

256
-spec delete_room(jid:jid(), binary()) ->
257
    ok | {error, not_found}.
258
delete_room(RoomJID, ReasonIn) ->
259
    case mod_muc:room_jid_to_pid(RoomJID) of
104✔
260
        {ok, Pid} ->
261
            gen_fsm_compat:send_all_state_event(Pid, {destroy, ReasonIn});
52✔
262
        {error, Reason} ->
263
            {error, Reason}
52✔
264
    end.
265

266
%% @doc Return true if UserJID can read room messages
267
-spec can_access_room(RoomJID :: jid:jid(), UserJID :: jid:jid()) ->
268
            {ok, boolean()} | {error, not_found}.
269
can_access_room(RoomJID, UserJID) ->
270
    case mod_muc:room_jid_to_pid(RoomJID) of
4,961✔
271
        {ok, Pid} ->
272
            gen_fsm_compat:sync_send_all_state_event(Pid, {can_access_room, UserJID});
4,488✔
273
        Error ->
274
            Error
473✔
275
    end.
276

277
%% @doc Return true if UserJID can read real user JIDs
278
-spec can_access_identity(RoomJID :: jid:jid(), UserJID :: jid:jid()) ->
279
    {ok, boolean()} | {error, not_found}.
280
can_access_identity(RoomJID, UserJID) ->
281
    case mod_muc:room_jid_to_pid(RoomJID) of
2,917✔
282
        {ok, Pid} ->
283
            gen_fsm_compat:sync_send_all_state_event(Pid, {can_access_identity, UserJID});
2,623✔
284
        {error, Reason} ->
285
            {error, Reason}
294✔
286
    end.
287

288
%%%----------------------------------------------------------------------
289
%%% Callback functions from gen_fsm
290
%%%----------------------------------------------------------------------
291

292
%% @doc A room is created. Depending on request type (MUC/groupchat 1.0) the
293
%% next state is determined accordingly (a locked room for MUC or an instant
294
%% one for groupchat).
295
-spec init(map()) ->
296
    {ok, statename(), state()} | {ok, statename(), state(), timeout()}.
297
init(#{init_type := start_new} = Args) ->
298
    init_new(Args);
6,361✔
299
init(#{init_type := start_restored} = Args) ->
300
    init_restored(Args).
65✔
301

302
init_new(#{init_type := start_new, host_type := HostType, muc_host := Host,
303
           server_host := ServerHost, access := Access, room_name := Room,
304
           history_size := HistorySize, room_shaper := RoomShaper,
305
           http_auth_pool := HttpAuthPool, creator := Creator, nick := _Nick,
306
           def_opts := DefRoomOpts}) when is_list(DefRoomOpts) ->
307
    process_flag(trap_exit, true),
6,361✔
308
    Shaper = mongoose_shaper:new(RoomShaper),
6,361✔
309
    State = #state{host = Host, host_type = HostType, server_host = ServerHost,
6,361✔
310
                   access = Access,
311
                   room = Room,
312
                   history = lqueue_new(HistorySize),
313
                   jid = jid:make_bare(Room, Host),
314
                   just_created = true,
315
                   room_shaper = Shaper,
316
                   http_auth_pool = HttpAuthPool,
317
                   hibernate_timeout = read_hibernate_timeout(HostType)},
318
    State1 = set_opts(DefRoomOpts, State),
6,361✔
319
    State2 = set_affiliation(Creator, owner, State1),
6,361✔
320
    ?LOG_INFO(ls(#{what => muc_room_started,
6,361✔
321
                   creator_jid => jid:to_binary(Creator)}, State)),
6,361✔
322
    add_to_log(room_existence, created, State2),
6,361✔
323
    State3 = case proplists:get_value(subject, DefRoomOpts, none) of
6,361✔
324
        none ->
325
            State2;
5,667✔
326
        _ ->
327
            set_opts([{subject_timestamp, get_current_timestamp()}], State2)
694✔
328
    end,
329
    case proplists:get_value(instant, DefRoomOpts, false) of
6,361✔
330
        true ->
331
            %% Instant room -- groupchat 1.0 request
332
            add_to_log(room_existence, started, State3),
5,834✔
333
            save_persistent_room_state(State3),
5,834✔
334
            {ok, normal_state, State3, State3#state.hibernate_timeout};
5,834✔
335
        false ->
336
            %% Locked room waiting for configuration -- MUC request
337
            {ok, initial_state, State3}
527✔
338
    end.
339

340
%% @doc A room is restored
341
init_restored(#{init_type := start_restored,
342
                host_type := HostType, muc_host := Host,
343
                server_host := ServerHost, access := Access,
344
                room_name := Room, history_size := HistorySize,
345
                room_shaper := RoomShaper, http_auth_pool := HttpAuthPool,
346
                opts := Opts}) ->
347
    process_flag(trap_exit, true),
65✔
348
    Shaper = mongoose_shaper:new(RoomShaper),
65✔
349
    RoomJid = jid:make_bare(Room, Host),
65✔
350
    State = set_opts(Opts, #state{host = Host, host_type = HostType,
65✔
351
                                  server_host = ServerHost,
352
                                  access = Access,
353
                                  room = Room,
354
                                  history = lqueue_new(HistorySize),
355
                                  jid = RoomJid,
356
                                  room_shaper = Shaper,
357
                                  http_auth_pool = HttpAuthPool,
358
                                  hibernate_timeout = read_hibernate_timeout(HostType)
359
                                 }),
360
    add_to_log(room_existence, started, State),
65✔
361
    mongoose_instrument:execute(mod_muc_process_recreations, #{host_type => HostType},
65✔
362
                                #{count => 1, jid => RoomJid}),
363
    {ok, normal_state, State, State#state.hibernate_timeout}.
65✔
364

365
%% @doc In the locked state StateData contains the same settings it previously
366
%% held for the normal_state. The fsm awaits either a confirmation or a
367
%% configuration form from the creator. Responds with error to any other queries.
368
-spec locked_error({'route', jid:jid(), _, mongoose_acc:t(), exml:element()},
369
                   statename(), state()) -> fsm_return().
370
locked_error({route, From, ToNick, Acc, Packet}, NextState, StateData) ->
371
    ?LOG_INFO(ls(#{what => muc_route_to_locked_room, acc => Acc}, StateData)),
11✔
372
    ErrText = <<"This room is locked">>,
11✔
373
    Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>),
11✔
374
    {Acc1, Err} = jlib:make_error_reply(Acc, Packet, mongoose_xmpp_errors:item_not_found(Lang, ErrText)),
11✔
375
    ejabberd_router:route(jid:replace_resource(StateData#state.jid,
11✔
376
                                               ToNick),
377
                          From, Acc1, Err),
378
    {next_state, NextState, StateData}.
11✔
379

380
%% @doc  Receive the room-creating Stanza. Will crash if any other stanza is
381
%% received in this state.
382
-spec initial_state({'route', From :: jid:jid(), To :: mod_muc:nick(),
383
                    Acc :: mongoose_acc:t(), Presence :: exml:element()}, state()) -> fsm_return().
384
initial_state({route, From, ToNick, _Acc, % TOODOO
385
              #xmlel{name = <<"presence">>} = Presence}, StateData) ->
386
    %% this should never happen so crash if it does
387
    <<>> = exml_query:attr(Presence, <<"type">>, <<>>),
516✔
388
    owner = get_affiliation(From, StateData), %% prevent race condition (2 users create same room)
516✔
389
    XNamespaces = exml_query:paths(Presence, [{element, <<"x">>}, {attr, <<"xmlns">>}]),
516✔
390
    case lists:member(?NS_MUC, XNamespaces) of
516✔
391
        true ->
392
            %% FIXME
393
            add_to_log(room_existence, started, StateData),
516✔
394
            process_presence(From, ToNick, Presence, StateData, locked_state);
516✔
395
            %% The fragment of normal_state with Activity that used to do this - how does that work?
396
            %% Seems to work without it
397
        false ->
398
            %% groupchat 1.0 user, straight to normal_state
399
            process_presence(From, ToNick, Presence, StateData)
×
400
    end.
401

402
-spec is_query_allowed(exml:element()) -> boolean().
403
is_query_allowed(#xmlel{children = Els}) ->
404
    case jlib:remove_cdata(Els) of
450✔
405
        [#xmlel{name = <<"destroy">>}] ->
406
            true;
33✔
407
        [El] ->
408
            mongoose_data_forms:is_form(El, [<<"submit">>, <<"cancel">>]);
417✔
409
        _ ->
410
            false
×
411
    end.
412

413
-spec locked_state_process_owner_iq(jid:jid(), exml:element(),
414
        ejabberd:lang(), 'error' | 'get' | 'invalid' | 'result', _)
415
            -> {{'error', exml:element()}, statename()}
416
               | {{result, [exml:child()], state() | stop}, statename()}.
417
locked_state_process_owner_iq(From, Query, Lang, set, StateData) ->
418
    Result = case is_query_allowed(Query) of
450✔
419
                 true ->
420
                     process_iq_owner(From, set, Lang, Query, StateData, locked_state);
450✔
421
                 false ->
422
                     {error, mongoose_xmpp_errors:item_not_found(Lang, <<"Query not allowed">>)}
×
423
             end,
424
    {Result, normal_state};
450✔
425
locked_state_process_owner_iq(From, Query, Lang, get, StateData) ->
426
    {process_iq_owner(From, get, Lang, Query, StateData, locked_state), locked_state};
44✔
427
locked_state_process_owner_iq(_From, _Query, Lang, _Type, _StateData) ->
428
    {{error, mongoose_xmpp_errors:item_not_found(Lang, <<"Wrong type">>)}, locked_state}.
×
429

430

431
%% @doc Destroy room / confirm instant room / configure room
432
-spec locked_state({'route', From :: jid:jid(), To :: mod_muc:nick(),
433
                    Acc :: mongoose_acc:t(), Packet :: exml:element()}, state()) -> fsm_return().
434
locked_state({route, From, _ToNick, Acc,
435
              #xmlel{name = <<"iq">>} = Packet}, StateData) ->
436
    #iq{lang = Lang, sub_el = Query, xmlns = NS} = IQ = jlib:iq_query_info(Packet),
505✔
437
    {Result, NextState1} =
505✔
438
        case {NS, get_affiliation(From, StateData)} of
439
            {?NS_MUC_OWNER, owner} ->
440
                locked_state_process_owner_iq(From, Query, Lang, IQ#iq.type, StateData);
494✔
441
            {?NS_DISCO_INFO, owner} ->
442
                {process_iq_disco_info(From, IQ#iq.type, Lang, StateData), locked_state};
11✔
443
            _ ->
444
                ErrText = <<"This room is locked">>,
×
445
                {{error, mongoose_xmpp_errors:item_not_found(Lang, ErrText)}, locked_state}
×
446
        end,
447
    MkQueryResult = fun(Res) ->
505✔
448
                        IQ#iq{type = result,
494✔
449
                            sub_el = [#xmlel{name = <<"query">>,
450
                                             attrs = #{<<"xmlns">> => NS},
451
                                             children = Res}]}
452
                    end,
453
    {IQRes, StateData3, NextState2} =
505✔
454
        case Result of
455
            {result, InnerRes, stop} -> {MkQueryResult(InnerRes), StateData, stop};
55✔
456
            {result, InnerRes, StateData2} -> {MkQueryResult(InnerRes), StateData2, NextState1};
439✔
457
            {error, Error} -> {IQ#iq{type = error, sub_el = [Query, Error]}, StateData, NextState1}
11✔
458
        end,
459
    ejabberd_router:route(StateData3#state.jid, From, Acc, jlib:iq_to_xml(IQRes)),
505✔
460
    case NextState2 of
505✔
461
        stop ->
462
            {stop, normal, StateData3};
55✔
463
        locked_state ->
464
            {next_state, NextState2, StateData3};
55✔
465
        normal_state ->
466
            next_normal_state(StateData3#state{just_created = false})
395✔
467
    end;
468
%% Let owner leave. Destroy the room.
469
locked_state({route, From, ToNick, _Acc,
470
              #xmlel{name = <<"presence">>} = Presence} = Call,
471
             StateData) ->
472
    case exml_query:attr(Presence, <<"type">>) =:= <<"unavailable">>
44✔
473
        andalso get_affiliation(From, StateData)  =:= owner of
33✔
474
        true ->
475
            %% Will let the owner leave and destroy the room if it's not persistant
476
            %% The rooms are not persistent by default, but just to be safe...
477
            NewConfig = (StateData#state.config)#config{persistent = false},
33✔
478
            StateData1 = StateData#state{config = NewConfig},
33✔
479
            process_presence(From, ToNick, Presence, StateData1, locked_state);
33✔
480
        _ ->
481
            locked_error(Call, locked_state, StateData)
11✔
482
    end;
483
locked_state(timeout, StateData) ->
484
    {next_state, locked_state, StateData};
×
485
locked_state(Call, StateData) ->
486
    locked_error(Call, locked_state, StateData).
×
487

488

489
-spec normal_state({route, From :: jid:jid(), To :: mod_muc:nick(), Acc :: mongoose_acc:t(),
490
                   Packet :: exml:element()}, state()) -> fsm_return().
491
normal_state({route, From, <<>>, _Acc, #xmlel{name = <<"message">>} = Packet}, StateData) ->
492
    Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>),
8,886✔
493
    Type = exml_query:attr(Packet, <<"type">>, <<>>),
8,886✔
494

495
    NewStateData = route_message(#routed_message{
8,886✔
496
        allowed = can_send_to_conference(From, StateData),
497
        type = Type,
498
        from = From,
499
        packet = Packet,
500
        lang = Lang}, StateData),
501
    next_normal_state(NewStateData);
8,886✔
502
normal_state({route, From, <<>>, Acc0, #xmlel{name = <<"iq">>} = Packet}, StateData) ->
503
    {IQ, Acc} = mongoose_iq:info(Acc0),
6,926✔
504
    {RoutingEffect, NewStateData} = route_iq(Acc, #routed_iq{
6,926✔
505
        iq = IQ,
506
        from = From,
507
        packet = Packet}, StateData),
508
    case RoutingEffect of
6,926✔
509
        ok -> next_normal_state(NewStateData);
6,904✔
510
        stop -> {stop, normal, NewStateData}
22✔
511
    end;
512
normal_state({route, From, Nick, _Acc, #xmlel{name = <<"presence">>} = Packet}, StateData) ->
513
    % FIXME sessions do we need to route presences to all sessions
514
    Activity = get_user_activity(From, StateData),
14,425✔
515
    Now = os:system_time(microsecond),
14,425✔
516
    MinPresenceInterval = trunc(get_opt(StateData, min_presence_interval) * 1000000),
14,425✔
517
    case (Now >= Activity#activity.presence_time + MinPresenceInterval) and
14,425✔
518
         (Activity#activity.presence == undefined) of
519
        true ->
520
            NewActivity = Activity#activity{presence_time = Now},
14,425✔
521
            StateData1 = store_user_activity(From, NewActivity, StateData),
14,425✔
522
            process_presence(From, Nick, Packet, StateData1);
14,425✔
523
        false ->
524
            case Activity#activity.presence == undefined of
×
525
                true ->
526
                    Interval = (Activity#activity.presence_time +
×
527
                                MinPresenceInterval - Now) div 1000,
528
                    erlang:send_after(Interval, self(), {process_user_presence, From});
×
529
                false ->
530
                    ok
×
531
            end,
532
            NewActivity = Activity#activity{presence = {Nick, Packet}},
×
533
            StateData1 = store_user_activity(From, NewActivity, StateData),
×
534
            next_normal_state(StateData1)
×
535
    end;
536
normal_state({route, From, ToNick, _Acc, #xmlel{name = <<"message">>} = Packet}, StateData) ->
537
    Type = exml_query:attr(Packet, <<"type">>, <<>>),
284✔
538
    FunRouteNickMessage = fun(JID, StateDataAcc) ->
284✔
539
        route_nick_message(#routed_nick_message{
295✔
540
        allow_pm = (StateDataAcc#state.config)#config.allow_private_messages,
541
        online = is_user_online(From, StateDataAcc),
542
        type = Type,
543
        from = From,
544
        nick = ToNick,
545
        lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>),
546
        decide = decide_fate_message(Type, Packet, From, StateDataAcc),
547
        packet = Packet,
548
        jid = JID}, StateDataAcc)
549
    end,
550
    NewStateData = case find_jids_by_nick(ToNick, StateData) of
284✔
551
        [] -> FunRouteNickMessage(false, StateData);
12✔
552
        JIDs -> lists:foldl(FunRouteNickMessage, StateData, JIDs)
272✔
553
    end,
554
    next_normal_state(NewStateData);
284✔
555
normal_state({route, From, ToNick, _Acc, #xmlel{name = <<"iq">>} = Packet}, StateData) ->
556
    Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>),
×
557
    StanzaId = exml_query:attr(Packet, <<"id">>, <<>>),
×
558
    FunRouteNickIq = fun(JID) ->
×
559
        route_nick_iq(#routed_nick_iq{
×
560
            allow_query = (StateData#state.config)#config.allow_query_users,
561
            online = is_user_online_iq(StanzaId, From, StateData),
562
            jid = JID,
563
            iq = jlib:iq_query_info(Packet),
564
            packet = Packet,
565
            lang = Lang,
566
            from = From,
567
            stanza = StanzaId,
568
            nick = ToNick}, StateData)
569
    end,
570
    case find_jids_by_nick(ToNick, StateData) of
×
571
        [] -> FunRouteNickIq(false);
×
572
        JIDs -> lists:foreach(FunRouteNickIq, JIDs)
×
573
    end,
574
    next_normal_state(StateData);
×
575
normal_state({http_auth, AuthPid, Result, From, Nick, Packet, Role}, StateData) ->
576
    AuthPids = StateData#state.http_auth_pids,
11✔
577
    StateDataWithoutPid = StateData#state{http_auth_pids = lists:delete(AuthPid, AuthPids)},
11✔
578
    NewStateData = handle_http_auth_result(Result, From, Nick, Packet, Role, StateDataWithoutPid),
11✔
579
    destroy_temporary_room_if_empty(NewStateData, normal_state);
11✔
580
normal_state(timeout, StateData = #state{host_type = HostType, jid = RoomJid}) ->
581
    erlang:put(hibernated, os:timestamp()),
1,347✔
582
    mongoose_instrument:execute(mod_muc_hibernations, #{host_type => HostType},
1,347✔
583
                                #{count => 1, jid => RoomJid}),
584
    {next_state, normal_state, StateData, hibernate};
1,347✔
585
normal_state(_Event, StateData) ->
586
    next_normal_state(StateData).
×
587

588
normal_state({set_admin_items, UJID, Items}, _From,
589
             #state{hibernate_timeout = Timeout} = StateData) ->
590
    case process_admin_items_set(UJID, Items, <<"en">>, StateData) of
575✔
591
        {result, [], StateData2} ->
592
            {reply, ok, normal_state, StateData2, Timeout};
446✔
593
        {error, Error} ->
594
            {reply, {error, Error}, normal_state, StateData, Timeout}
129✔
595
    end.
596

597
handle_event({service_message, Msg}, _StateName, StateData) ->
598
    MessagePkt = #xmlel{name = <<"message">>,
×
599
                        attrs = #{<<"type">> => <<"groupchat">>},
600
                        children = [#xmlel{name = <<"body">>,
601
                                           children = [#xmlcdata{content = Msg}]}]},
602
    send_to_all_users(MessagePkt, StateData),
×
603
    NSD = add_message_to_history(<<>>,
×
604
                 StateData#state.jid,
605
                 MessagePkt,
606
                 StateData),
607
    next_normal_state(NSD);
×
608

609
handle_event({destroy, Reason}, _StateName, StateData) ->
610
    {result, [], stop} =
3,105✔
611
        destroy_room(
612
          #xmlel{name = <<"destroy">>, attrs = #{<<"xmlns">> => ?NS_MUC_OWNER},
613
                 children = case Reason of
614
                                none -> [];
3,064✔
615
                                _Else ->
616
                                    [#xmlel{name = <<"reason">>,
41✔
617
                                            children = [#xmlcdata{content = Reason}]}]
618
                            end}, StateData),
619
    ?LOG_INFO(ls(#{what => muc_room_destroyed, text => <<"Destroyed MUC room">>,
3,105✔
620
                   reason => Reason}, StateData)),
3,105✔
621
    add_to_log(room_existence, destroyed, StateData),
3,105✔
622
    {stop, shutdown, StateData};
3,105✔
623
handle_event(destroy, StateName, StateData) ->
624
    handle_event({destroy, none}, StateName, StateData);
3,053✔
625

626
handle_event({set_affiliations, Affiliations},
627
             #state{hibernate_timeout = Timeout} = StateName, StateData) ->
628
    {next_state, StateName, StateData#state{affiliations = Affiliations}, Timeout};
×
629

630
handle_event(_Event, StateName, #state{hibernate_timeout = Timeout} = StateData) ->
631
    {next_state, StateName, StateData, Timeout}.
×
632

633
handle_sync_event({get_disco_item, JID, Lang}, _From, StateName, StateData) ->
634
    Reply = get_roomdesc_reply(JID, StateData,
1,975✔
635
                   get_roomdesc_tail(StateData, Lang)),
636
    reply_with_timeout(Reply, StateName, StateData);
1,975✔
637
handle_sync_event(get_config, _From, StateName, StateData) ->
638
    reply_with_timeout({ok, StateData#state.config}, StateName, StateData);
186✔
639
handle_sync_event(get_state, _From, StateName, StateData) ->
640
    reply_with_timeout({ok, StateData}, StateName, StateData);
×
641
handle_sync_event(get_room_users, _From, StateName, StateData) ->
642
    reply_with_timeout({ok, maps:values(StateData#state.users)}, StateName, StateData);
771✔
643
handle_sync_event(get_room_affiliations, _From, StateName, StateData) ->
644
    reply_with_timeout({ok, StateData#state.affiliations}, StateName, StateData);
541✔
645
handle_sync_event({is_room_owner, UserJID}, _From, StateName, StateData) ->
646
    reply_with_timeout({ok, get_affiliation(UserJID, StateData) =:= owner}, StateName, StateData);
1,482✔
647
handle_sync_event({can_access_room, UserJID}, _From, StateName, StateData) ->
648
    reply_with_timeout({ok,  can_read_conference(UserJID, StateData)}, StateName, StateData);
4,488✔
649
handle_sync_event({can_access_identity, UserJID}, _From, StateName, StateData) ->
650
    reply_with_timeout({ok,  can_user_access_identity(UserJID, StateData)}, StateName, StateData);
2,623✔
651
handle_sync_event({change_config, Config}, _From, StateName, StateData) ->
652
    {result, [], NSD} = change_config(Config, StateData),
41✔
653
    reply_with_timeout({ok, NSD#state.config}, StateName, NSD);
41✔
654
handle_sync_event({change_state, NewStateData}, _From, StateName, _StateData) ->
655
    reply_with_timeout({ok, NewStateData}, StateName, NewStateData);
×
656
handle_sync_event(_Event, _From, StateName, StateData) ->
657
    reply_with_timeout(ok, StateName, StateData).
×
658

659
reply_with_timeout(Reply, StateName, #state{hibernate_timeout = Timeout} = State) ->
660
    {reply, Reply, StateName, State, Timeout}.
12,107✔
661

662
code_change(_OldVsn, StateName, StateData, _Extra) ->
663
    {ok, StateName, StateData}.
×
664

665
maybe_prepare_room_queue(RoomQueue, StateData) ->
666
    StateData1 = StateData#state{room_queue = RoomQueue},
×
667
    case queue:is_empty(StateData#state.room_queue) of
×
668
        true ->
669
            StateData2 = prepare_room_queue(StateData1),
×
670
            next_normal_state(StateData2);
×
671
        _ ->
672
            next_normal_state(StateData1)
×
673
    end.
674

675
-type info_msg() :: {process_user_presence | process_user_message, jid:jid()}
676
                    | process_room_queue.
677
-spec handle_info(info_msg(), statename(), state()) -> fsm_return().
678
handle_info({process_user_presence, From}, normal_state = _StateName, StateData) ->
679
    RoomQueue = queue:in({presence, From}, StateData#state.room_queue),
×
680
    maybe_prepare_room_queue(RoomQueue, StateData);
×
681
handle_info({process_user_message, From}, normal_state = _StateName, StateData) ->
682
    RoomQueue = queue:in({message, From}, StateData#state.room_queue),
×
683
    maybe_prepare_room_queue(RoomQueue, StateData);
×
684
handle_info(process_room_queue, normal_state, StateData) ->
685
    case queue:out(StateData#state.room_queue) of
×
686
    {{value, {message, From}}, RoomQueue} ->
687
        Activity = get_user_activity(From, StateData),
×
688
        Packet = Activity#activity.message,
×
689
        NewActivity = Activity#activity{message = undefined},
×
690
        StateData1 =
×
691
        store_user_activity(
692
          From, NewActivity, StateData),
693
        StateData2 =
×
694
        StateData1#state{
695
          room_queue = RoomQueue},
696
        StateData3 = prepare_room_queue(StateData2),
×
697
        process_groupchat_message(From, Packet, StateData3);
×
698
    {{value, {presence, From}}, RoomQueue} ->
699
        Activity = get_user_activity(From, StateData),
×
700
        {Nick, Packet} = Activity#activity.presence,
×
701
        NewActivity = Activity#activity{presence = undefined},
×
702
        StateData1 =
×
703
        store_user_activity(
704
          From, NewActivity, StateData),
705
        StateData2 =
×
706
        StateData1#state{
707
          room_queue = RoomQueue},
708
        StateData3 = prepare_room_queue(StateData2),
×
709
        process_presence(From, Nick, Packet, StateData3);
×
710
    {empty, _} ->
711
            next_normal_state(StateData)
×
712
    end;
713
handle_info({'EXIT', FromPid, _Reason}, StateName, StateData) ->
714
    AuthPids = StateData#state.http_auth_pids,
11✔
715
    StateWithoutPid = StateData#state{http_auth_pids = lists:delete(FromPid, AuthPids)},
11✔
716
    destroy_temporary_room_if_empty(StateWithoutPid, StateName);
11✔
717
handle_info(stop_persistent_room_process, normal_state,
718
            #state{room = RoomName,
719
                   config = #config{persistent = true}} = StateData) ->
720
    maybe_stop_persistent_room(RoomName, is_empty_room(StateData), StateData);
714✔
721
handle_info(_Info, StateName, #state{hibernate_timeout = Timeout} = StateData) ->
722
    {next_state, StateName, StateData, Timeout}.
208✔
723

724
maybe_stop_persistent_room(RoomName, true, State) ->
725
    do_stop_persistent_room(RoomName, State);
538✔
726
maybe_stop_persistent_room(RoomName, _, State) ->
727
    stop_if_only_owner_is_online(RoomName, count_users(State), State).
176✔
728

729
stop_if_only_owner_is_online(RoomName, 1, #state{users = Users, jid = RoomJID} = State) ->
730
    [{LJID, #user{jid = LastUser, nick = Nick}}] = maps:to_list(Users),
33✔
731

732
    case get_affiliation(LastUser, State) of
33✔
733
        owner ->
734
            ItemAttrs = #{<<"affiliation">> => <<"owner">>, <<"role">> => <<"none">>},
33✔
735
            Packet = unavailable_presence(ItemAttrs, <<"Room hibernation">>),
33✔
736
            FromRoom = jid:replace_resource(RoomJID, Nick),
33✔
737
            ejabberd_router:route(FromRoom, LastUser, Packet),
33✔
738
            tab_remove_online_user(LJID, State),
33✔
739
            do_stop_persistent_room(RoomName, State);
33✔
740
        _ ->
741
            next_normal_state(State)
×
742
    end;
743
stop_if_only_owner_is_online(_, _, State) ->
744
    next_normal_state(State).
143✔
745

746
do_stop_persistent_room(_RoomName, State = #state{host_type = HostType, jid = RoomJid}) ->
747
    ?LOG_INFO(ls(#{what => muc_room_stopping_persistent,
571✔
748
                   text => <<"Stopping persistent room's process">>}, State)),
571✔
749
    mongoose_instrument:execute(mod_muc_deep_hibernations, #{host_type => HostType},
571✔
750
                                #{count => 1, jid => RoomJid}),
751
    {stop, normal, State}.
571✔
752

753
%% @doc Purpose: Shutdown the fsm
754
-spec terminate(any(), statename(), state()) -> 'ok'.
755
terminate(Reason, _StateName, StateData) ->
756
    ?LOG_INFO(ls(#{what => muc_room_stopping, text => <<"Stopping room's process">>,
6,203✔
757
                   reason => Reason}, StateData)),
6,203✔
758
    ReasonT = case Reason of
6,203✔
759
          shutdown -> <<"You are being removed from the room because of a system shutdown">>;
3,105✔
760
          _ -> <<"Room terminates">>
3,098✔
761
          end,
762
    ItemAttrs = #{<<"affiliation">> => <<"none">>, <<"role">> => <<"none">>},
6,203✔
763
    Packet = unavailable_presence(ItemAttrs, ReasonT),
6,203✔
764
    maps_foreach(
6,203✔
765
      fun(LJID, Info) ->
766
              Nick = Info#user.nick,
442✔
767
              case Reason of
442✔
768
                  shutdown ->
769
                      ejabberd_router:route(
252✔
770
                        jid:replace_resource(StateData#state.jid, Nick),
771
                        Info#user.jid,
772
                        Packet);
773
                  _ -> ok
190✔
774
              end,
775
              tab_remove_online_user(LJID, StateData)
442✔
776
      end, StateData#state.users),
777
    add_to_log(room_existence, stopped, StateData),
6,203✔
778
    mod_muc:room_destroyed(StateData#state.host_type,
6,203✔
779
                           StateData#state.host,
780
                           StateData#state.room, self()),
781
    ok.
6,197✔
782

783
%%%----------------------------------------------------------------------
784
%%% Internal functions
785
%%%----------------------------------------------------------------------
786

787
unavailable_presence(ItemAttrs, ReasonT) ->
788
    ReasonEl = #xmlel{name = <<"reason">>,
6,236✔
789
                      children = [#xmlcdata{content = ReasonT}]},
790
    #xmlel{name = <<"presence">>,
6,236✔
791
           attrs = #{<<"type">> => <<"unavailable">>},
792
           children = [#xmlel{name = <<"x">>,
793
                              attrs = #{<<"xmlns">> => ?NS_MUC_USER},
794
                              children = [#xmlel{name = <<"item">>,
795
                                                 attrs = ItemAttrs,
796
                                                 children = [ReasonEl]},
797
                                          #xmlel{name = <<"status">>,
798
                                                 attrs = #{<<"code">> => <<"332">>}}
799
                                         ]}]}.
800

801
-spec occupant_jid(user(), 'undefined' | jid:jid()) -> 'error' | jid:jid().
802
occupant_jid(#user{nick=Nick}, RoomJID) ->
803
    jid:replace_resource(RoomJID, Nick).
340✔
804

805

806
-spec route(atom() | pid() | port() | {atom(), _} | {'via', _, _},
807
    From :: jid:jid(), To :: mod_muc:nick(), Acc :: mongoose_acc:t(),
808
    Pkt :: exml:element()) -> 'ok'.
809
route(Pid, From, ToNick, Acc, Packet) ->
810
    gen_fsm_compat:send_event(Pid, {route, From, ToNick, Acc, Packet}).
31,626✔
811

812

813
-spec process_groupchat_message(jid:simple_jid() | jid:jid(),
814
                                exml:element(), state()) -> fsm_return().
815
process_groupchat_message(From, #xmlel{name = <<"message">>} = Packet, StateData) ->
816
    Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>),
8,529✔
817
    case can_send_to_conference(From, StateData) of
8,529✔
818
        true ->
819
            process_message_from_allowed_user(From, Packet, StateData);
8,529✔
820
        false ->
821
            send_error_only_occupants(<<"messages">>, Packet, Lang,
×
822
                                      StateData#state.jid, From),
823
            next_normal_state(StateData)
×
824
    end.
825

826
can_send_to_conference(From, StateData) ->
827
    is_user_online(From, StateData)
17,415✔
828
    orelse
829
    is_allowed_nonparticipant(From, StateData).
151✔
830

831
can_read_conference(UserJID,
832
                    StateData=#state{config = #config{members_only = MembersOnly,
833
                                                      password_protected = Protected}}) ->
834
    Affiliation = get_affiliation(UserJID, StateData),
4,488✔
835
    %% In a members-only chat room, only owners, admins or members can query a room archive.
836
    case {MembersOnly, Protected} of
4,488✔
837
        {_, true} ->
838
            %% For querying password-protected room user should be a member
839
            %% or inside the room
840
            is_user_online(UserJID, StateData)
384✔
841
            orelse
842
            lists:member(Affiliation, [owner, admin, member]);
384✔
843
        {true, false} ->
844
            lists:member(Affiliation, [owner, admin, member]);
30✔
845
        {false, false} ->
846
            %% Outcast (banned) cannot read
847
            Affiliation =/= outcast
4,074✔
848
    end.
849

850
can_user_access_identity(UserJID, StateData) ->
851
    is_room_non_anonymous(StateData)
2,623✔
852
    orelse
853
    is_user_moderator(UserJID, StateData).
617✔
854

855
is_room_non_anonymous(StateData) ->
856
    not is_room_anonymous(StateData).
2,623✔
857

858
is_room_anonymous(#state{config = #config{anonymous = IsAnon}}) ->
859
    IsAnon.
2,623✔
860

861
is_user_moderator(UserJID, StateData) ->
862
    get_role(UserJID, StateData) =:= moderator.
617✔
863

864
process_message_from_allowed_user(From, Packet, StateData) ->
865
    Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>),
8,529✔
866
    {FromNick, Role} = get_participant_data(From, StateData),
8,529✔
867
    CanSendBroadcasts = can_send_broadcasts(Role, StateData),
8,529✔
868
    case CanSendBroadcasts of
8,529✔
869
        true ->
870
            {NewState, Changed} = change_subject_if_allowed(FromNick, Role,
8,529✔
871
                                                            Packet, StateData),
872
            case Changed of
8,529✔
873
                true ->
874
                    broadcast_room_packet(From, FromNick, Role, Packet, NewState);
8,518✔
875
                false ->
876
                    change_subject_error(From, FromNick, Packet, Lang, NewState),
11✔
877
                    next_normal_state(NewState)
11✔
878
            end;
879
        false ->
880
            ErrText = <<"Visitors are not allowed to send messages to all occupants">>,
×
881
            Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:forbidden(Lang, ErrText)),
×
882
            ejabberd_router:route(StateData#state.jid, From, Err),
×
883
            next_normal_state(StateData)
×
884
    end.
885

886
can_send_broadcasts(Role, StateData) ->
887
    (Role == moderator)
888
    or (Role == participant)
889
    or ((StateData#state.config)#config.moderated == false).
8,529✔
890

891
broadcast_room_packet(From, FromNick, Role, Packet, StateData) ->
892
    TS = erlang:system_time(microsecond),
8,518✔
893
    Affiliation = get_affiliation(From, StateData),
8,518✔
894
    EventData = #{from_nick => FromNick, from_jid => From,
8,518✔
895
                  room_jid => StateData#state.jid, role => Role,
896
                  affiliation => Affiliation, timestamp => TS},
897
    FilteredPacket = mongoose_hooks:filter_room_packet(
8,518✔
898
        StateData#state.host_type, Packet, EventData),
899
    RouteFrom = jid:replace_resource(StateData#state.jid,
8,518✔
900
                                     FromNick),
901
    RoomJid = StateData#state.jid,
8,518✔
902
    HookInfo = #{host_type => StateData#state.host_type,
8,518✔
903
                 room_jid => RoomJid,
904
                 from_jid => From,
905
                 from_room_jid => RouteFrom,
906
                 packet => FilteredPacket,
907
                 affiliations_map => StateData#state.affiliations},
908
    run_update_inbox_for_muc_hook(StateData#state.host_type, HookInfo),
8,518✔
909
    maps_foreach(fun(_LJID, Info) ->
8,518✔
910
                          ejabberd_router:route(RouteFrom,
22,146✔
911
                                                Info#user.jid,
912
                                                FilteredPacket)
913
                  end, StateData#state.users),
914
    NewStateData2 = add_message_to_history(FromNick,
8,518✔
915
                                           From,
916
                                           FilteredPacket,
917
                                           StateData),
918
    next_normal_state(NewStateData2).
8,518✔
919

920
-spec run_update_inbox_for_muc_hook(mongooseim:host_type(),
921
                                    update_inbox_for_muc_payload()) -> ok.
922
run_update_inbox_for_muc_hook(HostType, HookInfo) ->
923
    mongoose_hooks:update_inbox_for_muc(HostType, HookInfo),
8,518✔
924
    ok.
8,518✔
925

926
change_subject_error(From, FromNick, Packet, Lang, StateData) ->
927
    Err = case (StateData#state.config)#config.allow_change_subj of
11✔
928
              true -> mongoose_xmpp_errors:forbidden(Lang, <<"Only moderators and participants are allowed"
×
929
                                              " to change the subject in this room">>);
930
              _ -> mongoose_xmpp_errors:forbidden(Lang, <<"Only moderators are allowed"
11✔
931
                                           " to change the subject in this room">>)
932
          end,
933
    ejabberd_router:route(jid:replace_resource(StateData#state.jid,
11✔
934
                                               FromNick),
935
                          From,
936
                          jlib:make_error_reply(Packet, Err)).
937

938
change_subject_if_allowed(FromNick, Role, Packet, StateData) ->
939
    case check_subject(Packet) of
8,529✔
940
        undefined ->
941
            {StateData, true};
8,496✔
942
        Subject ->
943
            case can_change_subject(Role, StateData) of
33✔
944
                true ->
945
                    NSD = StateData#state{subject = Subject,
22✔
946
                                          subject_author = FromNick,
947
                                          subject_timestamp = get_current_timestamp()},
948
                    save_persistent_room_state(NSD),
22✔
949
                    {NSD, true};
22✔
950
                _ ->
951
                    {StateData, false}
11✔
952
            end
953
    end.
954

955
save_persistent_room_state(StateData) ->
956
    case (StateData#state.config)#config.persistent of
7,235✔
957
        true ->
958
            mod_muc:store_room(StateData#state.host_type,
4,158✔
959
                               StateData#state.host,
960
                               StateData#state.room,
961
                               make_opts(StateData));
962
        _ ->
963
            ok
3,077✔
964
    end.
965

966
%% @doc Check if this non participant can send message to room.
967
%%
968
%% XEP-0045 v1.23:
969
%% 7.9 Sending a Message to All Occupants
970
%% an implementation MAY allow users with certain privileges
971
%% (e.g., a room owner, room admin, or service-level admin)
972
%% to send messages to the room even if those users are not occupants.
973
-spec is_allowed_nonparticipant(jid:jid(), state()) -> boolean().
974
is_allowed_nonparticipant(JID, StateData) ->
975
    get_service_affiliation(JID, StateData) =:= owner.
151✔
976

977
%% @doc Get information of this participant, or default values.
978
%% If the JID is not a participant, return values for a service message.
979
-spec get_participant_data(jid:simple_jid() | jid:jid(), state()) -> {_, _}.
980
get_participant_data(From, StateData) ->
981
    case maps:find(jid:to_lower(From), StateData#state.users) of
8,551✔
982
        {ok, #user{nick = FromNick, role = Role}} ->
983
            {FromNick, Role};
8,551✔
984
        error ->
985
            {<<>>, moderator}
×
986
    end.
987

988
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
989
%% Presence processing
990

991
%% @doc Process presence stanza and destroy the room, if it is empty.
992
-spec process_presence(From :: jid:jid(), Nick :: mod_muc:nick(),
993
                       Packet :: exml:element(), state()) -> fsm_return().
994
process_presence(From, ToNick, Presence, StateData) ->
995
    StateData1 = process_presence1(From, ToNick, Presence, StateData),
14,974✔
996
    destroy_temporary_room_if_empty(StateData1, normal_state).
14,973✔
997

998

999
-spec process_presence(From :: jid:jid(), Nick :: mod_muc:nick(),
1000
        Presence :: exml:element(), state(), statename()) -> fsm_return().
1001
process_presence(From, ToNick, Presence, StateData, NextState) ->
1002
    StateData1 = process_presence(From, ToNick, Presence, StateData),
549✔
1003
    rewrite_next_state(NextState, StateData1).
549✔
1004

1005

1006
-spec rewrite_next_state(statename(), fsm_return()) -> fsm_return().
1007
rewrite_next_state(NewState, {next_state, _, StateData, Timeout}) ->
1008
    {next_state, NewState, StateData, Timeout};
483✔
1009
rewrite_next_state(NewState, {next_state, _, StateData}) ->
1010
    {next_state, NewState, StateData};
×
1011
rewrite_next_state(_, {stop, normal, StateData}) ->
1012
    {stop, normal, StateData}.
66✔
1013

1014

1015
-spec destroy_temporary_room_if_empty(state(), atom()) -> fsm_return().
1016
destroy_temporary_room_if_empty(StateData=#state{config=C=#config{}}, NextState) ->
1017
    case (not C#config.persistent) andalso is_empty_room(StateData)
14,995✔
1018
        andalso StateData#state.http_auth_pids =:= [] of
1,550✔
1019
        true ->
1020
            ?LOG_INFO(ls(#{what => muc_empty_room_destroyed,
1,550✔
1021
                           text => <<"Destroyed MUC room because it's temporary and empty">>},
1022
                         StateData)),
1,550✔
1023
            add_to_log(room_existence, destroyed, StateData),
1,550✔
1024
            {stop, normal, StateData};
1,550✔
1025
        _ ->
1026
            case NextState of
13,445✔
1027
                normal_state ->
1028
                    next_normal_state(StateData);
13,445✔
1029
                _ ->
1030
                    {next_state, NextState, StateData}
×
1031
            end
1032
    end.
1033

1034
next_normal_state(#state{hibernate_timeout = Timeout} = StateData) ->
1035
    {next_state, normal_state, StateData, Timeout}.
38,586✔
1036

1037
-spec process_presence1(From, Nick, Packet, state()) -> state() when
1038
      From :: jid:jid(),
1039
      Nick :: mod_muc:nick(),
1040
      Packet :: exml:element().
1041
process_presence1(From, Nick, #xmlel{name = <<"presence">>} = Packet, StateData = #state{}) ->
1042
    Type = exml_query:attr(Packet, <<"type">>, <<>>),
14,974✔
1043
    Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>),
14,974✔
1044
    case Type of
14,974✔
1045
        <<"unavailable">> ->
1046
            process_presence_unavailable(From, Packet, StateData);
6,892✔
1047
        <<"error">> ->
1048
            process_presence_error(From, Packet, Lang, StateData);
11✔
1049
        <<>> ->
1050
            case is_new_nick_of_online_user(From, Nick, StateData) of
8,071✔
1051
                true ->
1052
                    process_presence_nick_change(From, Nick, Packet, Lang, StateData);
22✔
1053
                false ->
1054
                    process_simple_presence(From, Packet, StateData);
22✔
1055
                user_is_offline ->
1056
                    %% at this point we know that the presence has no type
1057
                    %% (user wants to enter the room)
1058
                    %% and that the user is not alredy online
1059
                    handle_new_user(From, Nick, Packet, StateData, Packet)
8,027✔
1060
            end;
1061
        _NotOnline ->
1062
            StateData
×
1063
    end.
1064

1065

1066
-spec process_simple_presence(jid:jid(), exml:element(), state()) -> state().
1067
process_simple_presence(From, Packet, StateData) ->
1068
    NewPacket = check_and_strip_visitor_status(From, Packet, StateData),
22✔
1069
    NewState = add_user_presence(From, NewPacket, StateData),
22✔
1070
    send_new_presence(From, NewState),
22✔
1071
    NewState.
22✔
1072

1073

1074
-spec process_presence_error(jid:simple_jid() | jid:jid(),
1075
                             exml:element(), ejabberd:lang(), state()) -> state().
1076
process_presence_error(From, Packet, Lang, StateData) ->
1077
    case is_user_online(From, StateData) of
11✔
1078
        true ->
1079
            ErrorText
11✔
1080
            = <<"This participant is kicked from the room because he sent an error presence">>,
1081
            expulse_participant(Packet, From, StateData, service_translations:do(Lang, ErrorText));
11✔
1082
        _ ->
1083
            StateData
×
1084
    end.
1085

1086

1087
-spec process_presence_unavailable(jid:jid(), exml:element(), state())
1088
                                    -> state().
1089
process_presence_unavailable(From, Packet, StateData) ->
1090
    case is_user_online(From, StateData) of
6,892✔
1091
        true ->
1092
            NewPacket = check_and_strip_visitor_status(From, Packet, StateData),
6,892✔
1093
            NewState = add_user_presence_un(From, NewPacket, StateData),
6,892✔
1094
            send_new_presence_un(From, NewState),
6,892✔
1095
            Reason = exml_query:path(NewPacket, [{element, <<"status">>}, cdata], <<>>),
6,891✔
1096
            remove_online_user(From, NewState, Reason);
6,891✔
1097
        _ ->
UNCOV
1098
            StateData
×
1099
    end.
1100

1101

1102
-spec choose_nick_change_strategy(jid:jid(), binary(), state())
1103
    -> 'allowed' | 'conflict_registered' | 'conflict_use' | 'not_allowed_visitor'.
1104
choose_nick_change_strategy(From, Nick, StateData) ->
1105
    case {is_nick_exists(Nick, StateData),
22✔
1106
          mod_muc:can_use_nick(StateData#state.host_type, StateData#state.host, From, Nick),
1107
          (StateData#state.config)#config.allow_visitor_nickchange,
1108
          is_visitor(From, StateData)} of
1109
        {_, _, false, true} ->
1110
            not_allowed_visitor;
×
1111
        {true, _, _, _} ->
1112
            conflict_use;
11✔
1113
        {_, false, _, _} ->
1114
            conflict_registered;
×
1115
        _ ->
1116
            allowed
11✔
1117
    end.
1118

1119

1120
-spec process_presence_nick_change(jid:jid(), mod_muc:nick(), exml:element(),
1121
        ejabberd:lang(), state()) -> state().
1122
process_presence_nick_change(From, Nick, Packet, Lang, StateData) ->
1123
    case choose_nick_change_strategy(From, Nick, StateData) of
22✔
1124
        not_allowed_visitor ->
1125
            ErrText = <<"Visitors are not allowed to change their nicknames in this room">>,
×
1126
            Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:not_allowed(Lang, ErrText)),
×
1127
            route_error(Nick, From, Err, StateData);
×
1128
        conflict_use ->
1129
            ErrText = <<"That nickname is already in use by another occupant">>,
11✔
1130
            Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:conflict(Lang, ErrText)),
11✔
1131
            route_error(Nick, From, Err, StateData);
11✔
1132
        conflict_registered ->
1133
            ErrText = <<"That nickname is registered by another person">>,
×
1134
            Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:conflict(Lang, ErrText)),
×
1135
            route_error(Nick, From, Err, StateData);
×
1136
        allowed ->
1137
            change_nick(From, Nick, StateData)
11✔
1138
    end.
1139

1140

1141
-spec check_and_strip_visitor_status(jid:jid(), exml:element(), state())
1142
                                        -> exml:element().
1143
check_and_strip_visitor_status(From, Packet, StateData) ->
1144
    case {(StateData#state.config)#config.allow_visitor_status,
6,914✔
1145
          is_visitor(From, StateData)} of
1146
        {false, true} ->
1147
            strip_status(Packet);
×
1148
        _ ->
1149
            Packet
6,914✔
1150
    end.
1151

1152

1153
-spec handle_new_user(jid:jid(), mod_muc:nick(), exml:element(), state(), exml:element()) ->
1154
    state().
1155
handle_new_user(From, Nick = <<>>, _Packet, StateData, Packet) ->
1156
    Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>),
11✔
1157
    ErrText = <<"No nickname">>,
11✔
1158
    Error =jlib:make_error_reply(
11✔
1159
                #xmlel{name = <<"presence">>},
1160
                mongoose_xmpp_errors:jid_malformed(Lang, ErrText)),
1161
    %ejabberd_route(From, To, Packet),
1162
    ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), From, Error),
11✔
1163
    StateData;
11✔
1164
handle_new_user(From, Nick, Packet, StateData, Packet) ->
1165
    case exml_query:path(Packet, [{element, <<"x">>}]) of
8,016✔
1166
        undefined ->
1167
            Response = kick_stanza_for_old_protocol(Packet),
11✔
1168
            ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), From, Response),
11✔
1169
            StateData;
11✔
1170
        _ ->
1171
            add_new_user(From, Nick, Packet, StateData)
8,005✔
1172
    end.
1173

1174

1175
-spec is_user_online(jid:simple_jid() | jid:jid(), state()) -> boolean().
1176
is_user_online(JID, StateData) ->
1177
    LJID = jid:to_lower(JID),
25,061✔
1178
    maps:is_key(LJID, StateData#state.users).
25,061✔
1179

1180

1181
%% @doc Check if the user is occupant of the room, or at least is an admin
1182
%% or owner.
1183
-spec is_occupant_or_admin(jid:jid(), state()) -> boolean().
1184
is_occupant_or_admin(JID, StateData) ->
1185
    FAffiliation = get_affiliation(JID, StateData),
1,986✔
1186
    FRole = get_role(JID, StateData),
1,986✔
1187
    (FRole /= none) orelse
1,986✔
1188
    (FAffiliation == admin) orelse
1,934✔
1189
    (FAffiliation == owner).
1,934✔
1190

1191
%%%
1192
%%% Handle IQ queries of vCard
1193
%%%
1194

1195
-spec is_user_online_iq(_, jid:jid(), state())
1196
            -> {'false', _, jid:jid()} | {'true', _, jid:jid()}.
1197
is_user_online_iq(StanzaId, JID, StateData) when JID#jid.lresource /= <<>> ->
1198
    {is_user_online(JID, StateData), StanzaId, JID};
×
1199
is_user_online_iq(StanzaId, JID, StateData) when JID#jid.lresource == <<>> ->
1200
    try stanzaid_unpack(StanzaId) of
×
1201
        {OriginalId, Resource} ->
1202
            JIDWithResource = jid:replace_resource(JID, Resource),
×
1203
            {is_user_online(JIDWithResource, StateData),
×
1204
             OriginalId, JIDWithResource}
1205
    catch
1206
        _:_ ->
1207
            {is_user_online(JID, StateData), StanzaId, JID}
×
1208
    end.
1209

1210

1211
-spec handle_iq_vcard(jid:jid(), jid:simple_jid() | jid:jid(),
1212
                      binary(), any(), exml:element()) ->
1213
                {jid:simple_jid() | jid:jid(), exml:element()}.
1214
handle_iq_vcard(FromFull, ToJID, StanzaId, NewId, Packet) ->
1215
    ToBareJID = jid:to_bare(ToJID),
×
1216
    IQ = jlib:iq_query_info(Packet),
×
1217
    handle_iq_vcard2(FromFull, ToJID, ToBareJID, StanzaId, NewId, IQ, Packet).
×
1218

1219

1220
-spec handle_iq_vcard2(FromFull :: jid:jid(),
1221
        ToJID :: jid:simple_jid() | jid:jid(),
1222
        ToBareJID :: jid:simple_jid() | jid:jid(),
1223
        binary(), _NewID, 'invalid' | 'not_iq' | 'reply' | jlib:iq(),
1224
        exml:element()) -> {jid:simple_jid() | jid:jid(), exml:element()}.
1225
handle_iq_vcard2(_FromFull, ToJID, ToBareJID, StanzaId, _NewId,
1226
         #iq{type = get, xmlns = ?NS_VCARD}, Packet)
1227
  when ToBareJID /= ToJID ->
1228
    {ToBareJID, change_stanzaid(StanzaId, ToJID, Packet)};
×
1229
handle_iq_vcard2(_FromFull, ToJID, _ToBareJID, _StanzaId, NewId, _IQ, Packet) ->
1230
    {ToJID, change_stanzaid(NewId, Packet)}.
×
1231

1232

1233
-spec stanzaid_pack(binary(), jid:resource()) -> binary().
1234
stanzaid_pack(OriginalId, Resource) ->
1235
    Data64 = base64:encode(<<"ejab\0", OriginalId/binary, 0, Resource/binary>>),
×
1236
    <<"berd", Data64/binary>>.
×
1237

1238

1239
-spec stanzaid_unpack(binary()) -> stanzaid().
1240
stanzaid_unpack(<<"berd", StanzaIdBase64/binary>>) ->
1241
    StanzaId = base64:decode(StanzaIdBase64),
×
1242
    [<<"ejab">>, OriginalId, Resource] = binary:split(StanzaId, <<"\0">>),
×
1243
    {OriginalId, Resource}.
×
1244

1245

1246
-spec change_stanzaid(binary(), exml:element()) -> exml:element().
1247
change_stanzaid(NewId, #xmlel{attrs = Attrs} = Packet) ->
1248
    Packet#xmlel{attrs = Attrs#{<<"id">> => NewId}}.
×
1249

1250
change_stanzaid(PreviousId, ToJID, Packet) ->
1251
    NewId = stanzaid_pack(PreviousId, ToJID#jid.lresource),
×
1252
    change_stanzaid(NewId, Packet).
×
1253

1254
%%%
1255
%%%
1256

1257
-spec role_to_binary(mod_muc:role()) -> binary().
1258
role_to_binary(Role) ->
1259
    case Role of
20,107✔
1260
        moderator   -> <<"moderator">>;
6,569✔
1261
        participant -> <<"participant">>;
6,333✔
1262
        visitor     -> <<"visitor">>;
239✔
1263
        none        -> <<"none">>
6,966✔
1264
    end.
1265

1266
-spec affiliation_to_binary(mod_muc:affiliation()) -> binary().
1267
affiliation_to_binary(Affiliation) ->
1268
    case Affiliation of
20,280✔
1269
        owner   -> <<"owner">>;
9,439✔
1270
        admin   -> <<"admin">>;
110✔
1271
        member  -> <<"member">>;
738✔
1272
        outcast -> <<"outcast">>;
66✔
1273
        none    -> <<"none">>
9,927✔
1274
    end.
1275

1276
-spec binary_to_role(binary()) -> mod_muc:role().
1277
binary_to_role(Role) ->
1278
    case Role of
714✔
1279
        <<"moderator">>     -> moderator;
272✔
1280
        <<"participant">>   -> participant;
228✔
1281
        <<"visitor">>       -> visitor;
118✔
1282
        <<"none">>          -> none
85✔
1283
    end.
1284

1285
-spec binary_to_affiliation(binary()) -> mod_muc:affiliation().
1286
binary_to_affiliation(Affiliation) ->
1287
    case Affiliation of
1,300✔
1288
        <<"owner">>     -> owner;
129✔
1289
        <<"admin">>     -> admin;
162✔
1290
        <<"member">>    -> member;
674✔
1291
        <<"outcast">>   -> outcast;
151✔
1292
        <<"none">>      -> none
173✔
1293
    end.
1294

1295

1296
%% @doc Decide the fate of the message and its sender
1297
%% Returns: continue_delivery | forget_message | {expulse_sender, Reason}
1298
-spec decide_fate_message(binary(), exml:element(), jid:simple_jid() | jid:jid(),
1299
        state()) -> 'continue_delivery'
1300
                  | 'forget_message'
1301
                  | {'expulse_sender', string()}.
1302
decide_fate_message(<<"error">>, Packet, From, StateData) ->
1303
    %% Make a preliminary decision
1304
    PD = case check_error_kick(Packet) of
1✔
1305
         %% If this is an error stanza and its condition matches a criteria
1306
         true ->
1307
         Reason = "This participant is considered a ghost and is expulsed: " ++
1✔
1308
            binary_to_list(jid:to_binary(From)),
1309
         {expulse_sender, Reason};
1✔
1310
         false ->
1311
         continue_delivery
×
1312
     end,
1313
    case PD of
1✔
1314
    {expulse_sender, R} ->
1315
        case is_user_online(From, StateData) of
1✔
1316
        true ->
1317
            {expulse_sender, R};
×
1318
        false ->
1319
            forget_message
1✔
1320
        end;
1321
    Other ->
1322
        Other
×
1323
    end;
1324
decide_fate_message(_, _, _, _) ->
1325
    continue_delivery.
294✔
1326

1327

1328
%% @doc Check if the elements of this error stanza indicate
1329
%% that the sender is a dead participant.
1330
%% If so, return true to kick the participant.
1331
-spec check_error_kick(exml:element()) -> boolean().
1332
check_error_kick(Packet) ->
1333
    case get_error_condition(Packet) of
1✔
1334
        <<"gone">>                      -> true;
×
1335
        <<"internal-server-error">>     -> true;
×
1336
        <<"item-not-found">>            -> true;
×
1337
        <<"jid-malformed">>             -> true;
×
1338
        <<"recipient-unavailable">>     -> true;
×
1339
        <<"redirect">>                  -> true;
×
1340
        <<"remote-server-not-found">>   -> true;
×
1341
        <<"remote-server-timeout">>     -> true;
×
1342
        <<"service-unavailable">>       -> true;
1✔
1343
        _                               -> false
×
1344
    end.
1345

1346

1347
-spec get_error_condition(exml:element()) -> binary().
1348
get_error_condition(Packet) ->
1349
    case catch get_error_condition2(Packet) of
75✔
1350
        {condition, ErrorCondition} ->
1351
            ErrorCondition;
64✔
1352
        {'EXIT', _} ->
1353
            <<"badformed error stanza">>
11✔
1354
    end.
1355

1356

1357
-spec get_error_condition2(exml:element()) -> {condition, binary()}.
1358
get_error_condition2(Packet) ->
1359
    #xmlel{children = EEls} = exml_query:subelement(Packet, <<"error">>),
75✔
1360
    [Condition] = [Name || #xmlel{name = Name,
64✔
1361
                                  attrs = #{<<"xmlns">> := ?NS_STANZAS},
1362
                                  children = []} <- EEls],
64✔
1363
    {condition, Condition}.
64✔
1364

1365

1366
-spec expulse_participant(exml:element(), jid:jid(), state(), binary()) -> state().
1367
expulse_participant(Packet, From, StateData, Reason1) ->
1368
    ErrorCondition = get_error_condition(Packet),
74✔
1369
    Reason2 = <<Reason1/binary, ": ", ErrorCondition/binary>>,
74✔
1370
    NewState = add_user_presence_un(
74✔
1371
        From,
1372
        #xmlel{name = <<"presence">>, attrs = #{<<"type">> => <<"unavailable">>},
1373
               children = [#xmlel{name = <<"status">>,
1374
                                  children = [#xmlcdata{content = Reason2}]}]},
1375
    StateData),
1376
    send_new_presence_un(From, NewState),
74✔
1377
    remove_online_user(From, NewState).
74✔
1378

1379

1380
-spec access_admin(state()) -> any().
1381
access_admin(#state{access=Access}) ->
1382
    {_AccessRoute, _AccessCreate, AccessAdmin, _AccessPersistent} = Access,
67,526✔
1383
    AccessAdmin.
67,526✔
1384

1385

1386
-spec access_persistent(state()) -> any().
1387
access_persistent(#state{access=Access}) ->
1388
    {_AccessRoute, _AccessCreate, _AccessAdmin, AccessPersistent} = Access,
228✔
1389
    AccessPersistent.
228✔
1390

1391

1392
-spec set_affiliation(jid:jid(), mod_muc:affiliation(), state()) -> state().
1393
set_affiliation(JID, Affiliation, StateData)
1394
        when is_atom(Affiliation) ->
1395
    LJID = jid:to_bare(jid:to_lower(JID)),
6,361✔
1396
    Affiliations = case Affiliation of
6,361✔
1397
               none -> maps:remove(LJID, StateData#state.affiliations);
×
1398
               _ -> maps:put(LJID, Affiliation, StateData#state.affiliations)
6,361✔
1399
           end,
1400
    StateData#state{affiliations = Affiliations}.
6,361✔
1401

1402

1403
-spec set_affiliation_and_reason(jid:jid(), mod_muc:affiliation(), term(),
1404
                                 state()) -> state().
1405
set_affiliation_and_reason(JID, Affiliation, Reason, StateData)
1406
        when is_atom(Affiliation) ->
1407
    LJID = jid:to_bare(jid:to_lower(JID)),
948✔
1408
    Affiliations = case Affiliation of
948✔
1409
               none -> maps:remove(LJID, StateData#state.affiliations);
162✔
1410
               _ -> maps:put(LJID, {Affiliation, Reason}, StateData#state.affiliations)
786✔
1411
           end,
1412
    StateData#state{affiliations = Affiliations}.
948✔
1413

1414

1415
-spec get_affiliation(jid:jid(), state()) -> mod_muc:affiliation().
1416
get_affiliation(JID, StateData) ->
1417
    AccessAdmin = access_admin(StateData),
49,890✔
1418
    case acl:match_rule(StateData#state.host_type, StateData#state.server_host, AccessAdmin, JID) of
49,890✔
1419
        allow ->
1420
            owner;
×
1421
        _ ->
1422
            LJID = jid:to_lower(JID),
49,890✔
1423
            LJID1 = jid:to_bare(LJID),
49,890✔
1424
            LJID2 = setelement(1, LJID, <<>>),
49,890✔
1425
            LJID3 = jid:to_bare(LJID2),
49,890✔
1426
            lookup_affiliation([ LJID, LJID1, LJID2, LJID3 ], StateData#state.affiliations)
49,890✔
1427
    end.
1428

1429
-spec lookup_affiliation(JIDs :: [jid:simple_jid()],
1430
                         Affiliations :: affiliations_map()) ->
1431
    mod_muc:affiliation().
1432
lookup_affiliation([ JID | RJIDs ], Affiliations) ->
1433
    case maps:find(JID, Affiliations) of
141,550✔
1434
        {ok, {Affiliation, _Reason}} -> Affiliation;
1,538✔
1435
        {ok, Affiliation} -> Affiliation;
26,449✔
1436
        _ -> lookup_affiliation(RJIDs, Affiliations)
113,563✔
1437
    end;
1438
lookup_affiliation([], _Affiliations) ->
1439
    none.
21,903✔
1440

1441
-spec get_service_affiliation(jid:jid(), state()) -> mod_muc:affiliation().
1442
get_service_affiliation(JID, StateData) ->
1443
    AccessAdmin = access_admin(StateData),
17,636✔
1444
    case acl:match_rule(StateData#state.host_type, StateData#state.server_host, AccessAdmin, JID) of
17,636✔
1445
    allow ->
1446
        owner;
×
1447
    _ ->
1448
        none
17,636✔
1449
    end.
1450

1451

1452
-spec set_role(JID :: jid:jid(), Role :: mod_muc:role(), state()) -> state().
1453
set_role(JID, none, StateData) ->
1454
    erase_matched_users(JID, StateData);
214✔
1455
set_role(JID, Role, StateData) ->
1456
    update_matched_users(fun(User) -> User#user{role = Role} end,
1,011✔
1457
                         JID, StateData).
1458

1459

1460
-spec get_role( jid:jid(), state()) -> mod_muc:role().
1461
get_role(JID, StateData) ->
1462
    LJID = jid:to_lower(JID),
12,968✔
1463
    case maps:find(LJID, StateData#state.users) of
12,968✔
1464
        {ok, #user{role = Role}} -> Role;
9,194✔
1465
        _ -> none
3,774✔
1466
    end.
1467

1468

1469
-spec get_default_role(mod_muc:affiliation(), state()) -> mod_muc:role().
1470
get_default_role(owner, _StateData) -> moderator;
3,617✔
1471
get_default_role(admin, _StateData) -> moderator;
×
1472
get_default_role(member, _StateData) -> participant;
49✔
1473
get_default_role(outcast, _StateData) -> none;
11✔
1474
get_default_role(none, StateData) ->
1475
    case (StateData#state.config)#config.members_only of
4,328✔
1476
        true -> none;
11✔
1477
        _ ->
1478
            case (StateData#state.config)#config.members_by_default of
4,317✔
1479
                true -> participant;
4,174✔
1480
                _ -> visitor
143✔
1481
            end
1482
    end.
1483

1484

1485
-spec is_visitor(jid:jid(), state()) -> boolean().
1486
is_visitor(Jid, StateData) ->
1487
    get_role(Jid, StateData) =:= visitor.
6,936✔
1488

1489

1490
-spec is_empty_room(state()) -> boolean().
1491
is_empty_room(#state{users=Users}) ->
1492
    is_empty_map(Users).
7,077✔
1493

1494

1495
-spec is_empty_map(map()) -> boolean().
1496
is_empty_map(Map) ->
1497
    maps:size(Map) =:= 0.
7,077✔
1498

1499

1500
-spec map_foreach_value(fun((_) -> ok), users_map()) -> any().
1501
map_foreach_value(F, Map) ->
1502
    maps:fold(fun(_Key, Value, _) -> F(Value) end, ok, Map).
3,355✔
1503

1504

1505
-spec count_users(state()) -> non_neg_integer().
1506
count_users(#state{users=Users}) ->
1507
    maps:size(Users).
10,156✔
1508

1509

1510
-spec get_max_users(state()) -> integer() | none.
1511
get_max_users(StateData) ->
1512
    MaxUsers = (StateData#state.config)#config.max_users,
8,126✔
1513
    ServiceMaxUsers = get_service_max_users(StateData),
8,126✔
1514
    case MaxUsers =< ServiceMaxUsers of
8,126✔
1515
        true -> MaxUsers;
8,126✔
1516
        false -> ServiceMaxUsers
×
1517
    end.
1518

1519

1520
-spec get_service_max_users(state()) -> integer() | none.
1521
get_service_max_users(StateData) ->
1522
    get_opt(StateData, max_users).
8,995✔
1523

1524
-spec get_max_users_admin_threshold(state()) -> integer().
1525
get_max_users_admin_threshold(StateData) ->
1526
    get_opt(StateData, max_users_admin_threshold).
8,005✔
1527

1528
-spec get_user_activity(jid:simple_jid() | jid:jid(), state())
1529
                        -> activity().
1530
get_user_activity(JID, StateData) ->
1531
    case treap:lookup(jid:to_lower(JID), StateData#state.activity) of
22,954✔
1532
    {ok, _P, A} -> A;
×
1533
    error ->
1534
        MessageShaper = mongoose_shaper:new(get_opt(StateData, user_message_shaper)),
22,954✔
1535
        PresenceShaper = mongoose_shaper:new(get_opt(StateData, user_presence_shaper)),
22,954✔
1536
        #activity{message_shaper = MessageShaper,
22,954✔
1537
                  presence_shaper = PresenceShaper}
1538
    end.
1539

1540

1541
-spec store_user_activity(jid:simple_jid() | jid:jid(), activity(),
1542
                         state()) -> state().
1543
store_user_activity(JID, UserActivity, StateData) ->
1544
    MinMessageInterval = get_opt(StateData, min_message_interval),
22,954✔
1545
    MinPresenceInterval = get_opt(StateData, min_presence_interval),
22,954✔
1546
    Key = jid:to_lower(JID),
22,954✔
1547
    Now = os:system_time(microsecond),
22,954✔
1548
    Activity1 = clean_treap(StateData#state.activity, {1, -Now}),
22,954✔
1549
    Activity =
22,954✔
1550
    case treap:lookup(Key, Activity1) of
1551
        {ok, _P, _A} ->
1552
            treap:delete(Key, Activity1);
×
1553
        error ->
1554
            Activity1
22,954✔
1555
    end,
1556
    StateData1 =
22,954✔
1557
    case (MinMessageInterval == 0) andalso
×
1558
        (MinPresenceInterval == 0) andalso
22,954✔
1559
        (UserActivity#activity.message_shaper == none) andalso
22,954✔
1560
        (UserActivity#activity.presence_shaper == none) andalso
22,954✔
1561
        (UserActivity#activity.message == undefined) andalso
22,954✔
1562
        (UserActivity#activity.presence == undefined) of
22,954✔
1563
        true ->
1564
        StateData#state{activity = Activity};
22,954✔
1565
        false ->
1566
        case (UserActivity#activity.message == undefined) andalso
×
1567
            (UserActivity#activity.presence == undefined) of
×
1568
            true ->
1569
            {_, MessageShaperInterval} =
×
1570
                mongoose_shaper:update(UserActivity#activity.message_shaper,
1571
                      100000),
1572
            {_, PresenceShaperInterval} =
×
1573
                mongoose_shaper:update(UserActivity#activity.presence_shaper,
1574
                      100000),
1575
            Delay = lists:max([MessageShaperInterval,
×
1576
                       PresenceShaperInterval,
1577
                       MinMessageInterval * 1000,
1578
                       MinPresenceInterval * 1000]) * 1000,
1579
            Priority = {1, -(Now + Delay)},
×
1580
            StateData#state{
×
1581
              activity = treap:insert(
1582
                       Key,
1583
                       Priority,
1584
                       UserActivity,
1585
                       Activity)};
1586
            false ->
1587
            Priority = {0, 0},
×
1588
            StateData#state{
×
1589
              activity = treap:insert(
1590
                       Key,
1591
                       Priority,
1592
                       UserActivity,
1593
                       Activity)}
1594
        end
1595
    end,
1596
    StateData1.
22,954✔
1597

1598

1599
-spec clean_treap(treap:treap(), {1, integer()}) -> treap:treap().
1600
clean_treap(Treap, CleanPriority) ->
1601
    case treap:is_empty(Treap) of
22,954✔
1602
        true ->
1603
            Treap;
22,954✔
1604
        false ->
1605
            {_Key, Priority, _Value} = treap:get_root(Treap),
×
1606
            case Priority > CleanPriority of
×
1607
                true -> clean_treap(treap:delete_root(Treap), CleanPriority);
×
1608
                false -> Treap
×
1609
            end
1610
    end.
1611

1612

1613
-spec prepare_room_queue(state()) -> state().
1614
prepare_room_queue(StateData) ->
1615
    case queue:out(StateData#state.room_queue) of
×
1616
    {{value, {message, From}}, _RoomQueue} ->
1617
        Activity = get_user_activity(From, StateData),
×
1618
        Packet = Activity#activity.message,
×
1619
        Size = element_size(Packet),
×
1620
        {RoomShaper, RoomShaperInterval} =
×
1621
        mongoose_shaper:update(StateData#state.room_shaper, Size),
1622
        erlang:send_after(
×
1623
          RoomShaperInterval, self(),
1624
          process_room_queue),
1625
        StateData#state{
×
1626
          room_shaper = RoomShaper};
1627
    {{value, {presence, From}}, _RoomQueue} ->
1628
        Activity = get_user_activity(From, StateData),
×
1629
        {_Nick, Packet} = Activity#activity.presence,
×
1630
        Size = element_size(Packet),
×
1631
        {RoomShaper, RoomShaperInterval} =
×
1632
        mongoose_shaper:update(StateData#state.room_shaper, Size),
1633
        erlang:send_after(
×
1634
          RoomShaperInterval, self(),
1635
          process_room_queue),
1636
        StateData#state{
×
1637
          room_shaper = RoomShaper};
1638
    {empty, _} ->
1639
        StateData
×
1640
    end.
1641

1642
-spec is_first_session(mod_muc:nick(), state()) -> boolean().
1643
is_first_session(Nick, StateData) ->
1644
    case maps:find(Nick, StateData#state.sessions) of
7,596✔
1645
        {ok, _Val} -> false;
44✔
1646
        error -> true
7,552✔
1647
    end.
1648

1649
-spec is_last_session(mod_muc:nick(), state()) -> boolean().
1650
is_last_session(Nick, StateData) ->
1651
    case maps:find(Nick, StateData#state.sessions) of
13,931✔
1652
        {ok, [_Val]} -> true;
13,843✔
1653
        _ -> false
88✔
1654
    end.
1655

1656
-spec add_online_user(jid:jid(), mod_muc:nick(), mod_muc:role(), state())
1657
                        -> state().
1658
add_online_user(JID, Nick, Role, StateData) ->
1659
    LJID = jid:to_lower(JID),
7,596✔
1660
    Sessions = maps_append(Nick, JID, StateData#state.sessions),
7,596✔
1661
    Info = #user{jid = JID,
7,596✔
1662
                 nick = Nick,
1663
                 role = Role},
1664
    Users = maps:put(LJID, Info, StateData#state.users),
7,596✔
1665
    case is_first_session(Nick, StateData) of
7,596✔
1666
        true ->
1667
            add_to_log(join, Nick, StateData),
7,552✔
1668
            tab_add_online_user(JID, StateData),
7,552✔
1669
            run_join_room_hook(JID, StateData);
7,552✔
1670
        _ ->
1671
            ok
44✔
1672
    end,
1673
    notify_users_modified(StateData#state{users = Users, sessions = Sessions}).
7,596✔
1674

1675
-spec run_join_room_hook(jid:jid(), state()) -> ok.
1676
run_join_room_hook(JID, #state{room = Room, host = Host, jid = MucJID, server_host = ServerHost}) ->
1677
  mongoose_hooks:join_room(ServerHost, Room, Host, JID, MucJID),
7,552✔
1678
  ok.
7,552✔
1679

1680
-spec remove_online_user(jid:jid(), state()) -> state().
1681
remove_online_user(JID, StateData) ->
1682
    remove_online_user(JID, StateData, <<>>).
74✔
1683

1684
-spec remove_online_user(jid:jid(), state(), Reason :: binary()) -> state().
1685
remove_online_user(JID, StateData, Reason) ->
1686

1687
    LJID = jid:to_lower(JID),
6,965✔
1688
    {ok, #user{nick = Nick}} =
6,965✔
1689
        maps:find(LJID, StateData#state.users),
1690
    Sessions = case is_last_session(Nick, StateData) of
6,965✔
1691
        true ->
1692
            add_to_log(leave, {Nick, Reason}, StateData),
6,921✔
1693
            tab_remove_online_user(JID, StateData),
6,921✔
1694
            run_leave_room_hook(JID, StateData),
6,921✔
1695
            maps:remove(Nick, StateData#state.sessions);
6,921✔
1696
        false ->
1697
            IsOtherLJID = fun(J) -> jid:to_lower(J) /= LJID end,
44✔
1698
            F = fun (JIDs) -> lists:filter(IsOtherLJID, JIDs) end,
44✔
1699
            maps:update_with(Nick, F, StateData#state.sessions)
44✔
1700
    end,
1701
    Users = maps:remove(LJID, StateData#state.users),
6,965✔
1702

1703
    notify_users_modified(StateData#state{users = Users, sessions = Sessions}).
6,965✔
1704

1705
-spec run_leave_room_hook(jid:jid(), state()) -> ok.
1706
run_leave_room_hook(JID, #state{room = Room, host = Host, jid = MucJID, server_host = ServerHost}) ->
1707
  mongoose_hooks:leave_room(ServerHost, Room, Host, JID, MucJID),
6,921✔
1708
  ok.
6,921✔
1709

1710
-spec filter_presence(exml:element()) -> exml:element().
1711
filter_presence(#xmlel{name = <<"presence">>, attrs = Attrs, children = Els}) ->
1712
    FEls = lists:filter(
14,584✔
1713
             fun(#xmlcdata{}) ->
1714
                     false;
×
1715
                (#xmlel{} = El) ->
1716
                     case exml_query:attr(El, <<"xmlns">>, <<>>) of
14,448✔
1717
                         <<?NS_MUC_S, _/binary>> -> false;
7,648✔
1718
                         _ -> true
6,800✔
1719
                     end
1720
             end, Els),
1721
    #xmlel{name = <<"presence">>, attrs = Attrs, children = FEls}.
14,584✔
1722

1723

1724
-spec strip_status(exml:element()) -> exml:element().
1725
strip_status(#xmlel{name = <<"presence">>, attrs = Attrs,
1726
                    children = Els}) ->
1727
    FEls = lists:filter(
×
1728
         fun(#xmlel{name = <<"status">>}) ->
1729
                     false;
×
1730
                (_) -> true
×
1731
         end, Els),
1732
    #xmlel{name = <<"presence">>, attrs = Attrs, children = FEls}.
×
1733

1734

1735
-spec add_user_presence(jid:jid(), exml:element(), state()) -> state().
1736
add_user_presence(JID, Presence, StateData) ->
1737
    LJID = jid:to_lower(JID),
7,618✔
1738
    FPresence = filter_presence(Presence),
7,618✔
1739
    Users =
7,618✔
1740
    maps:update_with(
1741
      LJID,
1742
      fun(#user{} = User) ->
1743
              User#user{last_presence = FPresence}
7,618✔
1744
      end, StateData#state.users),
1745
    notify_users_modified(StateData#state{users = Users}).
7,618✔
1746

1747

1748
-spec add_user_presence_un(jid:simple_jid() | jid:jid(), exml:element(),
1749
                        state()) -> state().
1750
add_user_presence_un(JID, Presence, StateData) ->
1751
    LJID = jid:to_lower(JID),
6,966✔
1752
    FPresence = filter_presence(Presence),
6,966✔
1753
    Users =
6,966✔
1754
    maps:update_with(
1755
      LJID,
1756
      fun(#user{} = User) ->
1757
              User#user{last_presence = FPresence, role = none}
6,966✔
1758
      end, StateData#state.users),
1759
    notify_users_modified(StateData#state{users = Users}).
6,966✔
1760

1761

1762
-spec is_nick_exists(mod_muc:nick(), state()) -> boolean().
1763
is_nick_exists(Nick, StateData) ->
1764
    maps:is_key(Nick, StateData#state.sessions).
8,027✔
1765

1766

1767
-spec find_jids_by_nick(mod_muc:nick(), state()) -> [jid:jid()].
1768
find_jids_by_nick(Nick, StateData) ->
1769
    case maps:find(Nick, StateData#state.sessions) of
8,824✔
1770
        error -> [];
7,992✔
1771
        {ok, JIDs} -> JIDs
832✔
1772
    end.
1773

1774
-spec is_new_nick_of_online_user(jid:simple_jid() | jid:jid(), mod_muc:nick(),
1775
                                 state()) -> boolean() | user_is_offline.
1776
is_new_nick_of_online_user(JID, Nick, StateData) ->
1777
    LJID = jid:to_lower(JID),
8,071✔
1778
    case maps:find(LJID, StateData#state.users) of
8,071✔
1779
        {ok, #user{nick = OldNick}} -> Nick /= <<>> andalso Nick /= OldNick;
44✔
1780
        error -> user_is_offline
8,027✔
1781
    end.
1782

1783
-spec is_user_limit_reached(jid:jid(), mod_muc:affiliation(), state()) -> boolean().
1784
is_user_limit_reached(From, Affiliation, StateData) ->
1785
    MaxUsers = get_max_users(StateData),
8,005✔
1786
    MaxAdminUsers = case MaxUsers of
8,005✔
1787
                        none -> none;
×
1788
                        _ -> MaxUsers + get_max_users_admin_threshold(StateData)
8,005✔
1789
                    end,
1790
    NUsers = count_users(StateData),
8,005✔
1791
    ServiceAffiliation = get_service_affiliation(From, StateData),
8,005✔
1792
    NConferences = tab_count_user(From),
8,005✔
1793
    MaxConferences = get_opt(StateData, max_user_conferences),
8,005✔
1794
    (ServiceAffiliation == owner orelse
8,005✔
1795
       MaxUsers == none orelse
8,005✔
1796
       ((Affiliation == admin orelse Affiliation == owner) andalso
8,005✔
1797
        NUsers < MaxAdminUsers) orelse
3,617✔
1798
       NUsers < MaxUsers) andalso
4,388✔
1799
      NConferences < MaxConferences.
7,994✔
1800

1801
is_next_session_of_occupant(From, Nick, StateData) ->
1802
  IsAllowed = (StateData#state.config)#config.allow_multiple_sessions,
8,005✔
1803
  case {IsAllowed, find_jids_by_nick(Nick, StateData)} of
8,005✔
1804
    {false, _} ->
1805
        false;
7,873✔
1806
    {_, []} ->
1807
        false;
88✔
1808
    {true, Jids} ->
1809
        lists:any(fun(Jid) ->
44✔
1810
          From#jid.lserver == Jid#jid.lserver
44✔
1811
          andalso From#jid.luser == Jid#jid.luser
44✔
1812
        end, Jids)
1813
  end.
1814

1815
-spec choose_new_user_strategy(jid:jid(), mod_muc:nick(),
1816
        mod_muc:affiliation(), mod_muc:role(), exml:element(),
1817
        state()) -> new_user_strategy().
1818
choose_new_user_strategy(From, Nick, Affiliation, Role, Packet, StateData) ->
1819
    case {is_user_limit_reached(From, Affiliation, StateData),
8,005✔
1820
          is_nick_exists(Nick, StateData),
1821
          is_next_session_of_occupant(From, Nick, StateData),
1822
          mod_muc:can_use_nick(StateData#state.host_type, StateData#state.host, From, Nick),
1823
          Role,
1824
          Affiliation} of
1825
        {false, _, _, _, _, _} ->
1826
            limit_reached;
11✔
1827
        {_, _, _, _, none, outcast} ->
1828
            user_banned;
11✔
1829
        {_, _, _, _, none, _} ->
1830
            require_membership;
11✔
1831
        {_, true, false, _, _, _} ->
1832
            conflict_use;
22✔
1833
        {_, _, _, false, _, _} ->
1834
            conflict_registered;
×
1835
        _ ->
1836
            choose_new_user_password_strategy(From, Packet, StateData)
7,950✔
1837
    end.
1838

1839
-spec choose_new_user_password_strategy(
1840
        jid:jid(), exml:element(), state()) -> new_user_strategy().
1841
choose_new_user_password_strategy(From, Packet, StateData) ->
1842
    ServiceAffiliation = get_service_affiliation(From, StateData),
7,950✔
1843
    Config = StateData#state.config,
7,950✔
1844
    case is_password_required(ServiceAffiliation, Config) of
7,950✔
1845
        false -> allowed;
7,470✔
1846
        true -> case extract_password(Packet) of
480✔
1847
                    undefined -> require_password;
310✔
1848
                    Password -> check_password(StateData, Password)
170✔
1849
                end
1850
    end.
1851

1852
-spec add_new_user(jid:jid(), mod_muc:nick(), exml:element(), state()) -> state().
1853
add_new_user(From, Nick, Packet, StateData) ->
1854
    Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>),
8,005✔
1855
    Affiliation = get_affiliation(From, StateData),
8,005✔
1856
    Role = get_default_role(Affiliation, StateData),
8,005✔
1857
    case choose_new_user_strategy(From, Nick, Affiliation, Role, Packet, StateData) of
8,005✔
1858
        limit_reached ->
1859
            % max user reached and user is not admin or owner
1860
            Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:service_unavailable_wait()),
11✔
1861
            route_error(Nick, From, Err, StateData);
11✔
1862
        user_banned ->
1863
            ErrText = <<"You have been banned from this room">>,
11✔
1864
            Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:forbidden(Lang, ErrText)),
11✔
1865
            route_error(Nick, From, Err, StateData);
11✔
1866
        require_membership ->
1867
            ErrText = <<"Membership is required to enter this room">>,
11✔
1868
            Err = jlib:make_error_reply(
11✔
1869
                Packet, mongoose_xmpp_errors:registration_required(Lang, ErrText)),
1870
            route_error(Nick, From, Err, StateData);
11✔
1871
        conflict_use ->
1872
            ErrText = <<"That nickname is already in use by another occupant">>,
22✔
1873
            Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:conflict(Lang, ErrText)),
22✔
1874
            route_error(Nick, From, Err, StateData);
22✔
1875
        conflict_registered ->
1876
            ErrText = <<"That nickname is registered by another person">>,
×
1877
            Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:conflict(Lang, ErrText)),
×
1878
            route_error(Nick, From, Err, StateData);
×
1879
        require_password ->
1880
            ErrText = <<"A password is required to enter this room">>,
310✔
1881
            Err = jlib:make_error_reply(
310✔
1882
                Packet, mongoose_xmpp_errors:not_authorized(Lang, ErrText)),
1883
            route_error(Nick, From, Err, StateData);
310✔
1884
        invalid_password ->
1885
            ErrText = <<"Incorrect password">>,
×
1886
            Err = jlib:make_error_reply(
×
1887
                Packet, mongoose_xmpp_errors:not_authorized(Lang, ErrText)),
1888
            route_error(Nick, From, Err, StateData);
×
1889
        http_auth ->
1890
            Password = extract_password(Packet),
77✔
1891
            perform_http_auth(From, Nick, Packet, Role, Password, StateData);
77✔
1892
        allowed ->
1893
            do_add_new_user(From, Nick, Packet, Role, StateData)
7,563✔
1894
    end.
1895

1896
perform_http_auth(From, Nick, Packet, Role, Password, StateData) ->
1897
    RoomPid = self(),
77✔
1898
    RoomJid = StateData#state.jid,
77✔
1899
    Pool = StateData#state.http_auth_pool,
77✔
1900
    case is_empty_room(StateData) of
77✔
1901
        true ->
1902
            Result = make_http_auth_request(From, RoomJid, Password, Pool),
66✔
1903
            handle_http_auth_result(Result, From, Nick, Packet, Role, StateData);
66✔
1904
        false ->
1905
            %% Perform the request in a separate process to prevent room freeze
1906
            Pid = proc_lib:spawn_link(
11✔
1907
                    fun() ->
1908
                            Result = make_http_auth_request(From, RoomJid, Password, Pool),
11✔
1909
                            gen_fsm_compat:send_event(RoomPid, {http_auth, self(), Result,
11✔
1910
                                                         From, Nick, Packet, Role})
1911
                    end),
1912
            AuthPids = StateData#state.http_auth_pids,
11✔
1913
            StateData#state{http_auth_pids = [Pid | AuthPids]}
11✔
1914
    end.
1915

1916
make_http_auth_request(From, RoomJid, Password, Pool) ->
1917
    Query = uri_string:compose_query(
77✔
1918
              [{<<"from">>, jid:to_binary(From)},
1919
               {<<"to">>, jid:to_binary(RoomJid)},
1920
               {<<"pass">>, Password}
1921
              ]),
1922
    Path = <<"check_password", "?", Query/binary>>,
77✔
1923
    case mongoose_http_client:get(global, Pool, Path, []) of
77✔
1924
        {ok, {<<"200">>, Body}} -> decode_http_auth_response(Body);
55✔
1925
        _ -> error
22✔
1926
    end.
1927

1928
handle_http_auth_result(allowed, From, Nick, Packet, Role, StateData) ->
1929
    do_add_new_user(From, Nick, Packet, Role, StateData);
33✔
1930
handle_http_auth_result({invalid_password, ErrorMsg}, From, Nick, Packet, _Role, StateData) ->
1931
    reply_not_authorized(From, Nick, Packet, StateData, ErrorMsg);
22✔
1932
handle_http_auth_result(error, From, Nick, Packet, _Role, StateData) ->
1933
    reply_service_unavailable(From, Nick, Packet, StateData, <<"Internal server error">>).
22✔
1934

1935
decode_http_auth_response(Body) ->
1936
    try decode_json_auth_response(Body) of
55✔
1937
        {0, _} ->
1938
            allowed;
33✔
1939
        {AuthCode, Msg} ->
1940
            {invalid_password, iolist_to_binary([integer_to_list(AuthCode), $ , Msg])}
22✔
1941
    catch
1942
        error:_ -> error
×
1943
    end.
1944

1945
decode_json_auth_response(Body) ->
1946
    Elements = jiffy:decode(Body, [return_maps]),
55✔
1947
    Code = maps:get(<<"code">>, Elements, undefined),
55✔
1948
    Msg = maps:get(<<"msg">>, Elements, undefined),
55✔
1949
    {Code, Msg}.
55✔
1950

1951
reply_not_authorized(From, Nick, Packet, StateData, ErrText) ->
1952
    Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>),
22✔
1953
    Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:not_authorized(Lang, ErrText)),
22✔
1954
    route_error(Nick, From, Err, StateData).
22✔
1955

1956
reply_service_unavailable(From, Nick, Packet, StateData, ErrText) ->
1957
    Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>),
22✔
1958
    Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:service_unavailable(Lang, ErrText)),
22✔
1959
    route_error(Nick, From, Err, StateData).
22✔
1960

1961
do_add_new_user(From, Nick, #xmlel{children = Els} = Packet,
1962
                Role, StateData) ->
1963
    Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>),
7,596✔
1964
    NewState =
7,596✔
1965
        add_user_presence(
1966
          From, Packet,
1967
          add_online_user(From, Nick, Role, StateData)),
1968
    send_existing_presences(From, NewState),
7,596✔
1969
    send_new_presence(From, NewState),
7,596✔
1970
    Shift = count_stanza_shift(Nick, Els, NewState),
7,596✔
1971
    case send_history(From, Shift, NewState) of
7,596✔
1972
        true ->
1973
            ok;
11✔
1974
        _ ->
1975
            send_subject(From, Lang, StateData)
7,585✔
1976
    end,
1977
    case NewState#state.just_created of
7,596✔
1978
        true ->
1979
            NewState#state{just_created = false};
3,899✔
1980
        false ->
1981
            Robots = maps:remove(From, StateData#state.robots),
3,697✔
1982
            NewState#state{robots = Robots}
3,697✔
1983
    end.
1984

1985
is_password_required(owner, _Config) ->
1986
    %% Don't check pass if user is owner in MUC service (access_admin option)
1987
    false;
×
1988
is_password_required(_, Config) ->
1989
    Config#config.password_protected.
7,950✔
1990

1991
check_password(#state{http_auth_pool = none,
1992
                      config = #config{password = Password}}, Password) ->
1993
    allowed;
93✔
1994
check_password(#state{http_auth_pool = none}, _Password) ->
1995
    ?LOG_WARNING(#{what => muc_check_password_failed,
×
1996
                   text => <<"http_auth_pool not found">>}),
×
1997
    invalid_password;
×
1998
check_password(#state{http_auth_pool = _Pool}, _Password) ->
1999
    http_auth.
77✔
2000

2001
-spec extract_password(exml:element()) ->
2002
    undefined | binary().
2003
extract_password(Packet) ->
2004
    exml_query:path(Packet, [{element_with_ns, <<"x">>, ?NS_MUC},
557✔
2005
                             {element, <<"password">>},
2006
                             cdata]).
2007

2008
-spec count_stanza_shift(mod_muc:nick(), [exml:child()],
2009
                        state()) -> any().
2010
count_stanza_shift(Nick, Els, StateData) ->
2011
    HL = lqueue_to_list(StateData#state.history),
7,596✔
2012
    Since = extract_history(Els, <<"since">>),
7,596✔
2013
    Shift0 = case Since of
7,596✔
2014
         false ->
2015
             0;
7,585✔
2016
         _ ->
2017
             count_seconds_shift(Since, HL)
11✔
2018
         end,
2019
    Seconds = extract_history(Els, <<"seconds">>),
7,596✔
2020
    Shift1 = case Seconds of
7,596✔
2021
         false ->
2022
             0;
7,596✔
2023
         _ ->
2024
             Sec = os:system_time(seconds) - Seconds,
×
2025
             count_seconds_shift(Sec, HL)
×
2026
         end,
2027
    MaxStanzas = extract_history(Els, <<"maxstanzas">>),
7,596✔
2028
    Shift2 = case MaxStanzas of
7,596✔
2029
         false ->
2030
             0;
7,596✔
2031
         _ ->
2032
             count_maxstanzas_shift(MaxStanzas, HL)
×
2033
         end,
2034
    MaxChars = extract_history(Els, <<"maxchars">>),
7,596✔
2035
    Shift3 = case MaxChars of
7,596✔
2036
         false ->
2037
             0;
7,596✔
2038
         _ ->
2039
             count_maxchars_shift(Nick, MaxChars, HL)
×
2040
         end,
2041
    lists:max([Shift0, Shift1, Shift2, Shift3]).
7,596✔
2042

2043

2044
-spec count_seconds_shift(integer(), [any()]) -> number().
2045
count_seconds_shift(Seconds, HistoryList) ->
2046
    lists:sum(
11✔
2047
      lists:map(
2048
        fun({_Nick, _Packet, _HaveSubject, TimeStamp, _Size}) ->
2049
                case TimeStamp < Seconds of
22✔
2050
                    true -> 1;
×
2051
                    false -> 0
22✔
2052
                end
2053
        end, HistoryList)).
2054

2055

2056
-spec count_maxstanzas_shift(non_neg_integer(), [any()]) -> integer().
2057
count_maxstanzas_shift(MaxStanzas, HistoryList) ->
2058
    S = length(HistoryList) - MaxStanzas,
×
2059
    max(0, S).
×
2060

2061

2062
-spec count_maxchars_shift(mod_muc:nick(), non_neg_integer(),
2063
                          [any()]) -> non_neg_integer().
2064
count_maxchars_shift(Nick, MaxSize, HistoryList) ->
2065
    NLen = string:len(binary_to_list(Nick)) + 1,
×
2066
    Sizes = lists:map(
×
2067
          fun({_Nick, _Packet, _HaveSubject, _TimeStamp, Size}) ->
2068
          Size + NLen
×
2069
          end, HistoryList),
2070
    calc_shift(MaxSize, Sizes).
×
2071

2072

2073
-spec calc_shift(non_neg_integer(), [number()]) -> non_neg_integer().
2074
calc_shift(MaxSize, Sizes) ->
2075
    Total = lists:sum(Sizes),
×
2076
    calc_shift(MaxSize, Total, 0, Sizes).
×
2077

2078

2079
-spec calc_shift(_MaxSize :: non_neg_integer(),
2080
        _Size :: number(), Shift :: non_neg_integer(), TSizes :: [number()]
2081
        ) -> non_neg_integer().
2082
calc_shift(_MaxSize, _Size, Shift, []) ->
2083
    Shift;
×
2084
calc_shift(MaxSize, Size, Shift, _Sizes) when MaxSize >= Size ->
2085
    Shift;
×
2086
calc_shift(MaxSize, Size, Shift, [S | TSizes]) ->
2087
    calc_shift(MaxSize, Size - S, Shift + 1, TSizes).
×
2088

2089

2090
-spec extract_history([exml:child()], Type :: binary()) ->
2091
    false | non_neg_integer().
2092
extract_history([], _Type) ->
2093
    false;
×
2094
extract_history([#xmlel{} = El | Els], Type) ->
2095
    case exml_query:attr(El, <<"xmlns">>, <<>>) of
30,428✔
2096
        ?NS_MUC ->
2097
            Path = [{element, <<"history">>}, {attr, Type}],
30,384✔
2098
            parse_history_val(exml_query:path(El, Path, <<>>), Type);
30,384✔
2099
        _ ->
2100
            extract_history(Els, Type)
44✔
2101
    end;
2102
extract_history([_ | Els], Type) ->
2103
    extract_history(Els, Type).
×
2104

2105
-spec parse_history_val(binary(), binary()) -> false | non_neg_integer().
2106
parse_history_val(AttrVal, <<"since">>) ->
2107
    case catch calendar:rfc3339_to_system_time(binary_to_list(AttrVal)) of
7,596✔
2108
        IntVal when is_integer(IntVal) and (IntVal >= 0) ->
2109
            IntVal;
11✔
2110
        _ ->
2111
            false
7,585✔
2112
    end;
2113
parse_history_val(AttrVal, _) ->
2114
    case catch binary_to_integer(AttrVal) of
22,788✔
2115
        IntVal when is_integer(IntVal) and (IntVal >= 0) ->
2116
            IntVal;
×
2117
        _ ->
2118
            false
22,788✔
2119
    end.
2120

2121
-spec send_update_presence(jid:jid(), Reason :: binary(), state()) -> any().
2122
send_update_presence(JID, Reason, StateData) ->
2123
    foreach_matched_jid(fun(J) ->
808✔
2124
                          send_new_presence(J, Reason, StateData)
490✔
2125
                        end, JID, StateData).
2126

2127

2128
-spec foreach_matched_jid(fun((_) -> 'ok'), jid:jid(), state()) -> ok.
2129
foreach_matched_jid(F, JID, #state{users=Users}) ->
2130
    LJID = jid:to_lower(JID),
808✔
2131
    case LJID of
808✔
2132
        %% Match by bare JID
2133
        {U, S, <<>>} ->
2134
            FF = fun({U0, S0, _}, #user{jid = MatchedJID})
808✔
2135
                       when U =:= U0, S =:= S0 ->
2136
                         F(MatchedJID);
490✔
2137
                    (_, _) -> ok
857✔
2138
                 end,
2139
            maps_foreach(FF, Users);
808✔
2140
        %% Match by full JID
2141
        _ ->
2142
            case maps:is_key(LJID, Users) of
×
2143
                true ->
2144
                    F(JID),
×
2145
                    ok;
×
2146
                false ->
2147
                    ok
×
2148
            end
2149
    end.
2150

2151

2152
-spec foreach_matched_user(fun((_) -> 'ok'), jid:simple_jid() | jid:jid(),
2153
                           state()) -> ok.
2154
foreach_matched_user(F, JID, #state{users=Users}) ->
2155
    LJID = jid:to_lower(JID),
214✔
2156
    case LJID of
214✔
2157
        %% Match by bare JID
2158
        {U, S, <<>>} ->
2159
            FF = fun({U0, S0, _}, User) when U =:= U0, S =:= S0 ->
140✔
2160
                         F(User);
55✔
2161
                    (_, _) -> ok
33✔
2162
                 end,
2163
            maps_foreach(FF, Users);
140✔
2164
        %% Match by full JID
2165
        _ ->
2166
            case maps:find(LJID, Users) of
74✔
2167
                {ok, User} -> F(User);
74✔
2168
                error -> ok
×
2169
            end
2170
    end.
2171

2172

2173
-spec foreach_user(fun((_) -> 'ok'), state()) -> any().
2174
foreach_user(F, #state{users=Users}) ->
2175
    map_foreach_value(F, Users).
3,355✔
2176

2177

2178
-spec erase_matched_users(jid:simple_jid() | jid:jid(), state()) -> state().
2179
erase_matched_users(JID, StateData=#state{users=Users, sessions=Sessions}) ->
2180
    LJID = jid:to_lower(JID),
214✔
2181
    {NewUsers, NewSessions} = erase_matched_users_map(LJID, Users, Sessions),
214✔
2182
    notify_users_modified(StateData#state{users=NewUsers, sessions=NewSessions}).
214✔
2183

2184

2185
-spec erase_matched_users_map(error | jid:simple_jid(),
2186
                              users_map(), sessions_map()) -> any().
2187
erase_matched_users_map({U, S, <<>>}, Users, Sessions) ->
2188
    FF = fun({U0, S0, _} = J, #user{nick=Nick}, {Us, Ss}) when U =:= U0 andalso S =:= S0->
140✔
2189
                 {maps:remove(J, Us), maps:remove(Nick, Ss)};
55✔
2190
            (_, _, Acc) ->
2191
                 Acc
33✔
2192
         end,
2193
    maps:fold(FF, {Users, Sessions}, Users);
140✔
2194
erase_matched_users_map(LJID, Users, Sessions) ->
2195
    {ok, #user{nick=Nick}} = maps:find(LJID, Users),
74✔
2196
    {maps:remove(LJID, Users), maps:remove(Nick, Sessions)}.
74✔
2197

2198

2199
-spec update_matched_users(F :: fun((user()) -> user()), JID :: jid:jid(),
2200
                           state()) -> state().
2201
update_matched_users(F, JID, StateData=#state{users=Users}) ->
2202
    LJID = jid:to_lower(JID),
1,011✔
2203
    NewUsers = update_matched_users_map(F, LJID, Users),
1,011✔
2204
    notify_users_modified(StateData#state{users=NewUsers}).
1,011✔
2205

2206

2207
-spec update_matched_users_map(fun((user()) -> user()),
2208
                               error | jid:simple_jid(), users_map()) -> any().
2209
update_matched_users_map(F, {U, S, <<>>}, Users) ->
2210
    FF = fun({U0, S0, _} = J, User, Us) when U =:= U0 andalso S =:= S0->
679✔
2211
                 maps:put(J, F(User), Us);
435✔
2212
            (_, _, Us) ->
2213
                 Us
780✔
2214
         end,
2215
    maps:fold(FF, Users, Users);
679✔
2216
update_matched_users_map(F, LJID, Users) ->
2217
    case maps:find(LJID, Users) of
332✔
2218
        {ok, User} -> maps:put(LJID, F(User), Users);
332✔
2219
        error -> Users
×
2220
    end.
2221

2222
-spec send_new_presence_un(jid:jid(), state()) -> 'ok'.
2223
send_new_presence_un(NJID, StateData) ->
2224
    send_new_presence_un(NJID, <<>>, StateData).
6,966✔
2225

2226

2227
-spec send_new_presence_un(jid:jid(), binary(), state()) -> 'ok'.
2228
send_new_presence_un(NJID, Reason, StateData) ->
2229
    {ok, #user{nick = Nick}} = maps:find(jid:to_lower(NJID), StateData#state.users),
6,966✔
2230
    case is_last_session(Nick, StateData) of
6,966✔
2231
        true ->
2232
            send_new_presence(NJID, Reason, StateData);
6,922✔
2233
        false ->
2234
            UserJIDs = maps:get(Nick, StateData#state.sessions),
44✔
2235
            GetUserTupleByJID = fun(JID) ->
44✔
2236
                LJID = jid:to_lower(JID),
88✔
2237
                {LJID, maps:get(LJID, StateData#state.users)}
88✔
2238
            end,
2239
            CurrentSessionUsers = lists:map(GetUserTupleByJID, UserJIDs),
44✔
2240
            send_new_presence_to(NJID, Reason, CurrentSessionUsers, StateData)
44✔
2241
    end.
2242

2243

2244
-spec send_new_presence(jid:jid(), state()) -> 'ok'.
2245
send_new_presence(NJID, StateData) ->
2246
    send_new_presence(NJID, <<>>, StateData).
7,618✔
2247

2248

2249
-spec send_new_presence(jid:jid(), binary(), state()) -> 'ok'.
2250
send_new_presence(NJID, Reason, StateData) ->
2251
    send_new_presence_to(NJID, Reason, StateData#state.users, StateData).
15,362✔
2252

2253

2254
%% Receivers can be a list or a map
2255
-spec send_new_presence_to(jid:jid(), binary(), users_map() | users_pairs(), state()) -> ok.
2256
send_new_presence_to(NJID, Reason, Receivers, StateData) ->
2257
    {ok, #user{ role = Role } = User} = maps:find(jid:to_lower(NJID), StateData#state.users),
15,406✔
2258
    Affiliation = get_affiliation(NJID, StateData),
15,406✔
2259
    BAffiliation = affiliation_to_binary(Affiliation),
15,406✔
2260
    BRole = role_to_binary(Role),
15,406✔
2261
    F = fun(_LJID, Info) ->
15,406✔
2262
        send_new_presence_to_single(NJID, User, BAffiliation, BRole, Reason, Info, StateData)
25,133✔
2263
      end,
2264
    maps_or_pairs_foreach(F, Receivers).
15,406✔
2265

2266
send_new_presence_to_single(NJID, #user{jid = RealJID, nick = Nick, last_presence = Presence},
2267
                            BAffiliation, BRole, Reason, ReceiverInfo, StateData) ->
2268
    ItemAttrs =
25,133✔
2269
    case (ReceiverInfo#user.role == moderator) orelse
10,297✔
2270
         ((StateData#state.config)#config.anonymous == false) of
14,836✔
2271
        true ->
2272
            #{<<"jid">> => jid:to_binary(RealJID),
14,632✔
2273
              <<"affiliation">> => BAffiliation,
2274
              <<"role">> => BRole};
2275
        _ ->
2276
            #{<<"affiliation">> => BAffiliation,
10,501✔
2277
              <<"role">> => BRole}
2278
    end,
2279
    ItemEls = case Reason of
25,133✔
2280
                  <<>> ->
2281
                      [];
24,847✔
2282
                  _ ->
2283
                      [#xmlel{name = <<"reason">>, children = [#xmlcdata{content = Reason}]}]
286✔
2284
              end,
2285
    Status = case StateData#state.just_created of
25,133✔
2286
                 true ->
2287
                     [status_code(201)];
3,899✔
2288
                 false ->
2289
                     []
21,234✔
2290
             end,
2291
    Status2 = case (NJID == ReceiverInfo#user.jid) of
25,133✔
2292
                  true ->
2293
                      Status0 = case (StateData#state.config)#config.logging of
15,405✔
2294
                                    true ->
2295
                                        [status_code(170) | Status];
22✔
2296
                                    false ->
2297
                                        Status
15,383✔
2298
                                end,
2299
                      Status1 = case ((StateData#state.config)#config.anonymous==false) of
15,405✔
2300
                                    true ->
2301
                                        [status_code(100) | Status0];
5,139✔
2302
                                    false ->
2303
                                        Status0
10,266✔
2304
                                end,
2305
                      case ((NJID == ReceiverInfo#user.jid)==true) of
15,405✔
2306
                          true ->
2307
                              [status_code(110) | Status1];
15,405✔
2308
                          false ->
2309
                              Status1
×
2310
                      end;
2311
                  false ->
2312
                      Status
9,728✔
2313
              end,
2314
    Packet = jlib:append_subtags(
25,133✔
2315
               Presence,
2316
               [#xmlel{name = <<"x">>, attrs = #{<<"xmlns">> => ?NS_MUC_USER},
2317
                       children = [#xmlel{name = <<"item">>, attrs = ItemAttrs,
2318
                                          children = ItemEls} | Status2]}]),
2319
    ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick),
25,133✔
2320
                          ReceiverInfo#user.jid, Packet).
2321

2322
-spec send_existing_presences(jid:jid(), state()) -> 'ok'.
2323
send_existing_presences(ToJID, StateData) ->
2324
    LToJID = jid:to_lower(ToJID),
7,596✔
2325
    {ok, #user{jid = RealToJID, role = Role, nick = _Nick}} =
7,596✔
2326
    maps:find(LToJID, StateData#state.users),
2327
    % if you don't want to send presences of other sessions of occupant with ToJID
2328
    % switch following lines
2329
    % JIDsToSkip = [RealToJID | find_jids_by_nick(_Nick, StateData)],
2330
    JIDsToSkip = [RealToJID],
7,596✔
2331
    maps_foreach(
7,596✔
2332
        fun({_, #user{jid = FromJID}} = User) ->
2333
                case lists:member(FromJID, JIDsToSkip) of
11,945✔
2334
                    true -> ok;
7,596✔
2335
                    _ -> send_existing_presence(User, Role, RealToJID, StateData)
4,349✔
2336
                end
2337
        end, StateData#state.users).
2338

2339
-spec send_existing_presence({jid:simple_jid(), mod_muc_room_user()}, mod_muc:role(),
2340
                             jid:jid(), state()) -> mongoose_acc:t().
2341
send_existing_presence({_LJID, #user{jid = FromJID, nick = FromNick,
2342
                                    role = FromRole, last_presence = Presence}},
2343
                       Role, RealToJID, StateData) ->
2344
    FromAffiliation = get_affiliation(FromJID, StateData),
4,349✔
2345
    ItemAttrs =
4,349✔
2346
    case (Role == moderator) orelse ((StateData#state.config)#config.anonymous == false) of
4,349✔
2347
        true ->
2348
            #{<<"jid">> => jid:to_binary(FromJID),
1,939✔
2349
              <<"affiliation">> => affiliation_to_binary(FromAffiliation),
2350
              <<"role">> => role_to_binary(FromRole)};
2351
        _ ->
2352
            #{<<"affiliation">> => affiliation_to_binary(FromAffiliation),
2,410✔
2353
              <<"role">> => role_to_binary(FromRole)}
2354
    end,
2355
    Packet = jlib:append_subtags(
4,349✔
2356
               Presence,
2357
               [#xmlel{name = <<"x">>,
2358
                       attrs = #{<<"xmlns">> => ?NS_MUC_USER},
2359
                       children = [#xmlel{name = <<"item">>,
2360
                                          attrs = ItemAttrs}]}]),
2361
    ejabberd_router:route(jid:replace_resource(StateData#state.jid, FromNick), RealToJID, Packet).
4,349✔
2362

2363
-spec send_config_update(atom(), state()) -> 'ok'.
2364
send_config_update(Type, StateData) ->
2365
    Status = case Type of
44✔
2366
            logging_enabled     -> <<"170">>;
11✔
2367
            logging_disabled    -> <<"171">>;
11✔
2368
            nonanonymous        -> <<"172">>;
11✔
2369
            semianonymous       -> <<"173">>
11✔
2370
        end,
2371
    Message = jlib:make_config_change_message(Status),
44✔
2372
    send_to_all_users(Message, StateData).
44✔
2373

2374

2375
-spec send_invitation(jid:jid(), jid:jid(), binary(), state()) -> mongoose_acc:t().
2376
send_invitation(From, To, Reason, StateData=#state{host=Host,
2377
                                                   server_host=ServerHost,
2378
                                                   jid=RoomJID}) ->
2379
    mongoose_hooks:invitation_sent(Host, ServerHost, RoomJID, From, To, Reason),
77✔
2380
    Config = StateData#state.config,
77✔
2381
    Password = case Config#config.password_protected of
77✔
2382
        false -> <<>>;
77✔
2383
        true -> Config#config.password
×
2384
    end,
2385
    Packet = jlib:make_invitation(jid:to_bare(From), Password, Reason),
77✔
2386
    ejabberd_router:route(RoomJID, To, Packet).
77✔
2387

2388

2389
-spec change_nick(jid:jid(), binary(), state()) -> state().
2390
change_nick(JID, Nick, StateData) ->
2391
    LJID = jid:to_lower(JID),
11✔
2392
    {ok, #user{nick = OldNick}} =
11✔
2393
    maps:find(LJID, StateData#state.users),
2394
    Users =
11✔
2395
    maps:update_with(
2396
      LJID,
2397
      fun(#user{} = User) ->
2398
              User#user{nick = Nick}
11✔
2399
      end, StateData#state.users),
2400
    {ok, JIDs} = maps:find(OldNick, StateData#state.sessions),
11✔
2401
    Sessions = maps:remove(OldNick, maps:put(Nick, JIDs, StateData#state.sessions)),
11✔
2402
    NewStateData = notify_users_modified(StateData#state{users = Users, sessions = Sessions}),
11✔
2403
    send_nick_changing(JID, OldNick, NewStateData),
11✔
2404
    add_to_log(nickchange, {OldNick, Nick}, StateData),
11✔
2405
    NewStateData.
11✔
2406

2407

2408
-spec send_nick_changing(jid:jid(), mod_muc:nick(), state()) -> 'ok'.
2409
send_nick_changing(JID, OldNick, StateData) ->
2410
    User = maps:find(jid:to_lower(JID), StateData#state.users),
11✔
2411
    {ok, #user{jid = RealJID,
11✔
2412
               nick = Nick,
2413
               role = Role,
2414
               last_presence = Presence}} = User,
2415
    Affiliation = get_affiliation(JID, StateData),
11✔
2416
    maps_foreach(mk_send_nick_change(Presence, OldNick, JID, RealJID,
11✔
2417
                                      Affiliation, Role, Nick, StateData),
2418
                  StateData#state.users).
2419

2420
mk_send_nick_change(Presence, OldNick, JID, RealJID,  Affiliation,
2421
                    Role, Nick, StateData) ->
2422
    fun({LJID, Info}) ->
11✔
2423
            send_nick_change(Presence, OldNick, JID, RealJID, Affiliation,
22✔
2424
                             Role, Nick, LJID, Info, StateData)
2425
    end.
2426

2427
send_nick_change(Presence, OldNick, JID, RealJID, Affiliation, Role,
2428
                 Nick, _LJID, Info, #state{} = S) ->
2429
    MaybePublicJID = case is_nick_change_public(Info, S#state.config) of
22✔
2430
                         true -> RealJID;
×
2431
                         false -> undefined
22✔
2432
                     end,
2433
    MaybeSelfPresenceCode = case JID == Info#user.jid of
22✔
2434
                                true -> status_code(110);
11✔
2435
                                false -> undefined
11✔
2436
                            end,
2437
    Unavailable = nick_unavailable_presence(MaybePublicJID, Nick, Affiliation,
22✔
2438
                                            Role, MaybeSelfPresenceCode),
2439
    ejabberd_router:route(jid:replace_resource(S#state.jid, OldNick),
22✔
2440
                          Info#user.jid, Unavailable),
2441
    Available = nick_available_presence(Presence, MaybePublicJID, Affiliation,
22✔
2442
                                        Role, MaybeSelfPresenceCode),
2443
    ejabberd_router:route(jid:replace_resource(S#state.jid, Nick),
22✔
2444
                          Info#user.jid, Available).
2445

2446
-spec is_nick_change_public(user(), config()) -> boolean().
2447
is_nick_change_public(UserInfo, RoomConfig) ->
2448
    UserInfo#user.role == moderator
22✔
2449
    orelse
2450
    RoomConfig#config.anonymous == false.
22✔
2451

2452
-spec status_code(integer()) -> exml:element().
2453
status_code(Code) ->
2454
    #xmlel{name = <<"status">>,
24,531✔
2455
           attrs = #{<<"code">> => integer_to_binary(Code)}}.
2456

2457
-spec nick_unavailable_presence(MaybeJID, Nick, Affiliation, Role, MaybeCode) ->
2458
    exml:element() when
2459
      MaybeJID :: 'undefined' | jid:jid(),
2460
      Nick :: mod_muc:nick(),
2461
      Affiliation :: mod_muc:affiliation(),
2462
      Role :: mod_muc:role(),
2463
      MaybeCode :: 'undefined' | exml:element().
2464
nick_unavailable_presence(MaybeJID, Nick, Affiliation, Role, MaybeCode) ->
2465
    presence(<<"unavailable">>,
22✔
2466
             [muc_user_x([muc_user_item(MaybeJID, Nick, Affiliation, Role),
2467
                          status_code(303)]
2468
                         ++ [MaybeCode || MaybeCode /= undefined])]).
11✔
2469

2470
-spec nick_available_presence(LastPresence, MaybeJID, Affiliation,
2471
                              Role, MaybeCode) -> exml:element() when
2472
      LastPresence :: exml:element(),
2473
      MaybeJID :: 'undefined' | jid:jid(),
2474
      Affiliation :: mod_muc:affiliation(),
2475
      Role :: mod_muc:role(),
2476
      MaybeCode :: 'undefined' | exml:element().
2477
nick_available_presence(LastPresence, MaybeJID, Affiliation, Role, MaybeCode) ->
2478
    Item = muc_user_item(MaybeJID, undefined, Affiliation, Role),
22✔
2479
    jlib:append_subtags(LastPresence,
22✔
2480
                       [muc_user_x([Item] ++ [MaybeCode
11✔
2481
                                              || MaybeCode /= undefined])]).
22✔
2482

2483
-spec muc_user_item(MaybeJID, MaybeNick, Affiliation, Role) -> exml:element() when
2484
      MaybeJID :: 'undefined' | jid:jid(),
2485
      MaybeNick :: 'undefined' | mod_muc:nick(),
2486
      Affiliation :: mod_muc:affiliation(),
2487
      Role :: mod_muc:role().
2488
muc_user_item(MaybeJID, MaybeNick, Affiliation, Role) ->
2489
    Attr1 = if MaybeJID =:= undefined -> #{};
44✔
2490
               true -> #{<<"jid">> => jid:to_binary(MaybeJID)}
×
2491
            end,
2492
    Attr2 = if MaybeNick =:= undefined -> Attr1;
44✔
2493
               true -> Attr1#{<<"nick">> => MaybeNick}
22✔
2494
            end,
2495
    #xmlel{name = <<"item">>,
44✔
2496
           attrs = Attr2#{<<"affiliation">> => affiliation_to_binary(Affiliation),
2497
                          <<"role">> => role_to_binary(Role)}}.
2498

2499
-spec muc_user_x([exml:element()]) -> exml:element().
2500
muc_user_x(Children) ->
2501
    #xmlel{name = <<"x">>,
44✔
2502
           attrs = #{<<"xmlns">> => ?NS_MUC_USER},
2503
           children = Children}.
2504

2505
-spec presence(binary(), [exml:element()]) -> exml:element().
2506
%% Add and validate other types if need be.
2507
presence(<<"unavailable">> = Type, Children) ->
2508
    #xmlel{name = <<"presence">>,
22✔
2509
           attrs = #{<<"type">> => Type || Type /= <<"available">>},
22✔
2510
           children = Children}.
2511

2512

2513
-spec lqueue_new(integer()) -> lqueue().
2514
lqueue_new(Max) ->
2515
    #lqueue{queue = queue:new(),
6,426✔
2516
        len = 0,
2517
        max = Max}.
2518

2519

2520
%% @doc If the message queue limit is set to 0, do not store messages.
2521
%% Otherwise, rotate messages in the queue store.
2522
-spec lqueue_in(any(), lqueue()) -> lqueue().
2523
lqueue_in(_Item, LQ = #lqueue{max = 0}) ->
2524
    LQ;
×
2525
lqueue_in(Item, #lqueue{queue = Q1, len = Len, max = Max}) ->
2526
    Q2 = queue:in(Item, Q1),
8,518✔
2527
    case Len >= Max of
8,518✔
2528
        true ->
2529
            Q3 = lqueue_cut(Q2, Len - Max + 1),
1,416✔
2530
            #lqueue{queue = Q3, len = Max, max = Max};
1,416✔
2531
        false ->
2532
            #lqueue{queue = Q2, len = Len + 1, max = Max}
7,102✔
2533
    end.
2534

2535

2536
-spec lqueue_cut(queue:queue(), non_neg_integer()) -> queue:queue().
2537
lqueue_cut(Q, 0) ->
2538
    Q;
1,416✔
2539
lqueue_cut(Q, N) ->
2540
    {_, Q1} = queue:out(Q),
1,416✔
2541
    lqueue_cut(Q1, N - 1).
1,416✔
2542

2543

2544
-spec lqueue_to_list(lqueue()) -> [any()].
2545
lqueue_to_list(#lqueue{queue = Q1}) ->
2546
    queue:to_list(Q1).
15,192✔
2547

2548

2549
-spec add_message_to_history(mod_muc:nick(), jid:jid(), exml:element(),
2550
                            state()) -> state().
2551
add_message_to_history(FromNick, FromJID, Packet, StateData) ->
2552
    HaveSubject = undefined =/= exml_query:subelement(Packet, <<"subject">>),
8,518✔
2553
    SystemTime = os:system_time(second),
8,518✔
2554
    TimeStamp = calendar:system_time_to_rfc3339(SystemTime, [{offset, "Z"}]),
8,518✔
2555
    %% Chatroom history is stored as XMPP packets, so
2556
    %% the decision to include the original sender's JID or not is based on the
2557
    %% chatroom configuration when the message was originally sent.
2558
    %% Also, if the chatroom is anonymous, even moderators will not get the real JID
2559
    SenderJid = case   (StateData#state.config)#config.anonymous of
8,518✔
2560
    true -> StateData#state.jid;
5,892✔
2561
    false -> FromJID
2,626✔
2562
    end,
2563
    TSPacket = jlib:append_subtags(Packet, [jlib:timestamp_to_xml(TimeStamp, SenderJid, <<>>)]),
8,518✔
2564
    SPacket = jlib:replace_from_to(
8,518✔
2565
        jid:replace_resource(StateData#state.jid, FromNick),
2566
        StateData#state.jid,
2567
        TSPacket),
2568
    Size = element_size(SPacket),
8,518✔
2569
    Q1 = lqueue_in({FromNick, TSPacket, HaveSubject, SystemTime, Size},
8,518✔
2570
           StateData#state.history),
2571
    add_to_log(text, {FromNick, Packet}, StateData),
8,518✔
2572
    mongoose_hooks:room_packet(StateData#state.host,
8,518✔
2573
                               FromNick, FromJID, StateData#state.jid, Packet),
2574
    StateData#state{history = Q1}.
8,518✔
2575

2576

2577
-spec send_history(jid:jid(), Shift :: non_neg_integer(), state()) -> boolean().
2578
send_history(JID, Shift, StateData) ->
2579
    lists:foldl(
7,596✔
2580
      fun({Nick, Packet, HaveSubject, _TimeStamp, _Size}, B) ->
2581
          ejabberd_router:route(
173✔
2582
        jid:replace_resource(StateData#state.jid, Nick),
2583
        JID,
2584
        Packet),
2585
          B or HaveSubject
173✔
2586
      end, false, lists:nthtail(Shift, lqueue_to_list(StateData#state.history))).
2587

2588

2589
-spec send_subject(jid:jid(), ejabberd:lang(), state()) -> mongoose_acc:t().
2590
send_subject(JID, _Lang, StateData = #state{subject = <<>>, subject_author = <<>>}) ->
2591
    Packet = #xmlel{name = <<"message">>,
7,563✔
2592
                    attrs = #{<<"type">> => <<"groupchat">>},
2593
                    children = [#xmlel{name = <<"subject">>},
2594
                                #xmlel{name = <<"body">>}]},
2595
    ejabberd_router:route(
7,563✔
2596
        StateData#state.jid,
2597
        JID,
2598
        Packet);
2599
send_subject(JID, _Lang, StateData) ->
2600
    Subject = StateData#state.subject,
22✔
2601
    TimeStamp = StateData#state.subject_timestamp,
22✔
2602
    RoomJID = StateData#state.jid,
22✔
2603
    Packet = #xmlel{name = <<"message">>,
22✔
2604
                    attrs = #{<<"type">> => <<"groupchat">>},
2605
                    children = [#xmlel{name = <<"subject">>,
2606
                                       children = [#xmlcdata{content = Subject}]},
2607
                                #xmlel{name = <<"delay">>,
2608
                                       attrs = #{<<"xmlns">> => ?NS_DELAY,
2609
                                                 <<"from">> => jid:to_binary(RoomJID),
2610
                                                 <<"stamp">> => TimeStamp}}]},
2611
    ejabberd_router:route(RoomJID, JID, Packet).
22✔
2612

2613

2614
-spec check_subject(exml:element()) -> undefined | binary().
2615
check_subject(Packet) ->
2616
    exml_query:path(Packet, [{element, <<"subject">>}, cdata]).
8,529✔
2617

2618
-spec can_change_subject(mod_muc:role(), state()) -> boolean().
2619
can_change_subject(Role, StateData) ->
2620
    case (StateData#state.config)#config.allow_change_subj of
33✔
2621
        true ->
2622
            (Role == moderator) orelse (Role == participant);
11✔
2623
        _ ->
2624
            Role == moderator
22✔
2625
    end.
2626

2627
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2628
% Admin stuff
2629

2630
-spec process_iq_admin(jid:jid(), get | set, ejabberd:lang(), exml:element(), state()) ->
2631
    state() | {error, exml:element()}.
2632
process_iq_admin(From, set, Lang, SubEl, StateData) ->
2633
    #xmlel{children = Items} = SubEl,
818✔
2634
    process_admin_items_set(From, Items, Lang, StateData);
818✔
2635
process_iq_admin(From, get, Lang, SubEl, StateData) ->
2636
    case exml_query:subelement(SubEl, <<"item">>) of
407✔
2637
        undefined ->
2638
            {error, mongoose_xmpp_errors:bad_request()};
×
2639
        Item ->
2640
            FAffiliation = get_affiliation(From, StateData),
407✔
2641
            FRole = get_role(From, StateData),
407✔
2642
            {RoleOrAff, _} = ExtractResult = extract_role_or_affiliation(Item),
407✔
2643
            IsAllowed = iq_admin_allowed(get, RoleOrAff, FAffiliation, FRole, StateData),
407✔
2644
            case {IsAllowed, ExtractResult} of
407✔
2645
                {true, {role, Role}} ->
2646
                    Items = items_with_role(Role, StateData),
165✔
2647
                    {result, Items, StateData};
165✔
2648
                {true, {affiliation, Affiliation}} ->
2649
                    Items = items_with_affiliation(Affiliation, StateData),
143✔
2650
                    {result, Items, StateData};
143✔
2651
                {_, {role, _}} ->
2652
                    ErrText = <<"Moderator privileges required">>,
11✔
2653
                    {error, mongoose_xmpp_errors:forbidden(Lang, ErrText)};
11✔
2654
                {_, {affiliation, _}} ->
2655
                    ErrText = <<"Administrator privileges required">>,
88✔
2656
                    {error, mongoose_xmpp_errors:forbidden(Lang, ErrText)};
88✔
2657
                {_, Error} ->
2658
                    Error
×
2659
            end
2660
    end.
2661

2662
-spec extract_role_or_affiliation(Item :: exml:element()) ->
2663
    {role, mod_muc:role()} | {affiliation, mod_muc:affiliation()} | {error, exml:element()}.
2664
extract_role_or_affiliation(Item) ->
2665
    case {exml_query:attr(Item, <<"role">>), exml_query:attr(Item, <<"affiliation">>)} of
407✔
2666
        {undefined, undefined} ->
2667
            {error, mongoose_xmpp_errors:bad_request()};
×
2668
        {undefined, BAffiliation} ->
2669
            case catch binary_to_affiliation(BAffiliation) of
231✔
2670
                {'EXIT', _} -> {error, mongoose_xmpp_errors:bad_request()};
×
2671
                Affiliation -> {affiliation, Affiliation}
231✔
2672
            end;
2673
        {BRole, _} ->
2674
            case catch binary_to_role(BRole) of
176✔
2675
                {'EXIT', _} -> {error, mongoose_xmpp_errors:bad_request()};
×
2676
                Role -> {role, Role}
176✔
2677
            end
2678
    end.
2679

2680
-spec iq_admin_allowed(atom(), atom(), atom(), atom(), state()) -> boolean().
2681
iq_admin_allowed(get, What, FAff, none, State) ->
2682
    %% no role is translated to 'visitor'
2683
    iq_admin_allowed(get, What, FAff, visitor, State);
88✔
2684
iq_admin_allowed(get, role, _, moderator, _) ->
2685
    %% moderator is allowed by definition, needs it to do his duty
2686
    true;
143✔
2687
iq_admin_allowed(get, role, _, Role, State) ->
2688
    Cfg = State#state.config,
33✔
2689
    lists:member(Role, Cfg#config.maygetmemberlist);
33✔
2690
iq_admin_allowed(get, affiliation, owner, _, _) ->
2691
    true;
110✔
2692
iq_admin_allowed(get, affiliation, admin, _, _) ->
2693
    true;
×
2694
iq_admin_allowed(get, affiliation, _, Role, State) ->
2695
    Cfg = State#state.config,
121✔
2696
    lists:member(Role, Cfg#config.maygetmemberlist).
121✔
2697

2698

2699
-spec items_with_role(mod_muc:role(), state()) -> [exml:element()].
2700
items_with_role(BRole, StateData) ->
2701
    lists:map(
165✔
2702
      fun({_, U}) ->
2703
          user_to_item(U, StateData)
308✔
2704
      end, search_role(BRole, StateData)).
2705

2706

2707
-spec items_with_affiliation(mod_muc:affiliation(), state()) -> [exml:element()].
2708
items_with_affiliation(BAffiliation, StateData) ->
2709
    lists:map(
143✔
2710
      fun({JID, {Affiliation, Reason}}) ->
2711
          #xmlel{name = <<"item">>,
33✔
2712
                 attrs = #{<<"affiliation">> => affiliation_to_binary(Affiliation),
2713
                           <<"jid">> => jid:to_binary(JID)},
2714
                 children = [#xmlel{name = <<"reason">>,
2715
                                    children = [#xmlcdata{content = Reason}]}]};
2716
         ({JID, Affiliation}) ->
2717
              #xmlel{name = <<"item">>,
11✔
2718
                     attrs = #{<<"affiliation">> => affiliation_to_binary(Affiliation),
2719
                               <<"jid">> => jid:to_binary(JID)}}
2720
      end, search_affiliation(BAffiliation, StateData)).
2721

2722

2723
-spec user_to_item(user(), state()) -> exml:element().
2724
user_to_item(#user{role = Role,
2725
           nick = Nick,
2726
           jid = JID
2727
          }, StateData) ->
2728
    Affiliation = get_affiliation(JID, StateData),
308✔
2729
    #xmlel{name = <<"item">>,
308✔
2730
           attrs = #{<<"role">> => role_to_binary(Role),
2731
                     <<"affiliation">> => affiliation_to_binary(Affiliation),
2732
                     <<"nick">> => Nick,
2733
                     <<"jid">> => jid:to_binary(JID)}}.
2734

2735

2736
-spec search_role(mod_muc:role(), state()) -> users_pairs().
2737
search_role(Role, StateData) ->
2738
    F = fun(_, #user{role = R}) -> Role == R end,
187✔
2739
    maps:to_list(maps:filter(F, StateData#state.users)).
187✔
2740

2741

2742
-spec search_affiliation(mod_muc:affiliation(), state()) -> [{_, _}].
2743
search_affiliation(Affiliation, StateData) when is_atom(Affiliation) ->
2744
    F = fun(_, A) ->
206✔
2745
          case A of
302✔
2746
          {A1, _Reason} ->
2747
              Affiliation == A1;
96✔
2748
          _ ->
2749
              Affiliation == A
206✔
2750
          end
2751
      end,
2752
    maps:to_list(maps:filter(F, StateData#state.affiliations)).
206✔
2753

2754

2755
-spec process_admin_items_set(jid:jid(), [exml:element(), ...], ejabberd:lang(), state()) ->
2756
    {'error', exml:element()} | {'result', [], state()}.
2757
process_admin_items_set(UJID, Items, Lang, StateData) ->
2758
    UAffiliation = get_affiliation(UJID, StateData),
1,404✔
2759
    URole = get_role(UJID, StateData),
1,404✔
2760
    case find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, []) of
1,404✔
2761
        {result, Res} ->
2762
            %% TODO Pass Acc here
2763
            ?LOG_INFO(ls(#{what => muc_admin_query, text => <<"Processing MUC admin query">>,
1,154✔
2764
                           from_jid => jid:to_binary(UJID), result => Res}, StateData)),
1,154✔
2765
            NSD = lists:foldl(
1,154✔
2766
                    fun(ChangedItem, SD) ->
2767
                            process_admin_item_set(ChangedItem, UJID, SD)
1,354✔
2768
                    end, StateData, Res),
2769
            save_persistent_room_state(NSD),
1,154✔
2770
            {result, [], NSD};
1,154✔
2771
        Err ->
2772
            Err
250✔
2773
    end.
2774

2775
process_admin_item_set(ChangedItem, UJID, SD) ->
2776
    try
1,354✔
2777
        process_admin_item_set_unsafe(ChangedItem, UJID, SD)
1,354✔
2778
    catch
2779
        Class:Reason:Stacktrace ->
2780
            ?LOG_ERROR(ls(#{what => muc_admin_item_set_failed,
×
2781
                            from_jid => jid:to_binary(UJID),
2782
                            changed_item => ChangedItem,
2783
                            class => Class, reason => Reason, stacktrace => Stacktrace}, SD)),
×
2784
            SD
×
2785
    end.
2786

2787
process_admin_item_set_unsafe({JID, affiliation, owner, _}, _UJID, SD)
2788
  when (JID#jid.luser == <<>>) ->
2789
    %% If the provided JID does not have username,
2790
    %% ignore the affiliation completely
2791
    SD;
×
2792
process_admin_item_set_unsafe({JID, role, none, Reason}, _UJID, SD) ->
2793
    safe_send_kickban_presence(JID, Reason, <<"307">>, SD),
74✔
2794
    set_role(JID, none, SD);
74✔
2795
process_admin_item_set_unsafe({JID, affiliation, none, Reason}, _UJID, SD) ->
2796
    case  (SD#state.config)#config.members_only of
162✔
2797
        true ->
2798
            safe_send_kickban_presence(JID, Reason, <<"321">>, none, SD),
33✔
2799
            SD1 = set_affiliation_and_reason(JID, none, Reason, SD),
33✔
2800
            set_role(JID, none, SD1);
33✔
2801
        _ ->
2802
            SD1 = set_affiliation_and_reason(JID, none, Reason, SD),
129✔
2803
            send_update_presence(JID, Reason, SD1),
129✔
2804
            SD1
129✔
2805
    end;
2806
process_admin_item_set_unsafe({JID, affiliation, outcast, Reason}, _UJID, SD) ->
2807
    safe_send_kickban_presence(JID, Reason, <<"301">>, outcast, SD),
107✔
2808
    set_affiliation_and_reason(JID, outcast, Reason, set_role(JID, none, SD));
107✔
2809
process_admin_item_set_unsafe({JID, affiliation, A, Reason}, _UJID, SD)
2810
  when (A == admin) or (A == owner) ->
2811
    SD1 = set_affiliation_and_reason(JID, A, Reason, SD),
203✔
2812
    SD2 = set_role(JID, moderator, SD1),
203✔
2813
    send_update_presence(JID, Reason, SD2),
203✔
2814
    SD2;
203✔
2815
process_admin_item_set_unsafe({JID, affiliation, member, Reason}, UJID, SD) ->
2816
    case (SD#state.config)#config.members_only of
476✔
2817
        true -> send_invitation(UJID, JID, Reason, SD);
77✔
2818
        _ -> ok
399✔
2819
    end,
2820
    SD1 = set_affiliation_and_reason(JID, member, Reason, SD),
476✔
2821
    SD2 = set_role(JID, participant, SD1),
476✔
2822
    send_update_presence(JID, Reason, SD2),
476✔
2823
    SD2;
476✔
2824
process_admin_item_set_unsafe({JID, role, Role, Reason}, _UJID, SD) ->
2825
    SD1 = set_role(JID, Role, SD),
332✔
2826
    catch send_new_presence(JID, Reason, SD1),
332✔
2827
    SD1;
332✔
2828
process_admin_item_set_unsafe({JID, affiliation, A, Reason}, _UJID, SD) ->
2829
    SD1 = set_affiliation(JID, A, SD),
×
2830
    send_update_presence(JID, Reason, SD1),
×
2831
    SD1.
×
2832

2833
-type res_row() :: {jid:simple_jid() | jid:jid(),
2834
                    'affiliation' | 'role', any(), any()}.
2835
-type find_changed_items_res() :: {'error', exml:element()} | {'result', [res_row()]}.
2836
-spec find_changed_items(jid:jid(), mod_muc:affiliation(), mod_muc:role(),
2837
                         [exml:element()], ejabberd:lang(), state(), [res_row()]) ->
2838
    find_changed_items_res().
2839
find_changed_items(_UJID, _UAffiliation, _URole, [], _Lang, _StateData, Res) ->
2840
    {result, Res};
1,154✔
2841
find_changed_items(UJID, UAffiliation, URole, [#xmlcdata{} | Items],
2842
                   Lang, StateData, Res) ->
2843
    find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, Res);
×
2844
find_changed_items(UJID, UAffiliation, URole,
2845
                   [#xmlel{name = <<"item">>} = Item | Items],
2846
                   Lang, StateData, Res) ->
2847
    case get_affected_jid(Item, Lang, StateData) of
1,615✔
2848
        {value, JID} ->
2849
            check_changed_item(UJID, UAffiliation, URole, JID, Item, Items, Lang, StateData, Res);
1,552✔
2850
        Err ->
2851
            Err
63✔
2852
    end;
2853
find_changed_items(_UJID, _UAffiliation, _URole, _Items, _Lang, _StateData, _Res) ->
2854
    {error, mongoose_xmpp_errors:bad_request()}.
×
2855

2856
-spec get_affected_jid(Item :: exml:element(),
2857
                       Lang :: ejabberd:lang(),
2858
                       StateData :: state()) ->
2859
    {value,jid:jid()} | {error, exml:element()}.
2860
get_affected_jid(Item, Lang, StateData) ->
2861
    case {exml_query:attr(Item, <<"jid">>), exml_query:attr(Item, <<"nick">>)} of
1,615✔
2862
        {S, _} when undefined =/= S ->
2863
            case jid:from_binary(S) of
1,080✔
2864
                error ->
2865
                    ErrText = <<(service_translations:do(Lang, <<"Jabber ID ">>))/binary,
11✔
2866
                                S/binary, (service_translations:do(Lang, <<" is invalid">>))/binary>>,
2867
                    {error, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)};
11✔
2868
                J ->
2869
                    {value, J}
1,069✔
2870
            end;
2871
        {_, N} when undefined =/= N ->
2872
            case find_jids_by_nick(N, StateData) of
535✔
2873
                [] ->
2874
                    ErrText
52✔
2875
                    = <<(service_translations:do(Lang, <<"Nickname ">>))/binary, N/binary,
2876
                        (service_translations:do(Lang, <<" does not exist in the room">>))/binary>>,
2877
                    {error, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)};
52✔
2878
                [FirstSessionJid | _RestOfSessions] ->
2879
                    {value, FirstSessionJid}
483✔
2880
            end;
2881
        _ ->
2882
            {error, mongoose_xmpp_errors:bad_request()}
×
2883
    end.
2884

2885
-spec check_changed_item(jid:jid(), mod_muc:affiliation(), mod_muc:role(),jid:jid(), exml:element(),
2886
                         [exml:element()], ejabberd:lang(), state(), [res_row()]) ->
2887
    find_changed_items_res().
2888
check_changed_item(UJID, UAffiliation, URole, JID, Item, Items, Lang, StateData, Res) ->
2889
    TAffiliation = get_affiliation(JID, StateData),
1,552✔
2890
    TRole = get_role(JID, StateData),
1,552✔
2891
    case which_property_changed(Item, Lang) of
1,552✔
2892
        {role, Role} ->
2893
            ServiceAf = get_service_affiliation(JID, StateData),
472✔
2894
            CanChangeRA =
472✔
2895
            case can_change_ra(UAffiliation, URole, TAffiliation, TRole, role, Role, ServiceAf) of
2896
                nothing -> nothing;
11✔
2897
                true -> true;
406✔
2898
                check_owner -> is_owner(UJID, StateData);
×
2899
                _ -> false
55✔
2900
            end,
2901
            case CanChangeRA of
472✔
2902
                nothing -> find_changed_items(UJID, UAffiliation, URole,
11✔
2903
                                              Items, Lang, StateData, Res);
2904
                true -> find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData,
406✔
2905
                                           [{JID, role, Role, decode_reason(Item)} | Res]);
2906
                _ -> {error, mongoose_xmpp_errors:not_allowed()}
55✔
2907
            end;
2908
        {affiliation, Affiliation} ->
2909
            ServiceAf = get_service_affiliation(JID, StateData),
1,058✔
2910
            CanChangeRA =
1,058✔
2911
            case can_change_ra(UAffiliation, URole, TAffiliation, TRole,
2912
                               affiliation, Affiliation, ServiceAf) of
2913
                nothing -> nothing;
×
2914
                true -> true;
885✔
2915
                cancel -> cancel;
22✔
2916
                check_owner -> is_owner(UJID, StateData);
63✔
2917
                _ ->
2918
                    false
88✔
2919
            end,
2920
            case CanChangeRA of
1,058✔
2921
                nothing -> find_changed_items(UJID, UAffiliation, URole, Items,
×
2922
                                              Lang, StateData, Res);
2923
                true -> find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData,
948✔
2924
                                           [{jid:to_bare(JID), affiliation,
2925
                                             Affiliation, decode_reason(Item)} | Res]);
2926
                cancel -> {error, mongoose_xmpp_errors:not_allowed()};
22✔
2927
                false -> {error, mongoose_xmpp_errors:forbidden()}
88✔
2928
            end;
2929
        Err -> Err
22✔
2930
    end.
2931

2932
-spec is_owner(UJID ::jid:jid(), StateData :: state()) -> boolean().
2933
is_owner(UJID, StateData) ->
2934
    case search_affiliation(owner, StateData) of
63✔
2935
        [{OJID, _}] -> jid:to_bare(OJID) /= jid:to_lower(jid:to_bare(UJID));
×
2936
        _ -> true
63✔
2937
    end.
2938

2939
-spec which_property_changed(Item :: exml:element(), Lang :: ejabberd:lang()) ->
2940
    {affiliation, mod_muc:affiliation()} | {role, mod_muc:role()} | {error, exml:element()}.
2941
which_property_changed(Item, Lang) ->
2942
    case {exml_query:attr(Item, <<"role">>), exml_query:attr(Item, <<"affiliation">>)} of
1,552✔
2943
        {undefined, undefined} ->
2944
            {error, mongoose_xmpp_errors:bad_request()};
×
2945
        {undefined, BAffiliation} ->
2946
            case catch binary_to_affiliation(BAffiliation) of
1,069✔
2947
                {'EXIT', _} ->
2948
                    ErrText1 = <<(service_translations:do(Lang, <<"Invalid affiliation ">>))/binary,
11✔
2949
                                 BAffiliation/binary>>,
2950
                    {error, mongoose_xmpp_errors:not_acceptable(Lang, ErrText1)};
11✔
2951
                Affiliation ->
2952
                    {affiliation, Affiliation}
1,058✔
2953
            end;
2954
        {BRole, _} ->
2955
            case catch binary_to_role(BRole) of
483✔
2956
                {'EXIT', _} ->
2957
                    ErrText1 = <<(service_translations:do(Lang, <<"Invalid role ">>))/binary,
11✔
2958
                                 BRole/binary>>,
2959
                    {error, mongoose_xmpp_errors:bad_request(Lang, ErrText1)};
11✔
2960
                Role ->
2961
                    {role, Role}
472✔
2962
            end
2963
    end.
2964

2965
-spec can_change_ra(FAff :: mod_muc:affiliation(), FRole :: mod_muc:role(),
2966
        TAff :: mod_muc:affiliation(), TRole :: mod_muc:role(),
2967
        RoleOrAff :: affiliation | role, Value :: any(),
2968
        ServiceAff :: mod_muc:affiliation())
2969
            -> cancel | check_owner | false | nothing | true.
2970
can_change_ra(FAff, _FRole, TAff, _TRole, affiliation, Value, ServiceAff) ->
2971
    can_change_aff(FAff, TAff, Value, ServiceAff);
1,058✔
2972
can_change_ra(FAff, FRole, TAff, TRole, role, Value, ServiceAff) ->
2973
    can_change_role(FAff, FRole, TAff, TRole, Value, ServiceAff).
472✔
2974

2975
%% A room owner tries to add as persistent owner a
2976
%% participant that is already owner because he is MUC admin:
2977
can_change_aff(_FAff, owner, owner, owner) -> true;
×
2978
%% Nobody can decrease MUC admin's role/affiliation:
2979
can_change_aff(_FAff, _TAff, _Value, owner) -> false;
×
2980
can_change_aff(FAff, TAff, Value, _ServiceAf) ->
2981
    can_change_aff(FAff, TAff, Value).
1,058✔
2982

2983
%% Nobody can decrease MUC admin's role/affiliation:
2984
can_change_role(_FAff, _FRole, _TAff, _TRole, _Value, owner) -> false;
×
2985
can_change_role(FAff, FRole, TAff, TRole, Value, _ServiceAf) ->
2986
    can_change_role(FAff, FRole, TAff, TRole, Value).
472✔
2987

2988
%% Arguments:
2989
%% - Affiliation of the user making the request
2990
%% - Old affiliation
2991
%% - New affiliation
2992
can_change_aff(_FAff, Aff, Aff) -> nothing;
×
2993
can_change_aff(owner, outcast, none) -> true;
22✔
2994
can_change_aff(owner, outcast, member) -> true;
×
2995
can_change_aff(owner, outcast, admin) -> true;
×
2996
can_change_aff(owner, outcast, owner) -> true;
×
2997
can_change_aff(owner, none, outcast) -> true;
96✔
2998
can_change_aff(owner, none, member) -> true;
465✔
2999
can_change_aff(owner, none, admin) -> true;
55✔
3000
can_change_aff(owner, none, owner) -> true;
44✔
3001
can_change_aff(owner, member, outcast) -> true;
×
3002
can_change_aff(owner, member, none) -> true;
66✔
3003
can_change_aff(owner, member, admin) -> true;
41✔
3004
can_change_aff(owner, member, owner) -> true;
×
3005
can_change_aff(owner, admin, _Aff) -> true;
63✔
3006
can_change_aff(owner, owner, _Aff) -> check_owner;
63✔
3007
can_change_aff(admin, none, member) -> true;
11✔
3008
can_change_aff(admin, none, outcast) -> true;
11✔
3009
can_change_aff(admin, outcast, none) -> true;
×
3010
can_change_aff(admin, outcast, member) -> true;
×
3011
can_change_aff(admin, member, outcast) -> true;
×
3012
can_change_aff(admin, member, none) -> true;
11✔
3013
can_change_aff(none,  admin, _Aff) -> cancel;
×
3014
can_change_aff(none,  owner, _Aff) -> cancel;
11✔
3015
can_change_aff(admin, owner, _Aff) -> cancel;
11✔
3016
can_change_aff(_FAff, _TAff, _Aff) -> false.
88✔
3017

3018
%% Arguments:
3019
%% - Affiliation of the user making the request
3020
%% - Role of the user making the request
3021
%% - Old affiliation
3022
%% - Old role
3023
%% - New role
3024
can_change_role(_FAff, _FRole, _TAff, Role, Role) -> nothing;
11✔
3025
can_change_role(_FAff, moderator, _TAff, visitor, none) -> true;
22✔
3026
can_change_role(_FAff, moderator, _TAff, visitor, participant) -> true;
107✔
3027
can_change_role(owner, _FRole, _TAff, visitor, moderator) -> true;
×
3028
can_change_role(admin, _FRole, _TAff, visitor, moderator) -> true;
×
3029
can_change_role(_FAff, moderator, _TAff, participant, none) -> true;
52✔
3030
can_change_role(_FAff, moderator, _TAff, participant, visitor) -> true;
85✔
3031
can_change_role(owner, _FRole, _TAff, participant, moderator) -> true;
96✔
3032
can_change_role(admin, _FRole, _TAff, participant, moderator) -> true;
×
3033
%% Owner/admin are always moderators:
3034
can_change_role(_FAff, _FRole, owner, moderator, visitor) -> false;
11✔
3035
can_change_role(_FAff, _FRole, admin, moderator, visitor) -> false;
×
3036
can_change_role(_FAff, _FRole, owner, moderator, participant) -> false;
11✔
3037
can_change_role(_FAff, _FRole, admin, moderator, participant) -> false;
×
3038
%% Non owner/admin could loose their moderator status:
3039
can_change_role(owner, _FRole, _TAff, moderator, visitor) -> true;
×
3040
can_change_role(admin, _FRole, _TAff, moderator, visitor) -> true;
×
3041
can_change_role(owner, _FRole, _TAff, moderator, participant) -> true;
44✔
3042
can_change_role(admin, _FRole, _TAff, moderator, participant) -> true;
×
3043
can_change_role(_FAff, _FRole, _TAff, _TRole, _NewRole) -> false.
33✔
3044

3045
safe_send_kickban_presence(JID, Reason, Code, StateData) ->
3046
    try
74✔
3047
        send_kickban_presence(JID, Reason, Code, StateData)
74✔
3048
    catch
3049
        Class:ErrorReason:Stacktrace ->
3050
            ?LOG_ERROR(ls(#{what => muc_send_kickban_presence_failed,
×
3051
                            kick_jid => jid:to_binary(JID), kick_reason => Reason,
3052
                            class => Class, reason => ErrorReason, stacktrace => Stacktrace}, StateData))
×
3053
    end.
3054

3055
-spec send_kickban_presence(jid:jid(), binary(), Code :: binary(),
3056
                            state()) -> any().
3057
send_kickban_presence(JID, Reason, Code, StateData) ->
3058
    NewAffiliation = get_affiliation(JID, StateData),
74✔
3059
    send_kickban_presence(JID, Reason, Code, NewAffiliation, StateData).
74✔
3060

3061

3062
safe_send_kickban_presence(JID, Reason, Code, NewAffiliation, StateData) ->
3063
    try
140✔
3064
        send_kickban_presence(JID, Reason, Code, NewAffiliation, StateData)
140✔
3065
    catch
3066
        Class:ErrorReason:Stacktrace ->
3067
            ?LOG_ERROR(ls(#{what => muc_send_kickban_presence_failed,
×
3068
                            new_affiliation => NewAffiliation,
3069
                            kick_jid => jid:to_binary(JID), kick_reason => Reason,
3070
                            class => Class, reason => ErrorReason, stacktrace => Stacktrace}, StateData))
×
3071
    end.
3072

3073
-spec send_kickban_presence(jid:simple_jid() | jid:jid(),
3074
                            Reason :: binary(), Code :: binary(),
3075
                            mod_muc:affiliation(), state()) -> any().
3076
send_kickban_presence(JID, Reason, Code, NewAffiliation, StateData) ->
3077
    foreach_matched_user(fun(#user{nick = Nick, jid = J}) ->
214✔
3078
      add_to_log(kickban, {Nick, Reason, Code}, StateData),
129✔
3079
      tab_remove_online_user(J, StateData),
129✔
3080
      send_kickban_presence1(J, Reason, Code, NewAffiliation, StateData)
129✔
3081
    end, JID, StateData).
3082

3083

3084
-spec send_kickban_presence1(jid:jid(), Reason :: binary(), Code :: binary(),
3085
                             mod_muc:affiliation(), state()) -> 'ok'.
3086
send_kickban_presence1(UJID, Reason, Code, Affiliation, StateData) ->
3087
    {ok, #user{jid = RealJID,
129✔
3088
           nick = Nick}} =
3089
    maps:find(jid:to_lower(UJID), StateData#state.users),
3090
    BAffiliation = affiliation_to_binary(Affiliation),
129✔
3091
    BannedJIDString = jid:to_binary(RealJID),
129✔
3092
    F = fun(Info) ->
129✔
3093
          JidAttrList =
247✔
3094
            case (Info#user.role == moderator) orelse
74✔
3095
                 ((StateData#state.config)#config.anonymous == false) of
173✔
3096
                true -> #{<<"jid">> => BannedJIDString};
74✔
3097
                false -> #{}
173✔
3098
                end,
3099
          ItemAttrs = JidAttrList#{<<"affiliation">> => BAffiliation,
247✔
3100
                                   <<"role">> => <<"none">>},
3101
          ItemEls = case Reason of
247✔
3102
                <<>> ->
3103
                [];
88✔
3104
                _ ->
3105
                [#xmlel{name = <<"reason">>, children = [#xmlcdata{content = Reason}]}]
159✔
3106
                    end,
3107
          Packet = #xmlel{name = <<"presence">>,
247✔
3108
                          attrs = #{<<"type">> => <<"unavailable">>},
3109
                          children = [#xmlel{name = <<"x">>,
3110
                                             attrs = #{<<"xmlns">> => ?NS_MUC_USER},
3111
                                             children = [#xmlel{name = <<"item">>,
3112
                                                                attrs = ItemAttrs,
3113
                                                                children = ItemEls},
3114
                                                         #xmlel{name = <<"status">>,
3115
                                                                attrs = #{<<"code">> => Code}}]}]},
3116
          ejabberd_router:route(
247✔
3117
        jid:replace_resource(StateData#state.jid, Nick),
3118
        Info#user.jid,
3119
        Packet)
3120
      end,
3121
    foreach_user(F, StateData).
129✔
3122

3123

3124

3125
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3126
% Owner stuff
3127

3128
-spec process_iq_owner(jid:jid(), get | set, ejabberd:lang(), exml:element(),
3129
                       state(), statename()) ->
3130
    {error, exml:element()} | {result, [exml:child()], state() | stop}.
3131
process_iq_owner(From, Type, Lang, SubEl, StateData, StateName) ->
3132
    case get_affiliation(From, StateData) of
758✔
3133
        owner ->
3134
            process_authorized_iq_owner(From, Type, Lang, SubEl, StateData, StateName);
736✔
3135
        _ ->
3136
            ErrText = <<"Owner privileges required">>,
22✔
3137
            {error, mongoose_xmpp_errors:forbidden(Lang, ErrText)}
22✔
3138
    end.
3139

3140
-spec process_authorized_iq_owner(jid:jid(), get | set, ejabberd:lang(), exml:element(),
3141
                                  state(), statename()) ->
3142
    {error, exml:element()} | {result, [exml:child()], state() | stop}.
3143
process_authorized_iq_owner(From, set, Lang, SubEl, StateData, StateName) ->
3144
    #xmlel{children = Els} = SubEl,
615✔
3145
    case jlib:remove_cdata(Els) of
615✔
3146
        [#xmlel{name = <<"destroy">>} = SubEl1] ->
3147
            ?LOG_INFO(ls(#{what => muc_room_destroy,
55✔
3148
                           text => <<"Destroyed MUC room by the owner">>,
3149
                           from_jid => jid:to_binary(From)}, StateData)),
55✔
3150
            add_to_log(room_existence, destroyed, StateData),
55✔
3151
            destroy_room(SubEl1, StateData);
55✔
3152
        [XEl] ->
3153
            case {mongoose_data_forms:parse_form(XEl), StateName} of
549✔
3154
                {#{type := <<"cancel">>}, locked_state} ->
3155
                    ?LOG_INFO(ls(#{what => muc_cancel_locked,
22✔
3156
                                   text => <<"Received cancel before the room was configured "
3157
                                             "- destroy room">>,
3158
                                   from_jid => jid:to_binary(From)}, StateData)),
22✔
3159
                    add_to_log(room_existence, destroyed, StateData),
22✔
3160
                    destroy_room(XEl, StateData);
22✔
3161
                {#{type := <<"cancel">>}, normal_state} ->
3162
                    %% received cancel when room was configured - continue without changes
3163
                    {result, [], StateData};
22✔
3164
                {#{type := <<"submit">>, kvs := KVs}, _} ->
3165
                    process_authorized_submit_owner(From, maps:to_list(KVs), StateData);
483✔
3166
                {{error, Msg}, _} ->
3167
                    {error, mongoose_xmpp_errors:bad_request(Lang, Msg)};
11✔
3168
                _ ->
3169
                    {error, mongoose_xmpp_errors:bad_request(Lang, <<"Invalid form type">>)}
11✔
3170
            end;
3171
        _ ->
3172
            {error, mongoose_xmpp_errors:bad_request()}
11✔
3173
    end;
3174
process_authorized_iq_owner(From, get, Lang, SubEl, StateData, _StateName) ->
3175
    case exml_query:path(SubEl, [{element, <<"item">>}, {attr, <<"affiliation">>}]) of
121✔
3176
        undefined ->
3177
            get_config(Lang, StateData, From);
121✔
3178
        BAffiliation ->
3179
            case catch binary_to_affiliation(BAffiliation) of
×
3180
                {'EXIT', _} ->
3181
                    InvAffT = service_translations:do(Lang, <<"Invalid affiliation ">>),
×
3182
                    ErrText = <<InvAffT/binary, BAffiliation/binary>>,
×
3183
                    {error, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)};
×
3184
                Affiliation ->
3185
                    Items = items_with_affiliation(Affiliation, StateData),
×
3186
                    {result, Items, StateData}
×
3187
            end
3188
    end.
3189

3190
-spec process_authorized_submit_owner(From ::jid:jid(), [{binary(), [binary()]}],
3191
                                      StateData :: state()) ->
3192
    {error, exml:element()} | {result, [exml:child()], state() | stop}.
3193
process_authorized_submit_owner(_From, [], StateData) ->
3194
    %confirm an instant room
3195
    save_persistent_room_state(StateData),
225✔
3196
    {result, [], StateData};
225✔
3197
process_authorized_submit_owner(From, XData, StateData) ->
3198
    %attempt to configure
3199
    case is_allowed_log_change(XData, StateData, From)
258✔
3200
         andalso is_allowed_persistent_change(XData, StateData, From)
258✔
3201
         andalso is_allowed_room_name_desc_limits(XData, StateData)
258✔
3202
         andalso is_password_settings_correct(XData, StateData) of
258✔
3203
        true -> set_config(XData, StateData);
247✔
3204
        false -> {error, mongoose_xmpp_errors:not_acceptable(<<"en">>, <<"not allowed to configure">>)}
11✔
3205
    end.
3206

3207
-spec is_allowed_log_change([{binary(), [binary()]}], state(), jid:jid()) -> boolean().
3208
is_allowed_log_change(XData, StateData, From) ->
3209
    case lists:keymember(<<"muc#roomconfig_enablelogging">>, 1, XData) of
258✔
3210
    false ->
3211
        true;
236✔
3212
    true ->
3213
        (allow == mod_muc_log:check_access_log(
22✔
3214
                    StateData#state.host_type,
3215
                    StateData#state.server_host, From))
3216
    end.
3217

3218

3219
-spec is_allowed_persistent_change([{binary(), [binary()]}], state(), jid:jid()) -> boolean().
3220
is_allowed_persistent_change(XData, StateData, From) ->
3221
    case lists:keymember(<<"muc#roomconfig_persistentroom">>, 1, XData) of
258✔
3222
    false ->
3223
        true;
151✔
3224
    true ->
3225
        AccessPersistent = access_persistent(StateData),
107✔
3226
        (allow == acl:match_rule(StateData#state.host_type, StateData#state.server_host,
107✔
3227
                                 AccessPersistent, From))
3228
    end.
3229

3230

3231
%% @doc Check if the Room Name and Room Description defined in the Data Form
3232
%% are conformant to the configured limits
3233
-spec is_allowed_room_name_desc_limits([{binary(), [binary()]}], state()) -> boolean().
3234
is_allowed_room_name_desc_limits(XData, StateData) ->
3235
    IsNameAccepted =
258✔
3236
    case lists:keysearch(<<"muc#roomconfig_roomname">>, 1, XData) of
3237
        {value, {_, [N]}} ->
3238
        byte_size(N) =< get_opt(StateData, max_room_name);
×
3239
        _ ->
3240
        true
258✔
3241
    end,
3242
    IsDescAccepted =
258✔
3243
    case lists:keysearch(<<"muc#roomconfig_roomdesc">>, 1, XData) of
3244
        {value, {_, [D]}} ->
3245
        byte_size(D) =< get_opt(StateData, max_room_desc);
×
3246
        _ ->
3247
        true
258✔
3248
    end,
3249
    IsNameAccepted and IsDescAccepted.
258✔
3250

3251
%% @doc Return false if:
3252
%% `<<"the password for a password-protected room is blank">>'
3253
-spec is_password_settings_correct([{binary(), [binary()]}], state()) -> boolean().
3254
is_password_settings_correct(KVs, StateData) ->
3255
    Config = StateData#state.config,
258✔
3256
    OldProtected = Config#config.password_protected,
258✔
3257
    OldPassword = Config#config.password,
258✔
3258
    NewProtected =
258✔
3259
        case lists:keysearch(<<"muc#roomconfig_passwordprotectedroom">>, 1, KVs) of
3260
            {value, {_, [<<"1">>]}} ->
3261
                true;
11✔
3262
            {value, {_, [<<"0">>]}} ->
3263
                false;
×
3264
            _ ->
3265
                undefined
247✔
3266
        end,
3267
    NewPassword =
258✔
3268
        case lists:keysearch(<<"muc#roomconfig_roomsecret">>, 1, KVs) of
3269
            {value, {_, [P]}} ->
3270
                P;
11✔
3271
            _ ->
3272
                undefined
247✔
3273
        end,
3274
    case {OldProtected, NewProtected, OldPassword, NewPassword} of
258✔
3275
        {true, undefined, <<>>, undefined} ->
3276
            false;
×
3277
        {true, undefined, _, <<>>} ->
3278
            false;
×
3279
        {_, true, <<>>, undefined} ->
3280
            false;
×
3281
        {_, true, _, <<>>} ->
3282
            false;
11✔
3283
        _ ->
3284
            true
247✔
3285
    end.
3286

3287
-spec get_default_room_maxusers(state()) -> any().
3288
get_default_room_maxusers(RoomState) ->
3289
    #{max_users := MaxUsers} = get_opt(RoomState, default_room),
121✔
3290
    MaxUsers.
121✔
3291

3292
-spec get_config(ejabberd:lang(), state(), jid:jid())
3293
            -> {'result', [exml:element(), ...], state()}.
3294
get_config(Lang, StateData, From) ->
3295
    AccessPersistent = access_persistent(StateData),
121✔
3296
    Config = StateData#state.config,
121✔
3297
    TitleTxt = service_translations:do(Lang, <<"Configuration of room ">>),
121✔
3298
    Title = <<TitleTxt/binary, (jid:to_binary(StateData#state.jid))/binary>>,
121✔
3299
    Fields =
121✔
3300
    [stringxfield(<<"Room title">>,
3301
               <<"muc#roomconfig_roomname">>,
3302
                Config#config.title, Lang),
3303
     stringxfield(<<"Room description">>,
3304
               <<"muc#roomconfig_roomdesc">>,
3305
                Config#config.description, Lang)
3306
    ] ++
3307
     case acl:match_rule(StateData#state.host_type, StateData#state.server_host,
3308
                         AccessPersistent, From) of
3309
        allow ->
3310
            [boolxfield(<<"Make room persistent">>,
121✔
3311
             <<"muc#roomconfig_persistentroom">>,
3312
              Config#config.persistent, Lang)];
3313
        _ -> []
×
3314
     end ++ [
3315
     boolxfield(<<"Make room public searchable">>,
3316
             <<"muc#roomconfig_publicroom">>,
3317
              Config#config.public, Lang),
3318
     boolxfield(<<"Make participants list public">>,
3319
             <<"public_list">>,
3320
              Config#config.public_list, Lang),
3321
     boolxfield(<<"Make room password protected">>,
3322
             <<"muc#roomconfig_passwordprotectedroom">>,
3323
              Config#config.password_protected, Lang),
3324
     privatexfield(<<"Password">>,
3325
            <<"muc#roomconfig_roomsecret">>,
3326
            case Config#config.password_protected of
3327
                true -> Config#config.password;
×
3328
                false -> <<>>
121✔
3329
            end, Lang),
3330
     getmemberlist_field(Lang),
3331
     maxusers_field(Lang, StateData),
3332
     whois_field(Lang, Config),
3333
     boolxfield(<<"Make room members-only">>,
3334
             <<"muc#roomconfig_membersonly">>,
3335
              Config#config.members_only, Lang),
3336
     boolxfield(<<"Make room moderated">>,
3337
             <<"muc#roomconfig_moderatedroom">>,
3338
              Config#config.moderated, Lang),
3339
     boolxfield(<<"Default users as participants">>,
3340
             <<"members_by_default">>,
3341
              Config#config.members_by_default, Lang),
3342
     boolxfield(<<"Allow users to change the subject">>,
3343
             <<"muc#roomconfig_changesubject">>,
3344
              Config#config.allow_change_subj, Lang),
3345
     boolxfield(<<"Allow users to send private messages">>,
3346
             <<"allow_private_messages">>,
3347
              Config#config.allow_private_messages, Lang),
3348
     boolxfield(<<"Allow users to query other users">>,
3349
             <<"allow_query_users">>,
3350
              Config#config.allow_query_users, Lang),
3351
     boolxfield(<<"Allow users to send invites">>,
3352
             <<"muc#roomconfig_allowinvites">>,
3353
              Config#config.allow_user_invites, Lang),
3354
     boolxfield(<<"Allow users to enter room with multiple sessions">>,
3355
             <<"muc#roomconfig_allowmultisessions">>,
3356
              Config#config.allow_multiple_sessions, Lang),
3357
     boolxfield(<<"Allow visitors to send status text in presence updates">>,
3358
             <<"muc#roomconfig_allowvisitorstatus">>,
3359
              Config#config.allow_visitor_status, Lang),
3360
     boolxfield(<<"Allow visitors to change nickname">>,
3361
             <<"muc#roomconfig_allowvisitornickchange">>,
3362
              Config#config.allow_visitor_nickchange, Lang)
3363
    ] ++
3364
     case mod_muc_log:check_access_log(StateData#state.host_type,
3365
                                       StateData#state.server_host, From) of
3366
         allow ->
3367
             [boolxfield(
121✔
3368
                <<"Enable logging">>,
3369
                <<"muc#roomconfig_enablelogging">>,
3370
                Config#config.logging, Lang)];
3371
         _ -> []
×
3372
     end,
3373
    InstructionsTxt = service_translations:do(
121✔
3374
                        Lang, <<"You need an x:data capable client to configure room">>),
3375
    {result, [#xmlel{name = <<"instructions">>, children = [#xmlcdata{content = InstructionsTxt}]},
121✔
3376
              mongoose_data_forms:form(#{title => Title, ns => ?NS_MUC_CONFIG, fields => Fields})],
3377
     StateData}.
3378

3379
-spec getmemberlist_field(Lang :: ejabberd:lang()) -> mongoose_data_forms:field().
3380
getmemberlist_field(Lang) ->
3381
    LabelTxt = service_translations:do(
121✔
3382
                 Lang, <<"Roles and affiliations that may retrieve member list">>),
3383
    Values = [<<"moderator">>, <<"participant">>, <<"visitor">>],
121✔
3384
    Options = [{service_translations:do(Lang, Opt), Opt} || Opt <- Values],
121✔
3385
    #{type => <<"list-multi">>, label => LabelTxt,
121✔
3386
      var => <<"muc#roomconfig_getmemberlist">>, values => Values, options => Options}.
3387

3388
maxusers_field(Lang, StateData) ->
3389
    ServiceMaxUsers = get_service_max_users(StateData),
121✔
3390
    DefaultRoomMaxUsers = get_default_room_maxusers(StateData),
121✔
3391
    {MaxUsersRoomInteger, MaxUsersRoomString} =
121✔
3392
    case get_max_users(StateData) of
3393
        N when is_integer(N) ->
3394
            {N, integer_to_binary(N)};
121✔
3395
        _ -> {0, <<"none">>}
×
3396
    end,
3397
    LabelTxt = service_translations:do(Lang, <<"Maximum Number of Occupants">>),
121✔
3398
    Options = if
121✔
3399
                  is_integer(ServiceMaxUsers) -> [];
121✔
3400
                  true -> {service_translations:do(Lang, <<"No limit">>), <<"none">>}
×
3401
              end ++
3402
        [integer_to_binary(N) ||
847✔
3403
            N <- lists:usort([ServiceMaxUsers, DefaultRoomMaxUsers, MaxUsersRoomInteger |
121✔
3404
                              ?MAX_USERS_DEFAULT_LIST]), N =< ServiceMaxUsers],
1,331✔
3405
    #{type => <<"list-single">>, label => LabelTxt,
121✔
3406
      var => <<"muc#roomconfig_maxusers">>, values => [MaxUsersRoomString], options => Options}.
3407

3408
-spec whois_field(Lang :: ejabberd:lang(), Config :: config()) -> mongoose_data_forms:field().
3409
whois_field(Lang, Config) ->
3410
    Value = if Config#config.anonymous -> <<"moderators">>;
121✔
3411
               true -> <<"anyone">>
11✔
3412
            end,
3413
    Options = [{service_translations:do(Lang, <<"moderators only">>), <<"moderators">>},
121✔
3414
               {service_translations:do(Lang, <<"anyone">>), <<"anyone">>}],
3415
    #{type => <<"list-single">>, label => service_translations:do(Lang, <<"moderators only">>),
121✔
3416
      var => <<"muc#roomconfig_whois">>, values => [Value], options => Options}.
3417

3418
-spec set_config([{binary(), [binary()]}], state()) -> any().
3419
set_config(XData, StateData) ->
3420
    case set_xoption(XData, StateData#state.config) of
247✔
3421
        #config{} = Config ->
3422
            Res = change_config(Config, StateData),
247✔
3423
            {result, _, NSD} = Res,
247✔
3424
            PrevLogging = (StateData#state.config)#config.logging,
247✔
3425
            NewLogging = Config#config.logging,
247✔
3426
            PrevAnon = (StateData#state.config)#config.anonymous,
247✔
3427
            NewAnon = Config#config.anonymous,
247✔
3428
            Type = notify_config_change_and_get_type(PrevLogging, NewLogging,
247✔
3429
                                                     PrevAnon, NewAnon, StateData),
3430
                    Users = [{U#user.jid, U#user.nick, U#user.role} ||
247✔
3431
                                {_, U} <- maps:to_list(StateData#state.users)],
247✔
3432
            add_to_log(Type, Users, NSD),
247✔
3433
            Res;
247✔
3434
                Err ->
3435
            Err
×
3436
    end.
3437

3438
-spec notify_config_change_and_get_type(PrevLogging :: boolean(), NewLogging :: boolean(),
3439
                                        PrevAnon :: boolean(), NewAnon :: boolean(),
3440
                                        StateData :: state()) ->
3441
    roomconfig_change_disabledlogging | roomconfig_change_enabledlogging
3442
    | roomconfig_change_nonanonymous | roomconfig_change_anonymous | roomconfig_change.
3443
notify_config_change_and_get_type(true, false, _, _, StateData) ->
3444
    send_config_update(logging_disabled, StateData),
11✔
3445
    roomconfig_change_disabledlogging;
11✔
3446
notify_config_change_and_get_type(false, true, _, _, StateData) ->
3447
    send_config_update(logging_enabled, StateData),
11✔
3448
    roomconfig_change_enabledlogging;
11✔
3449
notify_config_change_and_get_type(_, _, true, false, StateData) ->
3450
    send_config_update(nonanonymous, StateData),
11✔
3451
    roomconfig_change_nonanonymous;
11✔
3452
notify_config_change_and_get_type(_, _, false, true, StateData) ->
3453
    send_config_update(semianonymous, StateData),
11✔
3454
    roomconfig_change_anonymous;
11✔
3455
notify_config_change_and_get_type(_, _, _, _, _StateData) ->
3456
    roomconfig_change.
203✔
3457

3458
-define(SET_BOOL_XOPT(Opt, Val),
3459
    case Val of
3460
        <<"0">> -> set_xoption(Opts, Config#config{Opt = false});
3461
        <<"false">> -> set_xoption(Opts, Config#config{Opt = false});
3462
        <<"1">> -> set_xoption(Opts, Config#config{Opt = true});
3463
        <<"true">> -> set_xoption(Opts, Config#config{Opt = true});
3464
        _ -> {error, mongoose_xmpp_errors:bad_request()}
3465
    end).
3466

3467
-define(SET_NAT_XOPT(Opt, Val),
3468
    case catch binary_to_integer(Val) of
3469
        I when is_integer(I),
3470
               I > 0 ->
3471
        set_xoption(Opts, Config#config{Opt = I});
3472
        _ ->
3473
        {error, mongoose_xmpp_errors:bad_request()}
3474
    end).
3475

3476
-define(SET_XOPT(Opt, Val),
3477
    set_xoption(Opts, Config#config{Opt = Val})).
3478

3479
-spec set_xoption([{binary(), [binary()]}], config()) -> config() | {error, exml:element()}.
3480
set_xoption([], Config) ->
3481
    Config;
247✔
3482
set_xoption([{<<"muc#roomconfig_roomname">>, [Val]} | Opts], Config) ->
3483
    ?SET_XOPT(title, Val);
×
3484
set_xoption([{<<"muc#roomconfig_roomdesc">>, [Val]} | Opts], Config) ->
3485
    ?SET_XOPT(description, Val);
×
3486
set_xoption([{<<"muc#roomconfig_changesubject">>, [Val]} | Opts], Config) ->
3487
    ?SET_BOOL_XOPT(allow_change_subj, Val);
×
3488
set_xoption([{<<"allow_query_users">>, [Val]} | Opts], Config) ->
3489
    ?SET_BOOL_XOPT(allow_query_users, Val);
×
3490
set_xoption([{<<"allow_private_messages">>, [Val]} | Opts], Config) ->
3491
    ?SET_BOOL_XOPT(allow_private_messages, Val);
×
3492
set_xoption([{<<"muc#roomconfig_allowvisitorstatus">>, [Val]} | Opts], Config) ->
3493
    ?SET_BOOL_XOPT(allow_visitor_status, Val);
×
3494
set_xoption([{<<"muc#roomconfig_allowvisitornickchange">>, [Val]} | Opts], Config) ->
3495
    ?SET_BOOL_XOPT(allow_visitor_nickchange, Val);
×
3496
set_xoption([{<<"muc#roomconfig_publicroom">>, [Val]} | Opts], Config) ->
3497
    ?SET_BOOL_XOPT(public, Val);
22✔
3498
set_xoption([{<<"public_list">>, [Val]} | Opts], Config) ->
3499
    ?SET_BOOL_XOPT(public_list, Val);
×
3500
set_xoption([{<<"muc#roomconfig_persistentroom">>, [Val]} | Opts], Config) ->
3501
    ?SET_BOOL_XOPT(persistent, Val);
107✔
3502
set_xoption([{<<"muc#roomconfig_moderatedroom">>, [Val]} | Opts], Config) ->
3503
    ?SET_BOOL_XOPT(moderated, Val);
22✔
3504
set_xoption([{<<"members_by_default">>, [Val]} | Opts], Config) ->
3505
    ?SET_BOOL_XOPT(members_by_default, Val);
×
3506
set_xoption([{<<"muc#roomconfig_membersonly">>, [Val]} | Opts], Config) ->
3507
    ?SET_BOOL_XOPT(members_only, Val);
74✔
3508
set_xoption([{<<"muc#roomconfig_allowinvites">>, [Val]} | Opts], Config) ->
3509
    ?SET_BOOL_XOPT(allow_user_invites, Val);
×
3510
set_xoption([{<<"muc#roomconfig_allowmultisessions">>, [Val]} | Opts], Config) ->
3511
    ?SET_BOOL_XOPT(allow_multiple_sessions, Val);
×
3512
set_xoption([{<<"muc#roomconfig_passwordprotectedroom">>, [Val]} | Opts], Config) ->
3513
    ?SET_BOOL_XOPT(password_protected, Val);
×
3514
set_xoption([{<<"muc#roomconfig_roomsecret">>, [Val]} | Opts], Config) ->
3515
    ?SET_XOPT(password, Val);
×
3516
set_xoption([{<<"anonymous">>, [Val]} | Opts], Config) ->
3517
    ?SET_BOOL_XOPT(anonymous, Val);
×
3518
set_xoption([{<<"muc#roomconfig_whois">>, [Val]} | Opts], Config) ->
3519
    case Val of
22✔
3520
    <<"moderators">> ->
3521
        ?SET_XOPT(anonymous, true);
11✔
3522
    <<"anyone">> ->
3523
        ?SET_XOPT(anonymous, false);
11✔
3524
    _ ->
3525
        {error, mongoose_xmpp_errors:bad_request()}
×
3526
    end;
3527
set_xoption([{<<"muc#roomconfig_maxusers">>, [Val]} | Opts], Config) ->
3528
    case Val of
×
3529
    <<"none">> ->
3530
        ?SET_XOPT(max_users, none);
×
3531
    _ ->
3532
        ?SET_NAT_XOPT(max_users, Val)
×
3533
    end;
3534
set_xoption([{<<"muc#roomconfig_getmemberlist">>, Val} | Opts], Config) ->
3535
    case Val of
33✔
3536
        [<<"none">>] ->
3537
            ?SET_XOPT(maygetmemberlist, []);
×
3538
        _ ->
3539
            ?SET_XOPT(maygetmemberlist, [binary_to_role(V) || V <- Val])
33✔
3540
    end;
3541
set_xoption([{<<"muc#roomconfig_enablelogging">>, [Val]} | Opts], Config) ->
3542
    ?SET_BOOL_XOPT(logging, Val);
22✔
3543
set_xoption([_ | _Opts], _Config) ->
3544
    {error, mongoose_xmpp_errors:bad_request()}.
×
3545

3546

3547
-spec change_config(config(), state()) -> {'result', [], state()}.
3548
change_config(Config, StateData) ->
3549
    NSD = StateData#state{config = Config},
288✔
3550
    case {(StateData#state.config)#config.persistent,
288✔
3551
      Config#config.persistent} of
3552
    {_, true} ->
3553
        mod_muc:store_room(NSD#state.host_type, NSD#state.host, NSD#state.room, make_opts(NSD));
184✔
3554
    {true, false} ->
3555
        mod_muc:forget_room(NSD#state.host_type, NSD#state.host, NSD#state.room);
×
3556
    {false, false} ->
3557
        ok
104✔
3558
    end,
3559
    case {(StateData#state.config)#config.members_only,
288✔
3560
          Config#config.members_only} of
3561
    {false, true} ->
3562
        NSD1 = remove_nonmembers(NSD),
11✔
3563
        {result, [], NSD1};
11✔
3564
    _ ->
3565
        {result, [], NSD}
277✔
3566
    end.
3567

3568

3569
-spec remove_nonmembers(state()) -> state().
3570
remove_nonmembers(StateData) ->
3571
    F = fun(_LJID, #user{jid = JID}, SD) ->
11✔
3572
        Affiliation = get_affiliation(JID, SD),
11✔
3573
        case Affiliation of
11✔
3574
        none ->
3575
            safe_send_kickban_presence(JID, <<>>, <<"322">>, SD),
×
3576
            set_role(JID, none, SD);
×
3577
        _ ->
3578
            SD
11✔
3579
        end
3580
      end,
3581
    maps:fold(F, StateData, StateData#state.users).
11✔
3582

3583
-spec set_opts(Opts :: [{atom(), term()}], state()) -> state().
3584
set_opts([], SD) ->
3585
    SD;
7,120✔
3586
set_opts([{Opt, Val} | Opts], SD=#state{config = C = #config{}}) ->
3587
    NSD = case Opt of
31,758✔
3588
        title ->
3589
            SD#state{config = C#config{title = Val}};
737✔
3590
        description ->
3591
            SD#state{config = C#config{description = Val}};
737✔
3592
        allow_change_subj ->
3593
            SD#state{config = C#config{allow_change_subj = Val}};
891✔
3594
        allow_query_users ->
3595
            SD#state{config = C#config{allow_query_users = Val}};
737✔
3596
        allow_private_messages ->
3597
            SD#state{config = C#config{allow_private_messages = Val}};
737✔
3598
        allow_visitor_nickchange ->
3599
            SD#state{config = C#config{allow_visitor_nickchange = Val}};
737✔
3600
        allow_visitor_status ->
3601
            SD#state{config = C#config{allow_visitor_status = Val}};
737✔
3602
        public ->
3603
            SD#state{config = C#config{public = Val}};
737✔
3604
        public_list ->
3605
            SD#state{config = C#config{public_list = Val}};
778✔
3606
        persistent ->
3607
            SD#state{config = C#config{persistent = Val}};
4,301✔
3608
        moderated ->
3609
            SD#state{config = C#config{moderated = Val}};
891✔
3610
        members_by_default ->
3611
            SD#state{config = C#config{members_by_default = Val}};
891✔
3612
        members_only ->
3613
            SD#state{config = C#config{members_only = Val}};
822✔
3614
        allow_user_invites ->
3615
            SD#state{config = C#config{allow_user_invites = Val}};
737✔
3616
        allow_multiple_sessions ->
3617
            SD#state{config = C#config{allow_multiple_sessions = Val}};
792✔
3618
        password_protected ->
3619
            SD#state{config = C#config{password_protected = Val}};
1,110✔
3620
        password ->
3621
            SD#state{config = C#config{password = Val}};
1,055✔
3622
        anonymous ->
3623
            SD#state{config = C#config{anonymous = Val}};
3,340✔
3624
        logging ->
3625
            SD#state{config = C#config{logging = Val}};
748✔
3626
        max_users ->
3627
            MaxUsers = min(Val, get_service_max_users(SD)),
748✔
3628
            SD#state{config = C#config{max_users = MaxUsers}};
748✔
3629
        maygetmemberlist ->
3630
            SD#state{config = C#config{maygetmemberlist = Val}};
737✔
3631
        affiliations ->
3632
            SD#state{affiliations = maps:from_list(Val)};
745✔
3633
        subject ->
3634
            SD#state{subject = Val};
748✔
3635
        subject_author ->
3636
            SD#state{subject_author = Val};
737✔
3637
        subject_timestamp ->
3638
            SD#state{subject_timestamp = Val};
694✔
3639
        _ ->
3640
            SD
5,834✔
3641
       end,
3642
    set_opts(Opts, NSD).
31,758✔
3643

3644

3645
-define(MAKE_CONFIG_OPT(Opt), {Opt, Config#config.Opt}).
3646

3647
-spec make_opts(state()) -> [{atom(), _}, ...].
3648
make_opts(StateData) ->
3649
    Config = StateData#state.config,
4,441✔
3650
    [
4,441✔
3651
     ?MAKE_CONFIG_OPT(title),
3652
     ?MAKE_CONFIG_OPT(description),
3653
     ?MAKE_CONFIG_OPT(allow_change_subj),
3654
     ?MAKE_CONFIG_OPT(allow_query_users),
3655
     ?MAKE_CONFIG_OPT(allow_private_messages),
3656
     ?MAKE_CONFIG_OPT(allow_visitor_status),
3657
     ?MAKE_CONFIG_OPT(allow_visitor_nickchange),
3658
     ?MAKE_CONFIG_OPT(public),
3659
     ?MAKE_CONFIG_OPT(public_list),
3660
     ?MAKE_CONFIG_OPT(persistent),
3661
     ?MAKE_CONFIG_OPT(moderated),
3662
     ?MAKE_CONFIG_OPT(members_by_default),
3663
     ?MAKE_CONFIG_OPT(members_only),
3664
     ?MAKE_CONFIG_OPT(allow_user_invites),
3665
     ?MAKE_CONFIG_OPT(allow_multiple_sessions),
3666
     ?MAKE_CONFIG_OPT(password_protected),
3667
     ?MAKE_CONFIG_OPT(password),
3668
     ?MAKE_CONFIG_OPT(anonymous),
3669
     ?MAKE_CONFIG_OPT(logging),
3670
     ?MAKE_CONFIG_OPT(max_users),
3671
     ?MAKE_CONFIG_OPT(maygetmemberlist),
3672
     {affiliations, maps:to_list(StateData#state.affiliations)},
3673
     {subject, StateData#state.subject},
3674
     {subject_author, StateData#state.subject_author}
3675
    ].
3676

3677
-spec destroy_room(exml:element(), state()) -> {result, [], stop}.
3678
destroy_room(DestroyEl, StateData) ->
3679
    remove_each_occupant_from_room(DestroyEl, StateData),
3,182✔
3680
    case (StateData#state.config)#config.persistent of
3,182✔
3681
        true ->
3682
            mod_muc:forget_room(StateData#state.host_type,
2,542✔
3683
                                StateData#state.host,
3684
                                StateData#state.room);
3685
        false ->
3686
            ok
640✔
3687
    end,
3688
    {result, [], stop}.
3,182✔
3689

3690

3691
%% @doc Service Removes Each Occupant
3692
%%
3693
%% Send only one presence stanza of type "unavailable" to each occupant
3694
%% so that the user knows he or she has been removed from the room.
3695
%%
3696
%% If extended presence information specifying the JID of an alternate
3697
%% location and the reason for the room destruction was provided by the
3698
%% room owner, the presence stanza MUST include that information.
3699
%% @end
3700
-spec remove_each_occupant_from_room(exml:element(), state()) -> any().
3701
remove_each_occupant_from_room(DestroyEl, StateData) ->
3702
    Packet = presence_stanza_of_type_unavailable(DestroyEl),
3,182✔
3703
    send_to_occupants(Packet, StateData).
3,182✔
3704

3705

3706
-spec send_to_occupants(exml:element(), state()) -> any().
3707
send_to_occupants(Packet, StateData=#state{jid=RoomJID}) ->
3708
    F = fun(User=#user{jid=UserJID}) ->
3,182✔
3709
        ejabberd_router:route(occupant_jid(User, RoomJID), UserJID, Packet)
329✔
3710
        end,
3711
    foreach_user(F, StateData).
3,182✔
3712

3713
-spec send_to_all_users(exml:element(), state()) -> any().
3714
send_to_all_users(Packet, StateData=#state{jid=RoomJID}) ->
3715
    F = fun(#user{jid = UserJID}) ->
44✔
3716
          ejabberd_router:route(RoomJID, UserJID, Packet)
66✔
3717
      end,
3718
    foreach_user(F, StateData).
44✔
3719

3720

3721
-spec presence_stanza_of_type_unavailable(exml:element()) -> exml:element().
3722
presence_stanza_of_type_unavailable(DestroyEl) ->
3723
    ItemEl = #xmlel{
3,182✔
3724
        name = <<"item">>,
3725
        attrs = #{<<"affiliation">> => <<"none">>, <<"role">> => <<"none">>}},
3726
    XEl = #xmlel{
3,182✔
3727
        name = <<"x">>,
3728
        attrs = #{<<"xmlns">> => ?NS_MUC_USER},
3729
        children = [ItemEl, DestroyEl]},
3730
    #xmlel{
3,182✔
3731
        name = <<"presence">>,
3732
        attrs = #{<<"type">> => <<"unavailable">>},
3733
        children = [XEl]}.
3734

3735

3736
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3737
% Disco
3738

3739
-spec config_opt_to_feature(boolean(), Fiftrue :: binary(), Fiffalse :: binary()) -> binary().
3740
config_opt_to_feature(Opt, Fiftrue, Fiffalse) ->
3741
    case Opt of
312✔
3742
        true -> Fiftrue;
197✔
3743
        false -> Fiffalse
115✔
3744
    end.
3745

3746

3747
-spec process_iq_disco_info(jid:jid(), 'get' | 'set', ejabberd:lang(),
3748
                            state()) -> {'error', exml:element()}
3749
                                      | {'result', [exml:element()], state()}.
3750
process_iq_disco_info(_From, set, _Lang, _StateData) ->
3751
    {error, mongoose_xmpp_errors:not_allowed()};
×
3752
process_iq_disco_info(From, get, Lang, StateData) ->
3753
    RoomJID = StateData#state.jid,
52✔
3754
    Config = StateData#state.config,
52✔
3755
    HostType = StateData#state.host_type,
52✔
3756
    IdentityXML = mongoose_disco:identities_to_xml([identity(get_title(StateData))]),
52✔
3757
    FeatureXML =  mongoose_disco:get_muc_features(HostType, From, RoomJID, <<>>, Lang,
52✔
3758
                                                  room_features(Config)),
3759
    InfoXML = iq_disco_info_extras(Lang, StateData),
52✔
3760
    {result, IdentityXML ++ FeatureXML ++ InfoXML, StateData}.
52✔
3761

3762
identity(Name) ->
3763
    #{category => <<"conference">>,
52✔
3764
      type => <<"text">>,
3765
      name => Name}.
3766

3767
-spec room_features(config()) -> [mongoose_disco:feature()].
3768
room_features(Config) ->
3769
    [?NS_MUC,
52✔
3770
     ?NS_MUC_STABLE_ID,
3771
     config_opt_to_feature((Config#config.public),
3772
                           <<"muc_public">>, <<"muc_hidden">>),
3773
     config_opt_to_feature((Config#config.persistent),
3774
                           <<"muc_persistent">>, <<"muc_temporary">>),
3775
     config_opt_to_feature((Config#config.members_only),
3776
                           <<"muc_membersonly">>, <<"muc_open">>),
3777
     config_opt_to_feature((Config#config.anonymous),
3778
                           <<"muc_semianonymous">>, <<"muc_nonanonymous">>),
3779
     config_opt_to_feature((Config#config.moderated),
3780
                           <<"muc_moderated">>, <<"muc_unmoderated">>),
3781
     config_opt_to_feature((Config#config.password_protected),
3782
                           <<"muc_passwordprotected">>, <<"muc_unsecured">>)].
3783

3784
-spec iq_disco_info_extras(ejabberd:lang(), state()) -> [exml:element()].
3785
iq_disco_info_extras(Lang, StateData) ->
3786
    Len = integer_to_binary(maps:size(StateData#state.users)),
52✔
3787
    Description = (StateData#state.config)#config.description,
52✔
3788
    Fields = [info_field(<<"Room description">>, <<"muc#roominfo_description">>, Description, Lang),
52✔
3789
              info_field(<<"Number of occupants">>, <<"muc#roominfo_occupants">>, Len, Lang)],
3790
    Info = #{xmlns => <<"http://jabber.org/protocol/muc#roominfo">>, fields => Fields},
52✔
3791
    mongoose_disco:info_list_to_xml([Info]).
52✔
3792

3793
-spec info_field(binary(), binary(), binary(), ejabberd:lang()) -> mongoose_disco:info_field().
3794
info_field(Label, Var, Value, Lang) ->
3795
    #{label => service_translations:do(Lang, Label), var => Var, values => [Value]}.
104✔
3796

3797
-spec process_iq_disco_items(jid:jid(), 'get' | 'set', ejabberd:lang(),
3798
                            state()) -> {'error', exml:element()}
3799
                                      | {'result', [exml:element()], state()}.
3800
process_iq_disco_items(_From, set, _Lang, _StateData) ->
3801
    {error, mongoose_xmpp_errors:not_allowed()};
×
3802
process_iq_disco_items(From, get, _Lang, StateData) ->
3803
    case (StateData#state.config)#config.public_list of
22✔
3804
    true ->
3805
        {result, get_mucroom_disco_items(StateData), StateData};
11✔
3806
    _ ->
3807
        case is_occupant_or_admin(From, StateData) of
11✔
3808
        true ->
3809
            {result, get_mucroom_disco_items(StateData), StateData};
×
3810
        _ ->
3811
            {error, mongoose_xmpp_errors:forbidden()}
11✔
3812
        end
3813
    end.
3814

3815

3816
-spec get_title(state()) -> binary() | mod_muc:room().
3817
get_title(StateData) ->
3818
    case (StateData#state.config)#config.title of
2,018✔
3819
    <<>> ->
3820
        StateData#state.room;
2,018✔
3821
    Name ->
3822
        Name
×
3823
    end.
3824

3825

3826
-spec get_roomdesc_reply(jid:jid(), state(), Tail :: binary()
3827
                        ) -> 'false' | {'item', _}.
3828
get_roomdesc_reply(JID, StateData, Tail) ->
3829
    IsOccupantOrAdmin = is_occupant_or_admin(JID, StateData),
1,975✔
3830
    case {(StateData#state.config)#config.public or IsOccupantOrAdmin,
1,975✔
3831
          (StateData#state.config)#config.public_list or IsOccupantOrAdmin} of
3832
        {true, true} ->
3833
            Title = get_title(StateData),
1,791✔
3834
            {item, <<Title/binary, Tail/binary>>};
1,791✔
3835
        {true, false} ->
3836
            {item, get_title(StateData)};
175✔
3837
        _ ->
3838
            false
9✔
3839
    end.
3840

3841

3842
-spec get_roomdesc_tail(state(), ejabberd:lang()) -> binary().
3843
get_roomdesc_tail(StateData, Lang) ->
3844
    Desc = case (StateData#state.config)#config.public of
1,975✔
3845
               true ->
3846
                   <<>>;
1,966✔
3847
               _ ->
3848
                   service_translations:do(Lang, <<"private, ">>)
9✔
3849
           end,
3850
    Count = count_users(StateData),
1,975✔
3851
    CountBin = integer_to_binary(Count),
1,975✔
3852
    <<" (", Desc/binary, CountBin/binary, ")">>.
1,975✔
3853

3854

3855
-spec get_mucroom_disco_items(state()) -> [exml:element()].
3856
get_mucroom_disco_items(StateData=#state{jid=RoomJID}) ->
3857
    maps:fold(fun(_LJID, User, Acc) ->
11✔
3858
                      Item = disco_item(User, RoomJID),
11✔
3859
                      [Item|Acc]
11✔
3860
              end, [], StateData#state.users).
3861

3862
-spec disco_item(user(), 'undefined' | jid:jid()) -> exml:element().
3863
disco_item(User=#user{nick=Nick}, RoomJID) ->
3864
    #xmlel{
11✔
3865
        name = <<"item">>,
3866
        attrs = #{<<"jid">> => jid:to_binary(occupant_jid(User, RoomJID)),
3867
                  <<"name">> => Nick}}.
3868

3869
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3870
%% Handle voice request or approval (XEP-0045 7.13, 8.6)
3871
-spec check_voice_approval(From :: jid:jid(), El :: exml:element(),
3872
        Lang :: ejabberd:lang(), StateData :: state()
3873
        ) -> {form, BRole :: binary()}
3874
           | {role, BRole :: binary(), RoomNick :: mod_muc:nick()}
3875
           | {error, any()}
3876
           | ok.
3877
check_voice_approval(From, XEl, Lang, StateData) ->
3878
    case mongoose_data_forms:find_and_parse_form(XEl) of
99✔
3879
        #{type := <<"submit">>, kvs := #{<<"muc#role">> := [BRole]} = KVs} ->
3880
            case {get_role(From, StateData) =:= moderator,
66✔
3881
                  maps:find(<<"muc#request_allow">>, KVs),
3882
                  maps:find(<<"muc#roomnick">>, KVs)} of
3883
                {_, error, error} ->
3884
                    case catch binary_to_role(BRole) of
22✔
3885
                        {'EXIT', _} -> {error, mongoose_xmpp_errors:bad_request()};
×
3886
                        _ -> {form, BRole}
22✔
3887
                    end;
3888
                {false, _, _} ->
3889
                    {error, mongoose_xmpp_errors:not_allowed()};
11✔
3890
                {true, {ok, [<<"true">>]}, error} ->
3891
                    {error, mongoose_xmpp_errors:bad_request()};
11✔
3892
                {true, {ok, [<<"true">>]}, {ok, [RoomNick]}} ->
3893
                    {role, BRole, RoomNick};
11✔
3894
                {true, _, _} ->
3895
                    ok
11✔
3896
            end;
3897
        {error, Msg} ->
3898
            {error, mongoose_xmpp_errors:bad_request(Lang, Msg)};
22✔
3899
        _ ->
3900
            {error, mongoose_xmpp_errors:bad_request(Lang, <<"Invalid form">>)}
11✔
3901
    end.
3902

3903
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3904
%% Invitation support
3905

3906
-spec check_invitation(jid:simple_jid() | jid:jid(),
3907
        [exml:child()], ejabberd:lang(), state())
3908
            -> {'error', _} | {'ok', [jid:jid()]}.
3909
check_invitation(FromJID, Els, Lang, StateData) ->
3910
    try
44✔
3911
        unsafe_check_invitation(FromJID, Els, Lang, StateData)
44✔
3912
    catch throw:{error, Reason} -> {error, Reason}
11✔
3913
    end.
3914

3915

3916
-spec unsafe_check_invitation(jid:jid(), [exml:child()],
3917
                              ejabberd:lang(), state()) -> {ok, [jid:jid()]}.
3918
unsafe_check_invitation(FromJID, Els, Lang,
3919
                        StateData=#state{host=Host, server_host=ServerHost, jid=RoomJID}) ->
3920
    FAffiliation = get_affiliation(FromJID, StateData),
44✔
3921
    CanInvite = (StateData#state.config)#config.allow_user_invites
44✔
3922
                orelse (FAffiliation == admin)
44✔
3923
                orelse (FAffiliation == owner),
44✔
3924
    case CanInvite of
44✔
3925
        false ->
3926
            throw({error, mongoose_xmpp_errors:forbidden()});
11✔
3927
        true ->
3928
            InviteEls = find_invite_elems(Els),
33✔
3929
            %% Decode all JIDs first, so we fail early if any JID is invalid.
3930
            JIDs = lists:map(fun decode_destination_jid/1, InviteEls),
33✔
3931
            lists:foreach(
33✔
3932
              fun(InviteEl) ->
3933
                      {JID, Reason, Msg} = create_invite(FromJID, InviteEl, Lang, StateData),
44✔
3934
                      mongoose_hooks:invitation_sent(Host, ServerHost, RoomJID,
44✔
3935
                                                     FromJID, JID, Reason),
3936
                      ejabberd_router:route(StateData#state.jid, JID, Msg)
44✔
3937
              end, InviteEls),
3938
            {ok, JIDs}
33✔
3939
    end.
3940

3941
-spec create_invite(FromJID ::jid:jid(), InviteEl :: exml:element(),
3942
                    Lang :: ejabberd:lang(), StateData :: state()) ->
3943
    {JID ::jid:jid(), Reason :: binary(), Msg :: exml:element()}.
3944
create_invite(FromJID, InviteEl, Lang, StateData) ->
3945
    JID = decode_destination_jid(InviteEl),
44✔
3946
    %% Create an invitation message and send it to the user.
3947
    Reason = decode_reason(InviteEl),
44✔
3948
    ContinueEl =
44✔
3949
    case exml_query:path(InviteEl, [{element, <<"continue">>}], <<>>) of
3950
        <<>> -> [];
44✔
3951
        Continue1 -> [Continue1]
×
3952
    end,
3953
    ReasonEl = #xmlel{
44✔
3954
                  name = <<"reason">>,
3955
                  children = [#xmlcdata{content = Reason}]},
3956
    OutInviteEl = #xmlel{
44✔
3957
                     name = <<"invite">>,
3958
                     attrs = #{<<"from">> => jid:to_binary(FromJID)},
3959
                     children = [ReasonEl] ++ ContinueEl},
3960
    PasswdEl = create_password_elem(StateData),
44✔
3961
    BodyEl = invite_body_elem(FromJID, Reason, Lang, StateData),
44✔
3962
    Msg = create_invite_message_elem(
44✔
3963
            OutInviteEl, BodyEl, PasswdEl, Reason),
3964
    {JID, Reason, Msg}.
44✔
3965

3966
-spec decode_destination_jid(exml:element()) -> jid:jid().
3967
decode_destination_jid(InviteEl) ->
3968
    case jid:from_binary(exml_query:attr(InviteEl, <<"to">>, <<>>)) of
88✔
3969
      error -> throw({error, mongoose_xmpp_errors:jid_malformed()});
×
3970
      JID   -> JID
88✔
3971
    end.
3972

3973

3974
-spec find_invite_elems([exml:child()]) -> [exml:element()].
3975
find_invite_elems(Els) ->
3976
    case jlib:remove_cdata(Els) of
33✔
3977
    [#xmlel{name = <<"x">>, children = Els1} = XEl] ->
3978
            case exml_query:attr(XEl, <<"xmlns">>, <<>>) of
33✔
3979
        ?NS_MUC_USER ->
3980
            ok;
33✔
3981
        _ ->
3982
            throw({error, mongoose_xmpp_errors:bad_request()})
×
3983
        end,
3984

3985
        InviteEls =
33✔
3986
            [InviteEl || #xmlel{name = <<"invite">>} = InviteEl <- Els1],
44✔
3987
        case InviteEls of
33✔
3988
            [_|_] ->
3989
                InviteEls;
33✔
3990
            _ ->
3991
                throw({error, mongoose_xmpp_errors:bad_request()})
×
3992
        end;
3993
    _ ->
3994
        throw({error, mongoose_xmpp_errors:bad_request()})
×
3995
    end.
3996

3997

3998
-spec create_password_elem(state()) -> [exml:element()].
3999
create_password_elem(#state{config=#config{password_protected=IsProtected,
4000
                                           password=Password}}) ->
4001
    case IsProtected of
44✔
4002
        true ->
4003
        [#xmlel{
×
4004
            name = <<"password">>,
4005
            children = [#xmlcdata{content = Password}]}];
4006
        _ ->
4007
        []
44✔
4008
    end.
4009

4010

4011
-spec invite_body_elem(jid:jid(), binary(), ejabberd:lang(), state()
4012
                      ) -> exml:element().
4013
invite_body_elem(FromJID, Reason, Lang, StateData) ->
4014
    Text = invite_body_text(FromJID, Reason, Lang, StateData),
44✔
4015
    #xmlel{
44✔
4016
        name = <<"body">>,
4017
        children = [#xmlcdata{content = Text}]}.
4018

4019

4020
-spec invite_body_text(jid:jid(), binary(), ejabberd:lang(), state()) -> binary().
4021
invite_body_text(FromJID, Reason, Lang,
4022
        #state{
4023
            jid=RoomJID,
4024
            config=#config{
4025
                password_protected=IsProtected,
4026
                password=Password}}) ->
4027
    BFromJID = jid:to_binary(FromJID),
44✔
4028
    BRoomJID = jid:to_binary(RoomJID),
44✔
4029
    ITranslate = service_translations:do(Lang, <<" invites you to the room ">>),
44✔
4030
    IMessage = <<BFromJID/binary, ITranslate/binary, BRoomJID/binary>>,
44✔
4031
    BPassword = case IsProtected of
44✔
4032
        true ->
4033
            PTranslate = service_translations:do(Lang, <<"the password is">>),
×
4034
            <<", ", PTranslate/binary, " '", Password/binary, "'">>;
×
4035
        _ ->
4036
            <<>>
44✔
4037
        end,
4038
    BReason = case Reason of
44✔
4039
        <<>> -> <<>>;
44✔
4040
        _    -> <<" (", Reason/binary, ") ">>
×
4041
        end,
4042
    <<IMessage/binary, BPassword/binary, BReason/binary>>.
44✔
4043

4044

4045
-spec create_invite_message_elem(Inv :: exml:element(), Body :: exml:element(),
4046
        Passwd :: [exml:element()], Reason :: binary()
4047
        ) -> exml:element().
4048
create_invite_message_elem(InviteEl, BodyEl, PasswdEl, Reason)
4049
    when is_list(PasswdEl), is_binary(Reason) ->
4050
    UserXEl = #xmlel{
44✔
4051
        name = <<"x">>,
4052
        attrs = #{<<"xmlns">> => ?NS_MUC_USER},
4053
        children = [InviteEl|PasswdEl]},
4054
    #xmlel{
44✔
4055
        name = <<"message">>,
4056
        attrs = #{<<"type">> => <<"normal">>},
4057
        children = [UserXEl, BodyEl]}.
4058

4059

4060
%% @doc Handle a message sent to the room by a non-participant.
4061
%% If it is a decline, send to the inviter.
4062
%% Otherwise, an error message is sent to the sender.
4063
-spec handle_roommessage_from_nonparticipant(exml:element(), ejabberd:lang(),
4064
                    state(), jid:simple_jid() | jid:jid()) -> mongoose_acc:t().
4065
handle_roommessage_from_nonparticipant(Packet, Lang, StateData, From) ->
4066
    case catch check_decline_invitation(Packet) of
151✔
4067
        {true, DeclineData} ->
4068
            send_decline_invitation(DeclineData, StateData#state.jid, From);
11✔
4069
        _ ->
4070
            send_error_only_occupants(<<"messages">>, Packet, Lang, StateData#state.jid, From)
140✔
4071
    end.
4072

4073

4074
%% @doc Check in the packet is a decline. If so, also returns the splitted
4075
%% packet. This function must be catched, because it crashes when the packet
4076
%% is not a decline message.
4077
-spec check_decline_invitation(exml:element()) ->
4078
    {true, {exml:element(), exml:element(), exml:element(), 'error' | jid:jid()}}.
4079
check_decline_invitation(Packet) ->
4080
    #xmlel{name = <<"message">>} = Packet,
151✔
4081

4082
    XEl = exml_query:subelement(Packet, <<"x">>),
151✔
4083
    ?NS_MUC_USER = exml_query:attr(XEl, <<"xmlns">>),
151✔
4084

4085
    DEl = exml_query:subelement(XEl, <<"decline">>),
11✔
4086
    ToString = exml_query:attr(DEl, <<"to">>),
11✔
4087
    ToJID = jid:from_binary(ToString),
11✔
4088
    {true, {Packet, XEl, DEl, ToJID}}.
11✔
4089

4090

4091
%% @doc Send the decline to the inviter user.
4092
%% The original stanza must be slightly modified.
4093
-spec send_decline_invitation({exml:element(), exml:element(), exml:element(), jid:jid()},
4094
        jid:jid(), jid:simple_jid() | jid:jid()) -> mongoose_acc:t().
4095
send_decline_invitation({Packet, XEl, DEl, ToJID}, RoomJID, FromJID) ->
4096
    FromString = jid:to_binary(FromJID),
11✔
4097
    #xmlel{name = <<"decline">>, attrs = DAttrs, children = DEls} = DEl,
11✔
4098
    DAttrs2 = maps:remove(<<"to">>, DAttrs),
11✔
4099
    DAttrs3 = DAttrs2#{<<"from">> => FromString},
11✔
4100
    DEl2 = #xmlel{name = <<"decline">>, attrs = DAttrs3, children = DEls},
11✔
4101
    XEl2 = jlib:replace_subelement(XEl, DEl2),
11✔
4102
    Packet2 = jlib:replace_subelement(Packet, XEl2),
11✔
4103
    ejabberd_router:route(RoomJID, ToJID, Packet2).
11✔
4104

4105
-spec send_error_only_occupants(binary(), exml:element(),
4106
                                binary() | nonempty_string(),
4107
                                jid:jid(), jid:jid()) -> mongoose_acc:t().
4108
send_error_only_occupants(What, Packet, Lang, RoomJID, From)
4109
  when is_binary(What) ->
4110
    ErrText = <<"Only occupants are allowed to send ",
151✔
4111
                What/bytes, " to the conference">>,
4112
    Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)),
151✔
4113
    ejabberd_router:route(RoomJID, From, Err).
151✔
4114

4115

4116
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4117
% Logging
4118

4119
-spec add_to_log(atom(), any(), state()) -> 'ok'.
4120
add_to_log(Type, Data, StateData)
4121
  when Type == roomconfig_change_disabledlogging ->
4122
    %% When logging is disabled, the config change message must be logged:
4123
    mod_muc_log:add_to_log(
11✔
4124
      StateData#state.server_host, roomconfig_change, Data,
4125
      jid:to_binary(StateData#state.jid), make_opts(StateData));
4126
add_to_log(Type, Data, StateData) ->
4127
    case (StateData#state.config)#config.logging of
47,078✔
4128
    true ->
4129
        mod_muc_log:add_to_log(
88✔
4130
          StateData#state.server_host, Type, Data,
4131
          jid:to_binary(StateData#state.jid), make_opts(StateData));
4132
    false ->
4133
        ok
46,990✔
4134
    end.
4135

4136
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4137
%% Users number checking
4138

4139
-spec tab_add_online_user(jid:jid(), state()) -> any().
4140
tab_add_online_user(JID, StateData) ->
4141
    {LUser, LServer, _} = jid:to_lower(JID),
7,552✔
4142
    US = {LUser, LServer},
7,552✔
4143
    Room = StateData#state.room,
7,552✔
4144
    Host = StateData#state.host,
7,552✔
4145
    catch ets:insert(
7,552✔
4146
        muc_online_users,
4147
        #muc_online_users{us = US, room = Room, host = Host}).
4148

4149

4150
-spec tab_remove_online_user(jid:simple_jid() | jid:jid(), state()) -> any().
4151
tab_remove_online_user(JID, StateData) ->
4152
    {LUser, LServer, _} = jid:to_lower(JID),
7,525✔
4153
    US = {LUser, LServer},
7,525✔
4154
    Room = StateData#state.room,
7,525✔
4155
    Host = StateData#state.host,
7,525✔
4156
    catch ets:delete_object(
7,525✔
4157
        muc_online_users,
4158
        #muc_online_users{us = US, room = Room, host = Host}).
4159

4160

4161
-spec tab_count_user(jid:jid()) -> non_neg_integer().
4162
tab_count_user(JID) ->
4163
    {LUser, LServer, _} = jid:to_lower(JID),
8,005✔
4164
    US = {LUser, LServer},
8,005✔
4165
    case catch ets:select(
8,005✔
4166
         muc_online_users,
4167
         [{#muc_online_users{us = US, _ = '_'}, [], [[]]}]) of
4168
    Res when is_list(Res) ->
4169
        length(Res);
8,005✔
4170
    _ ->
4171
        0
×
4172
    end.
4173

4174
element_size(El) ->
4175
    exml:xml_size(El).
17,047✔
4176

4177
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4178
%% Routing functions
4179

4180
-spec route_message(routed_message(), state()) -> state().
4181
route_message(#routed_message{allowed = true, type = <<"groupchat">>,
4182
                              from = From, packet = Packet, lang = Lang}, StateData) ->
4183
    Activity = get_user_activity(From, StateData),
8,529✔
4184
    Now = os:system_time(microsecond),
8,529✔
4185
    MinMessageInterval = trunc(get_opt(StateData, min_message_interval) * 1000000),
8,529✔
4186
    Size = element_size(Packet),
8,529✔
4187
    {MessageShaper, MessageShaperInterval} = mongoose_shaper:update(Activity#activity.message_shaper, Size),
8,529✔
4188
    case {Activity#activity.message /= undefined,
8,529✔
4189
          Now >= Activity#activity.message_time + MinMessageInterval,
4190
          MessageShaperInterval} of
4191
        {true, _, _} ->
4192
            ErrText = <<"Traffic rate limit is exceeded">>,
×
4193
            Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:resource_constraint(Lang, ErrText)),
×
4194
            ejabberd_router:route(StateData#state.jid, From, Err),
×
4195
            StateData;
×
4196
        {false, true, 0} ->
4197
            {RoomShaper, RoomShaperInterval} = mongoose_shaper:update(StateData#state.room_shaper, Size),
8,529✔
4198
            RoomQueueEmpty = queue:is_empty(StateData#state.room_queue),
8,529✔
4199
            case {RoomShaperInterval, RoomQueueEmpty} of
8,529✔
4200
                {0, true} ->
4201
                    NewActivity = Activity#activity{
8,529✔
4202
                                    message_time = Now,
4203
                                    message_shaper = MessageShaper},
4204
                    StateData1 = store_user_activity(From, NewActivity, StateData),
8,529✔
4205
                    StateData2 = StateData1#state{room_shaper = RoomShaper},
8,529✔
4206
                    {next_state, normal_state, StateData3, _} =
8,529✔
4207
                    process_groupchat_message(From, Packet, StateData2),
4208
                    StateData3;
8,529✔
4209
                _ ->
4210
                    StateData1 = schedule_queue_processing_when_empty(
×
4211
                                   RoomQueueEmpty, RoomShaper, RoomShaperInterval, StateData),
4212
                    NewActivity = Activity#activity{
×
4213
                                    message_time = Now,
4214
                                    message_shaper = MessageShaper,
4215
                                    message = Packet},
4216
                    RoomQueue = queue:in({message, From}, StateData#state.room_queue),
×
4217
                    StateData2 = store_user_activity(From, NewActivity, StateData1),
×
4218
                    StateData2#state{room_queue = RoomQueue}
×
4219
            end;
4220
        _ ->
4221
            MessageInterval = (Activity#activity.message_time + MinMessageInterval - Now) div 1000,
×
4222
            Interval = lists:max([MessageInterval, MessageShaperInterval]),
×
4223
            erlang:send_after(Interval, self(), {process_user_message, From}),
×
4224
            NewActivity = Activity#activity{
×
4225
                            message = Packet,
4226
                            message_shaper = MessageShaper},
4227
            store_user_activity(From, NewActivity, StateData)
×
4228
    end;
4229
route_message(#routed_message{allowed = true, type = <<"error">>, from = From,
4230
    packet = Packet, lang = Lang}, StateData) ->
4231
    case is_user_online(From, StateData) of
63✔
4232
        true ->
4233
            ErrorText
63✔
4234
            = <<"This participant is kicked from the room because he sent an error message">>,
4235
            expulse_participant(Packet, From, StateData, service_translations:do(Lang, ErrorText));
63✔
4236
        _ ->
4237
            StateData
×
4238
    end;
4239
route_message(#routed_message{allowed = true, type = <<"chat">>, from = From, packet = Packet,
4240
    lang = Lang}, StateData) ->
4241
    ErrText = <<"It is not allowed to send private messages to the conference">>,
×
4242
    Err = jlib:make_error_reply(
×
4243
        Packet, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)),
4244
    ejabberd_router:route(
×
4245
        StateData#state.jid,
4246
        From, Err),
4247
    StateData;
×
4248
route_message(#routed_message{allowed = true, type = Type, from = From,
4249
                              packet = #xmlel{name = <<"message">>,
4250
                                              children = Els} = Packet, lang = Lang},
4251
              StateData) when (Type == <<>> orelse Type == <<"normal">>) ->
4252

4253
    case exml_query:path(Packet, [{element, <<"x">>}, {element, <<"invite">>}], <<>>) of
143✔
4254
        <<>> ->
4255
            AppType = check_voice_approval(From, Packet, Lang, StateData),
99✔
4256
            route_voice_approval(AppType, From, Packet, Lang, StateData);
99✔
4257
        _ ->
4258
            InType = check_invitation(From, Els, Lang, StateData),
44✔
4259
            route_invitation(InType, From, Packet, Lang, StateData)
44✔
4260
    end;
4261
route_message(#routed_message{allowed = true, from = From, packet = Packet,
4262
                              lang = Lang}, StateData) ->
4263
    ErrText = <<"Improper message type">>,
×
4264
    Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)),
×
4265
    ejabberd_router:route(StateData#state.jid,
×
4266
                          From, Err),
4267
    StateData;
×
4268
route_message(#routed_message{type = <<"error">>}, StateData) ->
UNCOV
4269
    StateData;
×
4270
route_message(#routed_message{from = From, packet = Packet, lang = Lang},
4271
              StateData) ->
4272
    handle_roommessage_from_nonparticipant(Packet, Lang, StateData, From),
151✔
4273
    StateData.
151✔
4274

4275
-spec schedule_queue_processing_when_empty(RoomQueueEmpty :: boolean(),
4276
                                           RoomShaper :: mongoose_shaper:shaper(),
4277
                                           RoomShaperInterval :: non_neg_integer(),
4278
                                           StateData :: state()) -> state().
4279
schedule_queue_processing_when_empty(true, RoomShaper, RoomShaperInterval, StateData) ->
4280
    erlang:send_after(RoomShaperInterval, self(), process_room_queue),
×
4281
    StateData#state{room_shaper = RoomShaper};
×
4282
schedule_queue_processing_when_empty(_RoomQueueEmpty, _RoomShaper,
4283
                                     _RoomShaperInterval, StateData) ->
4284
    StateData.
×
4285

4286
-spec route_error(mod_muc:nick(), jid:jid(), exml:element(), state()) -> state().
4287
route_error(Nick, From, Error, StateData) ->
4288
    %% TODO: s/Nick/<<>>/
4289
    ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick),
442✔
4290
                          From, Error),
4291
    StateData.
442✔
4292

4293

4294
-spec route_voice_approval('ok' | {'error', exml:element()} | {'form', binary()}
4295
        | {'role', binary(), binary()}, jid:jid(), exml:element(),
4296
        ejabberd:lang(), state()) -> state().
4297
route_voice_approval({error, ErrType}, From, Packet, _Lang, StateData) ->
4298
    ejabberd_router:route(StateData#state.jid, From,
55✔
4299
                          jlib:make_error_reply(Packet, ErrType)),
4300
    StateData;
55✔
4301
route_voice_approval({form, RoleName}, From, _Packet, _Lang, StateData) ->
4302
    {Nick, _} = get_participant_data(From, StateData),
22✔
4303
    ApprovalForm = make_voice_approval_form(From, Nick, RoleName),
22✔
4304
    F = fun({_, Info}) ->
22✔
4305
                ejabberd_router:route(StateData#state.jid, Info#user.jid,
22✔
4306
                                      ApprovalForm)
4307
        end,
4308
    lists:foreach(F, search_role(moderator, StateData)),
22✔
4309
    StateData;
22✔
4310
route_voice_approval({role, BRole, Nick}, From, Packet, Lang, StateData) ->
4311
    Items = [#xmlel{name = <<"item">>,
11✔
4312
                    attrs = #{<<"role">> => BRole,
4313
                              <<"nick">> => Nick}}],
4314
    case process_admin_items_set(From, Items, Lang, StateData) of
11✔
4315
        {result, _Res, SD1} -> SD1;
11✔
4316
        {error, Error} ->
4317
            ejabberd_router:route(StateData#state.jid, From,
×
4318
                                  jlib:make_error_reply(Packet, Error)),
4319
            StateData
×
4320
    end;
4321
route_voice_approval(_Type, From, Packet, _Lang, StateData) ->
4322
    ejabberd_router:route(StateData#state.jid, From,
11✔
4323
                          jlib:make_error_reply(Packet, mongoose_xmpp_errors:bad_request())),
4324
    StateData.
11✔
4325

4326

4327
-spec route_invitation(InvitationsOrError,
4328
                       From, Packet, Lang, state()) -> state() when
4329
      InvitationsOrError :: {'error', exml:cdata() | exml:element()}
4330
                          | {'ok', [jid:jid()]},
4331
      From :: jid:simple_jid() | jid:jid(),
4332
      Packet :: exml:element(),
4333
      Lang :: ejabberd:lang().
4334
route_invitation({error, Error}, From, Packet, _Lang, StateData) ->
4335
    Err = jlib:make_error_reply(Packet, Error),
11✔
4336
    ejabberd_router:route(StateData#state.jid, From, Err),
11✔
4337
    StateData;
11✔
4338
route_invitation({ok, IJIDs}, _From, _Packet, _Lang,
4339
                 #state{ config = #config{ members_only = true } } = StateData0) ->
4340
    lists:foldl(
×
4341
      fun(IJID, StateData) ->
4342
              case get_affiliation(IJID, StateData) of
×
4343
                  none ->
4344
                      NSD = set_affiliation(IJID, member, StateData),
×
4345
                      store_room_if_persistent(NSD),
×
4346
                      NSD;
×
4347
                  _ ->
4348
                      StateData
×
4349
              end
4350
      end, StateData0, IJIDs);
4351
route_invitation({ok, _IJIDs}, _From, _Packet, _Lang, StateData0) ->
4352
    StateData0.
33✔
4353

4354
-spec store_room_if_persistent(state()) -> any().
4355
store_room_if_persistent(#state{ host = Host, room = Room, host_type = HostType,
4356
                                 config = #config{ persistent = true } } = StateData) ->
4357
    mod_muc:store_room(HostType, Host, Room, make_opts(StateData));
×
4358
store_room_if_persistent(_SD) ->
4359
    ok.
×
4360

4361
-spec route_iq(mongoose_acc:t(), routed_iq(), state()) -> {ok | stop, state()}.
4362
route_iq(_Acc, #routed_iq{iq = #iq{type = Type}}, StateData)
4363
  when Type == error; Type == result ->
4364
    {ok, StateData};
×
4365
route_iq(Acc, #routed_iq{iq = #iq{type = Type, xmlns = ?NS_MUC_ADMIN, lang = Lang,
4366
    sub_el = SubEl}, from = From} = Routed, StateData) ->
4367
    Res = process_iq_admin(From, Type, Lang, SubEl, StateData),
1,225✔
4368
    do_route_iq(Acc, Res, Routed, StateData);
1,225✔
4369
route_iq(Acc, #routed_iq{iq = #iq{type = Type, xmlns = ?NS_MUC_OWNER, lang = Lang,
4370
    sub_el = SubEl}, from = From} = Routed, StateData) ->
4371
    Res = process_iq_owner(From, Type, Lang, SubEl, StateData, normal_state),
264✔
4372
    do_route_iq(Acc, Res, Routed, StateData);
264✔
4373
route_iq(Acc, #routed_iq{iq = #iq{type = Type, xmlns = ?NS_DISCO_INFO, lang = Lang},
4374
    from = From} = Routed, StateData) ->
4375
    Res = process_iq_disco_info(From, Type, Lang, StateData),
41✔
4376
    do_route_iq(Acc, Res, Routed, StateData);
41✔
4377
route_iq(Acc, #routed_iq{iq = #iq{type = Type, xmlns = ?NS_DISCO_ITEMS, lang = Lang},
4378
    from = From} = Routed, StateData) ->
4379
    Res = process_iq_disco_items(From, Type, Lang, StateData),
22✔
4380
    do_route_iq(Acc, Res, Routed, StateData);
22✔
4381
route_iq(Acc, #routed_iq{iq = IQ = #iq{}, packet = Packet, from = From},
4382
         #state{host = Host, host_type = HostType, jid = RoomJID} = StateData) ->
4383
    %% Custom IQ, addressed to this room's JID.
4384
    case mod_muc_iq:process_iq(Host, From, RoomJID, Acc, IQ) of
5,374✔
4385
        {Acc1, error} ->
4386
            ?LOG_WARNING(#{what => muc_process_iq_failed, acc => Acc, server => Host,
×
4387
                           host_type => HostType, room_jid => RoomJID}),
×
4388
            E = mongoose_xmpp_errors:feature_not_implemented(
×
4389
                  <<"en">>, <<"From mod_muc_room">>),
4390
            {Acc2, Err} = jlib:make_error_reply(Acc1, Packet, E),
×
4391
            ejabberd_router:route(RoomJID, From, Acc2, Err);
×
4392
        _ -> ok
5,374✔
4393
    end,
4394
    {ok, StateData};
5,374✔
4395
route_iq(Acc, #routed_iq{packet = Packet, from = From}, StateData) ->
4396
    {Acc1, Err} = jlib:make_error_reply(
×
4397
        Acc, Packet, mongoose_xmpp_errors:feature_not_implemented()),
4398
    ejabberd_router:route(StateData#state.jid, From, Acc1, Err),
×
4399
    {ok, StateData}.
×
4400

4401

4402
-spec do_route_iq(mongoose_acc:t(), {result, [exml:element()], state()} | {error, exml:element()},
4403
                  routed_iq(), state()) -> {ok | stop, state()}.
4404
do_route_iq(Acc, Res1, #routed_iq{iq = #iq{xmlns = XMLNS, sub_el = SubEl} = IQ,
4405
    from = From}, StateData) ->
4406
    {IQRes, RoutingResult} = case Res1 of
1,552✔
4407
        {result, Res, SD} ->
4408
            {
1,266✔
4409
             IQ#iq{type = result,
4410
                sub_el = [#xmlel{name = <<"query">>,
4411
                                 attrs = #{<<"xmlns">> => XMLNS},
4412
                                 children = Res}]},
4413
             case SD of
4414
                 stop -> {stop, StateData};
22✔
4415
                 _ -> {ok, SD}
1,244✔
4416
             end
4417
            };
4418
        {error, Error} ->
4419
            {
286✔
4420
             IQ#iq{type = error, sub_el = [SubEl, Error]},
4421
             {ok, StateData}
4422
            }
4423
    end,
4424
    ejabberd_router:route(StateData#state.jid, From, Acc,
1,552✔
4425
        jlib:iq_to_xml(IQRes)),
4426
    RoutingResult.
1,552✔
4427

4428

4429
-spec route_nick_message(routed_nick_message(), state()) -> state().
4430
route_nick_message(#routed_nick_message{decide = {expulse_sender, _Reason},
4431
    packet = Packet, lang = Lang, from = From}, StateData) ->
4432
    ErrorText = <<"This participant is kicked from the room because he",
×
4433
                  "sent an error message to another participant">>,
4434
    ?LOG_DEBUG(ls(#{what => muc_expulse_sender, text => ErrorText,
×
4435
                    user => From#jid.luser, exml_packet => Packet}, StateData)),
×
4436
    expulse_participant(Packet, From, StateData, service_translations:do(Lang, ErrorText));
×
4437
route_nick_message(#routed_nick_message{decide = forget_message}, StateData) ->
4438
    StateData;
1✔
4439
route_nick_message(#routed_nick_message{decide = continue_delivery, allow_pm = true,
4440
    online = true, packet = Packet, from = From, type = <<"groupchat">>,
4441
    lang = Lang, nick = ToNick}, StateData) ->
4442
    ErrText = <<"It is not allowed to send private messages of type groupchat">>,
11✔
4443
    Err = jlib:make_error_reply(
11✔
4444
        Packet, mongoose_xmpp_errors:bad_request(Lang, ErrText)),
4445
    route_error(ToNick, From, Err, StateData),
11✔
4446
    StateData;
11✔
4447
route_nick_message(#routed_nick_message{decide = continue_delivery, allow_pm = true,
4448
    online = true, packet = Packet, from = From,
4449
    lang = Lang, nick = ToNick, jid = false}, StateData) ->
4450
    ErrText = <<"Recipient is not in the conference room">>,
11✔
4451
    Err = jlib:make_error_reply(
11✔
4452
        Packet, mongoose_xmpp_errors:item_not_found(Lang, ErrText)),
4453
    route_error(ToNick, From, Err, StateData),
11✔
4454
    StateData;
11✔
4455
route_nick_message(#routed_nick_message{decide = continue_delivery, allow_pm = true,
4456
    online = true, packet = Packet, from = From, jid = ToJID}, StateData) ->
4457
    Packet1 = maybe_add_x_element(Packet),
261✔
4458
    {ok, #user{nick = FromNick}} = maps:find(jid:to_lower(From),
261✔
4459
        StateData#state.users),
4460
    ejabberd_router:route(
261✔
4461
        jid:replace_resource(StateData#state.jid, FromNick), ToJID, Packet1),
4462
    StateData;
261✔
4463
route_nick_message(#routed_nick_message{decide = continue_delivery,
4464
                                        allow_pm = true,
4465
                                        online = false} = Routed, StateData) ->
4466
    #routed_nick_message{packet = Packet, from = From,
11✔
4467
                         lang = Lang, nick = ToNick} = Routed,
4468
    RoomJID = jid:replace_resource(StateData#state.jid, ToNick),
11✔
4469
    send_error_only_occupants(<<"messages">>, Packet, Lang, RoomJID, From),
11✔
4470
    StateData;
11✔
4471
route_nick_message(#routed_nick_message{decide = continue_delivery, allow_pm = false,
4472
    packet = Packet, from = From,
4473
    lang = Lang, nick = ToNick}, StateData) ->
4474
    ErrText = <<"It is not allowed to send private messages">>,
×
4475
    Err = jlib:make_error_reply(
×
4476
        Packet, mongoose_xmpp_errors:forbidden(Lang, ErrText)),
4477
    route_error(ToNick, From, Err, StateData),
×
4478
    StateData.
×
4479

4480

4481
-spec route_nick_iq(routed_nick_iq(), state()) -> 'ok'.
4482
route_nick_iq(#routed_nick_iq{allow_query = true, online = {true, _, _}, jid = false,
4483
    iq = reply}, _StateData) ->
4484
    ok;
×
4485
route_nick_iq(#routed_nick_iq{allow_query = true, online = {true, _, _}, jid = false,
4486
    packet = Packet, lang = Lang, from = From, nick = ToNick}, StateData) ->
4487
    ErrText = <<"Recipient is not in the conference room">>,
×
4488
    Err = jlib:make_error_reply(
×
4489
        Packet, mongoose_xmpp_errors:item_not_found(Lang, ErrText)),
4490
    route_error(ToNick, From, Err, StateData);
×
4491
route_nick_iq(#routed_nick_iq{allow_query = true, online = {true, NewId, FromFull},
4492
    jid = ToJID, packet = Packet, stanza = StanzaId}, StateData) ->
4493
    {ok, #user{nick = FromNick}} = maps:find(jid:to_lower(FromFull),
×
4494
        StateData#state.users),
4495
    {ToJID2, Packet2} = handle_iq_vcard(FromFull, ToJID, StanzaId, NewId, Packet),
×
4496
    ejabberd_router:route(
×
4497
        jid:replace_resource(StateData#state.jid, FromNick),
4498
        ToJID2, Packet2);
4499
route_nick_iq(#routed_nick_iq{online = {false, _, _}, iq = reply}, _StateData) ->
4500
    ok;
×
4501
route_nick_iq(#routed_nick_iq{online = {false, _, _}, from = From, nick = ToNick,
4502
                              packet = Packet, lang = Lang}, StateData) ->
4503
    RoomJID = jid:replace_resource(StateData#state.jid, ToNick),
×
4504
    send_error_only_occupants(<<"queries">>, Packet, Lang, RoomJID, From);
×
4505
route_nick_iq(#routed_nick_iq{iq = reply}, _StateData) ->
4506
    ok;
×
4507
route_nick_iq(#routed_nick_iq{packet = Packet, lang = Lang, nick = ToNick,
4508
                              from = From}, StateData) ->
4509
    ErrText = <<"Queries to the conference members are "
×
4510
                "not allowed in this room">>,
4511
    Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:not_allowed(Lang, ErrText)),
×
4512
    route_error(ToNick, From, Err, StateData).
×
4513

4514

4515
-spec decode_reason(exml:element()) -> binary().
4516
decode_reason(Elem) ->
4517
    exml_query:path(Elem, [{element, <<"reason">>}, cdata], <<>>).
1,398✔
4518

4519
-spec make_voice_approval_form(From :: jid:simple_jid() | jid:jid(),
4520
                               Nick :: binary(), Role :: binary()) -> exml:element().
4521
make_voice_approval_form(From, Nick, Role) ->
4522
    Title = <<"Voice request">>,
22✔
4523
    Instructions = <<"To approve this request"
22✔
4524
                     " for voice, select the &quot;Grant voice to this person?&quot; checkbox"
4525
                     " and click OK. To skip this request, click the cancel button.">>,
4526
    Fields = [#{var => <<"muc#role">>, type => <<"list-single">>,
22✔
4527
                label => <<"Request role">>, values => [Role]},
4528
              #{var => <<"muc#jid">>, type => <<"jid-single">>,
4529
                label => <<"User ID">>, values => [jid:to_binary(From)]},
4530
              #{var => <<"muc#roomnick">>, type => <<"text-single">>,
4531
                label => <<"Room Nickname">>, values => [Nick]},
4532
              #{var => <<"muc#request_allow">>, type => <<"boolean">>,
4533
                label => <<"Grant voice to this person?">>, values => [<<"false">>]}],
4534
    Form = mongoose_data_forms:form(#{title => Title, instructions => Instructions,
22✔
4535
                                      ns => ?NS_MUC_REQUEST, fields => Fields}),
4536
    #xmlel{name = <<"message">>, children = [Form]}.
22✔
4537

4538
-spec xfield(binary(), any(), binary(), binary(), ejabberd:lang()) -> mongoose_data_forms:field().
4539
xfield(Type, Label, Var, Val, Lang) ->
4540
    #{type => Type, label => service_translations:do(Lang, Label), var => Var, values => [Val]}.
2,178✔
4541

4542
-spec boolxfield(any(), binary(), any(), ejabberd:lang()) -> mongoose_data_forms:field().
4543
boolxfield(Label, Var, Val, Lang) ->
4544
    xfield(<<"boolean">>, Label, Var,
1,815✔
4545
        case Val of
4546
            true -> <<"1">>;
1,177✔
4547
            _ -> <<"0">>
638✔
4548
        end, Lang).
4549

4550
stringxfield(Label, Var, Val, Lang) ->
4551
    xfield(<<"text-single">>, Label, Var, Val, Lang).
242✔
4552

4553
privatexfield(Label, Var, Val, Lang) ->
4554
    xfield(<<"text-private">>, Label, Var, Val, Lang).
121✔
4555

4556
notify_users_modified(#state{host_type = HostType, jid = JID, users = Users} = State) ->
4557
    mod_muc_log:set_room_occupants(HostType, self(), JID, maps:values(Users)),
30,381✔
4558
    State.
30,381✔
4559

4560
ls(LogMap, State) ->
4561
    maps:merge(LogMap, #{room => State#state.room,
×
4562
                         sub_host => State#state.host}).
4563

4564
get_opt(#state{host_type = HostType}, Opt) ->
4565
    gen_mod:get_module_opt(HostType, mod_muc, Opt).
139,896✔
4566

4567
get_current_timestamp() ->
4568
    SystemTime = os:system_time(second),
716✔
4569
    TimeStamp = calendar:system_time_to_rfc3339(SystemTime, [{offset, "Z"}]),
716✔
4570
    list_to_binary(TimeStamp).
716✔
4571

4572
read_hibernate_timeout(HostType) ->
4573
    gen_mod:get_module_opt(HostType, mod_muc, hibernate_timeout).
6,426✔
4574

4575
maybe_add_x_element(#xmlel{children = Children} = Msg) ->
4576
    XEl = #xmlel{name = <<"x">>, attrs = #{<<"xmlns">> => ?NS_MUC_USER}, children = []},
261✔
4577
    case lists:member(XEl, Children) of
261✔
4578
        true -> Msg;
11✔
4579
        false ->
4580
            NewChildren = lists:append(Children, [XEl]),
250✔
4581
            Msg#xmlel{children = NewChildren}
250✔
4582
    end.
4583

4584
kick_stanza_for_old_protocol(Packet) ->
4585
    Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>),
11✔
4586
    ErrText = <<"You are not in the room.">>,
11✔
4587
    ErrText2 = service_translations:do(Lang, ErrText),
11✔
4588
    Response = #xmlel{name = <<"presence">>, attrs = #{<<"type">> => <<"unavailable">>}},
11✔
4589
    ItemAttrs = #{<<"affiliation">> => <<"none">>, <<"role">> => <<"none">>},
11✔
4590
    ItemEls = [#xmlel{name = <<"reason">>, children = [#xmlcdata{content = ErrText2}]}],
11✔
4591
    Status = [status_code(110), status_code(307), status_code(333)],
11✔
4592
    jlib:append_subtags(
11✔
4593
        Response,
4594
        [#xmlel{name = <<"x">>, attrs = #{<<"xmlns">> => ?NS_MUC},
4595
                children = [#xmlel{name = <<"item">>, attrs = ItemAttrs,
4596
                                   children = ItemEls} | Status]}]).
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