俺が思いついたSkypeBotのシステムを紹介してみる。いやすでにダイスロール用BOTとか公開してるんだけど、SkypeBotとして最小限のものだけシンプルに組んでみた。エラー処理とか全然してないし、とにかくBotとしての最小構成システムとするのが目的。

これ設計書。

こんな風になる。

これコード。180行くらい。解説みたいなもんは下に。

# coding: utf-8

'''
最小構成のSkypeBotセット。
WatchDogクラスがSkypeのDBを監視して更新があったらSimpleSkypeBotを呼び出す。
SimpleSkypeBotクラスは発言の内容を調べて対応した内容をSkypeへ送る。
'''
author = 'Midoriiro'
date = '2016.09.07.'

import sqlite3
import requests
import sys
import random
import time
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer

# ==============================
# 設定
# ==============================

# keyを含む発言に反応しvalueを返す。
conf_pattern = {
    'how are you': 'BOT: so fine.',
    'who are you': 'BOT: i am bot.',
    'hello'      : 'BOT: hi.',
    'bye'        : 'BOT: take care.',
}

# skype for windowsのDBがあるディレクトリのパス
conf_dbDirPath = 'C:/Users/{ユーザ名}/AppData/Roaming/Skype/{垢名}'

# skype for web httpの送信先。httpヘッダを調べて書いてね。
conf_url = '後述'

# http用のトークン。httpヘッダを同上。
conf_token = '後述'

# ==============================
# 設定ここまで
# ==============================

# session
session = requests.session()
session.post(conf_url)
# BOT起動時のタイムスタンプ
startTimestamp = round(time.time())
# 反応済みIDが入るリスト
doneIdList = []
# skype for webへ送るリクエストヘッダ。
headers = {
    'Accept'            :'application/json, text/javascript',
    'Accept-Encoding'   :'gzip, deflate',
    'Accept-Language'   :'ja,en-US;q=0.8,en;q=0.6',
    'BehaviorOverride'  :'redirectAs404',
    'Cache-Control'     :'no-cache, no-store, must-revalidate',
    'ClientInfo'        :('os=Windows; osVer=7; proc=Win32; lcid=en-us;'
            + ' deviceType=1; country=n/a; clientName=skype.com;'
            + ' clientVer=908/1.42.0.98//skype.com'),
    'Connection'        :'keep-alive',
    'ContextId'         :'tcid=146372019467711519',
    'Content-Type'      :'application/json',
    'Expires'           :'0',
    'Host'              :'client-s.gateway.messenger.live.com',
    'Origin'            :'https://web.skype.com',
    'Pragma'            :'no-cache',
    'Referer'           :'https://web.skype.com/ja/',
    'User-Agent'        :('Mozilla/5.0 (Windows NT 6.1)'
            + ' AppleWebKit/537.36 (KHTML, like Gecko)'
            + ' Chrome/50.0.2661.102 Safari/537.36'),
    'RegistrationToken' :conf_token,
}

class SimpleSkypeBot:
    '''main.dbから発言の内容を取得し、対応する内容をSkypeへ送る。'''

    def main(self):
        '''トップレベルメソッド。'''
        # たまにsqlite3.OperationalError: disk I/O errorが出るので
        # そんときは処理をやり直すためのtry,except。たぶん邪道。
        while True:
            try:
                recordList = self.selectRecordList()
                break
            except sqlite3.OperationalError:
                print('sqlite3.OperationalErrorが出たヨ。')
                continue
        if not recordList:
            return False
        for record in recordList:
            # 発言の内容によって返答を作る。
            reply = self.getReply(record['body_xml'])
            if not reply:
                return
            # 反応済みリストにidを追加する。
            doneIdList.append(record['id'])
            # 返答をスカイプへ送信する。
            self.sendSkype(reply)
        return

    def selectRecordList(self):
        '''main.dbからレコードを取得する。'''
        # connectionをグローバルで作るとマルチスレッドエラーになっちゃうのでここで作る。
        connection = sqlite3.connect(conf_dbDirPath + '/main.db')
        cursor = connection.cursor()
        # SQLの「body_xmlにconf_patternの内容を含む」部分を作る。
        # AND (1=0 OR `body_xml` LIKE '%key%' OR `body_xml` LIKE '%key%')
        # こんな感じの。
        likePart = ''
        if conf_pattern:
            likePart = 'AND (1=0 '
            for key in conf_pattern:
                likePart += 'OR `body_xml` LIKE ¥'%%%s%%¥' ' % key
            likePart = likePart + ')'
        # SQLの「反応済みリストのIDを除く」部分を作る。AND `id` NOT IN (**,**)
        # こんな感じの。
        idPart = ''
        if doneIdList:
            idPart = 'AND `id` NOT IN ('
            for doneId in doneIdList:
                idPart += str(doneId) + ','
            idPart = idPart[0:-1] + ')'
        # 発言を取得するSQL。
        # 「BOT起動時のタイムスタンプ後」「body_xmlにconf_patternの内容を含む」
        # 「反応済みリストのIDを除く」というSQL。
        sql = ('SELECT id,body_xml FROM `Messages` '
            + 'WHERE `timestamp`>? %s %s' % (likePart, idPart))
        bind = (startTimestamp,)
        # 取得する。
        cursor.execute(sql, bind)
        trash = cursor.fetchall()
        # コネクション閉じる。
        connection.close()
        # 成形して返す。
        if not trash:
            return False
        else:
            return self.assoc(trash, ['id', 'body_xml'])

    def assoc(self, trash, columns):
        '''いつものsqlite3モジュール補助。
        [[1,A][2,B]]ってなってるのを{{id:1,name:A},{id:2,name:B}}ってディクショナリに。'''
        rows = []
        for i in range(len(trash)):
            rows.append({})
            for j in range(len(trash[i])):
                rows[i][columns[j]] = trash[i][j]
        return rows

    def getReply(self, body_xml):
        '''body_xmlの内容に従って返答を返す。'''
        for key,value in conf_pattern.items():
            if key in body_xml:
                return value
        return False

    def sendSkype(self, reply):
        '''skype for webに送信する。'''
        postjson = ('{' +
            'content        : "%s",' % reply +
            'clientmessageid: "%s",' % random.randint(1000000000000,
                9999999999999) +
            'messagetype    : "RichText",' +
            'contenttype    : "text",' +
        '}')
        session.post(conf_url, data=postjson, headers=headers)
        return True

class WatchDog(FileSystemEventHandler):
    '''ファイルの変更を感知したらSkypeBotオブジェクトのmainメソッドを走らせる。'''
    def on_modified(self, events):
        '''ファイルに変更(スカイプに発言)があったらSkypeBotオブジェクトの動作開始。'''
        if events.src_path.endswith('main.db'):
            bot.main()
            return

if __name__ in '__main__':
    bot = SimpleSkypeBot()
    dog = WatchDog()
    observer = Observer()
    observer.schedule(dog, conf_dbDirPath, recursive=True)
    observer.start()
    observer.join()
conf_urlに書くもの
skype for webでメッセージ送ったときのhttpリクエストURL。
conf_tokenに書くもの
skype for webでメッセージ送ったときのhttpリクエストヘッダの中にあるRegistrationTokenの値。'registrationToken=なんちゃらなんちゃら'ってやつ。めっちゃ長い。
requestsモジュールが必要
Python34/Scriptsで pip install requests
watchdogモジュールが必要
Python34/Scriptsで pip install watchdog
問題点1: BOTの発言に日本語を含めることができない
文字コードエラーによるもの。半角英数字は送れるので十分と判断し放置してる。
問題点2: sqlite3にアクセスする際たまにdisk I/Oエラーが出る
原因全然わかんない。お手上げ。(俺の見た目には)同じ条件で出たり出なかったりする。再現できないのでどうにもこうにもならない。勘弁して。苦肉の策で、その部分を無限ループにし、データ取得に成功したら抜け、エラーが出る限りずっとアクセスさせる、っつー情けない方法を採用してる。

今回は最低限の機能ってことで、ある文字列を含む発言がきたらそれに対応する応答を返す、ってだけの機能だけど、複雑なことしたかったらgetReplyメソッドの中にいろいろごちゃごちゃ書いてゆけばよい。冒頭のダイスロール用SkypeBotでもそうしている。

いつものコトだけど素人のやることだから多分バリ邪道だろう。デスクトップアプリのDBからログとってインターネットアプリにヘッダ偽造したhttpリクエスト飛ばすとか…。でも思いついたときはテンションだだ上がりで書くのも楽しめた。ぶっちゃけwatchdogモジュールはこんなスクリプトに使うのは宝の持ち腐れって感じだ。osモジュールとかでファイルの更新日時を1秒ごとくらいに取得すればいいんだし。でもまあ使ったことないモジュールだったし、watchdogって名前がなんか良くて使いたかった(犬派)。むしろこのモジュールのせいでマルチスレッドエラーとかで詰まりまくったとこあるけどな!(selectRecordListメソッドあたり参照)