./ct_report/coverage/mod_muc_light_api.COVER.html

1 %% @doc Provide an interface for frontends (like graphql or ctl) to manage MUC Light rooms.
2 -module(mod_muc_light_api).
3
4 -export([create_room/3,
5 create_room/4,
6 invite_to_room/3,
7 change_room_config/3,
8 change_affiliation/4,
9 send_message/3,
10 send_message/4,
11 delete_room/2,
12 delete_room/1,
13 get_room_messages/3,
14 get_room_messages/4,
15 get_room_messages/5,
16 get_user_rooms/1,
17 get_room_info/1,
18 get_room_info/2,
19 get_room_aff/1,
20 get_room_aff/2,
21 get_blocking_list/1,
22 set_blocking/2
23 ]).
24
25 -include("mod_muc_light.hrl").
26 -include("mongoose.hrl").
27 -include("jlib.hrl").
28 -include("mongoose_rsm.hrl").
29
30 -type room() :: #{jid := jid:jid(),
31 aff_users := aff_users(),
32 options := map()}.
33
34 -export_type([room/0]).
35
36 -define(ROOM_DELETED_SUCC_RESULT, {ok, "Room deleted successfully"}).
37 -define(USER_NOT_ROOM_MEMBER_RESULT, {not_room_member, "Given user does not occupy this room"}).
38 -define(ROOM_NOT_FOUND_RESULT, {room_not_found, "Room not found"}).
39 -define(MUC_SERVER_NOT_FOUND_RESULT, {muc_server_not_found, "MUC Light server not found"}).
40 -define(VALIDATION_ERROR_RESULT(Key, Reason),
41 {validation_error, io_lib:format("Validation failed for key: ~ts with reason ~p",
42 [Key, Reason])}).
43
44 -spec create_room(jid:lserver(), jid:jid(), map()) ->
45 {ok, room()} | {user_not_found | muc_server_not_found |
46 max_occupants_reached | validation_error, iolist()}.
47 create_room(MUCLightDomain, CreatorJID, Config) ->
48
:-(
M = #{user => CreatorJID, room => jid:make_bare(<<>>, MUCLightDomain), options => Config},
49
:-(
fold(M, [fun check_user/1, fun check_muc_domain/1, fun create_room_raw/1]).
50
51 -spec create_room(jid:lserver(), jid:luser(), jid:jid(), map()) ->
52 {ok, room()} | {user_not_found | muc_server_not_found | already_exists |
53 max_occupants_reached | validation_error, iolist()}.
54 create_room(MUCLightDomain, RoomID, CreatorJID, Config) ->
55
:-(
M = #{user => CreatorJID, room => jid:make_bare(RoomID, MUCLightDomain), options => Config},
56
:-(
fold(M, [fun check_user/1, fun check_muc_domain/1, fun create_room_raw/1]).
57
58 -spec invite_to_room(jid:jid(), jid:jid(), jid:jid()) ->
59 {ok | user_not_found | muc_server_not_found | room_not_found | not_room_member, iolist()}.
60 invite_to_room(RoomJID, SenderJID, RecipientJID) ->
61
:-(
M = #{user => SenderJID, room => RoomJID, recipient => RecipientJID},
62
:-(
fold(M, [fun check_user/1, fun check_muc_domain/1, fun get_user_aff/1,
63 fun do_invite_to_room/1]).
64
65 -spec change_room_config(jid:jid(), jid:jid(), map()) ->
66 {ok, room()} | {user_not_found | muc_server_not_found | room_not_found | not_room_member |
67 not_allowed | validation_error, iolist()}.
68 change_room_config(RoomJID, UserJID, Config) ->
69
:-(
M = #{user => UserJID, room => RoomJID, config => Config},
70
:-(
fold(M, [fun check_user/1, fun check_muc_domain/1, fun do_change_room_config/1]).
71
72 -spec change_affiliation(jid:jid(), jid:jid(), jid:jid(), add | remove) ->
73 {ok | user_not_found | muc_server_not_found | room_not_found | not_room_member |
74 not_allowed, iolist()}.
75 change_affiliation(RoomJID, SenderJID, RecipientJID, Op) ->
76
:-(
M = #{user => SenderJID, room => RoomJID, recipient => RecipientJID, op => Op},
77
:-(
fold(M, [fun check_user/1, fun check_muc_domain/1, fun get_user_aff/1,
78 fun check_aff_permission/1, fun do_change_affiliation/1]).
79
80 -spec send_message(jid:jid(), jid:jid(), binary()) ->
81 {ok | user_not_found | muc_server_not_found | room_not_found | not_room_member, iolist()}.
82 send_message(RoomJID, SenderJID, Text) when is_binary(Text) ->
83
:-(
Body = #xmlel{name = <<"body">>, children = [#xmlcdata{content = Text}]},
84
:-(
send_message(RoomJID, SenderJID, [Body], []).
85
86 -spec send_message(jid:jid(), jid:jid(), [exml:element()], [exml:attr()]) ->
87 {ok | user_not_found | muc_server_not_found | room_not_found | not_room_member, iolist()}.
88 send_message(RoomJID, SenderJID, Children, ExtraAttrs) ->
89
:-(
M = #{user => SenderJID, room => RoomJID, children => Children, attrs => ExtraAttrs},
90
:-(
fold(M, [fun check_user/1, fun check_muc_domain/1, fun get_user_aff/1, fun do_send_message/1]).
91
92 -spec delete_room(jid:jid(), jid:jid()) ->
93 {ok | not_allowed | room_not_found | not_room_member | muc_server_not_found , iolist()}.
94 delete_room(RoomJID, UserJID) ->
95
:-(
M = #{user => UserJID, room => RoomJID},
96
:-(
fold(M, [fun check_user/1, fun check_muc_domain/1, fun get_user_aff/1,
97 fun check_delete_permission/1, fun do_delete_room/1]).
98
99 -spec delete_room(jid:jid()) -> {ok | muc_server_not_found | room_not_found, iolist()}.
100 delete_room(RoomJID) ->
101
:-(
M = #{room => RoomJID},
102
:-(
fold(M, [fun check_muc_domain/1, fun do_delete_room/1]).
103
104 -spec get_room_messages(jid:jid(), jid:jid(), integer() | undefined,
105 mod_mam:unix_timestamp() | undefined) ->
106 {ok, list()} | {user_not_found | muc_server_not_found | room_not_found | not_room_member |
107 internal, iolist()}.
108 get_room_messages(RoomJID, UserJID, PageSize, Before) ->
109
:-(
M = #{user => UserJID, room => RoomJID, page_size => PageSize, before => Before},
110
:-(
fold(M, [fun check_user/1, fun check_muc_domain/1, fun get_user_aff/1,
111 fun do_get_room_messages/1]).
112
113 -spec get_room_messages(jid:jid(), integer() | undefined,
114 mod_mam:unix_timestamp() | undefined) ->
115 {ok, [mod_mam:message_row()]} | {muc_server_not_found | room_not_found | internal, iolist()}.
116 get_room_messages(RoomJID, PageSize, Before) ->
117
:-(
M = #{user => undefined, room => RoomJID, page_size => PageSize, before => Before},
118
:-(
fold(M, [fun check_muc_domain/1, fun check_room/1, fun do_get_room_messages/1]).
119
120 -spec get_room_info(jid:jid(), jid:jid()) ->
121 {ok, room()} | {user_not_found | muc_server_not_found | room_not_found | not_room_member,
122 iolist()}.
123 get_room_info(RoomJID, UserJID) ->
124
:-(
M = #{user => UserJID, room => RoomJID},
125
:-(
fold(M, [fun check_user/1, fun check_muc_domain/1, fun do_get_room_info/1,
126 fun check_room_member/1, fun return_info/1]).
127
128 -spec get_room_info(jid:jid()) -> {ok, room()} | {muc_server_not_found | room_not_found, iolist()}.
129 get_room_info(RoomJID) ->
130
:-(
M = #{room => RoomJID},
131
:-(
fold(M, [fun check_muc_domain/1, fun do_get_room_info/1, fun return_info/1]).
132
133 -spec get_room_aff(jid:jid(), jid:jid()) ->
134 {ok, aff_users()} | {user_not_found | muc_server_not_found | room_not_found | not_room_member,
135 iolist()}.
136 get_room_aff(RoomJID, UserJID) ->
137
:-(
M = #{user => UserJID, room => RoomJID},
138
:-(
fold(M, [fun check_user/1, fun check_muc_domain/1, fun do_get_room_aff/1,
139 fun check_room_member/1, fun return_aff/1]).
140
141 -spec get_room_aff(jid:jid()) ->
142 {ok, aff_users()} | {muc_server_not_found | room_not_found, iolist()}.
143 get_room_aff(RoomJID) ->
144
:-(
M = #{room => RoomJID},
145
:-(
fold(M, [fun check_muc_domain/1, fun do_get_room_aff/1, fun return_aff/1]).
146
147 -spec get_user_rooms(jid:jid()) ->
148 {ok, [RoomUS :: jid:simple_bare_jid()]} | {user_not_found, iolist()}.
149 get_user_rooms(UserJID) ->
150
:-(
fold(#{user => UserJID}, [fun check_user/1, fun do_get_user_rooms/1]).
151
152 -spec get_blocking_list(jid:jid()) -> {ok, [blocking_item()]} | {user_not_found, iolist()}.
153 get_blocking_list(UserJID) ->
154
:-(
fold(#{user => UserJID}, [fun check_user/1, fun do_get_blocking_list/1]).
155
156 -spec set_blocking(jid:jid(), [blocking_item()]) -> {ok | user_not_found, iolist()}.
157 set_blocking(UserJID, Items) ->
158
:-(
fold(#{user => UserJID, items => Items}, [fun check_user/1, fun do_set_blocking_list/1]).
159
160 %% Internal: steps used in fold/2
161
162 check_user(M = #{user := UserJID = #jid{lserver = LServer}}) ->
163
:-(
case mongoose_domain_api:get_domain_host_type(LServer) of
164 {ok, HostType} ->
165
:-(
case ejabberd_auth:does_user_exist(HostType, UserJID, stored) of
166
:-(
true -> M#{user_host_type => HostType};
167
:-(
false -> {user_not_found, "Given user does not exist"}
168 end;
169 {error, not_found} ->
170
:-(
{user_not_found, "User's domain does not exist"}
171 end.
172
173 check_muc_domain(M = #{room := #jid{lserver = LServer}}) ->
174
:-(
case mongoose_domain_api:get_subdomain_host_type(LServer) of
175 {ok, HostType} ->
176
:-(
M#{muc_host_type => HostType};
177 {error, not_found} ->
178
:-(
?MUC_SERVER_NOT_FOUND_RESULT
179 end.
180
181 check_room_member(M = #{user := UserJID, aff_users := AffUsers}) ->
182
:-(
case get_aff(jid:to_lus(UserJID), AffUsers) of
183 none ->
184
:-(
?USER_NOT_ROOM_MEMBER_RESULT;
185 _ ->
186
:-(
M
187 end.
188
189 create_room_raw(#{room := InRoomJID, user := CreatorJID, options := Options}) ->
190
:-(
Config = make_room_config(Options),
191
:-(
case mod_muc_light:try_to_create_room(CreatorJID, InRoomJID, Config) of
192 {ok, RoomJID, #create{aff_users = AffUsers, raw_config = Conf}} ->
193
:-(
{ok, make_room(RoomJID, Conf, AffUsers)};
194 {error, exists} ->
195
:-(
{already_exists, "Room already exists"};
196 {error, max_occupants_reached} ->
197
:-(
{max_occupants_reached, "Max occupants number reached"};
198 {error, {Key, Reason}} ->
199
:-(
?VALIDATION_ERROR_RESULT(Key, Reason)
200 end.
201
202 do_invite_to_room(#{user := SenderJID, room := RoomJID, recipient := RecipientJID}) ->
203
:-(
S = jid:to_bare(SenderJID),
204
:-(
R = jid:to_bare(RoomJID),
205
:-(
RecipientBin = jid:to_binary(jid:to_bare(RecipientJID)),
206
:-(
Changes = query(?NS_MUC_LIGHT_AFFILIATIONS, [affiliate(RecipientBin, <<"member">>)]),
207
:-(
ejabberd_router:route(S, R, iq(jid:to_binary(S), jid:to_binary(R), <<"set">>, [Changes])),
208
:-(
{ok, "User invited successfully"}.
209
210 do_change_room_config(#{user := UserJID, room := RoomJID, config := Config,
211 muc_host_type := HostType}) ->
212
:-(
UserUS = jid:to_bare(UserJID),
213
:-(
ConfigReq = #config{ raw_config = maps:to_list(Config) },
214
:-(
#jid{lserver = LServer} = UserJID,
215
:-(
#jid{luser = RoomID, lserver = MUCServer} = RoomJID,
216
:-(
Acc = mongoose_acc:new(#{location => ?LOCATION, lserver => LServer, host_type => HostType}),
217
:-(
case mod_muc_light:change_room_config(UserUS, RoomID, MUCServer, ConfigReq, Acc) of
218 {ok, RoomJID, KV} ->
219
:-(
{ok, make_room(RoomJID, KV, [])};
220 {error, item_not_found} ->
221
:-(
?USER_NOT_ROOM_MEMBER_RESULT;
222 {error, not_allowed} ->
223
:-(
{not_allowed, "Given user does not have permission to change config"};
224 {error, not_exists} ->
225
:-(
?ROOM_NOT_FOUND_RESULT;
226 {error, {Key, Reason}} ->
227
:-(
?VALIDATION_ERROR_RESULT(Key, Reason)
228 end.
229
230 check_aff_permission(M = #{user := UserJID, recipient := RecipientJID, aff := Aff, op := Op}) ->
231
:-(
case {Aff, Op} of
232 {member, remove} when RecipientJID =:= UserJID ->
233
:-(
M;
234 {owner, _} ->
235
:-(
M;
236
:-(
_ -> {not_allowed, "Given user does not have permission to change affiliations"}
237 end.
238
239
:-(
check_delete_permission(M = #{aff := owner}) -> M;
240
:-(
check_delete_permission(#{}) -> {not_allowed, "Given user cannot delete this room"}.
241
242 get_user_aff(M = #{muc_host_type := HostType, user := UserJID, room := RoomJID}) ->
243
:-(
case get_room_user_aff(HostType, RoomJID, UserJID) of
244 {ok, owner} ->
245
:-(
M#{aff => owner};
246 {ok, member} ->
247
:-(
M#{aff => member};
248 {ok, none} ->
249
:-(
?USER_NOT_ROOM_MEMBER_RESULT;
250 {error, room_not_found} ->
251
:-(
?ROOM_NOT_FOUND_RESULT
252 end.
253
254 do_change_affiliation(#{user := SenderJID, room := RoomJID, recipient := RecipientJID, op := Op}) ->
255
:-(
RecipientBare = jid:to_bare(RecipientJID),
256
:-(
S = jid:to_bare(SenderJID),
257
:-(
Changes = query(?NS_MUC_LIGHT_AFFILIATIONS,
258 [affiliate(jid:to_binary(RecipientBare), op_to_aff(Op))]),
259
:-(
ejabberd_router:route(S, RoomJID, iq(jid:to_binary(S), jid:to_binary(RoomJID),
260 <<"set">>, [Changes])),
261
:-(
{ok, "Affiliation change request sent successfully"}.
262
263 do_send_message(#{user := SenderJID, room := RoomJID, children := Children, attrs := ExtraAttrs}) ->
264
:-(
SenderBare = jid:to_bare(SenderJID),
265
:-(
RoomBare = jid:to_bare(RoomJID),
266
:-(
Stanza = #xmlel{name = <<"message">>,
267 attrs = [{<<"type">>, <<"groupchat">>} | ExtraAttrs],
268 children = Children},
269
:-(
ejabberd_router:route(SenderBare, RoomBare, Stanza),
270
:-(
{ok, "Message sent successfully"}.
271
272 do_delete_room(#{room := RoomJID}) ->
273
:-(
case mod_muc_light:delete_room(jid:to_lus(RoomJID)) of
274 ok ->
275
:-(
?ROOM_DELETED_SUCC_RESULT;
276 {error, not_exists} ->
277
:-(
?ROOM_NOT_FOUND_RESULT
278 end.
279
280 do_get_room_messages(#{user := CallerJID, room := RoomJID, page_size := PageSize, before := Before,
281 muc_host_type := HostType}) ->
282
:-(
get_room_messages(HostType, RoomJID, CallerJID, PageSize, Before).
283
284 %% Exported for mod_muc_api
285 get_room_messages(HostType, RoomJID, CallerJID, PageSize, Before) ->
286
:-(
ArchiveID = mod_mam_muc:archive_id_int(HostType, RoomJID),
287
:-(
Now = os:system_time(microsecond),
288
:-(
End = maybe_before(Before, Now),
289
:-(
RSM = #rsm_in{direction = before, id = undefined},
290
:-(
Params = #{archive_id => ArchiveID,
291 owner_jid => RoomJID,
292 rsm => RSM,
293 borders => undefined,
294 start_ts => undefined,
295 end_ts => End,
296 now => Now,
297 with_jid => undefined,
298 search_text => undefined,
299 page_size => PageSize,
300 limit_passed => true,
301 max_result_limit => 50,
302 is_simple => true},
303
:-(
case mod_mam_muc:lookup_messages(HostType, maybe_caller_jid(CallerJID, Params)) of
304 {ok, {_, _, Messages}} ->
305
:-(
{ok, Messages};
306 {error, Term} ->
307
:-(
{internal, io_lib:format("Internal error occured ~p", [Term])}
308 end.
309
310 do_get_room_info(M = #{room := RoomJID, muc_host_type := HostType}) ->
311
:-(
case mod_muc_light_db_backend:get_info(HostType, jid:to_lus(RoomJID)) of
312 {ok, Config, AffUsers, _Version} ->
313
:-(
M#{aff_users => AffUsers, options => Config};
314 {error, not_exists} ->
315
:-(
?ROOM_NOT_FOUND_RESULT
316 end.
317
318 return_info(#{room := RoomJID, aff_users := AffUsers, options := Config}) ->
319
:-(
{ok, make_room(jid:to_binary(RoomJID), Config, AffUsers)}.
320
321 do_get_room_aff(M = #{room := RoomJID, muc_host_type := HostType}) ->
322
:-(
case mod_muc_light_db_backend:get_aff_users(HostType, jid:to_lus(RoomJID)) of
323 {ok, AffUsers, _Version} ->
324
:-(
M#{aff_users => AffUsers};
325 {error, not_exists} ->
326
:-(
?ROOM_NOT_FOUND_RESULT
327 end.
328
329 return_aff(#{aff_users := AffUsers}) ->
330
:-(
{ok, AffUsers}.
331
332 check_room(M = #{room := RoomJID, muc_host_type := HostType}) ->
333
:-(
case mod_muc_light_db_backend:room_exists(HostType, jid:to_lus(RoomJID)) of
334 true ->
335
:-(
M;
336 false ->
337
:-(
?ROOM_NOT_FOUND_RESULT
338 end.
339
340 do_get_user_rooms(#{user := UserJID, user_host_type := HostType}) ->
341
:-(
MUCServer = mod_muc_light_utils:server_host_to_muc_host(HostType, UserJID#jid.lserver),
342
:-(
{ok, mod_muc_light_db_backend:get_user_rooms(HostType, jid:to_lus(UserJID), MUCServer)}.
343
344 do_get_blocking_list(#{user := UserJID, user_host_type := HostType}) ->
345
:-(
MUCServer = mod_muc_light_utils:server_host_to_muc_host(HostType, UserJID#jid.lserver),
346
:-(
{ok, mod_muc_light_db_backend:get_blocking(HostType, jid:to_lus(UserJID), MUCServer)}.
347
348 do_set_blocking_list(#{user := UserJID, user_host_type := HostType, items := Items}) ->
349
:-(
MUCServer = mod_muc_light_utils:server_host_to_muc_host(HostType, UserJID#jid.lserver),
350
:-(
Q = query(?NS_MUC_LIGHT_BLOCKING, [blocking_item(I) || I <- Items]),
351
:-(
Iq = iq(jid:to_binary(UserJID), MUCServer, <<"set">>, [Q]),
352
:-(
ejabberd_router:route(UserJID, jid:from_binary(MUCServer), Iq),
353
:-(
{ok, "User blocking list updated successfully"}.
354
355 %% Internal: helpers
356
357 -spec blocking_item(blocking_item()) -> exml:element().
358 blocking_item({What, Action, Who}) ->
359
:-(
#xmlel{name = atom_to_binary(What),
360 attrs = [{<<"action">>, atom_to_binary(Action)}],
361 children = [#xmlcdata{ content = jid:to_binary(Who)}]
362 }.
363
364 -spec make_room_config(map()) -> create_req_props().
365 make_room_config(Options) ->
366
:-(
#create{raw_config = maps:to_list(Options)}.
367
368 -spec get_room_user_aff(mongooseim:host_type(), jid:jid(), jid:jid()) ->
369 {ok, aff()} | {error, room_not_found}.
370 get_room_user_aff(HostType, RoomJID, UserJID) ->
371
:-(
RoomUS = jid:to_lus(RoomJID),
372
:-(
UserUS = jid:to_lus(UserJID),
373
:-(
case mod_muc_light_db_backend:get_aff_users(HostType, RoomUS) of
374 {ok, Affs, _Version} ->
375
:-(
{ok, get_aff(UserUS, Affs)};
376 {error, not_exists} ->
377
:-(
{error, room_not_found}
378 end.
379
380 -spec get_aff(jid:simple_bare_jid(), aff_users()) -> aff().
381 get_aff(UserUS, Affs) ->
382
:-(
case lists:keyfind(UserUS, 1, Affs) of
383
:-(
{_, Aff} -> Aff;
384
:-(
false -> none
385 end.
386
387 make_room(JID, #config{ raw_config = Options}, AffUsers) ->
388
:-(
make_room(JID, Options, AffUsers);
389 make_room(JID, Options, AffUsers) when is_list(Options) ->
390
:-(
make_room(JID, maps:from_list(ensure_keys_are_binaries(Options)), AffUsers);
391 make_room(JID, Options, AffUsers) when is_map(Options) ->
392
:-(
#{jid => JID, aff_users => AffUsers, options => Options}.
393
394 ensure_keys_are_binaries([{K, _}|_] = Conf) when is_binary(K) ->
395
:-(
Conf;
396 ensure_keys_are_binaries(Conf) ->
397
:-(
[{atom_to_binary(K), V} || {K, V} <- Conf].
398
399 iq(To, From, Type, Children) ->
400
:-(
UUID = uuid:uuid_to_string(uuid:get_v4(), binary_standard),
401
:-(
#xmlel{name = <<"iq">>,
402 attrs = [{<<"from">>, From},
403 {<<"to">>, To},
404 {<<"type">>, Type},
405 {<<"id">>, UUID}],
406 children = Children
407 }.
408
409 query(NS, Children) when is_binary(NS), is_list(Children) ->
410
:-(
#xmlel{name = <<"query">>,
411 attrs = [{<<"xmlns">>, NS}],
412 children = Children
413 }.
414
415 affiliate(JID, Kind) when is_binary(JID), is_binary(Kind) ->
416
:-(
#xmlel{name = <<"user">>,
417 attrs = [{<<"affiliation">>, Kind}],
418 children = [ #xmlcdata{ content = JID } ]
419 }.
420
421 maybe_before(undefined, Now) ->
422
:-(
Now;
423 maybe_before(Timestamp, _) ->
424
:-(
Timestamp.
425
426 maybe_caller_jid(undefined, Params) ->
427
:-(
Params;
428 maybe_caller_jid(CallerJID, Params) ->
429
:-(
Params#{caller_jid => CallerJID}.
430
431
:-(
op_to_aff(add) -> <<"member">>;
432
:-(
op_to_aff(remove) -> <<"none">>.
433
434 fold({_, _} = Result, _) ->
435
:-(
Result;
436 fold(M, [Step | Rest]) when is_map(M) ->
437
:-(
fold(Step(M), Rest).
Line Hits Source