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. |