ちょっと硬派なコンピュータフリークのBlogです。

カスタム検索

2010-09-14

RubyでXchatをもっと便利にしよう! その2

前回の投稿では、XchatにおいてRubyでプラグインを作成する方法について解説したが、かなり説明不足だったように思う。そこで、今日は「自動的に挨拶をする」ボットを作りつつ、Xchatのプラグインを作成する方法を解説しようと思う。同様の方法で、Xchat上にどのようなボットでも作成できるようになるだろう。

rubyenvの設定

今回のボットでは他のライブラリを利用しないが、Xchatプラグインにおいて他のライブラリをrequireするには、rubyenvというファイルを作成しなければならない。なので、おまじないのように次のコマンドを実行しよう。
ruby -e 'puts $:' > ~/.xchat2/rubyenv

チャンネルで発言する

挨拶をするというこことは、チャンネル上で発言をするということである。通常、Xchatを利用している場合、発言は単に入力エリアにメッセージを入力し、Enterで確定するだけであるが、実際にはその裏で/SAYというコマンドが発行されている。Xchatプラグインで発言を行うには、次のようにcommandメソッドを使おう。
def say(words)
    command("SAY #{words}")
  end
commandメソッドは、先頭のスラッシュ(/)を除いた一行のコマンド文字列を引数にとる。わざわざsayメソッドを作ることもないが、上のようにしておけばわかり易いだろう。

他のコマンドも同様の方法で実行することができる。どのようなコマンドがあるかは、Xchat上で/HELPコマンドを実行すれば分かるが、Xchat上で調べるのは一覧性に欠けるので、次のページなどを参照するといいだろう。
https://toxin.jottit.com/xchat_help_commands

誰かがチャンネルに入室した!

挨拶をするボットと言っても、闇雲に挨拶すればいいというものではない。例えばチャンネルに入室したタイミングなどが、挨拶をするのに適しているだろう。そのようなイベントに応答して何らかの処理を実行するにはフックを設定する必要がある。前回はChannel Messageなどのイベントにフックを設定したが、チャンネル参加時はJoinにフックを仕掛ける。フックを仕掛ける場所はinitializeメソッド中がいいだろう。
def initialize
    ...
    hook_print("Join", XCHAT_PRI_NORM, method(:join_hook), "Join.")
    ...
  end
ここでは、join_hookというメソッドにフックをしかけている。誰かがチャンネルにJoin(入室)すると、join_hookメソッドが呼ばれるのだ。join_hookは好きなように定義すればいい。「自動挨拶ボット」では次のようにしている。
def join_hook(words, data)
    return XCHAT_EAT_NONE if away?
    unless @greet_channel_list.include? words[1]
      return XCHAT_EAT_NONE
    end
    say_greetings_or_wb(normalize_nick(words[0]))
    return XCHAT_EAT_NONE
  end
Joinフックでは、words[0]に新たに参加した人のニックネームが、words[1]にチャンネルが格納される。

オマケだが、IRCを利用している人の中にはニックネームをfoo-awayなどのように変更して長時間離籍する人もいる。そのような人がチャンネルに復帰したことを検知するには、ニックネームの変更を追跡する必要があるだろう。そうするには、"Change Nick"イベントにフックを仕掛ければ良い。
def nick_change_hook(words, data)
    return XCHAT_EAT_NONE if away?
    unless @greet_channel_list.include? words[1]
      return XCHAT_EAT_NONE
    end
    away_re = /[\|\_](afk|away|awy|zzz|bbl|brb|out)/i
    if words[0] =~ away_re and not words[1] =~ away_re
      say_greetings_or_wb(normalize_nick(words[1]))
    end
    return XCHAT_EAT_NONE
  end
words[0]には変更前のニックネームが、words[1]には変更後のニックネームが格納されている。

在籍中か否か。

前述の例にはaway?という名前のメソッドが登場した。これは何かというと、自分がaway(離席中)かどうかを調べるメソッドである。Xchatプラグインでは、get_infoというメソッドで各種情報を取得できるようになっている。そのまんまやんけ!とツッコミをいれたくメソッド名であるが、気にせず利用しよう。get_infoは調べたい対象を文字列引数としてとる。自分がawayかどうかを調べるには、get_info('away')という具合に利用する。以下はボットプラグインで定義されているaway?メソッドの中身である。
def away?
    return ((not get_info('away').nil?) or\
            (get_info('nick') =~ /awa?y$|out$|afk$|bb[sl]$|lunch$/))
  end
get_info('away')は、away中であればその理由を文字列で返す。get_info('nick')は自分のニックネームである。自分もmikiya|awayなどというニックネームに変更して離籍することがあるので、そのような場合にはawayであると判定しなければならない。

get_infoで取得できる情報の詳細は、次のページを参照して欲しい。
http://xchat.org/docs/xchat2-perl.html#xchat_get_info

ディレイを入れる

誰かがチャンネルに参加したとき、電光石火で挨拶をしたらすぐにボットだと分かってしまうだろう。人間にはそのような離れ業は出来ないからだ。常識的に考えると、誰かがチャンネルに入ったのを見て、挨拶をタイプするという所作には、少なくとも数秒は掛かるはずである。しかし、ここで注意しなければならないのは、sleepしてはいけないということだ。sleepすると、Xchatの反応が止まってしまう。sleepせずに応答を遅らせるには、hook_timerを使う。
def timed_print(words)
    hook_timer(@delay * 1000, method(:say), words)
  end
このようにすると、@delay秒後に、sayメソッドが呼ばれる。その時の引数はwordsである。hook_timerの第一引数の単位はミリ秒である。

追記:hook_timerにより発動したイベントでは、フック登録時のコンテキスト(サーバーやチャンネルなど)が失われ、現在のチャンネルにメッセージが出力されることが分かった。フック登録時のコンテキストを利用するには、次のようにget_contextでコンテキストを保存し、set_contextを使ってコンテキストを復元しよう。
  def timed_print(words)
    ctx = XChatRuby::XChatRubyEnvironment.get_context()
    data = {
        :words => words,
        :ctx => ctx,
      }
    hook_timer(@delay * 1000, method(:say_with_context), data)
  end

  def say_with_context(data)
    ctx = data[:ctx]
    XChatRuby::XChatRubyEnvironment.set_context(ctx) unless ctx.nil?
    say(data[:words])
  end

チャンネル参加者一覧

get_infoは単一の情報を取得するためのメソッドであり、「今このチャンネルには参加者がどれだけいるか?」というようなリストを返す問いには答えることが出来ない。他の言語用のプラグインではget_listという関数を用いることが出来るのだが、Rubyプラグインではget_listはXChatRubyListというクラスでラッピングされている。次のメソッドは、現在のチャンネルに居る参加者の一覧を得るものである。
def get_users
    users = []
    cur = XChatRuby::XChatRubyList.new("users")
    while cur.next do
      users.push(cur.str('nick'))
    end
    users.sort
  end
下手にラッピングされているのでRubyっぽくないコードになってしまうが、気にせずカーソルを回して情報をゲッツしよう。

独自コマンドの定義

プラグインに命令を伝えるには、独自のコマンドを定義するのが一番の近道である。独自のコマンドを定義するのは非常に簡単だ。次のようにhook_commandメソッドを使って未定義のコマンドにフックを仕掛ければ良い。
def initialize
    ...
    hook_command("GREET", XCHAT_PRI_NORM, method(:handle_command),
                 "Usage: Greet , see /greet help for more info")
    ...
  end
この例ではGREETというコマンドを新たに定義して、handle_commandメソッドをコールバックしている。
def handle_command(words, words_eol, data)
    if words.size < 2
      display_help
    else
      case words[1].downcase
      when "help"
        display_help
      when ""
        display_help
      when "all"
        greet_all
      when "status"
        print_status
      when "list"
        list_nicks
      when "config"
        show_config
      end
    end
    return XCHAT_EAT_NONE
  end
words[0]にはGREETという文字列が、words[1]には一つめの引数が格納されている。words_eolは便利な引数で、コマンドの引数が長い場合、引数全体からwords[x-1]までの部分を取り除いた残りが格納される。words_eol[0]はコマンド全体を表す。

試したい人へ

今回のボットスクリプトは次のページに置いてあるので、試したい人はダウンロードしてほしい。 http://dl.dropbox.com/u/5700319/auto_greets.rb このファイルを$HOME/.xchat2ディレクトリに置いておけば、Xchat起動時に自動的に読み込んでくれる。

次回予告

今回は単純な自動挨拶ボットを作るのに必要な手順を紹介した。ボットを作るのは楽しいが、今回のボットではキー入力を減らす以外の生産性は得られないだろう。次回は、IRCでかわされた会話の再生産性を高めるべく、Groongaを使って全文検索をする方法を紹介したい。

0 コメント:

コメントを投稿