Slack,HipChat,IRCで利用されているXMPPプロトコルの解説と実装

『アッオー』でおなじみICQからIRC,Yahooメッセンジャー,HipChat,Slack,GoogleTalkの共通点は、すべて通信にXMLベースのXMPPプロトコルを利用していることです。インスタントメッセージサービスは目まぐるしく移り変わっていくため、変わらないコアの部分を抑えることが重要だと思います。

XMPPプロトコルを喋ると、大半のメッセージサービスを利用できるBotが作れます。この記事ではXMPPを開発・公開したJabber社のライブラリを利用して複数のメッセージサービスに対応したBotを実装していきます。

# 作るもの
起動したらHipChatとSlackに両方ログインしていて、話かけたらdeployしたりコマンド実行したりするBot。海外はSlack、国内はHipChatを使う特殊事例が発生して開発することになりました。~~過渡期つらい~~ 新しいものを、どんどん取り入れていくことは良いことだと思います。

■ Slack
![スクリーンショット 2015-11-30 18.02.01.png](https://qiita-image-store.s3.amazonaws.com/0/65312/3b1440df-7f95-903c-2f7b-e5a506e0ed62.png)

![スクリーンショット 2015-11-30 16.14.11.png](https://qiita-image-store.s3.amazonaws.com/0/65312/78e439cb-af40-00d7-b8d6-e4143d1575b6.png)

■ HipChat
![スクリーンショット 2015-11-30 16.16.15.png](https://qiita-image-store.s3.amazonaws.com/0/65312/a0cbf304-328b-c639-9b23-413cd0208372.png)




# 2分で判るXMPPプロトコルの概要
■ [rfc3920](http://www.ietf.org/rfc/rfc3920.txt)で仕様公開されている。
■ Jabber社が開発したXMLベースの通信プロトコル
■ 接続に必要な情報は次の5点セット
![スクリーンショット 2015-11-30 17.37.57.png](https://qiita-image-store.s3.amazonaws.com/0/65312/f9e52ad2-4430-c360-f3c9-2a664b4a885a.png)

```xml:通信フォーマットはXML
Server: <stream>
<message>...</message>
Client: <stream>
<message>...</message>
Server: <message>...</message>
Client: <message>...</message>
Server: <message>...</message>
Client: <message>...</message>
Server: </stream>
Client: </stream>
```


■ Slackで利用するには、AdminでXMPP GateWayを有効にして各アカウントで払い出しする
■ HipChatでは、デフォルト設定のままで利用可能。

# XMPPを作ったのはJabber社
Jabberのライブラリ`jabberbot`を使うと簡単にXMPPを喋るBotが実装できます。

## install
```py:install
pip install jabberbot
pip install xmpppy
pip install lazy-reload
pip install requests
pip install simplejson
```

## SlackとHipChatに対応したBot

```py:bot.py
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals

import sys
import time
import traceback
import logging
from jabberbot import botcmd, JabberBot, xmpp
import multiprocessing as mp


class ChatBot(JabberBot):
    """
    XMPP/Jabber botを利用してHipChatのメッセージサービスに接続する
    """

    _content_commands = {}
    _global_commands = []
    _command_aliases = {}
    _all_msg_handlers = []
    _last_message = ''
    _last_send_time = time.time()
    _restart = False

    def __init__(self, config):
        self._config = config
        channel = config['connection']['channel']
        username = u"%s@%s" % (config['connection']['username'],
                               config['connection'].get('host',
                                                        config['connection']['host']))
        self._username = username
        super(ChatBot, self).__init__(
            username=username,
            password=config['connection']['password'])

        self.PING_FREQUENCY = 50  # timeout sec
        self.join_room(channel, config['connection']['nickname'])
        self.log.setLevel(logging.INFO)

    def join_room(self, room, username=None, password=None):
        """
        ChatRoomにjoinする
        """
        NS_MUC = 'http://jabber.org/protocol/muc'
        if username is None:
            username = self._username.split('@')[0]
        my_room_JID = u'/'.join((room, username))
        pres = xmpp.Presence(to=my_room_JID)
        if password is not None:
            pres.setTag(
                'x', namespace=NS_MUC).setTagData('password', password)
        else:
            pres.setTag('x', namespace=NS_MUC)

        # Join時にメッセージ履歴を読まない
        pres.getTag('x').addChild('history', {'maxchars': '0',
                                              'maxstanzas': '0'})
        self.connect().send(pres)


    def callback_message(self, conn, mess):
        """
        メッセージを受信すると実行される
        """
        _type = mess.getType()
        jid = mess.getFrom()
        props = mess.getProperties()
        text = mess.getBody()
        username = self.get_sender_username(mess)

        print "callback_message:{}".format(text)
        print _type
        # print jid
        print props
        print username
        super(ChatBot, self).callback_message(conn, mess)


        # 問い合わせに特定文字列が入ってたら応答する
        import time
        import random
        time.sleep(1)
        if '願' in text:
            ret = ["りょ。ちょいまってねー",
                   "しばしお待ちを",
                   "http://rr.img.naver.jp/mig?src=http%3A%2F%2Fimgcc.naver.jp%2Fkaze%2Fmission%2FUSER%2F20141001%2F66%2F6169106%2F58%2F186x211xe15ffb8d1f6f246446c89d7e.jpg%2F300%2F600&twidth=300&theight=600&qlt=80&res_format=jpg&op=r",
                   "http://e-village.main.jp/gazou/image_gazou/gazou_0053.jpeg"]
            self.send_simple_reply(mess, random.choice(ret))

        if 'バグ' in text:
            ret = ["了解で後で直しておきます",
                   "認識しました",
                   "このあとの会議終わってから対応します。",
                   "最新版だと直っているので反映しておきます。"]
            self.send_simple_reply(mess, random.choice(ret))

    def send_message(self, mess):
        """Send an XMPP message
        Overridden from jabberbot to update _last_send_time
        """
        self._last_send_time = time.time()
        self.connect().send(mess)

def bot_start(conf):
    print "++++++++"
    print conf
    print "++++++++"
    bot = ChatBot(conf)
    bot.serve_forever()


class ChatDaemon(object):
    config = None

    def run(self):
        try:
            # Start Slack Bot
            process_slack = mp.Process(target=bot_start, args=(self.config_slack,))
            process_slack.start()

            # Start HipChat Bot
            process_hipchat = mp.Process(target=bot_start, args=(self.config_hipchat,))
            process_hipchat.start()
        except Exception, e:
            print >> sys.stderr, "ERROR: %s" % (e,)
            print >> sys.stderr, traceback.format_exc()
            return 1
        else:
            return 0


def main():
    import logging
    logging.basicConfig()
    config_slack = {
        'connection': {
            'username': '{{name}}',
            'password': '{{password}}',
            'nickname': '{{name}}',
            'host': '{{TeamName}}.xmpp.slack.com',
            'channel': '{{RoomName}}@conference.{{TeamName}}.xmpp.slack.com',
        }
    }
    config_hipchat = {
        'connection': {
            'username': '{{name}}',
            'password': '{{password}}',
            'nickname': '{{nickname}}',
            'host': 'chat.hipchat.com',
            'channel': '{{RoomName}}@conf.hipchat.com',
        }
    }

    runner = ChatDaemon()
    runner.config_slack = config_slack
    runner.config_hipchat = config_hipchat
    runner.run()


if __name__ == '__main__':
    sys.exit(main())

```

## 起動するとBotがログインする
![スクリーンショット 2015-11-30 17.34.18.png](https://qiita-image-store.s3.amazonaws.com/0/65312/7e890c12-62e8-ea55-e145-945ac5b6e8a4.png)

## HipChatとSlackから話かけてみた
![スクリーンショット 2015-11-30 16.38.34.png](https://qiita-image-store.s3.amazonaws.com/0/65312/696371b9-fa89-c342-051b-15d55747515e.png)

## メッセージを受け取れた
![スクリーンショット 2015-11-30 16.38.06.png](https://qiita-image-store.s3.amazonaws.com/0/65312/bc5baa59-ed94-3484-c4c5-5956b9ee7040.png)

# 設定情報の払い出し
■ 1.Slack
SLACKはadminでXMPPのGateway有効にしないと駄目
https://{{TeamName}}.slack.com/account/gateways

■ 2.HipChat
https://{{組織名}}.hipchat.com/rooms/show/{{room_id}}

# 参考
[rfc3920](http://www.ietf.org/rfc/rfc3920.txt)
[XMPP(Jabberのプロトコル)技術メモ](http://dev.ariel-networks.com/column/tech/xmpp/view)
[一般ユーザー向けXMPP入門ページ（仮）](http://www.im-net.org/sp/xmpp/)


# まとめ
もし新しくインスタントメッセージサービスを導入検討する際は、XMPPプロトコルに対応しているかの観点で確認するとエンジニアが幸せになるかもしれません。

`callback_message関数` に`os.subprocess('deploy cmd hogehoge')` を書いていけば、どんなコマンドでも実装出来ると思いますが、既製品のHuBotとか使った方が幸せになれると思います。

chat応対業務はBotに任せてコード書くんだ╭( ・ㅂ・)و