If you need non-standard functionality from the XMPP server ejabberd, you do not know how to set it up with standard tools and did not find a suitable module for this module, then you can write this module yourself.
So I decided when the authorities declared war to idle talk in jabber, for which it was necessary to prohibit some users from chatting with others, but allowing others to allow them. And although I still have a vague suspicion that this can be configured using access lists, I decided to write a module that I would be comfortable using. This module will serve as an example for the story.
Training
First you need to at least learn a little erlang programming language. I used
translation of article from RSDN Magazine and
official documentation .
After that, you need to prepare a computer to assemble the module. In addition to the installed erlang and ejabberd, we will need their sources in order to connect some libraries to our module.
To check the module in action, you will need:
- Compile it.
$ erlc mod_restrictions.erl
- Move the resulting binary into the folder to ejabberd.
$ mv mod_restrictions.beam /usr/lib/ejabberd/ebin/
- Load new (updated) module. To do this, you can simply restart ejabberd:
$ service ejabberd restart
or you can update the module on the fly , for example, in the ejabberd admin panel ( Nodes -> Your site -> Update )
(commands for Ubuntu 11.04)
')
The compiler can write something like this:
./mod_restrictions.erl:5: Warning: behaviour gen_mod undefined
It's not scary, everything will work. If there are any other errors and warnings, then they should be dealt with.
To connect a module in the ejabber config (/etc/ejabberd/ejabberd.cfg file), add the line in the list of modules (after
{modules,[
):
{mod_restrictions, []}
Skeleton
Modules ejabberd must inherit the
gen_mod interface. This behavior requires two functions:
start / 2 and
stop / 1 , which will be called, respectively, when the module is started and stopped. We write the following code in the mod_restrictions.erl file:
-module(mod_restrictions). -behavior(gen_mod). -export([start/2, stop/1]). start(_Host, _Opts) -> ok. stop(_Host) -> ok.
Here we:
- Specify the name of the module (should be as a file name without .erl).
- We inherit the behavior of gen_mod.
- We say that the start (with two arguments) and stop (with one argument) functions are accessible from outside the module.
- We write function. The lower space in the argument names tells Erlang that these arguments are not used.
- Return ok
We write to the logs
To record an event in the logs (for example, that the module started and stopped), you can use the macro
? INFO_MSG :
-module(mod_restrictions). -behavior(gen_mod). -include("ejabberd.hrl"). -export([start/2, stop/1]). start(Host, _Opts) -> ?INFO_MSG("mod_restrictions ~p", [Host]), ok. stop(Host) -> ?INFO_MSG("mod_restrictions ~p", [Host]), ok.
First, we include the ejabberd.hrl file with the definition of the INFO_MSG macro. Note that you need to specify the full or relative path to the included file, specifically this one lies in the source code of ejabber.
Our message will appear in the server logs (I have this /var/log/ejabberd/ejabberd.log), and instead of each ~ p there will be an element from the list - the second argument.
Ejabberd event handling
Ejabberd allows modules to be built into it using the mechanism of events (events) and hooks (hooks).
ejabberd_hooks:add(Hook, Host, Module, Function, Prioritet) ejabberd_hooks:remove(Hook, Host, Module, Function, Prioritet)
Ejabberd will call the
Module's Function Function Module (use the
? MODULE macro for your module) every time a
Hook event occurs on the
Host node (use
global if you want to listen to all nodes). A list of possible events can be viewed at
this link . If there are several subscribers to one event, they are executed in turn, first with a higher priority.
We are interested in the
filter_packet event, which occurs every time someone sends something to someone (for example, a message).
Now the module looks like this:
-module(mod_restrictions). -behavior(gen_mod). -include("ejabberd.hrl"). -export([start/2, stop/1, on_filter_packet/1]). start(_Host, _Opts) -> ejabberd_hooks:add(filter_packet, global, ?MODULE, on_filter_packet, 50), ok. stop(_Host) -> ejabberd_hooks:delete(filter_packet, global, ?MODULE, on_filter_packet, 50), ok. on_filter_packet(Packet) -> Packet.
Add module to admin panel
In order to manage users and groups, I want to add a module to the ejabberd web interface. Read more about it
here .
We connect the necessary files and add-delete hooks:
-include("web/ejabberd_http.hrl"). -include("web/ejabberd_web_admin.hrl"). start(_Host, _Opts) -> ... ejabberd_hooks:add(webadmin_menu_main, ?MODULE, web_menu_host, 50), ejabberd_hooks:add(webadmin_page_main, ?MODULE, web_page_host, 50), ... stop(_Host) -> ... ejabberd_hooks:delete(webadmin_menu_main, ?MODULE, web_menu_host, 50), ejabberd_hooks:delete(webadmin_page_main, ?MODULE, web_page_host, 50), ...
Add an item to the main admin menu:
web_menu_host(Acc, Lang) -> Acc ++ [{"mod_restrictions", ?T("Restrictions")}].
And the module page itself:
web_page_host(_, #request{method = Method, q = Query, path = ["mod_restrictions"], lang = _Lang}) -> case Method of 'POST' ->
Here we first process POST requests to change the list of user groups, then return the following HTML code:
<h1>Restriction module manager</h1> <table><tr><th>Users</th><th>Groups</th></tr><tr><td style="vertical-align:top;"> - <form action="" method="post"><input type="text" name="usr" value=""/><input type="text" name="grp" value=""/><input type="hidden" name="act" value="add_usrgrp"/><input type="submit" name="btn" value="Add"/></form></td><td style="vertical-align:top;"> - <form action="" method="post"><input type="text" name="grp" value=""/><input type="text" name="dest" value=""/><input type="hidden" name="act" value="add_grpdest"/><input type="submit" name="btn" value="Add"/></form></td></tr></table>
All HTML generation functions can be viewed in the ejabberd_web_admin.hrl file.
Work with database
The easiest way to use the Mnesia database with ejabberd.
We will need 2 tables: one for including users in groups, and another for setting group rights.
The structure of the tables is described using erlang-records.
-record(restrictions_users, {usr, grp}). -record(restrictions_groups, {grp, dest}).
We have created a
restrictions_users entry with the user's JID fields and his group, and
restrictions_groups entry with the group and direction fields (where it is allowed to send messages).
You can work with Mnesia tables in two ways: with transactions and “dirty” methods. The second method is faster, but does not guarantee the integrity of the database. We will use it when processing messages.
Creating tables
Now in the start function we will create tables corresponding to these entries. They will not overwrite when the module is restarted, unless they do it on purpose.
mnesia:create_table(restrictions_groups, [{disc_copies, [node()]} , {attributes, record_info(fields, restrictions_groups)}, {type, bag}]), mnesia:create_table(restrictions_users, [{disc_copies, [node()]} , {attributes, record_info(fields, restrictions_users)}, {type, bag}]),
Among other things, here we indicate that a copy of the table is stored on the hard disk (by default, the tables are stored only in RAM). The type of the
bag table means that the values in the fields may not be unique, but each entry in the table is unique.
SELECT * FROM table
In the admin area we need to display the tables entirely. To do this, you can use this design:
mnesia:dirty_match_object(mnesia:table_info(,wild_pattern))
It will give us a list of all the records in the table. Then you can use, for example, the function
lists: map / 2 in order to make an HTML label from the list.
JOIN
When processing messages, we will need to join tables to determine the correspondence between the user (usr) and the permissions (dest). We use the
qlc module for this:
Ftemp = fun() -> Qvve = qlc:q([allow || U <- mnesia:table(restrictions_users), G <- mnesia:table(restrictions_groups), (U#restrictions_users.usr == FromUsr++"@"++FromDomain) and (U#restrictions_users.grp == G#restrictions_groups.grp) and ( (G#restrictions_groups.dest == "all") or (G#restrictions_groups.dest == ToDomain) or (G#restrictions_groups.dest == ToUsr++"@"++ToDomain) ) ]), qlc:eval(Qvve) end, ?INFO_MSG("mod_restrictions: allow? ~p", [mnesia:transaction(Ftemp)]),
If the user From is in a group that has all permission, or permission to write To user to the domain, or To user himself, then the request returns {atomic, [allow, ...]}.
Module parameters
We can transfer parameters from the configuration file ejabberd.cfg to the module, replacing the connection string with the following, for example:
{mod_restrictions, [{deny_message," :("}]}
We will pass in the parameter text of the answering machine when the message is blocked.
In the module, the value of the parameter is obtained using the function get_module_opt:
gen_mod:get_module_opt(Host, Module, Opt, Default)
Sending a message from the module
Attrs = [{"type",Type},{"id","legal"},{"to",To#jid.user++"@"++To#jid.server++"/"++To#jid.resource},{"from",From#jid.user++"@"++From#jid.server++"/"++From#jid.resource}], Els = [{xmlcdata,<<"\n">>},{xmlelement,"body",[],[{xmlcdata,list_to_binary(Message)}]}], DenyMessage = {xmlelement,"message",Attrs,Els}, ejabberd_router:route(From,To,DenyMessage)
The From and To variables are taken from the on_filter_packet parameter, and they represent jid entries.
Conclusion
Full text of the module can be found
here .
Based on
this article .