このページを正しく表示するにはJavascriptを有効にしてください。
SlackのボットをPythonで作ってみる
Slack用のボットを作る場合Hubotと連携してnode.jsで作るのが一般的(?)みたいですが、どうせ作るならいつものPythonで作りたいと思い、作り方をまとめてみました。
Pythonであれば言語処理が豊富ですし、AppEngineで動かしたりも出来ます。
## 状況整理
ボットと一口に言っても実際は大きく分けて2パターンあります。
ひとつは一定時間ごとに一方的にポストするタイプのもの。
もう一つは発言に対して反応するタイプのものです。
前者であれば投稿APIとスケジューラーを組み合わせれば
言語を問わず簡単に作れそうです。(なので今回は説明しません)
後者の発言に反応するタイプとなるとちょっと難易度が上がります。
発言に対してリアルタイムで反応するためには、
投稿があるかどうかを常にチェックし、内容ごとに応答するかどうかを決め、
発言する際はボットとして投稿を行わなくてはなりません。
## Outgoing WebHooks
チャンネルAPIから投稿をこまめに監視して‥みたいなことやると
とても大変そうですしSlack側にも負荷をかけてしまいそうです。
そこでリアルタイム応答用のシステムが提供されています。
それがOutgoing WebHooksという仕組みです。
[https://kamatte-ch.slack.com/services/new/outgoing-webhook](https://kamatte-ch.slack.com/services/new/outgoing-webhook)
このIntegrationsを使うと、Slack上の投稿が随時取得出来るだけでなく、
ボットとしての応答も簡単に出来てしまいます。
このWebhooks、仕組みがシンプルなので、構造さえ理解すればPythonだけでなく好きな言語で固有のライブラリに依存する事なくボットが作れるようになります。
### Outgoing WebHooksの仕組み
まず最初にこのWebhooksの仕組みを説明しておきます。
IntegrationsからこのWebhooksを登録すると、
Slackに投稿があるごとに、こちらが指定したURLを叩いてくれるようになります。
なので投稿を一定時間で監視したりする必要がないです。
叩かれるときにはPOSTで投稿データも一緒に送られてきます。
ユーザー名だとか投稿内容だとかが入っているので、
これを調べることで応答するかどうかを決める事が出来ます。
さらに、ボットの応答をする際も別のAPIを叩く必要がなく、
叩かれた時のレスポンスとしてJSONを返してあげれば
それを勝手にSlackに投稿してくれます。
(応答が必要ない場合はブランクページを返してあげればOKです。)
つまりWebAPIを作る感覚でSlackのボットが作れてしまうわけです。
これは非常に便利ですね。
ちなみに定期投稿するボットを作る場合Inbound Webhooksの方を使うといいそうです。
## 実装
```python
#coding: utf-8
from __future__ import absolute_import, division, print_function
import logging
from flask import Flask, request, jsonify
app = Flask(__name__)
class Message(object):
"""Slackのメッセージクラス"""
token = ""
team_id = ""
channel_id = "" # 投稿されたチャンネルID
channel_name = "" # チャンネル名
timestamp = 0
user_id = ""
user_name = "" # 投稿ユーザー名
text = "" # 投稿内容
trigger_word = "" # OutgoingWebhooksに設定したトリガー
def __init__(self, params):
self.team_id = params["team_id"]
self.channel_id = params["channel_id"]
self.channel_name = params["channel_name"]
self.timestamp = params["timestamp"]
self.user_id = params["user_id"]
self.user_name = params["user_name"]
self.text = params["text"]
self.trigger_word = params["trigger_word"]
def __str__(self):
res = self.__class__.__name__
res += "@{0.token}[channel={0.channel_name}, user={0.user_name}, text={0.text}]".format(self)
return res
@app.route('/', methods=['POST'])
def mybot():
msg = Message(request.form)
logging.debug(msg)
# slackbotによる投稿は無視(無限ループ回避)
if msg.user_name == "slackbot":
return ''
# 投稿にyoが含まれてたらyoを返す
if "yo" in msg.text:
return _say("yo")
return ''
def _say(text):
"""Slackの形式でJSONを返す"""
return jsonify({
"text": text, # 投稿する内容
"username": "mybot", # bot名
"icon_emoji": "", # botのiconを絵文字の中から指定
})
```
Python + Flaskで実装してみるとこんな感じ。
投稿にyoが含まれてたらyoをtextに含んだJSONを返すだけです。
実装の際には入力に対して欲しいJSONを返しているかをみればよいので、
テストコードとかも普通のAPI実装のように書くことが出来ます。
アプリケーションのコードが完成したら、サーバーにデプロイして、
定義したURLをOutgoing WebhooksのURLに設定すれば動きます。
もちろんGoogleAppEngineでも問題ありません。
## 注意点
### ボット同士の会話
Slackbotによる発言はすべてユーザー名が「slackbot」になります。
これは他のJenkinsなどのIntegrationsを利用した時とも同じで、
Slack上の表示名にかかわらずユーザー名が「slackbot」として提供されます。
なのでボット同士で会話みたいなことを行う場合はslackbotかどうかを
判断して行ってあげましょう。
尚、応答の無限ループには注意しましょう。
ボット同士が会話できるようにした場合注意しないと無限ループに陥ります。
# 例:
userA : hello
mybot: hello
mybot: hello # (一個上のhelloに反応)
mybot: hello # (一個上のhelloに反応)
mybot: hello # (一個上のhelloに反応)
mybot: hello # (一個上のhelloに反応)
mybot: hello # (一個上のhelloに反応)
# ‥‥以下無限ループ......
#▂▅▇█▓▒░(’ω’)░▒▓█▇▅▂うわあああああ
無限ループに陥ると夥しい勢いで通知が飛び交いますので注意してください。
(いざとなったらIntegrationsのURLを変えることで停止できます。Slackに怒られる前に止めましょう)
ちなみにHubotとかだとデフォルトでボット同士が反応しないようになっています。
無限ループする心配がないので安心ですが、逆にボット同士でやりとりしたい時(Jenkinsの成功で拍手したり)などは不便です。
### Webhooksの制限
Outgoing WebHooksを使えばすべての投稿が取れるわけではありません。
取れるのは
* 設定した公開チャンネル内の発言
* 全公開チャンネル内の発言のうち特定の単語で始まっているもの
のどちらかしか取ることが出来ません。
設定は何個でもできるので全チャンネルを登録すればすべての投稿が取れますが、
チャンネルが増えた時とかは自分で対応する必要があります。
あと、プライベートなチャンネルや個人宛ての投稿は取得できないみたいです。
(取れてしまうと傍受出来てしまいますしね。)
# Python Slack Bot
[https://github.com/pistatium/python_slack_bot](https://github.com/pistatium/python_slack_bot)
ソース整理してレポジトリ作ってみました。
AppEngine上で簡単に動かせるサンプルです。
## 使い方
BaseBotを継承したクラスにメソッドを定義すると、
自動的に条件に合うものを選んで発言するようにしています。
例はsrc/bot/daniel.pyを見てもらえばわかるかと。
それぞれのメソッドで発言するかの条件をチェックし、
該当すればその発言をStringで返し、
該当しなければFalseを返してあげるだけで大丈夫です。
同じ条件の場合どちらのメソッドを使うかの制御は入れてないので、
Pythonの内部的な事情により応答メソッドが決定されます。
```python
class Daniel(BaseBot):
_NAME = "daniel"
_ICON = ":penguin:"
def echo(self):
"""オウム返しをする"""
if self.msg.command != u"echo":
return ""
return " ".join(self.msg.args[2:])
def yo(self):
"""YO"""
if self.msg.command != "yo":
return ""
return "yo"
```