IRC ボットを書きたくなったので調べてみた。 ボットでやりたかったことは発言の保存とメッセージが URL だったらウェブページのタイトルを取得して発言すること。 そのために IRC クライアントの実装の概要を掴めれば良い。

読んだソースは ASPN : Python Cookbook : Connect to an IRC server and store messages into a file 。 ネットワークプログラミングはやったことなかったので知識ほぼゼロからのスタート。


覚えたことあるいは必要になった知識

  • socket プログラミング
  • クライアント サーバ モデルのプログラミング
  • RFC を読む必要性
  • (重要なおまけ) プログラムをハングアップしないようにバックグラウンドで動かす方法

ソースコード

サンプルのソースコードはプライベートメッセージをファイルに保存するという目的で書かれている。

import socket, string

#some user data, change as per your taste
SERVER = '2night.azzurra.org'
PORT = 6667
NICKNAME = 'test_py'
CHANNEL = '#test_py'

#open a socket to handle the connection
IRC = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

#open a connection with the server
def irc_conn():
    IRC.connect((SERVER, PORT))

#simple function to send data through the socket
def send_data(command):
    IRC.send(command + '\n')

#join the channel
def join(channel):
    send_data("JOIN %s" % channel)

#send login data (customizable)
def login(nickname, username='user', password = None, realname='Pythonist', hostname='Helena', servername='Server'):
    send_data("USER %s %s %s %s" % (username, hostname, servername, realname))
    send_data("NICK " + nickname)

irc_conn()
login(NICKNAME)
join(CHANNEL)

while (1):
    buffer = IRC.recv(1024)
    msg = string.split(buffer)
    if msg[0] == "PING": #check if server have sent ping command
        send_data("PONG %s" % msg[1]) #answer with pong as per RFC 1459
    if msg [1] == 'PRIVMSG' and msg[2] == NICKNAME:
        filetxt = open('/tmp/msg.txt', 'a+') #open an arbitrary file to store the messages
        nick_name = msg[0][:string.find(msg[0],"!")] #if a private message is sent to you catch it
        message = ' '.join(msg[3:])
        filetxt.write(string.lstrip(nick_name, ':') + ' -> ' + string.lstrip(message, ':') + '\n') #write to the file
        filetxt.flush() #don't wait for next message, write it now!

socket を使った通信

まずは、socket を使って IRC サーバと通信する用意。 ソケット オブジェクト IRC を作成して irc_conn() で IRC サーバと接続を確立する。

#open a socket to handle the connection
IRC = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

#open a connection with the server
def irc_conn():
    IRC.connect((SERVER, PORT))

socket とはネットワークを介してデータを交換する為のインターフェース。 正確にはプロセス間通信と呼ばれるが IRC クライアントを書く場合には IRC サーバとデータをやりとりするためのモジュールだと思って構わないだろう。

irc_conn() に続いて login() で IRC サーバへログインした後 join() でチャネルへ入る。

irc_conn()
login(NICKNAME)
join(CHANNEL)

サーバとデータを交換するのに常に socket オブジェクトを利用する。 サンプルのソースコードでは IRC オブジェクトだ。 login(), join() 関数は send_data() 関数を呼び出している。 send_data()IRC.send() でサーバへデータを送っている。 つまり socket を使ってデータを送っているのだ。

#simple function to send data through the socket
def send_data(command):
    IRC.send(command + '\n')

send_data() でサーバへ送るメッセージは RFC を読んで理解しなければいけない。 login() 関数でサーバとの接続を確立する際、NICK メッセージを送っているが RFC では次のようなフォーマットでメッセージを送るようになっている。

NICK <nickname> [<hopcount>]

hopcount は無視できるが、nickname が衝突した場合の処理が無い。 nickname が衝突したときには KILL メッセージが送られてくる。 また、login() 関数でのメッセージの送信順序も推奨されているものとは違う。 推奨されている順番は USER, NICK である。

応答を待つ無限ループ

join() できたらサーバから応答を処理する無限ループに入る。

while(1):

無限ループで応答待ち状態を作るということだ。このループ内でサーバの応答を取得する。

buffer = IRC.recv(1024)

受け取ったデータを RFC に従って煮るなり焼くなりすればクライアントの役目を果たせる。

PING メッセージが送られてきたら PONG メッセージを返信

RFC によるとサーバはクライアントの接続を検知する為に一定間隔で PING メッセージをクライアントへ送信します。 PING メッセージを受け取ったクライアントは接続が維持されていることを示す為に PONG メッセージで返信します。 PONG メッセージを送らなければ切断されます。

msg = string.split(buffer)
if msg[0] == "PING": #check if server have sent ping command
    send_data("PONG %s" % msg[1]) #answer with pong as per RFC 1459

クライアント サーバ モデルのプログラム

クライアント サーバのシステムではクライアントはサーバのレスポンスを RFC などの仕様に従って処理すれば良い。 仕様に無いことは自分で考えます。 サンプルのソースコードですとサーバのメッセージを扱いやすいように string.split() を利用してリストを作っていますが、 このような実装は RFC で定義されているわけではないのでクライアントが適当に実装すればよいということです。 モデルとしてはウェブアプリケーションでウェブサーバとウェブブラウザの関係と同じです。

番外編

今回の目的である IRC クライアントの実装の概要からちょっとはずれた事柄。

URL のページタイトルを取得する

応答待ちのループ内で urllib なんかを使ってタイトルを取得してメッセージを送れば良い。ただ、取得したタイトルは NOTICE メッセージとして他のクライアントに迷惑をかけないようにするのが望ましいかな。

メッセージの文字コード

RFC で定められていないが日本語を扱うときは慣例的に ISO-2022-JP なので、PRIVMSG/NOTICE で送るテキストは ISO-2022-JP にしてやる。

バックグラウンドで動かす

ボットとして動作させるのでログアウトしてもプログラムは動作してもらわないと困る。 かといってデーモンプロセスに仕上げるのも面倒なので、とりあえず SSH を切断したり端末を終了してもバックグラウンドジョブとして生き残らせるために、nohup コマンドを使って3つの I/O ストリームをリダイレクトしてプログラムを実行する。 とくに標準入力は /dev/null にリダイレクトしておくことで SSH 切断時の問題を回避できることがわかった。 詳細は ログアウトしてもバックグラウンド ジョブを継続する方法 を見よ。

ネットワーク プログラミング フレームワーク

Twisted という Python で書かれたネットワーク プログラミングのフレームワークがある。


最終更新日: 2015年05月16日(土)


Back to top