Skypeのグループチャットにbotを放ったり色々したかったので作った。
Skype APIはSkypeが起動しているマシンの中でSkypeのプログラムと自分のアプリを通信させて使う物で、developer.skype.comにAPIのドキュメントがある。電話をかけたりチャットの中身を取得したり発言したりできる。
Skypeが起動しているマシンでskype socket gatewayを起動しておくと、他のマシンからもtelnetでSkype APIが使える。また、全チャット(含むグループチャット)のデータがJSONでどんどん流れてくるようになっている。
うちではWindows XPのネットブックでbotアカウントでSkypeを起動してあった。Skype chatはP2Pなのでログをそれぞれが持っていて、後で接続してきた人は先に居た人から取得する様になっている。だから、ただ何もしなくてもずっとオンラインの人がいるだけでログの流れが良くなる。
で、そのbotアカウントを真なるbotにするために、Windows用のActiveScriptRuby 1.8.7でskype socket gatewayを動かしてそこに別のLinuxのサーバーから複数のbotが接続して色々処理をしている。
Linux上で動くSkype用のbotを作る方法 – muddy brown thangにXvfbを使ってlinuxで仮想フレームバッファを作ってSkype APIを使う方法が解説されてるけど、なんかLinuxが難しくて挫折したのでwindowsでも動くsocket gatewayを作った。
■ソースコード
githubに全部置いておいた
shokai's skype-socket-gateway at master – GitHub
■インストール
git clone git://github.com/shokai/skype-socket-gateway.git
必要なgemもインストールする
gem install Ruby4Skype json eventmachine
Ruby4Skypeというのを使っているので、Windows、Mac、Linuxで動く。ただしRuby4Skypeが64bit MacOSXでは動かない。Linuxも、Ruby4Skypeのlib/skype/os/linux.rbを修正してinitialize methodのsuperにapp_nameを渡すように修正しないと動かない。
それとWindowsでRuby4Skypeを動かすには色々DLLを入れないとダメ →
WinXPでgem install Ruby4Skypeするメモ – yuisekiのいまさらruby厨日記 – Rubyist
■起動する
先にSkypeを起動しておく。
ruby skype-gateway.rb
でport 20000で起動する。
■使う
telnetで接続する。
telnet 192.168.1.37 20000
そしてそのまま
CALL shokaishokai
MESSAGE shokaishokai hello work!!
とか書く。Skype APIがそのまま実行できる。
レスポンスはJSONで来る。
あと、接続しているだけで全てのchatの発言が流れてくる。
■Windowsのスタートアップに登録する
run_skype_gateway.bat というバッチファイルを作る
"C:\Program Files\ruby-1.8\bin\ruby.exe" "C:\Document and Settings/sho/src/ruby/skype-socket-gateway/skype-gateway.rb"
中身はこの1行でいい。これをスタートメニューのスタートアップに入れておけば自動起動できる。
■Skype botを作る
RubyだとTCPSocketクラスを使うと簡単に作れる。
キーワードに反応する単純なbotの例。誰かが「ざんまい」と書くと、即座に「ざんまい行きたい!」と返す。
samples/zanmai_bot.rb at master from shokai's skype-socket-gateway – GitHub
require 'rubygems'
require 'socket'
require 'json'
require 'eventmachine'
$KCODE = 'u'
HOST = "192.168.1.37"
PORT = 20000
begin
s = TCPSocket.open(HOST, PORT)
s.puts "MESSAGE shokaishokai ざんまいbot start"
rescue => e
STDERR.puts e
exit 1
end
EventMachine::run do
EventMachine::defer do
loop do
res = s.gets
exit unless res
res = JSON.parse(res) rescue next
p res
if res['type'] == 'chat_message'
if res['body'] =~ /ざんまい/ # キーワードに反応
s.puts "CHATMESSAGE #{res['chat']} ざんまい行きたい!"
elsif res['body'] =~ /かず(すけ|助)/
s.puts "CHATMESSAGE #{res['chat']} かずにゃんぺろぺろ"
end
end
end
end
EventMachine::defer do
loop do
mes = gets
s.puts mes if mes
end
end
end
これにもう少し語彙を追加すると、だいたい俺の人格と機能を8割方再現したbotが作れる。
shokai_bot.rb at master from shokai's skype-bots – GitHub
あと、全chatをmongo dbに入れて検索したりできるbotなんかもいる。超便利。
tools/skype_chat_store_mongod.rb at master from shokai's skype-socket-gateway – GitHub
はてなランドに自分のskypeの発言を送るなんて事もできる
hatenaland_proxy.rb at master from shokai's skype-bots – GitHub
■EventMachine
EventMachineというサイバーパンクな名前のライブラリがRubyにあって、これを使うと並行処理が簡単に書けた。
skype socket gatewayでは
- 標準入力をSkype APIとして実行する
- 接続してきたTCP Socketクライアントそれぞれからの入力をSkype APIとして実行し、結果をそれぞれに返す
- 全ソケットクライアントに全chat logを送る
- クライアントが接続されているかたまに確認する
という処理をやっていて、これらを適当にEventMachine::defer doの中でloopで書いてやればうまく動いた。
不思議だったのは、新しくacceptしたsocket clientオブジェクトそれぞれを個別に処理するために
EventMachine::deferの中でclient毎にそれぞれEventMachine::deferを作ってみたら、ちゃんとスコープが別になってくれた。
EventMachine::defer do
loop do
s = sock.accept
clients << s
puts "--- new client : #{clients.size}"
# socket -> invoke Skype API
EventMachine::defer do
loop do
cmd = s.gets
next if cmd == nil or cmd.to_s.size < 1
puts "recv => #{cmd}"
begin
p res = {
:type => 'api_response',
:body => skype.invoke(cmd).to_s
}
rescue => e
res = {
:type => 'error',
:body => 'skype api invoke error'
}
STDERR.puts e
end
begin
s.puts "\n"+res.to_json
rescue => e
STDERR.puts e
c.close
break
end
end
end
end
end
それぞれのdeferの中でAPI呼び出しと結果を返す処理が動いている。これをthreadでやる時
Thread::start(s) do |s|
loop do
res = s.gets
# 何か処理
s.puts response.to_json
sleep 0.01
end
end
みたいに引数に渡さなくても勝手にスコープ分けてくれてた。
でも、
clientの配列とかchatメッセージのqueueを作って複数のEventMachine::deferからdeleteしたりpushしたりしていじっている部分では、defer間で共有されていた。不思議だ。
EventMachine::run do
# forward all skype chats -> socket
EventMachine::defer do
loop do
if chat_msgs.size > 0
msg = "\n"+chat_msgs.shift.to_json
clients.each{|c|
begin
c.puts msg
rescue => e
STDERR.puts e
end
}
end
end
end
# check clients connection
EventMachine::defer do
loop do
msg = ''
errors = Array.new
clients.each{|c|
begin
c.puts msg
rescue => e
STDERR.puts e
errors << c
end
}
errors.each{|c|
clients.delete(c)
c.close
}
sleep 15
end
end
よくわからないけどEventMachineが凄い。