Merge pull request #4118 from badlop/api-version-1

Commands API version 1
This commit is contained in:
badlop 2024-01-05 13:10:06 +01:00 committed by GitHub
commit e26c547afc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 409 additions and 243 deletions

View File

@ -67,42 +67,24 @@
args_example = none :: none | [any()] | '_',
result_example = none :: any()}).
%% TODO Fix me: Type is not up to date
-type ejabberd_commands() :: #ejabberd_commands{name :: atom(),
tags :: [atom()],
desc :: string(),
longdesc :: string(),
version :: integer(),
module :: atom(),
function :: atom(),
args :: [aterm()],
policy :: open | restricted | admin | user,
access :: [{atom(),atom(),atom()}|atom()],
result :: rterm()}.
-type ejabberd_commands() :: #ejabberd_commands{name :: atom(),
tags :: [atom()],
desc :: string(),
longdesc :: string(),
version :: integer(),
note :: string(),
weight :: integer(),
module :: atom(),
function :: atom(),
args :: [aterm()],
policy :: open | restricted | admin | user,
access :: [{atom(),atom(),atom()}|atom()],
definer :: atom(),
result :: rterm(),
args_rename :: [{atom(),atom()}],
args_desc :: none | [string()] | '_',
result_desc :: none | string() | '_',
args_example :: none | [any()] | '_',
result_example :: any()
}.
%% @type ejabberd_commands() = #ejabberd_commands{
%% name = atom(),
%% tags = [atom()],
%% desc = string(),
%% longdesc = string(),
%% module = atom(),
%% function = atom(),
%% args = [aterm()],
%% result = rterm()
%% }.
%% desc: Description of the command
%% args: Describe the accepted arguments.
%% This way the function that calls the command can format the
%% arguments before calling.
%% @type atype() = integer | string | {tuple, [aterm()]} | {list, aterm()}.
%% Allowed types for arguments are integer, string, tuple and list.
%% @type rtype() = integer | string | atom | {tuple, [rterm()]} | {list, rterm()} | rescode | restuple.
%% A rtype is either an atom or a tuple with two elements.
%% @type aterm() = {Name::atom(), Type::atype()}.
%% An argument term is a tuple with the term name and the term type.
%% @type rterm() = {Name::atom(), Type::rtype()}.
%% A result term is a tuple with the term name and the term type.

View File

@ -450,11 +450,11 @@ delete_obsolete_data() ->
%%%===================================================================
get_commands_spec() ->
[#ejabberd_commands{name = request_certificate, tags = [acme],
desc = "Requests certificates for all or the specified "
"domains: all | domain1,domain2,...",
desc = "Requests certificates for all or some domains",
longdesc = "Domains can be `all`, or a list of domains separared with comma characters",
module = ?MODULE, function = request_certificate,
args_desc = ["Domains for which to acquire a certificate"],
args_example = ["all | domain.tld,conference.domain.tld,..."],
args_example = ["example.com,domain.tld,conference.domain.tld"],
args = [{domains, string}],
result = {res, restuple}},
#ejabberd_commands{name = list_certificates, tags = [acme],

View File

@ -129,7 +129,7 @@ get_commands_spec() ->
desc = "Reopen the log files after being renamed",
longdesc = "This can be useful when an external tool is "
"used for log rotation. See "
"https://docs.ejabberd.im/admin/guide/troubleshooting/#log-files",
"[Log Files](https://docs.ejabberd.im/admin/guide/troubleshooting/#log-files).",
policy = admin,
module = ?MODULE, function = reopen_log,
args = [], result = {res, rescode}},
@ -157,9 +157,10 @@ get_commands_spec() ->
result = {levelatom, atom}},
#ejabberd_commands{name = set_loglevel, tags = [logs],
desc = "Set the loglevel",
longdesc = "Possible loglevels: `none`, `emergency`, `alert`, `critical`,
`error`, `warning`, `notice`, `info`, `debug`.",
module = ?MODULE, function = set_loglevel,
args_desc = ["Desired logging level: none | emergency | alert | critical "
"| error | warning | notice | info | debug"],
args_desc = ["Desired logging level"],
args_example = ["debug"],
args = [{loglevel, string}],
result = {res, rescode}},
@ -171,7 +172,8 @@ get_commands_spec() ->
result_example = ["mod_configure", "mod_vcard"],
result = {modules, {list, {module, string}}}},
#ejabberd_commands{name = update, tags = [server],
desc = "Update the given module, or use the keyword: all",
desc = "Update the given module",
longdesc = "To update all the possible modules, use `all`.",
module = ?MODULE, function = update,
args_example = ["mod_vcard"],
args = [{module, string}],
@ -373,7 +375,7 @@ get_commands_spec() ->
result = {res, rescode}},
#ejabberd_commands{name = set_master, tags = [cluster],
desc = "Set master node of the clustered Mnesia tables",
longdesc = "If you provide as nodename `self`, this "
longdesc = "If `nodename` is set to `self`, then this "
"node will be set as its own master.",
module = ?MODULE, function = set_master,
args_desc = ["Name of the erlang node that will be considered master of this node"],

View File

@ -86,7 +86,8 @@ get_commands_spec() ->
args_desc = ["Path to file where generated "
"documentation should be stored",
"Regexp matching names of commands or modules "
"that will be included inside generated document",
"that will be included inside generated document, "
"or `runtime` to get commands registered at runtime",
"Comma separated list of languages (chosen from `java`, `perl`, `xmlrpc`, `json`) "
"that will have example invocation include in markdown document"],
result_desc = "0 if command failed, 1 when succeeded",
@ -147,13 +148,25 @@ register_commands(Definer, Commands) ->
lists:foreach(
fun(Command) ->
%% XXX check if command exists
mnesia:dirty_write(Command#ejabberd_commands{definer = Definer})
mnesia:dirty_write(register_command_prepare(Command, Definer))
%% ?DEBUG("This command is already defined:~n~p", [Command])
end,
Commands),
ejabberd_access_permissions:invalidate(),
ok.
register_command_prepare(Command, Definer) ->
Tags1 = Command#ejabberd_commands.tags,
Tags2 = case Command#ejabberd_commands.version of
0 -> Tags1;
Version -> Tags1 ++ [list_to_atom("v"++integer_to_list(Version))]
end,
Command#ejabberd_commands{definer = Definer, tags = Tags2}.
-spec unregister_commands([ejabberd_commands()]) -> ok.
unregister_commands(Commands) ->

View File

@ -386,7 +386,7 @@ gen_doc(#ejabberd_commands{name=Name, tags=Tags, desc=Desc, longdesc=LongDesc,
ResultText = case Result of
{res,rescode} ->
[?TAG(dl, [gen_param(res, integer,
"Status code (0 on success, 1 otherwise)",
"Status code (`0` on success, `1` otherwise)",
HTMLOutput)])];
{res,restuple} ->
[?TAG(dl, [gen_param(res, string,
@ -400,9 +400,9 @@ gen_doc(#ejabberd_commands{name=Name, tags=Tags, desc=Desc, longdesc=LongDesc,
[?TAG(dl, [gen_param(RName, Type, ResultDesc, HTMLOutput)])]
end
end,
TagsText = [?RAW("*`"++atom_to_list(Tag)++"`* ") || Tag <- Tags],
TagsText = ?RAW(string:join(["*`"++atom_to_list(Tag)++"`*" || Tag <- Tags], ", ")),
IsDefinerMod = case Definer of
unknown -> true;
unknown -> false;
_ -> lists:member(gen_mod, proplists:get_value(behaviour, Definer:module_info(attributes)))
end,
ModuleText = case IsDefinerMod of
@ -477,8 +477,16 @@ maybe_add_policy_arguments(#ejabberd_commands{args=Args1, policy=user}=Cmd) ->
maybe_add_policy_arguments(Cmd) ->
Cmd.
generate_md_output(File, <<"runtime">>, Languages) ->
Cmds = lists:map(fun({N, _, _}) ->
ejabberd_commands:get_command_definition(N)
end, ejabberd_commands:list_commands()),
generate_md_output(File, <<".">>, Languages, Cmds);
generate_md_output(File, RegExp, Languages) ->
Cmds = find_commands_definitions(),
generate_md_output(File, RegExp, Languages, Cmds).
generate_md_output(File, RegExp, Languages, Cmds) ->
{ok, RE} = re:compile(RegExp),
Cmds2 = lists:filter(fun(#ejabberd_commands{name=Name, module=Module}) ->
re:run(atom_to_list(Name), RE, [{capture, none}]) == match orelse

View File

@ -23,8 +23,6 @@
%%%
%%%----------------------------------------------------------------------
%%% Does not support commands that have arguments with ctypes: list, tuple
-module(ejabberd_ctl).
-behaviour(gen_server).
@ -335,14 +333,14 @@ call_command([CmdString | Args], Auth, _AccessCommands, Version) ->
ArgsFormatted,
CI2,
Version),
format_result_preliminary(Result, ResultFormat);
format_result_preliminary(Result, ResultFormat, Version);
{'EXIT', {function_clause,[{lists,zip,[A1,A2|_], _} | _]}} ->
{NumCompa, TextCompa} =
case {length(A1), length(A2)} of
{L1, L2} when L1 < L2 -> {L2-L1, "less argument"};
{L1, L2} when L1 > L2 -> {L1-L2, "more argument"}
end,
process(["help" | [CmdString]]),
process(["help" | [CmdString]], Version),
{io_lib:format("Error: the command '~ts' requires ~p ~ts.",
[CmdString, NumCompa, TextCompa]),
wrong_command_arguments}
@ -372,6 +370,13 @@ format_arg(Arg, string) ->
NumChars = integer_to_list(length(Arg)),
Parse = "~" ++ NumChars ++ "c",
format_arg2(Arg, Parse);
format_arg(Arg, {list, {_ArgName, ArgFormat}}) ->
[format_arg(Element, ArgFormat) || Element <- string:tokens(Arg, ",")];
format_arg(Arg, {list, ArgFormat}) ->
[format_arg(Element, ArgFormat) || Element <- string:tokens(Arg, ",")];
format_arg(Arg, {tuple, Elements}) ->
Args = string:tokens(Arg, ":"),
list_to_tuple(format_args(Args, Elements));
format_arg(Arg, Format) ->
S = unicode:characters_to_binary(Arg, utf8),
JSON = jiffy:decode(S),
@ -385,67 +390,71 @@ format_arg2(Arg, Parse)->
%% Format result
%%-----------------------------
format_result_preliminary(Result, {A, {list, B}}) ->
format_result(Result, {A, {top_result_list, B}});
format_result_preliminary(Result, ResultFormat) ->
format_result(Result, ResultFormat).
format_result_preliminary(Result, {A, {list, B}}, Version) ->
format_result(Result, {A, {top_result_list, B}}, Version);
format_result_preliminary(Result, ResultFormat, Version) ->
format_result(Result, ResultFormat, Version).
format_result({error, ErrorAtom}, _) ->
format_result({error, ErrorAtom}, _, _Version) ->
{io_lib:format("Error: ~p", [ErrorAtom]), make_status(error)};
%% An error should always be allowed to return extended error to help with API.
%% Extended error is of the form:
%% {error, type :: atom(), code :: int(), Desc :: string()}
format_result({error, ErrorAtom, Code, Msg}, _) ->
format_result({error, ErrorAtom, Code, Msg}, _, _Version) ->
{io_lib:format("Error: ~p: ~s", [ErrorAtom, Msg]), make_status(Code)};
format_result(Atom, {_Name, atom}) ->
format_result(Atom, {_Name, atom}, _Version) ->
io_lib:format("~p", [Atom]);
format_result(Int, {_Name, integer}) ->
format_result(Int, {_Name, integer}, _Version) ->
io_lib:format("~p", [Int]);
format_result([A|_]=String, {_Name, string}) when is_list(String) and is_integer(A) ->
format_result([A|_]=String, {_Name, string}, _Version) when is_list(String) and is_integer(A) ->
io_lib:format("~ts", [String]);
format_result(Binary, {_Name, string}) when is_binary(Binary) ->
format_result(Binary, {_Name, string}, _Version) when is_binary(Binary) ->
io_lib:format("~ts", [binary_to_list(Binary)]);
format_result(Atom, {_Name, string}) when is_atom(Atom) ->
format_result(Atom, {_Name, string}, _Version) when is_atom(Atom) ->
io_lib:format("~ts", [atom_to_list(Atom)]);
format_result(Integer, {_Name, string}) when is_integer(Integer) ->
format_result(Integer, {_Name, string}, _Version) when is_integer(Integer) ->
io_lib:format("~ts", [integer_to_list(Integer)]);
format_result(Other, {_Name, string}) ->
format_result(Other, {_Name, string}, _Version) ->
io_lib:format("~p", [Other]);
format_result(Code, {_Name, rescode}) ->
format_result(Code, {_Name, rescode}, _Version) ->
make_status(Code);
format_result({Code, Text}, {_Name, restuple}) ->
format_result({Code, Text}, {_Name, restuple}, _Version) ->
{io_lib:format("~ts", [Text]), make_status(Code)};
format_result([], {_Name, {top_result_list, _ElementsDef}}) ->
format_result([], {_Name, {top_result_list, _ElementsDef}}, _Version) ->
"";
format_result([FirstElement | Elements], {_Name, {top_result_list, ElementsDef}}) ->
[format_result(FirstElement, ElementsDef) |
format_result([FirstElement | Elements], {_Name, {top_result_list, ElementsDef}}, Version) ->
[format_result(FirstElement, ElementsDef, Version) |
lists:map(
fun(Element) ->
["\n" | format_result(Element, ElementsDef)]
["\n" | format_result(Element, ElementsDef, Version)]
end,
Elements)];
%% The result is a list of something: [something()]
format_result([], {_Name, {list, _ElementsDef}}) ->
format_result([], {_Name, {list, _ElementsDef}}, _Version) ->
"";
format_result([FirstElement | Elements], {_Name, {list, ElementsDef}}) ->
format_result([FirstElement | Elements], {_Name, {list, ElementsDef}}, Version) ->
Separator = case Version of
0 -> ";";
_ -> ","
end,
%% Start formatting the first element
[format_result(FirstElement, ElementsDef) |
[format_result(FirstElement, ElementsDef, Version) |
%% If there are more elements, put always first a newline character
lists:map(
fun(Element) ->
[";" | format_result(Element, ElementsDef)]
[Separator | format_result(Element, ElementsDef, Version)]
end,
Elements)];
@ -453,17 +462,17 @@ format_result([FirstElement | Elements], {_Name, {list, ElementsDef}}) ->
%% NOTE: the elements in the tuple are separated with tabular characters,
%% if a string is empty, it will be difficult to notice in the shell,
%% maybe a different separation character should be used, like ;;?
format_result(ElementsTuple, {_Name, {tuple, ElementsDef}}) ->
format_result(ElementsTuple, {_Name, {tuple, ElementsDef}}, Version) ->
ElementsList = tuple_to_list(ElementsTuple),
[{FirstE, FirstD} | ElementsAndDef] = lists:zip(ElementsList, ElementsDef),
[format_result(FirstE, FirstD) |
[format_result(FirstE, FirstD, Version) |
lists:map(
fun({Element, ElementDef}) ->
["\t" | format_result(Element, ElementDef)]
["\t" | format_result(Element, ElementDef, Version)]
end,
ElementsAndDef)];
format_result(404, {_Name, _}) ->
format_result(404, {_Name, _}, _Version) ->
make_status(not_found).
make_status(ok) -> ?STATUS_SUCCESS;
@ -491,19 +500,24 @@ get_list_commands(Version) ->
tuple_command_help({Name, _Args, Desc}) ->
{Args, _, _} = ejabberd_commands:get_command_format(Name, admin),
Arguments = [atom_to_list(ArgN) || {ArgN, _ArgF} <- Args],
Prepend = case is_supported_args(Args) of
true -> "";
false -> "*"
end,
CallString = atom_to_list(Name),
{CallString, Arguments, Prepend ++ Desc}.
{CallString, Arguments, Desc}.
is_supported_args(Args) ->
lists:all(
fun({_Name, Format}) ->
(Format == integer)
or (Format == string)
or (Format == binary)
has_tuple_args(Args) ->
lists:any(
fun({_Name, tuple}) -> true;
({_Name, {tuple, _}}) -> true;
({_Name, {list, SubArg}}) ->
has_tuple_args([SubArg]);
(_) -> false
end,
Args).
has_list_args(Args) ->
lists:any(
fun({_Name, list}) -> true;
({_Name, {list, _}}) -> true;
(_) -> false
end,
Args).
@ -768,12 +782,13 @@ print_usage_help(MaxC, ShCode) ->
" ejabberdctl ", ?C("help"), " ", ?C("register"), "\n",
" ejabberdctl ", ?C("help"), " ", ?C("regist*"), "\n",
"\n",
"Please note that 'ejabberdctl' shows all ejabberd commands,\n",
"even those that cannot be used in the shell with ejabberdctl.\n",
"Those commands can be identified because their description starts with: *\n",
"Some command arguments are lists or tuples, like add_rosteritem and create_room_with_opts.\n",
"Separate the elements in a list with the , character.\n",
"Separate the elements in a tuple with the : character.\n",
"\n",
"Some commands return lists, like get_roster and get_user_subscriptions.\n",
"In those commands, the elements in the list are separated with: ;\n"],
"Some commands results are lists or tuples, like get_roster and get_user_subscriptions.\n",
"The elements in a list are separated with a , character.\n",
"The elements in a tuple are separated with a tabular character.\n"],
ArgsDef = [],
C = #ejabberd_commands{
name = help,
@ -893,9 +908,13 @@ print_usage_command2(Cmd, C, MaxC, ShCode) ->
_ -> ["", prepare_description(0, MaxC, LongDesc), "\n\n"]
end,
NoteEjabberdctl = case is_supported_args(ArgsDef) of
true -> "";
false -> [" ", ?B("Note:"), " This command cannot be executed using ejabberdctl. Try ejabberd_xmlrpc.\n\n"]
NoteEjabberdctlList = case has_list_args(ArgsDef) of
true -> [" ", ?B("Note:"), " In a list argument, separate the elements using the , character for example: one,two,three\n\n"];
false -> ""
end,
NoteEjabberdctlTuple = case has_tuple_args(ArgsDef) of
true -> [" ", ?B("Note:"), " In a tuple argument, separate the elements using the : character for example: members_only:true\n\n"];
false -> ""
end,
case Cmd of
@ -903,7 +922,7 @@ print_usage_command2(Cmd, C, MaxC, ShCode) ->
_ -> print([NameFmt, "\n", ArgsFmt, "\n", ReturnsFmt,
"\n\n", XmlrpcFmt, TagsFmt, "\n\n", ModuleFmt, DescFmt, "\n\n"], [])
end,
print([LongDescFmt, NoteEjabberdctl], []).
print([LongDescFmt, NoteEjabberdctlList, NoteEjabberdctlTuple], []).
format_usage_ctype(Type, _Indentation)
when (Type==atom) or (Type==integer) or (Type==string) or (Type==binary) or (Type==rescode) or (Type==restuple)->

View File

@ -81,7 +81,7 @@
get_commands_spec() ->
[
#ejabberd_commands{name = oauth_issue_token, tags = [oauth],
desc = "Issue an oauth token for the given jid",
desc = "Issue an [OAuth](https://docs.ejabberd.im/developer/ejabberd-api/oauth/) token for the given jid",
module = ?MODULE, function = oauth_issue_token,
args = [{jid, string},{ttl, integer}, {scopes, string}],
policy = restricted,
@ -91,16 +91,28 @@ get_commands_spec() ->
"List of scopes to allow, separated by ';'"],
result = {result, {tuple, [{token, string}, {scopes, string}, {expires_in, string}]}}
},
#ejabberd_commands{name = oauth_issue_token, tags = [oauth],
desc = "Issue an [OAuth](https://docs.ejabberd.im/developer/ejabberd-api/oauth/) token for the given jid",
module = ?MODULE, function = oauth_issue_token,
version = 1,
args = [{jid, string}, {ttl, integer}, {scopes, {list, {scope, binary}}}],
policy = restricted,
args_example = ["user@server.com", 3600, ["connected_users_number", "muc_online_rooms"]],
args_desc = ["Jid for which issue token",
"Time to live of generated token in seconds",
"List of scopes to allow"],
result = {result, {tuple, [{token, string}, {scopes, {list, {scope, string}}}, {expires_in, string}]}}
},
#ejabberd_commands{name = oauth_list_tokens, tags = [oauth],
desc = "List oauth tokens, user, scope, and seconds to expire (only Mnesia)",
longdesc = "List oauth tokens, their user and scope, and how many seconds remain until expirity",
desc = "List [OAuth](https://docs.ejabberd.im/developer/ejabberd-api/oauth/) tokens, user, scope, and seconds to expire (only Mnesia)",
longdesc = "List OAuth tokens, their user and scope, and how many seconds remain until expirity",
module = ?MODULE, function = oauth_list_tokens,
args = [],
policy = restricted,
result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}}
},
#ejabberd_commands{name = oauth_revoke_token, tags = [oauth],
desc = "Revoke authorization for a token",
desc = "Revoke authorization for an [OAuth](https://docs.ejabberd.im/developer/ejabberd-api/oauth/) token",
note = "changed in 22.05",
module = ?MODULE, function = oauth_revoke_token,
args = [{token, binary}],
@ -109,7 +121,7 @@ get_commands_spec() ->
result_desc = "Result code"
},
#ejabberd_commands{name = oauth_add_client_password, tags = [oauth],
desc = "Add OAUTH client_id with password grant type",
desc = "Add [OAuth](https://docs.ejabberd.im/developer/ejabberd-api/oauth/) client_id with password grant type",
module = ?MODULE, function = oauth_add_client_password,
args = [{client_id, binary},
{client_name, binary},
@ -118,7 +130,7 @@ get_commands_spec() ->
result = {res, restuple}
},
#ejabberd_commands{name = oauth_add_client_implicit, tags = [oauth],
desc = "Add OAUTH client_id with implicit grant type",
desc = "Add [OAuth](https://docs.ejabberd.im/developer/ejabberd-api/oauth/) client_id with implicit grant type",
module = ?MODULE, function = oauth_add_client_implicit,
args = [{client_id, binary},
{client_name, binary},
@ -127,7 +139,7 @@ get_commands_spec() ->
result = {res, restuple}
},
#ejabberd_commands{name = oauth_remove_client, tags = [oauth],
desc = "Remove OAUTH client_id",
desc = "Remove [OAuth](https://docs.ejabberd.im/developer/ejabberd-api/oauth/) client_id",
module = ?MODULE, function = oauth_remove_client,
args = [{client_id, binary}],
policy = restricted,
@ -135,8 +147,10 @@ get_commands_spec() ->
}
].
oauth_issue_token(Jid, TTLSeconds, ScopesString) ->
oauth_issue_token(Jid, TTLSeconds, [Head|_] = ScopesString) when is_integer(Head) ->
Scopes = [list_to_binary(Scope) || Scope <- string:tokens(ScopesString, ";")],
oauth_issue_token(Jid, TTLSeconds, Scopes);
oauth_issue_token(Jid, TTLSeconds, Scopes) ->
try jid:decode(list_to_binary(Jid)) of
#jid{luser =Username, lserver = Server} ->
Ctx1 = #oauth_ctx{password = admin_generated},

View File

@ -238,7 +238,7 @@ do_command(Auth, Command, AttrL, ArgsF, ArgsR,
ArgsFormatted = format_args(rename_old_args(AttrL, ArgsR), ArgsF),
Result = ejabberd_commands:execute_command2(Command, ArgsFormatted, Auth),
ResultFormatted = format_result(Result, ResultF),
{command_result, ResultFormatted}.
{command_result, {struct, [ResultFormatted]}}.
rename_old_args(Args, []) ->
Args;
@ -291,6 +291,14 @@ format_args(Args, ArgsFormat) ->
L when is_list(L) -> exit({additional_unused_args, L})
end.
format_arg({array, Elements},
{list, {_ElementDefName, ElementDefFormat}})
when is_list(Elements) ->
lists:map(fun (ElementValue) ->
format_arg(ElementValue, ElementDefFormat)
end,
Elements);
format_arg({array, Elements},
{list, {ElementDefName, ElementDefFormat}})
when is_list(Elements) ->
@ -307,11 +315,18 @@ format_arg({array, [{struct, Elements}]},
format_arg(ElementValue, ElementDefFormat)
end,
Elements);
%% Old ejabberd 23.10
format_arg({array, [{struct, Elements}]},
{tuple, ElementsDef})
when is_list(Elements) ->
FormattedList = format_args(Elements, ElementsDef),
list_to_tuple(FormattedList);
%% New ejabberd 24.xx
format_arg({struct, Elements},
{tuple, ElementsDef})
when is_list(Elements) ->
FormattedList = format_args(Elements, ElementsDef),
list_to_tuple(FormattedList);
format_arg({array, Elements}, {list, ElementsDef})
when is_list(Elements) and is_atom(ElementsDef) ->
[format_arg(Element, ElementsDef)
@ -336,6 +351,10 @@ process_unicode_codepoints(Str) ->
%% Result
%% -----------------------------
format_result(Code, {Name, rescode}) ->
{Name, make_status(Code)};
format_result({_Code, Text}, {_Name, restuple}) ->
{text, io_lib:format("~s", [Text])};
format_result({error, Error}, _) when is_list(Error) ->
throw({error, lists:flatten(Error)});
format_result({error, Error}, _) ->
@ -346,45 +365,36 @@ format_result({error, _Type, _Code, Error}, _) ->
throw({error, Error});
format_result(String, string) -> lists:flatten(String);
format_result(Atom, {Name, atom}) ->
{struct,
[{Name, iolist_to_binary(atom_to_list(Atom))}]};
{Name, iolist_to_binary(atom_to_list(Atom))};
format_result(Int, {Name, integer}) ->
{struct, [{Name, Int}]};
{Name, Int};
format_result([A|_]=String, {Name, string}) when is_list(String) and is_integer(A) ->
{struct, [{Name, lists:flatten(String)}]};
{Name, lists:flatten(String)};
format_result(Binary, {Name, string}) when is_binary(Binary) ->
{struct, [{Name, binary_to_list(Binary)}]};
{Name, binary_to_list(Binary)};
format_result(Atom, {Name, string}) when is_atom(Atom) ->
{struct, [{Name, atom_to_list(Atom)}]};
{Name, atom_to_list(Atom)};
format_result(Integer, {Name, string}) when is_integer(Integer) ->
{struct, [{Name, integer_to_list(Integer)}]};
{Name, integer_to_list(Integer)};
format_result(Other, {Name, string}) ->
{struct, [{Name, io_lib:format("~p", [Other])}]};
{Name, io_lib:format("~p", [Other])};
format_result(String, {Name, binary}) when is_list(String) ->
{struct, [{Name, lists:flatten(String)}]};
{Name, lists:flatten(String)};
format_result(Binary, {Name, binary}) when is_binary(Binary) ->
{struct, [{Name, binary_to_list(Binary)}]};
format_result(Code, {Name, rescode}) ->
{struct, [{Name, make_status(Code)}]};
format_result({Code, Text}, {Name, restuple}) ->
{struct,
[{Name, make_status(Code)},
{text, io_lib:format("~s", [Text])}]};
format_result(Elements, {Name, {list, ElementsDef}}) ->
FormattedList = lists:map(fun (Element) ->
format_result(Element, ElementsDef)
end,
Elements),
{struct, [{Name, {array, FormattedList}}]};
format_result(ElementsTuple,
{Name, {tuple, ElementsDef}}) ->
ElementsList = tuple_to_list(ElementsTuple),
ElementsAndDef = lists:zip(ElementsList, ElementsDef),
FormattedList = lists:map(fun ({Element, ElementDef}) ->
format_result(Element, ElementDef)
end,
ElementsAndDef),
{struct, [{Name, {array, FormattedList}}]};
{Name, binary_to_list(Binary)};
format_result(Els, {Name, {list, Def}}) ->
FormattedList = [element(2, format_result(El, Def)) || El <- Els],
{Name, {array, FormattedList}};
format_result(Tuple,
{Name, {tuple, Def}}) ->
Els = lists:zip(tuple_to_list(Tuple), Def),
FormattedList = [format_result(El, ElDef) || {El, ElDef} <- Els],
{Name, {struct, FormattedList}};
format_result(404, {Name, _}) ->
{struct, [{Name, make_status(not_found)}]}.

View File

@ -113,14 +113,14 @@ depends(_Host, _Opts) ->
%%%
get_commands_spec() ->
Vcard1FieldsString = "Some vcard field names in get/set_vcard are:\n\n"
Vcard1FieldsString = "Some vcard field names in `get`/`set_vcard` are:\n\n"
"* FN - Full Name\n"
"* NICKNAME - Nickname\n"
"* BDAY - Birthday\n"
"* TITLE - Work: Position\n"
"* ROLE - Work: Role\n",
Vcard2FieldsString = "Some vcard field names and subnames in get/set_vcard2 are:\n\n"
Vcard2FieldsString = "Some vcard field names and subnames in `get`/`set_vcard2` are:\n\n"
"* N FAMILY - Family name\n"
"* N GIVEN - Given name\n"
"* N MIDDLE - Middle name\n"
@ -134,8 +134,8 @@ get_commands_spec() ->
"* ORG ORGNAME - Work: Company\n"
"* ORG ORGUNIT - Work: Department\n",
VcardXEP = "For a full list of vCard fields check XEP-0054: vcard-temp at "
"https://xmpp.org/extensions/xep-0054.html",
VcardXEP = "For a full list of vCard fields check [XEP-0054: vcard-temp]"
"(https://xmpp.org/extensions/xep-0054.html)",
[
#ejabberd_commands{name = compile, tags = [erlang],
@ -145,8 +145,7 @@ get_commands_spec() ->
args_example = ["/home/me/srcs/ejabberd/mod_example.erl"],
args_desc = ["Filename of erlang source file to compile"],
result = {res, rescode},
result_example = ok,
result_desc = "Status code: 0 on success, 1 otherwise"},
result_example = ok},
#ejabberd_commands{name = get_cookie, tags = [erlang],
desc = "Get the Erlang cookie of this node",
module = ?MODULE, function = get_cookie,
@ -163,9 +162,9 @@ get_commands_spec() ->
result = {res, integer},
result_example = 0,
result_desc = "Returns integer code:\n"
" - 0: code reloaded, module restarted\n"
" - 1: error: module not loaded\n"
" - 2: code not reloaded, but module restarted"},
" - `0`: code reloaded, module restarted\n"
" - `1`: error: module not loaded\n"
" - `2`: code not reloaded, but module restarted"},
#ejabberd_commands{name = delete_old_users, tags = [accounts, purge],
desc = "Delete users that didn't log in last days, or that never logged",
longdesc = "To protect admin accounts, configure this for example:\n"
@ -206,8 +205,7 @@ get_commands_spec() ->
args_example = [<<"peter">>, <<"myserver.com">>],
args_desc = ["User name to check", "Server to check"],
result = {res, rescode},
result_example = ok,
result_desc = "Status code: 0 on success, 1 otherwise"},
result_example = ok},
#ejabberd_commands{name = check_password, tags = [accounts],
desc = "Check if a password is correct",
module = ?MODULE, function = check_password,
@ -215,8 +213,7 @@ get_commands_spec() ->
args_example = [<<"peter">>, <<"myserver.com">>, <<"secret">>],
args_desc = ["User name to check", "Server to check", "Password to check"],
result = {res, rescode},
result_example = ok,
result_desc = "Status code: 0 on success, 1 otherwise"},
result_example = ok},
#ejabberd_commands{name = check_password_hash, tags = [accounts],
desc = "Check if the password hash is correct",
longdesc = "Allows hash methods from the Erlang/OTP "
@ -229,8 +226,7 @@ get_commands_spec() ->
args_desc = ["User name to check", "Server to check",
"Password's hash value", "Name of hash method"],
result = {res, rescode},
result_example = ok,
result_desc = "Status code: 0 on success, 1 otherwise"},
result_example = ok},
#ejabberd_commands{name = change_password, tags = [accounts],
desc = "Change the password of an account",
module = ?MODULE, function = set_password,
@ -239,8 +235,7 @@ get_commands_spec() ->
args_desc = ["User name", "Server name",
"New password for user"],
result = {res, rescode},
result_example = ok,
result_desc = "Status code: 0 on success, 1 otherwise"},
result_example = ok},
#ejabberd_commands{name = ban_account, tags = [accounts],
desc = "Ban an account: kick sessions and set random password",
module = ?MODULE, function = ban_account,
@ -249,8 +244,7 @@ get_commands_spec() ->
args_desc = ["User name to ban", "Server name",
"Reason for banning user"],
result = {res, rescode},
result_example = ok,
result_desc = "Status code: 0 on success, 1 otherwise"},
result_example = ok},
#ejabberd_commands{name = num_resources, tags = [session],
desc = "Get the number of resources of a user",
module = ?MODULE, function = num_resources,
@ -278,8 +272,7 @@ get_commands_spec() ->
args_desc = ["User name", "Server name", "User's resource",
"Reason for closing session"],
result = {res, rescode},
result_example = ok,
result_desc = "Status code: 0 on success, 1 otherwise"},
result_example = ok},
#ejabberd_commands{name = status_num_host, tags = [session, statistics],
desc = "Number of logged users with this status in host",
policy = admin,
@ -430,6 +423,22 @@ get_commands_spec() ->
"Show: `away`, `chat`, `dnd`, `xa`.", "Status text",
"Priority, provide this value as an integer"],
result = {res, rescode}},
#ejabberd_commands{name = set_presence,
tags = [session],
desc = "Set presence of a session",
module = ?MODULE, function = set_presence,
version = 1,
args = [{user, binary}, {host, binary},
{resource, binary}, {type, binary},
{show, binary}, {status, binary},
{priority, integer}],
args_example = [<<"user1">>,<<"myserver.com">>,<<"tka1">>,
<<"available">>,<<"away">>,<<"BB">>, 7],
args_desc = ["User name", "Server name", "Resource",
"Type: `available`, `error`, `probe`...",
"Show: `away`, `chat`, `dnd`, `xa`.", "Status text",
"Priority, provide this value as an integer"],
result = {res, rescode}},
#ejabberd_commands{name = set_nickname, tags = [vcard],
desc = "Set nickname in a user's vCard",
@ -502,6 +511,20 @@ get_commands_spec() ->
args_desc = ["User name", "Server name", "Contact user name", "Contact server name",
"Nickname", "Group", "Subscription"],
result = {res, rescode}},
#ejabberd_commands{name = add_rosteritem, tags = [roster],
desc = "Add an item to a user's roster (supports ODBC)",
module = ?MODULE, function = add_rosteritem,
version = 1,
args = [{localuser, binary}, {localhost, binary},
{user, binary}, {host, binary},
{nick, binary}, {groups, {list, {group, binary}}},
{subs, binary}],
args_rename = [{localserver, localhost}, {server, host}],
args_example = [<<"user1">>,<<"myserver.com">>,<<"user2">>, <<"myserver.com">>,
<<"User 2">>, [<<"Friends">>, <<"Team 1">>], <<"both">>],
args_desc = ["User name", "Server name", "Contact user name", "Contact server name",
"Nickname", "Groups", "Subscription"],
result = {res, rescode}},
%%{"", "subs= none, from, to or both"},
%%{"", "example: add-roster peter localhost mike server.com MiKe Employees both"},
%%{"", "will add mike@server.com to peter@localhost roster"},
@ -516,52 +539,56 @@ get_commands_spec() ->
result = {res, rescode}},
#ejabberd_commands{name = process_rosteritems, tags = [roster],
desc = "List/delete rosteritems that match filter",
longdesc = "Explanation of each argument:\n"
" - action: what to do with each rosteritem that "
longdesc = "Explanation of each argument:\n\n"
"* `action`: what to do with each rosteritem that "
"matches all the filtering options\n"
" - subs: subscription type\n"
" - asks: pending subscription\n"
" - users: the JIDs of the local user\n"
" - contacts: the JIDs of the contact in the roster\n"
"* `subs`: subscription type\n"
"* `asks`: pending subscription\n"
"* `users`: the JIDs of the local user\n"
"* `contacts`: the JIDs of the contact in the roster\n"
"\n"
" *** Mnesia: \n"
"**Mnesia backend:**\n"
"\n"
"Allowed values in the arguments:\n"
" ACTION = list | delete\n"
" SUBS = SUB[:SUB]* | any\n"
" SUB = none | from | to | both\n"
" ASKS = ASK[:ASK]* | any\n"
" ASK = none | out | in\n"
" USERS = JID[:JID]* | any\n"
" CONTACTS = JID[:JID]* | any\n"
" JID = characters valid in a JID, and can use the "
"globs: *, ?, ! and [...]\n"
"Allowed values in the arguments:\n\n"
"* `action` = `list` | `delete`\n"
"* `subs` = `any` | SUB[:SUB]*\n"
"* `asks` = `any` | ASK[:ASK]*\n"
"* `users` = `any` | JID[:JID]*\n"
"* `contacts` = `any` | JID[:JID]*\n"
"\nwhere\n\n"
"* SUB = `none` | `from `| `to` | `both`\n"
"* ASK = `none` | `out` | `in`\n"
"* JID = characters valid in a JID, and can use the "
"globs: `*`, `?`, `!` and `[...]`\n"
"\n"
"This example will list roster items with subscription "
"'none', 'from' or 'to' that have any ask property, of "
"`none`, `from` or `to` that have any ask property, of "
"local users which JID is in the virtual host "
"'example.org' and that the contact JID is either a "
"`example.org` and that the contact JID is either a "
"bare server name (without user part) or that has a "
"user part and the server part contains the word 'icq'"
":\n list none:from:to any *@example.org *:*@*icq*"
"user part and the server part contains the word `icq`"
":\n `list none:from:to any *@example.org *:*@*icq*`"
"\n\n"
" *** SQL:\n"
"**SQL backend:**\n"
"\n"
"Allowed values in the arguments:\n"
" ACTION = list | delete\n"
" SUBS = any | none | from | to | both\n"
" ASKS = any | none | out | in\n"
" USERS = JID\n"
" CONTACTS = JID\n"
" JID = characters valid in a JID, and can use the "
"globs: _ and %\n"
"Allowed values in the arguments:\n\n"
"* `action` = `list` | `delete`\n"
"* `subs` = `any` | SUB\n"
"* `asks` = `any` | ASK\n"
"* `users` = JID\n"
"* `contacts` = JID\n"
"\nwhere\n\n"
"* SUB = `none` | `from` | `to` | `both`\n"
"* ASK = `none` | `out` | `in`\n"
"* JID = characters valid in a JID, and can use the "
"globs: `_` and `%`\n"
"\n"
"This example will list roster items with subscription "
"'to' that have any ask property, of "
"`to` that have any ask property, of "
"local users which JID is in the virtual host "
"'example.org' and that the contact JID's "
"server part contains the word 'icq'"
":\n list to any %@example.org %@%icq%",
"`example.org` and that the contact JID's "
"server part contains the word `icq`"
":\n `list to any %@example.org %@%icq%`",
module = mod_roster, function = process_rosteritems,
args = [{action, string}, {subs, string},
{asks, string}, {users, string},
@ -576,8 +603,8 @@ get_commands_spec() ->
#ejabberd_commands{name = get_roster, tags = [roster],
desc = "Get list of contacts in a local user roster",
longdesc =
"Subscription can be: \"none\", \"from\", \"to\", \"both\". "
"Pending can be: \"in\", \"out\", \"none\".",
"`subscription` can be: `none`, `from`, `to`, `both`.\n\n"
"`pending` can be: `in`, `out`, `none`.",
note = "improved in 23.10",
policy = user,
module = ?MODULE, function = get_roster,
@ -593,11 +620,12 @@ get_commands_spec() ->
#ejabberd_commands{name = push_roster, tags = [roster],
desc = "Push template roster from file to a user",
longdesc = "The text file must contain an erlang term: a list "
"of tuples with username, servername, group and nick. Example:\n"
"[{<<\"user1\">>, <<\"localhost\">>, <<\"Workers\">>, <<\"User 1\">>},\n"
" {<<\"user2\">>, <<\"localhost\">>, <<\"Workers\">>, <<\"User 2\">>}].\n"
"When using UTF8 character encoding add /utf8 to certain string. Example:\n"
"[{<<\"user2\">>, <<\"localhost\">>, <<\"Workers\"/utf8>>, <<\"User 2\"/utf8>>}].",
"of tuples with username, servername, group and nick. For example:\n"
"`[{\"user1\", \"localhost\", \"Workers\", \"User 1\"},\n"
" {\"user2\", \"localhost\", \"Workers\", \"User 2\"}].`\n\n"
"If there are problems parsing UTF8 character encoding, "
"provide the corresponding string with the `<<\"STRING\"/utf8>>` syntax, for example:\n"
"`[{\"user2\", \"localhost\", \"Workers\", <<\"User 2\"/utf8>>}]`.",
module = ?MODULE, function = push_roster,
args = [{file, binary}, {user, binary}, {host, binary}],
args_example = [<<"/home/ejabberd/roster.txt">>, <<"user1">>, <<"localhost">>],
@ -607,8 +635,8 @@ get_commands_spec() ->
desc = "Push template roster from file to all those users",
longdesc = "The text file must contain an erlang term: a list "
"of tuples with username, servername, group and nick. Example:\n"
"[{\"user1\", \"localhost\", \"Workers\", \"User 1\"},\n"
" {\"user2\", \"localhost\", \"Workers\", \"User 2\"}].",
"`[{\"user1\", \"localhost\", \"Workers\", \"User 1\"},\n"
" {\"user2\", \"localhost\", \"Workers\", \"User 2\"}].`",
module = ?MODULE, function = push_roster_all,
args = [{file, binary}],
args_example = [<<"/home/ejabberd/roster.txt">>],
@ -624,7 +652,9 @@ get_commands_spec() ->
#ejabberd_commands{name = get_last, tags = [last],
desc = "Get last activity information",
longdesc = "Timestamp is UTC and XEP-0082 format, for example: "
longdesc = "Timestamp is UTC and "
"[XEP-0082](https://xmpp.org/extensions/xep-0082.html)"
" format, for example: "
"`2017-02-23T22:25:28.063062Z ONLINE`",
module = ?MODULE, function = get_last,
args = [{user, binary}, {host, binary}],
@ -681,6 +711,18 @@ get_commands_spec() ->
args_desc = ["Group identifier", "Group server name", "Group name",
"Group description", "Groups to display"],
result = {res, rescode}},
#ejabberd_commands{name = srg_create, tags = [shared_roster_group],
desc = "Create a Shared Roster Group",
module = ?MODULE, function = srg_create,
version = 1,
args = [{group, binary}, {host, binary},
{label, binary}, {description, binary}, {display, {list, {group, binary}}}],
args_rename = [{name, label}],
args_example = [<<"group3">>, <<"myserver.com">>, <<"Group3">>,
<<"Third group">>, [<<"group1">>, <<"group2">>]],
args_desc = ["Group identifier", "Group server name", "Group name",
"Group description", "List of groups to display"],
result = {res, rescode}},
#ejabberd_commands{name = srg_delete, tags = [shared_roster_group],
desc = "Delete a Shared Roster Group",
module = ?MODULE, function = srg_delete,
@ -782,7 +824,9 @@ get_commands_spec() ->
result = {res, rescode}},
#ejabberd_commands{name = stats, tags = [statistics],
desc = "Get statistical value: registeredusers onlineusers onlineusersnode uptimeseconds processes",
desc = "Get some statistical value for the whole ejabberd server",
longdesc = "Allowed statistics `name` are: `registeredusers`, "
"`onlineusers`, `onlineusersnode`, `uptimeseconds`, `processes`.",
policy = admin,
module = ?MODULE, function = stats,
args = [{name, binary}],
@ -792,7 +836,8 @@ get_commands_spec() ->
result_desc = "Integer statistic value",
result = {stat, integer}},
#ejabberd_commands{name = stats_host, tags = [statistics],
desc = "Get statistical value for this host: registeredusers onlineusers",
desc = "Get some statistical value for this host",
longdesc = "Allowed statistics `name` are: `registeredusers`, `onlineusers`.",
policy = admin,
module = ?MODULE, function = stats,
args = [{name, binary}, {host, binary}],
@ -1081,14 +1126,10 @@ get_presence(U, S) ->
{FullJID, Show, Status}
end.
set_presence(User, Host, Resource, Type, Show, Status, Priority)
when is_integer(Priority) ->
BPriority = integer_to_binary(Priority),
set_presence(User, Host, Resource, Type, Show, Status, BPriority);
set_presence(User, Host, Resource, Type, Show, Status, Priority0) ->
Priority = if is_integer(Priority0) -> Priority0;
true -> binary_to_integer(Priority0)
end,
set_presence(User, Host, Resource, Type, Show, Status, Priority) when is_binary(Priority) ->
set_presence(User, Host, Resource, Type, Show, Status, binary_to_integer(Priority));
set_presence(User, Host, Resource, Type, Show, Status, Priority) ->
Pres = #presence{
from = jid:make(User, Host, Resource),
to = jid:make(User, Host),
@ -1286,14 +1327,16 @@ update_vcard_els(Data, ContentList, Els1) ->
%%% Roster
%%%
add_rosteritem(LocalUser, LocalServer, User, Server, Nick, Group, Subs) ->
add_rosteritem(LocalUser, LocalServer, User, Server, Nick, Group, Subs) when is_binary(Group) ->
add_rosteritem(LocalUser, LocalServer, User, Server, Nick, [Group], Subs);
add_rosteritem(LocalUser, LocalServer, User, Server, Nick, Groups, Subs) ->
case {jid:make(LocalUser, LocalServer), jid:make(User, Server)} of
{error, _} ->
throw({error, "Invalid 'localuser'/'localserver'"});
{_, error} ->
throw({error, "Invalid 'user'/'server'"});
{Jid, _Jid2} ->
RosterItem = build_roster_item(User, Server, {add, Nick, Subs, Group}),
RosterItem = build_roster_item(User, Server, {add, Nick, Subs, Groups}),
case mod_roster:set_item_and_notify_clients(Jid, RosterItem, true) of
ok -> ok;
_ -> error
@ -1408,6 +1451,11 @@ push_roster_item(LU, LS, R, U, S, Action) ->
ejabberd_router:route(
xmpp:set_from_to(ResIQ, jid:remove_resource(LJID), LJID)).
build_roster_item(U, S, {add, Nick, Subs, Groups}) when is_list(Groups) ->
#roster_item{jid = jid:make(U, S),
name = Nick,
subscription = misc:binary_to_atom(Subs),
groups = Groups};
build_roster_item(U, S, {add, Nick, Subs, Group}) ->
Groups = binary:split(Group,<<";">>, [global, trim]),
#roster_item{jid = jid:make(U, S),
@ -1488,11 +1536,14 @@ private_set2(Username, Host, Xml) ->
%%% Shared Roster Groups
%%%
srg_create(Group, Host, Label, Description, Display) ->
srg_create(Group, Host, Label, Description, Display) when is_binary(Display) ->
DisplayList = case Display of
<<>> -> [];
_ -> ejabberd_regexp:split(Display, <<"\\\\n">>)
<<>> -> [];
_ -> ejabberd_regexp:split(Display, <<"\\\\n">>)
end,
srg_create(Group, Host, Label, Description, DisplayList);
srg_create(Group, Host, Label, Description, DisplayList) ->
Opts = [{label, Label},
{displayed_groups, DisplayList},
{description, Description}],

View File

@ -72,8 +72,7 @@ get_commands_spec() ->
args_example = [],
args_desc = [],
result = {res, rescode},
result_example = ok,
result_desc = "Status code: 0 on success, 1 otherwise"}
result_example = ok}
].
update_sql() ->

View File

@ -39,7 +39,7 @@
-include("ejabberd_stacktrace.hrl").
-include("translate.hrl").
-define(DEFAULT_API_VERSION, 0).
-define(DEFAULT_API_VERSION, 1000000).
-define(CT_PLAIN,
{<<"Content-Type">>, <<"text/plain">>}).
@ -135,7 +135,7 @@ extract_auth(#request{auth = HTTPAuth, ip = {IP, _}, opts = Opts}) ->
process(_, #request{method = 'POST', data = <<>>}) ->
?DEBUG("Bad Request: no data", []),
badrequest_response(<<"Missing POST data">>);
process([Call], #request{method = 'POST', data = Data, ip = IPPort} = Req) ->
process([Call | _], #request{method = 'POST', data = Data, ip = IPPort} = Req) ->
Version = get_api_version(Req),
try
Args = extract_args(Data),
@ -153,7 +153,7 @@ process([Call], #request{method = 'POST', data = Data, ip = IPPort} = Req) ->
?DEBUG("Bad Request: ~p ~p", [_Error, StackTrace]),
badrequest_response()
end;
process([Call], #request{method = 'GET', q = Data, ip = {IP, _}} = Req) ->
process([Call | _], #request{method = 'GET', q = Data, ip = {IP, _}} = Req) ->
Version = get_api_version(Req),
try
Args = case Data of
@ -412,7 +412,15 @@ format_command_result(Cmd, Auth, Result, Version) ->
{_, T} = format_result(Result, ResultFormat),
{200, T};
_ ->
{200, {[format_result(Result, ResultFormat)]}}
OtherResult1 = format_result(Result, ResultFormat),
OtherResult2 = case Version of
0 ->
{[OtherResult1]};
_ ->
{_, Other3} = OtherResult1,
Other3
end,
{200, OtherResult2}
end.
format_result(Atom, {Name, atom}) ->

View File

@ -93,10 +93,11 @@ depends(_Host, _Opts) ->
get_commands_spec() ->
[
#ejabberd_commands{name = muc_online_rooms, tags = [muc],
desc = "List existing rooms ('global' to get all vhosts)",
desc = "List existing rooms",
longdesc = "Ask for a specific host, or `global` to use all vhosts.",
policy = admin,
module = ?MODULE, function = muc_online_rooms,
args_desc = ["MUC service, or 'global' for all"],
args_desc = ["MUC service, or `global` for all"],
args_example = ["muc.example.com"],
result_desc = "List of rooms",
result_example = ["room1@muc.example.com", "room2@muc.example.com"],
@ -104,10 +105,11 @@ get_commands_spec() ->
args_rename = [{host, service}],
result = {rooms, {list, {room, string}}}},
#ejabberd_commands{name = muc_online_rooms_by_regex, tags = [muc],
desc = "List existing rooms ('global' to get all vhosts) by regex",
desc = "List existing rooms filtered by regexp",
longdesc = "Ask for a specific host, or `global` to use all vhosts.",
policy = admin,
module = ?MODULE, function = muc_online_rooms_by_regex,
args_desc = ["MUC service, or 'global' for all",
args_desc = ["MUC service, or `global` for all",
"Regex pattern for room name"],
args_example = ["muc.example.com", "^prefix"],
result_desc = "List of rooms with summary",
@ -160,7 +162,7 @@ get_commands_spec() ->
args_example = ["/home/ejabberd/rooms.txt"],
args = [{file, string}],
result = {res, rescode}},
#ejabberd_commands{name = create_room_with_opts, tags = [muc_room],
#ejabberd_commands{name = create_room_with_opts, tags = [muc_room, muc_sub],
desc = "Create a MUC room name@service in host with given options",
longdesc =
"The syntax of `affiliations` is: `Type:JID,Type:JID`. "
@ -246,7 +248,7 @@ get_commands_spec() ->
result_example = ["room1@muc.example.com", "room2@muc.example.com"],
args = [{user, binary}, {host, binary}],
result = {rooms, {list, {room, string}}}},
#ejabberd_commands{name = get_user_subscriptions, tags = [muc],
#ejabberd_commands{name = get_user_subscriptions, tags = [muc, muc_sub],
desc = "Get the list of rooms where this user is subscribed",
note = "added in 21.04",
module = ?MODULE, function = get_user_subscriptions,
@ -306,6 +308,22 @@ get_commands_spec() ->
args = [{name, binary}, {service, binary}, {password, binary},
{reason, binary}, {users, binary}],
result = {res, rescode}},
#ejabberd_commands{name = send_direct_invitation, tags = [muc_room],
desc = "Send a direct invitation to several destinations",
longdesc = "Since ejabberd 20.12, this command is "
"asynchronous: the API call may return before the "
"server has send all the invitations.\n\n"
"`password` and `message` can be set to `none`.",
module = ?MODULE, function = send_direct_invitation,
version = 1,
args_desc = ["Room name", "MUC service", "Password, or `none`",
"Reason text, or `none`", "List of users JIDs"],
args_example = [<<"room1">>, <<"muc.example.com">>,
<<>>, <<"Check this out!">>,
["user2@localhost", "user3@example.com"]],
args = [{name, binary}, {service, binary}, {password, binary},
{reason, binary}, {users, {list, {jid, binary}}}],
result = {res, rescode}},
#ejabberd_commands{name = change_room_option, tags = [muc_room],
desc = "Change an option in a MUC room",
@ -329,7 +347,7 @@ get_commands_spec() ->
{value, string}
]}}
}}},
#ejabberd_commands{name = subscribe_room, tags = [muc_room],
#ejabberd_commands{name = subscribe_room, tags = [muc_room, muc_sub],
desc = "Subscribe to a MUC conference",
module = ?MODULE, function = subscribe_room,
args_desc = ["User JID", "a user's nick",
@ -342,7 +360,21 @@ get_commands_spec() ->
args = [{user, binary}, {nick, binary}, {room, binary},
{nodes, binary}],
result = {nodes, {list, {node, string}}}},
#ejabberd_commands{name = subscribe_room_many, tags = [muc_room],
#ejabberd_commands{name = subscribe_room, tags = [muc_room, muc_sub],
desc = "Subscribe to a MUC conference",
module = ?MODULE, function = subscribe_room,
version = 1,
args_desc = ["User JID", "a user's nick",
"the room to subscribe", "list of nodes"],
args_example = ["tom@localhost", "Tom", "room1@conference.localhost",
["urn:xmpp:mucsub:nodes:messages", "urn:xmpp:mucsub:nodes:affiliations"]],
result_desc = "The list of nodes that has subscribed",
result_example = ["urn:xmpp:mucsub:nodes:messages",
"urn:xmpp:mucsub:nodes:affiliations"],
args = [{user, binary}, {nick, binary}, {room, binary},
{nodes, {list, {node, binary}}}],
result = {nodes, {list, {node, string}}}},
#ejabberd_commands{name = subscribe_room_many, tags = [muc_room, muc_sub],
desc = "Subscribe several users to a MUC conference",
note = "added in 22.05",
longdesc = "This command accepts up to 50 users at once "
@ -365,14 +397,38 @@ get_commands_spec() ->
{room, binary},
{nodes, binary}],
result = {res, rescode}},
#ejabberd_commands{name = unsubscribe_room, tags = [muc_room],
#ejabberd_commands{name = subscribe_room_many, tags = [muc_room, muc_sub],
desc = "Subscribe several users to a MUC conference",
note = "added in 22.05",
longdesc = "This command accepts up to 50 users at once "
"(this is configurable with the *`mod_muc_admin`* option "
"`subscribe_room_many_max_users`)",
module = ?MODULE, function = subscribe_room_many,
version = 1,
args_desc = ["Users JIDs and nicks",
"the room to subscribe",
"nodes separated by commas: `,`"],
args_example = [[{"tom@localhost", "Tom"},
{"jerry@localhost", "Jerry"}],
"room1@conference.localhost",
["urn:xmpp:mucsub:nodes:messages", "urn:xmpp:mucsub:nodes:affiliations"]],
args = [{users, {list,
{user, {tuple,
[{jid, binary},
{nick, binary}
]}}
}},
{room, binary},
{nodes, {list, {node, binary}}}],
result = {res, rescode}},
#ejabberd_commands{name = unsubscribe_room, tags = [muc_room, muc_sub],
desc = "Unsubscribe from a MUC conference",
module = ?MODULE, function = unsubscribe_room,
args_desc = ["User JID", "the room to subscribe"],
args_example = ["tom@localhost", "room1@conference.localhost"],
args = [{user, binary}, {room, binary}],
result = {res, rescode}},
#ejabberd_commands{name = get_subscribers, tags = [muc_room],
#ejabberd_commands{name = get_subscribers, tags = [muc_room, muc_sub],
desc = "List subscribers of a MUC conference",
module = ?MODULE, function = get_subscribers,
args_desc = ["Room name", "MUC service"],
@ -1072,20 +1128,22 @@ get_room_occupants_number(Room, Host) ->
%%----------------------------
%% http://xmpp.org/extensions/xep-0249.html
send_direct_invitation(RoomName, RoomService, Password, Reason, UsersString) ->
send_direct_invitation(RoomName, RoomService, Password, Reason, UsersString) when is_binary(UsersString) ->
UsersStrings = binary:split(UsersString, <<":">>, [global]),
send_direct_invitation(RoomName, RoomService, Password, Reason, UsersStrings);
send_direct_invitation(RoomName, RoomService, Password, Reason, UsersStrings) ->
case jid:make(RoomName, RoomService) of
error ->
throw({error, "Invalid 'roomname' or 'service'"});
RoomJid ->
XmlEl = build_invitation(Password, Reason, RoomJid),
Users = get_users_to_invite(RoomJid, UsersString),
Users = get_users_to_invite(RoomJid, UsersStrings),
[send_direct_invitation(RoomJid, UserJid, XmlEl)
|| UserJid <- Users],
ok
end.
get_users_to_invite(RoomJid, UsersString) ->
UsersStrings = binary:split(UsersString, <<":">>, [global]),
get_users_to_invite(RoomJid, UsersStrings) ->
OccupantsTuples = get_room_occupants(RoomJid#jid.luser,
RoomJid#jid.lserver),
OccupantsJids = [jid:decode(JidString)
@ -1437,8 +1495,10 @@ set_room_affiliation(Name, Service, JID, AffiliationString) ->
subscribe_room(_User, Nick, _Room, _Nodes) when Nick == <<"">> ->
throw({error, "Nickname must be set"});
subscribe_room(User, Nick, Room, Nodes) ->
subscribe_room(User, Nick, Room, Nodes) when is_binary(Nodes) ->
NodeList = re:split(Nodes, "\\h*,\\h*"),
subscribe_room(User, Nick, Room, NodeList);
subscribe_room(User, Nick, Room, NodeList) ->
try jid:decode(Room) of
#jid{luser = Name, lserver = Host} when Name /= <<"">> ->
try jid:decode(User) of