今や一家に一台のSlackですが、やはりSlackを使う以上はChatOpsしないといけないよね、
ということでナウいBoltを使ってSocketModeで実装してみました。
Slackの設定
まずSlack側でBoltを使う準備をしなければなりません。
割とやることがあるので、詳しくは公式ページをみて頑張って下さい。
手順を掻い摘んで書くと、
- https://api.slack.com/apps にアクセス
-
Create New App
orCreate an App
をクリック - モーダルが表示されるので
From scratch
をクリック -
App Name
を入力して作成先のWorkspaceをリストから選択したらCreate App
をクリック - 左メニューの
Features -> OAuth & Permissions
をクリック - 画面を下にスクロールし、
Scopes
にてAdd an OAuth Scope
をクリックし、BotTokenScopesを適宜設定 - 左メニューの
Settings -> Socket Mode
をクリック -
Enable Socket Mode
のトグルをONにする - Token設定画面が出てくるので、
Token name
を入れてAdd Scope
で適宜Scopeを追加したらGenerateする - app tokenが表示されるのでメモる
- 左メニューの
Features -> Event Subscriptions
をクリック -
Enable Events
のトグルをONにする - 下の
Subscribe to bot events
を展開 -
Add Bot User Event
をクリックしてsubするイベントを適宜追加し、右下のSave Changes
をクリックする -
Settings -> Install App
をクリックする -
Install to Workspace
をクリックする - 画面遷移し、権限リクエストされるので
許可する
をクリックする - bot tokenが表示されるのでメモる
という感じです。画面遷移が多くて滅入ります。
Boltの苦労話
ここまできてようやくBoltでBotを構築していけます。
今回は、「メンションを飛ばしたらいい感じにモーダル開いてコマンドを実行できるようなやつ」を目指したんですが...割と挫折しました。
やろうとしたこと
まず最初に、「@bot check server」みたいに呼びかけたら、モーダルがボボッって出てそこから対象サーバーとコマンドを指定できるようなやつを作ろうとしました。
挫折ポイントその1
ところがどっこい、メンションから直接モーダルを起動する手段がありませんでした。
モーダルを起動するには、ここに書いてある通り、trigger_id
が必要になります。
が、trigger_idがPayloadに含まれるのは、スラッシュコマンド、ボタンの押下、メニューの選択、ショートカットなどになるので、メンションだとダメなんです
挫折ポイントその2
ということなので、最初はショートカットを利用しようとしました。
ショートカットにはGlobal
とOn messages
の二種類があるんですが、メッセージからコマンドを呼び出すのは意味がわからないので、Global一択となります。
/
で起動できるし、まぁいいかなと思ったのも束の間、GlobalShortcutsだと起動元のChannel情報が取れませんでした。
今回想定しているのはサーバーに対するクエリなので、誰がいつ実行したかをSlack上で把握できるようにしておきたい、証跡を残したいんです。
そのために、実行元のChannelに対して、Botに「こういうコマンドを誰々が実行しました」みたいなメッセージを実行後に出したくなるのは自然だと思います。
が、Globalだと実行元Channel情報がないため、これができません
ちなみに、On messagesの方であればChannel情報が入るようなのですが、やっぱりメッセージからコマンドを呼び出すのは意味がわからないのでやってません。
挫折ポイントその3
ショートカットを諦め、Channel起因でアクションが発生するボタンの押下をモーダルの契機とすることにしました。
メンションに反応してBotにAttachmentを表示させ、その中にモーダル起動ボタンを配置します。
これでOKだろうと思ったのも束の間、 Channel情報がモーダルのsubmitで発火した関数のBodyに入ってきていない...
おそらくモーダルもこれまたChannel情報を持たないんでしょうね...ということで、当初目標のコマンド実行の要件を満たさず、土日を使って収穫0かよ!と、割と絶望しました
福音
で、この記事を書くために調べ物していたら、たまたまこの記事に邂逅しました。
https://qiita.com/irico/items/63b98127644bb0f50fa6
private_metadataっていうのが使えるのかい!?
ということで、早速private_metadataを利用したところ...
キターーーーーー!!
Qiitaさまさまです
正直なところ、Boltはドキュメントがやや弱いので、ソースコードを読まざるを得ない状況にもなったりしたんですが、APIのリファレンスを見れば良いとは...盲点でした。
実装にあたってのポイント解説
完成品はGitHubをご覧ください。
関数の分散
公式の例だと、app.pyにdecoratorくっつけた関数をひたすら羅列していくという感じでしたが、それだとモノリシックになってやばいことになるのは明白なので、分散しました。
処理をクラスで分けようと思ったんですが、@app.deco
みたいな感じでAppのインスタンス経由でデコろうとする場合、グローバル変数にappを持たせないとうまくデコれませんでした(@self.app.decoは無理)。
なので、クラスにはinitの段階でappのインスタンスを渡すようにし、init内でデコ相当の処理を行うようにしました。
# 前
@app.event("app_mention")
def mention:
...
# 後
class Hoge:
def __init__(self, app):
app.event("app_mention")(self.mention)
def mention(self, event, say):
...
まぁこれがベストな分散方法かはわかりませんが...。
blockはjson化して外出し
コードの中でblockを管理するとコードの見通しが悪くなるので、同じ階層でjsonファイルとして管理しています。
で、I/Oが頻発するのも嫌なので、initで読み込み、インスタンス変数に格納するようにしてます。
def __init__(self, app):
with open("bot_test/modules/check_server/attachment.json", encoding="utf-8") as f:
self.attachment = json.loads(f.read())
def mention(self, event, say):
say(attachments=self.attachment["attachments"])
private_metadataによるデータ連携
参考記事の処理をBoltで実装すると以下のような感じになります。
# モーダル表示側
def action_check_server(self, ack, body, client):
ack()
channel = body["channel"]["id"]
check_view = deepcopy(self.check_view)
check_view["private_metadata"] = channel
client.views_open(trigger_id=body["trigger_id"], view=check_view)
# モーダルsubmit受信側
def handle_check_server_submission(self, ack, client, body, view):
(略)
channel = view["private_metadata"]
Blockの作成
blockの作成は、基本的にBlock Kit builderを使って行いますが、この時に単純にコピペしただけでは利用出来ませんので注意が必要です。
attachment
- fallbackという項目を設定しないと、Bolt起動時にWarningが出て鬱陶しいので設定しましょう。(要約メッセージを表示する時に使うみたいです)
- action_idはわかりやすいものに変更しましょう。
app.action
の引数に指定するのがaction_idですので、パッと見てどういう動作をするか判った方が良いです。
modal
- callback_idを設定しましょう。
app.view
の引数に指定するのがcallback_idです。これを指定することにより、当該callback_idを持つモーダルのsubmitを拾うことができるようになります。 - actionはattachmentと同様、わかりやすく変更するのが良いと思います。
終わりに
RTM時代にslackbotを使っていましたが、公式がBotをPythonでサポートしたということで、かなりやりやすくなったなと思いました。
Socket Modeのおかげでローカルでのテストがめちゃくちゃ楽になったので、開発も捗ります。
と同時に、そんなに難しくないと思っていたやりたいこと(関数分割とかchannel受け渡し)がなかなか出来ず、知見もドキュメントもない状態で途方に暮れてうーんと思ったのも事実です。
ドキュメントは今後に期待しつつ、とりあえず色々使って知見を深めようかと思いました。