./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 108 M = #{user => CreatorJID, room => jid:make_bare(<<>>, MUCLightDomain), options => Config},
49 108 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 57 M = #{user => CreatorJID, room => jid:make_bare(RoomID, MUCLightDomain), options => Config},
56 57 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 43 M = #{user => SenderJID, room => RoomJID, recipient => RecipientJID},
62 43 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 31 M = #{user => UserJID, room => RoomJID, config => Config},
70 31 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 29 M = #{user => SenderJID, room => RoomJID, recipient => RecipientJID, op => Op},
77 29 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 27 Body = #xmlel{name = <<"body">>, children = [#xmlcdata{content = Text}]},
84 27 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 39 M = #{user => SenderJID, room => RoomJID, children => Children, attrs => ExtraAttrs},
90 39 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 4 M = #{user => UserJID, room => RoomJID},
96 4 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 12 M = #{room => RoomJID},
102 12 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 16 M = #{user => UserJID, room => RoomJID, page_size => PageSize, before => Before},
110 16 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 14 M = #{user => undefined, room => RoomJID, page_size => PageSize, before => Before},
118 14 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 20 M = #{user => UserJID, room => RoomJID},
125 20 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 10 M = #{room => RoomJID},
131 10 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 5 M = #{user => UserJID, room => RoomJID},
138 5 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 8 M = #{room => RoomJID},
145 8 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 12 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 22 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 16 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 402 case mongoose_domain_api:get_domain_host_type(LServer) of
164 {ok, HostType} ->
165 389 case ejabberd_auth:does_user_exist(HostType, UserJID, stored) of
166 372 true -> M#{user_host_type => HostType};
167 17 false -> {user_not_found, "Given user does not exist"}
168 end;
169 {error, not_found} ->
170 13 {user_not_found, "User's domain does not exist"}
171 end.
172
173 check_muc_domain(M = #{room := #jid{lserver = LServer}}) ->
174 379 case mongoose_domain_api:get_subdomain_host_type(LServer) of
175 {ok, HostType} ->
176 341 M#{muc_host_type => HostType};
177 {error, not_found} ->
178 38 ?MUC_SERVER_NOT_FOUND_RESULT
179 end.
180
181 check_room_member(M = #{user := UserJID, aff_users := AffUsers}) ->
182 20 case get_aff(jid:to_lus(UserJID), AffUsers) of
183 none ->
184 3 ?USER_NOT_ROOM_MEMBER_RESULT;
185 _ ->
186 17 M
187 end.
188
189 create_room_raw(#{room := InRoomJID, user := CreatorJID, options := Options}) ->
190 142 Config = make_room_config(Options),
191 142 case mod_muc_light:try_to_create_room(CreatorJID, InRoomJID, Config) of
192 {ok, RoomJID, #create{aff_users = AffUsers, raw_config = Conf}} ->
193 133 {ok, make_room(RoomJID, Conf, AffUsers)};
194 {error, exists} ->
195 8 {already_exists, "Room already exists"};
196 {error, max_occupants_reached} ->
197
:-(
{max_occupants_reached, "Max occupants number reached"};
198 {error, {Key, Reason}} ->
199 1 ?VALIDATION_ERROR_RESULT(Key, Reason)
200 end.
201
202 do_invite_to_room(#{user := SenderJID, room := RoomJID, recipient := RecipientJID}) ->
203 27 S = jid:to_bare(SenderJID),
204 27 R = jid:to_bare(RoomJID),
205 27 RecipientBin = jid:to_binary(jid:to_bare(RecipientJID)),
206 27 Changes = query(?NS_MUC_LIGHT_AFFILIATIONS, [affiliate(RecipientBin, <<"member">>)]),
207 27 ejabberd_router:route(S, R, iq(jid:to_binary(S), jid:to_binary(R), <<"set">>, [Changes])),
208 27 {ok, "User invited successfully"}.
209
210 do_change_room_config(#{user := UserJID, room := RoomJID, config := Config,
211 muc_host_type := HostType}) ->
212 25 UserUS = jid:to_bare(UserJID),
213 25 ConfigReq = #config{ raw_config = maps:to_list(Config) },
214 25 #jid{lserver = LServer} = UserJID,
215 25 #jid{luser = RoomID, lserver = MUCServer} = RoomJID,
216 25 Acc = mongoose_acc:new(#{location => ?LOCATION, lserver => LServer, host_type => HostType}),
217 25 case mod_muc_light:change_room_config(UserUS, RoomID, MUCServer, ConfigReq, Acc) of
218 {ok, RoomJID, KV} ->
219 9 {ok, make_room(RoomJID, KV, [])};
220 {error, item_not_found} ->
221 5 ?USER_NOT_ROOM_MEMBER_RESULT;
222 {error, not_allowed} ->
223 5 {not_allowed, "Given user does not have permission to change config"};
224 {error, not_exists} ->
225 5 ?ROOM_NOT_FOUND_RESULT;
226 {error, {Key, Reason}} ->
227 1 ?VALIDATION_ERROR_RESULT(Key, Reason)
228 end.
229
230 check_aff_permission(M = #{user := UserJID, recipient := RecipientJID, aff := Aff, op := Op}) ->
231 27 case {Aff, Op} of
232 {member, remove} when RecipientJID =:= UserJID ->
233 5 M;
234 {owner, _} ->
235 20 M;
236 2 _ -> {not_allowed, "Given user does not have permission to change affiliations"}
237 end.
238
239 1 check_delete_permission(M = #{aff := owner}) -> M;
240 1 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 117 case get_room_user_aff(HostType, RoomJID, UserJID) of
244 {ok, owner} ->
245 75 M#{aff => owner};
246 {ok, member} ->
247 14 M#{aff => member};
248 {ok, none} ->
249 18 ?USER_NOT_ROOM_MEMBER_RESULT;
250 {error, room_not_found} ->
251 10 ?ROOM_NOT_FOUND_RESULT
252 end.
253
254 do_change_affiliation(#{user := SenderJID, room := RoomJID, recipient := RecipientJID, op := Op}) ->
255 25 RecipientBare = jid:to_bare(RecipientJID),
256 25 S = jid:to_bare(SenderJID),
257 25 Changes = query(?NS_MUC_LIGHT_AFFILIATIONS,
258 [affiliate(jid:to_binary(RecipientBare), op_to_aff(Op))]),
259 25 ejabberd_router:route(S, RoomJID, iq(jid:to_binary(S), jid:to_binary(RoomJID),
260 <<"set">>, [Changes])),
261 25 {ok, "Affiliation change request sent successfully"}.
262
263 do_send_message(#{user := SenderJID, room := RoomJID, children := Children, attrs := ExtraAttrs}) ->
264 21 SenderBare = jid:to_bare(SenderJID),
265 21 RoomBare = jid:to_bare(RoomJID),
266 21 Stanza = #xmlel{name = <<"message">>,
267 attrs = [{<<"type">>, <<"groupchat">>} | ExtraAttrs],
268 children = Children},
269 21 ejabberd_router:route(SenderBare, RoomBare, Stanza),
270 21 {ok, "Message sent successfully"}.
271
272 do_delete_room(#{room := RoomJID}) ->
273 9 case mod_muc_light:delete_room(jid:to_lus(RoomJID)) of
274 ok ->
275 5 ?ROOM_DELETED_SUCC_RESULT;
276 {error, not_exists} ->
277 4 ?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 21 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 26 ArchiveID = mod_mam_muc:archive_id_int(HostType, RoomJID),
287 26 Now = os:system_time(microsecond),
288 26 End = maybe_before(Before, Now),
289 26 RSM = #rsm_in{direction = before, id = undefined},
290 26 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 26 case mod_mam_muc:lookup_messages(HostType, maybe_caller_jid(CallerJID, Params)) of
304 {ok, {_, _, Messages}} ->
305 26 {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 27 case mod_muc_light_db_backend:get_info(HostType, jid:to_lus(RoomJID)) of
312 {ok, Config, AffUsers, _Version} ->
313 22 M#{aff_users => AffUsers, options => Config};
314 {error, not_exists} ->
315 5 ?ROOM_NOT_FOUND_RESULT
316 end.
317
318 return_info(#{room := RoomJID, aff_users := AffUsers, options := Config}) ->
319 20 {ok, make_room(jid:to_binary(RoomJID), Config, AffUsers)}.
320
321 do_get_room_aff(M = #{room := RoomJID, muc_host_type := HostType}) ->
322 10 case mod_muc_light_db_backend:get_aff_users(HostType, jid:to_lus(RoomJID)) of
323 {ok, AffUsers, _Version} ->
324 6 M#{aff_users => AffUsers};
325 {error, not_exists} ->
326 4 ?ROOM_NOT_FOUND_RESULT
327 end.
328
329 return_aff(#{aff_users := AffUsers}) ->
330 5 {ok, AffUsers}.
331
332 check_room(M = #{room := RoomJID, muc_host_type := HostType}) ->
333 12 case mod_muc_light_db_backend:room_exists(HostType, jid:to_lus(RoomJID)) of
334 true ->
335 9 M;
336 false ->
337 3 ?ROOM_NOT_FOUND_RESULT
338 end.
339
340 do_get_user_rooms(#{user := UserJID, user_host_type := HostType}) ->
341 7 MUCServer = mod_muc_light_utils:server_host_to_muc_host(HostType, UserJID#jid.lserver),
342 7 {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 18 MUCServer = mod_muc_light_utils:server_host_to_muc_host(HostType, UserJID#jid.lserver),
346 18 {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 12 MUCServer = mod_muc_light_utils:server_host_to_muc_host(HostType, UserJID#jid.lserver),
350 12 Q = query(?NS_MUC_LIGHT_BLOCKING, [blocking_item(I) || I <- Items]),
351 12 Iq = iq(jid:to_binary(UserJID), MUCServer, <<"set">>, [Q]),
352 12 ejabberd_router:route(UserJID, jid:from_binary(MUCServer), Iq),
353 12 {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 13 #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 142 #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 117 RoomUS = jid:to_lus(RoomJID),
372 117 UserUS = jid:to_lus(UserJID),
373 117 case mod_muc_light_db_backend:get_aff_users(HostType, RoomUS) of
374 {ok, Affs, _Version} ->
375 107 {ok, get_aff(UserUS, Affs)};
376 {error, not_exists} ->
377 10 {error, room_not_found}
378 end.
379
380 -spec get_aff(jid:simple_bare_jid(), aff_users()) -> aff().
381 get_aff(UserUS, Affs) ->
382 127 case lists:keyfind(UserUS, 1, Affs) of
383 106 {_, Aff} -> Aff;
384 21 false -> none
385 end.
386
387 make_room(JID, #config{ raw_config = Options}, AffUsers) ->
388 9 make_room(JID, Options, AffUsers);
389 make_room(JID, Options, AffUsers) when is_list(Options) ->
390 162 make_room(JID, maps:from_list(ensure_keys_are_binaries(Options)), AffUsers);
391 make_room(JID, Options, AffUsers) when is_map(Options) ->
392 162 #{jid => JID, aff_users => AffUsers, options => Options}.
393
394 ensure_keys_are_binaries([{K, _}|_] = Conf) when is_binary(K) ->
395 142 Conf;
396 ensure_keys_are_binaries(Conf) ->
397 20 [{atom_to_binary(K), V} || {K, V} <- Conf].
398
399 iq(To, From, Type, Children) ->
400 64 UUID = uuid:uuid_to_string(uuid:get_v4(), binary_standard),
401 64 #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 64 #xmlel{name = <<"query">>,
411 attrs = [{<<"xmlns">>, NS}],
412 children = Children
413 }.
414
415 affiliate(JID, Kind) when is_binary(JID), is_binary(Kind) ->
416 52 #xmlel{name = <<"user">>,
417 attrs = [{<<"affiliation">>, Kind}],
418 children = [ #xmlcdata{ content = JID } ]
419 }.
420
421 maybe_before(undefined, Now) ->
422 16 Now;
423 maybe_before(Timestamp, _) ->
424 10 Timestamp.
425
426 maybe_caller_jid(undefined, Params) ->
427 12 Params;
428 maybe_caller_jid(CallerJID, Params) ->
429 14 Params#{caller_jid => CallerJID}.
430
431 16 op_to_aff(add) -> <<"member">>;
432 9 op_to_aff(remove) -> <<"none">>.
433
434 fold({_, _} = Result, _) ->
435 446 Result;
436 fold(M, [Step | Rest]) when is_map(M) ->
437 1328 fold(Step(M), Rest).
Line Hits Source