diff --git a/include/ejabberd_commands.hrl b/include/ejabberd_commands.hrl index 812aa4d38..8707575f5 100644 --- a/include/ejabberd_commands.hrl +++ b/include/ejabberd_commands.hrl @@ -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. diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 5d8f9bd76..eda2a3c59 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -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], diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index a000beb54..c6a380e46 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -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"], diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl index e61569777..0114b1720 100644 --- a/src/ejabberd_commands.erl +++ b/src/ejabberd_commands.erl @@ -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) -> diff --git a/src/ejabberd_commands_doc.erl b/src/ejabberd_commands_doc.erl index 178e75a54..f6fe25616 100644 --- a/src/ejabberd_commands_doc.erl +++ b/src/ejabberd_commands_doc.erl @@ -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 diff --git a/src/ejabberd_ctl.erl b/src/ejabberd_ctl.erl index e244fef26..df2cce2ce 100644 --- a/src/ejabberd_ctl.erl +++ b/src/ejabberd_ctl.erl @@ -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)-> diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl index 83eceb1ba..c3d206e3f 100644 --- a/src/ejabberd_oauth.erl +++ b/src/ejabberd_oauth.erl @@ -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}, diff --git a/src/ejabberd_xmlrpc.erl b/src/ejabberd_xmlrpc.erl index 741bf8422..f6d55eba2 100644 --- a/src/ejabberd_xmlrpc.erl +++ b/src/ejabberd_xmlrpc.erl @@ -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)}]}. diff --git a/src/mod_admin_extra.erl b/src/mod_admin_extra.erl index fa292ae38..cb9deab1d 100644 --- a/src/mod_admin_extra.erl +++ b/src/mod_admin_extra.erl @@ -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}], diff --git a/src/mod_admin_update_sql.erl b/src/mod_admin_update_sql.erl index 74d30b3e3..85fb1320d 100644 --- a/src/mod_admin_update_sql.erl +++ b/src/mod_admin_update_sql.erl @@ -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() -> diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl index 514a8632c..c6a969091 100644 --- a/src/mod_http_api.erl +++ b/src/mod_http_api.erl @@ -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}) -> diff --git a/src/mod_muc_admin.erl b/src/mod_muc_admin.erl index 5c7453e2c..13f196278 100644 --- a/src/mod_muc_admin.erl +++ b/src/mod_muc_admin.erl @@ -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