Сегодня мы с вами постараемся немного отойти от темы околосистемного
программирования и углубиться в не менее увлекательный мир прикладного
программирования. А конкретнее - написания модулей расширения (или
плагинов) для одной интересной программы. Знакомьтесь, X-Chat! Как можно
догадаться по названию, это IRC-клиент. Причем не простой IRC-клиент, а
даже с поддержкой плагинов! :)
Ну чтож, а теперь я немного поясню причины выбора в качестве подопытной
крыски именно этой программы.. Уверен, практически каждый из вас
когда-нибудь общался в чате.. Будь то полноценный IRC либо веб-чаты -
не важно. Согласитесь, увлекательное (и чрезвычайно вредное для работы
:) занятие. Конечно, веб-чаты выигрывают у IRC по своей доступности и
простоте (чем и привлекают начинающих), но, все же, уступают (причем
очень сильно) последим по функциональности. Именно по этому в дальнейшем
мы будем рассматривать именно IRC.
Далее я буду предполагать, что читатель уже имеет некоторое
представление о принципе работы IRC и имеет хотя бы небольшой опыт
общения в нем. Если нет, советую почитать мануалы на dalnet.ru или любом
другом источнике.
Ну чтож, с выбором чата мы разобрались, теперь осталось выбрать
IRC-клиент. Из множества различных клиентов (на память: mIRC, X-Chat,
BitchX, irsii, Mozilla-IRC и других) я остановился именно на X-Chat по
нескольким причинам:
Огромный потенциал расширяемости. Программа имеет довольно мощную
систему плагинов, что открывает широкие возможности для расширения
функциональности. В отличии от аналогов расширяемость достигается не за
счет статичных (встроенных) скриптовых интерпретаторов, а за счет
динамических библиотек. Благодаря этому, в принципе, функциональность
клента может быть неограниченно увеличена. Самое интересное, что для
X-Chat существуют модули, обеспечивающие поддержку различных
скриптовых языков. На данный момент это Perl, Pyton, Tcl
Эргономичный и вполне дружественный интерфейс. Да, это наиболее
важное преимущество. Интерфейс программы отлично продуман и
качественно оформлен
Кросс-платформенность. По крайней мере, существуют версии
практически для всех популярных ОС.
Свободное распространение. Исходники X-Chat распространяются по
лицензии GNU GPL. Что позволяет изменять программу под себя не
только с помощью модулей :)
Надеюсь после этих речей вы все-таки решили дочитать эту статью до конца
несмотря на не совсем очевидную тему нашей работы.
Пришло время прояснить непонятные вещи.
Наверняка многие из вас встречались в чате с не совсем прилично ведущими
себя индивидуумами. Кто-то из них чрезмерно использует нецензурную
речь, кто-то оскорбляет других обитателей канала, кто-то, находясь не в
состоянии сделать что-либо более стоящего, попросту флудит в канал..
Вот последними мы и займемся.
Для этого я предлагаю написать небольшой плагин к X-Chat для
противодействия этим флудерам.
Для начала советую обратиться к оригинальной спецификации интерфейса
плагинов X-Chat'a: http://www.xchat.org/docs/plugin20.html Очень
внимательно изучите все основные функции интерфейса, иначе вы попросту
не сможете понять, как будет работать наш будущий супер-плагин :)
Принцип работы плагина
Он подсчитывает количество идентичных сообщений пользователя на канале
и, при достижении определенного лимита, безжалостно карает нарушителя
:) Довольно просто, не так ли? Но это только на первый взгляд..
Итак, с разбором теории мы закончили, теперь переходим непосредственно к
практике..
Практическая часть
Для того чтобы получить возможность работы с CALLBACK-функциями X-Chat'a
необходимо в главный модуль плагина включить заголовочный файл с
описанием всех этих функций и всех необходимых для работы с ними
структур и определений.
#include "xchat-plugin.h"
Этот файл вы можете взять в исходниках X-Chat'a или скачать с этой
страницы (см. в конце статьи).
Основной необходимой функцией в любом X-Chat-плагине является функция
Именно она является точкой входа в поток плагина. Соответственно, чтобы
эта функция была доступна основному приложению, мы должны объявить ее
как export-функцию. Для этого создадим файл xchat-norepeat.def со
следующим содержанием:
EXPORTS
xchat_plugin_init
эти строки помещают xchat_plugin_init в секцию экспорта, тем самым,
давая возможность ее вызова из основного процесса.
Теперь я поясню некоторые параметры функции инициализации:
xchat_plugin *plugin_handle - указатель на структуру с описателем
плагина. Этот параметр мы не будем изменять, т.к. основной процесс
передает при вызове xchat_plugin_init указатель уже готовую структуру,
которую мы будем в последствии использовать во всех CALLBACK функциях
X-Chat'а. plugin_handle - что-то вроде уникального идентификатора
плагина.
Поля char **plugin_name,char **plugin_desc,char **plugin_version мы
должны заполнить самостоятельно. В них должна содержаться некоторая
информация а плагине.
Для того чтобы мы могли получать и анализировать сообщения пользователей
на определенном канале (либо сразу на нескольких каналах), нам
необходимо перехватить сообщение сервера "PRIVMSG". Для этого в X-Chat
существует функция
xchat_plugin *ph - это plugin handle, указатель на структуру с описателем плагина
const char *name - имя команды. В нашем случае "PRIVMSG".
int pri - приоритет команды. В официальной документации советуют использовать XCHAT_PRI_NORM, что мы и сделаем
xchat_serv_cb *callb - адрес функции-обработчика. В дальнейшем мы рассмотрим ее более подробно
void *userdata - пользовательские данные, передаваемые в обработчик команды.
Используется для передачи ей некоторых аргументов
Ну чтож, вроде разобрались. Теперь рассмотрим функцию-обработчик:
static int priv_msg(char *word[], char *word_eol[], void *userdata);
Разберемся с параметрами:
char *word[] - массив аргументов серверной команды
word[1] == :user ident
word[2] == :command
word[3] == :channel
word[4] == :first word of message
word[5] == :second word of message
...
...
Далее идут слова из самого сообщения пользователя. Интересно, правда?
Возникает вопрос: и как же получить само сообщение? Конечно, можно
объединить все элементы массива strcat'ом, но можно поступить проще.
char *word_eol[] - возвращает все агрументы команды в виде строки
начиная с n'ого.
т.е. в word_eol[4] содержится само сообщение пользователя
Так, с обработчиком разобрались, теперь немного вернемся к теоретическим
вопросам.
Так как же будет действовать наш каратель флудеров?
А вот как:
Во-первых, нам необходимо будет создать список пользователей. Когда
пользователь что-то сказал в канал, наш плагин проверяет нет ли этого
пользователя в списке. Если нет, то заносит его туда. Если он уже там
есть, плагин сохраняет его сообщение и сравнивает его с его предыдущим
сообщением. Если они идентичны, то, соответственно, счетчик повторений
для данного пользователя увеличивается.
При достижении этим счетчиком критической отметки, плагин просто
посылает на сервер команду /mode с параметром +b (бан), и создает
таймер отсчета до снятия бана для этого пользователя.
вот структура данных пользователя:
typedef struct _tagUDATA {
char u_name[255]; // идент
char u_lastmsg[512]; // последнее сообщение
char u_channel[50]; // канал
uint u_rcounter; // количество одинаковых сообщений
xchat_hook *u_thook; // описатель таймера отсчета
} UDATA, **PUDATA;
Еще один аспект: если при обработке сообщения выяснилось, что
пользователь повторил свое сообщение, то, вместе с увеличением счетчика,
нам необходимо создать таймер сброса этого счетчика по истечении
определенного периода времени. Иначе даже если интервал между
одинаковыми сообщениями у пользователя довольно большой (что в принципе
уже не является флудом), он все равно попадает под флудера, а значит
рано или поздно будет забанен.
Во избежание этой проблемы нам и необходимо время от времени сбрасывать
счетчик.
Кроме всего этого необходимо еще сделать проверку на принадлежность
флудера к опам, хопам, войсам и самому себе :) Может кому-нибудь и
разрешено флудить на канале.
Для этого необходимо воспользоваться функцией получения списка
пользователей на канале. Для получения различных списков с данными в
X-Chat существует функция xchat_list *xchat_list_get(xchat_plugin *ph,
const char *list); в аргументе list указывается название требуемого
списка. Вот некоторые из них:
"channels" - список каналов и серверов
"dcc" - список передач по DCC
"ignore" - список игнорируемых пользователей
"notify" - список уведомлений
"users" - список пользователей текущего канала
Как видите, с помощью этой функции мы можем получить лишь список
пользователей текущего канала. А как же быть, если необходимый не
является текущим?
Для этого существуют обходные пути. Это функции xchat_find_context,
xchat_get_context и xchat_set_context.
Первая из них, как видно из названия осуществляет поиск контекста по
серверу или каналу:
int xchat_set_context(xchat_plugin *ph, xchat_context *ctx);
Итак, наш алгоритм будет следующим:
1) найти контекст нужного нам канала
2) запомнить текущий контекст
3) переключиться на найденный контекст
4) произвести необходимые операции
5) переключиться назад на сохраненный контекст
Гениальное всегда просто, не так ли?
вот как это выглядит на примере:
uint get_ustatus(UDATA* u) // получить статус пользователя (op, halfop, voice)
{
xchat_list *xu_list; // список
uint u_status=XSTATUS_NONE; // статус пользователя
xchat_context *xctx,*xctx_prev; // контексты
// ищем нужный нам контекст
if (!(xctx = xchat_find_context(ph, NULL, (const char*)u->u_channel))) { return XSTATUS_NONE; }
// сохраняем текущий
xctx_prev = xchat_get_context(ph);
// устанавливаем новый
xchat_set_context(ph, xctx);
// получаем список пользователей текущего канала
xu_list = xchat_list_get(ph, "users");
if (xu_list)
{
while(xchat_list_next(ph, xu_list)) // поочередно анализируем каждый элемент списка
{
// сверяем ники
if (strncmp(xchat_list_str(ph, xu_list, "nick"),get_nick(u->u_name),sizeof(u->u_name))==0) {
if (strcmp(xchat_list_str(ph, xu_list, "prefix"),"@")==0) { // если он оп
u_status = XSTATUS_OP;
break;
} else if (strcmp(xchat_list_str(ph, xu_list, "prefix"),"%")==0) { // если халфоп
u_status = XSTATUS_HOP;
break;
} else if (strcmp(xchat_list_str(ph, xu_list, "prefix"),"+")==0) { // если войс
u_status = XSTATUS_VOICE;
break;
}
}
}
xchat_list_free(ph, xu_list); // обязательно освобождаем память
}
xchat_set_context(ph, xctx_prev); // возвращаемся к сохраненному контексту
return u_status; // возвращаем статус пользователя
}
Как видите, код хорошо прокомментирован, поэтому его дополнительное
пояснение бессмысленно.
Итак, мы с вами разобрали все основные аспекты работы этого плагина, и я
надеюсь, то вы все-таки немного вошли в курс дела :). Если нет, советую
еще раз более внимательно прочитать эту статью, а также обратиться к
официальному руководству.
Исходный код плагина
Далее я выложу код главного модуля нашего плагина. Он достаточно
прокомментирован и поэтому не нуждается в пояснениях:
/*
* Другие необходимые для компиляции плагина файлы смотрите в прилагаемом архиве
* с исходниками.
*/
// макс. число повторений
#define MAX_REPEAT 4
// канал
#define CHANNEL "#chelny"
// время действия бана
#define BAN_EXPIRE 30000
// время, через которое счетчик сообщений определенного пользователя обнуляется
#define REPEAT_EXPIRE 15000
*/
UDATA *pu; // указатель на структуру с данными о пользователе
if (!enable) return XCHAT_EAT_NONE; // проверка запуска
if (strcmp(word[3],CHANNEL)==0) { // если channel==CHANNEL
if ((pu=ulist_get(word[1]))!=NULL) { // ищем в списке идент юзера (word[1]).. если нашли, продолжаем
if (strncmp(pu->u_lastmsg,word_eol[4],sizeof(pu->u_lastmsg))==0) { // сравниваем последнее сообщение юзера с message (word_eol[4]) если равно то..
if (pu->u_rcounter+1==MAX_REPEAT) { // увеличиваем счетчик.. если счетчик повторений равен MAX_REPEAT
if ((get_ustatus(pu)==0)&&(!(ban_users&BAN_OPS))) { // баним опов?
if (pu->u_thook)
xchat_unhook(ph, pu->u_thook);
ulist_set(pu->u_name,"",word[3],0);
return XCHAT_EAT_NONE;
}
if ((get_ustatus(pu)==1)&&(!(ban_users&BAN_HOPS))) { // баним халфопов?
if (pu->u_thook)
xchat_unhook(ph, pu->u_thook);
ulist_set(pu->u_name,"",word[3],0);
return XCHAT_EAT_NONE;
}
if ((get_ustatus(pu)==2)&&(!(ban_users&BAN_VOICES))) { // баним войсов?
if (pu->u_thook)
xchat_unhook(ph, pu->u_thook);
ulist_set(pu->u_name,"",word[3],0);
return XCHAT_EAT_NONE;
}
xchat_commandf(ph, "MODE %s +b %s", word[3], get_banmask(word[1])); // ставим бан
if (pu->u_thook)
xchat_unhook(ph, pu->u_thook); // уничтожаем таймер
pu->u_thook = xchat_hook_timer(ph, BAN_EXPIRE, timeout_bt, pu); // пускаем таймер истечения бана
ulist_set(pu->u_name,"",word[3],0); // обнуляем счетчик повторений у данного юзера
} else { // если не равен MAX_REPEAT
if (pu->u_thook) // уничтожаем таймер если он уже установлен
xchat_unhook(ph, pu->u_thook);
pu->u_thook = xchat_hook_timer(ph, REPEAT_EXPIRE, timeout_exp, pu); // пускаем таймер обнуления счетчика
ulist_set(pu->u_name,word_eol[4],word[3],pu->u_rcounter+1); // увеличиваем счетчик
}
} else { // если не равно..
if (pu->u_thook) // уничтожаем таймер если он уже установлен
xchat_unhook(ph, pu->u_thook);
pu->u_thook = xchat_hook_timer(ph, REPEAT_EXPIRE, timeout_exp, pu); // пускаем таймер обнуления счетчика
ulist_set(pu->u_name,word_eol[4],word[3],1);
}
} else { // если данного пользователя нет в списке
ulist_add(word[1],word_eol[4],word[3],1); // добавляем
pu=ulist_get(word[1]);
if (pu->u_thook)
xchat_unhook(ph, pu->u_thook);
pu->u_thook = xchat_hook_timer(ph, REPEAT_EXPIRE, timeout_exp, pu); // пускаем таймер обнуления счетчика
}
}
return XCHAT_EAT_NONE;/* don't eat this event, let other plugins and xchat see it too */
}
// включения режима norepeat
static int norepeattoggle_cb(char *word[], char *word_eol[], void *userdata)
{
if (!enable)
{
enable = 1;
xchat_print(ph, "norepeat is now enabled..n");
} else
{
enable = 0;
xchat_print(ph, "norepeat is now disabled..n");
}
return XCHAT_EAT_ALL; /* eat this command so xchat and other plugins can't process it */
}
// точка входа
int xchat_plugin_init(xchat_plugin *plugin_handle,
char **plugin_name,
char **plugin_desc,
char **plugin_version,
char *arg)
{
/* we need to save this for use with any xchat_* functions */
ph = plugin_handle;
Ну чтож, подведем итог.. Мы написали небольшое полезное дополнение для
X-Chat, обеспечивающее модерирование заданного канала и пресечение
попыток флуда.
Данный пример вполне функционален и выполняет все необходимые для этого функции.
Тем не менее, его вполне можно расширить, добавив новые возможности.
К примеру:
Поддержку работы с несколькими каналами
Поддержку работы с различными параметрами на различных серверах
Поддержку ведения лога на все действия
Загрузку параметров из конфигурационного файла
...
...
.. и много других возможностей..
К сожалению, рассмотрение всех этих нововведений выходит за рамки данной
статьи, т.ч. оставляю их реализацию вам. Буду искренне рад, если на
основе моего примера вы сможете написать что-нибудь действительно
стоящее и полезное. Т.ч., как говорится, дерзайте ;)
Исходный код плагина можно скачать здесь или здесь