Python
hipchat
Slack
ChatOps
xmpp

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

More than 1 year has passed since last update.

『アッオー』でおなじみ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

スクリーンショット 2015-11-30 16.14.11.png

■ HipChat
スクリーンショット 2015-11-30 16.16.15.png

2分で判るXMPPプロトコルの概要

rfc3920で仕様公開されている。
■ Jabber社が開発したXMLベースの通信プロトコル
■ 接続に必要な情報は次の5点セット
スクリーンショット 2015-11-30 17.37.57.png

通信フォーマットは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

install
pip install jabberbot
pip install xmpppy
pip install lazy-reload
pip install requests
pip install simplejson

SlackとHipChatに対応したBot

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

HipChatとSlackから話かけてみた

スクリーンショット 2015-11-30 16.38.34.png

メッセージを受け取れた

スクリーンショット 2015-11-30 16.38.06.png

設定情報の払い出し

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

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

参考

rfc3920
XMPP(Jabberのプロトコル)技術メモ
一般ユーザー向けXMPP入門ページ(仮)

まとめ

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

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

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