mirror of
https://github.com/processone/ejabberd
synced 2025-10-03 01:39:35 +02:00
Merge pull request #4357 from badlop/adhoc_api
New mod_adhoc_api and related improvements
This commit is contained in:
commit
418ca34c85
19 changed files with 1266 additions and 130 deletions
|
@ -118,7 +118,12 @@ api_permissions:
|
|||
from: ejabberd_web_admin
|
||||
who: admin
|
||||
what: "*"
|
||||
"admin access":
|
||||
"adhoc commands":
|
||||
from: mod_adhoc_api
|
||||
who: admin
|
||||
what: "*"
|
||||
"http access":
|
||||
from: mod_http_api
|
||||
who:
|
||||
access:
|
||||
allow:
|
||||
|
@ -159,6 +164,7 @@ shaper_rules:
|
|||
|
||||
modules:
|
||||
mod_adhoc: {}
|
||||
mod_adhoc_api: {}
|
||||
mod_admin_extra: {}
|
||||
mod_announce:
|
||||
access: announce
|
||||
|
|
|
@ -344,10 +344,20 @@ validator(from) ->
|
|||
fun(L) when is_list(L) ->
|
||||
lists:map(
|
||||
fun({K, V}) -> {(econf:enum([tag]))(K), (econf:binary())(V)};
|
||||
(A) -> (econf:enum([ejabberd_xmlrpc, mod_cron, mod_http_api, ejabberd_ctl, ejabberd_web_admin]))(A)
|
||||
(A) -> (econf:enum([ejabberd_ctl,
|
||||
ejabberd_web_admin,
|
||||
ejabberd_xmlrpc,
|
||||
mod_adhoc_api,
|
||||
mod_cron,
|
||||
mod_http_api]))(A)
|
||||
end, lists:flatten(L));
|
||||
(A) ->
|
||||
[(econf:enum([ejabberd_xmlrpc, mod_cron, mod_http_api, ejabberd_ctl, ejabberd_web_admin]))(A)]
|
||||
[(econf:enum([ejabberd_ctl,
|
||||
ejabberd_web_admin,
|
||||
ejabberd_xmlrpc,
|
||||
mod_adhoc_api,
|
||||
mod_cron,
|
||||
mod_http_api]))(A)]
|
||||
end;
|
||||
validator(what) ->
|
||||
econf:and_then(
|
||||
|
|
|
@ -598,8 +598,9 @@ get_commands_spec() ->
|
|||
args = [{node, atom}, {table, binary}, {page, integer}],
|
||||
result = {res, any}},
|
||||
|
||||
#ejabberd_commands{name = mnesia_list_tables, tags = [internal, mnesia],
|
||||
#ejabberd_commands{name = mnesia_list_tables, tags = [mnesia],
|
||||
desc = "List of Mnesia tables",
|
||||
note = "added in 25.xx",
|
||||
module = ?MODULE, function = mnesia_list_tables,
|
||||
result = {tables, {list, {table, {tuple, [{name, atom},
|
||||
{storage_type, binary},
|
||||
|
@ -615,8 +616,10 @@ get_commands_spec() ->
|
|||
{value, binary}
|
||||
]}}}}},
|
||||
|
||||
#ejabberd_commands{name = mnesia_table_change_storage, tags = [internal, mnesia],
|
||||
desc = "Change storage type of a Mnesia table to: ram_copies, disc_copies, or disc_only_copies.",
|
||||
#ejabberd_commands{name = mnesia_table_change_storage, tags = [mnesia],
|
||||
desc = "Change storage type of a Mnesia table",
|
||||
note = "added in 25.xx",
|
||||
longdesc = "Storage type can be: `ram_copies`, `disc_copies`, `disc_only_copies`, `remote_copy`.",
|
||||
module = ?MODULE, function = mnesia_table_change_storage,
|
||||
args = [{table, binary}, {storage_type, binary}],
|
||||
result = {res, restuple}},
|
||||
|
@ -1281,13 +1284,12 @@ is_my_host(Host) ->
|
|||
|
||||
%% @format-begin
|
||||
|
||||
%% mnesia:del_table_copy(Table, Node);
|
||||
%% mnesia:change_table_copy_type(Table, Node, Type);
|
||||
|
||||
mnesia_table_change_storage(STable, SType) ->
|
||||
Table = binary_to_existing_atom(STable, latin1),
|
||||
Type =
|
||||
case SType of
|
||||
<<"remote_copy">> ->
|
||||
remote_copy;
|
||||
<<"ram_copies">> ->
|
||||
ram_copies;
|
||||
<<"disc_copies">> ->
|
||||
|
@ -1297,7 +1299,24 @@ mnesia_table_change_storage(STable, SType) ->
|
|||
_ ->
|
||||
false
|
||||
end,
|
||||
mnesia:add_table_copy(Table, node(), Type).
|
||||
Node = node(),
|
||||
Result =
|
||||
case Type of
|
||||
false ->
|
||||
"Nothing to do";
|
||||
remote_copy ->
|
||||
mnesia:del_table_copy(Table, Node),
|
||||
"Deleted table copy";
|
||||
_ ->
|
||||
case mnesia:add_table_copy(Table, Node, Type) of
|
||||
{aborted, _} ->
|
||||
mnesia:change_table_copy_type(Table, Node, Type),
|
||||
"Changed table copy type";
|
||||
_ ->
|
||||
"Added table copy"
|
||||
end
|
||||
end,
|
||||
{ok, Result}.
|
||||
|
||||
mnesia_table_clear(STable) ->
|
||||
Table = binary_to_existing_atom(STable, latin1),
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
-export([start_link/0,
|
||||
list_commands/0,
|
||||
list_commands/1,
|
||||
list_commands/2,
|
||||
get_command_format/1,
|
||||
get_command_format/2,
|
||||
get_command_format/3,
|
||||
|
@ -42,7 +43,9 @@
|
|||
get_tags_commands/1,
|
||||
register_commands/1,
|
||||
register_commands/2,
|
||||
register_commands/3,
|
||||
unregister_commands/1,
|
||||
unregister_commands/3,
|
||||
get_commands_spec/0,
|
||||
get_commands_definition/0,
|
||||
get_commands_definition/1,
|
||||
|
@ -142,19 +145,34 @@ code_change(_OldVsn, State, _Extra) ->
|
|||
register_commands(Commands) ->
|
||||
register_commands(unknown, Commands).
|
||||
|
||||
-spec register_commands(atom(), [ejabberd_commands()]) -> ok.
|
||||
|
||||
register_commands(Definer, Commands) ->
|
||||
ExistingCommands = list_commands(),
|
||||
lists:foreach(
|
||||
fun(Command) ->
|
||||
%% XXX check if command exists
|
||||
mnesia:dirty_write(register_command_prepare(Command, Definer))
|
||||
%% ?DEBUG("This command is already defined:~n~p", [Command])
|
||||
Name = Command#ejabberd_commands.name,
|
||||
case lists:keyfind(Name, 1, ExistingCommands) of
|
||||
false ->
|
||||
mnesia:dirty_write(register_command_prepare(Command, Definer));
|
||||
_ ->
|
||||
OtherCommandDef = get_command_definition(Name),
|
||||
?CRITICAL_MSG("Error trying to define a command: another one already exists with the same name:~n Existing: ~p~n New: ~p", [OtherCommandDef, Command])
|
||||
end
|
||||
end,
|
||||
Commands),
|
||||
ejabberd_access_permissions:invalidate(),
|
||||
ok.
|
||||
|
||||
-spec register_commands(binary(), atom(), [ejabberd_commands()]) -> ok.
|
||||
|
||||
|
||||
register_commands(Host, Definer, Commands) ->
|
||||
case gen_mod:is_loaded_elsewhere(Host, Definer) of
|
||||
false ->
|
||||
register_commands(Definer, Commands);
|
||||
true ->
|
||||
ok
|
||||
end.
|
||||
|
||||
register_command_prepare(Command, Definer) ->
|
||||
Tags1 = Command#ejabberd_commands.tags,
|
||||
|
@ -175,6 +193,16 @@ unregister_commands(Commands) ->
|
|||
Commands),
|
||||
ejabberd_access_permissions:invalidate().
|
||||
|
||||
-spec unregister_commands(binary(), atom(), [ejabberd_commands()]) -> ok.
|
||||
|
||||
unregister_commands(Host, Definer, Commands) ->
|
||||
case gen_mod:is_loaded_elsewhere(Host, Definer) of
|
||||
false ->
|
||||
unregister_commands(Commands);
|
||||
true ->
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec list_commands() -> [{atom(), [aterm()], string()}].
|
||||
|
||||
list_commands() ->
|
||||
|
@ -190,6 +218,16 @@ list_commands(Version) ->
|
|||
desc = Desc} <- Commands,
|
||||
not lists:member(internal, Tags)].
|
||||
|
||||
-spec list_commands(integer(), map()) -> [{atom(), [aterm()], string()}].
|
||||
|
||||
list_commands(Version, CallerInfo) ->
|
||||
lists:filter(
|
||||
fun({Name, _Args, _Desc}) ->
|
||||
allow == ejabberd_access_permissions:can_access(Name, CallerInfo)
|
||||
end,
|
||||
list_commands(Version)
|
||||
).
|
||||
|
||||
-spec get_command_format(atom()) -> {[aterm()], [{atom(),atom()}], rterm()}.
|
||||
|
||||
get_command_format(Name) ->
|
||||
|
|
729
src/mod_adhoc_api.erl
Normal file
729
src/mod_adhoc_api.erl
Normal file
|
@ -0,0 +1,729 @@
|
|||
%%%----------------------------------------------------------------------
|
||||
%%% File : mod_adhoc_api.erl
|
||||
%%% Author : Badlop <badlop@process-one.net>
|
||||
%%% Purpose : Frontend for ejabberd API Commands via XEP-0050 Ad-Hoc Commands
|
||||
%%% Created : 21 Feb 2025 by Badlop <badlop@process-one.net>
|
||||
%%%
|
||||
%%%
|
||||
%%% ejabberd, Copyright (C) 2002-2025 ProcessOne
|
||||
%%%
|
||||
%%% This program is free software; you can redistribute it and/or
|
||||
%%% modify it under the terms of the GNU General Public License as
|
||||
%%% published by the Free Software Foundation; either version 2 of the
|
||||
%%% License, or (at your option) any later version.
|
||||
%%%
|
||||
%%% This program is distributed in the hope that it will be useful,
|
||||
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
%%% General Public License for more details.
|
||||
%%%
|
||||
%%% You should have received a copy of the GNU General Public License along
|
||||
%%% with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
%%%
|
||||
%%%----------------------------------------------------------------------
|
||||
|
||||
%%%% definitions
|
||||
%% @format-begin
|
||||
|
||||
-module(mod_adhoc_api).
|
||||
|
||||
-behaviour(gen_mod).
|
||||
|
||||
-author('badlop@process-one.net').
|
||||
|
||||
%% gen_mod callbacks
|
||||
-export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]).
|
||||
%% hooks
|
||||
-export([adhoc_local_commands/4, adhoc_local_items/4, disco_local_features/5,
|
||||
disco_local_identity/5, disco_local_items/5]).
|
||||
|
||||
-include("ejabberd_commands.hrl").
|
||||
-include("ejabberd_sm.hrl").
|
||||
-include("logger.hrl").
|
||||
-include("translate.hrl").
|
||||
|
||||
-include_lib("stdlib/include/ms_transform.hrl").
|
||||
-include_lib("xmpp/include/xmpp.hrl").
|
||||
|
||||
-define(DEFAULT_API_VERSION, 1000000).
|
||||
|
||||
%%%==================================
|
||||
%%%% gen_mod
|
||||
|
||||
start(_Host, _Opts) ->
|
||||
{ok,
|
||||
[{hook, adhoc_local_commands, adhoc_local_commands, 40},
|
||||
{hook, adhoc_local_items, adhoc_local_items, 40},
|
||||
{hook, disco_local_features, disco_local_features, 40},
|
||||
{hook, disco_local_identity, disco_local_identity, 40},
|
||||
{hook, disco_local_items, disco_local_items, 40}]}.
|
||||
|
||||
stop(_Host) ->
|
||||
ok.
|
||||
|
||||
reload(_Host, _NewOpts, _OldOpts) ->
|
||||
ok.
|
||||
|
||||
mod_opt_type(default_version) ->
|
||||
econf:either(
|
||||
econf:int(0, 3),
|
||||
econf:and_then(
|
||||
econf:binary(),
|
||||
fun(Binary) ->
|
||||
case binary_to_list(Binary) of
|
||||
F when F >= "24.06" ->
|
||||
2;
|
||||
F when (F > "23.10") and (F < "24.06") ->
|
||||
1;
|
||||
F when F =< "23.10" ->
|
||||
0
|
||||
end
|
||||
end)).
|
||||
|
||||
-spec mod_options(binary()) -> [{default_version, integer()}].
|
||||
mod_options(_) ->
|
||||
[{default_version, ?DEFAULT_API_VERSION}].
|
||||
|
||||
depends(_Host, _Opts) ->
|
||||
[{mod_adhoc, hard}, {mod_last, soft}].
|
||||
|
||||
mod_doc() ->
|
||||
#{desc =>
|
||||
?T("Execute https://docs.ejabberd.im/developer/ejabberd-api/[API Commands] "
|
||||
"in a XMPP client using "
|
||||
"https://xmpp.org/extensions/xep-0050.html[XEP-0050: Ad-Hoc Commands]. "
|
||||
"This module requires _`mod_adhoc`_ (to execute the commands), "
|
||||
"and recommends _`mod_disco`_ (to discover the commands)."),
|
||||
note => "added in 25.xx",
|
||||
opts =>
|
||||
[{default_version,
|
||||
#{value => "integer() | string()",
|
||||
desc =>
|
||||
?T("What API version to use. "
|
||||
"If setting an ejabberd version, it will use the latest API "
|
||||
"version that was available in that ejabberd version. "
|
||||
"For example, setting '\"24.06\"' in this option implies '2'. "
|
||||
"The default value is the latest version.")}}],
|
||||
example =>
|
||||
["acl:",
|
||||
" admin:",
|
||||
" user: jan@localhost",
|
||||
"",
|
||||
"api_permissions:",
|
||||
" \"adhoc commands\":",
|
||||
" from: mod_adhoc_api",
|
||||
" who: admin",
|
||||
" what:",
|
||||
" - \"[tag:roster]\"",
|
||||
" - \"[tag:session]\"",
|
||||
" - stats",
|
||||
" - status",
|
||||
"",
|
||||
"modules:",
|
||||
" mod_adhoc_api:",
|
||||
" default_version: 2"]}.
|
||||
|
||||
%%%==================================
|
||||
%%%% Ad-Hoc Commands (copied from mod_configure)
|
||||
|
||||
-define(INFO_IDENTITY(Category, Type, Name, Lang),
|
||||
[#identity{category = Category,
|
||||
type = Type,
|
||||
name = tr(Lang, Name)}]).
|
||||
-define(INFO_COMMAND(Name, Lang),
|
||||
?INFO_IDENTITY(<<"automation">>, <<"command-node">>, Name, Lang)).
|
||||
-define(NODE(Name, Node),
|
||||
#disco_item{jid = jid:make(Server),
|
||||
node = Node,
|
||||
name = tr(Lang, Name)}).
|
||||
|
||||
-spec tokenize(binary()) -> [binary()].
|
||||
tokenize(Node) ->
|
||||
str:tokens(Node, <<"/#">>).
|
||||
|
||||
-spec tr(binary(), binary()) -> binary().
|
||||
tr(Lang, Text) ->
|
||||
translate:translate(Lang, Text).
|
||||
|
||||
%%%==================================
|
||||
%%%% - disco identity
|
||||
|
||||
-spec disco_local_identity([identity()], jid(), jid(), binary(), binary()) ->
|
||||
[identity()].
|
||||
disco_local_identity(Acc, _From, #jid{lserver = LServer} = _To, Node, Lang) ->
|
||||
case tokenize(Node) of
|
||||
[<<"api-commands">>] ->
|
||||
?INFO_COMMAND(?T("API Commands"), Lang);
|
||||
[<<"api-commands">>, CommandName] ->
|
||||
?INFO_COMMAND(get_api_command_desc(CommandName, LServer), Lang);
|
||||
_ ->
|
||||
Acc
|
||||
end.
|
||||
|
||||
get_api_command_desc(NameAtom, Host) ->
|
||||
iolist_to_binary((get_api_command(NameAtom, Host))#ejabberd_commands.desc).
|
||||
|
||||
%%%==================================
|
||||
%%%% - disco features
|
||||
|
||||
-spec disco_local_features(mod_disco:features_acc(), jid(), jid(), binary(), binary()) ->
|
||||
mod_disco:features_acc().
|
||||
disco_local_features(Acc, _From, #jid{lserver = LServer} = _To, Node, _Lang) ->
|
||||
case gen_mod:is_loaded(LServer, mod_adhoc) of
|
||||
false ->
|
||||
Acc;
|
||||
_ ->
|
||||
case tokenize(Node) of
|
||||
[<<"api-commands">>] ->
|
||||
{result, []};
|
||||
[<<"api-commands">>, _] ->
|
||||
{result, [?NS_COMMANDS]};
|
||||
_ ->
|
||||
Acc
|
||||
end
|
||||
end.
|
||||
|
||||
%%%==================================
|
||||
%%%% - adhoc items
|
||||
|
||||
-spec adhoc_local_items(mod_disco:items_acc(), jid(), jid(), binary()) ->
|
||||
mod_disco:items_acc().
|
||||
adhoc_local_items(Acc, From, #jid{lserver = LServer, server = Server} = To, Lang) ->
|
||||
Items =
|
||||
case Acc of
|
||||
{result, Its} ->
|
||||
Its;
|
||||
empty ->
|
||||
[]
|
||||
end,
|
||||
Nodes = recursively_get_local_items(From, global, LServer, <<"">>, Server, Lang),
|
||||
Nodes1 =
|
||||
lists:filter(fun(#disco_item{node = Nd}) ->
|
||||
F = disco_local_features(empty, From, To, Nd, Lang),
|
||||
case F of
|
||||
{result, [?NS_COMMANDS]} ->
|
||||
true;
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end,
|
||||
Nodes),
|
||||
{result, Items ++ Nodes1}.
|
||||
|
||||
-spec recursively_get_local_items(jid(),
|
||||
global | vhost,
|
||||
binary(),
|
||||
binary(),
|
||||
binary(),
|
||||
binary()) ->
|
||||
[disco_item()].
|
||||
recursively_get_local_items(From, PermLev, LServer, Node, Server, Lang) ->
|
||||
Items =
|
||||
case get_local_items2(From, {PermLev, LServer}, tokenize(Node), Server, Lang) of
|
||||
{result, Res} ->
|
||||
Res;
|
||||
{error, _Error} ->
|
||||
[]
|
||||
end,
|
||||
lists:flatten(
|
||||
lists:map(fun(#disco_item{jid = #jid{server = S}, node = Nd} = Item) ->
|
||||
if (S /= Server) or (Nd == <<"">>) ->
|
||||
[];
|
||||
true ->
|
||||
[Item,
|
||||
recursively_get_local_items(From, PermLev, LServer, Nd, Server, Lang)]
|
||||
end
|
||||
end,
|
||||
Items)).
|
||||
|
||||
%%%==================================
|
||||
%%%% - disco items
|
||||
|
||||
-spec disco_local_items(mod_disco:items_acc(), jid(), jid(), binary(), binary()) ->
|
||||
mod_disco:items_acc().
|
||||
disco_local_items(Acc, From, #jid{lserver = LServer} = To, Node, Lang) ->
|
||||
case gen_mod:is_loaded(LServer, mod_adhoc) of
|
||||
false ->
|
||||
Acc;
|
||||
_ ->
|
||||
Items =
|
||||
case Acc of
|
||||
{result, Its} ->
|
||||
Its;
|
||||
empty ->
|
||||
[];
|
||||
Other ->
|
||||
Other
|
||||
end,
|
||||
case tokenize(Node) of
|
||||
LNode when (LNode == [<<"api-commands">>]) or (LNode == []) ->
|
||||
case get_local_items2(From, {global, LServer}, LNode, jid:encode(To), Lang) of
|
||||
{result, Res} ->
|
||||
{result, Res};
|
||||
{error, Error} ->
|
||||
{error, Error}
|
||||
end;
|
||||
_ ->
|
||||
{result, Items}
|
||||
end
|
||||
end.
|
||||
|
||||
%%%==================================
|
||||
%%%% - get_local_items2
|
||||
|
||||
-spec get_local_items2(jid(),
|
||||
{global | vhost, binary()},
|
||||
[binary()],
|
||||
binary(),
|
||||
binary()) ->
|
||||
{result, [disco_item()]} | {error, stanza_error()}.
|
||||
get_local_items2(_From, _Host, [], Server, Lang) ->
|
||||
{result, [?NODE(?T("API Commands"), <<"api-commands">>)]};
|
||||
get_local_items2(From, {_, Host}, [<<"api-commands">>], _Server, Lang) ->
|
||||
{result, get_api_commands(From, Host, Lang)};
|
||||
get_local_items2(_From, {_, _Host}, [<<"api-commands">>, _], _Server, _Lang) ->
|
||||
{result, []};
|
||||
get_local_items2(_From, _Host, _, _Server, _Lang) ->
|
||||
{error, xmpp:err_item_not_found()}.
|
||||
|
||||
-spec get_api_commands(jid(), binary(), binary()) -> [disco_item()].
|
||||
get_api_commands(From, Server, Lang) ->
|
||||
ApiVersion = mod_adhoc_api_opt:default_version(Server),
|
||||
lists:map(fun({Name, _Args, _Desc}) ->
|
||||
NameBin = list_to_binary(atom_to_list(Name)),
|
||||
?NODE(NameBin, <<"api-commands/", NameBin/binary>>)
|
||||
end,
|
||||
ejabberd_commands:list_commands(ApiVersion, get_caller_info(From))).
|
||||
|
||||
%%%==================================
|
||||
%%%% - adhoc commands
|
||||
|
||||
-define(COMMANDS_RESULT(LServerOrGlobal, From, To, Request, Lang),
|
||||
adhoc_local_commands(From, To, Request)).
|
||||
|
||||
-spec adhoc_local_commands(adhoc_command(), jid(), jid(), adhoc_command()) ->
|
||||
adhoc_command() | {error, stanza_error()}.
|
||||
adhoc_local_commands(Acc, From, To, #adhoc_command{node = Node} = Request) ->
|
||||
case tokenize(Node) of
|
||||
[<<"api-commands">>, _CommandName] ->
|
||||
?COMMANDS_RESULT(LServer, From, To, Request, Lang);
|
||||
_ ->
|
||||
Acc
|
||||
end.
|
||||
|
||||
-spec adhoc_local_commands(jid(), jid(), adhoc_command()) ->
|
||||
adhoc_command() | {error, stanza_error()}.
|
||||
adhoc_local_commands(From,
|
||||
#jid{lserver = LServer} = _To,
|
||||
#adhoc_command{lang = Lang,
|
||||
node = Node,
|
||||
sid = SessionID,
|
||||
action = Action,
|
||||
xdata = XData} =
|
||||
Request) ->
|
||||
LNode = tokenize(Node),
|
||||
ActionIsExecute = Action == execute orelse Action == complete,
|
||||
if Action == cancel ->
|
||||
#adhoc_command{status = canceled,
|
||||
lang = Lang,
|
||||
node = Node,
|
||||
sid = SessionID};
|
||||
XData == undefined, ActionIsExecute ->
|
||||
case get_form(LServer, LNode, Lang) of
|
||||
{result, Form} ->
|
||||
xmpp_util:make_adhoc_response(Request,
|
||||
#adhoc_command{status = executing, xdata = Form});
|
||||
{error, Error} ->
|
||||
{error, Error}
|
||||
end;
|
||||
XData /= undefined, ActionIsExecute ->
|
||||
case set_form(From, LServer, LNode, Lang, XData) of
|
||||
{result, Res} ->
|
||||
xmpp_util:make_adhoc_response(Request,
|
||||
#adhoc_command{xdata = Res, status = completed});
|
||||
%%{'EXIT', _} -> {error, xmpp:err_bad_request()};
|
||||
{error, Error} ->
|
||||
{error, Error}
|
||||
end;
|
||||
true ->
|
||||
{error, xmpp:err_bad_request(?T("Unexpected action"), Lang)}
|
||||
end.
|
||||
|
||||
-spec get_form(binary(), [binary()], binary()) ->
|
||||
{result, xdata()} | {error, stanza_error()}.
|
||||
get_form(Host, [<<"api-commands">>, CommandName], Lang) ->
|
||||
get_form_api_command(CommandName, Host, Lang);
|
||||
get_form(_Host, _, _Lang) ->
|
||||
{error, xmpp:err_service_unavailable()}.
|
||||
|
||||
-spec set_form(jid(), binary(), [binary()], binary(), xdata()) ->
|
||||
{result, xdata() | undefined} | {error, stanza_error()}.
|
||||
set_form(From, Host, [<<"api-commands">>, Command], Lang, XData) ->
|
||||
set_form_api_command(From, Host, Command, XData, Lang);
|
||||
set_form(_From, _Host, _, _Lang, _XData) ->
|
||||
{error, xmpp:err_service_unavailable()}.
|
||||
|
||||
%%%==================================
|
||||
%%%% API Commands
|
||||
|
||||
get_api_command(Name, Host) when is_binary(Name) ->
|
||||
get_api_command(binary_to_existing_atom(Name, latin1), Host);
|
||||
get_api_command(Name, Host) when is_atom(Name) ->
|
||||
ApiVersion = mod_adhoc_api_opt:default_version(Host),
|
||||
ejabberd_commands:get_command_definition(Name, ApiVersion).
|
||||
|
||||
get_caller_info(#jid{user = User, server = Server} = From) ->
|
||||
#{tag => <<>>,
|
||||
usr => {User, Server, <<"">>},
|
||||
caller_server => Server,
|
||||
ip => get_ip_address(From),
|
||||
caller_module => ?MODULE}.
|
||||
|
||||
get_ip_address(#jid{user = User,
|
||||
server = Server,
|
||||
resource = Resource}) ->
|
||||
case ejabberd_sm:get_user_ip(User, Server, Resource) of
|
||||
{IP, _Port} when is_tuple(IP) ->
|
||||
IP;
|
||||
_ ->
|
||||
error_ip_address
|
||||
end.
|
||||
|
||||
%%%==================================
|
||||
%%%% - get form
|
||||
|
||||
get_form_api_command(NameBin, Host, _Lang) ->
|
||||
Def = get_api_command(NameBin, Host),
|
||||
Title = list_to_binary(atom_to_list(Def#ejabberd_commands.name)),
|
||||
Instructions = get_instructions(Def),
|
||||
FieldsArgs =
|
||||
build_fields(Def#ejabberd_commands.args,
|
||||
Def#ejabberd_commands.args_desc,
|
||||
Def#ejabberd_commands.args_example,
|
||||
Def#ejabberd_commands.policy,
|
||||
get_replacements(Host),
|
||||
true),
|
||||
FieldsArgsWithHeads =
|
||||
case FieldsArgs of
|
||||
[] ->
|
||||
[];
|
||||
_ ->
|
||||
[#xdata_field{type = fixed, label = ?T("Arguments")} | FieldsArgs]
|
||||
end,
|
||||
NodeFields = build_node_fields(),
|
||||
{result,
|
||||
#xdata{title = Title,
|
||||
type = form,
|
||||
instructions = Instructions,
|
||||
fields = FieldsArgsWithHeads ++ NodeFields}}.
|
||||
|
||||
get_replacements(Host) ->
|
||||
[{user, <<"">>},
|
||||
{localuser, <<"">>},
|
||||
{host, Host},
|
||||
{localhost, Host},
|
||||
{password, <<"">>},
|
||||
{newpass, <<"">>},
|
||||
{service, mod_muc_admin:find_hosts(Host)}].
|
||||
|
||||
build_node_fields() ->
|
||||
build_node_fields([node() | nodes()]).
|
||||
|
||||
build_node_fields([_ThisNode]) ->
|
||||
[];
|
||||
build_node_fields(AtomNodes) ->
|
||||
[ThisNode | _] = Nodes = [atom_to_binary(Atom, latin1) || Atom <- AtomNodes],
|
||||
Options = [#xdata_option{label = N, value = N} || N <- Nodes],
|
||||
[#xdata_field{type = fixed, label = ?T("Clustering")},
|
||||
#xdata_field{type = 'list-single',
|
||||
label = <<"ejabberd node">>,
|
||||
var = <<"mod_adhoc_api_target_node">>,
|
||||
values = [ThisNode],
|
||||
options = Options}].
|
||||
|
||||
%%%==================================
|
||||
%%%% - set form
|
||||
|
||||
set_form_api_command(From, Host, CommandNameBin, XData, _Lang) ->
|
||||
%% Description
|
||||
Def = get_api_command(CommandNameBin, Host),
|
||||
Title = list_to_binary(atom_to_list(Def#ejabberd_commands.name)),
|
||||
Instructions = get_instructions(Def),
|
||||
|
||||
%% Arguments
|
||||
FieldsArgs1 = [Field || Field <- XData#xdata.fields, Field#xdata_field.type /= fixed],
|
||||
|
||||
{Node, FieldsArgs} =
|
||||
case lists:keytake(<<"mod_adhoc_api_target_node">>, #xdata_field.var, FieldsArgs1) of
|
||||
{value, #xdata_field{values = [TargetNode]}, FAs} ->
|
||||
{binary_to_existing_atom(TargetNode, latin1), FAs};
|
||||
false ->
|
||||
{node(), FieldsArgs1}
|
||||
end,
|
||||
|
||||
FieldsArgsWithHeads =
|
||||
case FieldsArgs of
|
||||
[] ->
|
||||
[];
|
||||
_ ->
|
||||
[#xdata_field{type = fixed, label = ?T("Arguments")} | FieldsArgs]
|
||||
end,
|
||||
|
||||
%% Execute
|
||||
Arguments = api_extract_fields(FieldsArgs, Def#ejabberd_commands.args),
|
||||
ApiVersion = mod_adhoc_api_opt:default_version(Host),
|
||||
CallResult =
|
||||
ejabberd_cluster:call(Node,
|
||||
mod_http_api,
|
||||
handle,
|
||||
[binary_to_existing_atom(CommandNameBin, latin1),
|
||||
get_caller_info(From),
|
||||
Arguments,
|
||||
ApiVersion]),
|
||||
|
||||
%% Command result
|
||||
FieldsResult2 =
|
||||
case CallResult of
|
||||
{200, RR} ->
|
||||
build_fields([Def#ejabberd_commands.result],
|
||||
[Def#ejabberd_commands.result_desc],
|
||||
[RR],
|
||||
restricted,
|
||||
[{host, Host}],
|
||||
false);
|
||||
{Code, _ApiErrorCode, MessageBin} ->
|
||||
[#xdata_field{type = 'text-single',
|
||||
label = <<"Error ", (integer_to_binary(Code))/binary>>,
|
||||
values = encode(MessageBin, irrelevat_type),
|
||||
var = <<"error">>}];
|
||||
{Code, MessageBin} ->
|
||||
[#xdata_field{type = 'text-single',
|
||||
label = <<"Error ", (integer_to_binary(Code))/binary>>,
|
||||
values = encode(MessageBin, irrelevat_type),
|
||||
var = <<"error">>}]
|
||||
end,
|
||||
FieldsResultWithHeads =
|
||||
[#xdata_field{type = fixed, label = ?T("")},
|
||||
#xdata_field{type = fixed, label = ?T("Result")}
|
||||
| FieldsResult2],
|
||||
|
||||
%% Result stanza
|
||||
{result,
|
||||
#xdata{title = Title,
|
||||
type = result,
|
||||
instructions = Instructions,
|
||||
fields = FieldsArgsWithHeads ++ FieldsResultWithHeads}}.
|
||||
|
||||
api_extract_fields(Fields, ArgsDef) ->
|
||||
lists:map(fun(#xdata_field{values = Values, var = ANameBin}) ->
|
||||
ArgDef = proplists:get_value(binary_to_existing_atom(ANameBin, latin1), ArgsDef),
|
||||
V = case {Values, ArgDef} of
|
||||
{Values, {list, {_ElementName, {tuple, ElementsDef}}}} ->
|
||||
[parse_tuple(ElementsDef, Value) || Value <- Values];
|
||||
{[Value], {tuple, ElementsDef}} ->
|
||||
parse_tuple(ElementsDef, Value);
|
||||
{[Value], _} ->
|
||||
Value;
|
||||
_ ->
|
||||
Values
|
||||
end,
|
||||
{ANameBin, V}
|
||||
end,
|
||||
Fields).
|
||||
|
||||
parse_tuple(ElementsDef, Value) ->
|
||||
Values = str:tokens(Value, <<":">>),
|
||||
List1 =
|
||||
[{atom_to_binary(Name, latin1), Val}
|
||||
|| {{Name, _Type}, Val} <- lists:zip(ElementsDef, Values)],
|
||||
maps:from_list(List1).
|
||||
|
||||
%%%==================================
|
||||
%%%% - get instructions
|
||||
|
||||
get_instructions(Def) ->
|
||||
Note2 =
|
||||
case Def#ejabberd_commands.note of
|
||||
[] ->
|
||||
[];
|
||||
Note ->
|
||||
N = iolist_to_binary(Note),
|
||||
[<<"Note: ", N/binary>>]
|
||||
end,
|
||||
Tags2 =
|
||||
case Def#ejabberd_commands.tags of
|
||||
[] ->
|
||||
[];
|
||||
Tags ->
|
||||
T = str:join([atom_to_binary(Tag, latin1) || Tag <- Tags], <<", ">>),
|
||||
[<<"Tags: ", T/binary>>]
|
||||
end,
|
||||
Module2 =
|
||||
case Def#ejabberd_commands.definer of
|
||||
unknown ->
|
||||
[];
|
||||
DefinerAtom ->
|
||||
D = atom_to_binary(DefinerAtom, latin1),
|
||||
[<<"Module: ", D/binary>>]
|
||||
end,
|
||||
Version2 =
|
||||
case Def#ejabberd_commands.version of
|
||||
0 ->
|
||||
[];
|
||||
Version ->
|
||||
V = integer_to_binary(Version),
|
||||
[<<"API version: ", V/binary>>]
|
||||
end,
|
||||
get_instructions2([Def#ejabberd_commands.desc, Def#ejabberd_commands.longdesc]
|
||||
++ Note2
|
||||
++ Tags2
|
||||
++ Module2
|
||||
++ Version2).
|
||||
|
||||
get_instructions2(ListStrings) ->
|
||||
[re:replace(String, "[\t]*[ ]+", " ", [{return, binary}, global])
|
||||
|| String <- ListStrings, String /= ""].
|
||||
|
||||
%%%==================================
|
||||
%%%% - build fields
|
||||
|
||||
build_fields(NameTypes, none, Examples, Policy, Replacements, Required) ->
|
||||
build_fields(NameTypes, [], Examples, Policy, Replacements, Required);
|
||||
build_fields(NameTypes, Descs, none, Policy, Replacements, Required) ->
|
||||
build_fields(NameTypes, Descs, [], Policy, Replacements, Required);
|
||||
build_fields(NameTypes, [none], Examples, Policy, Replacements, Required) ->
|
||||
build_fields(NameTypes, [], Examples, Policy, Replacements, Required);
|
||||
build_fields(NameTypes, Descs, [none], Policy, Replacements, Required) ->
|
||||
build_fields(NameTypes, Descs, [], Policy, Replacements, Required);
|
||||
build_fields(NameTypes, Descs, Examples, Policy, Replacements, Required) ->
|
||||
{NameTypes2, Descs2, Examples2} =
|
||||
case Policy of
|
||||
user ->
|
||||
{[{user, binary}, {host, binary} | NameTypes],
|
||||
["Username", "Server host" | Descs],
|
||||
["tom", "example.com" | Examples]};
|
||||
_ ->
|
||||
{NameTypes, Descs, Examples}
|
||||
end,
|
||||
build_fields2(NameTypes2, Descs2, Examples2, Replacements, Required).
|
||||
|
||||
build_fields2([{_ArgName, {list, _ArgNameType}}] = NameTypes,
|
||||
Descs,
|
||||
Examples,
|
||||
_Replacements,
|
||||
Required) ->
|
||||
Args = lists_zip3_pad(NameTypes, Descs, Examples),
|
||||
lists:map(fun({{AName, AType}, ADesc, AExample}) ->
|
||||
ANameBin = list_to_binary(atom_to_list(AName)),
|
||||
#xdata_field{type = 'text-multi',
|
||||
label = ANameBin,
|
||||
desc = list_to_binary(ADesc),
|
||||
values = encode(AExample, AType),
|
||||
required = Required,
|
||||
var = ANameBin}
|
||||
end,
|
||||
Args);
|
||||
build_fields2(NameTypes, Descs, Examples, Replacements, Required) ->
|
||||
Args = lists_zip3_pad(NameTypes, Descs, Examples),
|
||||
lists:map(fun({{AName, AType}, ADesc, AExample}) ->
|
||||
ANameBin = list_to_binary(atom_to_list(AName)),
|
||||
AValue = proplists:get_value(AName, Replacements, AExample),
|
||||
Values = encode(AValue, AType),
|
||||
Type =
|
||||
case {AType, Values} of
|
||||
{{list, _}, _} ->
|
||||
'text-multi';
|
||||
{string, [_, _ | _]} ->
|
||||
'text-multi';
|
||||
_ ->
|
||||
'text-single'
|
||||
end,
|
||||
#xdata_field{type = Type,
|
||||
label = ANameBin,
|
||||
desc = make_desc(ADesc, AValue),
|
||||
values = Values,
|
||||
required = Required,
|
||||
var = ANameBin}
|
||||
end,
|
||||
Args).
|
||||
|
||||
-ifdef(OTP_BELOW_26).
|
||||
|
||||
lists_zip3_pad(As, Bs, Cs) ->
|
||||
lists_zip3_pad(As, Bs, Cs, []).
|
||||
|
||||
lists_zip3_pad([A | As], [B | Bs], [C | Cs], Xs) ->
|
||||
lists_zip3_pad(As, Bs, Cs, [{A, B, C} | Xs]);
|
||||
lists_zip3_pad([A | As], [B | Bs], Nil, Xs) when (Nil == none) or (Nil == []) ->
|
||||
lists_zip3_pad(As, Bs, [], [{A, B, ""} | Xs]);
|
||||
lists_zip3_pad([A | As], Nil, [C | Cs], Xs) when (Nil == none) or (Nil == []) ->
|
||||
lists_zip3_pad(As, [], Cs, [{A, "", C} | Xs]);
|
||||
lists_zip3_pad([A | As], Nil, Nil, Xs) when (Nil == none) or (Nil == []) ->
|
||||
lists_zip3_pad(As, [], [], [{A, "", ""} | Xs]);
|
||||
lists_zip3_pad([], Nil, Nil, Xs) when (Nil == none) or (Nil == []) ->
|
||||
lists:reverse(Xs).
|
||||
|
||||
-else.
|
||||
|
||||
lists_zip3_pad(As, Bs, Cs) ->
|
||||
lists:zip3(As, Bs, Cs, {pad, {error_missing_args_def, "", ""}}).
|
||||
|
||||
-endif.
|
||||
|
||||
make_desc(ADesc, T) when is_tuple(T) ->
|
||||
T3 = string:join(tuple_to_list(T), " : "),
|
||||
iolist_to_binary([ADesc, " {", T3, "}"]);
|
||||
make_desc(ADesc, M) when is_map(M) ->
|
||||
M2 = [binary_to_list(V) || V <- maps:keys(M)],
|
||||
M3 = string:join(M2, " : "),
|
||||
iolist_to_binary([ADesc, " {", M3, "}"]);
|
||||
make_desc(ADesc, _M) ->
|
||||
iolist_to_binary(ADesc).
|
||||
|
||||
%%%==================================
|
||||
%%%% - encode
|
||||
|
||||
encode({[T | _] = List}, Type) when is_tuple(T) ->
|
||||
encode(List, Type);
|
||||
encode([T | _] = List, Type) when is_tuple(T) ->
|
||||
[encode(Element, Type) || Element <- List];
|
||||
encode(T, _Type) when is_tuple(T) ->
|
||||
T2 = [x_to_binary(E) || E <- tuple_to_list(T)],
|
||||
T3 = str:join(T2, <<":">>),
|
||||
[T3];
|
||||
encode(M, {tuple, Types}) when is_map(M) ->
|
||||
M2 = [x_to_list(maps:get(atom_to_binary(Key, latin1), M))
|
||||
|| {Key, _ElementType} <- Types],
|
||||
M3 = string:join(M2, " : "),
|
||||
[iolist_to_binary(M3)];
|
||||
encode([S | _] = SList, _Type) when is_list(S) ->
|
||||
[iolist_to_binary(A) || A <- SList];
|
||||
encode([B | _] = BList, _Type) when is_binary(B) ->
|
||||
BList;
|
||||
encode(I, _Type) when is_integer(I) ->
|
||||
[integer_to_binary(I)];
|
||||
encode([M | _] = List, {list, {_Name, TupleType}}) when is_map(M) ->
|
||||
[encode(M1, TupleType) || M1 <- List];
|
||||
encode(S, _Type) when is_list(S) ->
|
||||
[iolist_to_binary(S)];
|
||||
encode(B, _Type) when is_binary(B) ->
|
||||
str:tokens(B, <<"\n">>).
|
||||
|
||||
x_to_list(B) when is_binary(B) ->
|
||||
binary_to_list(B);
|
||||
x_to_list(I) when is_integer(I) ->
|
||||
integer_to_list(I);
|
||||
x_to_list(L) when is_list(L) ->
|
||||
L.
|
||||
|
||||
x_to_binary(B) when is_binary(B) ->
|
||||
B;
|
||||
x_to_binary(I) when is_integer(I) ->
|
||||
integer_to_binary(I);
|
||||
x_to_binary(L) when is_list(L) ->
|
||||
iolist_to_binary(L).
|
||||
|
||||
%%%==================================
|
||||
|
||||
%%% vim: set foldmethod=marker foldmarker=%%%%,%%%=:
|
13
src/mod_adhoc_api_opt.erl
Normal file
13
src/mod_adhoc_api_opt.erl
Normal file
|
@ -0,0 +1,13 @@
|
|||
%% Generated automatically
|
||||
%% DO NOT EDIT: run `make options` instead
|
||||
|
||||
-module(mod_adhoc_api_opt).
|
||||
|
||||
-export([default_version/1]).
|
||||
|
||||
-spec default_version(gen_mod:opts() | global | binary()) -> integer().
|
||||
default_version(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(default_version, Opts);
|
||||
default_version(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_adhoc_api, default_version).
|
||||
|
|
@ -104,8 +104,8 @@
|
|||
%%% gen_mod
|
||||
%%%
|
||||
|
||||
start(_Host, _Opts) ->
|
||||
ejabberd_commands:register_commands(?MODULE, get_commands_spec()),
|
||||
start(Host, _Opts) ->
|
||||
ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()),
|
||||
{ok, [{hook, webadmin_menu_main, web_menu_main, 50, global},
|
||||
{hook, webadmin_page_main, web_page_main, 50, global},
|
||||
{hook, webadmin_menu_host, web_menu_host, 50},
|
||||
|
@ -118,12 +118,7 @@ start(_Host, _Opts) ->
|
|||
{hook, webadmin_page_node, web_page_node, 50, global}]}.
|
||||
|
||||
stop(Host) ->
|
||||
case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
|
||||
false ->
|
||||
ejabberd_commands:unregister_commands(get_commands_spec());
|
||||
true ->
|
||||
ok
|
||||
end.
|
||||
ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()).
|
||||
|
||||
reload(_Host, _NewOpts, _OldOpts) ->
|
||||
ok.
|
||||
|
|
|
@ -47,11 +47,11 @@
|
|||
%%% gen_mod
|
||||
%%%
|
||||
|
||||
start(_Host, _Opts) ->
|
||||
ejabberd_commands:register_commands(?MODULE, get_commands_spec()).
|
||||
start(Host, _Opts) ->
|
||||
ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()).
|
||||
|
||||
stop(_Host) ->
|
||||
ejabberd_commands:unregister_commands(get_commands_spec()).
|
||||
stop(Host) ->
|
||||
ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()).
|
||||
|
||||
reload(_Host, _NewOpts, _OldOpts) ->
|
||||
ok.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
%%%----------------------------------------------------------------------
|
||||
%%% File : mod_configure.erl
|
||||
%%% Author : Alexey Shchepin <alexey@process-one.net>
|
||||
%%% Purpose : Support for online configuration of ejabberd
|
||||
%%% Purpose : Support for online configuration of ejabberd using XEP-0050
|
||||
%%% Created : 19 Jan 2003 by Alexey Shchepin <alexey@process-one.net>
|
||||
%%%
|
||||
%%%
|
||||
|
@ -36,6 +36,7 @@
|
|||
adhoc_local_items/4, adhoc_local_commands/4,
|
||||
get_sm_identity/5, get_sm_features/5, get_sm_items/5,
|
||||
adhoc_sm_items/4, adhoc_sm_commands/4, mod_options/1,
|
||||
mod_opt_type/1,
|
||||
depends/2, mod_doc/0]).
|
||||
|
||||
-include("logger.hrl").
|
||||
|
@ -92,6 +93,10 @@ depends(_Host, _Opts) ->
|
|||
-spec tokenize(binary()) -> [binary()].
|
||||
tokenize(Node) -> str:tokens(Node, <<"/#">>).
|
||||
|
||||
acl_match_rule(Host, From) ->
|
||||
Access = mod_configure_opt:access(Host),
|
||||
acl:match_rule(Host, Access, From).
|
||||
|
||||
-spec get_sm_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()].
|
||||
get_sm_identity(Acc, _From, _To, Node, Lang) ->
|
||||
case Node of
|
||||
|
@ -167,7 +172,7 @@ get_sm_features(Acc, From,
|
|||
case gen_mod:is_loaded(LServer, mod_adhoc) of
|
||||
false -> Acc;
|
||||
_ ->
|
||||
Allow = acl:match_rule(LServer, configure, From),
|
||||
Allow = acl_match_rule(LServer, From),
|
||||
case Node of
|
||||
<<"config">> -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang);
|
||||
_ -> Acc
|
||||
|
@ -182,7 +187,7 @@ get_local_features(Acc, From,
|
|||
false -> Acc;
|
||||
_ ->
|
||||
LNode = tokenize(Node),
|
||||
Allow = acl:match_rule(LServer, configure, From),
|
||||
Allow = acl_match_rule(LServer, From),
|
||||
case LNode of
|
||||
[<<"config">>] -> ?INFO_RESULT(Allow, [], Lang);
|
||||
[<<"user">>] -> ?INFO_RESULT(Allow, [], Lang);
|
||||
|
@ -240,7 +245,7 @@ get_local_features(Acc, From,
|
|||
jid(), jid(), binary()) -> mod_disco:items_acc().
|
||||
adhoc_sm_items(Acc, From, #jid{lserver = LServer} = To,
|
||||
Lang) ->
|
||||
case acl:match_rule(LServer, configure, From) of
|
||||
case acl_match_rule(LServer, From) of
|
||||
allow ->
|
||||
Items = case Acc of
|
||||
{result, Its} -> Its;
|
||||
|
@ -266,7 +271,7 @@ get_sm_items(Acc, From,
|
|||
{result, Its} -> Its;
|
||||
empty -> []
|
||||
end,
|
||||
case {acl:match_rule(LServer, configure, From), Node} of
|
||||
case {acl_match_rule(LServer, From), Node} of
|
||||
{allow, <<"">>} ->
|
||||
Nodes = [?NODEJID(To, ?T("Configuration"),
|
||||
<<"config">>),
|
||||
|
@ -295,13 +300,13 @@ get_user_resources(User, Server) ->
|
|||
jid(), jid(), binary()) -> mod_disco:items_acc().
|
||||
adhoc_local_items(Acc, From,
|
||||
#jid{lserver = LServer, server = Server} = To, Lang) ->
|
||||
case acl:match_rule(LServer, configure, From) of
|
||||
case acl_match_rule(LServer, From) of
|
||||
allow ->
|
||||
Items = case Acc of
|
||||
{result, Its} -> Its;
|
||||
empty -> []
|
||||
end,
|
||||
PermLev = get_permission_level(From),
|
||||
PermLev = get_permission_level(From, LServer),
|
||||
Nodes = recursively_get_local_items(PermLev, LServer,
|
||||
<<"">>, Server, Lang),
|
||||
Nodes1 = lists:filter(
|
||||
|
@ -348,9 +353,10 @@ recursively_get_local_items(PermLev, LServer, Node,
|
|||
end,
|
||||
Items)).
|
||||
|
||||
-spec get_permission_level(jid()) -> global | vhost.
|
||||
get_permission_level(JID) ->
|
||||
case acl:match_rule(global, configure, JID) of
|
||||
-spec get_permission_level(jid(), binary()) -> global | vhost.
|
||||
get_permission_level(JID, Host) ->
|
||||
Access = mod_configure_opt:access(Host),
|
||||
case acl:match_rule(global, Access, JID) of
|
||||
allow -> global;
|
||||
deny -> vhost
|
||||
end.
|
||||
|
@ -361,7 +367,7 @@ get_permission_level(JID) ->
|
|||
case Allow of
|
||||
deny -> Fallback;
|
||||
allow ->
|
||||
PermLev = get_permission_level(From),
|
||||
PermLev = get_permission_level(From, LServer),
|
||||
case get_local_items({PermLev, LServer}, LNode,
|
||||
jid:encode(To), Lang)
|
||||
of
|
||||
|
@ -381,11 +387,11 @@ get_local_items(Acc, From, #jid{lserver = LServer} = To,
|
|||
{result, Its} -> Its;
|
||||
empty -> []
|
||||
end,
|
||||
Allow = acl:match_rule(LServer, configure, From),
|
||||
Allow = acl_match_rule(LServer, From),
|
||||
case Allow of
|
||||
deny -> {result, Items};
|
||||
allow ->
|
||||
PermLev = get_permission_level(From),
|
||||
PermLev = get_permission_level(From, LServer),
|
||||
case get_local_items({PermLev, LServer}, [],
|
||||
jid:encode(To), Lang)
|
||||
of
|
||||
|
@ -400,7 +406,7 @@ get_local_items(Acc, From, #jid{lserver = LServer} = To,
|
|||
false -> Acc;
|
||||
_ ->
|
||||
LNode = tokenize(Node),
|
||||
Allow = acl:match_rule(LServer, configure, From),
|
||||
Allow = acl_match_rule(LServer, From),
|
||||
Err = xmpp:err_forbidden(?T("Access denied by service policy"), Lang),
|
||||
case LNode of
|
||||
[<<"config">>] ->
|
||||
|
@ -690,7 +696,7 @@ get_stopped_nodes(_Lang) ->
|
|||
|
||||
-define(COMMANDS_RESULT(LServerOrGlobal, From, To,
|
||||
Request, Lang),
|
||||
case acl:match_rule(LServerOrGlobal, configure, From) of
|
||||
case acl_match_rule(LServerOrGlobal, From) of
|
||||
deny -> {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)};
|
||||
allow -> adhoc_local_commands(From, To, Request)
|
||||
end).
|
||||
|
@ -1268,7 +1274,7 @@ set_form(From, Host, ?NS_ADMINL(<<"add-user">>), _Lang,
|
|||
Server = AccountJID#jid.lserver,
|
||||
true = lists:member(Server, ejabberd_option:hosts()),
|
||||
true = Server == Host orelse
|
||||
get_permission_level(From) == global,
|
||||
get_permission_level(From, Host) == global,
|
||||
case ejabberd_auth:try_register(User, Server, Password) of
|
||||
ok -> {result, undefined};
|
||||
{error, exists} -> {error, xmpp:err_conflict()};
|
||||
|
@ -1284,7 +1290,7 @@ set_form(From, Host, ?NS_ADMINL(<<"delete-user">>),
|
|||
User = JID#jid.luser,
|
||||
Server = JID#jid.lserver,
|
||||
true = Server == Host orelse
|
||||
get_permission_level(From) == global,
|
||||
get_permission_level(From, Host) == global,
|
||||
true = ejabberd_auth:user_exists(User, Server),
|
||||
{User, Server}
|
||||
end,
|
||||
|
@ -1298,7 +1304,7 @@ set_form(From, Host, ?NS_ADMINL(<<"end-user-session">>),
|
|||
JID = jid:decode(AccountString),
|
||||
LServer = JID#jid.lserver,
|
||||
true = LServer == Host orelse
|
||||
get_permission_level(From) == global,
|
||||
get_permission_level(From, Host) == global,
|
||||
case JID#jid.lresource of
|
||||
<<>> ->
|
||||
ejabberd_sm:kick_user(JID#jid.luser, JID#jid.lserver);
|
||||
|
@ -1314,7 +1320,7 @@ set_form(From, Host,
|
|||
User = JID#jid.luser,
|
||||
Server = JID#jid.lserver,
|
||||
true = Server == Host orelse
|
||||
get_permission_level(From) == global,
|
||||
get_permission_level(From, Host) == global,
|
||||
true = ejabberd_auth:user_exists(User, Server),
|
||||
ejabberd_auth:set_password(User, Server, Password),
|
||||
{result, undefined};
|
||||
|
@ -1325,7 +1331,7 @@ set_form(From, Host,
|
|||
User = JID#jid.luser,
|
||||
Server = JID#jid.lserver,
|
||||
true = Server == Host orelse
|
||||
get_permission_level(From) == global,
|
||||
get_permission_level(From, Host) == global,
|
||||
FLast = case ejabberd_sm:get_user_resources(User,
|
||||
Server)
|
||||
of
|
||||
|
@ -1357,7 +1363,7 @@ set_form(From, Host, ?NS_ADMINL(<<"user-stats">>), Lang,
|
|||
User = JID#jid.luser,
|
||||
Server = JID#jid.lserver,
|
||||
true = Server == Host orelse
|
||||
get_permission_level(From) == global,
|
||||
get_permission_level(From, Host) == global,
|
||||
Resources = ejabberd_sm:get_user_resources(User,
|
||||
Server),
|
||||
IPs1 = [ejabberd_sm:get_user_ip(User, Server, Resource)
|
||||
|
@ -1448,7 +1454,7 @@ adhoc_sm_commands(_Acc, From,
|
|||
#jid{user = User, server = Server, lserver = LServer},
|
||||
#adhoc_command{lang = Lang, node = <<"config">>,
|
||||
action = Action, xdata = XData} = Request) ->
|
||||
case acl:match_rule(LServer, configure, From) of
|
||||
case acl_match_rule(LServer, From) of
|
||||
deny ->
|
||||
{error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)};
|
||||
allow ->
|
||||
|
@ -1530,12 +1536,76 @@ set_sm_form(_User, _Server, _Node, _Request) ->
|
|||
tr(Lang, Text) ->
|
||||
translate:translate(Lang, Text).
|
||||
|
||||
mod_options(_) -> [].
|
||||
-spec mod_opt_type(atom()) -> econf:validator().
|
||||
mod_opt_type(access) ->
|
||||
econf:acl().
|
||||
|
||||
-spec mod_options(binary()) -> [{services, [tuple()]} | {atom(), any()}].
|
||||
mod_options(_Host) ->
|
||||
[{access, configure}].
|
||||
|
||||
%% @format-begin
|
||||
|
||||
%% All ad-hoc commands implemented by mod_configure are available as API Commands:
|
||||
%% - add-user -> register
|
||||
%% - delete-user -> unregister
|
||||
%% - end-user-session -> kick_session / kick_user
|
||||
%% - change-user-password -> change_password
|
||||
%% - get-user-lastlogin -> get_last
|
||||
%% - user-stats -> user_sessions_info
|
||||
%% - get-registered-users-list -> registered_users
|
||||
%% - get-registered-users-num -> stats
|
||||
%% - get-online-users-list -> connected_users
|
||||
%% - get-online-users-num -> stats
|
||||
%% - stopped nodes -> list_cluster_detailed
|
||||
%% - DB -> mnesia_list_tables and mnesia_table_change_storage
|
||||
%% - restart -> stop_kindly / restart
|
||||
%% - shutdown -> stop_kindly
|
||||
%% - backup -> backup
|
||||
%% - restore -> restore
|
||||
%% - textfile -> dump
|
||||
%% - import/file -> import_file
|
||||
%% - import/dir -> import_dir
|
||||
%%
|
||||
%% An exclusive feature available only in this module is to list items and discover them:
|
||||
%% - outgoing s2s
|
||||
%% - online users
|
||||
%% - all users
|
||||
|
||||
mod_doc() ->
|
||||
#{desc =>
|
||||
?T("The module provides server configuration functionality via "
|
||||
"https://xmpp.org/extensions/xep-0050.html[XEP-0050: Ad-Hoc Commands]. "
|
||||
"Implements many commands as defined in "
|
||||
"https://xmpp.org/extensions/xep-0133.html[XEP-0133: Service Administration]. "
|
||||
"This module requires _`mod_adhoc`_ to be loaded.")}.
|
||||
[?T("The module provides server configuration functionalities using "
|
||||
"https://xmpp.org/extensions/xep-0030.html[XEP-0030: Service Discovery] and "
|
||||
"https://xmpp.org/extensions/xep-0050.html[XEP-0050: Ad-Hoc Commands]:"),
|
||||
"",
|
||||
"- List and discover outgoing s2s, online client sessions and all registered accounts",
|
||||
"- Most of the ad-hoc commands defined in https://xmpp.org/extensions/xep-0133.html[XEP-0133: Service Administration]",
|
||||
"- Additional custom ad-hoc commands specific to ejabberd",
|
||||
"",
|
||||
?T("This module requires _`mod_adhoc`_ (to execute the commands), "
|
||||
"and recommends _`mod_disco`_ (to discover the commands). "),
|
||||
"",
|
||||
?T("Please notice that all the ad-hoc commands implemented by this module "
|
||||
"have an equivalent "
|
||||
"https://docs.ejabberd.im/developer/ejabberd-api/[API Command] "
|
||||
"that you can execute using _`mod_adhoc_api`_ or any other API frontend.")],
|
||||
opts =>
|
||||
[{access,
|
||||
#{value => ?T("AccessName"),
|
||||
note => "added in 25.xx",
|
||||
desc =>
|
||||
?T("This option defines which access rule will be used to "
|
||||
"control who is allowed to access the features provided by this module. "
|
||||
"The default value is 'configure'.")}}],
|
||||
example =>
|
||||
["acl:",
|
||||
" admin:",
|
||||
" user: sun@localhost",
|
||||
"",
|
||||
"access_rules:",
|
||||
" configure:",
|
||||
" allow: admin",
|
||||
"",
|
||||
"modules:",
|
||||
" mod_configure:",
|
||||
" access: configure"]}.
|
||||
|
|
13
src/mod_configure_opt.erl
Normal file
13
src/mod_configure_opt.erl
Normal file
|
@ -0,0 +1,13 @@
|
|||
%% Generated automatically
|
||||
%% DO NOT EDIT: run `make options` instead
|
||||
|
||||
-module(mod_configure_opt).
|
||||
|
||||
-export([access/1]).
|
||||
|
||||
-spec access(gen_mod:opts() | global | binary()) -> 'configure' | acl:acl().
|
||||
access(Opts) when is_map(Opts) ->
|
||||
gen_mod:get_opt(access, Opts);
|
||||
access(Host) ->
|
||||
gen_mod:get_module_opt(Host, mod_configure, access).
|
||||
|
|
@ -107,16 +107,11 @@ c2s_stream_started(#{ip := {Addr, _}} = State, _) ->
|
|||
start(Host, Opts) ->
|
||||
catch ets:new(failed_auth, [named_table, public,
|
||||
{heir, erlang:group_leader(), none}]),
|
||||
ejabberd_commands:register_commands(?MODULE, get_commands_spec()),
|
||||
ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()),
|
||||
gen_mod:start_child(?MODULE, Host, Opts).
|
||||
|
||||
stop(Host) ->
|
||||
case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
|
||||
false ->
|
||||
ejabberd_commands:unregister_commands(get_commands_spec());
|
||||
true ->
|
||||
ok
|
||||
end,
|
||||
ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()),
|
||||
gen_mod:stop_child(?MODULE, Host).
|
||||
|
||||
reload(_Host, _NewOpts, _OldOpts) ->
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
-behaviour(gen_mod).
|
||||
|
||||
-export([start/2, stop/1, reload/3, process/2, depends/2,
|
||||
format_arg/2,
|
||||
format_arg/2, handle/4,
|
||||
mod_opt_type/1, mod_options/1, mod_doc/0]).
|
||||
|
||||
-include_lib("xmpp/include/xmpp.hrl").
|
||||
|
@ -353,6 +353,9 @@ format_arg(Elements,
|
|||
|| Element <- Elements];
|
||||
|
||||
%% Covered by command_test_list and command_test_list_tuple
|
||||
format_arg(Element, {list, Def})
|
||||
when not is_list(Element) ->
|
||||
format_arg([Element], {list, Def});
|
||||
format_arg(Elements,
|
||||
{list, {_ElementDefName, ElementDefFormat}})
|
||||
when is_list(Elements) ->
|
||||
|
@ -395,6 +398,7 @@ format_arg(Elements, {list, ElementsDef})
|
|||
|| Element <- Elements];
|
||||
|
||||
format_arg(Arg, integer) when is_integer(Arg) -> Arg;
|
||||
format_arg(Arg, integer) when is_binary(Arg) -> binary_to_integer(Arg);
|
||||
format_arg(Arg, binary) when is_list(Arg) -> process_unicode_codepoints(Arg);
|
||||
format_arg(Arg, binary) when is_binary(Arg) -> Arg;
|
||||
format_arg(Arg, string) when is_list(Arg) -> Arg;
|
||||
|
@ -460,6 +464,9 @@ format_result([String | _] = StringList, {Name, string}) when is_list(String) ->
|
|||
format_result(String, {Name, string}) ->
|
||||
{misc:atom_to_binary(Name), iolist_to_binary(String)};
|
||||
|
||||
format_result(Binary, {Name, binary}) ->
|
||||
{misc:atom_to_binary(Name), Binary};
|
||||
|
||||
format_result(Code, {Name, rescode}) ->
|
||||
{misc:atom_to_binary(Name), Code == true orelse Code == ok};
|
||||
|
||||
|
@ -473,14 +480,17 @@ format_result(Code, {Name, restuple}) ->
|
|||
{[{<<"res">>, Code == true orelse Code == ok},
|
||||
{<<"text">>, <<"">>}]}};
|
||||
|
||||
format_result(Els, {Name, {list, {_, {tuple, [{_, atom}, _]}} = Fmt}}) ->
|
||||
format_result(Els1, {Name, {list, {_, {tuple, [{_, atom}, _]}} = Fmt}}) ->
|
||||
Els = lists:keysort(1, Els1),
|
||||
{misc:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}};
|
||||
|
||||
format_result(Els, {Name, {list, {_, {tuple, [{name, string}, {value, _}]}} = Fmt}}) ->
|
||||
format_result(Els1, {Name, {list, {_, {tuple, [{name, string}, {value, _}]}} = Fmt}}) ->
|
||||
Els = lists:keysort(1, Els1),
|
||||
{misc:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}};
|
||||
|
||||
%% Covered by command_test_list and command_test_list_tuple
|
||||
format_result(Els, {Name, {list, Def}}) ->
|
||||
format_result(Els1, {Name, {list, Def}}) ->
|
||||
Els = lists:sort(Els1),
|
||||
{misc:atom_to_binary(Name), [element(2, format_result(El, Def)) || El <- Els]};
|
||||
|
||||
format_result(Tuple, {_Name, {tuple, [{_, atom}, ValFmt]}}) ->
|
||||
|
|
|
@ -182,7 +182,7 @@ start(Host, Opts) ->
|
|||
ejabberd_hooks:add(check_create_room, Host, ?MODULE,
|
||||
check_create_room, 50)
|
||||
end,
|
||||
ejabberd_commands:register_commands(?MODULE, get_commands_spec()),
|
||||
ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()),
|
||||
ok;
|
||||
Err ->
|
||||
Err
|
||||
|
@ -263,12 +263,7 @@ stop(Host) ->
|
|||
ejabberd_hooks:delete(check_create_room, Host, ?MODULE,
|
||||
check_create_room, 50)
|
||||
end,
|
||||
case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
|
||||
false ->
|
||||
ejabberd_commands:unregister_commands(get_commands_spec());
|
||||
true ->
|
||||
ok
|
||||
end.
|
||||
ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()).
|
||||
|
||||
reload(Host, NewOpts, OldOpts) ->
|
||||
NewMod = gen_mod:db_mod(NewOpts, ?MODULE),
|
||||
|
|
|
@ -68,8 +68,8 @@
|
|||
%% gen_mod
|
||||
%%----------------------------
|
||||
|
||||
start(_Host, _Opts) ->
|
||||
ejabberd_commands:register_commands(?MODULE, get_commands_spec()),
|
||||
start(Host, _Opts) ->
|
||||
ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()),
|
||||
{ok, [{hook, webadmin_menu_main, web_menu_main, 50, global},
|
||||
{hook, webadmin_page_main, web_page_main, 50, global},
|
||||
{hook, webadmin_menu_host, web_menu_host, 50},
|
||||
|
@ -79,12 +79,7 @@ start(_Host, _Opts) ->
|
|||
]}.
|
||||
|
||||
stop(Host) ->
|
||||
case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
|
||||
false ->
|
||||
ejabberd_commands:unregister_commands(get_commands_spec());
|
||||
true ->
|
||||
ok
|
||||
end.
|
||||
ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()).
|
||||
|
||||
reload(_Host, _NewOpts, _OldOpts) ->
|
||||
ok.
|
||||
|
@ -194,14 +189,20 @@ get_commands_spec() ->
|
|||
#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`. "
|
||||
"The syntax of `subscribers` is: `JID:Nick:Node:Node2:Node3,JID:Nick:Node`.",
|
||||
"Options `affiliations` and `subscribers` are lists of tuples. "
|
||||
"The tuples in the list are separated with `;` and "
|
||||
"the elements in each tuple are separated with `=` "
|
||||
"(until ejabberd 24.12 the separators were `,` and `:` respectively). "
|
||||
"Each subscriber can have one or more nodes. "
|
||||
"In summary, `affiliations` is like `Type1=JID1;Type2=JID2` "
|
||||
"and `subscribers` is like `JID1=Nick1=Node1A=Node1B=Node1C;JID2=Nick2=Node2`.",
|
||||
note = "modified in 25.xx",
|
||||
module = ?MODULE, function = create_room_with_opts,
|
||||
args_desc = ["Room name", "MUC service", "Server host", "List of options"],
|
||||
args_example = ["room1", "conference.example.com", "localhost",
|
||||
[{"members_only","true"},
|
||||
{"affiliations", "owner:bob@example.com,member:peter@example.com"},
|
||||
{"subscribers", "bob@example.com:Bob:messages:subject,anne@example.com:Anne:messages"}]],
|
||||
{"affiliations", "owner=user1@localhost;member=user2@localhost"},
|
||||
{"subscribers", "user3@localhost=User3=messages=subject;user4@localhost=User4=messages"}]],
|
||||
args = [{room, binary}, {service, binary},
|
||||
{host, binary},
|
||||
{options, {list,
|
||||
|
@ -1731,9 +1732,9 @@ format_room_option(OptionString, ValueString) ->
|
|||
lang -> ValueString;
|
||||
pubsub -> ValueString;
|
||||
affiliations ->
|
||||
[parse_affiliation_string(Opt) || Opt <- str:tokens(ValueString, <<",">>)];
|
||||
[parse_affiliation_string(Opt) || Opt <- str:tokens(ValueString, <<";,">>)];
|
||||
subscribers ->
|
||||
[parse_subscription_string(Opt) || Opt <- str:tokens(ValueString, <<",">>)];
|
||||
[parse_subscription_string(Opt) || Opt <- str:tokens(ValueString, <<";,">>)];
|
||||
allow_private_messages_from_visitors when
|
||||
(ValueString == <<"anyone">>) or
|
||||
(ValueString == <<"moderators">>) or
|
||||
|
@ -1765,10 +1766,16 @@ throw_error(O, V) ->
|
|||
|
||||
parse_affiliation_string(String) ->
|
||||
{Type, JidS} = case String of
|
||||
%% Old syntax
|
||||
<<"owner:", Jid/binary>> -> {owner, Jid};
|
||||
<<"admin:", Jid/binary>> -> {admin, Jid};
|
||||
<<"member:", Jid/binary>> -> {member, Jid};
|
||||
<<"outcast:", Jid/binary>> -> {outcast, Jid};
|
||||
%% New syntax
|
||||
<<"owner=", Jid/binary>> -> {owner, Jid};
|
||||
<<"admin=", Jid/binary>> -> {admin, Jid};
|
||||
<<"member=", Jid/binary>> -> {member, Jid};
|
||||
<<"outcast=", Jid/binary>> -> {outcast, Jid};
|
||||
_ -> throw({error, "Invalid 'affiliation'"})
|
||||
end,
|
||||
try jid:decode(JidS) of
|
||||
|
@ -1779,7 +1786,7 @@ parse_affiliation_string(String) ->
|
|||
end.
|
||||
|
||||
parse_subscription_string(String) ->
|
||||
case str:tokens(String, <<":">>) of
|
||||
case str:tokens(String, <<"=:">>) of
|
||||
[_] ->
|
||||
throw({error, "Invalid 'subscribers' - missing nick"});
|
||||
[_, _] ->
|
||||
|
|
|
@ -71,7 +71,7 @@ start(Host, Opts) ->
|
|||
Mod = gen_mod:db_mod(Opts, ?MODULE),
|
||||
Mod:init(Host, Opts),
|
||||
init_cache(Mod, Host, Opts),
|
||||
ejabberd_commands:register_commands(?MODULE, get_commands_spec()),
|
||||
ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()),
|
||||
{ok, [{hook, remove_user, remove_user, 50},
|
||||
{hook, disco_sm_features, get_sm_features, 50},
|
||||
{hook, pubsub_publish_item, pubsub_publish_item, 50},
|
||||
|
@ -82,12 +82,7 @@ start(Host, Opts) ->
|
|||
{iq_handler, ejabberd_sm, ?NS_PRIVATE, process_sm_iq}]}.
|
||||
|
||||
stop(Host) ->
|
||||
case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
|
||||
false ->
|
||||
ejabberd_commands:unregister_commands(get_commands_spec());
|
||||
true ->
|
||||
ok
|
||||
end.
|
||||
ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()).
|
||||
|
||||
reload(Host, NewOpts, OldOpts) ->
|
||||
NewMod = gen_mod:db_mod(NewOpts, ?MODULE),
|
||||
|
|
|
@ -338,7 +338,7 @@ init([ServerHost|_]) ->
|
|||
false ->
|
||||
ok
|
||||
end,
|
||||
ejabberd_commands:register_commands(?MODULE, get_commands_spec()),
|
||||
ejabberd_commands:register_commands(ServerHost, ?MODULE, get_commands_spec()),
|
||||
NodeTree = config(ServerHost, nodetree),
|
||||
Plugins = config(ServerHost, plugins),
|
||||
PepMapping = config(ServerHost, pep_mapping),
|
||||
|
@ -809,12 +809,7 @@ terminate(_Reason,
|
|||
terminate_plugins(Host, ServerHost, Plugins, TreePlugin),
|
||||
ejabberd_router:unregister_route(Host)
|
||||
end, Hosts),
|
||||
case gen_mod:is_loaded_elsewhere(ServerHost, ?MODULE) of
|
||||
false ->
|
||||
ejabberd_commands:unregister_commands(get_commands_spec());
|
||||
true ->
|
||||
ok
|
||||
end.
|
||||
ejabberd_commands:unregister_commands(ServerHost, ?MODULE, get_commands_spec()).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
|
||||
|
|
|
@ -96,7 +96,7 @@ start(Host, Opts) ->
|
|||
Mod = gen_mod:db_mod(Opts, ?MODULE),
|
||||
Mod:init(Host, Opts),
|
||||
init_cache(Mod, Host, Opts),
|
||||
ejabberd_commands:register_commands(?MODULE, get_commands_spec()),
|
||||
ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()),
|
||||
{ok, [{iq_handler, ejabberd_sm, ?NS_PUSH_0, process_iq},
|
||||
{hook, disco_sm_features, disco_sm_features, 50},
|
||||
{hook, c2s_session_pending, c2s_session_pending, 50},
|
||||
|
@ -111,12 +111,7 @@ start(Host, Opts) ->
|
|||
|
||||
-spec stop(binary()) -> ok.
|
||||
stop(Host) ->
|
||||
case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of
|
||||
false ->
|
||||
ejabberd_commands:unregister_commands(get_commands_spec());
|
||||
true ->
|
||||
ok
|
||||
end.
|
||||
ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()).
|
||||
|
||||
-spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok.
|
||||
reload(Host, NewOpts, OldOpts) ->
|
||||
|
|
|
@ -23,8 +23,6 @@
|
|||
|
||||
%%%% definitions
|
||||
|
||||
%% @format-begin
|
||||
|
||||
-module(commands_tests).
|
||||
|
||||
-compile(export_all).
|
||||
|
@ -56,10 +54,29 @@ single_cases() ->
|
|||
single_test(http_tuple),
|
||||
single_test(http_list_tuple),
|
||||
single_test(http_list_tuple_map),
|
||||
single_test(adhoc_list_commands),
|
||||
single_test(adhoc_apiversion),
|
||||
single_test(adhoc_apizero),
|
||||
single_test(adhoc_apione),
|
||||
single_test(adhoc_integer),
|
||||
single_test(adhoc_string),
|
||||
single_test(adhoc_binary),
|
||||
single_test(adhoc_tuple),
|
||||
single_test(adhoc_list),
|
||||
single_test(adhoc_list_tuple),
|
||||
single_test(adhoc_atom),
|
||||
single_test(adhoc_rescode),
|
||||
single_test(adhoc_restuple),
|
||||
%%single_test(adhoc_all),
|
||||
single_test(clean)]}.
|
||||
|
||||
-endif.
|
||||
|
||||
%% @format-begin
|
||||
|
||||
single_test(T) ->
|
||||
list_to_atom("commands_" ++ atom_to_list(T)).
|
||||
|
||||
setup(_Config) ->
|
||||
M = <<"mod_example">>,
|
||||
clean(_Config),
|
||||
|
@ -90,6 +107,9 @@ ejabberdctl(_Config) ->
|
|||
Installed = execute(modules_installed, []),
|
||||
?match(true, lists:keymember(mod_example, 1, Installed)).
|
||||
|
||||
execute(Name, Args) ->
|
||||
ejabberd_commands:execute_command2(Name, Args, #{caller_module => ejabberd_ctl}, 1000000).
|
||||
|
||||
%%%==================================
|
||||
%%%% mod_http_api
|
||||
|
||||
|
@ -130,7 +150,7 @@ http_restuple(Config) ->
|
|||
|
||||
http_list(Config) ->
|
||||
ListS = ["one", "first", "primary"],
|
||||
ListB = [<<"one">>, <<"first">>, <<"primary">>],
|
||||
ListB = lists:sort([<<"one">>, <<"first">>, <<"primary">>]),
|
||||
?match(ListB, query(Config, "command_test_list", #{arg_list => ListS})),
|
||||
?match(ListB, query(Config, "command_test_list", #{arg_list => ListB})).
|
||||
|
||||
|
@ -148,36 +168,24 @@ http_tuple(Config) ->
|
|||
|
||||
http_list_tuple(Config) ->
|
||||
LTA = [#{element1 => "one", element2 => "uno"},
|
||||
#{element1 => "dos", element2 => "two"},
|
||||
#{element1 => "two", element2 => "dos"},
|
||||
#{element1 => "three", element2 => "tres"}],
|
||||
LTB = [#{<<"element1">> => <<"one">>, <<"element2">> => <<"uno">>},
|
||||
#{<<"element1">> => <<"dos">>, <<"element2">> => <<"two">>},
|
||||
#{<<"element1">> => <<"three">>, <<"element2">> => <<"tres">>}],
|
||||
LTB = lists:sort([#{<<"element1">> => <<"one">>, <<"element2">> => <<"uno">>},
|
||||
#{<<"element1">> => <<"two">>, <<"element2">> => <<"dos">>},
|
||||
#{<<"element1">> => <<"three">>, <<"element2">> => <<"tres">>}]),
|
||||
?match(LTB, query(Config, "command_test_list_tuple", #{arg_list => LTA})),
|
||||
?match(LTB, query(Config, "command_test_list_tuple", #{arg_list => LTB})).
|
||||
|
||||
http_list_tuple_map(Config) ->
|
||||
LTA = #{<<"one">> => <<"uno">>,
|
||||
<<"dos">> => <<"two">>,
|
||||
<<"two">> => <<"dos">>,
|
||||
<<"three">> => <<"tres">>},
|
||||
LTB = lists:sort([#{<<"element1">> => <<"one">>, <<"element2">> => <<"uno">>},
|
||||
#{<<"element1">> => <<"dos">>, <<"element2">> => <<"two">>},
|
||||
#{<<"element1">> => <<"two">>, <<"element2">> => <<"dos">>},
|
||||
#{<<"element1">> => <<"three">>, <<"element2">> => <<"tres">>}]),
|
||||
?match(LTB, lists:sort(query(Config, "command_test_list_tuple", #{arg_list => LTA}))).
|
||||
|
||||
%%%==================================
|
||||
%%%% internal functions
|
||||
|
||||
single_test(T) ->
|
||||
list_to_atom("commands_" ++ atom_to_list(T)).
|
||||
|
||||
execute(Name, Args) ->
|
||||
ejabberd_commands:execute_command2(Name, Args, #{caller_module => ejabberd_ctl}, 1000000).
|
||||
|
||||
page(Config, Tail) ->
|
||||
Server = ?config(server_host, Config),
|
||||
Port = ct:get_config(web_port, 5280),
|
||||
"http://" ++ Server ++ ":" ++ integer_to_list(Port) ++ "/api/" ++ Tail.
|
||||
%%% internal functions
|
||||
|
||||
query(Config, Tail, Map) ->
|
||||
BodyQ = misc:json_encode(Map),
|
||||
|
@ -192,6 +200,248 @@ make_query(Config, Tail, BodyQ) ->
|
|||
[{body_format, binary}]),
|
||||
Body).
|
||||
|
||||
page(Config, Tail) ->
|
||||
Server = ?config(server_host, Config),
|
||||
Port = ct:get_config(web_port, 5280),
|
||||
"http://" ++ Server ++ ":" ++ integer_to_list(Port) ++ "/api/" ++ Tail.
|
||||
|
||||
%%%==================================
|
||||
%%%% ad-hoc
|
||||
|
||||
%%% list commands
|
||||
|
||||
adhoc_list_commands(Config) ->
|
||||
{ok, Result} = get_items(Config, <<"api-commands">>),
|
||||
{value, #disco_item{name = <<"command_test_binary">>}} =
|
||||
lists:keysearch(<<"command_test_binary">>, #disco_item.name, Result),
|
||||
suite:disconnect(Config).
|
||||
|
||||
get_items(Config, Node) ->
|
||||
case suite:send_recv(Config,
|
||||
#iq{type = get,
|
||||
to = server_jid(Config),
|
||||
sub_els = [#disco_items{node = Node}]})
|
||||
of
|
||||
#iq{type = result, sub_els = [#disco_items{node = Node, items = Items}]} ->
|
||||
{ok, Items};
|
||||
#iq{type = result, sub_els = []} ->
|
||||
{empty, []};
|
||||
#iq{type = error} = Err ->
|
||||
xmpp:get_error(Err)
|
||||
end.
|
||||
|
||||
%%% apiversion
|
||||
|
||||
adhoc_apiversion(Config) ->
|
||||
Node = <<"api-commands/command_test_apiversion">>,
|
||||
ArgFields = make_fields_args([]),
|
||||
ResFields = make_fields_res([{<<"apiversion">>, <<"2">>}]),
|
||||
{ok, Sid, _FormFields} = get_form(Config, Node),
|
||||
?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)),
|
||||
suite:disconnect(Config).
|
||||
|
||||
%%% apizero
|
||||
|
||||
adhoc_apizero(Config) ->
|
||||
Node = <<"api-commands/command_test_apizero">>,
|
||||
ArgFields = make_fields_args([]),
|
||||
ResFields = make_fields_res([{<<"apiversion">>, <<"0">>}]),
|
||||
{ok, Sid, _FormFields} = get_form(Config, Node),
|
||||
?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)),
|
||||
suite:disconnect(Config).
|
||||
|
||||
%%% apione
|
||||
|
||||
adhoc_apione(Config) ->
|
||||
Node = <<"api-commands/command_test_apione">>,
|
||||
ArgFields = make_fields_args([]),
|
||||
ResFields = make_fields_res([{<<"apiversion">>, <<"1">>}]),
|
||||
{ok, Sid, _FormFields} = get_form(Config, Node),
|
||||
?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)),
|
||||
suite:disconnect(Config).
|
||||
|
||||
%%% integer
|
||||
|
||||
adhoc_integer(Config) ->
|
||||
Node = <<"api-commands/command_test_integer">>,
|
||||
ArgFields = make_fields_args([{<<"arg_integer">>, <<"12345">>}]),
|
||||
ResFields = make_fields_res([{<<"res_integer">>, <<"12345">>}]),
|
||||
{ok, Sid, _FormFields} = get_form(Config, Node),
|
||||
?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)),
|
||||
suite:disconnect(Config).
|
||||
|
||||
%%% string
|
||||
|
||||
adhoc_string(Config) ->
|
||||
Node = <<"api-commands/command_test_string">>,
|
||||
ArgFields = make_fields_args([{<<"arg_string">>, <<"Some string.">>}]),
|
||||
ResFields = make_fields_res([{<<"res_string">>, <<"Some string.">>}]),
|
||||
{ok, Sid, _FormFields} = get_form(Config, Node),
|
||||
?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)),
|
||||
suite:disconnect(Config).
|
||||
|
||||
%%% binary
|
||||
|
||||
adhoc_binary(Config) ->
|
||||
Node = <<"api-commands/command_test_binary">>,
|
||||
ArgFields = make_fields_args([{<<"arg_binary">>, <<"Some binary.">>}]),
|
||||
ResFields = make_fields_res([{<<"res_string">>, <<"Some binary.">>}]),
|
||||
{ok, Sid, _FormFields} = get_form(Config, Node),
|
||||
?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)),
|
||||
suite:disconnect(Config).
|
||||
|
||||
%%% tuple
|
||||
|
||||
adhoc_tuple(Config) ->
|
||||
Node = <<"api-commands/command_test_tuple">>,
|
||||
ArgFields = make_fields_args([{<<"arg_tuple">>, <<"one:two:three">>}]),
|
||||
{ok, Sid, _FormFields} = get_form(Config, Node),
|
||||
?match({ok,
|
||||
[{xdata_field,
|
||||
<<"res_tuple">>,
|
||||
'text-single',
|
||||
<<"res_tuple">>,
|
||||
false,
|
||||
<<" {element1 : element2 : element3}">>,
|
||||
[<<"one : two : three">>],
|
||||
[],
|
||||
[]}]},
|
||||
set_form(Config, Node, Sid, ArgFields)),
|
||||
suite:disconnect(Config).
|
||||
|
||||
%%% list
|
||||
|
||||
adhoc_list(Config) ->
|
||||
Node = <<"api-commands/command_test_list">>,
|
||||
ArgFields = make_fields_args([{<<"arg_list">>, [<<"one">>, <<"first">>, <<"primary">>]}]),
|
||||
ResFields =
|
||||
make_fields_res([{<<"res_list">>, lists:sort([<<"one">>, <<"first">>, <<"primary">>])}]),
|
||||
{ok, Sid, _FormFields} = get_form(Config, Node),
|
||||
?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)),
|
||||
suite:disconnect(Config).
|
||||
|
||||
%%% list_tuple
|
||||
|
||||
adhoc_list_tuple(Config) ->
|
||||
Node = <<"api-commands/command_test_list_tuple">>,
|
||||
ArgFields =
|
||||
make_fields_args([{<<"arg_list">>, [<<"one:uno">>, <<"two:dos">>, <<"three:tres">>]}]),
|
||||
ResFields =
|
||||
make_fields_res([{<<"res_list">>,
|
||||
lists:sort([<<"one : uno">>, <<"two : dos">>, <<"three : tres">>])}]),
|
||||
{ok, Sid, _FormFields} = get_form(Config, Node),
|
||||
?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)),
|
||||
suite:disconnect(Config).
|
||||
|
||||
%%% atom
|
||||
|
||||
adhoc_atom(Config) ->
|
||||
Node = <<"api-commands/command_test_atom">>,
|
||||
ArgFields = make_fields_args([{<<"arg_string">>, <<"a_test_atom">>}]),
|
||||
ResFields = make_fields_res([{<<"res_atom">>, <<"a_test_atom">>}]),
|
||||
{ok, Sid, _FormFields} = get_form(Config, Node),
|
||||
?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)),
|
||||
suite:disconnect(Config).
|
||||
|
||||
%%% rescode
|
||||
|
||||
adhoc_rescode(Config) ->
|
||||
Node = <<"api-commands/command_test_rescode">>,
|
||||
ArgFields = make_fields_args([{<<"code">>, <<"ok">>}]),
|
||||
ResFields = make_fields_res([{<<"res_atom">>, <<"0">>}]),
|
||||
{ok, Sid, _FormFields} = get_form(Config, Node),
|
||||
?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)),
|
||||
suite:disconnect(Config).
|
||||
|
||||
%%% restuple
|
||||
|
||||
adhoc_restuple(Config) ->
|
||||
Node = <<"api-commands/command_test_restuple">>,
|
||||
ArgFields =
|
||||
make_fields_args([{<<"code">>, <<"ok">>}, {<<"text">>, <<"Just a result text">>}]),
|
||||
ResFields = make_fields_res([{<<"res_atom">>, <<"Just a result text">>}]),
|
||||
{ok, Sid, _FormFields} = get_form(Config, Node),
|
||||
?match({ok, ResFields}, set_form(Config, Node, Sid, ArgFields)),
|
||||
suite:disconnect(Config).
|
||||
|
||||
%%% internal functions
|
||||
|
||||
server_jid(Config) ->
|
||||
jid:make(<<>>, ?config(server, Config), <<>>).
|
||||
|
||||
make_fields_args(Fields) ->
|
||||
lists:map(fun ({Var, Values}) when is_list(Values) ->
|
||||
#xdata_field{label = Var,
|
||||
var = Var,
|
||||
required = true,
|
||||
type = 'text-multi',
|
||||
values = Values};
|
||||
({Var, Value}) ->
|
||||
#xdata_field{label = Var,
|
||||
var = Var,
|
||||
required = true,
|
||||
type = 'text-single',
|
||||
values = [Value]}
|
||||
end,
|
||||
Fields).
|
||||
|
||||
make_fields_res(Fields) ->
|
||||
lists:map(fun ({Var, Values}) when is_list(Values) ->
|
||||
#xdata_field{label = Var,
|
||||
var = Var,
|
||||
type = 'text-multi',
|
||||
values = Values};
|
||||
({Var, Value}) ->
|
||||
#xdata_field{label = Var,
|
||||
var = Var,
|
||||
type = 'text-single',
|
||||
values = [Value]}
|
||||
end,
|
||||
Fields).
|
||||
|
||||
get_form(Config, Node) ->
|
||||
case suite:send_recv(Config,
|
||||
#iq{type = set,
|
||||
to = server_jid(Config),
|
||||
sub_els = [#adhoc_command{node = Node}]})
|
||||
of
|
||||
#iq{type = result,
|
||||
sub_els =
|
||||
[#adhoc_command{node = Node,
|
||||
action = execute,
|
||||
status = executing,
|
||||
sid = Sid,
|
||||
actions = #adhoc_actions{execute = complete, complete = true},
|
||||
xdata = #xdata{fields = Fields}}]} ->
|
||||
{ok, Sid, [F || F <- Fields, F#xdata_field.type /= fixed]};
|
||||
#iq{type = error} = Err ->
|
||||
xmpp:get_error(Err)
|
||||
end.
|
||||
|
||||
set_form(Config, Node, Sid, ArgFields) ->
|
||||
Xdata = #xdata{type = submit, fields = ArgFields},
|
||||
case suite:send_recv(Config,
|
||||
#iq{type = set,
|
||||
to = server_jid(Config),
|
||||
sub_els =
|
||||
[#adhoc_command{node = Node,
|
||||
action = complete,
|
||||
sid = Sid,
|
||||
xdata = Xdata}]})
|
||||
of
|
||||
#iq{type = result,
|
||||
sub_els =
|
||||
[#adhoc_command{node = Node,
|
||||
action = execute,
|
||||
status = completed,
|
||||
sid = Sid,
|
||||
xdata = #xdata{fields = ResFields}}]} ->
|
||||
ResFields2 = [F || F <- ResFields, F#xdata_field.type /= fixed],
|
||||
{ok, ResFields2 -- ArgFields};
|
||||
#iq{type = error} = Err ->
|
||||
xmpp:get_error(Err)
|
||||
end.
|
||||
|
||||
%%%==================================
|
||||
|
||||
%%% vim: set foldmethod=marker foldmarker=%%%%,%%%=:
|
||||
|
|
|
@ -111,6 +111,7 @@ max_fsm_queue: 1000
|
|||
queue_type: file
|
||||
modules:
|
||||
mod_adhoc: []
|
||||
mod_adhoc_api: []
|
||||
mod_admin_extra: []
|
||||
mod_admin_update_sql: []
|
||||
mod_announce: []
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue