./ct_report/coverage/mod_commands.COVER.html

1 -module(mod_commands).
2 -author('bartlomiej.gorny@erlang-solutions.com').
3
4 -behaviour(gen_mod).
5 -behaviour(mongoose_module_metrics).
6
7 -export([start/0, stop/0, supported_features/0,
8 start/2, stop/1,
9 register/3,
10 unregister/2,
11 registered_commands/0,
12 registered_users/1,
13 change_user_password/3,
14 list_sessions/1,
15 list_contacts/1,
16 add_contact/2,
17 add_contact/3,
18 add_contact/4,
19 delete_contacts/2,
20 delete_contact/2,
21 subscription/3,
22 set_subscription/3,
23 kick_session/3,
24 get_recent_messages/3,
25 get_recent_messages/4,
26 send_message/3,
27 send_stanza/1
28 ]).
29
30 -ignore_xref([add_contact/2, add_contact/3, add_contact/4, change_user_password/3,
31 delete_contact/2, delete_contacts/2, get_recent_messages/3,
32 get_recent_messages/4, kick_session/3, list_contacts/1,
33 list_sessions/1, register/3, registered_commands/0, registered_users/1,
34 send_message/3, send_stanza/1, set_subscription/3, start/0, stop/0,
35 subscription/3, unregister/2]).
36
37 -include("mongoose.hrl").
38 -include("jlib.hrl").
39 -include("mongoose_rsm.hrl").
40 -include("session.hrl").
41
42 start() ->
43 286 mongoose_commands:register(commands()).
44
45 stop() ->
46 286 mongoose_commands:unregister(commands()).
47
48 286 start(_, _) -> start().
49 286 stop(_) -> stop().
50
51 -spec supported_features() -> [atom()].
52 supported_features() ->
53 133 [dynamic_domains].
54
55 %%%
56 %%% mongoose commands
57 %%%
58
59 commands() ->
60 572 [
61 [
62 {name, list_methods},
63 {category, <<"commands">>},
64 {desc, <<"List commands">>},
65 {module, ?MODULE},
66 {function, registered_commands},
67 {action, read},
68 {args, []},
69 {result, []}
70 ],
71 [
72 {name, list_users},
73 {category, <<"users">>},
74 {desc, <<"List registered users on this host">>},
75 {module, ?MODULE},
76 {function, registered_users},
77 {action, read},
78 {args, [{host, binary}]},
79 {result, []}
80 ],
81 [
82 {name, register_user},
83 {category, <<"users">>},
84 {desc, <<"Register a user">>},
85 {module, ?MODULE},
86 {function, register},
87 {action, create},
88 {args, [{host, binary}, {username, binary}, {password, binary}]},
89 {result, {msg, binary}}
90 ],
91 [
92 {name, unregister_user},
93 {category, <<"users">>},
94 {desc, <<"UnRegister a user">>},
95 {module, ?MODULE},
96 {function, unregister},
97 {action, delete},
98 {args, [{host, binary}, {user, binary}]},
99 {result, {msg, binary}}
100 ],
101 [
102 {name, list_sessions},
103 {category, <<"sessions">>},
104 {desc, <<"Get session list">>},
105 {module, ?MODULE},
106 {function, list_sessions},
107 {action, read},
108 {args, [{host, binary}]},
109 {result, []}
110 ],
111 [
112 {name, kick_user},
113 {category, <<"sessions">>},
114 {desc, <<"Terminate user connection">>},
115 {module, ?MODULE},
116 {function, kick_session},
117 {action, delete},
118 {args, [{host, binary}, {user, binary}, {res, binary}]},
119 {result, {msg, binary}}
120 ],
121 [
122 {name, list_contacts},
123 {category, <<"contacts">>},
124 {desc, <<"Get roster">>},
125 {module, ?MODULE},
126 {function, list_contacts},
127 {action, read},
128 {security_policy, [user]},
129 {args, [{caller, binary}]},
130 {result, []}
131 ],
132 [
133 {name, add_contact},
134 {category, <<"contacts">>},
135 {desc, <<"Add a contact to roster">>},
136 {module, ?MODULE},
137 {function, add_contact},
138 {action, create},
139 {security_policy, [user]},
140 {args, [{caller, binary}, {jid, binary}]},
141 {result, ok}
142 ],
143 [
144 {name, subscription},
145 {category, <<"contacts">>},
146 {desc, <<"Send out a subscription request">>},
147 {module, ?MODULE},
148 {function, subscription},
149 {action, update},
150 {security_policy, [user]},
151 {identifiers, [caller, jid]},
152 % caller has to be in identifiers, otherwise it breaks admin rest api
153 {args, [{caller, binary}, {jid, binary}, {action, binary}]},
154 {result, ok}
155 ],
156 [
157 {name, set_subscription},
158 {category, <<"contacts">>},
159 {subcategory, <<"manage">>},
160 {desc, <<"Set / unset mutual subscription">>},
161 {module, ?MODULE},
162 {function, set_subscription},
163 {action, update},
164 {identifiers, [caller, jid]},
165 {args, [{caller, binary}, {jid, binary}, {action, binary}]},
166 {result, ok}
167 ],
168 [
169 {name, delete_contact},
170 {category, <<"contacts">>},
171 {desc, <<"Remove a contact from roster">>},
172 {module, ?MODULE},
173 {function, delete_contact},
174 {action, delete},
175 {security_policy, [user]},
176 {args, [{caller, binary}, {jid, binary}]},
177 {result, ok}
178 ],
179 [
180 {name, delete_contacts},
181 {category, <<"contacts">>},
182 {subcategory, <<"multiple">>},
183 {desc, <<"Remove provided contacts from roster">>},
184 {module, ?MODULE},
185 {function, delete_contacts},
186 {action, delete},
187 {security_policy, [user]},
188 {args, [{caller, binary}, {jids, [binary]}]},
189 {result, []}
190 ],
191 [
192 {name, send_message},
193 {category, <<"messages">>},
194 {desc, <<"Send chat message from to">>},
195 {module, ?MODULE},
196 {function, send_message},
197 {action, create},
198 {security_policy, [user]},
199 {args, [{caller, binary}, {to, binary}, {body, binary}]},
200 {result, ok}
201 ],
202 [
203 {name, send_stanza},
204 {category, <<"stanzas">>},
205 {desc, <<"Send an arbitrary stanza">>},
206 {module, ?MODULE},
207 {function, send_stanza},
208 {action, create},
209 {args, [{stanza, binary}]},
210 {result, ok}
211 ],
212 [
213 {name, get_last_messages_with_everybody},
214 {category, <<"messages">>},
215 {desc, <<"Get n last messages from archive, optionally before a certain date (unixtime)">>},
216 {module, ?MODULE},
217 {function, get_recent_messages},
218 {action, read},
219 {security_policy, [user]},
220 {args, [{caller, binary}]},
221 {optargs, [{before, integer, 0}, {limit, integer, 100}]},
222 {result, []}
223 ],
224 [
225 {name, get_last_messages},
226 {category, <<"messages">>},
227 {desc, <<"Get n last messages to/from given contact, with limit and date">>},
228 {module, ?MODULE},
229 {function, get_recent_messages},
230 {action, read},
231 {security_policy, [user]},
232 {args, [{caller, binary}, {with, binary}]},
233 {optargs, [{before, integer, 0}, {limit, integer, 100}]},
234 {result, []}
235 ],
236 [
237 {name, change_password},
238 {category, <<"users">>},
239 {desc, <<"Change user password">>},
240 {module, ?MODULE},
241 {function, change_user_password},
242 {action, update},
243 {security_policy, [user]},
244 {identifiers, [host, user]},
245 {args, [{host, binary}, {user, binary}, {newpass, binary}]},
246 {result, ok}
247 ]
248 ].
249
250 kick_session(Host, User, Resource) ->
251 2 case mongoose_session_api:kick_session(User, Host, Resource, <<"kicked">>) of
252 1 {ok, Msg} -> Msg;
253 1 {no_session, Msg} -> {error, not_found, Msg}
254 end.
255
256 list_sessions(Host) ->
257 3 mongoose_session_api:list_resources(Host).
258
259 registered_users(Host) ->
260 3 mongoose_account_api:list_users(Host).
261
262 register(Host, User, Password) ->
263 3 Res = mongoose_account_api:register_user(User, Host, Password),
264 3 format_account_result(Res).
265
266 unregister(Host, User) ->
267 2 Res = mongoose_account_api:unregister_user(User, Host),
268 2 format_account_result(Res).
269
270 change_user_password(Host, User, Password) ->
271 4 Res = mongoose_account_api:change_password(User, Host, Password),
272 4 format_account_result(Res).
273
274 4 format_account_result({ok, Msg}) -> iolist_to_binary(Msg);
275 1 format_account_result({empty_password, Msg}) -> {error, bad_request, Msg};
276 3 format_account_result({invalid_jid, Msg}) -> {error, bad_request, Msg};
277
:-(
format_account_result({not_allowed, Msg}) -> {error, denied, Msg};
278 1 format_account_result({exists, Msg}) -> {error, denied, Msg};
279
:-(
format_account_result({cannot_register, Msg}) -> {error, internal, Msg}.
280
281 send_message(From, To, Body) ->
282 6 case mongoose_stanza_helper:parse_from_to(From, To) of
283 {ok, FromJID, ToJID} ->
284 4 Packet = mongoose_stanza_helper:build_message(From, To, Body),
285 4 do_send_packet(FromJID, ToJID, Packet);
286 Error ->
287 2 Error
288 end.
289
290 send_stanza(BinStanza) ->
291 3 case exml:parse(BinStanza) of
292 {ok, Packet} ->
293 2 From = exml_query:attr(Packet, <<"from">>),
294 2 To = exml_query:attr(Packet, <<"to">>),
295 2 case mongoose_stanza_helper:parse_from_to(From, To) of
296 {ok, FromJID, ToJID} ->
297 1 do_send_packet(FromJID, ToJID, Packet);
298 {error, missing} ->
299 1 {error, bad_request, "both from and to are required"};
300 {error, type_error, E} ->
301
:-(
{error, type_error, E}
302 end;
303 {error, Reason} ->
304 1 {error, bad_request, io_lib:format("Malformed stanza: ~p", [Reason])}
305 end.
306
307 do_send_packet(From, To, Packet) ->
308 5 case mongoose_domain_api:get_domain_host_type(From#jid.lserver) of
309 {ok, HostType} ->
310 5 Acc = mongoose_acc:new(#{location => ?LOCATION,
311 host_type => HostType,
312 lserver => From#jid.lserver,
313 element => Packet}),
314 5 Acc1 = mongoose_hooks:user_send_packet(Acc, From, To, Packet),
315 5 ejabberd_router:route(From, To, Acc1),
316 5 ok;
317 {error, not_found} ->
318
:-(
{error, unknown_domain}
319 end.
320
321 list_contacts(Caller) ->
322 31 CallerJID = #jid{lserver = LServer} = jid:from_binary(Caller),
323 31 {ok, HostType} = mongoose_domain_api:get_domain_host_type(LServer),
324 31 Acc0 = mongoose_acc:new(#{ location => ?LOCATION,
325 host_type => HostType,
326 lserver => LServer,
327 element => undefined }),
328 31 Acc1 = mongoose_acc:set(roster, show_full_roster, true, Acc0),
329 31 Acc2 = mongoose_hooks:roster_get(Acc1, CallerJID),
330 31 Res = mongoose_acc:get(roster, items, Acc2),
331 31 [roster_info(mod_roster:item_to_map(I)) || I <- Res].
332
333 roster_info(M) ->
334 19 Jid = jid:to_binary(maps:get(jid, M)),
335 19 #{subscription := Sub, ask := Ask} = M,
336 19 #{jid => Jid, subscription => Sub, ask => Ask}.
337
338 add_contact(Caller, JabberID) ->
339 17 add_contact(Caller, JabberID, <<"">>, []).
340
341 add_contact(Caller, JabberID, Name) ->
342
:-(
add_contact(Caller, JabberID, Name, []).
343
344 add_contact(Caller, Other, Name, Groups) ->
345 17 case mongoose_stanza_helper:parse_from_to(Caller, Other) of
346 {ok, CallerJid = #jid{lserver = LServer}, OtherJid} ->
347 15 case mongoose_domain_api:get_domain_host_type(LServer) of
348 {ok, HostType} ->
349 15 mod_roster:set_roster_entry(HostType, CallerJid, OtherJid, Name, Groups);
350 {error, not_found} ->
351
:-(
{error, unknown_domain}
352 end;
353 E ->
354 2 E
355 end.
356
357 delete_contacts(Caller, ToDelete) ->
358 2 maybe_delete_contacts(Caller, ToDelete, []).
359
360 2 maybe_delete_contacts(_, [], NotDeleted) -> NotDeleted;
361 maybe_delete_contacts(Caller, [H | T], NotDeleted) ->
362 5 case delete_contact(Caller, H) of
363 ok ->
364 4 maybe_delete_contacts(Caller, T, NotDeleted);
365 {error, _Reason} ->
366 1 maybe_delete_contacts(Caller, T, NotDeleted ++ [H])
367 end.
368
369 delete_contact(Caller, Other) ->
370 10 case mongoose_stanza_helper:parse_from_to(Caller, Other) of
371 {ok, CallerJid = #jid{lserver = LServer}, OtherJid} ->
372 10 case mongoose_domain_api:get_domain_host_type(LServer) of
373 {ok, HostType} ->
374 10 mod_roster:remove_from_roster(HostType, CallerJid, OtherJid);
375 {error, not_found} ->
376
:-(
{error, unknown_domain}
377 end;
378 E ->
379
:-(
E
380 end.
381
382 registered_commands() ->
383 6 Items = collect_commands(),
384 6 sort_commands(Items).
385
386 sort_commands(Items) ->
387 6 WithKey = [{get_sorting_key(Item), Item} || Item <- Items],
388 6 Sorted = lists:keysort(1, WithKey),
389 6 [Item || {_Key, Item} <- Sorted].
390
391 get_sorting_key(Item) ->
392 162 maps:get(path, Item).
393
394 collect_commands() ->
395 6 [#{name => mongoose_commands:name(C),
396 category => mongoose_commands:category(C),
397 action => mongoose_commands:action(C),
398 method => mongoose_api_common:action_to_method(mongoose_commands:action(C)),
399 desc => mongoose_commands:desc(C),
400 args => format_args(mongoose_commands:args(C)),
401 path => mongoose_api_common:create_admin_url_path(C)
402 6 } || C <- mongoose_commands:list(admin)].
403
404 format_args(Args) ->
405 162 maps:from_list([{term_as_binary(Name), term_as_binary(rewrite_type(Type))}
406 162 || {Name, Type} <- Args]).
407
408 %% binary is useful internally, but could confuse a regular user
409 438 rewrite_type(binary) -> string;
410 6 rewrite_type(Type) -> Type.
411
412 term_as_binary(X) ->
413 888 iolist_to_binary(io_lib:format("~p", [X])).
414
415 get_recent_messages(Caller, Before, Limit) ->
416 1 get_recent_messages(Caller, undefined, Before, Limit).
417
418 get_recent_messages(Caller, With, Before, Limit) ->
419 7 Before2 = maybe_seconds_to_microseconds(Before),
420 7 Res = mongoose_stanza_api:lookup_recent_messages(Caller, With, Before2, Limit),
421 7 lists:map(fun row_to_map/1, Res).
422
423 maybe_seconds_to_microseconds(X) when is_number(X) ->
424 7 X * 1000000;
425 maybe_seconds_to_microseconds(X) ->
426
:-(
X.
427
428 -spec row_to_map(mod_mam:message_row()) -> map().
429 row_to_map(#{id := Id, jid := From, packet := Msg}) ->
430 21 Jbin = jid:to_binary(From),
431 21 {Msec, _} = mod_mam_utils:decode_compact_uuid(Id),
432 21 MsgId = case xml:get_tag_attr(<<"id">>, Msg) of
433 6 {value, MId} -> MId;
434 15 false -> <<"">>
435 end,
436 21 Body = exml_query:path(Msg, [{element, <<"body">>}, cdata]),
437 21 #{sender => Jbin, timestamp => round(Msec / 1000000), message_id => MsgId,
438 body => Body}.
439
440 subscription(Caller, Other, Action) ->
441 13 case decode_action(Action) of
442 error ->
443 1 {error, bad_request, <<"invalid action">>};
444 Act ->
445 12 case mongoose_stanza_helper:parse_from_to(Caller, Other) of
446 {ok, CallerJid, OtherJid} ->
447 10 run_subscription(Act, CallerJid, OtherJid);
448 E ->
449 2 E
450 end
451 end.
452
453 7 decode_action(<<"subscribe">>) -> subscribe;
454 5 decode_action(<<"subscribed">>) -> subscribed;
455 1 decode_action(_) -> error.
456
457 -spec run_subscription(subscribe | subscribed, jid:jid(), jid:jid()) -> ok.
458 run_subscription(Type, CallerJid, OtherJid) ->
459 10 StanzaType = atom_to_binary(Type, latin1),
460 10 El = #xmlel{name = <<"presence">>, attrs = [{<<"type">>, StanzaType}]},
461 10 LServer = CallerJid#jid.lserver,
462 10 {ok, HostType} = mongoose_domain_api:get_domain_host_type(LServer),
463 10 Acc1 = mongoose_acc:new(#{ location => ?LOCATION,
464 from_jid => CallerJid,
465 to_jid => OtherJid,
466 host_type => HostType,
467 lserver => LServer,
468 element => El }),
469 % set subscription to
470 10 Acc2 = mongoose_hooks:roster_out_subscription(Acc1, CallerJid, OtherJid, Type),
471 10 ejabberd_router:route(CallerJid, OtherJid, Acc2),
472 10 ok.
473
474
475 set_subscription(Caller, Other, Action) ->
476 5 case mongoose_stanza_helper:parse_from_to(Caller, Other) of
477 {ok, CallerJid, OtherJid} ->
478 3 case Action of
479 A when A == <<"connect">>; A == <<"disconnect">> ->
480 2 do_set_subscription(CallerJid, OtherJid, Action);
481 _ ->
482 1 {error, bad_request, <<"invalid action">>}
483 end;
484 E ->
485 2 E
486 end.
487
488 do_set_subscription(Caller, Other, <<"connect">>) ->
489 1 add_contact(Caller, Other),
490 1 add_contact(Other, Caller),
491 1 subscription(Caller, Other, <<"subscribe">>),
492 1 subscription(Other, Caller, <<"subscribe">>),
493 1 subscription(Other, Caller, <<"subscribed">>),
494 1 subscription(Caller, Other, <<"subscribed">>),
495 1 ok;
496 do_set_subscription(Caller, Other, <<"disconnect">>) ->
497 1 delete_contact(Caller, Other),
498 1 delete_contact(Other, Caller),
499 1 ok.
Line Hits Source