こんばんは、最近なんだかバテ気味の若手ブログ初登場、nakazawa-kです。 よろしくお願いします。 どうやらIRCボットのコンテストをやるらしいと聞いて息巻き、大急ぎでざくっと実装してから実に2ヶ月ほど寝かせてしまったボットを投下してみます。 最初のお題が出た瞬間に思いついたのが「Pythonボット」でした。『Pythonで書かれた』という意味ではなく『Pythonを実行してくれる』ということです。
(nakazawa-k) >>>print "hello!"
(bot) hello!
こういう風にIRCがPythonコードであふれたら素敵だと思いませんか!? 私は思います。作りましょう。これで機能が決まりました。 次は超重要、名前です。 みなさんパイソンといえば何を思い浮かべるでしょう。 ニシキヘビ? いえいえパイソンといえばモンティ・パイソンです。テリー・ギリアムです。 Pythonを実行してくれるボットの名前にモンティ以上のものはないでしょう。 ということでPythonで書かれたPythonコードを実行してくれるモンティボットを作ってみました。 実装を始めるにあたり、まず目標をKLabの社内にある「どぶろく制度」を使って『1時間以内にサクッと動くようにする』と設定しました。 ちなみに「どぶろく制度」とは標準業務時間の10%を好きに使い、上司に断ることなく自分の興味が赴くまま研究や開発を行えるというものです。Googleの20%ルールや3Mの15%ルール(こちらは不文律ですね)と似たものです。 標準業務時間の10%とは、だらだらとやっていてはすぐに過ぎ去ってしまう位です。KLab入社間もなかったnakazawa-kにとって、IRCボットは打って付けのネタでした。 閑話休題。目標が決まったのでとにかくシンプルな実装を目指していきました。 IRCのプロトコルにも多少興味はあったのでRFC1459を流し読みし、その上でIRC接続ライブラリとしてPython IRC libraryを利用しました。 ※ソースは記事の最後に貼り付けてあります。 使い方
$ ./monty.py server[:port] #channel nickname
いじり方
(nakazawa-k) >>>self.v = range(1,101) (nakazawa-k) >>>print self.v (monty) [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100]
こういう感じで、>>>に続けてPythonの式を書くと評価してくれます。printは結果を取得してIRCへ流してくれます。 「あの実装どうやるんだっけ?誰か教えてー」 という質問にサンプルコードを返信するだけではあまりテンションが上がりませんが、コードの先頭に">>>"を書くだけで実行結果を即確認出来超ハイテンションになれます。これで皆もっとPythonが好きになってくれるはずです!!
超簡単な仕組み SingleServerIRCBotクラスを継承すると、チャンネルでの発言時にon_pubmsg()が呼ばれます。この中に">>>"で始まるものを見つけると、右から左でPythonコードとして実行しています。 これだけでもPythonの実行権限で出来ることは、実質相当色々出来てしまいます。それこそファイルの作成や削除など、結構思いのままなので専用VMを作成して走らせています。
応用例 若手ブログ未登場のkさんによる「チャンネル内のユーザからなると(op権限)を奪いまくる」ボットの機能を模倣してみる
>>>self.excp="logbot" >>>[(nick not in [self.excp,self.nick]) and (c.notice(self.channel, nick + ' is dead'), c.mode(self.channel, "-o "+ nick)) or 1 for nick in [u for u in self.channels.items()[0][1].users()]]
発言が行われたチャンネルで、指定nickと自分以外の全ユーザからop権限を剥奪してくれます。 このように、MontyはPythonの式として書けるものなら、非常に多くの処理を自由に実行することが出来ます。
残念なところ ・execでのコード実行は、あまりにもフリーダムすぎる →PyPyベースのsandbox環境へ持ち込みたい。ただ、既存ライブラリから完全に切り離された環境ではあまり面白いことが出来ないのでほどほどに・・・。 例えば
(nakazawa-k) >>>weather (monty) =六本木付近の天気予報= 8/19晴れ 8/20曇りのち雨のち晴れのち小雨
こういうことが出来ると段々夢が広がってくるじゃないですか!
夢破れて・・・ 1) 当初はインタラクティブシェルをそのままIRC上へ持ち込み、謎のIRCペアプログラミング(複数人が1つのインタラクティブシェルを使ってコードを開発するというすさまじい共同作業)などをやりたいなーと夢想しました。まあこれは洒落なので実際にやりたければscreenを使うのが近道でしょう。 2) 当初はpopenして適宜入出力をパイプ取得すれば良いと考えましたが、そうそう素直にstdoutへ各行出力をしてくれるわけではありませんでした。ターミナルの機能を内部でそこそこ使っていたりと結構複雑化するポイントが垣間見えました。それでは手軽に書いてサクッと動かすという主旨に反してしまうので泣く泣く断念。
最後に マルチチャンネル非対応のため、社内の技術雑談チャンネルで放し飼いにしています。 ちょっと残念なコードを食べさせるとinternal exceptionといって実行を放り出してしまうドジッ子(いえ、コードを書いた人のほうがドジッ子なんです)ですがKLab IRCサーバへお立ち寄りの際(!?)は可愛がってあげてください。
#! /usr/bin/env python

from ircbot import SingleServerIRCBot
from irclib import nm_to_n, nm_to_h, irc_lower, ip_numstr_to_quad, ip_quad_to_numstr
import random
import re
import os
import sys
import StringIO
import time

class Monty(SingleServerIRCBot):
    def __init__(self, channel, nickname, server, port=6667):
        SingleServerIRCBot.__init__(self, [(server, port)], nickname, nickname)
        self.nick = nickname
        self.channel = channel

    def on_nicknameinuse(self, c, e):
        c.nick(c.get_nickname() + "_")
        self.nick += "_"

    def on_welcome(self, c, e):
        c.join(self.channel)

    def on_pubmsg(self, c, e):
        nick = nm_to_n(e.source())
        matched = re.match(r">>>(.*)", e.arguments()[0])
        if matched != None:
            outputBuffer = StringIO.StringIO()
            sys.stdout = outputBuffer
            exceptionBuf = ''
            try:
                exec matched.group(1)
            except:
                exceptionBuf = sys.exc_info()[0]
                print "internal exception"
            c.notice(self.channel, outputBuffer.getvalue())
            sys.stdout = sys.__stdout__
            print exceptionBuf
        return

def main():
    import sys
    if len(sys.argv) != 4:
        print "Usage: testbot   "
        sys.exit(1)

    s = sys.argv[1].split(":", 1)
    server = s[0]
    if len(s) == 2:
        try:
            port = int(s[1])
        except ValueError:
            print "Error: Erroneous port."
            sys.exit(1)
    else:
        port = 6667
    channel = sys.argv[2]
    nickname = sys.argv[3]

    bot = Monty(channel, nickname, server, port)
    bot.start()

if __name__ == "__main__":
    main()