./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 317 mongoose_commands:register(commands()).
44
45 stop() ->
46 317 mongoose_commands:unregister(commands()).
47
48 317 start(_, _) -> start().
49 317 stop(_) -> stop().
50
51 -spec supported_features() -> [atom()].
52 supported_features() ->
53 146 [dynamic_domains].
54
55 %%%
56 %%% mongoose commands
57 %%%
58
59 commands() ->
60 634 [
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 4 case mongoose_stanza_helper:parse_from_to(From, To) of
283 {ok, FromJID, ToJID} ->
284 2 Packet = mongoose_stanza_helper:build_message(From, To, Body),
285 2 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 3 case mongoose_domain_api:get_domain_host_type(From#jid.lserver) of
309 {ok, HostType} ->
310 3 Acc = mongoose_acc:new(#{location => ?LOCATION,
311 host_type => HostType,
312 lserver => From#jid.lserver,
313 element => Packet}),
314 3 Acc1 = mongoose_hooks:user_send_packet(Acc, From, To, Packet),
315 3 ejabberd_router:route(From, To, Acc1),
316 3 ok;
317 {error, not_found} ->
318
:-(
{error, unknown_domain}
319 end.
320
321 list_contacts(Caller) ->
322 31 case mod_roster_api:list_contacts(jid:from_binary(Caller)) of
323 {ok, Rosters} ->
324 31 [roster_info(mod_roster:item_to_map(R)) || R <- Rosters];
325 Error ->
326
:-(
skip_result_msg(Error)
327 end.
328
329 roster_info(M) ->
330 19 Jid = jid:to_binary(maps:get(jid, M)),
331 19 #{subscription := Sub, ask := Ask} = M,
332 19 #{jid => Jid, subscription => Sub, ask => Ask}.
333
334 add_contact(Caller, JabberID) ->
335 15 add_contact(Caller, JabberID, <<"">>, []).
336
337 add_contact(Caller, JabberID, Name) ->
338
:-(
add_contact(Caller, JabberID, Name, []).
339
340 add_contact(Caller, Other, Name, Groups) ->
341 15 case mongoose_stanza_helper:parse_from_to(Caller, Other) of
342 {ok, CallerJid, OtherJid} ->
343 13 Res = mod_roster_api:add_contact(CallerJid, OtherJid, Name, Groups),
344 13 skip_result_msg(Res);
345 E ->
346 2 E
347 end.
348
349 delete_contacts(Caller, ToDelete) ->
350 2 maybe_delete_contacts(Caller, ToDelete, []).
351
352 2 maybe_delete_contacts(_, [], NotDeleted) -> NotDeleted;
353 maybe_delete_contacts(Caller, [H | T], NotDeleted) ->
354 5 case delete_contact(Caller, H) of
355 ok ->
356 4 maybe_delete_contacts(Caller, T, NotDeleted);
357 _Error ->
358 1 maybe_delete_contacts(Caller, T, NotDeleted ++ [H])
359 end.
360
361 delete_contact(Caller, Other) ->
362 8 case mongoose_stanza_helper:parse_from_to(Caller, Other) of
363 {ok, CallerJID, OtherJID} ->
364 8 Res = mod_roster_api:delete_contact(CallerJID, OtherJID),
365 8 skip_result_msg(Res);
366 E ->
367
:-(
E
368 end.
369
370 registered_commands() ->
371 6 Items = collect_commands(),
372 6 sort_commands(Items).
373
374 sort_commands(Items) ->
375 6 WithKey = [{get_sorting_key(Item), Item} || Item <- Items],
376 6 Sorted = lists:keysort(1, WithKey),
377 6 [Item || {_Key, Item} <- Sorted].
378
379 get_sorting_key(Item) ->
380 162 maps:get(path, Item).
381
382 collect_commands() ->
383 6 [#{name => mongoose_commands:name(C),
384 category => mongoose_commands:category(C),
385 action => mongoose_commands:action(C),
386 method => mongoose_api_common:action_to_method(mongoose_commands:action(C)),
387 desc => mongoose_commands:desc(C),
388 args => format_args(mongoose_commands:args(C)),
389 path => mongoose_api_common:create_admin_url_path(C)
390 6 } || C <- mongoose_commands:list(admin)].
391
392 format_args(Args) ->
393 162 maps:from_list([{term_as_binary(Name), term_as_binary(rewrite_type(Type))}
394 162 || {Name, Type} <- Args]).
395
396 %% binary is useful internally, but could confuse a regular user
397 432 rewrite_type(binary) -> string;
398 6 rewrite_type(Type) -> Type.
399
400 term_as_binary(X) ->
401 876 iolist_to_binary(io_lib:format("~p", [X])).
402
403 get_recent_messages(Caller, Before, Limit) ->
404
:-(
get_recent_messages(Caller, undefined, Before, Limit).
405
406 get_recent_messages(Caller, With, Before, Limit) ->
407
:-(
Before2 = maybe_seconds_to_microseconds(Before),
408
:-(
Res = mongoose_stanza_api:lookup_recent_messages(Caller, With, Before2, Limit),
409
:-(
lists:map(fun row_to_map/1, Res).
410
411 maybe_seconds_to_microseconds(X) when is_number(X) ->
412
:-(
X * 1000000;
413 maybe_seconds_to_microseconds(X) ->
414
:-(
X.
415
416 -spec row_to_map(mod_mam:message_row()) -> map().
417 row_to_map(#{id := Id, jid := From, packet := Msg}) ->
418
:-(
Jbin = jid:to_binary(From),
419
:-(
{Msec, _} = mod_mam_utils:decode_compact_uuid(Id),
420
:-(
MsgId = case xml:get_tag_attr(<<"id">>, Msg) of
421
:-(
{value, MId} -> MId;
422
:-(
false -> <<"">>
423 end,
424
:-(
Body = exml_query:path(Msg, [{element, <<"body">>}, cdata]),
425
:-(
#{sender => Jbin, timestamp => round(Msec / 1000000), message_id => MsgId,
426 body => Body}.
427
428 subscription(Caller, Other, Action) ->
429 9 case decode_action(Action) of
430 error ->
431 1 {error, bad_request, <<"invalid action">>};
432 Act ->
433 8 case mongoose_stanza_helper:parse_from_to(Caller, Other) of
434 {ok, CallerJID, OtherJID} ->
435 6 Res = mod_roster_api:subscription(CallerJID, OtherJID, Act),
436 6 skip_result_msg(Res);
437 E ->
438 2 E
439 end
440 end.
441
442 5 decode_action(<<"subscribe">>) -> subscribe;
443 3 decode_action(<<"subscribed">>) -> subscribed;
444 1 decode_action(_) -> error.
445
446 set_subscription(Caller, Other, Action) ->
447 5 case mongoose_stanza_helper:parse_from_to(Caller, Other) of
448 {ok, CallerJID, OtherJID} ->
449 3 case decode_both_sub_action(Action) of
450 error ->
451 1 {error, bad_request, <<"invalid action">>};
452 ActionDecoded ->
453 2 Res = mod_roster_api:set_mutual_subscription(CallerJID, OtherJID,
454 ActionDecoded),
455 2 skip_result_msg(Res)
456 end;
457 E ->
458 2 E
459 end.
460
461 1 decode_both_sub_action(<<"connect">>) -> connect;
462 1 decode_both_sub_action(<<"disconnect">>) -> disconnect;
463 1 decode_both_sub_action(_) -> error.
464
465 28 skip_result_msg({ok, _Msg}) -> ok;
466 1 skip_result_msg({ErrCode, _Msg}) -> {error, ErrCode}.
Line Hits Source