Пишем icq-бот на perl

Если вы не знаете, но хотите узнать, как сделать программу, которая постоянно висит в памяти и что-то нужное периодически выполняет, и при этом вы пишете на языке perl — попытаюсь рассказать об этом как можно более понятно.
Я сам люблю подробные инструкции, которые подразумевают, что человек, её читающий, не обязан знать всё, что было пропущено. Пусть лучше он сам пропустит то, что знает и так. Тем более, что о чём-то он может иметь ошибочное представление.

Ну а чтобы не было скучно, совместим этот пример с ещё одной темой — icq, чтобы у нас получился icq-bot.



Для начала — о демонах.



Несмотря на развитие perl под win32 и даже появившуюся возможность эмуляции функции fork, я поведу речь о unix. Всё равно модуль, необходимый для общения с сервером icq, под win32 не заточен.

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

В то же время, icq-bot должен работать постоянно, независимо от того, откуда его запустили, и прекратила ли работу запустившая этот процесс другая программа. Например, я залогинился на сервер по ssh, из при помощи bash запустил бота. После того, как я закрою bash и ssh-соединение, бот должен продолжать работу, как ни в чём не бывало.

Этой цели служит функция fork, которая запускает дочерний процесс, не связанный с тем терминалом, из которого запустили родительский. Следом за этим необходимо вызвать функцию setsid, которая завершает работу над обособлением нашего процесса.

После этих действий новорожденному демону, если он хочет быть хорошим демоном, необходимо сменить рабочую директорию на корневую — на тот случай, если он запустился с раздела, который был подключен через mount. Иначе раздел будет занят, и его невозможно будет отключить.

Кроме этого, поскольку ввод с терминала и вывод на терминал демоном производится не будут — необходимо перенаправить эти стандартные каналы в нуль. Если вы хотите отслеживать работу бота, то можно перенаправить вывод в лог-файл.

Самая хитрый этап — это использование функции fork. Она создаёт ещё один точно такой же процесс. Единственное отличие между ними — первый был родительским, и должен завершиться, а второй — дочерний, и должен остаться. Поэтому процессу необходима самоидентификация — кто я?
Она достигается проверкой результата, который возвращает функция fork. Если она вернула ненулевое значение — это pid нового дочернего процесса, который был запущен. Значит, я — родитель, и я ухожу. Если же pid нулевой — значит, я дочка, и я работаю дальше.

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

sub Daemonize {<br/>
 return if ($^O eq 'MSWin32'); # под виндой мы не работаем, мы там играем<br/>
 chdir '/' or die "Can't chdir to /: $!"; # не мешаем unmount<br/>
 umask 0; # устанавливаем разрешения открываемых файлов по умолчанию<br/>
 open STDIN, '/dev/null'   or die "Can't read /dev/null: $!"; # перенаправим ввод в нуль<br/>
 open STDOUT, '>/dev/null' or die "Can't write to /dev/null: $!"; # перенаправим вывод в нуль<br/>
 open STDERR, '>/dev/null' or die "Can't write to /dev/null: $!"; # перенаправим вывод ошибок в нуль<br/>
 defined(my $pid = fork)   or die "Can't fork: $!"; # пытаемся выполнить fork<br/>
 exit if $pid; # я родитель, если получен id дочернего процесса<br/>
 setsid or die "Can't start a new session: $!"; # обосабливаемся<br/>
}<br/>
 


Обработка сообщений



ICQ-бот по идее должен отправлять какие-то сообщения и обрабатывать полученные. Сделаем простой вариант бота, который работает уведомителем — отправляет куда надо поступающие от системы сообщения, а на входящие сообщения просто выдаёт стандартный ответ. И для наглядности сделаем обработку одной команды — quit.
А дальше уже всё зависит от вашей фантазии — обрабатывать входящие сообщения можно как угодно, и тут можно напридумывать много интересного — от выдачи характеристик системы по команде до искусственного интеллекта.

Проще всего получать сообщения для рассылки из файлов. Нужно завести отдельную директорию для файлов с исходящими сообщениями. Программы, которым нужно отправлять сообщения, будут складывать их в эту папку.
Боту останется её периодически проверять и при обнаружении новых сообщений — отправлять их.

Сделаем так — в папке будут создаваться файлы вида nnn.rrr, где nnn — это icq UIN получателя, а rrr — это случайный номер, чтобы файлы не перезаписывались, если в один момент на один номер свалится несколько сообщений.

Итого — нам надо просканировать папку “входящие”, и для каждого файла отправить его содержимое на номер, указанный в имени файла.

sub CheckTasks {<br/>
 # проверим задания<br/>
 my($file, $path, $text, $size, $recipient);<br/>
 <br/>
 $path="~username/icq/icq_tasks"; # директория с входящими<br/>
 <br/>
 opendir DIR, $path;<br/>
 for $file (grep /^\d+\.\d+$/, readdir DIR) {  # файлы вида nnn.rrr<br/>
    $file=~/^(\d+)\.\d+$/;<br/>
    $recipient=$1; # цифры до точки - это UIN<br/>
    $size=(stat("$path/$file"))[7];<br/>
    if ($size>0 && $size<200) { # если размер не слишком велик и не слишком мал<br/>
     $text=ReadFile("$path/$file"); # читаем содержимое<br/>
     unlink("$path/$file"); # удаляем<br/>
     $oscar -> send_im($recipient, $text); # отправляем. О том, кто такой oscar - попозже<br/>
    }<br/>
 }<br/>
}<br/>
 


Основная программа



Вспомогательные функции мы почти все сделали. Теперь сделаем основную программу.

Для взаимодействия с сервером ICQ я использую модуль Net::OSCAR. Это объектный модуль, поэтому нам нужно создать объект, который соединится с сервером, будет общаться с ним, и периодически пинать сервер, чтобы он не отсоединил нас. Кроме того, необходимо проверять статус соединения, и в случае разрыва соединяться заново. Ну и конечно, отправлять и принимать сообщения.

Поехали:

#!/usr/bin/perl -w<br/>
 <br/>
use Net::OSCAR; # подключаем модуль<br/>
use strict; # примерные программисты используют strict<br/>
use POSIX qw(setsid); # одна функция из модуля POSIX, необходимая для демонизации<br/>
 <br/>
Daemonize(); # демонизируемся<br/>
 <br/>
my($UIN, $PASSWORD, $oscar, $t);<br/>
 <br/>
$UIN='123456'; # UIN для бота<br/>
$PASSWORD='mypass'; # пароль для UIN<br/>
 <br/>
$oscar = Net::OSCAR -> new(); # создаём объект для общения с icq сервером<br/>
$oscar -> set_callback_im_in(\&send_answer); <br/>
 # назначаем функцию для ответа на входящие сообщения - об этом ниже<br/>
$t=0; # переменная для отслеживания текущего времени<br/>
 <br/>
while (1) { # вечный цикл<br/>
 if (!$oscar->is_on && (time()-$t)>120) {  # Мы не в сети уже более 2 минут!<br/>
    $oscar->signon($UIN, $PASSWORD); # Законнектимся снова<br/>
    $t=time(); # Точка отсчёта<br/>
 }<br/>
 $oscar->do_one_loop(); # Эта внутренняя функция модуля пинает сервер<br/>
 CheckTasks() if ($oscar->is_on); # Если мы в сети - обработаем исходящие сообщения<br/>
 sleep(5); # Ждём 5 секунд, мы же не хотим слишком часто долбать сервер<br/>
}


Поясню подробнее некоторые пункты:

Модуль занимается обработкой событий, одним из которых является поступление входящего сообщения. Чтобы на такие сообщения бот мог посылать ответы, мы вешаем на это событие вызов функции send_answer():

$oscar -> set_callback_im_in(\&send_answer)

Она может выглядеть вот так:

sub send_answer() {<br/>
  my($oscar, $sender, $msg) = @_; <br/>
  # функции автоматически передаются три параметра -<br/>
  # ссылка на объект, номер отправителя сообщения и само сообщение<br/>
  if ($msg eq "quit") { # простой обработчик команд. команда одна - отбой<br/>
    $oscar -> signoff(); # отсоединиться<br/>
    exit(); # пока-пока<br/>
  }<br/>
  $oscar -> send_im($sender, 'Я бот, интеллекта не имею, общаться не могу.'); <br/>
  # ответ на любое другое сообщение<br/>
}



Итак, мы создаем обработчик входящих и занимаемся обработкой исходящих в бесконечном цикле. Чтобы проверять, в сети ли мы, используем флажок $oscar->is_on
А чтобы не долбить слишком часто запросами на коннект, используем таймер — в $t хранится время последней попытки соединения. В том числе, этот участок сработает и после запуска программы, для установления первого соединения с сервером.

Если мы в сети, периодически вызываем метод $oscar->do_one_loop(), который поддерживает наш бот в залогиненном состоянии.

И если мы в сети, проверяем папку входящих сообщений, и при необходимости отправляем их.

Компонуем, сохраняем, запускаем. Простейший icq-бот готов! Я использую такого бота для отсылки уведомлений мне с сервера.

А что теперь?



Из недостатков данного бота отмечу, что модуль Net::OSCAR не умеет отсылать сообщения пользователям, находящимся в офлайне.

В качестве домашнего задания предлагаю следующее:
— на четвёрочку — сделать так, чтобы при первом запуске бот коннектился к icq сразу, а не ждал 2 минуты
— на четвёрочку с плюсом — написать функцию ReadFile
— на пятерочку — разобраться в документации Net::OSCAR, научить бот определять, находится ли получатель сообщения в офлайне и не отправлять ему сообщения в офлайн.
Метки:
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 13
  • +5
    Ладно, Perl я еще понимаю, но ICQ?

    Не учите детей гадостям, лучше бы про Jabber сделали, куда полезней было бы!
    • 0
      И ваще sleep(5)ж — это не ок.
      use Time::HiRes qw(usleep);
      usleep(100);

      Так кашерней!
      • +2
        Я про то, что юзать юслип, что бы не убить проц пустым лупом :)
        А вообще лучше это как-то на евентах сделать, а не циклодрочем.
      • 0
        Да, в следующий раз можно и жабробот сделать.
        Хотя каждому своё — кому нужно icq, тому пригодится.
        • +1
          На CPAN-е есть под AnyEvent и POE, на выбор. Беспроблемно можно и на Modjo сделать, очень просто будет.
          • 0
            А это уже задание на 5+ я полагаю.
            • 0
              с ICQ еще как полезно — вешать на свой юин бота, который отвечал бы «обращайтесь в мой джаббер, тут меня уже нет» :)
            • 0
              Тоже думал о подобной статье, но про IRC/DC++ (хотя последней не пользуюсь, но больно уж протокол там простой). Как думаете, будет интересна такая тема?
              • +4
                Первого с утра любая тема интересная!
              • 0
                Автор, ты бы хоть единой стилистики кода придерживался, а то создается впечатление, что скрипт был собран из кусков, которые ты практически не трогал.
                Почему $path локальная переменная внутри функции, логичнее было бы её вынести туда же, где задается uin и пароль. Вместо stat стоило бы -s написать, это читабельность кода повысит, т.к. не каждый помнит порядок, возвращаемый функцией stat. Скобки и кавычки местами тоже создают, гм, двоякое впечатление.
                • 0
                  Действительно, нет предела совершенству.

                  Возможно, лучше вывести основную программу в main(). И переменную $path определять там вместе с l/p, и вызывать CheckTasks($path)

                  Писался скрипт постепенно, по мере копания в немного путаной документации Net::OSCAR.

                  Возможно, впечатление портят неровные отступы — с непривычки не разобрался с подсветкой синтаксиса.
                • 0
                  А в java как такое сделать?
                  там же своя машина — форка нет. я столкнулся с этим, правда решил работать через threads — для одного решения сработало.

                  Как запустить просто параллельный процесс?
                  • 0
                    А «google java threads» уже не спасает? :) Как-то неприлично в топике и блоге про Perl спрашивать о Java. Тем более то, что можно найти за 5 секунд в любой книге либо в интернете.

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