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を送る
- クライアントが接続されているかたまに確認する
不思議だったのは、新しく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が凄い。