diff --git a/ChangeLog b/ChangeLog index ecbe9cf74..c7c78b5d0 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,9 @@ 2008-10-12 Badlop + * src/ejabberd_commands.erl: New 'ejabberd commands': separate + command definition and calling interface (EJAB-694) + * src/ejabberd_commands.hrl: Likewise + * src/mod_proxy65/mod_proxy65.erl: Update so the listener starts correctly (EJAB-303) * src/mod_proxy65/mod_proxy65_service.erl: Likewise diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl new file mode 100644 index 000000000..0388748ff --- /dev/null +++ b/src/ejabberd_commands.erl @@ -0,0 +1,328 @@ +%%%---------------------------------------------------------------------- +%%% File : ejabberd_commands.erl +%%% Author : Badlop +%%% Purpose : Management of ejabberd commands +%%% Created : 20 May 2008 by Badlop +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2008 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +%%% @headerfile "ejabberd_commands.hrl" + +%%% @doc Management of ejabberd commands. +%%% +%%% An ejabberd command is an abstract function identified by a name, +%%% with a defined number and type of calling arguments and type of +%%% result, that can be defined in any Erlang module and executed +%%% using any valid frontend. +%%% +%%% +%%% == Define a new ejabberd command == +%%% +%%% ejabberd commands can be defined and registered in +%%% any Erlang module. +%%% +%%% Some commands are procedures; and their purpose is to perform an +%%% action in the server, so the command result is only some result +%%% code or result tuple. Other commands are inspectors, and their +%%% purpose is to gather some information about the server and return +%%% a detailed response: it can be integer, string, atom, tuple, list +%%% or a mix of those ones. +%%% +%%% The arguments and result of an ejabberd command are strictly +%%% defined. The number and format of the arguments provided when +%%% calling an ejabberd command must match the definition of that +%%% command. The format of the result provided by an ejabberd command +%%% must be exactly its definition. For example, if a command is said +%%% to return an integer, it must always return an integer (except in +%%% case of a crash). +%%% +%%% If you are developing an Erlang module that will run inside +%%% ejabberd and you want to provide a new ejabberd command to +%%% administer some task related to your module, you only need to: +%%% implement a function, define the command, and register it. +%%% +%%% +%%% === Define a new ejabberd command === +%%% +%%% An ejabberd command is defined using the Erlang record +%%% 'ejabberd_commands'. This record has several elements that you +%%% must define. Note that 'tags', 'desc' and 'longdesc' are optional. +%%% +%%% For example let's define an ejabberd command 'pow' that gets the +%%% integers 'base' and 'exponent'. Its result will be an integer +%%% 'power': +%%% +%%%
#ejabberd_commands{name = pow, tags = [test],
+%%%                 desc = "Return the power of base for exponent",
+%%%                 longdesc = "This is an example command. The formula is:\n"
+%%%                 "  power = base ^ exponent",
+%%%                 module = ?MODULE, function = pow,
+%%%                 args = [{base, integer}, {exponent, integer}],
+%%%                 result = {power, integer}}
+%%% +%%% +%%% === Implement the function associated to the command === +%%% +%%% Now implement a function in your module that matches the arguments +%%% and result of the ejabberd command. +%%% +%%% For example the function calc_power gets two integers Base and +%%% Exponent. It calculates the power and rounds to an integer: +%%% +%%%
calc_power(Base, Exponent) ->
+%%%    PowFloat = math:pow(Base, Exponent),
+%%%    round(PowFloat).
+%%% +%%% Since this function will be called by ejabberd_commands, it must be exported. +%%% Add to your module: +%%%
-export([calc_power/2]).
+%%% +%%% Only some types of result formats are allowed. +%%% If the format is defined as 'rescode', then your function must return: +%%% ok | true | atom() +%%% where the atoms ok and true as considered positive answers, +%%% and any other response atom is considered negative. +%%% +%%% If the format is defined as 'restuple', then the command must return: +%%% {rescode(), string()} +%%% +%%% If the format is defined as '{list, something()}', then the command +%%% must return a list of something(). +%%% +%%% +%%% === Register the command === +%%% +%%% Define this function and put inside the #ejabberd_command you +%%% defined in the beginning: +%%% +%%%
commands() ->
+%%%    [
+%%%
+%%%    ].
+%%% +%%% You need to include this header file in order to use the record: +%%% +%%%
-include("ejabberd_commands.hrl").
+%%% +%%% When your module is initialized or started, register your commands: +%%% +%%%
ejabberd_commands:register_commands(commands()),
+%%% +%%% And when your module is stopped, unregister your commands: +%%% +%%%
ejabberd_commands:unregister_commands(commands()),
+%%% +%%% That's all! Now when your module is started, the command will be +%%% registered and any frontend can access it. For example: +%%% +%%%
$ ejabberdctl help pow
+%%%
+%%%   Command Name: pow
+%%%
+%%%   Arguments: base::integer
+%%%              exponent::integer
+%%%
+%%%   Returns: power::integer
+%%%
+%%%   Tags: test
+%%%
+%%%   Description: Return the power of base for exponent
+%%%
+%%% This is an example command. The formula is:
+%%%  power = base ^ exponent
+%%%
+%%% $ ejabberdctl pow 3 4
+%%% 81
+%%% 
+%%% +%%% +%%% == Execute an ejabberd command == +%%% +%%% ejabberd commands are mean to be executed using any valid +%%% frontend. An ejabberd commands is implemented in a regular Erlang +%%% function, so it is also possible to execute this function in any +%%% Erlang module, without dealing with the associated ejabberd +%%% commands. +%%% +%%% +%%% == Frontend to ejabberd commands == +%%% +%%% Currently there are two frontends to ejabberd commands: the shell +%%% script {@link ejabberd_ctl. ejabberdctl}, and the XML-RPC server +%%% mod_xmlrpc. +%%% +%%% +%%% === ejabberdctl as a frontend to ejabberd commands === +%%% +%%% It is possible to use ejabberdctl to get documentation of any +%%% command. But ejabberdctl does not support all the argument types +%%% allowed in ejabberd commands, so there are some ejabberd commands +%%% that cannot be executed using ejabberdctl. +%%% +%%% Also note that the ejabberdctl shell administration script also +%%% manages ejabberdctl commands, which are unrelated to ejabberd +%%% commands and can only be executed using ejabberdctl. +%%% +%%% +%%% === ejabberd_xmlrpc as a frontend to ejabberd commands === +%%% +%%% ejabberd_xmlrpc provides an XML-RPC server to execute ejabberd commands. +%%% ejabberd_xmlrpc is a contributed module published in ejabberd-modules SVN. +%%% +%%% Since ejabberd_xmlrpc does not provide any method to get documentation +%%% of the ejabberd commands, please use ejabberdctl to know which +%%% commands are available, and their usage. +%%% +%%% The number and format of the arguments provided when calling an +%%% ejabberd command must match the definition of that command. Please +%%% make sure the XML-RPC call provides the required arguments, with +%%% the specified format. The order of the arguments in an XML-RPC +%%% call is not important, because all the data is tagged and will be +%%% correctly prepared by mod_xmlrpc before executing the ejabberd +%%% command. + +%%% TODO: consider this feature: +%%% All commands are catched. If an error happens, return the restuple: +%%% {error, flattened error string} +%%% This means that ecomm call APIs (ejabberd_ctl, ejabberd_xmlrpc) need to allows this. +%%% And ejabberd_xmlrpc must be prepared to handle such an unexpected response. + + +-module(ejabberd_commands). +-author('badlop@process-one.net'). + +-export([init/0, + list_commands/0, + get_command_format/1, + get_command_definition/1, + get_tags_commands/0, + register_commands/1, + unregister_commands/1, + execute_command/2 + ]). + +-include("ejabberd_commands.hrl"). +-include("ejabberd.hrl"). + + +init() -> + ets:new(ejabberd_commands, [named_table, set, public, + {keypos, #ejabberd_commands.name}]). + +%% @spec ([ejabberd_commands()]) -> ok +%% @doc Register ejabberd commands. +%% If a command is already registered, a warning is printed and the old command is preserved. +register_commands(Commands) -> + lists:foreach( + fun(Command) -> + case ets:insert_new(ejabberd_commands, Command) of + true -> + ok; + false -> + ?WARNING_MSG("This command is already defined:~n~p", [Command]) + end + end, + Commands). + +%% @spec ([ejabberd_commands()]) -> ok +%% @doc Unregister ejabberd commands. +unregister_commands(Commands) -> + lists:foreach( + fun(Command) -> + ets:delete_object(ejabberd_commands, Command) + end, + Commands). + +%% @spec () -> [{Name::atom(), Args::[aterm()], Desc::string()}] +%% @doc Get a list of all the available commands, arguments and description. +list_commands() -> + Commands = ets:match(ejabberd_commands, + #ejabberd_commands{name = '$1', + args = '$2', + desc = '$3', + _ = '_'}), + [{A, B, C} || [A, B, C] <- Commands]. + +%% @spec (Name::atom()) -> {Args::[aterm()], Result::rterm()} | {error, command_unknown} +%% @doc Get the format of arguments and result of a command. +get_command_format(Name) -> + Matched = ets:match(ejabberd_commands, + #ejabberd_commands{name = Name, + args = '$1', + result = '$2', + _ = '_'}), + case Matched of + [] -> + {error, command_unknown}; + [[Args, Result]] -> + {Args, Result} + end. + +%% @spec (Name::atom()) -> ejabberd_commands() | command_not_found +%% @doc Get the definition record of a command. +get_command_definition(Name) -> + case ets:lookup(ejabberd_commands, Name) of + [E] -> E; + [] -> command_not_found + end. + +%% @spec (Name::atom(), Arguments) -> ResultTerm | {error, command_unknown} +%% @doc Execute a command. +execute_command(Name, Arguments) -> + case ets:lookup(ejabberd_commands, Name) of + [Command] -> + execute_command2(Command, Arguments); + [] -> + {error, command_unknown} + end. + +execute_command2(Command, Arguments) -> + Module = Command#ejabberd_commands.module, + Function = Command#ejabberd_commands.function, + apply(Module, Function, Arguments). + +%% @spec () -> [{Tag::string(), [CommandName::string()]}] +%% @doc Get all the tags and associated commands. +get_tags_commands() -> + CommandTags = ets:match(ejabberd_commands, + #ejabberd_commands{ + name = '$1', + tags = '$2', + _ = '_'}), + Dict = lists:foldl( + fun([CommandNameAtom, CTags], D) -> + CommandName = atom_to_list(CommandNameAtom), + case CTags of + [] -> + orddict:append("untagged", CommandName, D); + _ -> + lists:foldl( + fun(TagAtom, DD) -> + Tag = atom_to_list(TagAtom), + orddict:append(Tag, CommandName, DD) + end, + D, + CTags) + end + end, + orddict:new(), + CommandTags), + orddict:to_list(Dict). diff --git a/src/ejabberd_commands.hrl b/src/ejabberd_commands.hrl new file mode 100644 index 000000000..ce7aadb9b --- /dev/null +++ b/src/ejabberd_commands.hrl @@ -0,0 +1,52 @@ +%%%---------------------------------------------------------------------- +%%% +%%% ejabberd, Copyright (C) 2002-2008 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., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA +%%% +%%%---------------------------------------------------------------------- + +-record(ejabberd_commands, {name, tags = [], + desc = "", longdesc = "", + module, function, + args = [], result = rescode}). + +%% @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.