Pull to refresh

Пишем модуль для Ejabberd

Reading time 9 min
Views 6.8K
Если вам нужна нестандартная функциональность от XMPP сервера ejabberd, вы не знаете, как это настроить штатными средствами и не нашли подходящего для этого модуля, то можно написать этот модуль самим.

Так я решил, когда начальство объявило войну пустой болтовней в jabber-е, для чего необходимо было запретить некоторым пользователям чатица с другими, но разрешить с третьими. И хотя у меня всё ещё есть смутное подозрение, что это можно настроить с помощью списков доступа, я решил написать модуль, которым мне будет удобно пользоваться. Этот модуль и послужит примером для рассказа.

Подготовка


Для начала надо хоть немного освоить язык программирования erlang. Я пользовался переводом статьи из RSDN Magazine и официальной документацией.
После этого надо подготовить компьютер для сборки модуля. Кроме установленных erlang и ejabberd нам понадобятся их исходники для того, чтобы подключать некоторые библиотеки к нашему модулю.

Чтобы проверять модуль в действии, надо будет:
  1. Скомпилировать его.
    $ erlc mod_restrictions.erl
  2. Переместить получившийся бинарник в папку к ejabberd.
    $ mv mod_restrictions.beam /usr/lib/ejabberd/ebin/
  3. Подгрузить новый (обновлённый) модуль. Для этого можно просто перезапустить ejabberd:
    $ service ejabberd restart
    а можно обновить модуль «на лету», например, в админке ejabberd (Узлы -> Ваш узел -> Обновить)
(команды для Ubuntu 11.04)

Компилятор при этом может писать что-то вроде:

./mod_restrictions.erl:5: Warning: behaviour gen_mod undefined

Это не страшно, всё будет работать. Если же есть какие-то другие ошибки и предупреждения, то с ними надо разобраться.

Для подключения модуля в конфиге ejabber (файл /etc/ejabberd/ejabberd.cfg) надо в списке модулей (после {modules,[) добавить строчку:

{mod_restrictions, []}

Скелет


Модули ejabberd должен наследовать интерфейс gen_mod. Это поведение требует наличия двух функций: start/2 и stop/1, которые будут вызываться, соответственно, при запуске и остановке модуля. Пишем следующий код в файл mod_restrictions.erl:

-module(mod_restrictions).
-behavior(gen_mod). 
-export([start/2, stop/1]).
start(_Host, _Opts) ->
  ok.
stop(_Host) ->
  ok.

Здесь мы:
  1. Указываем имя модуля (должно быть как имя файла без .erl).
  2. Наследуем поведение gen_mod.
  3. Говорим что функции start (с двумя аргументами) и stop (с одним аргументом) доступны извне модуля.
  4. Пишем функцию. Нижний пробел в именах аргументов говорит Erlang-у, что эти аргументы не используются.
  5. Возвращаем ok

Пишем в логи


Чтобы записать событие в логи (например, что модуль запустился и остановился) можно воспользоваться макросом ?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.

Для начала мы подключаем файл ejabberd.hrl с определением макроса INFO_MSG. Заметим что надо указывать полный или относительный путь до подключаемого файла, конкретно этот лежит в исходниках ejabber-а.
Наше сообщение появится в логах сервера (у меня это /var/log/ejabberd/ejabberd.log), а вместо каждого ~p будет стоять элемент из списка — второго аргумента.

Обработка событий ejabberd


Ejabberd позволяет модулям встраиваться в него с помощью механизма событий (events) и крючков (hooks).

ejabberd_hooks:add(Hook, Host, Module, Function, Prioritet)
ejabberd_hooks:remove(Hook, Host, Module, Function, Prioritet)

Ejabberd будет вызывать функцию Function модуля Module (используйте макрос ?MODULE чтобы это был ваш модуль), каждый раз, когда будет происходить событие Hook на узле Host (используйте global, если вы хотите слушать все узлы). Список возможных событий можно посмотреть по этой ссылке. Если подписчиков на одно событие несколько, то они выполняются по очереди, вначале — с более высоким приоритетом.
Нас интересует событие filter_packet, которое происходит каждый раз, когда кто-нибудь кому-нибудь отправляет что-нибудь (например, сообщение).
Теперь модуль выглядит так:

-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.

Добавление модуля в админку


Для того, чтобы управлять пользователями и группами, хочется добавить модуль в web-интерфейс ejabberd-а. Подробнее об этом написано здесь.

Подключаем нужные файлы и добавляем-удаляем хуки:

-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),
  ...


Добавляем пункт в основное меню админки:

web_menu_host(Acc, Lang) ->
  Acc ++ [{"mod_restrictions", ?T("Restrictions")}].

И сама страница модуля:

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.

Здесь мы сначала обрабатываем POST-запросы на изменение списка пользователей-групп, потом возвращаем такой HTML код:

<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>

Все функции генерации HTML можно посмотреть в файле ejabberd_web_admin.hrl

Работа с базой данных


Проще всего вместе с ejabberd использовать базу данных Mnesia.

Нам понадобится 2 таблицы: одна для включения пользователей в группы, а другая для настройки прав групп.
Структура таблиц описывается с помощью erlang-записей.

-record(restrictions_users, {usr, grp}).
-record(restrictions_groups, {grp, dest}).

Мы создали запись restrictions_users с полями JID пользователя и его группа, и запись restrictions_groups с полями группа и направление (куда разрешено отправлять сообщения).

С таблицами Mnesia можно работать двумя способами: транзакциями и «грязными» методами. Второй метод работает быстрее, но не гарантирует целостности базы данных. Мы будем использовать его при обработке сообщений.

Создание таблиц

Теперь в функции start создадим соответствующие этим записям таблицы. Они не будут перезаписывать при перезапуске модуля, если только не делать этого специально.

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}]),

Кроме всего прочего, здесь мы указываем, что копия таблицы сохраняется на жестком диске (по умолчанию таблицы хранятся только в оперативной памяти). Тип таблиц bag значит, что значения в полях могут быть не уникальны, но каждая запись в таблице уникальна.

SELECT * FROM таблица

В админке нам нужно отображать таблицы целиком. Для этого можно воспользоваться такой конструкцией:

mnesia:dirty_match_object(mnesia:table_info(ИмяТаблицы,wild_pattern))

Она вернёт нам список всех записей таблицы. Далее можно будет воспользоваться, например, функцией lists:map/2 для того, чтобы сделать из списка HTML табличку.

JOIN

При обработке сообщений нам надо будет соединять таблицы, чтобы определить соответствия между пользователем (usr) и разрешениями (dest). Используем для этого модуль qlc:

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)]),

Если пользователь From находится в группе, у которой есть разрешение all, либо разрешение писать в домен пользователю To, либо писать самому пользователю To, то запрос возвращает {atomic, [allow,...]}.

Параметры модуля


Мы можем передавать в модуль параметры из файла настроек ejabberd.cfg, заменив строку подключения на такую, например:

{mod_restrictions,    [{deny_message,"Вы не можете писать мне:("}]}

Мы будем передавать в параметре текст автоответчика, когда cообщение заблокировано.
В модуле значение параметра получаем с помощью функции get_module_opt:

gen_mod:get_module_opt(Host, Module, Opt, Default)

Отправка сообщения из модуля


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)

Переменные From и To мы берем из параметра on_filter_packet, и они представляют записи типа jid.

Заключение


Полностью текст модуля можно посмотреть здесь.
За основу взята эта статья.
Tags:
Hubs:
+30
Comments 10
Comments Comments 10

Articles