📜 ⬆️ ⬇️

Writing a module for Ejabberd

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:
  1. Compile it.
    $ erlc mod_restrictions.erl 
  2. Move the resulting binary into the folder to ejabberd.
     $ mv mod_restrictions.beam /usr/lib/ejabberd/ebin/ 
  3. 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:
  1. Specify the name of the module (should be as a file name without .erl).
  2. We inherit the behavior of gen_mod.
  3. We say that the start (with two arguments) and stop (with one argument) functions are accessible from outside the module.
  4. We write function. The lower space in the argument names tells Erlang that these arguments are not used.
  5. 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' -> %% Handle database query      case lists:keyfind("act", 1, Query) of        {"act","add_usrgrp"} -> %% add user to group          {"usr",NewUser} = lists:keyfind("usr", 1, Query),          {"grp",NewGroup} = lists:keyfind("grp", 1, Query),          ?INFO_MSG("mod_restrictions: ADD NewUser=~p NewGroup=~p", [NewUser, NewGroup]);        ...        _ -> none      end;    _ -> none  end,  Res = [?XC("h1", "Restriction module manager"),      ?XE("table",[        ?XE("tr",          [?XC("th","Users"),?XC("th","Groups")]        ),        ?XE("tr",          [?XAE("td",[{"style","vertical-align:top;"}],web_user_list()),?XAE("td",[{"style","vertical-align:top;"}],web_group_list())]        )]      )     ],  {stop, Res}; web_page_host(Acc, _) -> Acc. 

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 .

Source: https://habr.com/ru/post/130396/


All Articles