IRC ボットを書きたくなったので調べてみた。 ボットでやりたかったことは発言の保存とメッセージが URL だったらウェブページのタイトルを取得して発言すること。 そのために IRC クライアントの実装の概要を掴めれば良い。
読んだソースは ASPN : Python Cookbook : Connect to an IRC server and store messages into a file 。 ネットワークプログラミングはやったことなかったので知識ほぼゼロからのスタート。
サンプルのソースコードはプライベートメッセージをファイルに保存するという目的で書かれている。
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 を使って 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 に従って煮るなり焼くなりすればクライアントの役目を果たせる。
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 クライアントの実装の概要からちょっとはずれた事柄。
応答待ちのループ内で urllib
なんかを使ってタイトルを取得してメッセージを送れば良い。ただ、取得したタイトルは NOTICE メッセージとして他のクライアントに迷惑をかけないようにするのが望ましいかな。
RFC で定められていないが日本語を扱うときは慣例的に ISO-2022-JP なので、PRIVMSG/NOTICE で送るテキストは ISO-2022-JP にしてやる。
ボットとして動作させるのでログアウトしてもプログラムは動作してもらわないと困る。
かといってデーモンプロセスに仕上げるのも面倒なので、とりあえず SSH を切断したり端末を終了してもバックグラウンドジョブとして生き残らせるために、nohup コマンドを使って3つの I/O ストリームをリダイレクトしてプログラムを実行する。
とくに標準入力は /dev/null
にリダイレクトしておくことで SSH 切断時の問題を回避できることがわかった。
詳細は ログアウトしてもバックグラウンド ジョブを継続する方法 を見よ。
Twisted という Python で書かれたネットワーク プログラミングのフレームワークがある。
最終更新日: 2017年07月23日(日)