Краткое описание протокола IRC и пример бота

25 ноября 2010

Вот решил написать небольшой пост о протоколе IRC. Во-первых, что-то я давно не пополнял свою коллекцию описаний сетевых протоколов, а во-вторых это в тему про ботнеты, о которых недавно шла речь.

Вообще-то IRC прекрасно описан в RFC, русском переводе этого RFC и Википедии. Потому я решил не ограничиваться одним его описанием и выложить пример простенького бота. Бот этот заходит на канал и ждет команду:

!google [запрос]

Получив ее, бот посылает запрос поисковику, кидает первую ссылку из выдачи в чат и ждет следующих команд.

#!/usr/bin/perl

# Googler IRC Bot
# (c) 2010 Alexandr A Alexeev
# http://eax.me/

use IO::Socket::INET;
use URI::Escape;
use Text::Iconv;
use strict;

my ($server, $port, $botnick, $charset) =
  qw/irc.dalnet.ru 6667 googler cp1251/;
my $pass = "";
my @channels = qw/googlertest/;

my $opt = "--timeout=10 --no-check-certificate --user-agent='Mozilla/5.0 (X11; U; FreeBSD i386; ru-RU; rv:1.9.1.10) Gecko/20100625 Firefox/3.5.10'";
my $conv = Text::Iconv->new($charset, "utf-8");

while(1) {
  print "CONNECTING: server = $server, port = $port...\n";
  my $irc = IO::Socket::INET->new(
    PeerAddr => $server,
    PeerPort => $port,
    Proto => 'tcp',
    Type => SOCK_STREAM,
    Timeout => 10
  );

  if(defined $irc) {
    print $irc "USER $botnick localhost localhost $botnick\r\n";
    print $irc "NICK $botnick\r\n";
    print $irc "PASS $pass\r\n" if $pass;
    print $irc "JOIN #$_\r\n" for @channels;

    while(my $line = <$irc>) {
      $line = $conv->convert($line);
      if($line =~ /^PING \:([a-zA-Z\-\.0-9]+)/){
        print $irc "PONG $1\r\n";
      } elsif($line =~ /^\:([^\!\ ]+)\![^\ ]+\ ([^\r\n]+)\r\n$/) {
        my $nick = $1; $line = $2;

        if(my($channel,$msg) = $line=~/^PRIVMSG #([^\ ]+) \:(.+)$/) {
          # см http://ru.wikipedia.org/wiki/IRC#CTCP
          if(my ($action) = $msg =~ /^\x01ACTION ([^\x01]+)\x01?$/){
             print irc_msg_encode("[$channel] *** $nick $action\n");
          } else {
             print irc_msg_encode("[$channel] $nick: $msg\n");
             if($msg =~ /\!google (.*)/) {
               my $result = do_google($1);
               print $irc "PRIVMSG #$channel :$nick, $result\r\n";
             }
          }
        } elsif(my($chan,$nick) = $line=~/^KICK #([^\ ]+) ([^\ ]+)/) {
          if($nick eq $botnick) {
            print "DEBUG: i'm kicked from #$chan\n";
            last;
          }
        }
      } # if($line =~ /^PONG ...
    } # while($line = <$irc> ...
  } else { # if(defined $irc) ...
    print "CONNECTION FAILED!\n";
  }
  sleep 300;
}

# поиск в Google
sub do_google {
  my $query = $_[0];
  my $url = "http://google.com/search?q=".uri_escape($query);
  my $data = `wget $opt -q '$url' -O -`;
  return "query failed - error $?" if $?;
  return $1 if $data =~ /<h3 class="r"><a href="([^"]+)"/i;
  return "nothing found";
}

# вырезаем выделения цветом и тп
sub irc_msg_encode {
  my $text = $_[0];
  $text =~ s/\x0F//g;
  $text =~ s/\x02//g; $text =~ s/\x1F//g;
  $text =~ s/\x03([0-9]{1,2}(\,[0-9]{1,2})?)?//g;
  $text;
}

Пример работы скрипта:

Пример IRC бота

На базе этого примера можно написать бота, который постит новости, определяет погоду и тд. Считайте это домашним заданием.

Краткое описание основных сообщений сервера:

  • PING :[что-то] — время от времени посылается сервером, на случай, если клиент повиснет. Получив такую команду, нужно как можно быстрее ответить PONG (см ниже), иначе сервер может разорвать соединение.
  • :[ник]![юзер@хост] [событие] — [событие], созданное пользователем [ник]. Например, [ник] мог послать вам сообщение ([событие] = «PRIVMSG …») или выкинуть вас с канала ([событие] = «KICK …»).
  • PRIVMSG [кому] :[сообщение] — новое [сообщение], личное ([кому] = ваш ник) или не личное ([кому] = #channel). Как определить отправителя, см в предыдущем пункте.
  • KICK [канал] [ник] … — пользователя [ник] кикнули с канала [канал]. Кто кикнул, определяется так же, как и в случае с PRIVMSG.

Основные команды клиента:

  • USER — представиться серверу, см код бота.
  • PASS [пароль] — если для захода на сервер нужен пароль.
  • NICK [ник] — сменить ник.
  • PONG :[что-то] — ответ на команду PING.
  • JOIN #[канал] — зайти на канал.
  • PART #[канал] — уйти с канала. Второй необязательный аргумент — причина ухода.
  • PRIVMSG [кому] :[сообщение] — послать сообщение конкретному пользователю или на канал.
  • QUIT — отсоединиться от сервера.

Несколько замечаний. Во-первых, максимальная длина одной строки в протоколе составляет 512 символов. Вычитаем два символа-терминатора (\r\n) — получаем 510 символов. Также нужно учесть длину самой команды, пробелы между аргументами и тд. При посылке длинного сообщения, я бы советовал нарезать его на части по 256 символов. Или 140, если на сервере используется UTF-8.

Во-вторых, имена каналов начинаются с решетки. Решетка не может использоваться в никах, так что по первому аргументу команд типа PRIVMSG сразу видно, куда шлется сообщение, в «личку» или на канал.

В-третьих, в IRC можно выделять текст цветом и менять его фон. Найти бы и жестокого наказать того, кто придумал это расширение! Все ненужные украшательства в моем боте вырезаются функцией irc_msg_encode.

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

PART #channel :всем пока, пора работу работать!

Ну вот по большому счету и все. Как я уже писал когда-то, в 90% случаев используется лишь 10% протокола. Сравните одну страничку этого поста и 65 страниц RFC. Если вы собрались писать собственного IRC бота или даже небольшой IRC-клиент/сервер, то этой заметки наверняка окажется достаточно. Ну а если нет, дополнительную информацию всегда можно найти в Вики и RFC.

Дополнение: Простейший IRC-бот на Python, а также при чем тут Slack, Gitter и прочие веб-чаты

Метки: , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.