Kentaro Kuribayashi's blog

Software Engineering, Management, Books, and Daily Journal.

PEAR::Net_SmartIRC を使って、一定間隔でニュースを配信する IRC BOT を作成する

最近は、IRC#順列都市なるチャンネルにておしゃべり暮らしているわけですが、IRC というものに触れるのが初めてなので、いろんなことが面白い。そんな中でやっぱりいちばん感銘を覚えたのが BOT なる存在。世の中にはいろんな BOT さんがいるようで、#順列都市ではロイディさんに来ていただいています。んで、召還した彼(?)に語りかけるなどして寂しさを紛らわしたり、と、そんな毎日です。

そのような BOT さんたちを見ていると、ヘタレながらも「こういうのって、自分で作れないかなぁ」と、まぁ思うわけです。とはいえ、一から作るのは無理があり過ぎるので、例によって PEAR 漁り。すると、Net_SmartIRC という、ちょうどいい感じのパッケージがありました。そこでこの Net_SmartIRC パッケージを用いて、ごく簡単な IRC BOT を作成してみたいと思います。かといって特に「これだ!」というオリジナルなネタもないので、とりあえず「Going My Way: IRCを使ったニュースとBlog情報の自動配信」でその概要が紹介されている、ニュースを自動配信してくれる BOT を…パクる…とかそんなんじゃなくて、えーと、インスパイア! されました! ってな、そんな感じで。もちろん、劣化コピーです!

まずはともあれ、具体的なイメージをつかんでいただくため、以下のチャンネルで実際の BOT を動かしていますので、join してみてください。

まずは PEAR パッケージをインストールします。また、RSS をパースするのに XML_RSS も必要とするので、一緒にインストールしておきます。

$ su -
# pear install Net_SmartIRC
# pear install XML_RSS

パッケージのインストールを終えたら、以下の 4 つのファイルをどこか適当なディレクトリに設置します(rss_list.txt 以外については、拡張子が .txt になっているのを事前に外してください)。

最後の rss_list.txt には登録した RSS データを書き込むので、設置や実行の条件によっては rss_list.txt に対して書き込みすることができるように権限を設定する必要があるかもしれません(666 とか)。

では、それぞれのファイルについて、解説。まずは設定ファイルである conf.inc について。とりあえず「BOT の基本設定」と「IRC 関連設定」という箇所を変更します。BOT に素敵な名前をつけてあげましょう。IRC 関連の設定については「国内 IRC サーバリスト (Mar/9/2004 現在)」というページを参考にするといいでしょう。その他の設定については、スクリプト内のコメントに従って適当に変更したり変更しなかったりしてください。また、エンコーディングEUC-JP にしておくのが無難かもしれません。

conf.inc:

<?php

/**
 * Bot 設定
 *
 * IRC 関連の設定については、以下のページを参照してください
 * 「国内 IRC サーバリスト (Mar/9/2004 現在)」[http://irc.kyoto-u.ac.jp/list-of-servers.html]
 *
 **/

// BOT の基本設定
define("BOT_NICK", "news_bot"); // 英数字で
define("BOT_NAME", "php_bot");  // 英数字で

// IRC 関連設定
define("SERVER_HOST", "irc.nara.wide.ad.jp"); // なんか適当なサーバ
define("SERVER_PORT", 6668);                  // 適当なポート
define("CHANNEL", "#_順列都市");              // BOT をログインさせるチャンネル名

// コマンド(変更不要)
define("DELIMITER", ":");                // BOT のニックネームとコマンドとのつなぎ目
define("COMMAND_FEED_NEWS", "feedNews"); // 一定時間ごとにニュースを配信するコマンド
define("COMMAND_ADD_RSS", "addRSS");     // RSS を新規に登録するコマンド
define("COMMAND_LIST_RSS", "listRSS");   // 登録されている RSS のリストを表示するコマンド
define("COMMAND_QUIT", "quit");          // BOT を終了するコマンド

// コマンド関連設定ここから

// COMMAND_FEED_NEWS 関連
define("RSS_FEED_INTERVAL", 10);  // ニュースを配信する間隔(秒単位)
define("RSS_COUNT", 1);           // 読み込む RSS の数
define("ITEM_COUNT", 1);          // 読み込む item の数
define("RSS_NEWS_PREFIX", "■ "); // 書き出す item の前に付す文字
define("RSS_NEWS_HOOK", ":");     // 記事のタイトルと URI の間に付す文字

// COMMAND_ADD_RSS 関連(それぞれの意味については、察してください)
define("RSS_ADD_SUCCESS_MSG", "指定された RSS をリストに追加しますた!");
define("RSS_ADD_ERR_MSG", "そんな RSS はないっぽですよ?");
define("RSS_ADD_ERR2_MSG", "指定された RSS はすでにリストに存在します。");

// COMMAND_LIST_RSS 関連
define("RSS_LIST_FILE", "./rss_list.txt"); // RSS リスト
//define("RSS_LIST_FILE_PATH", "/home/kentaro/public_html/tmp/rss_list.txt"); // RSS リストをコピーするパス
define("RSS_LIST_FILE_URI", "http://antipop.zapto.org/tmp/rss_list.txt");     // RSS リストの URI

// COMMAND_QUIT 関連(それぞれの意味については、察してください)
define("QUIT_MSG", "終了します");

// コマンド関連設定ここまで

// 共通設定(それぞれの意味については、察してください)
define("FILE_OPEN_ERR_MSG", "ファイルを開けませんですた。。。");;
define("URL_PATTERN", "|(http://[a-zA-Z0-9@:%_.~#-\?&]+[a-zA-Z0-9@:%_~#\?&/])|");
define("IRC_ENCODING", "ISO-2022-JP"); // 変更不要

?>

次に、bot.php について。これが bot の本体です。conf.inc で設定したチャンネルにログインした後、registerTimehandler() で登録したメソッドを conf.inc で設定した間隔で実行したり、registerActionhandler() で登録したメソッドを IRC 参加者からのコマンド呼び出しによって実行したりします。

bot.php:

<?php

/**
 * Bot メイン
 *
 **/

include_once "Net/SmartIRC.php";
include_once "./bot.class.inc";
include_once "./conf.inc";

$bot = &new mybot();
$irc = &new Net_SmartIRC();
$irc->setDebug(SMARTIRC_DEBUG_ALL);
$irc->setUseSockets(TRUE);

// ニュースを表示
$irc->registerTimehandler*1;
$irc->listen();

?>

最後に、BOT の挙動を定義するクラス bot.class.inc について。実際に IRC 上から使用するメソッドとして、以下の 3 つが定義されています。

addRSS
RSS を新規に登録するコマンド
listRSS
登録されている RSS のリストを表示するコマンド
quit
BOT を終了するコマンド

それぞれの使い方については後述します。また、feedNews については、BOT が自動的に実行するメソッドなので、IRC 参加者が直接コマンドから呼び出すものではありません。

bot.class.inc:

<?php

/**
 * Bot クラス
 *
 **/

include_once "XML/RSS.php";

class mybot {

  /**
   * RSS リストからランダムに読み込んだ RSS をパースして表示
   *
   *
  */

  function feedNews (&$irc) {

    // RSS リスト読み込み
    $list = file(RSS_LIST_FILE);

    // RSS_COUNT の回数だけ RSS を読み込む
    for ($i = 0; $i < RSS_COUNT; $i++) {

      // 乱数生成
      // リストから RSS をランダムに取得
      srand();
      $num = rand(0, count($list));

      // RSS パーサ生成 & RSS をパース
      $rss = &new XML_RSS(trim($list[$num]));
      $rss->parse();

      // channel タイトルを書き出し
      $channel_info = $rss->getChannelInfo();
      $channel_title = $this->_convert($channel_info['title']);
      $irc->message(SMARTIRC_TYPE_NOTICE, $this->_convert(CHANNEL), $channel_title);

      // ITEM_COUNT の回数だけ item を書き出す
      $item = $rss->getItems();
      for ($j = 0; $j < ITEM_COUNT; $j++) {
        $title = $this->_convert($item[$j]['title']);
        $link = $this->_convert($item[$j]['link']);
        $irc->message(SMARTIRC_TYPE_NOTICE, $this->_convert(CHANNEL), $this->_convert(RSS_NEWS_PREFIX) . $title . $this->_convert(RSS_NEWS_HOOK) . $link);
      }
      unset($rss);
    }
  }



  /**
   * RSS をリストに追加
   *
   *
  */

  function addRSS (&$irc, &$data) {

    // URI が妥当ならば、RSS を検証後、リストに追加
    if (!(preg_match(URL_PATTERN, $data->message, $matches))) {
      $irc->message(SMARTIRC_TYPE_NOTICE, $this->_convert(CHANNEL), $this->_convert(RSS_ADD_ERR_MSG));
    } else {

      // RSS パーサ生成 & RSS をパース
      $rss_uri = trim($matches[1]);
      $rss = &new XML_RSS($rss_uri);
      $rss->parse();
      $channel_info = $rss->getChannelInfo();
      $channel_link = $channel_info['link'];

      // RSS をパースした結果、channel の link を取得できなかったらエラー
      // でなければ、リストに追加
      if (!$channel_link) {
        $irc->message(SMARTIRC_TYPE_NOTICE, $this->_convert(CHANNEL), $this->_convert(RSS_ADD_ERR_MSG));
      } else {

        // リストを開けなかったらエラー
        if (!($fp = fopen(RSS_LIST_FILE, "a+"))) {
          $irc->message(SMARTIRC_TYPE_NOTICE, $this->_convert(CHANNEL), $this->_convert(FILE_OPEN_ERR_MSG));
        } else {
          flock($fp, LOCK_EX);
          while (!(feof($fp))) {
            $lines .= fgets($fp);
          }
          $lines = explode("\n", $lines);
          if (!in_array($rss_uri, $lines)) {
            fputs($fp, $rss_uri . "\n");
            $irc->message(SMARTIRC_TYPE_NOTICE, $this->_convert(CHANNEL), $this->_convert(RSS_ADD_SUCCESS_MSG));
          } else {
            $irc->message(SMARTIRC_TYPE_NOTICE, $this->_convert(CHANNEL), $this->_convert(RSS_ADD_ERR2_MSG));
          }
          flock($fp, LOCK_UN);
          fclose($fp);
        }
      }
    unset($rss);
    }
  }

  /**
   * RSS のリストを示す
   *
   *
  */

  function listRSS (&$irc, &$data) {
    exec("cp " . RSS_LIST_FILE . " " . RSS_LIST_FILE_PATH);
    $irc->message(SMARTIRC_TYPE_NOTICE, $this->_convert(CHANNEL), RSS_LIST_FILE_URI);
  }

  /**
   * BOT を終了する
   *
   *
  */

  function quit (&$irc) {
    $irc->quit($this->_convert(QUIT_MSG));
  }

  /**
   * IRC のエンコーディングへ変換
   *
  */

  function _convert ($str) {
    $str = mb_convert_encoding($str, IRC_ENCODING, "auto");
    return $str;
  }

}

?>

設定等が済んだら、スクリプトを設置したディレクトリに移動した後、コンソールから以下の通り実行します。

$ cd /path/to/script/dir
$ php bot.php &

うまくいけば、指定したチャンネルに BOT がログインし、即座にニュースの配信を開始します。以下にスクリーンショットを示します(クリックで拡大画像を表示)。青っぽい色のメッセージが、BOT が配信している内容です。rss_list.txt に登録された RSS をランダムに読み込んで、conf.inc で設定した間隔でニュースを配信します。

BOT がニュースを配信している様子

その他のコマンドについて解説します。BOT に対して送るコマンドは、デフォルトの設定では、例えば次のようになります。

news_bot:addRSS

conf.inc の設定でいうと、"BOT_NICK + DELIMITER + COMMAND_[コマンド名]" と指定してください。

コマンド addRSS は、RSSURI を指定することで、その RSS が妥当な RSS であり、かつ、リストに登録されていなければ、新規に登録します。

コマンド listRSS は、現在登録されている RSS のリストを Web から見える場所に書き出します(正確にいうと、cp コマンドでコピーした後、リストの URI を書き出します)。

コマンド quit は、BOT を終了させます。

以上の通り、ここではごくごく簡単な BOT を作成してみました。適当なチャンネルにこの BOT を常駐させて、配信されるニュースを暇な時につらつらと眺めるなどすると、RSS リーダを普通に使用するのでは見えてこない面白さがあったりもするのではないかと思います。

ここで使用した Net_SmartIRC には上で例示したもの以外にも様々な機能が用意されていますし、また、その他の PEAR パッケージと組み合わせることで、アイディア次第でさらに便利で面白い BOT を作成することができるのではないかと思います。みなさんがそれぞれでオリジナルな BOT を作成して、ぜひともパク…いや、リスペクトかつオマージュさせてください!

この項、今回作成した IRC BOT の機能を今後拡充するにあたり、PEAR のいろんなパッケージを使い、かつ、Blog 関連のコアなテクノロジをからめつつ、そのあたりひっくるめてお勉強ってなニュアンスで続けていきたいなぁ、ってな所存、だけど、まぁ飽きそう。というわけで、次回に続く…かも。

この項、シリーズ IRC BOT を作ろう!第 2 回「マルコフ連鎖による文章の自動生成」に続く。

*1:RSS_FEED_INTERVAL * 1000), $bot, COMMAND_FEED_NEWS); // RSS を追加 $irc->registerActionhandler(SMARTIRC_TYPE_CHANNEL, '^' . BOT_NICK . DELIMITER . COMMAND_ADD_RSS, $bot, COMMAND_ADD_RSS); // RSS のリストを示す $irc->registerActionhandler(SMARTIRC_TYPE_CHANNEL, '^' . BOT_NICK . DELIMITER . COMMAND_LIST_RSS, $bot, COMMAND_LIST_RSS); // BOT を終了する $irc->registerActionhandler(SMARTIRC_TYPE_CHANNEL, '^' . BOT_NICK . DELIMITER . COMMAND_QUIT, $bot, COMMAND_QUIT); $channel = $bot->_convert(CHANNEL); $irc->connect(SERVER_HOST, SERVER_PORT); $irc->login(BOT_NICK, BOT_NAME); $irc->join(array($channel