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 |
595 |
Supervisor = gen_mod:get_module_proc(HostType, ejabberd_mod_muc_sup), |
165 |
595 |
Args = #{init_type => start_new, host_type => HostType, |
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 |
595 |
supervisor:start_child(Supervisor, [Args]). |
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 |
5 |
Supervisor = gen_mod:get_module_proc(HostType, ejabberd_mod_muc_sup), |
180 |
5 |
Args = #{init_type => start_restored, host_type => HostType, |
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 |
5 |
supervisor:start_child(Supervisor, [Args]). |
186 |
|
|
187 |
|
start_link(Args = #{}) -> |
188 |
600 |
gen_fsm_compat:start_link(?MODULE, Args, []). |
189 |
|
|
190 |
|
stop(Pid) -> |
191 |
56 |
gen_fsm_compat:stop(Pid). |
192 |
|
|
193 |
|
-spec get_room_users(RoomJID :: jid:jid()) -> |
194 |
|
{ok, [user()]} | {error, not_found}. |
195 |
|
get_room_users(RoomJID) -> |
196 |
232 |
case mod_muc:room_jid_to_pid(RoomJID) of |
197 |
|
{ok, Pid} -> |
198 |
72 |
gen_fsm_compat:sync_send_all_state_event(Pid, get_room_users); |
199 |
|
{error, Reason} -> |
200 |
160 |
{error, Reason} |
201 |
|
end. |
202 |
|
|
203 |
|
-spec get_room_affiliations(RoomJID :: jid:jid()) -> |
204 |
|
{ok, affiliations_map()} | {error, not_found}. |
205 |
|
get_room_affiliations(RoomJID) -> |
206 |
57 |
case mod_muc:room_jid_to_pid(RoomJID) of |
207 |
|
{ok, Pid} -> |
208 |
53 |
gen_fsm_compat:sync_send_all_state_event(Pid, get_room_affiliations); |
209 |
|
{error, Reason} -> |
210 |
4 |
{error, Reason} |
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 |
9 |
case mod_muc:room_jid_to_pid(RoomJID) of |
217 |
|
{ok, Pid} -> |
218 |
6 |
gen_fsm_compat:sync_send_all_state_event(Pid, {is_room_owner, UserJID}); |
219 |
|
{error, Reason} -> |
220 |
3 |
{error, Reason} |
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 |
56 |
case mod_muc:room_jid_to_pid(RoomJID) of |
230 |
|
{ok, Pid} -> |
231 |
55 |
gen_fsm_compat:sync_send_event(Pid, {set_admin_items, ModJID, Items}); |
232 |
|
{error, Reason} -> |
233 |
1 |
{error, Reason} |
234 |
|
end. |
235 |
|
|
236 |
|
-spec get_room_config(jid:jid()) -> |
237 |
|
{ok, config()} | {error, not_found}. |
238 |
|
get_room_config(RoomJID) -> |
239 |
17 |
case mod_muc:room_jid_to_pid(RoomJID) of |
240 |
|
{ok, Pid} -> |
241 |
15 |
gen_fsm_compat:sync_send_all_state_event(Pid, get_config); |
242 |
|
{error, Reason} -> |
243 |
2 |
{error, Reason} |
244 |
|
end. |
245 |
|
|
246 |
|
-spec change_room_config(jid:jid(), config()) -> |
247 |
|
{ok, config()} | {error, not_found}. |
248 |
|
change_room_config(RoomJID, NewConfig) -> |
249 |
4 |
case mod_muc:room_jid_to_pid(RoomJID) of |
250 |
|
{ok, Pid} -> |
251 |
4 |
gen_fsm_compat:sync_send_all_state_event(Pid, {change_config, NewConfig}); |
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 |
9 |
case mod_muc:room_jid_to_pid(RoomJID) of |
260 |
|
{ok, Pid} -> |
261 |
4 |
gen_fsm_compat:send_all_state_event(Pid, {destroy, ReasonIn}); |
262 |
|
{error, Reason} -> |
263 |
5 |
{error, Reason} |
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 |
499 |
case mod_muc:room_jid_to_pid(RoomJID) of |
271 |
|
{ok, Pid} -> |
272 |
454 |
gen_fsm_compat:sync_send_all_state_event(Pid, {can_access_room, UserJID}); |
273 |
|
Error -> |
274 |
45 |
Error |
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 |
319 |
case mod_muc:room_jid_to_pid(RoomJID) of |
282 |
|
{ok, Pid} -> |
283 |
284 |
gen_fsm_compat:sync_send_all_state_event(Pid, {can_access_identity, UserJID}); |
284 |
|
{error, Reason} -> |
285 |
35 |
{error, Reason} |
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 |
595 |
init_new(Args); |
299 |
|
init(#{init_type := start_restored} = Args) -> |
300 |
5 |
init_restored(Args). |
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 |
595 |
process_flag(trap_exit, true), |
308 |
595 |
Shaper = mongoose_shaper:new(RoomShaper), |
309 |
595 |
State = #state{host = Host, host_type = HostType, server_host = ServerHost, |
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 |
595 |
State1 = set_opts(DefRoomOpts, State), |
319 |
595 |
State2 = set_affiliation(Creator, owner, State1), |
320 |
595 |
?LOG_INFO(ls(#{what => muc_room_started, |
321 |
595 |
creator_jid => jid:to_binary(Creator)}, State)), |
322 |
595 |
add_to_log(room_existence, created, State2), |
323 |
595 |
State3 = case proplists:get_value(subject, DefRoomOpts, none) of |
324 |
|
none -> |
325 |
538 |
State2; |
326 |
|
_ -> |
327 |
57 |
set_opts([{subject_timestamp, get_current_timestamp()}], State2) |
328 |
|
end, |
329 |
595 |
case proplists:get_value(instant, DefRoomOpts, false) of |
330 |
|
true -> |
331 |
|
%% Instant room -- groupchat 1.0 request |
332 |
556 |
add_to_log(room_existence, started, State3), |
333 |
556 |
save_persistent_room_state(State3), |
334 |
556 |
{ok, normal_state, State3, State3#state.hibernate_timeout}; |
335 |
|
false -> |
336 |
|
%% Locked room waiting for configuration -- MUC request |
337 |
39 |
{ok, initial_state, State3} |
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 |
5 |
process_flag(trap_exit, true), |
348 |
5 |
Shaper = mongoose_shaper:new(RoomShaper), |
349 |
5 |
State = set_opts(Opts, #state{host = Host, host_type = HostType, |
350 |
|
server_host = ServerHost, |
351 |
|
access = Access, |
352 |
|
room = Room, |
353 |
|
history = lqueue_new(HistorySize), |
354 |
|
jid = jid:make_bare(Room, Host), |
355 |
|
room_shaper = Shaper, |
356 |
|
http_auth_pool = HttpAuthPool, |
357 |
|
hibernate_timeout = read_hibernate_timeout(HostType) |
358 |
|
}), |
359 |
5 |
add_to_log(room_existence, started, State), |
360 |
5 |
mongoose_metrics:update(global, [mod_muc, process_recreations], 1), |
361 |
5 |
{ok, normal_state, State, State#state.hibernate_timeout}. |
362 |
|
|
363 |
|
%% @doc In the locked state StateData contains the same settings it previously |
364 |
|
%% held for the normal_state. The fsm awaits either a confirmation or a |
365 |
|
%% configuration form from the creator. Responds with error to any other queries. |
366 |
|
-spec locked_error({'route', jid:jid(), _, mongoose_acc:t(), exml:element()}, |
367 |
|
statename(), state()) -> fsm_return(). |
368 |
|
locked_error({route, From, ToNick, Acc, #xmlel{attrs = Attrs} = Packet}, |
369 |
|
NextState, StateData) -> |
370 |
1 |
?LOG_INFO(ls(#{what => muc_route_to_locked_room, acc => Acc}, StateData)), |
371 |
1 |
ErrText = <<"This room is locked">>, |
372 |
1 |
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), |
373 |
1 |
{Acc1, Err} = jlib:make_error_reply(Acc, Packet, mongoose_xmpp_errors:item_not_found(Lang, ErrText)), |
374 |
1 |
ejabberd_router:route(jid:replace_resource(StateData#state.jid, |
375 |
|
ToNick), |
376 |
|
From, Acc1, Err), |
377 |
1 |
{next_state, NextState, StateData}. |
378 |
|
|
379 |
|
%% @doc Receive the room-creating Stanza. Will crash if any other stanza is |
380 |
|
%% received in this state. |
381 |
|
-spec initial_state({'route', From :: jid:jid(), To :: mod_muc:nick(), |
382 |
|
Acc :: mongoose_acc:t(), Presence :: exml:element()}, state()) -> fsm_return(). |
383 |
|
initial_state({route, From, ToNick, _Acc, % TOODOO |
384 |
|
#xmlel{name = <<"presence">>} = Presence}, StateData) -> |
385 |
|
%% this should never happen so crash if it does |
386 |
39 |
<<>> = exml_query:attr(Presence, <<"type">>, <<>>), |
387 |
39 |
owner = get_affiliation(From, StateData), %% prevent race condition (2 users create same room) |
388 |
39 |
XNamespaces = exml_query:paths(Presence, [{element, <<"x">>}, {attr, <<"xmlns">>}]), |
389 |
39 |
case lists:member(?NS_MUC, XNamespaces) of |
390 |
|
true -> |
391 |
|
%% FIXME |
392 |
39 |
add_to_log(room_existence, started, StateData), |
393 |
39 |
process_presence(From, ToNick, Presence, StateData, locked_state); |
394 |
|
%% The fragment of normal_state with Activity that used to do this - how does that work? |
395 |
|
%% Seems to work without it |
396 |
|
false -> |
397 |
|
%% groupchat 1.0 user, straight to normal_state |
398 |
:-( |
process_presence(From, ToNick, Presence, StateData) |
399 |
|
end. |
400 |
|
|
401 |
|
-spec is_query_allowed(exml:element()) -> boolean(). |
402 |
|
is_query_allowed(#xmlel{children = Els}) -> |
403 |
33 |
case xml:remove_cdata(Els) of |
404 |
|
[#xmlel{name = <<"destroy">>}] -> |
405 |
3 |
true; |
406 |
|
[El] -> |
407 |
30 |
mongoose_data_forms:is_form(El, [<<"submit">>, <<"cancel">>]); |
408 |
|
_ -> |
409 |
:-( |
false |
410 |
|
end. |
411 |
|
|
412 |
|
-spec locked_state_process_owner_iq(jid:jid(), exml:element(), |
413 |
|
ejabberd:lang(), 'error' | 'get' | 'invalid' | 'result', _) |
414 |
|
-> {{'error', exml:element()}, statename()} |
415 |
|
| {{result, [exml:element() | jlib:xmlcdata()], state() | stop}, statename()}. |
416 |
|
locked_state_process_owner_iq(From, Query, Lang, set, StateData) -> |
417 |
33 |
Result = case is_query_allowed(Query) of |
418 |
|
true -> |
419 |
33 |
process_iq_owner(From, set, Lang, Query, StateData, locked_state); |
420 |
|
false -> |
421 |
:-( |
{error, mongoose_xmpp_errors:item_not_found(Lang, <<"Query not allowed">>)} |
422 |
|
end, |
423 |
33 |
{Result, normal_state}; |
424 |
|
locked_state_process_owner_iq(From, Query, Lang, get, StateData) -> |
425 |
4 |
{process_iq_owner(From, get, Lang, Query, StateData, locked_state), locked_state}; |
426 |
|
locked_state_process_owner_iq(_From, _Query, Lang, _Type, _StateData) -> |
427 |
:-( |
{{error, mongoose_xmpp_errors:item_not_found(Lang, <<"Wrong type">>)}, locked_state}. |
428 |
|
|
429 |
|
|
430 |
|
%% @doc Destroy room / confirm instant room / configure room |
431 |
|
-spec locked_state({'route', From :: jid:jid(), To :: mod_muc:nick(), |
432 |
|
Acc :: mongoose_acc:t(), Packet :: exml:element()}, state()) -> fsm_return(). |
433 |
|
locked_state({route, From, _ToNick, Acc, |
434 |
|
#xmlel{name = <<"iq">>} = Packet}, StateData) -> |
435 |
38 |
#iq{lang = Lang, sub_el = Query, xmlns = NS} = IQ = jlib:iq_query_info(Packet), |
436 |
38 |
{Result, NextState1} = |
437 |
|
case {NS, get_affiliation(From, StateData)} of |
438 |
|
{?NS_MUC_OWNER, owner} -> |
439 |
37 |
locked_state_process_owner_iq(From, Query, Lang, IQ#iq.type, StateData); |
440 |
|
{?NS_DISCO_INFO, owner} -> |
441 |
1 |
{process_iq_disco_info(From, IQ#iq.type, Lang, StateData), locked_state}; |
442 |
|
_ -> |
443 |
:-( |
ErrText = <<"This room is locked">>, |
444 |
:-( |
{{error, mongoose_xmpp_errors:item_not_found(Lang, ErrText)}, locked_state} |
445 |
|
end, |
446 |
38 |
MkQueryResult = fun(Res) -> |
447 |
37 |
IQ#iq{type = result, |
448 |
|
sub_el = [#xmlel{name = <<"query">>, |
449 |
|
attrs = [{<<"xmlns">>, NS}], |
450 |
|
children = Res}]} |
451 |
|
end, |
452 |
38 |
{IQRes, StateData3, NextState2} = |
453 |
|
case Result of |
454 |
5 |
{result, InnerRes, stop} -> {MkQueryResult(InnerRes), StateData, stop}; |
455 |
32 |
{result, InnerRes, StateData2} -> {MkQueryResult(InnerRes), StateData2, NextState1}; |
456 |
1 |
{error, Error} -> {IQ#iq{type = error, sub_el = [Query, Error]}, StateData, NextState1} |
457 |
|
end, |
458 |
38 |
ejabberd_router:route(StateData3#state.jid, From, Acc, jlib:iq_to_xml(IQRes)), |
459 |
38 |
case NextState2 of |
460 |
|
stop -> |
461 |
5 |
{stop, normal, StateData3}; |
462 |
|
locked_state -> |
463 |
5 |
{next_state, NextState2, StateData3}; |
464 |
|
normal_state -> |
465 |
28 |
next_normal_state(StateData3#state{just_created = false}) |
466 |
|
end; |
467 |
|
%% Let owner leave. Destroy the room. |
468 |
|
locked_state({route, From, ToNick, _Acc, |
469 |
|
#xmlel{name = <<"presence">>, attrs = Attrs} = Presence} = Call, |
470 |
|
StateData) -> |
471 |
4 |
case xml:get_attr_s(<<"type">>, Attrs) =:= <<"unavailable">> |
472 |
3 |
andalso get_affiliation(From, StateData) =:= owner of |
473 |
|
true -> |
474 |
|
%% Will let the owner leave and destroy the room if it's not persistant |
475 |
|
%% The rooms are not persistent by default, but just to be safe... |
476 |
3 |
NewConfig = (StateData#state.config)#config{persistent = false}, |
477 |
3 |
StateData1 = StateData#state{config = NewConfig}, |
478 |
3 |
process_presence(From, ToNick, Presence, StateData1, locked_state); |
479 |
|
_ -> |
480 |
1 |
locked_error(Call, locked_state, StateData) |
481 |
|
end; |
482 |
|
locked_state(timeout, StateData) -> |
483 |
:-( |
{next_state, locked_state, StateData}; |
484 |
|
locked_state(Call, StateData) -> |
485 |
:-( |
locked_error(Call, locked_state, StateData). |
486 |
|
|
487 |
|
|
488 |
|
-spec normal_state({route, From :: jid:jid(), To :: mod_muc:nick(), Acc :: mongoose_acc:t(), |
489 |
|
Packet :: exml:element()}, state()) -> fsm_return(). |
490 |
|
normal_state({route, From, <<>>, _Acc, |
491 |
|
#xmlel{name = <<"message">>, attrs = Attrs} = Packet}, |
492 |
|
StateData) -> |
493 |
424 |
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), |
494 |
424 |
Type = xml:get_attr_s(<<"type">>, Attrs), |
495 |
|
|
496 |
424 |
NewStateData = route_message(#routed_message{ |
497 |
|
allowed = can_send_to_conference(From, StateData), |
498 |
|
type = Type, |
499 |
|
from = From, |
500 |
|
packet = Packet, |
501 |
|
lang = Lang}, StateData), |
502 |
424 |
next_normal_state(NewStateData); |
503 |
|
normal_state({route, From, <<>>, Acc0, |
504 |
|
#xmlel{name = <<"iq">>} = Packet}, |
505 |
|
StateData) -> |
506 |
559 |
{IQ, Acc} = mongoose_iq:info(Acc0), |
507 |
559 |
{RoutingEffect, NewStateData} = route_iq(Acc, #routed_iq{ |
508 |
|
iq = IQ, |
509 |
|
from = From, |
510 |
|
packet = Packet}, StateData), |
511 |
559 |
case RoutingEffect of |
512 |
557 |
ok -> next_normal_state(NewStateData); |
513 |
2 |
stop -> {stop, normal, NewStateData} |
514 |
|
end; |
515 |
|
normal_state({route, From, Nick, _Acc, |
516 |
|
#xmlel{name = <<"presence">>} = Packet}, |
517 |
|
StateData) -> |
518 |
|
% FIXME sessions do we need to route presences to all sessions |
519 |
1328 |
Activity = get_user_activity(From, StateData), |
520 |
1328 |
Now = os:system_time(microsecond), |
521 |
1328 |
MinPresenceInterval = trunc(get_opt(StateData, min_presence_interval) * 1000000), |
522 |
1328 |
case (Now >= Activity#activity.presence_time + MinPresenceInterval) and |
523 |
|
(Activity#activity.presence == undefined) of |
524 |
|
true -> |
525 |
1328 |
NewActivity = Activity#activity{presence_time = Now}, |
526 |
1328 |
StateData1 = store_user_activity(From, NewActivity, StateData), |
527 |
1328 |
process_presence(From, Nick, Packet, StateData1); |
528 |
|
false -> |
529 |
:-( |
case Activity#activity.presence == undefined of |
530 |
|
true -> |
531 |
:-( |
Interval = (Activity#activity.presence_time + |
532 |
|
MinPresenceInterval - Now) div 1000, |
533 |
:-( |
erlang:send_after(Interval, self(), {process_user_presence, From}); |
534 |
|
false -> |
535 |
:-( |
ok |
536 |
|
end, |
537 |
:-( |
NewActivity = Activity#activity{presence = {Nick, Packet}}, |
538 |
:-( |
StateData1 = store_user_activity(From, NewActivity, StateData), |
539 |
:-( |
next_normal_state(StateData1) |
540 |
|
end; |
541 |
|
normal_state({route, From, ToNick, _Acc, |
542 |
|
#xmlel{name = <<"message">>, attrs = Attrs} = Packet}, |
543 |
|
StateData) -> |
544 |
30 |
Type = xml:get_attr_s(<<"type">>, Attrs), |
545 |
30 |
FunRouteNickMessage = fun(JID, StateDataAcc) -> |
546 |
31 |
route_nick_message(#routed_nick_message{ |
547 |
|
allow_pm = (StateDataAcc#state.config)#config.allow_private_messages, |
548 |
|
online = is_user_online(From, StateDataAcc), |
549 |
|
type = Type, |
550 |
|
from = From, |
551 |
|
nick = ToNick, |
552 |
|
lang = xml:get_attr_s(<<"xml:lang">>, Attrs), |
553 |
|
decide = decide_fate_message(Type, Packet, From, StateDataAcc), |
554 |
|
packet = Packet, |
555 |
|
jid = JID}, StateDataAcc) |
556 |
|
end, |
557 |
30 |
NewStateData = case find_jids_by_nick(ToNick, StateData) of |
558 |
1 |
[] -> FunRouteNickMessage(false, StateData); |
559 |
29 |
JIDs -> lists:foldl(FunRouteNickMessage, StateData, JIDs) |
560 |
|
end, |
561 |
30 |
next_normal_state(NewStateData); |
562 |
|
normal_state({route, From, ToNick, _Acc, |
563 |
|
#xmlel{name = <<"iq">>, attrs = Attrs} = Packet}, |
564 |
|
StateData) -> |
565 |
:-( |
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), |
566 |
:-( |
StanzaId = xml:get_attr_s(<<"id">>, Attrs), |
567 |
:-( |
FunRouteNickIq = fun(JID) -> |
568 |
:-( |
route_nick_iq(#routed_nick_iq{ |
569 |
|
allow_query = (StateData#state.config)#config.allow_query_users, |
570 |
|
online = is_user_online_iq(StanzaId, From, StateData), |
571 |
|
jid = JID, |
572 |
|
iq = jlib:iq_query_info(Packet), |
573 |
|
packet = Packet, |
574 |
|
lang = Lang, |
575 |
|
from = From, |
576 |
|
stanza = StanzaId, |
577 |
|
nick = ToNick}, StateData) |
578 |
|
end, |
579 |
:-( |
case find_jids_by_nick(ToNick, StateData) of |
580 |
:-( |
[] -> FunRouteNickIq(false); |
581 |
:-( |
JIDs -> lists:foreach(FunRouteNickIq, JIDs) |
582 |
|
end, |
583 |
:-( |
next_normal_state(StateData); |
584 |
|
normal_state({http_auth, AuthPid, Result, From, Nick, Packet, Role}, StateData) -> |
585 |
1 |
AuthPids = StateData#state.http_auth_pids, |
586 |
1 |
StateDataWithoutPid = StateData#state{http_auth_pids = lists:delete(AuthPid, AuthPids)}, |
587 |
1 |
NewStateData = handle_http_auth_result(Result, From, Nick, Packet, Role, StateDataWithoutPid), |
588 |
1 |
destroy_temporary_room_if_empty(NewStateData, normal_state); |
589 |
|
normal_state(timeout, StateData) -> |
590 |
127 |
erlang:put(hibernated, os:timestamp()), |
591 |
127 |
mongoose_metrics:update(global, [mod_muc, hibernations], 1), |
592 |
127 |
{next_state, normal_state, StateData, hibernate}; |
593 |
|
normal_state(_Event, StateData) -> |
594 |
:-( |
next_normal_state(StateData). |
595 |
|
|
596 |
|
normal_state({set_admin_items, UJID, Items}, _From, |
597 |
|
#state{hibernate_timeout = Timeout} = StateData) -> |
598 |
55 |
case process_admin_items_set(UJID, Items, <<"en">>, StateData) of |
599 |
|
{result, [], StateData2} -> |
600 |
43 |
{reply, ok, normal_state, StateData2, Timeout}; |
601 |
|
{error, Error} -> |
602 |
12 |
{reply, {error, Error}, normal_state, StateData, Timeout} |
603 |
|
end. |
604 |
|
|
605 |
|
handle_event({service_message, Msg}, _StateName, StateData) -> |
606 |
:-( |
MessagePkt = #xmlel{name = <<"message">>, |
607 |
|
attrs = [{<<"type">>, <<"groupchat">>}], |
608 |
|
children = [#xmlel{name = <<"body">>, |
609 |
|
children = [#xmlcdata{content = Msg}]}]}, |
610 |
:-( |
send_to_all_users(MessagePkt, StateData), |
611 |
:-( |
NSD = add_message_to_history(<<>>, |
612 |
|
StateData#state.jid, |
613 |
|
MessagePkt, |
614 |
|
StateData), |
615 |
:-( |
next_normal_state(NSD); |
616 |
|
|
617 |
|
handle_event({destroy, Reason}, _StateName, StateData) -> |
618 |
270 |
{result, [], stop} = |
619 |
|
destroy_room( |
620 |
|
#xmlel{name = <<"destroy">>, attrs = [{<<"xmlns">>, ?NS_MUC_OWNER}], |
621 |
|
children = case Reason of |
622 |
266 |
none -> []; |
623 |
|
_Else -> |
624 |
4 |
[#xmlel{name = <<"reason">>, |
625 |
|
children = [#xmlcdata{content = Reason}]}] |
626 |
|
end}, StateData), |
627 |
270 |
?LOG_INFO(ls(#{what => muc_room_destroyed, text => <<"Destroyed MUC room">>, |
628 |
270 |
reason => Reason}, StateData)), |
629 |
270 |
add_to_log(room_existence, destroyed, StateData), |
630 |
270 |
{stop, shutdown, StateData}; |
631 |
|
handle_event(destroy, StateName, StateData) -> |
632 |
266 |
handle_event({destroy, none}, StateName, StateData); |
633 |
|
|
634 |
|
handle_event({set_affiliations, Affiliations}, |
635 |
|
#state{hibernate_timeout = Timeout} = StateName, StateData) -> |
636 |
:-( |
{next_state, StateName, StateData#state{affiliations = Affiliations}, Timeout}; |
637 |
|
|
638 |
|
handle_event(_Event, StateName, #state{hibernate_timeout = Timeout} = StateData) -> |
639 |
:-( |
{next_state, StateName, StateData, Timeout}. |
640 |
|
|
641 |
|
handle_sync_event({get_disco_item, JID, Lang}, _From, StateName, StateData) -> |
642 |
158 |
Reply = get_roomdesc_reply(JID, StateData, |
643 |
|
get_roomdesc_tail(StateData, Lang)), |
644 |
158 |
reply_with_timeout(Reply, StateName, StateData); |
645 |
|
handle_sync_event(get_config, _From, StateName, StateData) -> |
646 |
15 |
reply_with_timeout({ok, StateData#state.config}, StateName, StateData); |
647 |
|
handle_sync_event(get_state, _From, StateName, StateData) -> |
648 |
:-( |
reply_with_timeout({ok, StateData}, StateName, StateData); |
649 |
|
handle_sync_event(get_room_users, _From, StateName, StateData) -> |
650 |
72 |
reply_with_timeout({ok, maps:values(StateData#state.users)}, StateName, StateData); |
651 |
|
handle_sync_event(get_room_affiliations, _From, StateName, StateData) -> |
652 |
53 |
reply_with_timeout({ok, StateData#state.affiliations}, StateName, StateData); |
653 |
|
handle_sync_event({is_room_owner, UserJID}, _From, StateName, StateData) -> |
654 |
6 |
reply_with_timeout({ok, get_affiliation(UserJID, StateData) =:= owner}, StateName, StateData); |
655 |
|
handle_sync_event({can_access_room, UserJID}, _From, StateName, StateData) -> |
656 |
454 |
reply_with_timeout({ok, can_read_conference(UserJID, StateData)}, StateName, StateData); |
657 |
|
handle_sync_event({can_access_identity, UserJID}, _From, StateName, StateData) -> |
658 |
284 |
reply_with_timeout({ok, can_user_access_identity(UserJID, StateData)}, StateName, StateData); |
659 |
|
handle_sync_event({change_config, Config}, _From, StateName, StateData) -> |
660 |
4 |
{result, [], NSD} = change_config(Config, StateData), |
661 |
4 |
reply_with_timeout({ok, NSD#state.config}, StateName, NSD); |
662 |
|
handle_sync_event({change_state, NewStateData}, _From, StateName, _StateData) -> |
663 |
:-( |
reply_with_timeout({ok, NewStateData}, StateName, NewStateData); |
664 |
|
handle_sync_event(_Event, _From, StateName, StateData) -> |
665 |
:-( |
reply_with_timeout(ok, StateName, StateData). |
666 |
|
|
667 |
|
reply_with_timeout(Reply, StateName, #state{hibernate_timeout = Timeout} = State) -> |
668 |
1046 |
{reply, Reply, StateName, State, Timeout}. |
669 |
|
|
670 |
|
code_change(_OldVsn, StateName, StateData, _Extra) -> |
671 |
:-( |
{ok, StateName, StateData}. |
672 |
|
|
673 |
|
maybe_prepare_room_queue(RoomQueue, StateData) -> |
674 |
:-( |
StateData1 = StateData#state{room_queue = RoomQueue}, |
675 |
:-( |
case queue:is_empty(StateData#state.room_queue) of |
676 |
|
true -> |
677 |
:-( |
StateData2 = prepare_room_queue(StateData1), |
678 |
:-( |
next_normal_state(StateData2); |
679 |
|
_ -> |
680 |
:-( |
next_normal_state(StateData1) |
681 |
|
end. |
682 |
|
|
683 |
|
-type info_msg() :: {process_user_presence | process_user_message, jid:jid()} |
684 |
|
| process_room_queue. |
685 |
|
-spec handle_info(info_msg(), statename(), state()) -> fsm_return(). |
686 |
|
handle_info({process_user_presence, From}, normal_state = _StateName, StateData) -> |
687 |
:-( |
RoomQueue = queue:in({presence, From}, StateData#state.room_queue), |
688 |
:-( |
maybe_prepare_room_queue(RoomQueue, StateData); |
689 |
|
handle_info({process_user_message, From}, normal_state = _StateName, StateData) -> |
690 |
:-( |
RoomQueue = queue:in({message, From}, StateData#state.room_queue), |
691 |
:-( |
maybe_prepare_room_queue(RoomQueue, StateData); |
692 |
|
handle_info(process_room_queue, normal_state, StateData) -> |
693 |
:-( |
case queue:out(StateData#state.room_queue) of |
694 |
|
{{value, {message, From}}, RoomQueue} -> |
695 |
:-( |
Activity = get_user_activity(From, StateData), |
696 |
:-( |
Packet = Activity#activity.message, |
697 |
:-( |
NewActivity = Activity#activity{message = undefined}, |
698 |
:-( |
StateData1 = |
699 |
|
store_user_activity( |
700 |
|
From, NewActivity, StateData), |
701 |
:-( |
StateData2 = |
702 |
|
StateData1#state{ |
703 |
|
room_queue = RoomQueue}, |
704 |
:-( |
StateData3 = prepare_room_queue(StateData2), |
705 |
:-( |
process_groupchat_message(From, Packet, StateData3); |
706 |
|
{{value, {presence, From}}, RoomQueue} -> |
707 |
:-( |
Activity = get_user_activity(From, StateData), |
708 |
:-( |
{Nick, Packet} = Activity#activity.presence, |
709 |
:-( |
NewActivity = Activity#activity{presence = undefined}, |
710 |
:-( |
StateData1 = |
711 |
|
store_user_activity( |
712 |
|
From, NewActivity, StateData), |
713 |
:-( |
StateData2 = |
714 |
|
StateData1#state{ |
715 |
|
room_queue = RoomQueue}, |
716 |
:-( |
StateData3 = prepare_room_queue(StateData2), |
717 |
:-( |
process_presence(From, Nick, Packet, StateData3); |
718 |
|
{empty, _} -> |
719 |
:-( |
next_normal_state(StateData) |
720 |
|
end; |
721 |
|
handle_info({'EXIT', FromPid, _Reason}, StateName, StateData) -> |
722 |
1 |
AuthPids = StateData#state.http_auth_pids, |
723 |
1 |
StateWithoutPid = StateData#state{http_auth_pids = lists:delete(FromPid, AuthPids)}, |
724 |
1 |
destroy_temporary_room_if_empty(StateWithoutPid, StateName); |
725 |
|
handle_info(stop_persistent_room_process, normal_state, |
726 |
|
#state{room = RoomName, |
727 |
|
config = #config{persistent = true}} = StateData) -> |
728 |
60 |
maybe_stop_persistent_room(RoomName, is_empty_room(StateData), StateData); |
729 |
|
handle_info(_Info, StateName, #state{hibernate_timeout = Timeout} = StateData) -> |
730 |
21 |
{next_state, StateName, StateData, Timeout}. |
731 |
|
|
732 |
|
maybe_stop_persistent_room(RoomName, true, State) -> |
733 |
49 |
do_stop_persistent_room(RoomName, State); |
734 |
|
maybe_stop_persistent_room(RoomName, _, State) -> |
735 |
11 |
stop_if_only_owner_is_online(RoomName, count_users(State), State). |
736 |
|
|
737 |
|
stop_if_only_owner_is_online(RoomName, 1, #state{users = Users, jid = RoomJID} = State) -> |
738 |
3 |
[{LJID, #user{jid = LastUser, nick = Nick}}] = maps:to_list(Users), |
739 |
|
|
740 |
3 |
case get_affiliation(LastUser, State) of |
741 |
|
owner -> |
742 |
3 |
ItemAttrs = [{<<"affiliation">>, <<"owner">>}, {<<"role">>, <<"none">>}], |
743 |
3 |
Packet = unavailable_presence(ItemAttrs, <<"Room hibernation">>), |
744 |
3 |
FromRoom = jid:replace_resource(RoomJID, Nick), |
745 |
3 |
ejabberd_router:route(FromRoom, LastUser, Packet), |
746 |
3 |
tab_remove_online_user(LJID, State), |
747 |
3 |
do_stop_persistent_room(RoomName, State); |
748 |
|
_ -> |
749 |
:-( |
next_normal_state(State) |
750 |
|
end; |
751 |
|
stop_if_only_owner_is_online(_, _, State) -> |
752 |
8 |
next_normal_state(State). |
753 |
|
|
754 |
|
do_stop_persistent_room(_RoomName, State) -> |
755 |
52 |
?LOG_INFO(ls(#{what => muc_room_stopping_persistent, |
756 |
52 |
text => <<"Stopping persistent room's process">>}, State)), |
757 |
52 |
mongoose_metrics:update(global, [mod_muc, deep_hibernations], 1), |
758 |
52 |
{stop, normal, State}. |
759 |
|
|
760 |
|
%% @doc Purpose: Shutdown the fsm |
761 |
|
-spec terminate(any(), statename(), state()) -> 'ok'. |
762 |
|
terminate(Reason, _StateName, StateData) -> |
763 |
589 |
?LOG_INFO(ls(#{what => muc_room_stopping, text => <<"Stopping room's process">>, |
764 |
589 |
reason => Reason}, StateData)), |
765 |
589 |
ReasonT = case Reason of |
766 |
270 |
shutdown -> <<"You are being removed from the room because of a system shutdown">>; |
767 |
319 |
_ -> <<"Room terminates">> |
768 |
|
end, |
769 |
589 |
ItemAttrs = [{<<"affiliation">>, <<"none">>}, {<<"role">>, <<"none">>}], |
770 |
589 |
Packet = unavailable_presence(ItemAttrs, ReasonT), |
771 |
589 |
maps_foreach( |
772 |
|
fun(LJID, Info) -> |
773 |
37 |
Nick = Info#user.nick, |
774 |
37 |
case Reason of |
775 |
|
shutdown -> |
776 |
17 |
ejabberd_router:route( |
777 |
|
jid:replace_resource(StateData#state.jid, Nick), |
778 |
|
Info#user.jid, |
779 |
|
Packet); |
780 |
20 |
_ -> ok |
781 |
|
end, |
782 |
37 |
tab_remove_online_user(LJID, StateData) |
783 |
|
end, StateData#state.users), |
784 |
589 |
add_to_log(room_existence, stopped, StateData), |
785 |
589 |
mod_muc:room_destroyed(StateData#state.host_type, |
786 |
|
StateData#state.host, |
787 |
|
StateData#state.room, self()), |
788 |
589 |
ok. |
789 |
|
|
790 |
|
%%%---------------------------------------------------------------------- |
791 |
|
%%% Internal functions |
792 |
|
%%%---------------------------------------------------------------------- |
793 |
|
|
794 |
|
unavailable_presence(ItemAttrs, ReasonT) -> |
795 |
592 |
ReasonEl = #xmlel{name = <<"reason">>, |
796 |
|
children = [#xmlcdata{content = ReasonT}]}, |
797 |
592 |
#xmlel{name = <<"presence">>, |
798 |
|
attrs = [{<<"type">>, <<"unavailable">>}], |
799 |
|
children = [#xmlel{name = <<"x">>, |
800 |
|
attrs = [{<<"xmlns">>, ?NS_MUC_USER}], |
801 |
|
children = [#xmlel{name = <<"item">>, |
802 |
|
attrs = ItemAttrs, |
803 |
|
children = [ReasonEl]}, |
804 |
|
#xmlel{name = <<"status">>, |
805 |
|
attrs = [{<<"code">>, <<"332">>}]} |
806 |
|
]}]}. |
807 |
|
|
808 |
|
-spec occupant_jid(user(), 'undefined' | jid:jid()) -> 'error' | jid:jid(). |
809 |
|
occupant_jid(#user{nick=Nick}, RoomJID) -> |
810 |
25 |
jid:replace_resource(RoomJID, Nick). |
811 |
|
|
812 |
|
|
813 |
|
-spec route(atom() | pid() | port() | {atom(), _} | {'via', _, _}, |
814 |
|
From :: jid:jid(), To :: mod_muc:nick(), Acc :: mongoose_acc:t(), |
815 |
|
Pkt :: exml:element()) -> 'ok'. |
816 |
|
route(Pid, From, ToNick, Acc, Packet) -> |
817 |
2426 |
gen_fsm_compat:send_event(Pid, {route, From, ToNick, Acc, Packet}). |
818 |
|
|
819 |
|
|
820 |
|
-spec process_groupchat_message(jid:simple_jid() | jid:jid(), |
821 |
|
exml:element(), state()) -> fsm_return(). |
822 |
|
process_groupchat_message(From, #xmlel{name = <<"message">>, |
823 |
|
attrs = Attrs} = Packet, |
824 |
|
StateData) -> |
825 |
392 |
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), |
826 |
392 |
case can_send_to_conference(From, StateData) of |
827 |
|
true -> |
828 |
392 |
process_message_from_allowed_user(From, Packet, StateData); |
829 |
|
false -> |
830 |
:-( |
send_error_only_occupants(<<"messages">>, Packet, Lang, |
831 |
|
StateData#state.jid, From), |
832 |
:-( |
next_normal_state(StateData) |
833 |
|
end. |
834 |
|
|
835 |
|
can_send_to_conference(From, StateData) -> |
836 |
816 |
is_user_online(From, StateData) |
837 |
|
orelse |
838 |
16 |
is_allowed_nonparticipant(From, StateData). |
839 |
|
|
840 |
|
can_read_conference(UserJID, |
841 |
|
StateData=#state{config = #config{members_only = MembersOnly, |
842 |
|
password_protected = Protected}}) -> |
843 |
454 |
Affiliation = get_affiliation(UserJID, StateData), |
844 |
|
%% In a members-only chat room, only owners, admins or members can query a room archive. |
845 |
454 |
case {MembersOnly, Protected} of |
846 |
|
{_, true} -> |
847 |
|
%% For querying password-protected room user should be a member |
848 |
|
%% or inside the room |
849 |
45 |
is_user_online(UserJID, StateData) |
850 |
|
orelse |
851 |
45 |
lists:member(Affiliation, [owner, admin, member]); |
852 |
|
{true, false} -> |
853 |
3 |
lists:member(Affiliation, [owner, admin, member]); |
854 |
|
{false, false} -> |
855 |
|
%% Outcast (banned) cannot read |
856 |
406 |
Affiliation =/= outcast |
857 |
|
end. |
858 |
|
|
859 |
|
can_user_access_identity(UserJID, StateData) -> |
860 |
284 |
is_room_non_anonymous(StateData) |
861 |
|
orelse |
862 |
45 |
is_user_moderator(UserJID, StateData). |
863 |
|
|
864 |
|
is_room_non_anonymous(StateData) -> |
865 |
284 |
not is_room_anonymous(StateData). |
866 |
|
|
867 |
|
is_room_anonymous(#state{config = #config{anonymous = IsAnon}}) -> |
868 |
284 |
IsAnon. |
869 |
|
|
870 |
|
is_user_moderator(UserJID, StateData) -> |
871 |
45 |
get_role(UserJID, StateData) =:= moderator. |
872 |
|
|
873 |
|
process_message_from_allowed_user(From, #xmlel{attrs = Attrs} = Packet, |
874 |
|
StateData) -> |
875 |
392 |
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), |
876 |
392 |
{FromNick, Role} = get_participant_data(From, StateData), |
877 |
392 |
CanSendBroadcasts = can_send_broadcasts(Role, StateData), |
878 |
392 |
case CanSendBroadcasts of |
879 |
|
true -> |
880 |
392 |
{NewState, Changed} = change_subject_if_allowed(FromNick, Role, |
881 |
|
Packet, StateData), |
882 |
392 |
case Changed of |
883 |
|
true -> |
884 |
391 |
broadcast_room_packet(From, FromNick, Role, Packet, NewState); |
885 |
|
false -> |
886 |
1 |
change_subject_error(From, FromNick, Packet, Lang, NewState), |
887 |
1 |
next_normal_state(NewState) |
888 |
|
end; |
889 |
|
false -> |
890 |
:-( |
ErrText = <<"Visitors are not allowed to send messages to all occupants">>, |
891 |
:-( |
Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:forbidden(Lang, ErrText)), |
892 |
:-( |
ejabberd_router:route(StateData#state.jid, From, Err), |
893 |
:-( |
next_normal_state(StateData) |
894 |
|
end. |
895 |
|
|
896 |
|
can_send_broadcasts(Role, StateData) -> |
897 |
|
(Role == moderator) |
898 |
|
or (Role == participant) |
899 |
392 |
or ((StateData#state.config)#config.moderated == false). |
900 |
|
|
901 |
|
broadcast_room_packet(From, FromNick, Role, Packet, StateData) -> |
902 |
391 |
Activity = get_user_activity(From, StateData), |
903 |
391 |
TS = Activity#activity.message_time, |
904 |
391 |
Affiliation = get_affiliation(From, StateData), |
905 |
391 |
EventData = #{from_nick => FromNick, from_jid => From, |
906 |
|
room_jid => StateData#state.jid, role => Role, |
907 |
|
affiliation => Affiliation, timestamp => TS}, |
908 |
391 |
FilteredPacket = mongoose_hooks:filter_room_packet( |
909 |
|
StateData#state.host_type, Packet, EventData), |
910 |
391 |
RouteFrom = jid:replace_resource(StateData#state.jid, |
911 |
|
FromNick), |
912 |
391 |
RoomJid = StateData#state.jid, |
913 |
391 |
HookInfo = #{host_type => StateData#state.host_type, |
914 |
|
room_jid => RoomJid, |
915 |
|
from_jid => From, |
916 |
|
from_room_jid => RouteFrom, |
917 |
|
packet => FilteredPacket, |
918 |
|
affiliations_map => StateData#state.affiliations}, |
919 |
391 |
run_update_inbox_for_muc_hook(StateData#state.host_type, HookInfo), |
920 |
391 |
maps_foreach(fun(_LJID, Info) -> |
921 |
802 |
ejabberd_router:route(RouteFrom, |
922 |
|
Info#user.jid, |
923 |
|
FilteredPacket) |
924 |
|
end, StateData#state.users), |
925 |
391 |
NewStateData2 = add_message_to_history(FromNick, |
926 |
|
From, |
927 |
|
FilteredPacket, |
928 |
|
StateData), |
929 |
391 |
next_normal_state(NewStateData2). |
930 |
|
|
931 |
|
-spec run_update_inbox_for_muc_hook(mongooseim:host_type(), |
932 |
|
update_inbox_for_muc_payload()) -> ok. |
933 |
|
run_update_inbox_for_muc_hook(HostType, HookInfo) -> |
934 |
391 |
mongoose_hooks:update_inbox_for_muc(HostType, HookInfo), |
935 |
391 |
ok. |
936 |
|
|
937 |
|
change_subject_error(From, FromNick, Packet, Lang, StateData) -> |
938 |
1 |
Err = case (StateData#state.config)#config.allow_change_subj of |
939 |
:-( |
true -> mongoose_xmpp_errors:forbidden(Lang, <<"Only moderators and participants are allowed" |
940 |
|
" to change the subject in this room">>); |
941 |
1 |
_ -> mongoose_xmpp_errors:forbidden(Lang, <<"Only moderators are allowed" |
942 |
|
" to change the subject in this room">>) |
943 |
|
end, |
944 |
1 |
ejabberd_router:route(jid:replace_resource(StateData#state.jid, |
945 |
|
FromNick), |
946 |
|
From, |
947 |
|
jlib:make_error_reply(Packet, Err)). |
948 |
|
|
949 |
|
change_subject_if_allowed(FromNick, Role, Packet, StateData) -> |
950 |
392 |
case check_subject(Packet) of |
951 |
|
false -> |
952 |
389 |
{StateData, true}; |
953 |
|
Subject -> |
954 |
3 |
case can_change_subject(Role, StateData) of |
955 |
|
true -> |
956 |
2 |
NSD = StateData#state{subject = Subject, |
957 |
|
subject_author = FromNick, |
958 |
|
subject_timestamp = get_current_timestamp()}, |
959 |
2 |
save_persistent_room_state(NSD), |
960 |
2 |
{NSD, true}; |
961 |
|
_ -> |
962 |
1 |
{StateData, false} |
963 |
|
end |
964 |
|
end. |
965 |
|
|
966 |
|
save_persistent_room_state(StateData) -> |
967 |
682 |
case (StateData#state.config)#config.persistent of |
968 |
|
true -> |
969 |
382 |
mod_muc:store_room(StateData#state.host_type, |
970 |
|
StateData#state.host, |
971 |
|
StateData#state.room, |
972 |
|
make_opts(StateData)); |
973 |
|
_ -> |
974 |
300 |
ok |
975 |
|
end. |
976 |
|
|
977 |
|
%% @doc Check if this non participant can send message to room. |
978 |
|
%% |
979 |
|
%% XEP-0045 v1.23: |
980 |
|
%% 7.9 Sending a Message to All Occupants |
981 |
|
%% an implementation MAY allow users with certain privileges |
982 |
|
%% (e.g., a room owner, room admin, or service-level admin) |
983 |
|
%% to send messages to the room even if those users are not occupants. |
984 |
|
-spec is_allowed_nonparticipant(jid:jid(), state()) -> boolean(). |
985 |
|
is_allowed_nonparticipant(JID, StateData) -> |
986 |
16 |
get_service_affiliation(JID, StateData) =:= owner. |
987 |
|
|
988 |
|
%% @doc Get information of this participant, or default values. |
989 |
|
%% If the JID is not a participant, return values for a service message. |
990 |
|
-spec get_participant_data(jid:simple_jid() | jid:jid(), state()) -> {_, _}. |
991 |
|
get_participant_data(From, StateData) -> |
992 |
394 |
case maps:find(jid:to_lower(From), StateData#state.users) of |
993 |
|
{ok, #user{nick = FromNick, role = Role}} -> |
994 |
394 |
{FromNick, Role}; |
995 |
|
error -> |
996 |
:-( |
{<<>>, moderator} |
997 |
|
end. |
998 |
|
|
999 |
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
1000 |
|
%% Presence processing |
1001 |
|
|
1002 |
|
%% @doc Process presence stanza and destroy the room, if it is empty. |
1003 |
|
-spec process_presence(From :: jid:jid(), Nick :: mod_muc:nick(), |
1004 |
|
Packet :: exml:element(), state()) -> fsm_return(). |
1005 |
|
process_presence(From, ToNick, Presence, StateData) -> |
1006 |
1370 |
StateData1 = process_presence1(From, ToNick, Presence, StateData), |
1007 |
1369 |
destroy_temporary_room_if_empty(StateData1, normal_state). |
1008 |
|
|
1009 |
|
|
1010 |
|
-spec process_presence(From :: jid:jid(), Nick :: mod_muc:nick(), |
1011 |
|
Presence :: exml:element(), state(), statename()) -> fsm_return(). |
1012 |
|
process_presence(From, ToNick, Presence, StateData, NextState) -> |
1013 |
42 |
StateData1 = process_presence(From, ToNick, Presence, StateData), |
1014 |
42 |
rewrite_next_state(NextState, StateData1). |
1015 |
|
|
1016 |
|
|
1017 |
|
-spec rewrite_next_state(statename(), fsm_return()) -> fsm_return(). |
1018 |
|
rewrite_next_state(NewState, {next_state, _, StateData, Timeout}) -> |
1019 |
36 |
{next_state, NewState, StateData, Timeout}; |
1020 |
|
rewrite_next_state(NewState, {next_state, _, StateData}) -> |
1021 |
:-( |
{next_state, NewState, StateData}; |
1022 |
|
rewrite_next_state(_, {stop, normal, StateData}) -> |
1023 |
6 |
{stop, normal, StateData}. |
1024 |
|
|
1025 |
|
|
1026 |
|
-spec destroy_temporary_room_if_empty(state(), atom()) -> fsm_return(). |
1027 |
|
destroy_temporary_room_if_empty(StateData=#state{config=C=#config{}}, NextState) -> |
1028 |
1370 |
case (not C#config.persistent) andalso is_empty_room(StateData) |
1029 |
155 |
andalso StateData#state.http_auth_pids =:= [] of |
1030 |
|
true -> |
1031 |
155 |
?LOG_INFO(ls(#{what => muc_empty_room_destroyed, |
1032 |
|
text => <<"Destroyed MUC room because it's temporary and empty">>}, |
1033 |
155 |
StateData)), |
1034 |
155 |
add_to_log(room_existence, destroyed, StateData), |
1035 |
155 |
{stop, normal, StateData}; |
1036 |
|
_ -> |
1037 |
1215 |
case NextState of |
1038 |
|
normal_state -> |
1039 |
1215 |
next_normal_state(StateData); |
1040 |
|
_ -> |
1041 |
:-( |
{next_state, NextState, StateData} |
1042 |
|
end |
1043 |
|
end. |
1044 |
|
|
1045 |
|
next_normal_state(#state{hibernate_timeout = Timeout} = StateData) -> |
1046 |
2654 |
{next_state, normal_state, StateData, Timeout}. |
1047 |
|
|
1048 |
|
-spec process_presence1(From, Nick, Packet, state()) -> state() when |
1049 |
|
From :: jid:jid(), |
1050 |
|
Nick :: mod_muc:nick(), |
1051 |
|
Packet :: exml:element(). |
1052 |
|
process_presence1(From, Nick, #xmlel{name = <<"presence">>, attrs = Attrs} = Packet, |
1053 |
|
StateData = #state{}) -> |
1054 |
1370 |
Type = xml:get_attr_s(<<"type">>, Attrs), |
1055 |
1370 |
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), |
1056 |
1370 |
case Type of |
1057 |
|
<<"unavailable">> -> |
1058 |
631 |
process_presence_unavailable(From, Packet, StateData); |
1059 |
|
<<"error">> -> |
1060 |
1 |
process_presence_error(From, Packet, Lang, StateData); |
1061 |
|
<<>> -> |
1062 |
738 |
case is_new_nick_of_online_user(From, Nick, StateData) of |
1063 |
|
true -> |
1064 |
2 |
process_presence_nick_change(From, Nick, Packet, Lang, StateData); |
1065 |
|
false -> |
1066 |
2 |
process_simple_presence(From, Packet, StateData); |
1067 |
|
user_is_offline -> |
1068 |
|
%% at this point we know that the presence has no type |
1069 |
|
%% (user wants to enter the room) |
1070 |
|
%% and that the user is not alredy online |
1071 |
734 |
handle_new_user(From, Nick, Packet, StateData, Attrs) |
1072 |
|
end; |
1073 |
|
_NotOnline -> |
1074 |
:-( |
StateData |
1075 |
|
end. |
1076 |
|
|
1077 |
|
|
1078 |
|
-spec process_simple_presence(jid:jid(), exml:element(), state()) -> state(). |
1079 |
|
process_simple_presence(From, Packet, StateData) -> |
1080 |
2 |
NewPacket = check_and_strip_visitor_status(From, Packet, StateData), |
1081 |
2 |
NewState = add_user_presence(From, NewPacket, StateData), |
1082 |
2 |
send_new_presence(From, NewState), |
1083 |
2 |
NewState. |
1084 |
|
|
1085 |
|
|
1086 |
|
-spec process_presence_error(jid:simple_jid() | jid:jid(), |
1087 |
|
exml:element(), ejabberd:lang(), state()) -> state(). |
1088 |
|
process_presence_error(From, Packet, Lang, StateData) -> |
1089 |
1 |
case is_user_online(From, StateData) of |
1090 |
|
true -> |
1091 |
1 |
ErrorText |
1092 |
|
= <<"This participant is kicked from the room because he sent an error presence">>, |
1093 |
1 |
expulse_participant(Packet, From, StateData, translate:translate(Lang, ErrorText)); |
1094 |
|
_ -> |
1095 |
:-( |
StateData |
1096 |
|
end. |
1097 |
|
|
1098 |
|
|
1099 |
|
-spec process_presence_unavailable(jid:jid(), exml:element(), state()) |
1100 |
|
-> state(). |
1101 |
|
process_presence_unavailable(From, Packet, StateData) -> |
1102 |
631 |
case is_user_online(From, StateData) of |
1103 |
|
true -> |
1104 |
631 |
NewPacket = check_and_strip_visitor_status(From, Packet, StateData), |
1105 |
631 |
NewState = add_user_presence_un(From, NewPacket, StateData), |
1106 |
631 |
send_new_presence_un(From, NewState), |
1107 |
630 |
Reason = case xml:get_subtag(NewPacket, <<"status">>) of |
1108 |
20 |
false -> <<>>; |
1109 |
610 |
StatusEl -> xml:get_tag_cdata(StatusEl) |
1110 |
|
end, |
1111 |
630 |
remove_online_user(From, NewState, Reason); |
1112 |
|
_ -> |
1113 |
:-( |
StateData |
1114 |
|
end. |
1115 |
|
|
1116 |
|
|
1117 |
|
-spec choose_nick_change_strategy(jid:jid(), binary(), state()) |
1118 |
|
-> 'allowed' | 'conflict_registered' | 'conflict_use' | 'not_allowed_visitor'. |
1119 |
|
choose_nick_change_strategy(From, Nick, StateData) -> |
1120 |
2 |
case {is_nick_exists(Nick, StateData), |
1121 |
|
mod_muc:can_use_nick(StateData#state.host_type, StateData#state.host, From, Nick), |
1122 |
|
(StateData#state.config)#config.allow_visitor_nickchange, |
1123 |
|
is_visitor(From, StateData)} of |
1124 |
|
{_, _, false, true} -> |
1125 |
:-( |
not_allowed_visitor; |
1126 |
|
{true, _, _, _} -> |
1127 |
1 |
conflict_use; |
1128 |
|
{_, false, _, _} -> |
1129 |
:-( |
conflict_registered; |
1130 |
|
_ -> |
1131 |
1 |
allowed |
1132 |
|
end. |
1133 |
|
|
1134 |
|
|
1135 |
|
-spec process_presence_nick_change(jid:jid(), mod_muc:nick(), exml:element(), |
1136 |
|
ejabberd:lang(), state()) -> state(). |
1137 |
|
process_presence_nick_change(From, Nick, Packet, Lang, StateData) -> |
1138 |
2 |
case choose_nick_change_strategy(From, Nick, StateData) of |
1139 |
|
not_allowed_visitor -> |
1140 |
:-( |
ErrText = <<"Visitors are not allowed to change their nicknames in this room">>, |
1141 |
:-( |
Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:not_allowed(Lang, ErrText)), |
1142 |
:-( |
route_error(Nick, From, Err, StateData); |
1143 |
|
conflict_use -> |
1144 |
1 |
ErrText = <<"That nickname is already in use by another occupant">>, |
1145 |
1 |
Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:conflict(Lang, ErrText)), |
1146 |
1 |
route_error(Nick, From, Err, StateData); |
1147 |
|
conflict_registered -> |
1148 |
:-( |
ErrText = <<"That nickname is registered by another person">>, |
1149 |
:-( |
Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:conflict(Lang, ErrText)), |
1150 |
:-( |
route_error(Nick, From, Err, StateData); |
1151 |
|
allowed -> |
1152 |
1 |
change_nick(From, Nick, StateData) |
1153 |
|
end. |
1154 |
|
|
1155 |
|
|
1156 |
|
-spec check_and_strip_visitor_status(jid:jid(), exml:element(), state()) |
1157 |
|
-> exml:element(). |
1158 |
|
check_and_strip_visitor_status(From, Packet, StateData) -> |
1159 |
633 |
case {(StateData#state.config)#config.allow_visitor_status, |
1160 |
|
is_visitor(From, StateData)} of |
1161 |
|
{false, true} -> |
1162 |
:-( |
strip_status(Packet); |
1163 |
|
_ -> |
1164 |
633 |
Packet |
1165 |
|
end. |
1166 |
|
|
1167 |
|
|
1168 |
|
-spec handle_new_user(jid:jid(), mod_muc:nick(), exml:element(), state(), |
1169 |
|
[{binary(), binary()}]) -> state(). |
1170 |
|
handle_new_user(From, Nick = <<>>, _Packet, StateData, Attrs) -> |
1171 |
1 |
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), |
1172 |
1 |
ErrText = <<"No nickname">>, |
1173 |
1 |
Error =jlib:make_error_reply( |
1174 |
|
#xmlel{name = <<"presence">>}, |
1175 |
|
mongoose_xmpp_errors:jid_malformed(Lang, ErrText)), |
1176 |
|
%ejabberd_route(From, To, Packet), |
1177 |
1 |
ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), From, Error), |
1178 |
1 |
StateData; |
1179 |
|
handle_new_user(From, Nick, Packet, StateData, Attrs) -> |
1180 |
733 |
case exml_query:path(Packet, [{element, <<"x">>}]) of |
1181 |
|
undefined -> |
1182 |
1 |
Response = kick_stanza_for_old_protocol(Attrs), |
1183 |
1 |
ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), From, Response); |
1184 |
|
_ -> |
1185 |
732 |
add_new_user(From, Nick, Packet, StateData) |
1186 |
|
end. |
1187 |
|
|
1188 |
|
|
1189 |
|
-spec is_user_online(jid:simple_jid() | jid:jid(), state()) -> boolean(). |
1190 |
|
is_user_online(JID, StateData) -> |
1191 |
1527 |
LJID = jid:to_lower(JID), |
1192 |
1527 |
maps:is_key(LJID, StateData#state.users). |
1193 |
|
|
1194 |
|
|
1195 |
|
%% @doc Check if the user is occupant of the room, or at least is an admin |
1196 |
|
%% or owner. |
1197 |
|
-spec is_occupant_or_admin(jid:jid(), state()) -> boolean(). |
1198 |
|
is_occupant_or_admin(JID, StateData) -> |
1199 |
159 |
FAffiliation = get_affiliation(JID, StateData), |
1200 |
159 |
FRole = get_role(JID, StateData), |
1201 |
159 |
(FRole /= none) orelse |
1202 |
154 |
(FAffiliation == admin) orelse |
1203 |
154 |
(FAffiliation == owner). |
1204 |
|
|
1205 |
|
%%% |
1206 |
|
%%% Handle IQ queries of vCard |
1207 |
|
%%% |
1208 |
|
|
1209 |
|
-spec is_user_online_iq(_, jid:jid(), state()) |
1210 |
|
-> {'false', _, jid:jid()} | {'true', _, jid:jid()}. |
1211 |
|
is_user_online_iq(StanzaId, JID, StateData) when JID#jid.lresource /= <<>> -> |
1212 |
:-( |
{is_user_online(JID, StateData), StanzaId, JID}; |
1213 |
|
is_user_online_iq(StanzaId, JID, StateData) when JID#jid.lresource == <<>> -> |
1214 |
:-( |
try stanzaid_unpack(StanzaId) of |
1215 |
|
{OriginalId, Resource} -> |
1216 |
:-( |
JIDWithResource = jid:replace_resource(JID, Resource), |
1217 |
:-( |
{is_user_online(JIDWithResource, StateData), |
1218 |
|
OriginalId, JIDWithResource} |
1219 |
|
catch |
1220 |
|
_:_ -> |
1221 |
:-( |
{is_user_online(JID, StateData), StanzaId, JID} |
1222 |
|
end. |
1223 |
|
|
1224 |
|
|
1225 |
|
-spec handle_iq_vcard(jid:jid(), jid:simple_jid() | jid:jid(), |
1226 |
|
binary(), any(), exml:element()) -> |
1227 |
|
{jid:simple_jid() | jid:jid(), exml:element()}. |
1228 |
|
handle_iq_vcard(FromFull, ToJID, StanzaId, NewId, Packet) -> |
1229 |
:-( |
ToBareJID = jid:to_bare(ToJID), |
1230 |
:-( |
IQ = jlib:iq_query_info(Packet), |
1231 |
:-( |
handle_iq_vcard2(FromFull, ToJID, ToBareJID, StanzaId, NewId, IQ, Packet). |
1232 |
|
|
1233 |
|
|
1234 |
|
-spec handle_iq_vcard2(FromFull :: jid:jid(), |
1235 |
|
ToJID :: jid:simple_jid() | jid:jid(), |
1236 |
|
ToBareJID :: jid:simple_jid() | jid:jid(), |
1237 |
|
binary(), _NewID, 'invalid' | 'not_iq' | 'reply' | jlib:iq(), |
1238 |
|
exml:element()) -> {jid:simple_jid() | jid:jid(), exml:element()}. |
1239 |
|
handle_iq_vcard2(_FromFull, ToJID, ToBareJID, StanzaId, _NewId, |
1240 |
|
#iq{type = get, xmlns = ?NS_VCARD}, Packet) |
1241 |
|
when ToBareJID /= ToJID -> |
1242 |
:-( |
{ToBareJID, change_stanzaid(StanzaId, ToJID, Packet)}; |
1243 |
|
handle_iq_vcard2(_FromFull, ToJID, _ToBareJID, _StanzaId, NewId, _IQ, Packet) -> |
1244 |
:-( |
{ToJID, change_stanzaid(NewId, Packet)}. |
1245 |
|
|
1246 |
|
|
1247 |
|
-spec stanzaid_pack(binary(), jid:resource()) -> binary(). |
1248 |
|
stanzaid_pack(OriginalId, Resource) -> |
1249 |
:-( |
Data64 = base64:encode(<<"ejab\0", OriginalId/binary, 0, Resource/binary>>), |
1250 |
:-( |
<<"berd", Data64/binary>>. |
1251 |
|
|
1252 |
|
|
1253 |
|
-spec stanzaid_unpack(binary()) -> stanzaid(). |
1254 |
|
stanzaid_unpack(<<"berd", StanzaIdBase64/binary>>) -> |
1255 |
:-( |
StanzaId = base64:decode(StanzaIdBase64), |
1256 |
:-( |
[<<"ejab">>, OriginalId, Resource] = binary:split(StanzaId, <<"\0">>), |
1257 |
:-( |
{OriginalId, Resource}. |
1258 |
|
|
1259 |
|
|
1260 |
|
-spec change_stanzaid(binary(), exml:element()) -> exml:element(). |
1261 |
|
change_stanzaid(NewId, Packet) -> |
1262 |
:-( |
XE = #xmlel{attrs = Attrs} = jlib:remove_attr(<<"id">>, Packet), |
1263 |
:-( |
XE#xmlel{attrs = [{<<"id">>, NewId} | Attrs]}. |
1264 |
|
change_stanzaid(PreviousId, ToJID, Packet) -> |
1265 |
:-( |
NewId = stanzaid_pack(PreviousId, ToJID#jid.lresource), |
1266 |
:-( |
change_stanzaid(NewId, Packet). |
1267 |
|
|
1268 |
|
%%% |
1269 |
|
%%% |
1270 |
|
|
1271 |
|
-spec role_to_binary(mod_muc:role()) -> binary(). |
1272 |
|
role_to_binary(Role) -> |
1273 |
1821 |
case Role of |
1274 |
631 |
moderator -> <<"moderator">>; |
1275 |
533 |
participant -> <<"participant">>; |
1276 |
22 |
visitor -> <<"visitor">>; |
1277 |
635 |
none -> <<"none">> |
1278 |
|
end. |
1279 |
|
|
1280 |
|
-spec affiliation_to_binary(mod_muc:affiliation()) -> binary(). |
1281 |
|
affiliation_to_binary(Affiliation) -> |
1282 |
1836 |
case Affiliation of |
1283 |
900 |
owner -> <<"owner">>; |
1284 |
10 |
admin -> <<"admin">>; |
1285 |
85 |
member -> <<"member">>; |
1286 |
6 |
outcast -> <<"outcast">>; |
1287 |
835 |
none -> <<"none">> |
1288 |
|
end. |
1289 |
|
|
1290 |
|
-spec binary_to_role(binary()) -> mod_muc:role(). |
1291 |
|
binary_to_role(Role) -> |
1292 |
65 |
case Role of |
1293 |
25 |
<<"moderator">> -> moderator; |
1294 |
21 |
<<"participant">> -> participant; |
1295 |
11 |
<<"visitor">> -> visitor; |
1296 |
7 |
<<"none">> -> none |
1297 |
|
end. |
1298 |
|
|
1299 |
|
-spec binary_to_affiliation(binary()) -> mod_muc:affiliation(). |
1300 |
|
binary_to_affiliation(Affiliation) -> |
1301 |
128 |
case Affiliation of |
1302 |
12 |
<<"owner">> -> owner; |
1303 |
15 |
<<"admin">> -> admin; |
1304 |
70 |
<<"member">> -> member; |
1305 |
14 |
<<"outcast">> -> outcast; |
1306 |
16 |
<<"none">> -> none |
1307 |
|
end. |
1308 |
|
|
1309 |
|
|
1310 |
|
%% @doc Decide the fate of the message and its sender |
1311 |
|
%% Returns: continue_delivery | forget_message | {expulse_sender, Reason} |
1312 |
|
-spec decide_fate_message(binary(), exml:element(), jid:simple_jid() | jid:jid(), |
1313 |
|
state()) -> 'continue_delivery' |
1314 |
|
| 'forget_message' |
1315 |
|
| {'expulse_sender', string()}. |
1316 |
|
decide_fate_message(<<"error">>, Packet, From, StateData) -> |
1317 |
|
%% Make a preliminary decision |
1318 |
:-( |
PD = case check_error_kick(Packet) of |
1319 |
|
%% If this is an error stanza and its condition matches a criteria |
1320 |
|
true -> |
1321 |
:-( |
Reason = "This participant is considered a ghost and is expulsed: " ++ |
1322 |
|
binary_to_list(jid:to_binary(From)), |
1323 |
:-( |
{expulse_sender, Reason}; |
1324 |
|
false -> |
1325 |
:-( |
continue_delivery |
1326 |
|
end, |
1327 |
:-( |
case PD of |
1328 |
|
{expulse_sender, R} -> |
1329 |
:-( |
case is_user_online(From, StateData) of |
1330 |
|
true -> |
1331 |
:-( |
{expulse_sender, R}; |
1332 |
|
false -> |
1333 |
:-( |
forget_message |
1334 |
|
end; |
1335 |
|
Other -> |
1336 |
:-( |
Other |
1337 |
|
end; |
1338 |
|
decide_fate_message(_, _, _, _) -> |
1339 |
31 |
continue_delivery. |
1340 |
|
|
1341 |
|
|
1342 |
|
%% @doc Check if the elements of this error stanza indicate |
1343 |
|
%% that the sender is a dead participant. |
1344 |
|
%% If so, return true to kick the participant. |
1345 |
|
-spec check_error_kick(exml:element()) -> boolean(). |
1346 |
|
check_error_kick(Packet) -> |
1347 |
:-( |
case get_error_condition(Packet) of |
1348 |
:-( |
<<"gone">> -> true; |
1349 |
:-( |
<<"internal-server-error">> -> true; |
1350 |
:-( |
<<"item-not-found">> -> true; |
1351 |
:-( |
<<"jid-malformed">> -> true; |
1352 |
:-( |
<<"recipient-unavailable">> -> true; |
1353 |
:-( |
<<"redirect">> -> true; |
1354 |
:-( |
<<"remote-server-not-found">> -> true; |
1355 |
:-( |
<<"remote-server-timeout">> -> true; |
1356 |
:-( |
<<"service-unavailable">> -> true; |
1357 |
:-( |
_ -> false |
1358 |
|
end. |
1359 |
|
|
1360 |
|
|
1361 |
|
-spec get_error_condition(exml:element()) -> binary(). |
1362 |
|
get_error_condition(Packet) -> |
1363 |
4 |
case catch get_error_condition2(Packet) of |
1364 |
|
{condition, ErrorCondition} -> |
1365 |
3 |
ErrorCondition; |
1366 |
|
{'EXIT', _} -> |
1367 |
1 |
<<"badformed error stanza">> |
1368 |
|
end. |
1369 |
|
|
1370 |
|
|
1371 |
|
-spec get_error_condition2(exml:element()) -> {condition, binary()}. |
1372 |
|
get_error_condition2(Packet) -> |
1373 |
4 |
#xmlel{children = EEls} = xml:get_subtag(Packet, <<"error">>), |
1374 |
3 |
[Condition] = [Name || #xmlel{name = Name, |
1375 |
|
attrs = [{<<"xmlns">>, ?NS_STANZAS}], |
1376 |
3 |
children = []} <- EEls], |
1377 |
3 |
{condition, Condition}. |
1378 |
|
|
1379 |
|
|
1380 |
|
-spec expulse_participant(exml:element(), jid:jid(), state(), binary()) -> state(). |
1381 |
|
expulse_participant(Packet, From, StateData, Reason1) -> |
1382 |
4 |
ErrorCondition = get_error_condition(Packet), |
1383 |
4 |
Reason2 = <<Reason1/binary, ": ", ErrorCondition/binary>>, |
1384 |
4 |
NewState = add_user_presence_un( |
1385 |
|
From, |
1386 |
|
#xmlel{name = <<"presence">>, attrs = [{<<"type">>, <<"unavailable">>}], |
1387 |
|
children = [#xmlel{name = <<"status">>, |
1388 |
|
children = [#xmlcdata{content = Reason2}]}]}, |
1389 |
|
StateData), |
1390 |
4 |
send_new_presence_un(From, NewState), |
1391 |
4 |
remove_online_user(From, NewState). |
1392 |
|
|
1393 |
|
|
1394 |
|
-spec access_admin(state()) -> any(). |
1395 |
|
access_admin(#state{access=Access}) -> |
1396 |
5660 |
{_AccessRoute, _AccessCreate, AccessAdmin, _AccessPersistent} = Access, |
1397 |
5660 |
AccessAdmin. |
1398 |
|
|
1399 |
|
|
1400 |
|
-spec access_persistent(state()) -> any(). |
1401 |
|
access_persistent(#state{access=Access}) -> |
1402 |
21 |
{_AccessRoute, _AccessCreate, _AccessAdmin, AccessPersistent} = Access, |
1403 |
21 |
AccessPersistent. |
1404 |
|
|
1405 |
|
|
1406 |
|
-spec set_affiliation(jid:jid(), mod_muc:affiliation(), state()) -> state(). |
1407 |
|
set_affiliation(JID, Affiliation, StateData) |
1408 |
|
when is_atom(Affiliation) -> |
1409 |
595 |
LJID = jid:to_bare(jid:to_lower(JID)), |
1410 |
595 |
Affiliations = case Affiliation of |
1411 |
:-( |
none -> maps:remove(LJID, StateData#state.affiliations); |
1412 |
595 |
_ -> maps:put(LJID, Affiliation, StateData#state.affiliations) |
1413 |
|
end, |
1414 |
595 |
StateData#state{affiliations = Affiliations}. |
1415 |
|
|
1416 |
|
|
1417 |
|
-spec set_affiliation_and_reason(jid:jid(), mod_muc:affiliation(), term(), |
1418 |
|
state()) -> state(). |
1419 |
|
set_affiliation_and_reason(JID, Affiliation, Reason, StateData) |
1420 |
|
when is_atom(Affiliation) -> |
1421 |
96 |
LJID = jid:to_bare(jid:to_lower(JID)), |
1422 |
96 |
Affiliations = case Affiliation of |
1423 |
15 |
none -> maps:remove(LJID, StateData#state.affiliations); |
1424 |
81 |
_ -> maps:put(LJID, {Affiliation, Reason}, StateData#state.affiliations) |
1425 |
|
end, |
1426 |
96 |
StateData#state{affiliations = Affiliations}. |
1427 |
|
|
1428 |
|
|
1429 |
|
-spec get_affiliation(jid:jid(), state()) -> mod_muc:affiliation(). |
1430 |
|
get_affiliation(JID, StateData) -> |
1431 |
4036 |
AccessAdmin = access_admin(StateData), |
1432 |
4036 |
case acl:match_rule(StateData#state.host_type, StateData#state.server_host, AccessAdmin, JID) of |
1433 |
|
allow -> |
1434 |
:-( |
owner; |
1435 |
|
_ -> |
1436 |
4036 |
LJID = jid:to_lower(JID), |
1437 |
4036 |
LJID1 = jid:to_bare(LJID), |
1438 |
4036 |
LJID2 = setelement(1, LJID, <<>>), |
1439 |
4036 |
LJID3 = jid:to_bare(LJID2), |
1440 |
4036 |
lookup_affiliation([ LJID, LJID1, LJID2, LJID3 ], StateData#state.affiliations) |
1441 |
|
end. |
1442 |
|
|
1443 |
|
-spec lookup_affiliation(JIDs :: [jid:simple_jid()], |
1444 |
|
Affiliations :: affiliations_map()) -> |
1445 |
|
mod_muc:affiliation(). |
1446 |
|
lookup_affiliation([ JID | RJIDs ], Affiliations) -> |
1447 |
11338 |
case maps:find(JID, Affiliations) of |
1448 |
164 |
{ok, {Affiliation, _Reason}} -> Affiliation; |
1449 |
2165 |
{ok, Affiliation} -> Affiliation; |
1450 |
9009 |
_ -> lookup_affiliation(RJIDs, Affiliations) |
1451 |
|
end; |
1452 |
|
lookup_affiliation([], _Affiliations) -> |
1453 |
1707 |
none. |
1454 |
|
|
1455 |
|
-spec get_service_affiliation(jid:jid(), state()) -> mod_muc:affiliation(). |
1456 |
|
get_service_affiliation(JID, StateData) -> |
1457 |
1624 |
AccessAdmin = access_admin(StateData), |
1458 |
1624 |
case acl:match_rule(StateData#state.host_type, StateData#state.server_host, AccessAdmin, JID) of |
1459 |
|
allow -> |
1460 |
:-( |
owner; |
1461 |
|
_ -> |
1462 |
1624 |
none |
1463 |
|
end. |
1464 |
|
|
1465 |
|
|
1466 |
|
-spec set_role(JID :: jid:jid(), Role :: mod_muc:role(), state()) -> state(). |
1467 |
|
set_role(JID, none, StateData) -> |
1468 |
19 |
erase_matched_users(JID, StateData); |
1469 |
|
set_role(JID, Role, StateData) -> |
1470 |
102 |
update_matched_users(fun(User) -> User#user{role = Role} end, |
1471 |
|
JID, StateData). |
1472 |
|
|
1473 |
|
|
1474 |
|
-spec get_role( jid:jid(), state()) -> mod_muc:role(). |
1475 |
|
get_role(JID, StateData) -> |
1476 |
1166 |
LJID = jid:to_lower(JID), |
1477 |
1166 |
case maps:find(LJID, StateData#state.users) of |
1478 |
833 |
{ok, #user{role = Role}} -> Role; |
1479 |
333 |
_ -> none |
1480 |
|
end. |
1481 |
|
|
1482 |
|
|
1483 |
|
-spec get_default_role(mod_muc:affiliation(), state()) -> mod_muc:role(). |
1484 |
337 |
get_default_role(owner, _StateData) -> moderator; |
1485 |
:-( |
get_default_role(admin, _StateData) -> moderator; |
1486 |
5 |
get_default_role(member, _StateData) -> participant; |
1487 |
1 |
get_default_role(outcast, _StateData) -> none; |
1488 |
|
get_default_role(none, StateData) -> |
1489 |
389 |
case (StateData#state.config)#config.members_only of |
1490 |
1 |
true -> none; |
1491 |
|
_ -> |
1492 |
388 |
case (StateData#state.config)#config.members_by_default of |
1493 |
375 |
true -> participant; |
1494 |
13 |
_ -> visitor |
1495 |
|
end |
1496 |
|
end. |
1497 |
|
|
1498 |
|
|
1499 |
|
-spec is_visitor(jid:jid(), state()) -> boolean(). |
1500 |
|
is_visitor(Jid, StateData) -> |
1501 |
635 |
get_role(Jid, StateData) =:= visitor. |
1502 |
|
|
1503 |
|
|
1504 |
|
-spec is_empty_room(state()) -> boolean(). |
1505 |
|
is_empty_room(#state{users=Users}) -> |
1506 |
698 |
is_empty_map(Users). |
1507 |
|
|
1508 |
|
|
1509 |
|
-spec is_empty_map(map()) -> boolean(). |
1510 |
|
is_empty_map(Map) -> |
1511 |
698 |
maps:size(Map) =:= 0. |
1512 |
|
|
1513 |
|
|
1514 |
|
-spec map_foreach_value(fun((_) -> ok), users_map()) -> any(). |
1515 |
|
map_foreach_value(F, Map) -> |
1516 |
292 |
maps:fold(fun(_Key, Value, _) -> F(Value) end, ok, Map). |
1517 |
|
|
1518 |
|
|
1519 |
|
-spec count_users(state()) -> non_neg_integer(). |
1520 |
|
count_users(#state{users=Users}) -> |
1521 |
901 |
maps:size(Users). |
1522 |
|
|
1523 |
|
|
1524 |
|
-spec get_max_users(state()) -> integer() | none. |
1525 |
|
get_max_users(StateData) -> |
1526 |
743 |
MaxUsers = (StateData#state.config)#config.max_users, |
1527 |
743 |
ServiceMaxUsers = get_service_max_users(StateData), |
1528 |
743 |
case MaxUsers =< ServiceMaxUsers of |
1529 |
743 |
true -> MaxUsers; |
1530 |
:-( |
false -> ServiceMaxUsers |
1531 |
|
end. |
1532 |
|
|
1533 |
|
|
1534 |
|
-spec get_service_max_users(state()) -> integer() | none. |
1535 |
|
get_service_max_users(StateData) -> |
1536 |
816 |
get_opt(StateData, max_users). |
1537 |
|
|
1538 |
|
-spec get_max_users_admin_threshold(state()) -> integer(). |
1539 |
|
get_max_users_admin_threshold(StateData) -> |
1540 |
732 |
get_opt(StateData, max_users_admin_threshold). |
1541 |
|
|
1542 |
|
-spec get_user_activity(jid:simple_jid() | jid:jid(), state()) |
1543 |
|
-> activity(). |
1544 |
|
get_user_activity(JID, StateData) -> |
1545 |
2111 |
case treap:lookup(jid:to_lower(JID), StateData#state.activity) of |
1546 |
:-( |
{ok, _P, A} -> A; |
1547 |
|
error -> |
1548 |
2111 |
MessageShaper = mongoose_shaper:new(get_opt(StateData, user_message_shaper)), |
1549 |
2111 |
PresenceShaper = mongoose_shaper:new(get_opt(StateData, user_presence_shaper)), |
1550 |
2111 |
#activity{message_shaper = MessageShaper, |
1551 |
|
presence_shaper = PresenceShaper} |
1552 |
|
end. |
1553 |
|
|
1554 |
|
|
1555 |
|
-spec store_user_activity(jid:simple_jid() | jid:jid(), activity(), |
1556 |
|
state()) -> state(). |
1557 |
|
store_user_activity(JID, UserActivity, StateData) -> |
1558 |
1720 |
MinMessageInterval = get_opt(StateData, min_message_interval), |
1559 |
1720 |
MinPresenceInterval = get_opt(StateData, min_presence_interval), |
1560 |
1720 |
Key = jid:to_lower(JID), |
1561 |
1720 |
Now = os:system_time(microsecond), |
1562 |
1720 |
Activity1 = clean_treap(StateData#state.activity, {1, -Now}), |
1563 |
1720 |
Activity = |
1564 |
|
case treap:lookup(Key, Activity1) of |
1565 |
|
{ok, _P, _A} -> |
1566 |
:-( |
treap:delete(Key, Activity1); |
1567 |
|
error -> |
1568 |
1720 |
Activity1 |
1569 |
|
end, |
1570 |
1720 |
StateData1 = |
1571 |
:-( |
case (MinMessageInterval == 0) andalso |
1572 |
1720 |
(MinPresenceInterval == 0) andalso |
1573 |
1720 |
(UserActivity#activity.message_shaper == none) andalso |
1574 |
1720 |
(UserActivity#activity.presence_shaper == none) andalso |
1575 |
1720 |
(UserActivity#activity.message == undefined) andalso |
1576 |
1720 |
(UserActivity#activity.presence == undefined) of |
1577 |
|
true -> |
1578 |
1720 |
StateData#state{activity = Activity}; |
1579 |
|
false -> |
1580 |
:-( |
case (UserActivity#activity.message == undefined) andalso |
1581 |
:-( |
(UserActivity#activity.presence == undefined) of |
1582 |
|
true -> |
1583 |
:-( |
{_, MessageShaperInterval} = |
1584 |
|
mongoose_shaper:update(UserActivity#activity.message_shaper, |
1585 |
|
100000), |
1586 |
:-( |
{_, PresenceShaperInterval} = |
1587 |
|
mongoose_shaper:update(UserActivity#activity.presence_shaper, |
1588 |
|
100000), |
1589 |
:-( |
Delay = lists:max([MessageShaperInterval, |
1590 |
|
PresenceShaperInterval, |
1591 |
|
MinMessageInterval * 1000, |
1592 |
|
MinPresenceInterval * 1000]) * 1000, |
1593 |
:-( |
Priority = {1, -(Now + Delay)}, |
1594 |
:-( |
StateData#state{ |
1595 |
|
activity = treap:insert( |
1596 |
|
Key, |
1597 |
|
Priority, |
1598 |
|
UserActivity, |
1599 |
|
Activity)}; |
1600 |
|
false -> |
1601 |
:-( |
Priority = {0, 0}, |
1602 |
:-( |
StateData#state{ |
1603 |
|
activity = treap:insert( |
1604 |
|
Key, |
1605 |
|
Priority, |
1606 |
|
UserActivity, |
1607 |
|
Activity)} |
1608 |
|
end |
1609 |
|
end, |
1610 |
1720 |
StateData1. |
1611 |
|
|
1612 |
|
|
1613 |
|
-spec clean_treap(treap:treap(), {1, integer()}) -> treap:treap(). |
1614 |
|
clean_treap(Treap, CleanPriority) -> |
1615 |
1720 |
case treap:is_empty(Treap) of |
1616 |
|
true -> |
1617 |
1720 |
Treap; |
1618 |
|
false -> |
1619 |
:-( |
{_Key, Priority, _Value} = treap:get_root(Treap), |
1620 |
:-( |
case Priority > CleanPriority of |
1621 |
:-( |
true -> clean_treap(treap:delete_root(Treap), CleanPriority); |
1622 |
:-( |
false -> Treap |
1623 |
|
end |
1624 |
|
end. |
1625 |
|
|
1626 |
|
|
1627 |
|
-spec prepare_room_queue(state()) -> state(). |
1628 |
|
prepare_room_queue(StateData) -> |
1629 |
:-( |
case queue:out(StateData#state.room_queue) of |
1630 |
|
{{value, {message, From}}, _RoomQueue} -> |
1631 |
:-( |
Activity = get_user_activity(From, StateData), |
1632 |
:-( |
Packet = Activity#activity.message, |
1633 |
:-( |
Size = element_size(Packet), |
1634 |
:-( |
{RoomShaper, RoomShaperInterval} = |
1635 |
|
mongoose_shaper:update(StateData#state.room_shaper, Size), |
1636 |
:-( |
erlang:send_after( |
1637 |
|
RoomShaperInterval, self(), |
1638 |
|
process_room_queue), |
1639 |
:-( |
StateData#state{ |
1640 |
|
room_shaper = RoomShaper}; |
1641 |
|
{{value, {presence, From}}, _RoomQueue} -> |
1642 |
:-( |
Activity = get_user_activity(From, StateData), |
1643 |
:-( |
{_Nick, Packet} = Activity#activity.presence, |
1644 |
:-( |
Size = element_size(Packet), |
1645 |
:-( |
{RoomShaper, RoomShaperInterval} = |
1646 |
|
mongoose_shaper:update(StateData#state.room_shaper, Size), |
1647 |
:-( |
erlang:send_after( |
1648 |
|
RoomShaperInterval, self(), |
1649 |
|
process_room_queue), |
1650 |
:-( |
StateData#state{ |
1651 |
|
room_shaper = RoomShaper}; |
1652 |
|
{empty, _} -> |
1653 |
:-( |
StateData |
1654 |
|
end. |
1655 |
|
|
1656 |
|
-spec is_first_session(mod_muc:nick(), state()) -> boolean(). |
1657 |
|
is_first_session(Nick, StateData) -> |
1658 |
688 |
case maps:find(Nick, StateData#state.sessions) of |
1659 |
4 |
{ok, _Val} -> false; |
1660 |
684 |
error -> true |
1661 |
|
end. |
1662 |
|
|
1663 |
|
-spec is_last_session(mod_muc:nick(), state()) -> boolean(). |
1664 |
|
is_last_session(Nick, StateData) -> |
1665 |
1269 |
case maps:find(Nick, StateData#state.sessions) of |
1666 |
1261 |
{ok, [_Val]} -> true; |
1667 |
8 |
_ -> false |
1668 |
|
end. |
1669 |
|
|
1670 |
|
-spec add_online_user(jid:jid(), mod_muc:nick(), mod_muc:role(), state()) |
1671 |
|
-> state(). |
1672 |
|
add_online_user(JID, Nick, Role, StateData) -> |
1673 |
688 |
LJID = jid:to_lower(JID), |
1674 |
688 |
Sessions = maps_append(Nick, JID, StateData#state.sessions), |
1675 |
688 |
Info = #user{jid = JID, |
1676 |
|
nick = Nick, |
1677 |
|
role = Role}, |
1678 |
688 |
Users = maps:put(LJID, Info, StateData#state.users), |
1679 |
688 |
case is_first_session(Nick, StateData) of |
1680 |
|
true -> |
1681 |
684 |
add_to_log(join, Nick, StateData), |
1682 |
684 |
tab_add_online_user(JID, StateData), |
1683 |
684 |
run_join_room_hook(JID, StateData); |
1684 |
|
_ -> |
1685 |
4 |
ok |
1686 |
|
end, |
1687 |
688 |
notify_users_modified(StateData#state{users = Users, sessions = Sessions}). |
1688 |
|
|
1689 |
|
-spec run_join_room_hook(jid:jid(), state()) -> ok. |
1690 |
|
run_join_room_hook(JID, #state{room = Room, host = Host, jid = MucJID, server_host = ServerHost}) -> |
1691 |
684 |
mongoose_hooks:join_room(ServerHost, Room, Host, JID, MucJID), |
1692 |
684 |
ok. |
1693 |
|
|
1694 |
|
-spec remove_online_user(jid:jid(), state()) -> state(). |
1695 |
|
remove_online_user(JID, StateData) -> |
1696 |
4 |
remove_online_user(JID, StateData, <<>>). |
1697 |
|
|
1698 |
|
-spec remove_online_user(jid:jid(), state(), Reason :: binary()) -> state(). |
1699 |
|
remove_online_user(JID, StateData, Reason) -> |
1700 |
|
|
1701 |
634 |
LJID = jid:to_lower(JID), |
1702 |
634 |
{ok, #user{nick = Nick}} = |
1703 |
|
maps:find(LJID, StateData#state.users), |
1704 |
634 |
Sessions = case is_last_session(Nick, StateData) of |
1705 |
|
true -> |
1706 |
630 |
add_to_log(leave, {Nick, Reason}, StateData), |
1707 |
630 |
tab_remove_online_user(JID, StateData), |
1708 |
630 |
run_leave_room_hook(JID, StateData), |
1709 |
630 |
maps:remove(Nick, StateData#state.sessions); |
1710 |
|
false -> |
1711 |
4 |
IsOtherLJID = fun(J) -> jid:to_lower(J) /= LJID end, |
1712 |
4 |
F = fun (JIDs) -> lists:filter(IsOtherLJID, JIDs) end, |
1713 |
4 |
maps:update_with(Nick, F, StateData#state.sessions) |
1714 |
|
end, |
1715 |
634 |
Users = maps:remove(LJID, StateData#state.users), |
1716 |
|
|
1717 |
634 |
notify_users_modified(StateData#state{users = Users, sessions = Sessions}). |
1718 |
|
|
1719 |
|
-spec run_leave_room_hook(jid:jid(), state()) -> ok. |
1720 |
|
run_leave_room_hook(JID, #state{room = Room, host = Host, jid = MucJID, server_host = ServerHost}) -> |
1721 |
630 |
mongoose_hooks:leave_room(ServerHost, Room, Host, JID, MucJID), |
1722 |
630 |
ok. |
1723 |
|
|
1724 |
|
-spec filter_presence(exml:element()) -> exml:element(). |
1725 |
|
filter_presence(#xmlel{name = <<"presence">>, attrs = Attrs, children = Els}) -> |
1726 |
1325 |
FEls = lists:filter( |
1727 |
|
fun(#xmlcdata{}) -> |
1728 |
:-( |
false; |
1729 |
|
(#xmlel{attrs = Attrs1}) -> |
1730 |
1311 |
XMLNS = xml:get_attr_s(<<"xmlns">>, Attrs1), |
1731 |
1311 |
case XMLNS of |
1732 |
693 |
<<?NS_MUC_S, _/binary>> -> false; |
1733 |
618 |
_ -> true |
1734 |
|
end |
1735 |
|
end, Els), |
1736 |
1325 |
#xmlel{name = <<"presence">>, attrs = Attrs, children = FEls}. |
1737 |
|
|
1738 |
|
|
1739 |
|
-spec strip_status(exml:element()) -> exml:element(). |
1740 |
|
strip_status(#xmlel{name = <<"presence">>, attrs = Attrs, |
1741 |
|
children = Els}) -> |
1742 |
:-( |
FEls = lists:filter( |
1743 |
|
fun(#xmlel{name = <<"status">>}) -> |
1744 |
:-( |
false; |
1745 |
:-( |
(_) -> true |
1746 |
|
end, Els), |
1747 |
:-( |
#xmlel{name = <<"presence">>, attrs = Attrs, children = FEls}. |
1748 |
|
|
1749 |
|
|
1750 |
|
-spec add_user_presence(jid:jid(), exml:element(), state()) -> state(). |
1751 |
|
add_user_presence(JID, Presence, StateData) -> |
1752 |
690 |
LJID = jid:to_lower(JID), |
1753 |
690 |
FPresence = filter_presence(Presence), |
1754 |
690 |
Users = |
1755 |
|
maps:update_with( |
1756 |
|
LJID, |
1757 |
|
fun(#user{} = User) -> |
1758 |
690 |
User#user{last_presence = FPresence} |
1759 |
|
end, StateData#state.users), |
1760 |
690 |
notify_users_modified(StateData#state{users = Users}). |
1761 |
|
|
1762 |
|
|
1763 |
|
-spec add_user_presence_un(jid:simple_jid() | jid:jid(), exml:element(), |
1764 |
|
state()) -> state(). |
1765 |
|
add_user_presence_un(JID, Presence, StateData) -> |
1766 |
635 |
LJID = jid:to_lower(JID), |
1767 |
635 |
FPresence = filter_presence(Presence), |
1768 |
635 |
Users = |
1769 |
|
maps:update_with( |
1770 |
|
LJID, |
1771 |
|
fun(#user{} = User) -> |
1772 |
635 |
User#user{last_presence = FPresence, role = none} |
1773 |
|
end, StateData#state.users), |
1774 |
635 |
notify_users_modified(StateData#state{users = Users}). |
1775 |
|
|
1776 |
|
|
1777 |
|
-spec is_nick_exists(mod_muc:nick(), state()) -> boolean(). |
1778 |
|
is_nick_exists(Nick, StateData) -> |
1779 |
734 |
maps:is_key(Nick, StateData#state.sessions). |
1780 |
|
|
1781 |
|
|
1782 |
|
-spec find_jids_by_nick(mod_muc:nick(), state()) -> [jid:jid()]. |
1783 |
|
find_jids_by_nick(Nick, StateData) -> |
1784 |
810 |
case maps:find(Nick, StateData#state.sessions) of |
1785 |
730 |
error -> []; |
1786 |
80 |
{ok, JIDs} -> JIDs |
1787 |
|
end. |
1788 |
|
|
1789 |
|
-spec is_new_nick_of_online_user(jid:simple_jid() | jid:jid(), mod_muc:nick(), |
1790 |
|
state()) -> boolean() | user_is_offline. |
1791 |
|
is_new_nick_of_online_user(JID, Nick, StateData) -> |
1792 |
738 |
LJID = jid:to_lower(JID), |
1793 |
738 |
case maps:find(LJID, StateData#state.users) of |
1794 |
4 |
{ok, #user{nick = OldNick}} -> Nick /= <<>> andalso Nick /= OldNick; |
1795 |
734 |
error -> user_is_offline |
1796 |
|
end. |
1797 |
|
|
1798 |
|
-spec is_user_limit_reached(jid:jid(), mod_muc:affiliation(), state()) -> boolean(). |
1799 |
|
is_user_limit_reached(From, Affiliation, StateData) -> |
1800 |
732 |
MaxUsers = get_max_users(StateData), |
1801 |
732 |
MaxAdminUsers = case MaxUsers of |
1802 |
:-( |
none -> none; |
1803 |
732 |
_ -> MaxUsers + get_max_users_admin_threshold(StateData) |
1804 |
|
end, |
1805 |
732 |
NUsers = count_users(StateData), |
1806 |
732 |
ServiceAffiliation = get_service_affiliation(From, StateData), |
1807 |
732 |
NConferences = tab_count_user(From), |
1808 |
732 |
MaxConferences = get_opt(StateData, max_user_conferences), |
1809 |
732 |
(ServiceAffiliation == owner orelse |
1810 |
732 |
MaxUsers == none orelse |
1811 |
732 |
((Affiliation == admin orelse Affiliation == owner) andalso |
1812 |
337 |
NUsers < MaxAdminUsers) orelse |
1813 |
395 |
NUsers < MaxUsers) andalso |
1814 |
731 |
NConferences < MaxConferences. |
1815 |
|
|
1816 |
|
is_next_session_of_occupant(From, Nick, StateData) -> |
1817 |
732 |
IsAllowed = (StateData#state.config)#config.allow_multiple_sessions, |
1818 |
732 |
case {IsAllowed, find_jids_by_nick(Nick, StateData)} of |
1819 |
|
{false, _} -> |
1820 |
720 |
false; |
1821 |
|
{_, []} -> |
1822 |
8 |
false; |
1823 |
|
{true, Jids} -> |
1824 |
4 |
lists:any(fun(Jid) -> |
1825 |
4 |
From#jid.lserver == Jid#jid.lserver |
1826 |
4 |
andalso From#jid.luser == Jid#jid.luser |
1827 |
|
end, Jids) |
1828 |
|
end. |
1829 |
|
|
1830 |
|
-spec choose_new_user_strategy(jid:jid(), mod_muc:nick(), |
1831 |
|
mod_muc:affiliation(), mod_muc:role(), [jlib:xmlcdata() | exml:element()], |
1832 |
|
state()) -> new_user_strategy(). |
1833 |
|
choose_new_user_strategy(From, Nick, Affiliation, Role, Els, StateData) -> |
1834 |
732 |
case {is_user_limit_reached(From, Affiliation, StateData), |
1835 |
|
is_nick_exists(Nick, StateData), |
1836 |
|
is_next_session_of_occupant(From, Nick, StateData), |
1837 |
|
mod_muc:can_use_nick(StateData#state.host_type, StateData#state.host, From, Nick), |
1838 |
|
Role, |
1839 |
|
Affiliation} of |
1840 |
|
{false, _, _, _, _, _} -> |
1841 |
1 |
limit_reached; |
1842 |
|
{_, _, _, _, none, outcast} -> |
1843 |
1 |
user_banned; |
1844 |
|
{_, _, _, _, none, _} -> |
1845 |
1 |
require_membership; |
1846 |
|
{_, true, false, _, _, _} -> |
1847 |
2 |
conflict_use; |
1848 |
|
{_, _, _, false, _, _} -> |
1849 |
:-( |
conflict_registered; |
1850 |
|
_ -> |
1851 |
727 |
choose_new_user_password_strategy(From, Els, StateData) |
1852 |
|
end. |
1853 |
|
|
1854 |
|
-spec choose_new_user_password_strategy(jid:jid(), [jlib:xmlcdata() | exml:element()], |
1855 |
|
state()) -> new_user_strategy(). |
1856 |
|
choose_new_user_password_strategy(From, Els, StateData) -> |
1857 |
727 |
ServiceAffiliation = get_service_affiliation(From, StateData), |
1858 |
727 |
Config = StateData#state.config, |
1859 |
727 |
case is_password_required(ServiceAffiliation, Config) of |
1860 |
676 |
false -> allowed; |
1861 |
51 |
true -> case extract_password(Els) of |
1862 |
35 |
false -> require_password; |
1863 |
16 |
Password -> check_password(StateData, Password) |
1864 |
|
end |
1865 |
|
end. |
1866 |
|
|
1867 |
|
-spec add_new_user(jid:jid(), mod_muc:nick(), exml:element(), state() |
1868 |
|
) -> state(). |
1869 |
|
add_new_user(From, Nick, #xmlel{attrs = Attrs, children = Els} = Packet, StateData) -> |
1870 |
732 |
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), |
1871 |
732 |
Affiliation = get_affiliation(From, StateData), |
1872 |
732 |
Role = get_default_role(Affiliation, StateData), |
1873 |
732 |
case choose_new_user_strategy(From, Nick, Affiliation, Role, Els, StateData) of |
1874 |
|
limit_reached -> |
1875 |
|
% max user reached and user is not admin or owner |
1876 |
1 |
Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:service_unavailable_wait()), |
1877 |
1 |
route_error(Nick, From, Err, StateData); |
1878 |
|
user_banned -> |
1879 |
1 |
ErrText = <<"You have been banned from this room">>, |
1880 |
1 |
Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:forbidden(Lang, ErrText)), |
1881 |
1 |
route_error(Nick, From, Err, StateData); |
1882 |
|
require_membership -> |
1883 |
1 |
ErrText = <<"Membership is required to enter this room">>, |
1884 |
1 |
Err = jlib:make_error_reply( |
1885 |
|
Packet, mongoose_xmpp_errors:registration_required(Lang, ErrText)), |
1886 |
1 |
route_error(Nick, From, Err, StateData); |
1887 |
|
conflict_use -> |
1888 |
2 |
ErrText = <<"That nickname is already in use by another occupant">>, |
1889 |
2 |
Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:conflict(Lang, ErrText)), |
1890 |
2 |
route_error(Nick, From, Err, StateData); |
1891 |
|
conflict_registered -> |
1892 |
:-( |
ErrText = <<"That nickname is registered by another person">>, |
1893 |
:-( |
Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:conflict(Lang, ErrText)), |
1894 |
:-( |
route_error(Nick, From, Err, StateData); |
1895 |
|
require_password -> |
1896 |
35 |
ErrText = <<"A password is required to enter this room">>, |
1897 |
35 |
Err = jlib:make_error_reply( |
1898 |
|
Packet, mongoose_xmpp_errors:not_authorized(Lang, ErrText)), |
1899 |
35 |
route_error(Nick, From, Err, StateData); |
1900 |
|
invalid_password -> |
1901 |
:-( |
ErrText = <<"Incorrect password">>, |
1902 |
:-( |
Err = jlib:make_error_reply( |
1903 |
|
Packet, mongoose_xmpp_errors:not_authorized(Lang, ErrText)), |
1904 |
:-( |
route_error(Nick, From, Err, StateData); |
1905 |
|
http_auth -> |
1906 |
7 |
Password = extract_password(Els), |
1907 |
7 |
perform_http_auth(From, Nick, Packet, Role, Password, StateData); |
1908 |
|
allowed -> |
1909 |
685 |
do_add_new_user(From, Nick, Packet, Role, StateData) |
1910 |
|
end. |
1911 |
|
|
1912 |
|
perform_http_auth(From, Nick, Packet, Role, Password, StateData) -> |
1913 |
7 |
RoomPid = self(), |
1914 |
7 |
RoomJid = StateData#state.jid, |
1915 |
7 |
Pool = StateData#state.http_auth_pool, |
1916 |
7 |
case is_empty_room(StateData) of |
1917 |
|
true -> |
1918 |
6 |
Result = make_http_auth_request(From, RoomJid, Password, Pool), |
1919 |
6 |
handle_http_auth_result(Result, From, Nick, Packet, Role, StateData); |
1920 |
|
false -> |
1921 |
|
%% Perform the request in a separate process to prevent room freeze |
1922 |
1 |
Pid = proc_lib:spawn_link( |
1923 |
|
fun() -> |
1924 |
1 |
Result = make_http_auth_request(From, RoomJid, Password, Pool), |
1925 |
1 |
gen_fsm_compat:send_event(RoomPid, {http_auth, self(), Result, |
1926 |
|
From, Nick, Packet, Role}) |
1927 |
|
end), |
1928 |
1 |
AuthPids = StateData#state.http_auth_pids, |
1929 |
1 |
StateData#state{http_auth_pids = [Pid | AuthPids]} |
1930 |
|
end. |
1931 |
|
|
1932 |
|
make_http_auth_request(From, RoomJid, Password, Pool) -> |
1933 |
7 |
Query = uri_string:compose_query( |
1934 |
|
[{<<"from">>, jid:to_binary(From)}, |
1935 |
|
{<<"to">>, jid:to_binary(RoomJid)}, |
1936 |
|
{<<"pass">>, Password} |
1937 |
|
]), |
1938 |
7 |
Path = <<"check_password", "?", Query/binary>>, |
1939 |
7 |
case mongoose_http_client:get(global, Pool, Path, []) of |
1940 |
5 |
{ok, {<<"200">>, Body}} -> decode_http_auth_response(Body); |
1941 |
2 |
_ -> error |
1942 |
|
end. |
1943 |
|
|
1944 |
|
handle_http_auth_result(allowed, From, Nick, Packet, Role, StateData) -> |
1945 |
3 |
do_add_new_user(From, Nick, Packet, Role, StateData); |
1946 |
|
handle_http_auth_result({invalid_password, ErrorMsg}, From, Nick, Packet, _Role, StateData) -> |
1947 |
2 |
reply_not_authorized(From, Nick, Packet, StateData, ErrorMsg); |
1948 |
|
handle_http_auth_result(error, From, Nick, Packet, _Role, StateData) -> |
1949 |
2 |
reply_service_unavailable(From, Nick, Packet, StateData, <<"Internal server error">>). |
1950 |
|
|
1951 |
|
decode_http_auth_response(Body) -> |
1952 |
5 |
try decode_json_auth_response(Body) of |
1953 |
|
{0, _} -> |
1954 |
3 |
allowed; |
1955 |
|
{AuthCode, Msg} -> |
1956 |
2 |
{invalid_password, iolist_to_binary([integer_to_list(AuthCode), $ , Msg])} |
1957 |
|
catch |
1958 |
:-( |
error:_ -> error |
1959 |
|
end. |
1960 |
|
|
1961 |
|
decode_json_auth_response(Body) -> |
1962 |
5 |
Elements = jiffy:decode(Body, [return_maps]), |
1963 |
5 |
Code = maps:get(<<"code">>, Elements, undefined), |
1964 |
5 |
Msg = maps:get(<<"msg">>, Elements, undefined), |
1965 |
5 |
{Code, Msg}. |
1966 |
|
|
1967 |
|
reply_not_authorized(From, Nick, Packet, StateData, ErrText) -> |
1968 |
2 |
Lang = xml:get_attr_s(<<"xml:lang">>, Packet#xmlel.attrs), |
1969 |
2 |
Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:not_authorized(Lang, ErrText)), |
1970 |
2 |
route_error(Nick, From, Err, StateData). |
1971 |
|
|
1972 |
|
reply_service_unavailable(From, Nick, Packet, StateData, ErrText) -> |
1973 |
2 |
Lang = xml:get_attr_s(<<"xml:lang">>, Packet#xmlel.attrs), |
1974 |
2 |
Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:service_unavailable(Lang, ErrText)), |
1975 |
2 |
route_error(Nick, From, Err, StateData). |
1976 |
|
|
1977 |
|
do_add_new_user(From, Nick, #xmlel{attrs = Attrs, children = Els} = Packet, |
1978 |
|
Role, StateData) -> |
1979 |
688 |
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), |
1980 |
688 |
NewState = |
1981 |
|
add_user_presence( |
1982 |
|
From, Packet, |
1983 |
|
add_online_user(From, Nick, Role, StateData)), |
1984 |
688 |
send_existing_presences(From, NewState), |
1985 |
688 |
send_new_presence(From, NewState), |
1986 |
688 |
Shift = count_stanza_shift(Nick, Els, NewState), |
1987 |
688 |
case send_history(From, Shift, NewState) of |
1988 |
|
true -> |
1989 |
1 |
ok; |
1990 |
|
_ -> |
1991 |
687 |
send_subject(From, Lang, StateData) |
1992 |
|
end, |
1993 |
688 |
case NewState#state.just_created of |
1994 |
|
true -> |
1995 |
357 |
NewState#state{just_created = false}; |
1996 |
|
false -> |
1997 |
331 |
Robots = maps:remove(From, StateData#state.robots), |
1998 |
331 |
NewState#state{robots = Robots} |
1999 |
|
end. |
2000 |
|
|
2001 |
|
is_password_required(owner, _Config) -> |
2002 |
|
%% Don't check pass if user is owner in MUC service (access_admin option) |
2003 |
:-( |
false; |
2004 |
|
is_password_required(_, Config) -> |
2005 |
727 |
Config#config.password_protected. |
2006 |
|
|
2007 |
|
check_password(#state{http_auth_pool = none, |
2008 |
|
config = #config{password = Password}}, Password) -> |
2009 |
9 |
allowed; |
2010 |
|
check_password(#state{http_auth_pool = none}, _Password) -> |
2011 |
:-( |
?LOG_WARNING(#{what => muc_check_password_failed, |
2012 |
:-( |
text => <<"http_auth_pool not found">>}), |
2013 |
:-( |
invalid_password; |
2014 |
|
check_password(#state{http_auth_pool = _Pool}, _Password) -> |
2015 |
7 |
http_auth. |
2016 |
|
|
2017 |
|
-spec extract_password([jlib:xmlcdata() | exml:element()]) -> 'false' | binary(). |
2018 |
|
extract_password([]) -> |
2019 |
:-( |
false; |
2020 |
|
extract_password([#xmlel{attrs = Attrs} = El | Els]) -> |
2021 |
58 |
case xml:get_attr_s(<<"xmlns">>, Attrs) of |
2022 |
|
?NS_MUC -> |
2023 |
58 |
case xml:get_subtag(El, <<"password">>) of |
2024 |
|
false -> |
2025 |
35 |
false; |
2026 |
|
SubEl -> |
2027 |
23 |
xml:get_tag_cdata(SubEl) |
2028 |
|
end; |
2029 |
|
_ -> |
2030 |
:-( |
extract_password(Els) |
2031 |
|
end; |
2032 |
|
extract_password([_ | Els]) -> |
2033 |
:-( |
extract_password(Els). |
2034 |
|
|
2035 |
|
|
2036 |
|
-spec count_stanza_shift(mod_muc:nick(), [jlib:xmlcdata() | exml:element()], |
2037 |
|
state()) -> any(). |
2038 |
|
count_stanza_shift(Nick, Els, StateData) -> |
2039 |
688 |
HL = lqueue_to_list(StateData#state.history), |
2040 |
688 |
Since = extract_history(Els, <<"since">>), |
2041 |
688 |
Shift0 = case Since of |
2042 |
|
false -> |
2043 |
687 |
0; |
2044 |
|
_ -> |
2045 |
1 |
count_seconds_shift(Since, HL) |
2046 |
|
end, |
2047 |
688 |
Seconds = extract_history(Els, <<"seconds">>), |
2048 |
688 |
Shift1 = case Seconds of |
2049 |
|
false -> |
2050 |
688 |
0; |
2051 |
|
_ -> |
2052 |
:-( |
Sec = os:system_time(seconds) - Seconds, |
2053 |
:-( |
count_seconds_shift(Sec, HL) |
2054 |
|
end, |
2055 |
688 |
MaxStanzas = extract_history(Els, <<"maxstanzas">>), |
2056 |
688 |
Shift2 = case MaxStanzas of |
2057 |
|
false -> |
2058 |
688 |
0; |
2059 |
|
_ -> |
2060 |
:-( |
count_maxstanzas_shift(MaxStanzas, HL) |
2061 |
|
end, |
2062 |
688 |
MaxChars = extract_history(Els, <<"maxchars">>), |
2063 |
688 |
Shift3 = case MaxChars of |
2064 |
|
false -> |
2065 |
688 |
0; |
2066 |
|
_ -> |
2067 |
:-( |
count_maxchars_shift(Nick, MaxChars, HL) |
2068 |
|
end, |
2069 |
688 |
lists:max([Shift0, Shift1, Shift2, Shift3]). |
2070 |
|
|
2071 |
|
|
2072 |
|
-spec count_seconds_shift(integer(), [any()]) -> number(). |
2073 |
|
count_seconds_shift(Seconds, HistoryList) -> |
2074 |
1 |
lists:sum( |
2075 |
|
lists:map( |
2076 |
|
fun({_Nick, _Packet, _HaveSubject, TimeStamp, _Size}) -> |
2077 |
2 |
case TimeStamp < Seconds of |
2078 |
:-( |
true -> 1; |
2079 |
2 |
false -> 0 |
2080 |
|
end |
2081 |
|
end, HistoryList)). |
2082 |
|
|
2083 |
|
|
2084 |
|
-spec count_maxstanzas_shift(non_neg_integer(), [any()]) -> integer(). |
2085 |
|
count_maxstanzas_shift(MaxStanzas, HistoryList) -> |
2086 |
:-( |
S = length(HistoryList) - MaxStanzas, |
2087 |
:-( |
max(0, S). |
2088 |
|
|
2089 |
|
|
2090 |
|
-spec count_maxchars_shift(mod_muc:nick(), non_neg_integer(), |
2091 |
|
[any()]) -> non_neg_integer(). |
2092 |
|
count_maxchars_shift(Nick, MaxSize, HistoryList) -> |
2093 |
:-( |
NLen = string:len(binary_to_list(Nick)) + 1, |
2094 |
:-( |
Sizes = lists:map( |
2095 |
|
fun({_Nick, _Packet, _HaveSubject, _TimeStamp, Size}) -> |
2096 |
:-( |
Size + NLen |
2097 |
|
end, HistoryList), |
2098 |
:-( |
calc_shift(MaxSize, Sizes). |
2099 |
|
|
2100 |
|
|
2101 |
|
-spec calc_shift(non_neg_integer(), [number()]) -> non_neg_integer(). |
2102 |
|
calc_shift(MaxSize, Sizes) -> |
2103 |
:-( |
Total = lists:sum(Sizes), |
2104 |
:-( |
calc_shift(MaxSize, Total, 0, Sizes). |
2105 |
|
|
2106 |
|
|
2107 |
|
-spec calc_shift(_MaxSize :: non_neg_integer(), |
2108 |
|
_Size :: number(), Shift :: non_neg_integer(), TSizes :: [number()] |
2109 |
|
) -> non_neg_integer(). |
2110 |
|
calc_shift(_MaxSize, _Size, Shift, []) -> |
2111 |
:-( |
Shift; |
2112 |
|
calc_shift(MaxSize, Size, Shift, _Sizes) when MaxSize >= Size -> |
2113 |
:-( |
Shift; |
2114 |
|
calc_shift(MaxSize, Size, Shift, [S | TSizes]) -> |
2115 |
:-( |
calc_shift(MaxSize, Size - S, Shift + 1, TSizes). |
2116 |
|
|
2117 |
|
|
2118 |
|
-spec extract_history([jlib:xmlcdata() | exml:element()], Type :: binary()) -> |
2119 |
|
false | non_neg_integer(). |
2120 |
|
extract_history([], _Type) -> |
2121 |
:-( |
false; |
2122 |
|
extract_history([#xmlel{attrs = Attrs} = El | Els], Type) -> |
2123 |
2756 |
case xml:get_attr_s(<<"xmlns">>, Attrs) of |
2124 |
|
?NS_MUC -> |
2125 |
2752 |
parse_history_val(xml:get_path_s(El, [{elem, <<"history">>}, {attr, Type}]), Type); |
2126 |
|
_ -> |
2127 |
4 |
extract_history(Els, Type) |
2128 |
|
end; |
2129 |
|
extract_history([_ | Els], Type) -> |
2130 |
:-( |
extract_history(Els, Type). |
2131 |
|
|
2132 |
|
-spec parse_history_val(binary(), binary()) -> false | non_neg_integer(). |
2133 |
|
parse_history_val(AttrVal, <<"since">>) -> |
2134 |
688 |
case catch calendar:rfc3339_to_system_time(binary_to_list(AttrVal)) of |
2135 |
|
IntVal when is_integer(IntVal) and (IntVal >= 0) -> |
2136 |
1 |
IntVal; |
2137 |
|
_ -> |
2138 |
687 |
false |
2139 |
|
end; |
2140 |
|
parse_history_val(AttrVal, _) -> |
2141 |
2064 |
case catch binary_to_integer(AttrVal) of |
2142 |
|
IntVal when is_integer(IntVal) and (IntVal >= 0) -> |
2143 |
:-( |
IntVal; |
2144 |
|
_ -> |
2145 |
2064 |
false |
2146 |
|
end. |
2147 |
|
|
2148 |
|
-spec send_update_presence(jid:jid(), Reason :: binary(), state()) -> any(). |
2149 |
|
send_update_presence(JID, Reason, StateData) -> |
2150 |
83 |
foreach_matched_jid(fun(J) -> |
2151 |
53 |
send_new_presence(J, Reason, StateData) |
2152 |
|
end, JID, StateData). |
2153 |
|
|
2154 |
|
|
2155 |
|
-spec foreach_matched_jid(fun((_) -> 'ok'), jid:jid(), state()) -> ok. |
2156 |
|
foreach_matched_jid(F, JID, #state{users=Users}) -> |
2157 |
83 |
LJID = jid:to_lower(JID), |
2158 |
83 |
case LJID of |
2159 |
|
%% Match by bare JID |
2160 |
|
{U, S, <<>>} -> |
2161 |
83 |
FF = fun({U0, S0, _}, #user{jid = MatchedJID}) |
2162 |
|
when U =:= U0, S =:= S0 -> |
2163 |
53 |
F(MatchedJID); |
2164 |
94 |
(_, _) -> ok |
2165 |
|
end, |
2166 |
83 |
maps_foreach(FF, Users); |
2167 |
|
%% Match by full JID |
2168 |
|
_ -> |
2169 |
:-( |
case maps:is_key(LJID, Users) of |
2170 |
|
true -> |
2171 |
:-( |
F(JID), |
2172 |
:-( |
ok; |
2173 |
|
false -> |
2174 |
:-( |
ok |
2175 |
|
end |
2176 |
|
end. |
2177 |
|
|
2178 |
|
|
2179 |
|
-spec foreach_matched_user(fun((_) -> 'ok'), jid:simple_jid() | jid:jid(), |
2180 |
|
state()) -> ok. |
2181 |
|
foreach_matched_user(F, JID, #state{users=Users}) -> |
2182 |
19 |
LJID = jid:to_lower(JID), |
2183 |
19 |
case LJID of |
2184 |
|
%% Match by bare JID |
2185 |
|
{U, S, <<>>} -> |
2186 |
13 |
FF = fun({U0, S0, _}, User) when U =:= U0, S =:= S0 -> |
2187 |
5 |
F(User); |
2188 |
3 |
(_, _) -> ok |
2189 |
|
end, |
2190 |
13 |
maps_foreach(FF, Users); |
2191 |
|
%% Match by full JID |
2192 |
|
_ -> |
2193 |
6 |
case maps:find(LJID, Users) of |
2194 |
6 |
{ok, User} -> F(User); |
2195 |
:-( |
error -> ok |
2196 |
|
end |
2197 |
|
end. |
2198 |
|
|
2199 |
|
|
2200 |
|
-spec foreach_user(fun((_) -> 'ok'), state()) -> any(). |
2201 |
|
foreach_user(F, #state{users=Users}) -> |
2202 |
292 |
map_foreach_value(F, Users). |
2203 |
|
|
2204 |
|
|
2205 |
|
-spec erase_matched_users(jid:simple_jid() | jid:jid(), state()) -> state(). |
2206 |
|
erase_matched_users(JID, StateData=#state{users=Users, sessions=Sessions}) -> |
2207 |
19 |
LJID = jid:to_lower(JID), |
2208 |
19 |
{NewUsers, NewSessions} = erase_matched_users_map(LJID, Users, Sessions), |
2209 |
19 |
notify_users_modified(StateData#state{users=NewUsers, sessions=NewSessions}). |
2210 |
|
|
2211 |
|
|
2212 |
|
-spec erase_matched_users_map(error | jid:simple_jid(), |
2213 |
|
users_map(), sessions_map()) -> any(). |
2214 |
|
erase_matched_users_map({U, S, <<>>}, Users, Sessions) -> |
2215 |
13 |
FF = fun({U0, S0, _} = J, #user{nick=Nick}, {Us, Ss}) when U =:= U0 andalso S =:= S0-> |
2216 |
5 |
{maps:remove(J, Us), maps:remove(Nick, Ss)}; |
2217 |
|
(_, _, Acc) -> |
2218 |
3 |
Acc |
2219 |
|
end, |
2220 |
13 |
maps:fold(FF, {Users, Sessions}, Users); |
2221 |
|
erase_matched_users_map(LJID, Users, Sessions) -> |
2222 |
6 |
{ok, #user{nick=Nick}} = maps:find(LJID, Users), |
2223 |
6 |
{maps:remove(LJID, Users), maps:remove(Nick, Sessions)}. |
2224 |
|
|
2225 |
|
|
2226 |
|
-spec update_matched_users(F :: fun((user()) -> user()), JID :: jid:jid(), |
2227 |
|
state()) -> state(). |
2228 |
|
update_matched_users(F, JID, StateData=#state{users=Users}) -> |
2229 |
102 |
LJID = jid:to_lower(JID), |
2230 |
102 |
NewUsers = update_matched_users_map(F, LJID, Users), |
2231 |
102 |
notify_users_modified(StateData#state{users=NewUsers}). |
2232 |
|
|
2233 |
|
|
2234 |
|
-spec update_matched_users_map(fun((user()) -> user()), |
2235 |
|
error | jid:simple_jid(), users_map()) -> any(). |
2236 |
|
update_matched_users_map(F, {U, S, <<>>}, Users) -> |
2237 |
71 |
FF = fun({U0, S0, _} = J, User, Us) when U =:= U0 andalso S =:= S0-> |
2238 |
48 |
maps:put(J, F(User), Us); |
2239 |
|
(_, _, Us) -> |
2240 |
87 |
Us |
2241 |
|
end, |
2242 |
71 |
maps:fold(FF, Users, Users); |
2243 |
|
update_matched_users_map(F, LJID, Users) -> |
2244 |
31 |
case maps:find(LJID, Users) of |
2245 |
31 |
{ok, User} -> maps:put(LJID, F(User), Users); |
2246 |
:-( |
error -> Users |
2247 |
|
end. |
2248 |
|
|
2249 |
|
-spec send_new_presence_un(jid:jid(), state()) -> 'ok'. |
2250 |
|
send_new_presence_un(NJID, StateData) -> |
2251 |
635 |
send_new_presence_un(NJID, <<>>, StateData). |
2252 |
|
|
2253 |
|
|
2254 |
|
-spec send_new_presence_un(jid:jid(), binary(), state()) -> 'ok'. |
2255 |
|
send_new_presence_un(NJID, Reason, StateData) -> |
2256 |
635 |
{ok, #user{nick = Nick}} = maps:find(jid:to_lower(NJID), StateData#state.users), |
2257 |
635 |
case is_last_session(Nick, StateData) of |
2258 |
|
true -> |
2259 |
631 |
send_new_presence(NJID, Reason, StateData); |
2260 |
|
false -> |
2261 |
4 |
UserJIDs = maps:get(Nick, StateData#state.sessions), |
2262 |
4 |
GetUserTupleByJID = fun(JID) -> |
2263 |
8 |
LJID = jid:to_lower(JID), |
2264 |
8 |
{LJID, maps:get(LJID, StateData#state.users)} |
2265 |
|
end, |
2266 |
4 |
CurrentSessionUsers = lists:map(GetUserTupleByJID, UserJIDs), |
2267 |
4 |
send_new_presence_to(NJID, Reason, CurrentSessionUsers, StateData) |
2268 |
|
end. |
2269 |
|
|
2270 |
|
|
2271 |
|
-spec send_new_presence(jid:jid(), state()) -> 'ok'. |
2272 |
|
send_new_presence(NJID, StateData) -> |
2273 |
690 |
send_new_presence(NJID, <<>>, StateData). |
2274 |
|
|
2275 |
|
|
2276 |
|
-spec send_new_presence(jid:jid(), binary(), state()) -> 'ok'. |
2277 |
|
send_new_presence(NJID, Reason, StateData) -> |
2278 |
1405 |
send_new_presence_to(NJID, Reason, StateData#state.users, StateData). |
2279 |
|
|
2280 |
|
|
2281 |
|
%% Receivers can be a list or a map |
2282 |
|
-spec send_new_presence_to(jid:jid(), binary(), users_map() | users_pairs(), state()) -> ok. |
2283 |
|
send_new_presence_to(NJID, Reason, Receivers, StateData) -> |
2284 |
1409 |
{ok, #user{ role = Role } = User} = maps:find(jid:to_lower(NJID), StateData#state.users), |
2285 |
1409 |
Affiliation = get_affiliation(NJID, StateData), |
2286 |
1409 |
BAffiliation = affiliation_to_binary(Affiliation), |
2287 |
1409 |
BRole = role_to_binary(Role), |
2288 |
1409 |
F = fun(_LJID, Info) -> |
2289 |
2283 |
send_new_presence_to_single(NJID, User, BAffiliation, BRole, Reason, Info, StateData) |
2290 |
|
end, |
2291 |
1409 |
maps_or_pairs_foreach(F, Receivers). |
2292 |
|
|
2293 |
|
send_new_presence_to_single(NJID, #user{jid = RealJID, nick = Nick, last_presence = Presence}, |
2294 |
|
BAffiliation, BRole, Reason, ReceiverInfo, StateData) -> |
2295 |
2283 |
ItemAttrs = |
2296 |
980 |
case (ReceiverInfo#user.role == moderator) orelse |
2297 |
1303 |
((StateData#state.config)#config.anonymous == false) of |
2298 |
|
true -> |
2299 |
1400 |
[{<<"jid">>, jid:to_binary(RealJID)}, |
2300 |
|
{<<"affiliation">>, BAffiliation}, |
2301 |
|
{<<"role">>, BRole}]; |
2302 |
|
_ -> |
2303 |
883 |
[{<<"affiliation">>, BAffiliation}, |
2304 |
|
{<<"role">>, BRole}] |
2305 |
|
end, |
2306 |
2283 |
ItemEls = case Reason of |
2307 |
|
<<>> -> |
2308 |
2257 |
[]; |
2309 |
|
_ -> |
2310 |
26 |
[#xmlel{name = <<"reason">>, children = [#xmlcdata{content = Reason}]}] |
2311 |
|
end, |
2312 |
2283 |
Status = case StateData#state.just_created of |
2313 |
|
true -> |
2314 |
357 |
[status_code(201)]; |
2315 |
|
false -> |
2316 |
1926 |
[] |
2317 |
|
end, |
2318 |
2283 |
Status2 = case (NJID == ReceiverInfo#user.jid) of |
2319 |
|
true -> |
2320 |
1409 |
Status0 = case (StateData#state.config)#config.logging of |
2321 |
|
true -> |
2322 |
2 |
[status_code(170) | Status]; |
2323 |
|
false -> |
2324 |
1407 |
Status |
2325 |
|
end, |
2326 |
1409 |
Status1 = case ((StateData#state.config)#config.anonymous==false) of |
2327 |
|
true -> |
2328 |
525 |
[status_code(100) | Status0]; |
2329 |
|
false -> |
2330 |
884 |
Status0 |
2331 |
|
end, |
2332 |
1409 |
case ((NJID == ReceiverInfo#user.jid)==true) of |
2333 |
|
true -> |
2334 |
1409 |
[status_code(110) | Status1]; |
2335 |
|
false -> |
2336 |
:-( |
Status1 |
2337 |
|
end; |
2338 |
|
false -> |
2339 |
874 |
Status |
2340 |
|
end, |
2341 |
2283 |
Packet = xml:append_subtags( |
2342 |
|
Presence, |
2343 |
|
[#xmlel{name = <<"x">>, attrs = [{<<"xmlns">>, ?NS_MUC}], |
2344 |
|
children = [#xmlel{name = <<"item">>, attrs = ItemAttrs, |
2345 |
|
children = ItemEls} | Status2]}]), |
2346 |
2283 |
ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), |
2347 |
|
ReceiverInfo#user.jid, Packet). |
2348 |
|
|
2349 |
|
-spec send_existing_presences(jid:jid(), state()) -> 'ok'. |
2350 |
|
send_existing_presences(ToJID, StateData) -> |
2351 |
688 |
LToJID = jid:to_lower(ToJID), |
2352 |
688 |
{ok, #user{jid = RealToJID, role = Role, nick = _Nick}} = |
2353 |
|
maps:find(LToJID, StateData#state.users), |
2354 |
|
% if you don't want to send presences of other sessions of occupant with ToJID |
2355 |
|
% switch following lines |
2356 |
|
% JIDsToSkip = [RealToJID | find_jids_by_nick(_Nick, StateData)], |
2357 |
688 |
JIDsToSkip = [RealToJID], |
2358 |
688 |
maps_foreach( |
2359 |
|
fun({_, #user{jid = FromJID}} = User) -> |
2360 |
1068 |
case lists:member(FromJID, JIDsToSkip) of |
2361 |
688 |
true -> ok; |
2362 |
380 |
_ -> send_existing_presence(User, Role, RealToJID, StateData) |
2363 |
|
end |
2364 |
|
end, StateData#state.users). |
2365 |
|
|
2366 |
|
-spec send_existing_presence({jid:simple_jid(), mod_muc_room_user()}, mod_muc:role(), |
2367 |
|
jid:jid(), state()) -> mongoose_acc:t(). |
2368 |
|
send_existing_presence({_LJID, #user{jid = FromJID, nick = FromNick, |
2369 |
|
role = FromRole, last_presence = Presence}}, |
2370 |
|
Role, RealToJID, StateData) -> |
2371 |
380 |
FromAffiliation = get_affiliation(FromJID, StateData), |
2372 |
380 |
ItemAttrs = |
2373 |
380 |
case (Role == moderator) orelse ((StateData#state.config)#config.anonymous == false) of |
2374 |
|
true -> |
2375 |
183 |
[{<<"jid">>, jid:to_binary(FromJID)}, |
2376 |
|
{<<"affiliation">>, |
2377 |
|
affiliation_to_binary(FromAffiliation)}, |
2378 |
|
{<<"role">>, role_to_binary(FromRole)}]; |
2379 |
|
_ -> |
2380 |
197 |
[{<<"affiliation">>, |
2381 |
|
affiliation_to_binary(FromAffiliation)}, |
2382 |
|
{<<"role">>, role_to_binary(FromRole)}] |
2383 |
|
end, |
2384 |
380 |
Packet = xml:append_subtags( |
2385 |
|
Presence, |
2386 |
|
[#xmlel{name = <<"x">>, |
2387 |
|
attrs = [{<<"xmlns">>, ?NS_MUC_USER}], |
2388 |
|
children = [#xmlel{name = <<"item">>, |
2389 |
|
attrs = ItemAttrs}]}]), |
2390 |
380 |
ejabberd_router:route(jid:replace_resource(StateData#state.jid, FromNick), RealToJID, Packet). |
2391 |
|
|
2392 |
|
-spec send_config_update(atom(), state()) -> 'ok'. |
2393 |
|
send_config_update(Type, StateData) -> |
2394 |
4 |
Status = case Type of |
2395 |
1 |
logging_enabled -> <<"170">>; |
2396 |
1 |
logging_disabled -> <<"171">>; |
2397 |
1 |
nonanonymous -> <<"172">>; |
2398 |
1 |
semianonymous -> <<"173">> |
2399 |
|
end, |
2400 |
4 |
Message = jlib:make_config_change_message(Status), |
2401 |
4 |
send_to_all_users(Message, StateData). |
2402 |
|
|
2403 |
|
|
2404 |
|
-spec send_invitation(jid:jid(), jid:jid(), binary(), state()) -> mongoose_acc:t(). |
2405 |
|
send_invitation(From, To, Reason, StateData=#state{host=Host, |
2406 |
|
server_host=ServerHost, |
2407 |
|
jid=RoomJID}) -> |
2408 |
7 |
mongoose_hooks:invitation_sent(Host, ServerHost, RoomJID, From, To, Reason), |
2409 |
7 |
Config = StateData#state.config, |
2410 |
7 |
Password = case Config#config.password_protected of |
2411 |
7 |
false -> <<>>; |
2412 |
:-( |
true -> Config#config.password |
2413 |
|
end, |
2414 |
7 |
Packet = jlib:make_invitation(jid:to_bare(From), Password, Reason), |
2415 |
7 |
ejabberd_router:route(RoomJID, To, Packet). |
2416 |
|
|
2417 |
|
|
2418 |
|
-spec change_nick(jid:jid(), binary(), state()) -> state(). |
2419 |
|
change_nick(JID, Nick, StateData) -> |
2420 |
1 |
LJID = jid:to_lower(JID), |
2421 |
1 |
{ok, #user{nick = OldNick}} = |
2422 |
|
maps:find(LJID, StateData#state.users), |
2423 |
1 |
Users = |
2424 |
|
maps:update_with( |
2425 |
|
LJID, |
2426 |
|
fun(#user{} = User) -> |
2427 |
1 |
User#user{nick = Nick} |
2428 |
|
end, StateData#state.users), |
2429 |
1 |
{ok, JIDs} = maps:find(OldNick, StateData#state.sessions), |
2430 |
1 |
Sessions = maps:remove(OldNick, maps:put(Nick, JIDs, StateData#state.sessions)), |
2431 |
1 |
NewStateData = notify_users_modified(StateData#state{users = Users, sessions = Sessions}), |
2432 |
1 |
send_nick_changing(JID, OldNick, NewStateData), |
2433 |
1 |
add_to_log(nickchange, {OldNick, Nick}, StateData), |
2434 |
1 |
NewStateData. |
2435 |
|
|
2436 |
|
|
2437 |
|
-spec send_nick_changing(jid:jid(), mod_muc:nick(), state()) -> 'ok'. |
2438 |
|
send_nick_changing(JID, OldNick, StateData) -> |
2439 |
1 |
User = maps:find(jid:to_lower(JID), StateData#state.users), |
2440 |
1 |
{ok, #user{jid = RealJID, |
2441 |
|
nick = Nick, |
2442 |
|
role = Role, |
2443 |
|
last_presence = Presence}} = User, |
2444 |
1 |
Affiliation = get_affiliation(JID, StateData), |
2445 |
1 |
maps_foreach(mk_send_nick_change(Presence, OldNick, JID, RealJID, |
2446 |
|
Affiliation, Role, Nick, StateData), |
2447 |
|
StateData#state.users). |
2448 |
|
|
2449 |
|
mk_send_nick_change(Presence, OldNick, JID, RealJID, Affiliation, |
2450 |
|
Role, Nick, StateData) -> |
2451 |
1 |
fun({LJID, Info}) -> |
2452 |
2 |
send_nick_change(Presence, OldNick, JID, RealJID, Affiliation, |
2453 |
|
Role, Nick, LJID, Info, StateData) |
2454 |
|
end. |
2455 |
|
|
2456 |
|
send_nick_change(Presence, OldNick, JID, RealJID, Affiliation, Role, |
2457 |
|
Nick, _LJID, Info, #state{} = S) -> |
2458 |
2 |
MaybePublicJID = case is_nick_change_public(Info, S#state.config) of |
2459 |
:-( |
true -> RealJID; |
2460 |
2 |
false -> undefined |
2461 |
|
end, |
2462 |
2 |
MaybeSelfPresenceCode = case JID == Info#user.jid of |
2463 |
1 |
true -> status_code(110); |
2464 |
1 |
false -> undefined |
2465 |
|
end, |
2466 |
2 |
Unavailable = nick_unavailable_presence(MaybePublicJID, Nick, Affiliation, |
2467 |
|
Role, MaybeSelfPresenceCode), |
2468 |
2 |
ejabberd_router:route(jid:replace_resource(S#state.jid, OldNick), |
2469 |
|
Info#user.jid, Unavailable), |
2470 |
2 |
Available = nick_available_presence(Presence, MaybePublicJID, Affiliation, |
2471 |
|
Role, MaybeSelfPresenceCode), |
2472 |
2 |
ejabberd_router:route(jid:replace_resource(S#state.jid, Nick), |
2473 |
|
Info#user.jid, Available). |
2474 |
|
|
2475 |
|
-spec is_nick_change_public(user(), config()) -> boolean(). |
2476 |
|
is_nick_change_public(UserInfo, RoomConfig) -> |
2477 |
2 |
UserInfo#user.role == moderator |
2478 |
|
orelse |
2479 |
2 |
RoomConfig#config.anonymous == false. |
2480 |
|
|
2481 |
|
-spec status_code(integer()) -> exml:element(). |
2482 |
|
status_code(Code) -> |
2483 |
2299 |
#xmlel{name = <<"status">>, |
2484 |
|
attrs = [{<<"code">>, integer_to_binary(Code)}]}. |
2485 |
|
|
2486 |
|
-spec nick_unavailable_presence(MaybeJID, Nick, Affiliation, Role, MaybeCode) -> |
2487 |
|
exml:element() when |
2488 |
|
MaybeJID :: 'undefined' | jid:jid(), |
2489 |
|
Nick :: mod_muc:nick(), |
2490 |
|
Affiliation :: mod_muc:affiliation(), |
2491 |
|
Role :: mod_muc:role(), |
2492 |
|
MaybeCode :: 'undefined' | exml:element(). |
2493 |
|
nick_unavailable_presence(MaybeJID, Nick, Affiliation, Role, MaybeCode) -> |
2494 |
2 |
presence(<<"unavailable">>, |
2495 |
|
[muc_user_x([muc_user_item(MaybeJID, Nick, Affiliation, Role), |
2496 |
|
status_code(303)] |
2497 |
1 |
++ [MaybeCode || MaybeCode /= undefined])]). |
2498 |
|
|
2499 |
|
-spec nick_available_presence(LastPresence, MaybeJID, Affiliation, |
2500 |
|
Role, MaybeCode) -> exml:element() when |
2501 |
|
LastPresence :: exml:element(), |
2502 |
|
MaybeJID :: 'undefined' | jid:jid(), |
2503 |
|
Affiliation :: mod_muc:affiliation(), |
2504 |
|
Role :: mod_muc:role(), |
2505 |
|
MaybeCode :: 'undefined' | exml:element(). |
2506 |
|
nick_available_presence(LastPresence, MaybeJID, Affiliation, Role, MaybeCode) -> |
2507 |
2 |
Item = muc_user_item(MaybeJID, undefined, Affiliation, Role), |
2508 |
2 |
xml:append_subtags(LastPresence, |
2509 |
1 |
[muc_user_x([Item] ++ [MaybeCode |
2510 |
2 |
|| MaybeCode /= undefined])]). |
2511 |
|
|
2512 |
|
-spec muc_user_item(MaybeJID, MaybeNick, Affiliation, Role) -> exml:element() when |
2513 |
|
MaybeJID :: 'undefined' | jid:jid(), |
2514 |
|
MaybeNick :: 'undefined' | mod_muc:nick(), |
2515 |
|
Affiliation :: mod_muc:affiliation(), |
2516 |
|
Role :: mod_muc:role(). |
2517 |
|
muc_user_item(MaybeJID, MaybeNick, Affiliation, Role) -> |
2518 |
4 |
#xmlel{name = <<"item">>, |
2519 |
:-( |
attrs = [{<<"jid">>, jid:to_binary(MaybeJID)} |
2520 |
4 |
|| MaybeJID /= undefined] ++ |
2521 |
2 |
[{<<"nick">>, MaybeNick} || MaybeNick /= undefined] ++ |
2522 |
|
[{<<"affiliation">>, affiliation_to_binary(Affiliation)}, |
2523 |
|
{<<"role">>, role_to_binary(Role)}]}. |
2524 |
|
|
2525 |
|
-spec muc_user_x([exml:element()]) -> exml:element(). |
2526 |
|
muc_user_x(Children) -> |
2527 |
4 |
#xmlel{name = <<"x">>, |
2528 |
|
attrs = [{<<"xmlns">>, ?NS_MUC_USER}], |
2529 |
|
children = Children}. |
2530 |
|
|
2531 |
|
-spec presence(binary(), [exml:element()]) -> exml:element(). |
2532 |
|
%% Add and validate other types if need be. |
2533 |
|
presence(<<"unavailable">> = Type, Children) -> |
2534 |
2 |
#xmlel{name = <<"presence">>, |
2535 |
2 |
attrs = [{<<"type">>, Type} || Type /= <<"available">>], |
2536 |
|
children = Children}. |
2537 |
|
|
2538 |
|
|
2539 |
|
-spec lqueue_new(integer()) -> lqueue(). |
2540 |
|
lqueue_new(Max) -> |
2541 |
600 |
#lqueue{queue = queue:new(), |
2542 |
|
len = 0, |
2543 |
|
max = Max}. |
2544 |
|
|
2545 |
|
|
2546 |
|
%% @doc If the message queue limit is set to 0, do not store messages. |
2547 |
|
%% Otherwise, rotate messages in the queue store. |
2548 |
|
-spec lqueue_in(any(), lqueue()) -> lqueue(). |
2549 |
|
lqueue_in(_Item, LQ = #lqueue{max = 0}) -> |
2550 |
:-( |
LQ; |
2551 |
|
lqueue_in(Item, #lqueue{queue = Q1, len = Len, max = Max}) -> |
2552 |
391 |
Q2 = queue:in(Item, Q1), |
2553 |
391 |
case Len >= Max of |
2554 |
|
true -> |
2555 |
:-( |
Q3 = lqueue_cut(Q2, Len - Max + 1), |
2556 |
:-( |
#lqueue{queue = Q3, len = Max, max = Max}; |
2557 |
|
false -> |
2558 |
391 |
#lqueue{queue = Q2, len = Len + 1, max = Max} |
2559 |
|
end. |
2560 |
|
|
2561 |
|
|
2562 |
|
-spec lqueue_cut(queue:queue(), non_neg_integer()) -> queue:queue(). |
2563 |
|
lqueue_cut(Q, 0) -> |
2564 |
:-( |
Q; |
2565 |
|
lqueue_cut(Q, N) -> |
2566 |
:-( |
{_, Q1} = queue:out(Q), |
2567 |
:-( |
lqueue_cut(Q1, N - 1). |
2568 |
|
|
2569 |
|
|
2570 |
|
-spec lqueue_to_list(lqueue()) -> [any()]. |
2571 |
|
lqueue_to_list(#lqueue{queue = Q1}) -> |
2572 |
1376 |
queue:to_list(Q1). |
2573 |
|
|
2574 |
|
|
2575 |
|
-spec add_message_to_history(mod_muc:nick(), jid:jid(), exml:element(), |
2576 |
|
state()) -> state(). |
2577 |
|
add_message_to_history(FromNick, FromJID, Packet, StateData) -> |
2578 |
391 |
HaveSubject = case xml:get_subtag(Packet, <<"subject">>) of |
2579 |
|
false -> |
2580 |
389 |
false; |
2581 |
|
_ -> |
2582 |
2 |
true |
2583 |
|
end, |
2584 |
391 |
SystemTime = os:system_time(second), |
2585 |
391 |
TimeStamp = calendar:system_time_to_rfc3339(SystemTime, [{offset, "Z"}]), |
2586 |
|
%% Chatroom history is stored as XMPP packets, so |
2587 |
|
%% the decision to include the original sender's JID or not is based on the |
2588 |
|
%% chatroom configuration when the message was originally sent. |
2589 |
|
%% Also, if the chatroom is anonymous, even moderators will not get the real JID |
2590 |
391 |
SenderJid = case (StateData#state.config)#config.anonymous of |
2591 |
96 |
true -> StateData#state.jid; |
2592 |
295 |
false -> FromJID |
2593 |
|
end, |
2594 |
391 |
TSPacket = xml:append_subtags(Packet, [jlib:timestamp_to_xml(TimeStamp, SenderJid, <<>>)]), |
2595 |
391 |
SPacket = jlib:replace_from_to( |
2596 |
|
jid:replace_resource(StateData#state.jid, FromNick), |
2597 |
|
StateData#state.jid, |
2598 |
|
TSPacket), |
2599 |
391 |
Size = element_size(SPacket), |
2600 |
391 |
Q1 = lqueue_in({FromNick, TSPacket, HaveSubject, SystemTime, Size}, |
2601 |
|
StateData#state.history), |
2602 |
391 |
add_to_log(text, {FromNick, Packet}, StateData), |
2603 |
391 |
mongoose_hooks:room_packet(StateData#state.host, |
2604 |
|
FromNick, FromJID, StateData#state.jid, Packet), |
2605 |
391 |
StateData#state{history = Q1}. |
2606 |
|
|
2607 |
|
|
2608 |
|
-spec send_history(jid:jid(), Shift :: non_neg_integer(), state()) -> boolean(). |
2609 |
|
send_history(JID, Shift, StateData) -> |
2610 |
688 |
lists:foldl( |
2611 |
|
fun({Nick, Packet, HaveSubject, _TimeStamp, _Size}, B) -> |
2612 |
14 |
ejabberd_router:route( |
2613 |
|
jid:replace_resource(StateData#state.jid, Nick), |
2614 |
|
JID, |
2615 |
|
Packet), |
2616 |
14 |
B or HaveSubject |
2617 |
|
end, false, lists:nthtail(Shift, lqueue_to_list(StateData#state.history))). |
2618 |
|
|
2619 |
|
|
2620 |
|
-spec send_subject(jid:jid(), ejabberd:lang(), state()) -> mongoose_acc:t(). |
2621 |
|
send_subject(JID, _Lang, StateData = #state{subject = <<>>, subject_author = <<>>}) -> |
2622 |
685 |
Packet = #xmlel{name = <<"message">>, |
2623 |
|
attrs = [{<<"type">>, <<"groupchat">>}], |
2624 |
|
children = [#xmlel{name = <<"subject">>}, |
2625 |
|
#xmlel{name = <<"body">>}]}, |
2626 |
685 |
ejabberd_router:route( |
2627 |
|
StateData#state.jid, |
2628 |
|
JID, |
2629 |
|
Packet); |
2630 |
|
send_subject(JID, _Lang, StateData) -> |
2631 |
2 |
Subject = StateData#state.subject, |
2632 |
2 |
TimeStamp = StateData#state.subject_timestamp, |
2633 |
2 |
RoomJID = StateData#state.jid, |
2634 |
2 |
Packet = #xmlel{name = <<"message">>, |
2635 |
|
attrs = [{<<"type">>, <<"groupchat">>}], |
2636 |
|
children = [#xmlel{name = <<"subject">>, |
2637 |
|
children = [#xmlcdata{content = Subject}]}, |
2638 |
|
#xmlel{name = <<"delay">>, |
2639 |
|
attrs = [{<<"xmlns">>, ?NS_DELAY}, |
2640 |
|
{<<"from">>, jid:to_binary(RoomJID)}, |
2641 |
|
{<<"stamp">>, TimeStamp}]}]}, |
2642 |
2 |
ejabberd_router:route(RoomJID, JID, Packet). |
2643 |
|
|
2644 |
|
|
2645 |
|
-spec check_subject(exml:element()) -> 'false' | binary(). |
2646 |
|
check_subject(Packet) -> |
2647 |
392 |
case xml:get_subtag(Packet, <<"subject">>) of |
2648 |
|
false -> |
2649 |
389 |
false; |
2650 |
|
SubjEl -> |
2651 |
3 |
xml:get_tag_cdata(SubjEl) |
2652 |
|
end. |
2653 |
|
|
2654 |
|
|
2655 |
|
-spec can_change_subject(mod_muc:role(), state()) -> boolean(). |
2656 |
|
can_change_subject(Role, StateData) -> |
2657 |
3 |
case (StateData#state.config)#config.allow_change_subj of |
2658 |
|
true -> |
2659 |
1 |
(Role == moderator) orelse (Role == participant); |
2660 |
|
_ -> |
2661 |
2 |
Role == moderator |
2662 |
|
end. |
2663 |
|
|
2664 |
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
2665 |
|
% Admin stuff |
2666 |
|
|
2667 |
|
-spec process_iq_admin(jid:jid(), get | set, ejabberd:lang(), exml:element(), state()) -> |
2668 |
|
state() | {error, exml:element()}. |
2669 |
|
process_iq_admin(From, set, Lang, SubEl, StateData) -> |
2670 |
77 |
#xmlel{children = Items} = SubEl, |
2671 |
77 |
process_admin_items_set(From, Items, Lang, StateData); |
2672 |
|
process_iq_admin(From, get, Lang, SubEl, StateData) -> |
2673 |
37 |
case xml:get_subtag(SubEl, <<"item">>) of |
2674 |
|
false -> |
2675 |
:-( |
{error, mongoose_xmpp_errors:bad_request()}; |
2676 |
|
Item -> |
2677 |
37 |
FAffiliation = get_affiliation(From, StateData), |
2678 |
37 |
FRole = get_role(From, StateData), |
2679 |
37 |
{RoleOrAff, _} = ExtractResult = extract_role_or_affiliation(Item), |
2680 |
37 |
IsAllowed = iq_admin_allowed(get, RoleOrAff, FAffiliation, FRole, StateData), |
2681 |
37 |
case {IsAllowed, ExtractResult} of |
2682 |
|
{true, {role, Role}} -> |
2683 |
15 |
Items = items_with_role(Role, StateData), |
2684 |
15 |
{result, Items, StateData}; |
2685 |
|
{true, {affiliation, Affiliation}} -> |
2686 |
13 |
Items = items_with_affiliation(Affiliation, StateData), |
2687 |
13 |
{result, Items, StateData}; |
2688 |
|
{_, {role, _}} -> |
2689 |
1 |
ErrText = <<"Moderator privileges required">>, |
2690 |
1 |
{error, mongoose_xmpp_errors:forbidden(Lang, ErrText)}; |
2691 |
|
{_, {affiliation, _}} -> |
2692 |
8 |
ErrText = <<"Administrator privileges required">>, |
2693 |
8 |
{error, mongoose_xmpp_errors:forbidden(Lang, ErrText)}; |
2694 |
|
{_, Error} -> |
2695 |
:-( |
Error |
2696 |
|
end |
2697 |
|
end. |
2698 |
|
|
2699 |
|
-spec extract_role_or_affiliation(Item :: exml:element()) -> |
2700 |
|
{role, mod_muc:role()} | {affiliation, mod_muc:affiliation()} | {error, exml:element()}. |
2701 |
|
extract_role_or_affiliation(Item) -> |
2702 |
37 |
case {xml:get_tag_attr(<<"role">>, Item), xml:get_tag_attr(<<"affiliation">>, Item)} of |
2703 |
|
{false, false} -> |
2704 |
:-( |
{error, mongoose_xmpp_errors:bad_request()}; |
2705 |
|
{false, {value, BAffiliation}} -> |
2706 |
21 |
case catch binary_to_affiliation(BAffiliation) of |
2707 |
:-( |
{'EXIT', _} -> {error, mongoose_xmpp_errors:bad_request()}; |
2708 |
21 |
Affiliation -> {affiliation, Affiliation} |
2709 |
|
end; |
2710 |
|
{{value, BRole}, _} -> |
2711 |
16 |
case catch binary_to_role(BRole) of |
2712 |
:-( |
{'EXIT', _} -> {error, mongoose_xmpp_errors:bad_request()}; |
2713 |
16 |
Role -> {role, Role} |
2714 |
|
end |
2715 |
|
end. |
2716 |
|
|
2717 |
|
-spec iq_admin_allowed(atom(), atom(), atom(), atom(), state()) -> boolean(). |
2718 |
|
iq_admin_allowed(get, What, FAff, none, State) -> |
2719 |
|
%% no role is translated to 'visitor' |
2720 |
8 |
iq_admin_allowed(get, What, FAff, visitor, State); |
2721 |
|
iq_admin_allowed(get, role, _, moderator, _) -> |
2722 |
|
%% moderator is allowed by definition, needs it to do his duty |
2723 |
13 |
true; |
2724 |
|
iq_admin_allowed(get, role, _, Role, State) -> |
2725 |
3 |
Cfg = State#state.config, |
2726 |
3 |
lists:member(Role, Cfg#config.maygetmemberlist); |
2727 |
|
iq_admin_allowed(get, affiliation, owner, _, _) -> |
2728 |
10 |
true; |
2729 |
|
iq_admin_allowed(get, affiliation, admin, _, _) -> |
2730 |
:-( |
true; |
2731 |
|
iq_admin_allowed(get, affiliation, _, Role, State) -> |
2732 |
11 |
Cfg = State#state.config, |
2733 |
11 |
lists:member(Role, Cfg#config.maygetmemberlist). |
2734 |
|
|
2735 |
|
|
2736 |
|
-spec items_with_role(mod_muc:role(), state()) -> [exml:element()]. |
2737 |
|
items_with_role(BRole, StateData) -> |
2738 |
15 |
lists:map( |
2739 |
|
fun({_, U}) -> |
2740 |
28 |
user_to_item(U, StateData) |
2741 |
|
end, search_role(BRole, StateData)). |
2742 |
|
|
2743 |
|
|
2744 |
|
-spec items_with_affiliation(mod_muc:affiliation(), state()) -> [exml:element()]. |
2745 |
|
items_with_affiliation(BAffiliation, StateData) -> |
2746 |
13 |
lists:map( |
2747 |
|
fun({JID, {Affiliation, Reason}}) -> |
2748 |
3 |
#xmlel{name = <<"item">>, |
2749 |
|
attrs = [{<<"affiliation">>, affiliation_to_binary(Affiliation)}, |
2750 |
|
{<<"jid">>, jid:to_binary(JID)}], |
2751 |
|
children = [#xmlel{name = <<"reason">>, |
2752 |
|
children = [#xmlcdata{content = Reason}]}]}; |
2753 |
|
({JID, Affiliation}) -> |
2754 |
1 |
#xmlel{name = <<"item">>, |
2755 |
|
attrs = [{<<"affiliation">>, affiliation_to_binary(Affiliation)}, |
2756 |
|
{<<"jid">>, jid:to_binary(JID)}]} |
2757 |
|
end, search_affiliation(BAffiliation, StateData)). |
2758 |
|
|
2759 |
|
|
2760 |
|
-spec user_to_item(user(), state()) -> exml:element(). |
2761 |
|
user_to_item(#user{role = Role, |
2762 |
|
nick = Nick, |
2763 |
|
jid = JID |
2764 |
|
}, StateData) -> |
2765 |
28 |
Affiliation = get_affiliation(JID, StateData), |
2766 |
28 |
#xmlel{name = <<"item">>, |
2767 |
|
attrs = [{<<"role">>, role_to_binary(Role)}, |
2768 |
|
{<<"affiliation">>, affiliation_to_binary(Affiliation)}, |
2769 |
|
{<<"nick">>, Nick}, |
2770 |
|
{<<"jid">>, jid:to_binary(JID)}]}. |
2771 |
|
|
2772 |
|
|
2773 |
|
-spec search_role(mod_muc:role(), state()) -> users_pairs(). |
2774 |
|
search_role(Role, StateData) -> |
2775 |
17 |
F = fun(_, #user{role = R}) -> Role == R end, |
2776 |
17 |
maps:to_list(maps:filter(F, StateData#state.users)). |
2777 |
|
|
2778 |
|
|
2779 |
|
-spec search_affiliation(mod_muc:affiliation(), state()) -> [{_, _}]. |
2780 |
|
search_affiliation(Affiliation, StateData) when is_atom(Affiliation) -> |
2781 |
19 |
F = fun(_, A) -> |
2782 |
28 |
case A of |
2783 |
|
{A1, _Reason} -> |
2784 |
9 |
Affiliation == A1; |
2785 |
|
_ -> |
2786 |
19 |
Affiliation == A |
2787 |
|
end |
2788 |
|
end, |
2789 |
19 |
maps:to_list(maps:filter(F, StateData#state.affiliations)). |
2790 |
|
|
2791 |
|
|
2792 |
|
-spec process_admin_items_set(jid:jid(), [exml:element(), ...], ejabberd:lang(), state()) -> |
2793 |
|
{'error', exml:element()} | {'result', [], state()}. |
2794 |
|
process_admin_items_set(UJID, Items, Lang, StateData) -> |
2795 |
133 |
UAffiliation = get_affiliation(UJID, StateData), |
2796 |
133 |
URole = get_role(UJID, StateData), |
2797 |
133 |
case find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, []) of |
2798 |
|
{result, Res} -> |
2799 |
|
%% TODO Pass Acc here |
2800 |
111 |
?LOG_INFO(ls(#{what => muc_admin_query, text => <<"Processing MUC admin query">>, |
2801 |
111 |
from_jid => jid:to_binary(UJID), result => Res}, StateData)), |
2802 |
111 |
NSD = lists:foldl( |
2803 |
|
fun(ChangedItem, SD) -> |
2804 |
133 |
process_admin_item_set(ChangedItem, UJID, SD) |
2805 |
|
end, StateData, Res), |
2806 |
111 |
save_persistent_room_state(NSD), |
2807 |
111 |
{result, [], NSD}; |
2808 |
|
Err -> |
2809 |
22 |
Err |
2810 |
|
end. |
2811 |
|
|
2812 |
|
process_admin_item_set(ChangedItem, UJID, SD) -> |
2813 |
133 |
try |
2814 |
133 |
process_admin_item_set_unsafe(ChangedItem, UJID, SD) |
2815 |
|
catch |
2816 |
|
Class:Reason:Stacktrace -> |
2817 |
:-( |
?LOG_ERROR(ls(#{what => muc_admin_item_set_failed, |
2818 |
|
from_jid => jid:to_binary(UJID), |
2819 |
|
changed_item => ChangedItem, |
2820 |
:-( |
class => Class, reason => Reason, stacktrace => Stacktrace}, SD)), |
2821 |
:-( |
SD |
2822 |
|
end. |
2823 |
|
|
2824 |
|
process_admin_item_set_unsafe({JID, affiliation, owner, _}, _UJID, SD) |
2825 |
|
when (JID#jid.luser == <<>>) -> |
2826 |
|
%% If the provided JID does not have username, |
2827 |
|
%% ignore the affiliation completely |
2828 |
:-( |
SD; |
2829 |
|
process_admin_item_set_unsafe({JID, role, none, Reason}, _UJID, SD) -> |
2830 |
6 |
safe_send_kickban_presence(JID, Reason, <<"307">>, SD), |
2831 |
6 |
set_role(JID, none, SD); |
2832 |
|
process_admin_item_set_unsafe({JID, affiliation, none, Reason}, _UJID, SD) -> |
2833 |
15 |
case (SD#state.config)#config.members_only of |
2834 |
|
true -> |
2835 |
3 |
safe_send_kickban_presence(JID, Reason, <<"321">>, none, SD), |
2836 |
3 |
SD1 = set_affiliation_and_reason(JID, none, Reason, SD), |
2837 |
3 |
set_role(JID, none, SD1); |
2838 |
|
_ -> |
2839 |
12 |
SD1 = set_affiliation_and_reason(JID, none, Reason, SD), |
2840 |
12 |
send_update_presence(JID, Reason, SD1), |
2841 |
12 |
SD1 |
2842 |
|
end; |
2843 |
|
process_admin_item_set_unsafe({JID, affiliation, outcast, Reason}, _UJID, SD) -> |
2844 |
10 |
safe_send_kickban_presence(JID, Reason, <<"301">>, outcast, SD), |
2845 |
10 |
set_affiliation_and_reason(JID, outcast, Reason, set_role(JID, none, SD)); |
2846 |
|
process_admin_item_set_unsafe({JID, affiliation, A, Reason}, _UJID, SD) |
2847 |
|
when (A == admin) or (A == owner) -> |
2848 |
19 |
SD1 = set_affiliation_and_reason(JID, A, Reason, SD), |
2849 |
19 |
SD2 = set_role(JID, moderator, SD1), |
2850 |
19 |
send_update_presence(JID, Reason, SD2), |
2851 |
19 |
SD2; |
2852 |
|
process_admin_item_set_unsafe({JID, affiliation, member, Reason}, UJID, SD) -> |
2853 |
52 |
case (SD#state.config)#config.members_only of |
2854 |
7 |
true -> send_invitation(UJID, JID, Reason, SD); |
2855 |
45 |
_ -> ok |
2856 |
|
end, |
2857 |
52 |
SD1 = set_affiliation_and_reason(JID, member, Reason, SD), |
2858 |
52 |
SD2 = set_role(JID, participant, SD1), |
2859 |
52 |
send_update_presence(JID, Reason, SD2), |
2860 |
52 |
SD2; |
2861 |
|
process_admin_item_set_unsafe({JID, role, Role, Reason}, _UJID, SD) -> |
2862 |
31 |
SD1 = set_role(JID, Role, SD), |
2863 |
31 |
catch send_new_presence(JID, Reason, SD1), |
2864 |
31 |
SD1; |
2865 |
|
process_admin_item_set_unsafe({JID, affiliation, A, Reason}, _UJID, SD) -> |
2866 |
:-( |
SD1 = set_affiliation(JID, A, SD), |
2867 |
:-( |
send_update_presence(JID, Reason, SD1), |
2868 |
:-( |
SD1. |
2869 |
|
|
2870 |
|
-type res_row() :: {jid:simple_jid() | jid:jid(), |
2871 |
|
'affiliation' | 'role', any(), any()}. |
2872 |
|
-type find_changed_items_res() :: {'error', exml:element()} | {'result', [res_row()]}. |
2873 |
|
-spec find_changed_items(jid:jid(), mod_muc:affiliation(), mod_muc:role(), |
2874 |
|
[exml:element()], ejabberd:lang(), state(), [res_row()]) -> |
2875 |
|
find_changed_items_res(). |
2876 |
|
find_changed_items(_UJID, _UAffiliation, _URole, [], _Lang, _StateData, Res) -> |
2877 |
111 |
{result, Res}; |
2878 |
|
find_changed_items(UJID, UAffiliation, URole, [#xmlcdata{} | Items], |
2879 |
|
Lang, StateData, Res) -> |
2880 |
:-( |
find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, Res); |
2881 |
|
find_changed_items(UJID, UAffiliation, URole, |
2882 |
|
[#xmlel{name = <<"item">>, attrs = Attrs} = Item | Items], |
2883 |
|
Lang, StateData, Res) -> |
2884 |
156 |
case get_affected_jid(Attrs, Lang, StateData) of |
2885 |
|
{value, JID} -> |
2886 |
151 |
check_changed_item(UJID, UAffiliation, URole, JID, Item, Items, Lang, StateData, Res); |
2887 |
|
Err -> |
2888 |
5 |
Err |
2889 |
|
end; |
2890 |
|
find_changed_items(_UJID, _UAffiliation, _URole, _Items, _Lang, _StateData, _Res) -> |
2891 |
:-( |
{error, mongoose_xmpp_errors:bad_request()}. |
2892 |
|
|
2893 |
|
-spec get_affected_jid(Attrs :: [{binary(), binary()}], |
2894 |
|
Lang :: ejabberd:lang(), |
2895 |
|
StateData :: state()) -> |
2896 |
|
{value,jid:jid()} | {error, exml:element()}. |
2897 |
|
get_affected_jid(Attrs, Lang, StateData) -> |
2898 |
156 |
case {xml:get_attr(<<"jid">>, Attrs), xml:get_attr(<<"nick">>, Attrs)} of |
2899 |
|
{{value, S}, _} -> |
2900 |
108 |
case jid:from_binary(S) of |
2901 |
|
error -> |
2902 |
1 |
ErrText = <<(translate:translate(Lang, <<"Jabber ID ">>))/binary, |
2903 |
|
S/binary, (translate:translate(Lang, <<" is invalid">>))/binary>>, |
2904 |
1 |
{error, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)}; |
2905 |
|
J -> |
2906 |
107 |
{value, J} |
2907 |
|
end; |
2908 |
|
{_, {value, N}} -> |
2909 |
48 |
case find_jids_by_nick(N, StateData) of |
2910 |
|
[] -> |
2911 |
4 |
ErrText |
2912 |
|
= <<(translate:translate(Lang, <<"Nickname ">>))/binary, N/binary, |
2913 |
|
(translate:translate(Lang, <<" does not exist in the room">>))/binary>>, |
2914 |
4 |
{error, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)}; |
2915 |
|
[FirstSessionJid | _RestOfSessions] -> |
2916 |
44 |
{value, FirstSessionJid} |
2917 |
|
end; |
2918 |
|
_ -> |
2919 |
:-( |
{error, mongoose_xmpp_errors:bad_request()} |
2920 |
|
end. |
2921 |
|
|
2922 |
|
-spec check_changed_item(jid:jid(), mod_muc:affiliation(), mod_muc:role(),jid:jid(), exml:element(), |
2923 |
|
[exml:element()], ejabberd:lang(), state(), [res_row()]) -> |
2924 |
|
find_changed_items_res(). |
2925 |
|
check_changed_item(UJID, UAffiliation, URole, JID, #xmlel{ attrs = Attrs } = Item, Items, |
2926 |
|
Lang, StateData, Res) -> |
2927 |
151 |
TAffiliation = get_affiliation(JID, StateData), |
2928 |
151 |
TRole = get_role(JID, StateData), |
2929 |
151 |
case which_property_changed(Attrs, Lang) of |
2930 |
|
{role, Role} -> |
2931 |
43 |
ServiceAf = get_service_affiliation(JID, StateData), |
2932 |
43 |
CanChangeRA = |
2933 |
|
case can_change_ra(UAffiliation, URole, TAffiliation, TRole, role, Role, ServiceAf) of |
2934 |
1 |
nothing -> nothing; |
2935 |
37 |
true -> true; |
2936 |
:-( |
check_owner -> is_owner(UJID, StateData); |
2937 |
5 |
_ -> false |
2938 |
|
end, |
2939 |
43 |
case CanChangeRA of |
2940 |
1 |
nothing -> find_changed_items(UJID, UAffiliation, URole, |
2941 |
|
Items, Lang, StateData, Res); |
2942 |
37 |
true -> find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, |
2943 |
|
[{JID, role, Role, decode_reason(Item)} | Res]); |
2944 |
5 |
_ -> {error, mongoose_xmpp_errors:not_allowed()} |
2945 |
|
end; |
2946 |
|
{affiliation, Affiliation} -> |
2947 |
106 |
ServiceAf = get_service_affiliation(JID, StateData), |
2948 |
106 |
CanChangeRA = |
2949 |
|
case can_change_ra(UAffiliation, URole, TAffiliation, TRole, |
2950 |
|
affiliation, Affiliation, ServiceAf) of |
2951 |
:-( |
nothing -> nothing; |
2952 |
90 |
true -> true; |
2953 |
2 |
cancel -> cancel; |
2954 |
6 |
check_owner -> is_owner(UJID, StateData); |
2955 |
|
_ -> |
2956 |
8 |
false |
2957 |
|
end, |
2958 |
106 |
case CanChangeRA of |
2959 |
:-( |
nothing -> find_changed_items(UJID, UAffiliation, URole, Items, |
2960 |
|
Lang, StateData, Res); |
2961 |
96 |
true -> find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, |
2962 |
|
[{jid:to_bare(JID), affiliation, |
2963 |
|
Affiliation, decode_reason(Item)} | Res]); |
2964 |
2 |
cancel -> {error, mongoose_xmpp_errors:not_allowed()}; |
2965 |
8 |
false -> {error, mongoose_xmpp_errors:forbidden()} |
2966 |
|
end; |
2967 |
2 |
Err -> Err |
2968 |
|
end. |
2969 |
|
|
2970 |
|
-spec is_owner(UJID ::jid:jid(), StateData :: state()) -> boolean(). |
2971 |
|
is_owner(UJID, StateData) -> |
2972 |
6 |
case search_affiliation(owner, StateData) of |
2973 |
:-( |
[{OJID, _}] -> jid:to_bare(OJID) /= jid:to_lower(jid:to_bare(UJID)); |
2974 |
6 |
_ -> true |
2975 |
|
end. |
2976 |
|
|
2977 |
|
-spec which_property_changed(Attrs :: [{binary(), binary()}], Lang :: ejabberd:lang()) -> |
2978 |
|
{affiliation, mod_muc:affiliation()} | {role, mod_muc:role()} | {error, exml:element()}. |
2979 |
|
which_property_changed(Attrs, Lang) -> |
2980 |
151 |
case {xml:get_attr(<<"role">>, Attrs), xml:get_attr(<<"affiliation">>, Attrs)} of |
2981 |
|
{false, false} -> |
2982 |
:-( |
{error, mongoose_xmpp_errors:bad_request()}; |
2983 |
|
{false, {value, BAffiliation}} -> |
2984 |
107 |
case catch binary_to_affiliation(BAffiliation) of |
2985 |
|
{'EXIT', _} -> |
2986 |
1 |
ErrText1 = <<(translate:translate(Lang, <<"Invalid affiliation ">>))/binary, |
2987 |
|
BAffiliation/binary>>, |
2988 |
1 |
{error, mongoose_xmpp_errors:not_acceptable(Lang, ErrText1)}; |
2989 |
|
Affiliation -> |
2990 |
106 |
{affiliation, Affiliation} |
2991 |
|
end; |
2992 |
|
{{value, BRole}, _} -> |
2993 |
44 |
case catch binary_to_role(BRole) of |
2994 |
|
{'EXIT', _} -> |
2995 |
1 |
ErrText1 = <<(translate:translate(Lang, <<"Invalid role ">>))/binary, |
2996 |
|
BRole/binary>>, |
2997 |
1 |
{error, mongoose_xmpp_errors:bad_request(Lang, ErrText1)}; |
2998 |
|
Role -> |
2999 |
43 |
{role, Role} |
3000 |
|
end |
3001 |
|
end. |
3002 |
|
|
3003 |
|
-spec can_change_ra(FAff :: mod_muc:affiliation(), FRole :: mod_muc:role(), |
3004 |
|
TAff :: mod_muc:affiliation(), TRole :: mod_muc:role(), |
3005 |
|
RoleOrAff :: affiliation | role, Value :: any(), |
3006 |
|
ServiceAff :: mod_muc:affiliation()) |
3007 |
|
-> cancel | check_owner | false | nothing | true. |
3008 |
|
can_change_ra(FAff, _FRole, TAff, _TRole, affiliation, Value, ServiceAff) -> |
3009 |
106 |
can_change_aff(FAff, TAff, Value, ServiceAff); |
3010 |
|
can_change_ra(FAff, FRole, TAff, TRole, role, Value, ServiceAff) -> |
3011 |
43 |
can_change_role(FAff, FRole, TAff, TRole, Value, ServiceAff). |
3012 |
|
|
3013 |
|
%% A room owner tries to add as persistent owner a |
3014 |
|
%% participant that is already owner because he is MUC admin: |
3015 |
:-( |
can_change_aff(_FAff, owner, owner, owner) -> true; |
3016 |
|
%% Nobody can decrease MUC admin's role/affiliation: |
3017 |
:-( |
can_change_aff(_FAff, _TAff, _Value, owner) -> false; |
3018 |
|
can_change_aff(FAff, TAff, Value, _ServiceAf) -> |
3019 |
106 |
can_change_aff(FAff, TAff, Value). |
3020 |
|
|
3021 |
|
%% Nobody can decrease MUC admin's role/affiliation: |
3022 |
:-( |
can_change_role(_FAff, _FRole, _TAff, _TRole, _Value, owner) -> false; |
3023 |
|
can_change_role(FAff, FRole, TAff, TRole, Value, _ServiceAf) -> |
3024 |
43 |
can_change_role(FAff, FRole, TAff, TRole, Value). |
3025 |
|
|
3026 |
|
%% Arguments: |
3027 |
|
%% - Affiliation of the user making the request |
3028 |
|
%% - Old affiliation |
3029 |
|
%% - New affiliation |
3030 |
:-( |
can_change_aff(_FAff, Aff, Aff) -> nothing; |
3031 |
2 |
can_change_aff(owner, outcast, none) -> true; |
3032 |
:-( |
can_change_aff(owner, outcast, member) -> true; |
3033 |
:-( |
can_change_aff(owner, outcast, admin) -> true; |
3034 |
:-( |
can_change_aff(owner, outcast, owner) -> true; |
3035 |
9 |
can_change_aff(owner, none, outcast) -> true; |
3036 |
51 |
can_change_aff(owner, none, member) -> true; |
3037 |
5 |
can_change_aff(owner, none, admin) -> true; |
3038 |
4 |
can_change_aff(owner, none, owner) -> true; |
3039 |
:-( |
can_change_aff(owner, member, outcast) -> true; |
3040 |
6 |
can_change_aff(owner, member, none) -> true; |
3041 |
4 |
can_change_aff(owner, member, admin) -> true; |
3042 |
:-( |
can_change_aff(owner, member, owner) -> true; |
3043 |
6 |
can_change_aff(owner, admin, _Aff) -> true; |
3044 |
6 |
can_change_aff(owner, owner, _Aff) -> check_owner; |
3045 |
1 |
can_change_aff(admin, none, member) -> true; |
3046 |
1 |
can_change_aff(admin, none, outcast) -> true; |
3047 |
:-( |
can_change_aff(admin, outcast, none) -> true; |
3048 |
:-( |
can_change_aff(admin, outcast, member) -> true; |
3049 |
:-( |
can_change_aff(admin, member, outcast) -> true; |
3050 |
1 |
can_change_aff(admin, member, none) -> true; |
3051 |
:-( |
can_change_aff(none, admin, _Aff) -> cancel; |
3052 |
1 |
can_change_aff(none, owner, _Aff) -> cancel; |
3053 |
1 |
can_change_aff(admin, owner, _Aff) -> cancel; |
3054 |
8 |
can_change_aff(_FAff, _TAff, _Aff) -> false. |
3055 |
|
|
3056 |
|
%% Arguments: |
3057 |
|
%% - Affiliation of the user making the request |
3058 |
|
%% - Role of the user making the request |
3059 |
|
%% - Old affiliation |
3060 |
|
%% - Old role |
3061 |
|
%% - New role |
3062 |
1 |
can_change_role(_FAff, _FRole, _TAff, Role, Role) -> nothing; |
3063 |
2 |
can_change_role(_FAff, moderator, _TAff, visitor, none) -> true; |
3064 |
10 |
can_change_role(_FAff, moderator, _TAff, visitor, participant) -> true; |
3065 |
:-( |
can_change_role(owner, _FRole, _TAff, visitor, moderator) -> true; |
3066 |
:-( |
can_change_role(admin, _FRole, _TAff, visitor, moderator) -> true; |
3067 |
4 |
can_change_role(_FAff, moderator, _TAff, participant, none) -> true; |
3068 |
8 |
can_change_role(_FAff, moderator, _TAff, participant, visitor) -> true; |
3069 |
9 |
can_change_role(owner, _FRole, _TAff, participant, moderator) -> true; |
3070 |
:-( |
can_change_role(admin, _FRole, _TAff, participant, moderator) -> true; |
3071 |
|
%% Owner/admin are always moderators: |
3072 |
1 |
can_change_role(_FAff, _FRole, owner, moderator, visitor) -> false; |
3073 |
:-( |
can_change_role(_FAff, _FRole, admin, moderator, visitor) -> false; |
3074 |
1 |
can_change_role(_FAff, _FRole, owner, moderator, participant) -> false; |
3075 |
:-( |
can_change_role(_FAff, _FRole, admin, moderator, participant) -> false; |
3076 |
|
%% Non owner/admin could loose their moderator status: |
3077 |
:-( |
can_change_role(owner, _FRole, _TAff, moderator, visitor) -> true; |
3078 |
:-( |
can_change_role(admin, _FRole, _TAff, moderator, visitor) -> true; |
3079 |
4 |
can_change_role(owner, _FRole, _TAff, moderator, participant) -> true; |
3080 |
:-( |
can_change_role(admin, _FRole, _TAff, moderator, participant) -> true; |
3081 |
3 |
can_change_role(_FAff, _FRole, _TAff, _TRole, _NewRole) -> false. |
3082 |
|
|
3083 |
|
safe_send_kickban_presence(JID, Reason, Code, StateData) -> |
3084 |
6 |
try |
3085 |
6 |
send_kickban_presence(JID, Reason, Code, StateData) |
3086 |
|
catch |
3087 |
|
Class:ErrorReason:Stacktrace -> |
3088 |
:-( |
?LOG_ERROR(ls(#{what => muc_send_kickban_presence_failed, |
3089 |
|
kick_jid => jid:to_binary(JID), kick_reason => Reason, |
3090 |
:-( |
class => Class, reason => ErrorReason, stacktrace => Stacktrace}, StateData)) |
3091 |
|
end. |
3092 |
|
|
3093 |
|
-spec send_kickban_presence(jid:jid(), binary(), Code :: binary(), |
3094 |
|
state()) -> any(). |
3095 |
|
send_kickban_presence(JID, Reason, Code, StateData) -> |
3096 |
6 |
NewAffiliation = get_affiliation(JID, StateData), |
3097 |
6 |
send_kickban_presence(JID, Reason, Code, NewAffiliation, StateData). |
3098 |
|
|
3099 |
|
|
3100 |
|
safe_send_kickban_presence(JID, Reason, Code, NewAffiliation, StateData) -> |
3101 |
13 |
try |
3102 |
13 |
send_kickban_presence(JID, Reason, Code, NewAffiliation, StateData) |
3103 |
|
catch |
3104 |
|
Class:ErrorReason:Stacktrace -> |
3105 |
:-( |
?LOG_ERROR(ls(#{what => muc_send_kickban_presence_failed, |
3106 |
|
new_affiliation => NewAffiliation, |
3107 |
|
kick_jid => jid:to_binary(JID), kick_reason => Reason, |
3108 |
:-( |
class => Class, reason => ErrorReason, stacktrace => Stacktrace}, StateData)) |
3109 |
|
end. |
3110 |
|
|
3111 |
|
-spec send_kickban_presence(jid:simple_jid() | jid:jid(), |
3112 |
|
Reason :: binary(), Code :: binary(), |
3113 |
|
mod_muc:affiliation(), state()) -> any(). |
3114 |
|
send_kickban_presence(JID, Reason, Code, NewAffiliation, StateData) -> |
3115 |
19 |
foreach_matched_user(fun(#user{nick = Nick, jid = J}) -> |
3116 |
11 |
add_to_log(kickban, {Nick, Reason, Code}, StateData), |
3117 |
11 |
tab_remove_online_user(J, StateData), |
3118 |
11 |
send_kickban_presence1(J, Reason, Code, NewAffiliation, StateData) |
3119 |
|
end, JID, StateData). |
3120 |
|
|
3121 |
|
|
3122 |
|
-spec send_kickban_presence1(jid:jid(), Reason :: binary(), Code :: binary(), |
3123 |
|
mod_muc:affiliation(), state()) -> 'ok'. |
3124 |
|
send_kickban_presence1(UJID, Reason, Code, Affiliation, StateData) -> |
3125 |
11 |
{ok, #user{jid = RealJID, |
3126 |
|
nick = Nick}} = |
3127 |
|
maps:find(jid:to_lower(UJID), StateData#state.users), |
3128 |
11 |
BAffiliation = affiliation_to_binary(Affiliation), |
3129 |
11 |
BannedJIDString = jid:to_binary(RealJID), |
3130 |
11 |
F = fun(Info) -> |
3131 |
20 |
JidAttrList = case (Info#user.role == moderator) orelse |
3132 |
|
((StateData#state.config)#config.anonymous |
3133 |
14 |
== false) of |
3134 |
6 |
true -> [{<<"jid">>, BannedJIDString}]; |
3135 |
14 |
false -> [] |
3136 |
|
end, |
3137 |
20 |
ItemAttrs = [{<<"affiliation">>, BAffiliation}, |
3138 |
|
{<<"role">>, <<"none">>}] ++ JidAttrList, |
3139 |
20 |
ItemEls = case Reason of |
3140 |
|
<<>> -> |
3141 |
8 |
[]; |
3142 |
|
_ -> |
3143 |
12 |
[#xmlel{name = <<"reason">>, children = [#xmlcdata{content = Reason}]}] |
3144 |
|
end, |
3145 |
20 |
Packet = #xmlel{name = <<"presence">>, |
3146 |
|
attrs = [{<<"type">>, <<"unavailable">>}], |
3147 |
|
children = [#xmlel{name = <<"x">>, |
3148 |
|
attrs = [{<<"xmlns">>, ?NS_MUC_USER}], |
3149 |
|
children = [#xmlel{name = <<"item">>, |
3150 |
|
attrs = ItemAttrs, |
3151 |
|
children = ItemEls}, |
3152 |
|
#xmlel{name = <<"status">>, |
3153 |
|
attrs = [{<<"code">>, Code}]}]}]}, |
3154 |
20 |
ejabberd_router:route( |
3155 |
|
jid:replace_resource(StateData#state.jid, Nick), |
3156 |
|
Info#user.jid, |
3157 |
|
Packet) |
3158 |
|
end, |
3159 |
11 |
foreach_user(F, StateData). |
3160 |
|
|
3161 |
|
|
3162 |
|
|
3163 |
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
3164 |
|
% Owner stuff |
3165 |
|
|
3166 |
|
-spec process_iq_owner(jid:jid(), get | set, ejabberd:lang(), exml:element(), |
3167 |
|
state(), statename()) -> |
3168 |
|
{error, exml:element()} | {result, [exml:element() | jlib:xmlcdata()], state() | stop}. |
3169 |
|
process_iq_owner(From, Type, Lang, SubEl, StateData, StateName) -> |
3170 |
61 |
case get_affiliation(From, StateData) of |
3171 |
|
owner -> |
3172 |
59 |
process_authorized_iq_owner(From, Type, Lang, SubEl, StateData, StateName); |
3173 |
|
_ -> |
3174 |
2 |
ErrText = <<"Owner privileges required">>, |
3175 |
2 |
{error, mongoose_xmpp_errors:forbidden(Lang, ErrText)} |
3176 |
|
end. |
3177 |
|
|
3178 |
|
-spec process_authorized_iq_owner(jid:jid(), get | set, ejabberd:lang(), exml:element(), |
3179 |
|
state(), statename()) -> |
3180 |
|
{error, exml:element()} | {result, [exml:element() | jlib:xmlcdata()], state() | stop}. |
3181 |
|
process_authorized_iq_owner(From, set, Lang, SubEl, StateData, StateName) -> |
3182 |
48 |
#xmlel{children = Els} = SubEl, |
3183 |
48 |
case xml:remove_cdata(Els) of |
3184 |
|
[#xmlel{name = <<"destroy">>} = SubEl1] -> |
3185 |
5 |
?LOG_INFO(ls(#{what => muc_room_destroy, |
3186 |
|
text => <<"Destroyed MUC room by the owner">>, |
3187 |
5 |
from_jid => jid:to_binary(From)}, StateData)), |
3188 |
5 |
add_to_log(room_existence, destroyed, StateData), |
3189 |
5 |
destroy_room(SubEl1, StateData); |
3190 |
|
[XEl] -> |
3191 |
42 |
case {mongoose_data_forms:parse_form(XEl), StateName} of |
3192 |
|
{#{type := <<"cancel">>}, locked_state} -> |
3193 |
2 |
?LOG_INFO(ls(#{what => muc_cancel_locked, |
3194 |
|
text => <<"Received cancel before the room was configured " |
3195 |
|
"- destroy room">>, |
3196 |
2 |
from_jid => jid:to_binary(From)}, StateData)), |
3197 |
2 |
add_to_log(room_existence, destroyed, StateData), |
3198 |
2 |
destroy_room(XEl, StateData); |
3199 |
|
{#{type := <<"cancel">>}, normal_state} -> |
3200 |
|
%% received cancel when room was configured - continue without changes |
3201 |
2 |
{result, [], StateData}; |
3202 |
|
{#{type := <<"submit">>, kvs := KVs}, _} -> |
3203 |
36 |
process_authorized_submit_owner(From, maps:to_list(KVs), StateData); |
3204 |
|
{{error, Msg}, _} -> |
3205 |
1 |
{error, mongoose_xmpp_errors:bad_request(Lang, Msg)}; |
3206 |
|
_ -> |
3207 |
1 |
{error, mongoose_xmpp_errors:bad_request(Lang, <<"Invalid form type">>)} |
3208 |
|
end; |
3209 |
|
_ -> |
3210 |
1 |
{error, mongoose_xmpp_errors:bad_request()} |
3211 |
|
end; |
3212 |
|
process_authorized_iq_owner(From, get, Lang, SubEl, StateData, _StateName) -> |
3213 |
11 |
case exml_query:path(SubEl, [{element, <<"item">>}, {attr, <<"affiliation">>}]) of |
3214 |
|
undefined -> |
3215 |
11 |
get_config(Lang, StateData, From); |
3216 |
|
BAffiliation -> |
3217 |
:-( |
case catch binary_to_affiliation(BAffiliation) of |
3218 |
|
{'EXIT', _} -> |
3219 |
:-( |
InvAffT = translate:translate(Lang, <<"Invalid affiliation ">>), |
3220 |
:-( |
ErrText = <<InvAffT/binary, BAffiliation/binary>>, |
3221 |
:-( |
{error, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)}; |
3222 |
|
Affiliation -> |
3223 |
:-( |
Items = items_with_affiliation(Affiliation, StateData), |
3224 |
:-( |
{result, Items, StateData} |
3225 |
|
end |
3226 |
|
end. |
3227 |
|
|
3228 |
|
-spec process_authorized_submit_owner(From ::jid:jid(), [{binary(), [binary()]}], |
3229 |
|
StateData :: state()) -> |
3230 |
|
{error, exml:element()} | {result, [exml:element() | jlib:xmlcdata()], state() | stop}. |
3231 |
|
process_authorized_submit_owner(_From, [], StateData) -> |
3232 |
|
%confirm an instant room |
3233 |
13 |
save_persistent_room_state(StateData), |
3234 |
13 |
{result, [], StateData}; |
3235 |
|
process_authorized_submit_owner(From, XData, StateData) -> |
3236 |
|
%attempt to configure |
3237 |
23 |
case is_allowed_log_change(XData, StateData, From) |
3238 |
23 |
andalso is_allowed_persistent_change(XData, StateData, From) |
3239 |
23 |
andalso is_allowed_room_name_desc_limits(XData, StateData) |
3240 |
23 |
andalso is_password_settings_correct(XData, StateData) of |
3241 |
22 |
true -> set_config(XData, StateData); |
3242 |
1 |
false -> {error, mongoose_xmpp_errors:not_acceptable(<<"en">>, <<"not allowed to configure">>)} |
3243 |
|
end. |
3244 |
|
|
3245 |
|
-spec is_allowed_log_change([{binary(), [binary()]}], state(), jid:jid()) -> boolean(). |
3246 |
|
is_allowed_log_change(XData, StateData, From) -> |
3247 |
23 |
case lists:keymember(<<"muc#roomconfig_enablelogging">>, 1, XData) of |
3248 |
|
false -> |
3249 |
21 |
true; |
3250 |
|
true -> |
3251 |
2 |
(allow == mod_muc_log:check_access_log( |
3252 |
|
StateData#state.host_type, |
3253 |
|
StateData#state.server_host, From)) |
3254 |
|
end. |
3255 |
|
|
3256 |
|
|
3257 |
|
-spec is_allowed_persistent_change([{binary(), [binary()]}], state(), jid:jid()) -> boolean(). |
3258 |
|
is_allowed_persistent_change(XData, StateData, From) -> |
3259 |
23 |
case lists:keymember(<<"muc#roomconfig_persistentroom">>, 1, XData) of |
3260 |
|
false -> |
3261 |
13 |
true; |
3262 |
|
true -> |
3263 |
10 |
AccessPersistent = access_persistent(StateData), |
3264 |
10 |
(allow == acl:match_rule(StateData#state.host_type, StateData#state.server_host, |
3265 |
|
AccessPersistent, From)) |
3266 |
|
end. |
3267 |
|
|
3268 |
|
|
3269 |
|
%% @doc Check if the Room Name and Room Description defined in the Data Form |
3270 |
|
%% are conformant to the configured limits |
3271 |
|
-spec is_allowed_room_name_desc_limits([{binary(), [binary()]}], state()) -> boolean(). |
3272 |
|
is_allowed_room_name_desc_limits(XData, StateData) -> |
3273 |
23 |
IsNameAccepted = |
3274 |
|
case lists:keysearch(<<"muc#roomconfig_roomname">>, 1, XData) of |
3275 |
|
{value, {_, [N]}} -> |
3276 |
:-( |
byte_size(N) =< get_opt(StateData, max_room_name); |
3277 |
|
_ -> |
3278 |
23 |
true |
3279 |
|
end, |
3280 |
23 |
IsDescAccepted = |
3281 |
|
case lists:keysearch(<<"muc#roomconfig_roomdesc">>, 1, XData) of |
3282 |
|
{value, {_, [D]}} -> |
3283 |
:-( |
byte_size(D) =< get_opt(StateData, max_room_desc); |
3284 |
|
_ -> |
3285 |
23 |
true |
3286 |
|
end, |
3287 |
23 |
IsNameAccepted and IsDescAccepted. |
3288 |
|
|
3289 |
|
%% @doc Return false if: |
3290 |
|
%% `<<"the password for a password-protected room is blank">>' |
3291 |
|
-spec is_password_settings_correct([{binary(), [binary()]}], state()) -> boolean(). |
3292 |
|
is_password_settings_correct(KVs, StateData) -> |
3293 |
23 |
Config = StateData#state.config, |
3294 |
23 |
OldProtected = Config#config.password_protected, |
3295 |
23 |
OldPassword = Config#config.password, |
3296 |
23 |
NewProtected = |
3297 |
|
case lists:keysearch(<<"muc#roomconfig_passwordprotectedroom">>, 1, KVs) of |
3298 |
|
{value, {_, [<<"1">>]}} -> |
3299 |
1 |
true; |
3300 |
|
{value, {_, [<<"0">>]}} -> |
3301 |
:-( |
false; |
3302 |
|
_ -> |
3303 |
22 |
undefined |
3304 |
|
end, |
3305 |
23 |
NewPassword = |
3306 |
|
case lists:keysearch(<<"muc#roomconfig_roomsecret">>, 1, KVs) of |
3307 |
|
{value, {_, [P]}} -> |
3308 |
1 |
P; |
3309 |
|
_ -> |
3310 |
22 |
undefined |
3311 |
|
end, |
3312 |
23 |
case {OldProtected, NewProtected, OldPassword, NewPassword} of |
3313 |
|
{true, undefined, <<>>, undefined} -> |
3314 |
:-( |
false; |
3315 |
|
{true, undefined, _, <<>>} -> |
3316 |
:-( |
false; |
3317 |
|
{_, true, <<>>, undefined} -> |
3318 |
:-( |
false; |
3319 |
|
{_, true, _, <<>>} -> |
3320 |
1 |
false; |
3321 |
|
_ -> |
3322 |
22 |
true |
3323 |
|
end. |
3324 |
|
|
3325 |
|
-spec get_default_room_maxusers(state()) -> any(). |
3326 |
|
get_default_room_maxusers(RoomState) -> |
3327 |
11 |
#{max_users := MaxUsers} = get_opt(RoomState, default_room), |
3328 |
11 |
MaxUsers. |
3329 |
|
|
3330 |
|
-spec get_config(ejabberd:lang(), state(), jid:jid()) |
3331 |
|
-> {'result', [exml:element(), ...], state()}. |
3332 |
|
get_config(Lang, StateData, From) -> |
3333 |
11 |
AccessPersistent = access_persistent(StateData), |
3334 |
11 |
Config = StateData#state.config, |
3335 |
11 |
TitleTxt = translate:translate(Lang, <<"Configuration of room ">>), |
3336 |
11 |
Title = <<TitleTxt/binary, (jid:to_binary(StateData#state.jid))/binary>>, |
3337 |
11 |
Fields = |
3338 |
|
[stringxfield(<<"Room title">>, |
3339 |
|
<<"muc#roomconfig_roomname">>, |
3340 |
|
Config#config.title, Lang), |
3341 |
|
stringxfield(<<"Room description">>, |
3342 |
|
<<"muc#roomconfig_roomdesc">>, |
3343 |
|
Config#config.description, Lang) |
3344 |
|
] ++ |
3345 |
|
case acl:match_rule(StateData#state.host_type, StateData#state.server_host, |
3346 |
|
AccessPersistent, From) of |
3347 |
|
allow -> |
3348 |
11 |
[boolxfield(<<"Make room persistent">>, |
3349 |
|
<<"muc#roomconfig_persistentroom">>, |
3350 |
|
Config#config.persistent, Lang)]; |
3351 |
:-( |
_ -> [] |
3352 |
|
end ++ [ |
3353 |
|
boolxfield(<<"Make room public searchable">>, |
3354 |
|
<<"muc#roomconfig_publicroom">>, |
3355 |
|
Config#config.public, Lang), |
3356 |
|
boolxfield(<<"Make participants list public">>, |
3357 |
|
<<"public_list">>, |
3358 |
|
Config#config.public_list, Lang), |
3359 |
|
boolxfield(<<"Make room password protected">>, |
3360 |
|
<<"muc#roomconfig_passwordprotectedroom">>, |
3361 |
|
Config#config.password_protected, Lang), |
3362 |
|
privatexfield(<<"Password">>, |
3363 |
|
<<"muc#roomconfig_roomsecret">>, |
3364 |
|
case Config#config.password_protected of |
3365 |
:-( |
true -> Config#config.password; |
3366 |
11 |
false -> <<>> |
3367 |
|
end, Lang), |
3368 |
|
getmemberlist_field(Lang), |
3369 |
|
maxusers_field(Lang, StateData), |
3370 |
|
whois_field(Lang, Config), |
3371 |
|
boolxfield(<<"Make room members-only">>, |
3372 |
|
<<"muc#roomconfig_membersonly">>, |
3373 |
|
Config#config.members_only, Lang), |
3374 |
|
boolxfield(<<"Make room moderated">>, |
3375 |
|
<<"muc#roomconfig_moderatedroom">>, |
3376 |
|
Config#config.moderated, Lang), |
3377 |
|
boolxfield(<<"Default users as participants">>, |
3378 |
|
<<"members_by_default">>, |
3379 |
|
Config#config.members_by_default, Lang), |
3380 |
|
boolxfield(<<"Allow users to change the subject">>, |
3381 |
|
<<"muc#roomconfig_changesubject">>, |
3382 |
|
Config#config.allow_change_subj, Lang), |
3383 |
|
boolxfield(<<"Allow users to send private messages">>, |
3384 |
|
<<"allow_private_messages">>, |
3385 |
|
Config#config.allow_private_messages, Lang), |
3386 |
|
boolxfield(<<"Allow users to query other users">>, |
3387 |
|
<<"allow_query_users">>, |
3388 |
|
Config#config.allow_query_users, Lang), |
3389 |
|
boolxfield(<<"Allow users to send invites">>, |
3390 |
|
<<"muc#roomconfig_allowinvites">>, |
3391 |
|
Config#config.allow_user_invites, Lang), |
3392 |
|
boolxfield(<<"Allow users to enter room with multiple sessions">>, |
3393 |
|
<<"muc#roomconfig_allowmultisessions">>, |
3394 |
|
Config#config.allow_multiple_sessions, Lang), |
3395 |
|
boolxfield(<<"Allow visitors to send status text in presence updates">>, |
3396 |
|
<<"muc#roomconfig_allowvisitorstatus">>, |
3397 |
|
Config#config.allow_visitor_status, Lang), |
3398 |
|
boolxfield(<<"Allow visitors to change nickname">>, |
3399 |
|
<<"muc#roomconfig_allowvisitornickchange">>, |
3400 |
|
Config#config.allow_visitor_nickchange, Lang) |
3401 |
|
] ++ |
3402 |
|
case mod_muc_log:check_access_log(StateData#state.host_type, |
3403 |
|
StateData#state.server_host, From) of |
3404 |
|
allow -> |
3405 |
11 |
[boolxfield( |
3406 |
|
<<"Enable logging">>, |
3407 |
|
<<"muc#roomconfig_enablelogging">>, |
3408 |
|
Config#config.logging, Lang)]; |
3409 |
:-( |
_ -> [] |
3410 |
|
end, |
3411 |
11 |
InstructionsTxt = translate:translate( |
3412 |
|
Lang, <<"You need an x:data capable client to configure room">>), |
3413 |
11 |
{result, [#xmlel{name = <<"instructions">>, children = [#xmlcdata{content = InstructionsTxt}]}, |
3414 |
|
mongoose_data_forms:form(#{title => Title, ns => ?NS_MUC_CONFIG, fields => Fields})], |
3415 |
|
StateData}. |
3416 |
|
|
3417 |
|
-spec getmemberlist_field(Lang :: ejabberd:lang()) -> mongoose_data_forms:field(). |
3418 |
|
getmemberlist_field(Lang) -> |
3419 |
11 |
LabelTxt = translate:translate( |
3420 |
|
Lang, <<"Roles and affiliations that may retrieve member list">>), |
3421 |
11 |
Values = [<<"moderator">>, <<"participant">>, <<"visitor">>], |
3422 |
11 |
Options = [{translate:translate(Lang, Opt), Opt} || Opt <- Values], |
3423 |
11 |
#{type => <<"list-multi">>, label => LabelTxt, |
3424 |
|
var => <<"muc#roomconfig_getmemberlist">>, values => Values, options => Options}. |
3425 |
|
|
3426 |
|
maxusers_field(Lang, StateData) -> |
3427 |
11 |
ServiceMaxUsers = get_service_max_users(StateData), |
3428 |
11 |
DefaultRoomMaxUsers = get_default_room_maxusers(StateData), |
3429 |
11 |
{MaxUsersRoomInteger, MaxUsersRoomString} = |
3430 |
|
case get_max_users(StateData) of |
3431 |
|
N when is_integer(N) -> |
3432 |
11 |
{N, integer_to_binary(N)}; |
3433 |
:-( |
_ -> {0, <<"none">>} |
3434 |
|
end, |
3435 |
11 |
LabelTxt = translate:translate(Lang, <<"Maximum Number of Occupants">>), |
3436 |
11 |
Options = if |
3437 |
11 |
is_integer(ServiceMaxUsers) -> []; |
3438 |
:-( |
true -> {translate:translate(Lang, <<"No limit">>), <<"none">>} |
3439 |
|
end ++ |
3440 |
77 |
[integer_to_binary(N) || |
3441 |
11 |
N <- lists:usort([ServiceMaxUsers, DefaultRoomMaxUsers, MaxUsersRoomInteger | |
3442 |
121 |
?MAX_USERS_DEFAULT_LIST]), N =< ServiceMaxUsers], |
3443 |
11 |
#{type => <<"list-single">>, label => LabelTxt, |
3444 |
|
var => <<"muc#roomconfig_maxusers">>, values => [MaxUsersRoomString], options => Options}. |
3445 |
|
|
3446 |
|
-spec whois_field(Lang :: ejabberd:lang(), Config :: config()) -> mongoose_data_forms:field(). |
3447 |
|
whois_field(Lang, Config) -> |
3448 |
11 |
Value = if Config#config.anonymous -> <<"moderators">>; |
3449 |
1 |
true -> <<"anyone">> |
3450 |
|
end, |
3451 |
11 |
Options = [{translate:translate(Lang, <<"moderators only">>), <<"moderators">>}, |
3452 |
|
{translate:translate(Lang, <<"anyone">>), <<"anyone">>}], |
3453 |
11 |
#{type => <<"list-single">>, label => translate:translate(Lang, <<"moderators only">>), |
3454 |
|
var => <<"muc#roomconfig_whois">>, values => [Value], options => Options}. |
3455 |
|
|
3456 |
|
-spec set_config([{binary(), [binary()]}], state()) -> any(). |
3457 |
|
set_config(XData, StateData) -> |
3458 |
22 |
case set_xoption(XData, StateData#state.config) of |
3459 |
|
#config{} = Config -> |
3460 |
22 |
Res = change_config(Config, StateData), |
3461 |
22 |
{result, _, NSD} = Res, |
3462 |
22 |
PrevLogging = (StateData#state.config)#config.logging, |
3463 |
22 |
NewLogging = Config#config.logging, |
3464 |
22 |
PrevAnon = (StateData#state.config)#config.anonymous, |
3465 |
22 |
NewAnon = Config#config.anonymous, |
3466 |
22 |
Type = notify_config_change_and_get_type(PrevLogging, NewLogging, |
3467 |
|
PrevAnon, NewAnon, StateData), |
3468 |
22 |
Users = [{U#user.jid, U#user.nick, U#user.role} || |
3469 |
22 |
{_, U} <- maps:to_list(StateData#state.users)], |
3470 |
22 |
add_to_log(Type, Users, NSD), |
3471 |
22 |
Res; |
3472 |
|
Err -> |
3473 |
:-( |
Err |
3474 |
|
end. |
3475 |
|
|
3476 |
|
-spec notify_config_change_and_get_type(PrevLogging :: boolean(), NewLogging :: boolean(), |
3477 |
|
PrevAnon :: boolean(), NewAnon :: boolean(), |
3478 |
|
StateData :: state()) -> |
3479 |
|
roomconfig_change_disabledlogging | roomconfig_change_enabledlogging |
3480 |
|
| roomconfig_change_nonanonymous | roomconfig_change_anonymous | roomconfig_change. |
3481 |
|
notify_config_change_and_get_type(true, false, _, _, StateData) -> |
3482 |
1 |
send_config_update(logging_disabled, StateData), |
3483 |
1 |
roomconfig_change_disabledlogging; |
3484 |
|
notify_config_change_and_get_type(false, true, _, _, StateData) -> |
3485 |
1 |
send_config_update(logging_enabled, StateData), |
3486 |
1 |
roomconfig_change_enabledlogging; |
3487 |
|
notify_config_change_and_get_type(_, _, true, false, StateData) -> |
3488 |
1 |
send_config_update(nonanonymous, StateData), |
3489 |
1 |
roomconfig_change_nonanonymous; |
3490 |
|
notify_config_change_and_get_type(_, _, false, true, StateData) -> |
3491 |
1 |
send_config_update(semianonymous, StateData), |
3492 |
1 |
roomconfig_change_anonymous; |
3493 |
|
notify_config_change_and_get_type(_, _, _, _, _StateData) -> |
3494 |
18 |
roomconfig_change. |
3495 |
|
|
3496 |
|
-define(SET_BOOL_XOPT(Opt, Val), |
3497 |
|
case Val of |
3498 |
|
<<"0">> -> set_xoption(Opts, Config#config{Opt = false}); |
3499 |
|
<<"false">> -> set_xoption(Opts, Config#config{Opt = false}); |
3500 |
|
<<"1">> -> set_xoption(Opts, Config#config{Opt = true}); |
3501 |
|
<<"true">> -> set_xoption(Opts, Config#config{Opt = true}); |
3502 |
|
_ -> {error, mongoose_xmpp_errors:bad_request()} |
3503 |
|
end). |
3504 |
|
|
3505 |
|
-define(SET_NAT_XOPT(Opt, Val), |
3506 |
|
case catch binary_to_integer(Val) of |
3507 |
|
I when is_integer(I), |
3508 |
|
I > 0 -> |
3509 |
|
set_xoption(Opts, Config#config{Opt = I}); |
3510 |
|
_ -> |
3511 |
|
{error, mongoose_xmpp_errors:bad_request()} |
3512 |
|
end). |
3513 |
|
|
3514 |
|
-define(SET_XOPT(Opt, Val), |
3515 |
|
set_xoption(Opts, Config#config{Opt = Val})). |
3516 |
|
|
3517 |
|
-spec set_xoption([{binary(), [binary()]}], config()) -> config() | {error, exml:element()}. |
3518 |
|
set_xoption([], Config) -> |
3519 |
22 |
Config; |
3520 |
|
set_xoption([{<<"muc#roomconfig_roomname">>, [Val]} | Opts], Config) -> |
3521 |
:-( |
?SET_XOPT(title, Val); |
3522 |
|
set_xoption([{<<"muc#roomconfig_roomdesc">>, [Val]} | Opts], Config) -> |
3523 |
:-( |
?SET_XOPT(description, Val); |
3524 |
|
set_xoption([{<<"muc#roomconfig_changesubject">>, [Val]} | Opts], Config) -> |
3525 |
:-( |
?SET_BOOL_XOPT(allow_change_subj, Val); |
3526 |
|
set_xoption([{<<"allow_query_users">>, [Val]} | Opts], Config) -> |
3527 |
:-( |
?SET_BOOL_XOPT(allow_query_users, Val); |
3528 |
|
set_xoption([{<<"allow_private_messages">>, [Val]} | Opts], Config) -> |
3529 |
:-( |
?SET_BOOL_XOPT(allow_private_messages, Val); |
3530 |
|
set_xoption([{<<"muc#roomconfig_allowvisitorstatus">>, [Val]} | Opts], Config) -> |
3531 |
:-( |
?SET_BOOL_XOPT(allow_visitor_status, Val); |
3532 |
|
set_xoption([{<<"muc#roomconfig_allowvisitornickchange">>, [Val]} | Opts], Config) -> |
3533 |
:-( |
?SET_BOOL_XOPT(allow_visitor_nickchange, Val); |
3534 |
|
set_xoption([{<<"muc#roomconfig_publicroom">>, [Val]} | Opts], Config) -> |
3535 |
2 |
?SET_BOOL_XOPT(public, Val); |
3536 |
|
set_xoption([{<<"public_list">>, [Val]} | Opts], Config) -> |
3537 |
:-( |
?SET_BOOL_XOPT(public_list, Val); |
3538 |
|
set_xoption([{<<"muc#roomconfig_persistentroom">>, [Val]} | Opts], Config) -> |
3539 |
10 |
?SET_BOOL_XOPT(persistent, Val); |
3540 |
|
set_xoption([{<<"muc#roomconfig_moderatedroom">>, [Val]} | Opts], Config) -> |
3541 |
2 |
?SET_BOOL_XOPT(moderated, Val); |
3542 |
|
set_xoption([{<<"members_by_default">>, [Val]} | Opts], Config) -> |
3543 |
:-( |
?SET_BOOL_XOPT(members_by_default, Val); |
3544 |
|
set_xoption([{<<"muc#roomconfig_membersonly">>, [Val]} | Opts], Config) -> |
3545 |
6 |
?SET_BOOL_XOPT(members_only, Val); |
3546 |
|
set_xoption([{<<"muc#roomconfig_allowinvites">>, [Val]} | Opts], Config) -> |
3547 |
:-( |
?SET_BOOL_XOPT(allow_user_invites, Val); |
3548 |
|
set_xoption([{<<"muc#roomconfig_allowmultisessions">>, [Val]} | Opts], Config) -> |
3549 |
:-( |
?SET_BOOL_XOPT(allow_multiple_sessions, Val); |
3550 |
|
set_xoption([{<<"muc#roomconfig_passwordprotectedroom">>, [Val]} | Opts], Config) -> |
3551 |
:-( |
?SET_BOOL_XOPT(password_protected, Val); |
3552 |
|
set_xoption([{<<"muc#roomconfig_roomsecret">>, [Val]} | Opts], Config) -> |
3553 |
:-( |
?SET_XOPT(password, Val); |
3554 |
|
set_xoption([{<<"anonymous">>, [Val]} | Opts], Config) -> |
3555 |
:-( |
?SET_BOOL_XOPT(anonymous, Val); |
3556 |
|
set_xoption([{<<"muc#roomconfig_whois">>, [Val]} | Opts], Config) -> |
3557 |
2 |
case Val of |
3558 |
|
<<"moderators">> -> |
3559 |
1 |
?SET_XOPT(anonymous, true); |
3560 |
|
<<"anyone">> -> |
3561 |
1 |
?SET_XOPT(anonymous, false); |
3562 |
|
_ -> |
3563 |
:-( |
{error, mongoose_xmpp_errors:bad_request()} |
3564 |
|
end; |
3565 |
|
set_xoption([{<<"muc#roomconfig_maxusers">>, [Val]} | Opts], Config) -> |
3566 |
:-( |
case Val of |
3567 |
|
<<"none">> -> |
3568 |
:-( |
?SET_XOPT(max_users, none); |
3569 |
|
_ -> |
3570 |
:-( |
?SET_NAT_XOPT(max_users, Val) |
3571 |
|
end; |
3572 |
|
set_xoption([{<<"muc#roomconfig_getmemberlist">>, Val} | Opts], Config) -> |
3573 |
3 |
case Val of |
3574 |
|
[<<"none">>] -> |
3575 |
:-( |
?SET_XOPT(maygetmemberlist, []); |
3576 |
|
_ -> |
3577 |
3 |
?SET_XOPT(maygetmemberlist, [binary_to_role(V) || V <- Val]) |
3578 |
|
end; |
3579 |
|
set_xoption([{<<"muc#roomconfig_enablelogging">>, [Val]} | Opts], Config) -> |
3580 |
2 |
?SET_BOOL_XOPT(logging, Val); |
3581 |
|
set_xoption([_ | _Opts], _Config) -> |
3582 |
:-( |
{error, mongoose_xmpp_errors:bad_request()}. |
3583 |
|
|
3584 |
|
|
3585 |
|
-spec change_config(config(), state()) -> {'result', [], state()}. |
3586 |
|
change_config(Config, StateData) -> |
3587 |
26 |
NSD = StateData#state{config = Config}, |
3588 |
26 |
case {(StateData#state.config)#config.persistent, |
3589 |
|
Config#config.persistent} of |
3590 |
|
{_, true} -> |
3591 |
17 |
mod_muc:store_room(NSD#state.host_type, NSD#state.host, NSD#state.room, make_opts(NSD)); |
3592 |
|
{true, false} -> |
3593 |
:-( |
mod_muc:forget_room(NSD#state.host_type, NSD#state.host, NSD#state.room); |
3594 |
|
{false, false} -> |
3595 |
9 |
ok |
3596 |
|
end, |
3597 |
26 |
case {(StateData#state.config)#config.members_only, |
3598 |
|
Config#config.members_only} of |
3599 |
|
{false, true} -> |
3600 |
1 |
NSD1 = remove_nonmembers(NSD), |
3601 |
1 |
{result, [], NSD1}; |
3602 |
|
_ -> |
3603 |
25 |
{result, [], NSD} |
3604 |
|
end. |
3605 |
|
|
3606 |
|
|
3607 |
|
-spec remove_nonmembers(state()) -> state(). |
3608 |
|
remove_nonmembers(StateData) -> |
3609 |
1 |
F = fun(_LJID, #user{jid = JID}, SD) -> |
3610 |
1 |
Affiliation = get_affiliation(JID, SD), |
3611 |
1 |
case Affiliation of |
3612 |
|
none -> |
3613 |
:-( |
safe_send_kickban_presence(JID, <<>>, <<"322">>, SD), |
3614 |
:-( |
set_role(JID, none, SD); |
3615 |
|
_ -> |
3616 |
1 |
SD |
3617 |
|
end |
3618 |
|
end, |
3619 |
1 |
maps:fold(F, StateData, StateData#state.users). |
3620 |
|
|
3621 |
|
-spec set_opts(Opts :: [{atom(), term()}], state()) -> state(). |
3622 |
|
set_opts([], SD) -> |
3623 |
657 |
SD; |
3624 |
|
set_opts([{Opt, Val} | Opts], SD=#state{config = C = #config{}}) -> |
3625 |
2831 |
NSD = case Opt of |
3626 |
|
title -> |
3627 |
61 |
SD#state{config = C#config{title = Val}}; |
3628 |
|
description -> |
3629 |
61 |
SD#state{config = C#config{description = Val}}; |
3630 |
|
allow_change_subj -> |
3631 |
75 |
SD#state{config = C#config{allow_change_subj = Val}}; |
3632 |
|
allow_query_users -> |
3633 |
61 |
SD#state{config = C#config{allow_query_users = Val}}; |
3634 |
|
allow_private_messages -> |
3635 |
61 |
SD#state{config = C#config{allow_private_messages = Val}}; |
3636 |
|
allow_visitor_nickchange -> |
3637 |
61 |
SD#state{config = C#config{allow_visitor_nickchange = Val}}; |
3638 |
|
allow_visitor_status -> |
3639 |
61 |
SD#state{config = C#config{allow_visitor_status = Val}}; |
3640 |
|
public -> |
3641 |
61 |
SD#state{config = C#config{public = Val}}; |
3642 |
|
public_list -> |
3643 |
65 |
SD#state{config = C#config{public_list = Val}}; |
3644 |
|
persistent -> |
3645 |
389 |
SD#state{config = C#config{persistent = Val}}; |
3646 |
|
moderated -> |
3647 |
75 |
SD#state{config = C#config{moderated = Val}}; |
3648 |
|
members_by_default -> |
3649 |
75 |
SD#state{config = C#config{members_by_default = Val}}; |
3650 |
|
members_only -> |
3651 |
69 |
SD#state{config = C#config{members_only = Val}}; |
3652 |
|
allow_user_invites -> |
3653 |
61 |
SD#state{config = C#config{allow_user_invites = Val}}; |
3654 |
|
allow_multiple_sessions -> |
3655 |
66 |
SD#state{config = C#config{allow_multiple_sessions = Val}}; |
3656 |
|
password_protected -> |
3657 |
102 |
SD#state{config = C#config{password_protected = Val}}; |
3658 |
|
password -> |
3659 |
97 |
SD#state{config = C#config{password = Val}}; |
3660 |
|
anonymous -> |
3661 |
348 |
SD#state{config = C#config{anonymous = Val}}; |
3662 |
|
logging -> |
3663 |
62 |
SD#state{config = C#config{logging = Val}}; |
3664 |
|
max_users -> |
3665 |
62 |
MaxUsers = min(Val, get_service_max_users(SD)), |
3666 |
62 |
SD#state{config = C#config{max_users = MaxUsers}}; |
3667 |
|
maygetmemberlist -> |
3668 |
61 |
SD#state{config = C#config{maygetmemberlist = Val}}; |
3669 |
|
affiliations -> |
3670 |
61 |
SD#state{affiliations = maps:from_list(Val)}; |
3671 |
|
subject -> |
3672 |
62 |
SD#state{subject = Val}; |
3673 |
|
subject_author -> |
3674 |
61 |
SD#state{subject_author = Val}; |
3675 |
|
subject_timestamp -> |
3676 |
57 |
SD#state{subject_timestamp = Val}; |
3677 |
|
_ -> |
3678 |
556 |
SD |
3679 |
|
end, |
3680 |
2831 |
set_opts(Opts, NSD). |
3681 |
|
|
3682 |
|
|
3683 |
|
-define(MAKE_CONFIG_OPT(Opt), {Opt, Config#config.Opt}). |
3684 |
|
|
3685 |
|
-spec make_opts(state()) -> [{atom(), _}, ...]. |
3686 |
|
make_opts(StateData) -> |
3687 |
408 |
Config = StateData#state.config, |
3688 |
408 |
[ |
3689 |
|
?MAKE_CONFIG_OPT(title), |
3690 |
|
?MAKE_CONFIG_OPT(description), |
3691 |
|
?MAKE_CONFIG_OPT(allow_change_subj), |
3692 |
|
?MAKE_CONFIG_OPT(allow_query_users), |
3693 |
|
?MAKE_CONFIG_OPT(allow_private_messages), |
3694 |
|
?MAKE_CONFIG_OPT(allow_visitor_status), |
3695 |
|
?MAKE_CONFIG_OPT(allow_visitor_nickchange), |
3696 |
|
?MAKE_CONFIG_OPT(public), |
3697 |
|
?MAKE_CONFIG_OPT(public_list), |
3698 |
|
?MAKE_CONFIG_OPT(persistent), |
3699 |
|
?MAKE_CONFIG_OPT(moderated), |
3700 |
|
?MAKE_CONFIG_OPT(members_by_default), |
3701 |
|
?MAKE_CONFIG_OPT(members_only), |
3702 |
|
?MAKE_CONFIG_OPT(allow_user_invites), |
3703 |
|
?MAKE_CONFIG_OPT(allow_multiple_sessions), |
3704 |
|
?MAKE_CONFIG_OPT(password_protected), |
3705 |
|
?MAKE_CONFIG_OPT(password), |
3706 |
|
?MAKE_CONFIG_OPT(anonymous), |
3707 |
|
?MAKE_CONFIG_OPT(logging), |
3708 |
|
?MAKE_CONFIG_OPT(max_users), |
3709 |
|
?MAKE_CONFIG_OPT(maygetmemberlist), |
3710 |
|
{affiliations, maps:to_list(StateData#state.affiliations)}, |
3711 |
|
{subject, StateData#state.subject}, |
3712 |
|
{subject_author, StateData#state.subject_author} |
3713 |
|
]. |
3714 |
|
|
3715 |
|
-spec destroy_room(exml:element(), state()) -> {result, [], stop}. |
3716 |
|
destroy_room(DestroyEl, StateData) -> |
3717 |
277 |
remove_each_occupant_from_room(DestroyEl, StateData), |
3718 |
277 |
case (StateData#state.config)#config.persistent of |
3719 |
|
true -> |
3720 |
223 |
mod_muc:forget_room(StateData#state.host_type, |
3721 |
|
StateData#state.host, |
3722 |
|
StateData#state.room); |
3723 |
|
false -> |
3724 |
54 |
ok |
3725 |
|
end, |
3726 |
277 |
{result, [], stop}. |
3727 |
|
|
3728 |
|
|
3729 |
|
%% @doc Service Removes Each Occupant |
3730 |
|
%% |
3731 |
|
%% Send only one presence stanza of type "unavailable" to each occupant |
3732 |
|
%% so that the user knows he or she has been removed from the room. |
3733 |
|
%% |
3734 |
|
%% If extended presence information specifying the JID of an alternate |
3735 |
|
%% location and the reason for the room destruction was provided by the |
3736 |
|
%% room owner, the presence stanza MUST include that information. |
3737 |
|
%% @end |
3738 |
|
-spec remove_each_occupant_from_room(exml:element(), state()) -> any(). |
3739 |
|
remove_each_occupant_from_room(DestroyEl, StateData) -> |
3740 |
277 |
Packet = presence_stanza_of_type_unavailable(DestroyEl), |
3741 |
277 |
send_to_occupants(Packet, StateData). |
3742 |
|
|
3743 |
|
|
3744 |
|
-spec send_to_occupants(exml:element(), state()) -> any(). |
3745 |
|
send_to_occupants(Packet, StateData=#state{jid=RoomJID}) -> |
3746 |
277 |
F = fun(User=#user{jid=UserJID}) -> |
3747 |
24 |
ejabberd_router:route(occupant_jid(User, RoomJID), UserJID, Packet) |
3748 |
|
end, |
3749 |
277 |
foreach_user(F, StateData). |
3750 |
|
|
3751 |
|
-spec send_to_all_users(exml:element(), state()) -> any(). |
3752 |
|
send_to_all_users(Packet, StateData=#state{jid=RoomJID}) -> |
3753 |
4 |
F = fun(#user{jid = UserJID}) -> |
3754 |
6 |
ejabberd_router:route(RoomJID, UserJID, Packet) |
3755 |
|
end, |
3756 |
4 |
foreach_user(F, StateData). |
3757 |
|
|
3758 |
|
|
3759 |
|
-spec presence_stanza_of_type_unavailable(exml:element()) -> exml:element(). |
3760 |
|
presence_stanza_of_type_unavailable(DestroyEl) -> |
3761 |
277 |
ItemEl = #xmlel{ |
3762 |
|
name = <<"item">>, |
3763 |
|
attrs = [{<<"affiliation">>, <<"none">>}, {<<"role">>, <<"none">>}]}, |
3764 |
277 |
XEl = #xmlel{ |
3765 |
|
name = <<"x">>, |
3766 |
|
attrs = [{<<"xmlns">>, ?NS_MUC_USER}], |
3767 |
|
children = [ItemEl, DestroyEl]}, |
3768 |
277 |
#xmlel{ |
3769 |
|
name = <<"presence">>, |
3770 |
|
attrs = [{<<"type">>, <<"unavailable">>}], |
3771 |
|
children = [XEl]}. |
3772 |
|
|
3773 |
|
|
3774 |
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
3775 |
|
% Disco |
3776 |
|
|
3777 |
|
-spec config_opt_to_feature(boolean(), Fiftrue :: binary(), Fiffalse :: binary()) -> binary(). |
3778 |
|
config_opt_to_feature(Opt, Fiftrue, Fiffalse) -> |
3779 |
30 |
case Opt of |
3780 |
19 |
true -> Fiftrue; |
3781 |
11 |
false -> Fiffalse |
3782 |
|
end. |
3783 |
|
|
3784 |
|
|
3785 |
|
-spec process_iq_disco_info(jid:jid(), 'get' | 'set', ejabberd:lang(), |
3786 |
|
state()) -> {'error', exml:element()} |
3787 |
|
| {'result', [exml:element()], state()}. |
3788 |
|
process_iq_disco_info(_From, set, _Lang, _StateData) -> |
3789 |
:-( |
{error, mongoose_xmpp_errors:not_allowed()}; |
3790 |
|
process_iq_disco_info(From, get, Lang, StateData) -> |
3791 |
5 |
RoomJID = StateData#state.jid, |
3792 |
5 |
Config = StateData#state.config, |
3793 |
5 |
HostType = StateData#state.host_type, |
3794 |
5 |
IdentityXML = mongoose_disco:identities_to_xml([identity(get_title(StateData))]), |
3795 |
5 |
FeatureXML = mongoose_disco:get_muc_features(HostType, From, RoomJID, <<>>, Lang, |
3796 |
|
room_features(Config)), |
3797 |
5 |
InfoXML = iq_disco_info_extras(Lang, StateData), |
3798 |
5 |
{result, IdentityXML ++ FeatureXML ++ InfoXML, StateData}. |
3799 |
|
|
3800 |
|
identity(Name) -> |
3801 |
5 |
#{category => <<"conference">>, |
3802 |
|
type => <<"text">>, |
3803 |
|
name => Name}. |
3804 |
|
|
3805 |
|
-spec room_features(config()) -> [mongoose_disco:feature()]. |
3806 |
|
room_features(Config) -> |
3807 |
5 |
[?NS_MUC, |
3808 |
|
?NS_MUC_STABLE_ID, |
3809 |
|
config_opt_to_feature((Config#config.public), |
3810 |
|
<<"muc_public">>, <<"muc_hidden">>), |
3811 |
|
config_opt_to_feature((Config#config.persistent), |
3812 |
|
<<"muc_persistent">>, <<"muc_temporary">>), |
3813 |
|
config_opt_to_feature((Config#config.members_only), |
3814 |
|
<<"muc_membersonly">>, <<"muc_open">>), |
3815 |
|
config_opt_to_feature((Config#config.anonymous), |
3816 |
|
<<"muc_semianonymous">>, <<"muc_nonanonymous">>), |
3817 |
|
config_opt_to_feature((Config#config.moderated), |
3818 |
|
<<"muc_moderated">>, <<"muc_unmoderated">>), |
3819 |
|
config_opt_to_feature((Config#config.password_protected), |
3820 |
|
<<"muc_passwordprotected">>, <<"muc_unsecured">>)]. |
3821 |
|
|
3822 |
|
-spec iq_disco_info_extras(ejabberd:lang(), state()) -> [exml:element()]. |
3823 |
|
iq_disco_info_extras(Lang, StateData) -> |
3824 |
5 |
Len = integer_to_binary(maps:size(StateData#state.users)), |
3825 |
5 |
Description = (StateData#state.config)#config.description, |
3826 |
5 |
Fields = [info_field(<<"Room description">>, <<"muc#roominfo_description">>, Description, Lang), |
3827 |
|
info_field(<<"Number of occupants">>, <<"muc#roominfo_occupants">>, Len, Lang)], |
3828 |
5 |
Info = #{xmlns => <<"http://jabber.org/protocol/muc#roominfo">>, fields => Fields}, |
3829 |
5 |
mongoose_disco:info_list_to_xml([Info]). |
3830 |
|
|
3831 |
|
-spec info_field(binary(), binary(), binary(), ejabberd:lang()) -> mongoose_disco:info_field(). |
3832 |
|
info_field(Label, Var, Value, Lang) -> |
3833 |
10 |
#{label => translate:translate(Lang, Label), var => Var, values => [Value]}. |
3834 |
|
|
3835 |
|
-spec process_iq_disco_items(jid:jid(), 'get' | 'set', ejabberd:lang(), |
3836 |
|
state()) -> {'error', exml:element()} |
3837 |
|
| {'result', [exml:element()], state()}. |
3838 |
|
process_iq_disco_items(_From, set, _Lang, _StateData) -> |
3839 |
:-( |
{error, mongoose_xmpp_errors:not_allowed()}; |
3840 |
|
process_iq_disco_items(From, get, _Lang, StateData) -> |
3841 |
2 |
case (StateData#state.config)#config.public_list of |
3842 |
|
true -> |
3843 |
1 |
{result, get_mucroom_disco_items(StateData), StateData}; |
3844 |
|
_ -> |
3845 |
1 |
case is_occupant_or_admin(From, StateData) of |
3846 |
|
true -> |
3847 |
:-( |
{result, get_mucroom_disco_items(StateData), StateData}; |
3848 |
|
_ -> |
3849 |
1 |
{error, mongoose_xmpp_errors:forbidden()} |
3850 |
|
end |
3851 |
|
end. |
3852 |
|
|
3853 |
|
|
3854 |
|
-spec get_title(state()) -> binary() | mod_muc:room(). |
3855 |
|
get_title(StateData) -> |
3856 |
163 |
case (StateData#state.config)#config.title of |
3857 |
|
<<>> -> |
3858 |
163 |
StateData#state.room; |
3859 |
|
Name -> |
3860 |
:-( |
Name |
3861 |
|
end. |
3862 |
|
|
3863 |
|
|
3864 |
|
-spec get_roomdesc_reply(jid:jid(), state(), Tail :: binary() |
3865 |
|
) -> 'false' | {'item', _}. |
3866 |
|
get_roomdesc_reply(JID, StateData, Tail) -> |
3867 |
158 |
IsOccupantOrAdmin = is_occupant_or_admin(JID, StateData), |
3868 |
158 |
case {(StateData#state.config)#config.public or IsOccupantOrAdmin, |
3869 |
|
(StateData#state.config)#config.public_list or IsOccupantOrAdmin} of |
3870 |
|
{true, true} -> |
3871 |
141 |
Title = get_title(StateData), |
3872 |
141 |
{item, <<Title/binary, Tail/binary>>}; |
3873 |
|
{true, false} -> |
3874 |
17 |
{item, get_title(StateData)}; |
3875 |
|
_ -> |
3876 |
:-( |
false |
3877 |
|
end. |
3878 |
|
|
3879 |
|
|
3880 |
|
-spec get_roomdesc_tail(state(), ejabberd:lang()) -> binary(). |
3881 |
|
get_roomdesc_tail(StateData, Lang) -> |
3882 |
158 |
Desc = case (StateData#state.config)#config.public of |
3883 |
|
true -> |
3884 |
158 |
<<>>; |
3885 |
|
_ -> |
3886 |
:-( |
translate:translate(Lang, <<"private, ">>) |
3887 |
|
end, |
3888 |
158 |
Count = count_users(StateData), |
3889 |
158 |
CountBin = list_to_binary(integer_to_list(Count)), |
3890 |
158 |
<<" (", Desc/binary, CountBin/binary, ")">>. |
3891 |
|
|
3892 |
|
|
3893 |
|
-spec get_mucroom_disco_items(state()) -> [exml:element()]. |
3894 |
|
get_mucroom_disco_items(StateData=#state{jid=RoomJID}) -> |
3895 |
1 |
maps:fold(fun(_LJID, User, Acc) -> |
3896 |
1 |
Item = disco_item(User, RoomJID), |
3897 |
1 |
[Item|Acc] |
3898 |
|
end, [], StateData#state.users). |
3899 |
|
|
3900 |
|
-spec disco_item(user(), 'undefined' | jid:jid()) -> exml:element(). |
3901 |
|
disco_item(User=#user{nick=Nick}, RoomJID) -> |
3902 |
1 |
#xmlel{ |
3903 |
|
name = <<"item">>, |
3904 |
|
attrs = [{<<"jid">>, jid:to_binary(occupant_jid(User, RoomJID))}, |
3905 |
|
{<<"name">>, Nick}]}. |
3906 |
|
|
3907 |
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
3908 |
|
%% Handle voice request or approval (XEP-0045 7.13, 8.6) |
3909 |
|
-spec check_voice_approval(From :: jid:jid(), El :: exml:element(), |
3910 |
|
Lang :: ejabberd:lang(), StateData :: state() |
3911 |
|
) -> {form, BRole :: binary()} |
3912 |
|
| {role, BRole :: binary(), RoomNick :: mod_muc:nick()} |
3913 |
|
| {error, any()} |
3914 |
|
| ok. |
3915 |
|
check_voice_approval(From, XEl, Lang, StateData) -> |
3916 |
9 |
case mongoose_data_forms:find_and_parse_form(XEl) of |
3917 |
|
#{type := <<"submit">>, kvs := #{<<"muc#role">> := [BRole]} = KVs} -> |
3918 |
6 |
case {get_role(From, StateData) =:= moderator, |
3919 |
|
maps:find(<<"muc#request_allow">>, KVs), |
3920 |
|
maps:find(<<"muc#roomnick">>, KVs)} of |
3921 |
|
{_, error, error} -> |
3922 |
2 |
case catch binary_to_role(BRole) of |
3923 |
:-( |
{'EXIT', _} -> {error, mongoose_xmpp_errors:bad_request()}; |
3924 |
2 |
_ -> {form, BRole} |
3925 |
|
end; |
3926 |
|
{false, _, _} -> |
3927 |
1 |
{error, mongoose_xmpp_errors:not_allowed()}; |
3928 |
|
{true, {ok, [<<"true">>]}, error} -> |
3929 |
1 |
{error, mongoose_xmpp_errors:bad_request()}; |
3930 |
|
{true, {ok, [<<"true">>]}, {ok, [RoomNick]}} -> |
3931 |
1 |
{role, BRole, RoomNick}; |
3932 |
|
{true, _, _} -> |
3933 |
1 |
ok |
3934 |
|
end; |
3935 |
|
{error, Msg} -> |
3936 |
2 |
{error, mongoose_xmpp_errors:bad_request(Lang, Msg)}; |
3937 |
|
_ -> |
3938 |
1 |
{error, mongoose_xmpp_errors:bad_request(Lang, <<"Invalid form">>)} |
3939 |
|
end. |
3940 |
|
|
3941 |
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
3942 |
|
%% Invitation support |
3943 |
|
|
3944 |
|
-spec check_invitation(jid:simple_jid() | jid:jid(), |
3945 |
|
[jlib:xmlcdata() | exml:element()], ejabberd:lang(), state()) |
3946 |
|
-> {'error', _} | {'ok', [jid:jid()]}. |
3947 |
|
check_invitation(FromJID, Els, Lang, StateData) -> |
3948 |
4 |
try |
3949 |
4 |
unsafe_check_invitation(FromJID, Els, Lang, StateData) |
3950 |
1 |
catch throw:{error, Reason} -> {error, Reason} |
3951 |
|
end. |
3952 |
|
|
3953 |
|
|
3954 |
|
-spec unsafe_check_invitation(jid:jid(), [jlib:xmlcdata() | exml:element()], |
3955 |
|
ejabberd:lang(), state()) -> {ok, [jid:jid()]}. |
3956 |
|
unsafe_check_invitation(FromJID, Els, Lang, |
3957 |
|
StateData=#state{host=Host, server_host=ServerHost, jid=RoomJID}) -> |
3958 |
4 |
FAffiliation = get_affiliation(FromJID, StateData), |
3959 |
4 |
CanInvite = (StateData#state.config)#config.allow_user_invites |
3960 |
4 |
orelse (FAffiliation == admin) |
3961 |
4 |
orelse (FAffiliation == owner), |
3962 |
4 |
case CanInvite of |
3963 |
|
false -> |
3964 |
1 |
throw({error, mongoose_xmpp_errors:forbidden()}); |
3965 |
|
true -> |
3966 |
3 |
InviteEls = find_invite_elems(Els), |
3967 |
|
%% Decode all JIDs first, so we fail early if any JID is invalid. |
3968 |
3 |
JIDs = lists:map(fun decode_destination_jid/1, InviteEls), |
3969 |
3 |
lists:foreach( |
3970 |
|
fun(InviteEl) -> |
3971 |
4 |
{JID, Reason, Msg} = create_invite(FromJID, InviteEl, Lang, StateData), |
3972 |
4 |
mongoose_hooks:invitation_sent(Host, ServerHost, RoomJID, |
3973 |
|
FromJID, JID, Reason), |
3974 |
4 |
ejabberd_router:route(StateData#state.jid, JID, Msg) |
3975 |
|
end, InviteEls), |
3976 |
3 |
{ok, JIDs} |
3977 |
|
end. |
3978 |
|
|
3979 |
|
-spec create_invite(FromJID ::jid:jid(), InviteEl :: exml:element(), |
3980 |
|
Lang :: ejabberd:lang(), StateData :: state()) -> |
3981 |
|
{JID ::jid:jid(), Reason :: binary(), Msg :: exml:element()}. |
3982 |
|
create_invite(FromJID, InviteEl, Lang, StateData) -> |
3983 |
4 |
JID = decode_destination_jid(InviteEl), |
3984 |
|
%% Create an invitation message and send it to the user. |
3985 |
4 |
Reason = decode_reason(InviteEl), |
3986 |
4 |
ContinueEl = |
3987 |
|
case xml:get_path_s(InviteEl, [{elem, <<"continue">>}]) of |
3988 |
4 |
<<>> -> []; |
3989 |
:-( |
Continue1 -> [Continue1] |
3990 |
|
end, |
3991 |
4 |
ReasonEl = #xmlel{ |
3992 |
|
name = <<"reason">>, |
3993 |
|
children = [#xmlcdata{content = Reason}]}, |
3994 |
4 |
OutInviteEl = #xmlel{ |
3995 |
|
name = <<"invite">>, |
3996 |
|
attrs = [{<<"from">>, jid:to_binary(FromJID)}], |
3997 |
|
children = [ReasonEl] ++ ContinueEl}, |
3998 |
4 |
PasswdEl = create_password_elem(StateData), |
3999 |
4 |
BodyEl = invite_body_elem(FromJID, Reason, Lang, StateData), |
4000 |
4 |
Msg = create_invite_message_elem( |
4001 |
|
OutInviteEl, BodyEl, PasswdEl, Reason), |
4002 |
4 |
{JID, Reason, Msg}. |
4003 |
|
|
4004 |
|
-spec decode_destination_jid(exml:element()) -> jid:jid(). |
4005 |
|
decode_destination_jid(InviteEl) -> |
4006 |
8 |
case jid:from_binary(xml:get_tag_attr_s(<<"to">>, InviteEl)) of |
4007 |
:-( |
error -> throw({error, mongoose_xmpp_errors:jid_malformed()}); |
4008 |
8 |
JID -> JID |
4009 |
|
end. |
4010 |
|
|
4011 |
|
|
4012 |
|
-spec find_invite_elems([jlib:xmlcdata() | exml:element()]) -> [exml:element()]. |
4013 |
|
find_invite_elems(Els) -> |
4014 |
3 |
case xml:remove_cdata(Els) of |
4015 |
|
[#xmlel{name = <<"x">>, children = Els1} = XEl] -> |
4016 |
3 |
case xml:get_tag_attr_s(<<"xmlns">>, XEl) of |
4017 |
|
?NS_MUC_USER -> |
4018 |
3 |
ok; |
4019 |
|
_ -> |
4020 |
:-( |
throw({error, mongoose_xmpp_errors:bad_request()}) |
4021 |
|
end, |
4022 |
|
|
4023 |
3 |
InviteEls = |
4024 |
4 |
[InviteEl || #xmlel{name = <<"invite">>} = InviteEl <- Els1], |
4025 |
3 |
case InviteEls of |
4026 |
|
[_|_] -> |
4027 |
3 |
InviteEls; |
4028 |
|
_ -> |
4029 |
:-( |
throw({error, mongoose_xmpp_errors:bad_request()}) |
4030 |
|
end; |
4031 |
|
_ -> |
4032 |
:-( |
throw({error, mongoose_xmpp_errors:bad_request()}) |
4033 |
|
end. |
4034 |
|
|
4035 |
|
|
4036 |
|
-spec create_password_elem(state()) -> [exml:element()]. |
4037 |
|
create_password_elem(#state{config=#config{password_protected=IsProtected, |
4038 |
|
password=Password}}) -> |
4039 |
4 |
case IsProtected of |
4040 |
|
true -> |
4041 |
:-( |
[#xmlel{ |
4042 |
|
name = <<"password">>, |
4043 |
|
children = [#xmlcdata{content = Password}]}]; |
4044 |
|
_ -> |
4045 |
4 |
[] |
4046 |
|
end. |
4047 |
|
|
4048 |
|
|
4049 |
|
-spec invite_body_elem(jid:jid(), binary(), ejabberd:lang(), state() |
4050 |
|
) -> exml:element(). |
4051 |
|
invite_body_elem(FromJID, Reason, Lang, StateData) -> |
4052 |
4 |
Text = invite_body_text(FromJID, Reason, Lang, StateData), |
4053 |
4 |
#xmlel{ |
4054 |
|
name = <<"body">>, |
4055 |
|
children = [#xmlcdata{content = Text}]}. |
4056 |
|
|
4057 |
|
|
4058 |
|
-spec invite_body_text(jid:jid(), binary(), ejabberd:lang(), state()) -> binary(). |
4059 |
|
invite_body_text(FromJID, Reason, Lang, |
4060 |
|
#state{ |
4061 |
|
jid=RoomJID, |
4062 |
|
config=#config{ |
4063 |
|
password_protected=IsProtected, |
4064 |
|
password=Password}}) -> |
4065 |
4 |
BFromJID = jid:to_binary(FromJID), |
4066 |
4 |
BRoomJID = jid:to_binary(RoomJID), |
4067 |
4 |
ITranslate = translate:translate(Lang, <<" invites you to the room ">>), |
4068 |
4 |
IMessage = <<BFromJID/binary, ITranslate/binary, BRoomJID/binary>>, |
4069 |
4 |
BPassword = case IsProtected of |
4070 |
|
true -> |
4071 |
:-( |
PTranslate = translate:translate(Lang, <<"the password is">>), |
4072 |
:-( |
<<", ", PTranslate/binary, " '", Password/binary, "'">>; |
4073 |
|
_ -> |
4074 |
4 |
<<>> |
4075 |
|
end, |
4076 |
4 |
BReason = case Reason of |
4077 |
4 |
<<>> -> <<>>; |
4078 |
:-( |
_ -> <<" (", Reason/binary, ") ">> |
4079 |
|
end, |
4080 |
4 |
<<IMessage/binary, BPassword/binary, BReason/binary>>. |
4081 |
|
|
4082 |
|
|
4083 |
|
-spec create_invite_message_elem(Inv :: exml:element(), Body :: exml:element(), |
4084 |
|
Passwd :: [exml:element()], Reason :: binary() |
4085 |
|
) -> exml:element(). |
4086 |
|
create_invite_message_elem(InviteEl, BodyEl, PasswdEl, Reason) |
4087 |
|
when is_list(PasswdEl), is_binary(Reason) -> |
4088 |
4 |
UserXEl = #xmlel{ |
4089 |
|
name = <<"x">>, |
4090 |
|
attrs = [{<<"xmlns">>, ?NS_MUC_USER}], |
4091 |
|
children = [InviteEl|PasswdEl]}, |
4092 |
4 |
#xmlel{ |
4093 |
|
name = <<"message">>, |
4094 |
|
attrs = [{<<"type">>, <<"normal">>}], |
4095 |
|
children = [UserXEl, BodyEl]}. |
4096 |
|
|
4097 |
|
|
4098 |
|
%% @doc Handle a message sent to the room by a non-participant. |
4099 |
|
%% If it is a decline, send to the inviter. |
4100 |
|
%% Otherwise, an error message is sent to the sender. |
4101 |
|
-spec handle_roommessage_from_nonparticipant(exml:element(), ejabberd:lang(), |
4102 |
|
state(), jid:simple_jid() | jid:jid()) -> mongoose_acc:t(). |
4103 |
|
handle_roommessage_from_nonparticipant(Packet, Lang, StateData, From) -> |
4104 |
16 |
case catch check_decline_invitation(Packet) of |
4105 |
|
{true, DeclineData} -> |
4106 |
1 |
send_decline_invitation(DeclineData, StateData#state.jid, From); |
4107 |
|
_ -> |
4108 |
15 |
send_error_only_occupants(<<"messages">>, Packet, Lang, StateData#state.jid, From) |
4109 |
|
end. |
4110 |
|
|
4111 |
|
|
4112 |
|
%% @doc Check in the packet is a decline. If so, also returns the splitted |
4113 |
|
%% packet. This function must be catched, because it crashes when the packet |
4114 |
|
%% is not a decline message. |
4115 |
|
-spec check_decline_invitation(exml:element()) -> |
4116 |
|
{'true', {exml:element(), exml:element(), exml:element(), 'error' | jid:jid()}}. |
4117 |
|
check_decline_invitation(Packet) -> |
4118 |
16 |
#xmlel{name = <<"message">>} = Packet, |
4119 |
16 |
XEl = xml:get_subtag(Packet, <<"x">>), |
4120 |
16 |
?NS_MUC_USER = xml:get_tag_attr_s(<<"xmlns">>, XEl), |
4121 |
1 |
DEl = xml:get_subtag(XEl, <<"decline">>), |
4122 |
1 |
ToString = xml:get_tag_attr_s(<<"to">>, DEl), |
4123 |
1 |
ToJID = jid:from_binary(ToString), |
4124 |
1 |
{true, {Packet, XEl, DEl, ToJID}}. |
4125 |
|
|
4126 |
|
|
4127 |
|
%% @doc Send the decline to the inviter user. |
4128 |
|
%% The original stanza must be slightly modified. |
4129 |
|
-spec send_decline_invitation({exml:element(), exml:element(), exml:element(), jid:jid()}, |
4130 |
|
jid:jid(), jid:simple_jid() | jid:jid()) -> mongoose_acc:t(). |
4131 |
|
send_decline_invitation({Packet, XEl, DEl, ToJID}, RoomJID, FromJID) -> |
4132 |
1 |
FromString = jid:to_binary(FromJID), |
4133 |
1 |
#xmlel{name = <<"decline">>, attrs = DAttrs, children = DEls} = DEl, |
4134 |
1 |
DAttrs2 = lists:keydelete(<<"to">>, 1, DAttrs), |
4135 |
1 |
DAttrs3 = [{<<"from">>, FromString} | DAttrs2], |
4136 |
1 |
DEl2 = #xmlel{name = <<"decline">>, attrs = DAttrs3, children = DEls}, |
4137 |
1 |
XEl2 = xml:replace_subelement(XEl, DEl2), |
4138 |
1 |
Packet2 = xml:replace_subelement(Packet, XEl2), |
4139 |
1 |
ejabberd_router:route(RoomJID, ToJID, Packet2). |
4140 |
|
|
4141 |
|
-spec send_error_only_occupants(binary(), exml:element(), |
4142 |
|
binary() | nonempty_string(), |
4143 |
|
jid:jid(), jid:jid()) -> mongoose_acc:t(). |
4144 |
|
send_error_only_occupants(What, Packet, Lang, RoomJID, From) |
4145 |
|
when is_binary(What) -> |
4146 |
16 |
ErrText = <<"Only occupants are allowed to send ", |
4147 |
|
What/bytes, " to the conference">>, |
4148 |
16 |
Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)), |
4149 |
16 |
ejabberd_router:route(RoomJID, From, Err). |
4150 |
|
|
4151 |
|
|
4152 |
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
4153 |
|
% Logging |
4154 |
|
|
4155 |
|
-spec add_to_log(atom(), any(), state()) -> 'ok'. |
4156 |
|
add_to_log(Type, Data, StateData) |
4157 |
|
when Type == roomconfig_change_disabledlogging -> |
4158 |
|
%% When logging is disabled, the config change message must be logged: |
4159 |
1 |
mod_muc_log:add_to_log( |
4160 |
|
StateData#state.server_host, roomconfig_change, Data, |
4161 |
|
jid:to_binary(StateData#state.jid), make_opts(StateData)); |
4162 |
|
add_to_log(Type, Data, StateData) -> |
4163 |
3954 |
case (StateData#state.config)#config.logging of |
4164 |
|
true -> |
4165 |
8 |
mod_muc_log:add_to_log( |
4166 |
|
StateData#state.server_host, Type, Data, |
4167 |
|
jid:to_binary(StateData#state.jid), make_opts(StateData)); |
4168 |
|
false -> |
4169 |
3946 |
ok |
4170 |
|
end. |
4171 |
|
|
4172 |
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
4173 |
|
%% Users number checking |
4174 |
|
|
4175 |
|
-spec tab_add_online_user(jid:jid(), state()) -> any(). |
4176 |
|
tab_add_online_user(JID, StateData) -> |
4177 |
684 |
{LUser, LServer, _} = jid:to_lower(JID), |
4178 |
684 |
US = {LUser, LServer}, |
4179 |
684 |
Room = StateData#state.room, |
4180 |
684 |
Host = StateData#state.host, |
4181 |
684 |
catch ets:insert( |
4182 |
|
muc_online_users, |
4183 |
|
#muc_online_users{us = US, room = Room, host = Host}). |
4184 |
|
|
4185 |
|
|
4186 |
|
-spec tab_remove_online_user(jid:simple_jid() | jid:jid(), state()) -> any(). |
4187 |
|
tab_remove_online_user(JID, StateData) -> |
4188 |
681 |
{LUser, LServer, _} = jid:to_lower(JID), |
4189 |
681 |
US = {LUser, LServer}, |
4190 |
681 |
Room = StateData#state.room, |
4191 |
681 |
Host = StateData#state.host, |
4192 |
681 |
catch ets:delete_object( |
4193 |
|
muc_online_users, |
4194 |
|
#muc_online_users{us = US, room = Room, host = Host}). |
4195 |
|
|
4196 |
|
|
4197 |
|
-spec tab_count_user(jid:jid()) -> non_neg_integer(). |
4198 |
|
tab_count_user(JID) -> |
4199 |
732 |
{LUser, LServer, _} = jid:to_lower(JID), |
4200 |
732 |
US = {LUser, LServer}, |
4201 |
732 |
case catch ets:select( |
4202 |
|
muc_online_users, |
4203 |
|
[{#muc_online_users{us = US, _ = '_'}, [], [[]]}]) of |
4204 |
|
Res when is_list(Res) -> |
4205 |
732 |
length(Res); |
4206 |
|
_ -> |
4207 |
:-( |
0 |
4208 |
|
end. |
4209 |
|
|
4210 |
|
element_size(El) -> |
4211 |
783 |
exml:xml_size(El). |
4212 |
|
|
4213 |
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
4214 |
|
%% Routing functions |
4215 |
|
|
4216 |
|
-spec route_message(routed_message(), state()) -> state(). |
4217 |
|
route_message(#routed_message{allowed = true, type = <<"groupchat">>, |
4218 |
|
from = From, packet = Packet, lang = Lang}, StateData) -> |
4219 |
392 |
Activity = get_user_activity(From, StateData), |
4220 |
392 |
Now = os:system_time(microsecond), |
4221 |
392 |
MinMessageInterval = trunc(get_opt(StateData, min_message_interval) * 1000000), |
4222 |
392 |
Size = element_size(Packet), |
4223 |
392 |
{MessageShaper, MessageShaperInterval} = mongoose_shaper:update(Activity#activity.message_shaper, Size), |
4224 |
392 |
case {Activity#activity.message /= undefined, |
4225 |
|
Now >= Activity#activity.message_time + MinMessageInterval, |
4226 |
|
MessageShaperInterval} of |
4227 |
|
{true, _, _} -> |
4228 |
:-( |
ErrText = <<"Traffic rate limit is exceeded">>, |
4229 |
:-( |
Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:resource_constraint(Lang, ErrText)), |
4230 |
:-( |
ejabberd_router:route(StateData#state.jid, From, Err), |
4231 |
:-( |
StateData; |
4232 |
|
{false, true, 0} -> |
4233 |
392 |
{RoomShaper, RoomShaperInterval} = mongoose_shaper:update(StateData#state.room_shaper, Size), |
4234 |
392 |
RoomQueueEmpty = queue:is_empty(StateData#state.room_queue), |
4235 |
392 |
case {RoomShaperInterval, RoomQueueEmpty} of |
4236 |
|
{0, true} -> |
4237 |
392 |
NewActivity = Activity#activity{ |
4238 |
|
message_time = Now, |
4239 |
|
message_shaper = MessageShaper}, |
4240 |
392 |
StateData1 = store_user_activity(From, NewActivity, StateData), |
4241 |
392 |
StateData2 = StateData1#state{room_shaper = RoomShaper}, |
4242 |
392 |
{next_state, normal_state, StateData3, _} = |
4243 |
|
process_groupchat_message(From, Packet, StateData2), |
4244 |
392 |
StateData3; |
4245 |
|
_ -> |
4246 |
:-( |
StateData1 = schedule_queue_processing_when_empty( |
4247 |
|
RoomQueueEmpty, RoomShaper, RoomShaperInterval, StateData), |
4248 |
:-( |
NewActivity = Activity#activity{ |
4249 |
|
message_time = Now, |
4250 |
|
message_shaper = MessageShaper, |
4251 |
|
message = Packet}, |
4252 |
:-( |
RoomQueue = queue:in({message, From}, StateData#state.room_queue), |
4253 |
:-( |
StateData2 = store_user_activity(From, NewActivity, StateData1), |
4254 |
:-( |
StateData2#state{room_queue = RoomQueue} |
4255 |
|
end; |
4256 |
|
_ -> |
4257 |
:-( |
MessageInterval = (Activity#activity.message_time + MinMessageInterval - Now) div 1000, |
4258 |
:-( |
Interval = lists:max([MessageInterval, MessageShaperInterval]), |
4259 |
:-( |
erlang:send_after(Interval, self(), {process_user_message, From}), |
4260 |
:-( |
NewActivity = Activity#activity{ |
4261 |
|
message = Packet, |
4262 |
|
message_shaper = MessageShaper}, |
4263 |
:-( |
store_user_activity(From, NewActivity, StateData) |
4264 |
|
end; |
4265 |
|
route_message(#routed_message{allowed = true, type = <<"error">>, from = From, |
4266 |
|
packet = Packet, lang = Lang}, StateData) -> |
4267 |
3 |
case is_user_online(From, StateData) of |
4268 |
|
true -> |
4269 |
3 |
ErrorText |
4270 |
|
= <<"This participant is kicked from the room because he sent an error message">>, |
4271 |
3 |
expulse_participant(Packet, From, StateData, translate:translate(Lang, ErrorText)); |
4272 |
|
_ -> |
4273 |
:-( |
StateData |
4274 |
|
end; |
4275 |
|
route_message(#routed_message{allowed = true, type = <<"chat">>, from = From, packet = Packet, |
4276 |
|
lang = Lang}, StateData) -> |
4277 |
:-( |
ErrText = <<"It is not allowed to send private messages to the conference">>, |
4278 |
:-( |
Err = jlib:make_error_reply( |
4279 |
|
Packet, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)), |
4280 |
:-( |
ejabberd_router:route( |
4281 |
|
StateData#state.jid, |
4282 |
|
From, Err), |
4283 |
:-( |
StateData; |
4284 |
|
route_message(#routed_message{allowed = true, type = Type, from = From, |
4285 |
|
packet = #xmlel{name = <<"message">>, |
4286 |
|
children = Els} = Packet, lang = Lang}, |
4287 |
|
StateData) when (Type == <<>> orelse Type == <<"normal">>) -> |
4288 |
|
|
4289 |
13 |
Invite = xml:get_path_s(Packet, [{elem, <<"x">>}, {elem, <<"invite">>}]), |
4290 |
13 |
case Invite of |
4291 |
|
<<>> -> |
4292 |
9 |
AppType = check_voice_approval(From, Packet, Lang, StateData), |
4293 |
9 |
route_voice_approval(AppType, From, Packet, Lang, StateData); |
4294 |
|
_ -> |
4295 |
4 |
InType = check_invitation(From, Els, Lang, StateData), |
4296 |
4 |
route_invitation(InType, From, Packet, Lang, StateData) |
4297 |
|
end; |
4298 |
|
route_message(#routed_message{allowed = true, from = From, packet = Packet, |
4299 |
|
lang = Lang}, StateData) -> |
4300 |
:-( |
ErrText = <<"Improper message type">>, |
4301 |
:-( |
Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:not_acceptable(Lang, ErrText)), |
4302 |
:-( |
ejabberd_router:route(StateData#state.jid, |
4303 |
|
From, Err), |
4304 |
:-( |
StateData; |
4305 |
|
route_message(#routed_message{type = <<"error">>}, StateData) -> |
4306 |
:-( |
StateData; |
4307 |
|
route_message(#routed_message{from = From, packet = Packet, lang = Lang}, |
4308 |
|
StateData) -> |
4309 |
16 |
handle_roommessage_from_nonparticipant(Packet, Lang, StateData, From), |
4310 |
16 |
StateData. |
4311 |
|
|
4312 |
|
-spec schedule_queue_processing_when_empty(RoomQueueEmpty :: boolean(), |
4313 |
|
RoomShaper :: mongoose_shaper:shaper(), |
4314 |
|
RoomShaperInterval :: non_neg_integer(), |
4315 |
|
StateData :: state()) -> state(). |
4316 |
|
schedule_queue_processing_when_empty(true, RoomShaper, RoomShaperInterval, StateData) -> |
4317 |
:-( |
erlang:send_after(RoomShaperInterval, self(), process_room_queue), |
4318 |
:-( |
StateData#state{room_shaper = RoomShaper}; |
4319 |
|
schedule_queue_processing_when_empty(_RoomQueueEmpty, _RoomShaper, |
4320 |
|
_RoomShaperInterval, StateData) -> |
4321 |
:-( |
StateData. |
4322 |
|
|
4323 |
|
-spec route_error(mod_muc:nick(), jid:jid(), exml:element(), state()) -> state(). |
4324 |
|
route_error(Nick, From, Error, StateData) -> |
4325 |
|
%% TODO: s/Nick/<<>>/ |
4326 |
47 |
ejabberd_router:route(jid:replace_resource(StateData#state.jid, Nick), |
4327 |
|
From, Error), |
4328 |
47 |
StateData. |
4329 |
|
|
4330 |
|
|
4331 |
|
-spec route_voice_approval('ok' | {'error', exml:element()} | {'form', binary()} |
4332 |
|
| {'role', binary(), binary()}, jid:jid(), exml:element(), |
4333 |
|
ejabberd:lang(), state()) -> state(). |
4334 |
|
route_voice_approval({error, ErrType}, From, Packet, _Lang, StateData) -> |
4335 |
5 |
ejabberd_router:route(StateData#state.jid, From, |
4336 |
|
jlib:make_error_reply(Packet, ErrType)), |
4337 |
5 |
StateData; |
4338 |
|
route_voice_approval({form, RoleName}, From, _Packet, _Lang, StateData) -> |
4339 |
2 |
{Nick, _} = get_participant_data(From, StateData), |
4340 |
2 |
ApprovalForm = make_voice_approval_form(From, Nick, RoleName), |
4341 |
2 |
F = fun({_, Info}) -> |
4342 |
2 |
ejabberd_router:route(StateData#state.jid, Info#user.jid, |
4343 |
|
ApprovalForm) |
4344 |
|
end, |
4345 |
2 |
lists:foreach(F, search_role(moderator, StateData)), |
4346 |
2 |
StateData; |
4347 |
|
route_voice_approval({role, BRole, Nick}, From, Packet, Lang, StateData) -> |
4348 |
1 |
Items = [#xmlel{name = <<"item">>, |
4349 |
|
attrs = [{<<"role">>, BRole}, |
4350 |
|
{<<"nick">>, Nick}]}], |
4351 |
1 |
case process_admin_items_set(From, Items, Lang, StateData) of |
4352 |
1 |
{result, _Res, SD1} -> SD1; |
4353 |
|
{error, Error} -> |
4354 |
:-( |
ejabberd_router:route(StateData#state.jid, From, |
4355 |
|
jlib:make_error_reply(Packet, Error)), |
4356 |
:-( |
StateData |
4357 |
|
end; |
4358 |
|
route_voice_approval(_Type, From, Packet, _Lang, StateData) -> |
4359 |
1 |
ejabberd_router:route(StateData#state.jid, From, |
4360 |
|
jlib:make_error_reply(Packet, mongoose_xmpp_errors:bad_request())), |
4361 |
1 |
StateData. |
4362 |
|
|
4363 |
|
|
4364 |
|
-spec route_invitation(InvitationsOrError, |
4365 |
|
From, Packet, Lang, state()) -> state() when |
4366 |
|
InvitationsOrError :: {'error', jlib:xmlcdata() | exml:element()} |
4367 |
|
| {'ok', [jid:jid()]}, |
4368 |
|
From :: jid:simple_jid() | jid:jid(), |
4369 |
|
Packet :: exml:element(), |
4370 |
|
Lang :: ejabberd:lang(). |
4371 |
|
route_invitation({error, Error}, From, Packet, _Lang, StateData) -> |
4372 |
1 |
Err = jlib:make_error_reply(Packet, Error), |
4373 |
1 |
ejabberd_router:route(StateData#state.jid, From, Err), |
4374 |
1 |
StateData; |
4375 |
|
route_invitation({ok, IJIDs}, _From, _Packet, _Lang, |
4376 |
|
#state{ config = #config{ members_only = true } } = StateData0) -> |
4377 |
:-( |
lists:foldl( |
4378 |
|
fun(IJID, StateData) -> |
4379 |
:-( |
case get_affiliation(IJID, StateData) of |
4380 |
|
none -> |
4381 |
:-( |
NSD = set_affiliation(IJID, member, StateData), |
4382 |
:-( |
store_room_if_persistent(NSD), |
4383 |
:-( |
NSD; |
4384 |
|
_ -> |
4385 |
:-( |
StateData |
4386 |
|
end |
4387 |
|
end, StateData0, IJIDs); |
4388 |
|
route_invitation({ok, _IJIDs}, _From, _Packet, _Lang, StateData0) -> |
4389 |
3 |
StateData0. |
4390 |
|
|
4391 |
|
-spec store_room_if_persistent(state()) -> any(). |
4392 |
|
store_room_if_persistent(#state{ host = Host, room = Room, host_type = HostType, |
4393 |
|
config = #config{ persistent = true } } = StateData) -> |
4394 |
:-( |
mod_muc:store_room(HostType, Host, Room, make_opts(StateData)); |
4395 |
|
store_room_if_persistent(_SD) -> |
4396 |
:-( |
ok. |
4397 |
|
|
4398 |
|
-spec route_iq(mongoose_acc:t(), routed_iq(), state()) -> {ok | stop, state()}. |
4399 |
|
route_iq(_Acc, #routed_iq{iq = #iq{type = Type}}, StateData) |
4400 |
|
when Type == error; Type == result -> |
4401 |
:-( |
{ok, StateData}; |
4402 |
|
route_iq(Acc, #routed_iq{iq = #iq{type = Type, xmlns = ?NS_MUC_ADMIN, lang = Lang, |
4403 |
|
sub_el = SubEl}, from = From} = Routed, StateData) -> |
4404 |
114 |
Res = process_iq_admin(From, Type, Lang, SubEl, StateData), |
4405 |
114 |
do_route_iq(Acc, Res, Routed, StateData); |
4406 |
|
route_iq(Acc, #routed_iq{iq = #iq{type = Type, xmlns = ?NS_MUC_OWNER, lang = Lang, |
4407 |
|
sub_el = SubEl}, from = From} = Routed, StateData) -> |
4408 |
24 |
Res = process_iq_owner(From, Type, Lang, SubEl, StateData, normal_state), |
4409 |
24 |
do_route_iq(Acc, Res, Routed, StateData); |
4410 |
|
route_iq(Acc, #routed_iq{iq = #iq{type = Type, xmlns = ?NS_DISCO_INFO, lang = Lang}, |
4411 |
|
from = From} = Routed, StateData) -> |
4412 |
4 |
Res = process_iq_disco_info(From, Type, Lang, StateData), |
4413 |
4 |
do_route_iq(Acc, Res, Routed, StateData); |
4414 |
|
route_iq(Acc, #routed_iq{iq = #iq{type = Type, xmlns = ?NS_DISCO_ITEMS, lang = Lang}, |
4415 |
|
from = From} = Routed, StateData) -> |
4416 |
2 |
Res = process_iq_disco_items(From, Type, Lang, StateData), |
4417 |
2 |
do_route_iq(Acc, Res, Routed, StateData); |
4418 |
|
route_iq(Acc, #routed_iq{iq = IQ = #iq{}, packet = Packet, from = From}, |
4419 |
|
#state{host = Host, host_type = HostType, jid = RoomJID} = StateData) -> |
4420 |
|
%% Custom IQ, addressed to this room's JID. |
4421 |
415 |
case mod_muc_iq:process_iq(Host, From, RoomJID, Acc, IQ) of |
4422 |
|
{Acc1, error} -> |
4423 |
:-( |
?LOG_WARNING(#{what => muc_process_iq_failed, acc => Acc, server => Host, |
4424 |
:-( |
host_type => HostType, room_jid => RoomJID}), |
4425 |
:-( |
E = mongoose_xmpp_errors:feature_not_implemented( |
4426 |
|
<<"en">>, <<"From mod_muc_room">>), |
4427 |
:-( |
{Acc2, Err} = jlib:make_error_reply(Acc1, Packet, E), |
4428 |
:-( |
ejabberd_router:route(RoomJID, From, Acc2, Err); |
4429 |
415 |
_ -> ok |
4430 |
|
end, |
4431 |
415 |
{ok, StateData}; |
4432 |
|
route_iq(Acc, #routed_iq{packet = Packet, from = From}, StateData) -> |
4433 |
:-( |
{Acc1, Err} = jlib:make_error_reply( |
4434 |
|
Acc, Packet, mongoose_xmpp_errors:feature_not_implemented()), |
4435 |
:-( |
ejabberd_router:route(StateData#state.jid, From, Acc1, Err), |
4436 |
:-( |
{ok, StateData}. |
4437 |
|
|
4438 |
|
|
4439 |
|
-spec do_route_iq(mongoose_acc:t(), {result, [exml:element()], state()} | {error, exml:element()}, |
4440 |
|
routed_iq(), state()) -> {ok | stop, state()}. |
4441 |
|
do_route_iq(Acc, Res1, #routed_iq{iq = #iq{xmlns = XMLNS, sub_el = SubEl} = IQ, |
4442 |
|
from = From}, StateData) -> |
4443 |
144 |
{IQRes, RoutingResult} = case Res1 of |
4444 |
|
{result, Res, SD} -> |
4445 |
119 |
{ |
4446 |
|
IQ#iq{type = result, |
4447 |
|
sub_el = [#xmlel{name = <<"query">>, |
4448 |
|
attrs = [{<<"xmlns">>, XMLNS}], |
4449 |
|
children = Res}]}, |
4450 |
|
case SD of |
4451 |
2 |
stop -> {stop, StateData}; |
4452 |
117 |
_ -> {ok, SD} |
4453 |
|
end |
4454 |
|
}; |
4455 |
|
{error, Error} -> |
4456 |
25 |
{ |
4457 |
|
IQ#iq{type = error, sub_el = [SubEl, Error]}, |
4458 |
|
{ok, StateData} |
4459 |
|
} |
4460 |
|
end, |
4461 |
144 |
ejabberd_router:route(StateData#state.jid, From, Acc, |
4462 |
|
jlib:iq_to_xml(IQRes)), |
4463 |
144 |
RoutingResult. |
4464 |
|
|
4465 |
|
|
4466 |
|
-spec route_nick_message(routed_nick_message(), state()) -> state(). |
4467 |
|
route_nick_message(#routed_nick_message{decide = {expulse_sender, _Reason}, |
4468 |
|
packet = Packet, lang = Lang, from = From}, StateData) -> |
4469 |
:-( |
ErrorText = <<"This participant is kicked from the room because he", |
4470 |
|
"sent an error message to another participant">>, |
4471 |
:-( |
?LOG_DEBUG(ls(#{what => muc_expulse_sender, text => ErrorText, |
4472 |
:-( |
user => From#jid.luser, exml_packet => Packet}, StateData)), |
4473 |
:-( |
expulse_participant(Packet, From, StateData, translate:translate(Lang, ErrorText)); |
4474 |
|
route_nick_message(#routed_nick_message{decide = forget_message}, StateData) -> |
4475 |
:-( |
StateData; |
4476 |
|
route_nick_message(#routed_nick_message{decide = continue_delivery, allow_pm = true, |
4477 |
|
online = true, packet = Packet, from = From, type = <<"groupchat">>, |
4478 |
|
lang = Lang, nick = ToNick}, StateData) -> |
4479 |
1 |
ErrText = <<"It is not allowed to send private messages of type groupchat">>, |
4480 |
1 |
Err = jlib:make_error_reply( |
4481 |
|
Packet, mongoose_xmpp_errors:bad_request(Lang, ErrText)), |
4482 |
1 |
route_error(ToNick, From, Err, StateData), |
4483 |
1 |
StateData; |
4484 |
|
route_nick_message(#routed_nick_message{decide = continue_delivery, allow_pm = true, |
4485 |
|
online = true, packet = Packet, from = From, |
4486 |
|
lang = Lang, nick = ToNick, jid = false}, StateData) -> |
4487 |
1 |
ErrText = <<"Recipient is not in the conference room">>, |
4488 |
1 |
Err = jlib:make_error_reply( |
4489 |
|
Packet, mongoose_xmpp_errors:item_not_found(Lang, ErrText)), |
4490 |
1 |
route_error(ToNick, From, Err, StateData), |
4491 |
1 |
StateData; |
4492 |
|
route_nick_message(#routed_nick_message{decide = continue_delivery, allow_pm = true, |
4493 |
|
online = true, packet = Packet, from = From, jid = ToJID}, StateData) -> |
4494 |
28 |
Packet1 = maybe_add_x_element(Packet), |
4495 |
28 |
{ok, #user{nick = FromNick}} = maps:find(jid:to_lower(From), |
4496 |
|
StateData#state.users), |
4497 |
28 |
ejabberd_router:route( |
4498 |
|
jid:replace_resource(StateData#state.jid, FromNick), ToJID, Packet1), |
4499 |
28 |
StateData; |
4500 |
|
route_nick_message(#routed_nick_message{decide = continue_delivery, |
4501 |
|
allow_pm = true, |
4502 |
|
online = false} = Routed, StateData) -> |
4503 |
1 |
#routed_nick_message{packet = Packet, from = From, |
4504 |
|
lang = Lang, nick = ToNick} = Routed, |
4505 |
1 |
RoomJID = jid:replace_resource(StateData#state.jid, ToNick), |
4506 |
1 |
send_error_only_occupants(<<"messages">>, Packet, Lang, RoomJID, From), |
4507 |
1 |
StateData; |
4508 |
|
route_nick_message(#routed_nick_message{decide = continue_delivery, allow_pm = false, |
4509 |
|
packet = Packet, from = From, |
4510 |
|
lang = Lang, nick = ToNick}, StateData) -> |
4511 |
:-( |
ErrText = <<"It is not allowed to send private messages">>, |
4512 |
:-( |
Err = jlib:make_error_reply( |
4513 |
|
Packet, mongoose_xmpp_errors:forbidden(Lang, ErrText)), |
4514 |
:-( |
route_error(ToNick, From, Err, StateData), |
4515 |
:-( |
StateData. |
4516 |
|
|
4517 |
|
|
4518 |
|
-spec route_nick_iq(routed_nick_iq(), state()) -> 'ok'. |
4519 |
|
route_nick_iq(#routed_nick_iq{allow_query = true, online = {true, _, _}, jid = false, |
4520 |
|
iq = reply}, _StateData) -> |
4521 |
:-( |
ok; |
4522 |
|
route_nick_iq(#routed_nick_iq{allow_query = true, online = {true, _, _}, jid = false, |
4523 |
|
packet = Packet, lang = Lang, from = From, nick = ToNick}, StateData) -> |
4524 |
:-( |
ErrText = <<"Recipient is not in the conference room">>, |
4525 |
:-( |
Err = jlib:make_error_reply( |
4526 |
|
Packet, mongoose_xmpp_errors:item_not_found(Lang, ErrText)), |
4527 |
:-( |
route_error(ToNick, From, Err, StateData); |
4528 |
|
route_nick_iq(#routed_nick_iq{allow_query = true, online = {true, NewId, FromFull}, |
4529 |
|
jid = ToJID, packet = Packet, stanza = StanzaId}, StateData) -> |
4530 |
:-( |
{ok, #user{nick = FromNick}} = maps:find(jid:to_lower(FromFull), |
4531 |
|
StateData#state.users), |
4532 |
:-( |
{ToJID2, Packet2} = handle_iq_vcard(FromFull, ToJID, StanzaId, NewId, Packet), |
4533 |
:-( |
ejabberd_router:route( |
4534 |
|
jid:replace_resource(StateData#state.jid, FromNick), |
4535 |
|
ToJID2, Packet2); |
4536 |
|
route_nick_iq(#routed_nick_iq{online = {false, _, _}, iq = reply}, _StateData) -> |
4537 |
:-( |
ok; |
4538 |
|
route_nick_iq(#routed_nick_iq{online = {false, _, _}, from = From, nick = ToNick, |
4539 |
|
packet = Packet, lang = Lang}, StateData) -> |
4540 |
:-( |
RoomJID = jid:replace_resource(StateData#state.jid, ToNick), |
4541 |
:-( |
send_error_only_occupants(<<"queries">>, Packet, Lang, RoomJID, From); |
4542 |
|
route_nick_iq(#routed_nick_iq{iq = reply}, _StateData) -> |
4543 |
:-( |
ok; |
4544 |
|
route_nick_iq(#routed_nick_iq{packet = Packet, lang = Lang, nick = ToNick, |
4545 |
|
from = From}, StateData) -> |
4546 |
:-( |
ErrText = <<"Queries to the conference members are " |
4547 |
|
"not allowed in this room">>, |
4548 |
:-( |
Err = jlib:make_error_reply(Packet, mongoose_xmpp_errors:not_allowed(Lang, ErrText)), |
4549 |
:-( |
route_error(ToNick, From, Err, StateData). |
4550 |
|
|
4551 |
|
|
4552 |
|
-spec decode_reason(exml:element()) -> binary(). |
4553 |
|
decode_reason(Elem) -> |
4554 |
137 |
xml:get_path_s(Elem, [{elem, <<"reason">>}, cdata]). |
4555 |
|
|
4556 |
|
-spec make_voice_approval_form(From :: jid:simple_jid() | jid:jid(), |
4557 |
|
Nick :: binary(), Role :: binary()) -> exml:element(). |
4558 |
|
make_voice_approval_form(From, Nick, Role) -> |
4559 |
2 |
Title = <<"Voice request">>, |
4560 |
2 |
Instructions = <<"To approve this request" |
4561 |
|
" for voice, select the "Grant voice to this person?" checkbox" |
4562 |
|
" and click OK. To skip this request, click the cancel button.">>, |
4563 |
2 |
Fields = [#{var => <<"muc#role">>, type => <<"list-single">>, |
4564 |
|
label => <<"Request role">>, values => [Role]}, |
4565 |
|
#{var => <<"muc#jid">>, type => <<"jid-single">>, |
4566 |
|
label => <<"User ID">>, values => [jid:to_binary(From)]}, |
4567 |
|
#{var => <<"muc#roomnick">>, type => <<"text-single">>, |
4568 |
|
label => <<"Room Nickname">>, values => [Nick]}, |
4569 |
|
#{var => <<"muc#request_allow">>, type => <<"boolean">>, |
4570 |
|
label => <<"Grant voice to this person?">>, values => [<<"false">>]}], |
4571 |
2 |
Form = mongoose_data_forms:form(#{title => Title, instructions => Instructions, |
4572 |
|
ns => ?NS_MUC_REQUEST, fields => Fields}), |
4573 |
2 |
#xmlel{name = <<"message">>, children = [Form]}. |
4574 |
|
|
4575 |
|
-spec xfield(binary(), any(), binary(), binary(), ejabberd:lang()) -> mongoose_data_forms:field(). |
4576 |
|
xfield(Type, Label, Var, Val, Lang) -> |
4577 |
198 |
#{type => Type, label => translate:translate(Lang, Label), var => Var, values => [Val]}. |
4578 |
|
|
4579 |
|
-spec boolxfield(any(), binary(), any(), ejabberd:lang()) -> mongoose_data_forms:field(). |
4580 |
|
boolxfield(Label, Var, Val, Lang) -> |
4581 |
165 |
xfield(<<"boolean">>, Label, Var, |
4582 |
|
case Val of |
4583 |
107 |
true -> <<"1">>; |
4584 |
58 |
_ -> <<"0">> |
4585 |
|
end, Lang). |
4586 |
|
|
4587 |
|
stringxfield(Label, Var, Val, Lang) -> |
4588 |
22 |
xfield(<<"text-single">>, Label, Var, Val, Lang). |
4589 |
|
|
4590 |
|
privatexfield(Label, Var, Val, Lang) -> |
4591 |
11 |
xfield(<<"text-private">>, Label, Var, Val, Lang). |
4592 |
|
|
4593 |
|
notify_users_modified(#state{host_type = HostType, jid = JID, users = Users} = State) -> |
4594 |
2769 |
mod_muc_log:set_room_occupants(HostType, self(), JID, maps:values(Users)), |
4595 |
2769 |
State. |
4596 |
|
|
4597 |
|
ls(LogMap, State) -> |
4598 |
:-( |
maps:merge(LogMap, #{room => State#state.room, |
4599 |
|
sub_host => State#state.host}). |
4600 |
|
|
4601 |
|
get_opt(#state{host_type = HostType}, Opt) -> |
4602 |
11673 |
gen_mod:get_module_opt(HostType, mod_muc, Opt). |
4603 |
|
|
4604 |
|
get_current_timestamp() -> |
4605 |
59 |
SystemTime = os:system_time(second), |
4606 |
59 |
TimeStamp = calendar:system_time_to_rfc3339(SystemTime, [{offset, "Z"}]), |
4607 |
59 |
list_to_binary(TimeStamp). |
4608 |
|
|
4609 |
|
read_hibernate_timeout(HostType) -> |
4610 |
600 |
gen_mod:get_module_opt(HostType, mod_muc, hibernate_timeout). |
4611 |
|
|
4612 |
|
maybe_add_x_element(Msg) -> |
4613 |
28 |
{xmlel, Type, InfoXML, Children} = Msg, |
4614 |
28 |
case lists:member({xmlel, <<"x">>, [{<<"xmlns">>, ?NS_MUC_USER}], []}, Children) of |
4615 |
1 |
true -> Msg; |
4616 |
|
false -> |
4617 |
27 |
NewChildren = lists:append(Children, |
4618 |
|
[{xmlel, <<"x">>, [{<<"xmlns">>, ?NS_MUC_USER}], []}]), |
4619 |
27 |
{xmlel, Type, InfoXML, NewChildren} |
4620 |
|
end. |
4621 |
|
|
4622 |
|
kick_stanza_for_old_protocol(Attrs) -> |
4623 |
1 |
Lang = xml:get_attr_s(<<"xml:lang">>, Attrs), |
4624 |
1 |
ErrText = <<"You are not in the room.">>, |
4625 |
1 |
ErrText2 = translate:translate(Lang, ErrText), |
4626 |
1 |
Response = #xmlel{name = <<"presence">>, attrs = [{<<"type">>, <<"unavailable">>}]}, |
4627 |
1 |
ItemAttrs = [{<<"affiliation">>, <<"none">>}, {<<"role">>, <<"none">>}], |
4628 |
1 |
ItemEls = [#xmlel{name = <<"reason">>, children = [#xmlcdata{content = ErrText2}]}], |
4629 |
1 |
Status = [status_code(110), status_code(307), status_code(333)], |
4630 |
1 |
xml:append_subtags( |
4631 |
|
Response, |
4632 |
|
[#xmlel{name = <<"x">>, attrs = [{<<"xmlns">>, ?NS_MUC}], |
4633 |
|
children = [#xmlel{name = <<"item">>, attrs = ItemAttrs, |
4634 |
|
children = ItemEls} | Status]}]). |