13
7

More than 1 year has passed since last update.

SlackのBolt(Python)を使ってChatOpsしようとしたら割と苦労した

Posted at

今や一家に一台のSlackですが、やはりSlackを使う以上はChatOpsしないといけないよね、
ということでナウいBoltを使ってSocketModeで実装してみました。

Slackの設定

まずSlack側でBoltを使う準備をしなければなりません。
割とやることがあるので、詳しくは公式ページをみて頑張って下さい。

手順を掻い摘んで書くと、

  • https://api.slack.com/apps にアクセス
  • Create New App or Create 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に含まれるのは、スラッシュコマンド、ボタンの押下、メニューの選択、ショートカットなどになるので、メンションだとダメなんです :innocent:

挫折ポイントその2

ということなので、最初はショートカットを利用しようとしました。
ショートカットにはGlobalOn messagesの二種類があるんですが、メッセージからコマンドを呼び出すのは意味がわからないので、Global一択となります。
/で起動できるし、まぁいいかなと思ったのも束の間、GlobalShortcutsだと起動元のChannel情報が取れませんでした
今回想定しているのはサーバーに対するクエリなので、誰がいつ実行したかをSlack上で把握できるようにしておきたい、証跡を残したいんです。
そのために、実行元のChannelに対して、Botに「こういうコマンドを誰々が実行しました」みたいなメッセージを実行後に出したくなるのは自然だと思います。
が、Globalだと実行元Channel情報がないため、これができません :innocent:

ちなみに、On messagesの方であればChannel情報が入るようなのですが、やっぱりメッセージからコマンドを呼び出すのは意味がわからないのでやってません。

挫折ポイントその3

ショートカットを諦め、Channel起因でアクションが発生するボタンの押下をモーダルの契機とすることにしました。
メンションに反応してBotにAttachmentを表示させ、その中にモーダル起動ボタンを配置します。

これでOKだろうと思ったのも束の間、 Channel情報がモーダルのsubmitで発火した関数のBodyに入ってきていない...
おそらくモーダルもこれまたChannel情報を持たないんでしょうね...ということで、当初目標のコマンド実行の要件を満たさず、土日を使って収穫0かよ!と、割と絶望しました :innocent:

福音

で、この記事を書くために調べ物していたら、たまたまこの記事に邂逅しました。
https://qiita.com/irico/items/63b98127644bb0f50fa6

private_metadataっていうのが使えるのかい!?

ということで、早速private_metadataを利用したところ...

キターーーーーー!!

Qiitaさまさまです :pray:
正直なところ、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受け渡し)がなかなか出来ず、知見もドキュメントもない状態で途方に暮れてうーんと思ったのも事実です。
ドキュメントは今後に期待しつつ、とりあえず色々使って知見を深めようかと思いました。

13
7
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
7