slack: metadata で構造化されたデータを扱う - bot 間の連携に使う実装例

slack で bot を動かしていて 「 別の bot のメッセージに反応して何かする bot を書くユースケースがあります *1

ユースケースの例

例えば こんな メッセージを出す bot がいるとします *2

上記の メッセージ から UUID ( OpenStack の Server UUID ) を取り出して、別の bot で何か処理をしたい場合を考えましょう

  • text を抜き出して正規表現で UUID のマッチを試みる
  • attachments や blocks の Hash/Array をイテレートして UUID を取り出す

といったアプローチでメッセージを扱うかと思います。一種のスクレイピングですね

非構造化されたデータを扱う問題

非構造化されたデータ を扱うのは何かと面倒で変更にも弱いですね

元のメッセージのフォーマットが変わる ( text の内容が変わる, attachments や blocks の構造が変わる ) と bot を再実装する必要が出そうです 。人間向けの view に依存してるコードは実装が複雑になったり、メンテナンス性が下がります。スクレイピングのコードを書いたことのある人はよくわかるかと思います。

botbot のメッセージを扱う場合に、構造化されたデータ ( Hash, Array ) で扱えると楽だよなぁと.... 長らく思っていたのでした。

metadata を使おう

で、改めて Slack のドキュメントを調べてみたら、メッセージに metadata として Hash 構造のデータを付けられるのを この度 知ったのでした。

api.slack.com

今年の4月頃に出ていたのかな? *3

metadata の例

下記のように metadataevent_typeevent_payload を入れてメッセージを POST できる!

{
    "channel": "C23456",
    "text": "New teammate @Billy just joined",
    "metadata": {
        "event_type": "new_teammate",
        "event_payload": {
            "id": "TK-2132",
            "summary": "New teammate has been added to the channel",
            "description": "@Billy is a new teammate and needs to be added to the neccesary channels",
            "priority": "HIGH",
            "resource_ type": "TASK"
        }
    }
}

別の bot のメッセージに反応して何かする bot を作る際、metadata を参照すると構造化されたデータで扱いやすそう (ただし メッセージを出す bot で metadata を付与してないといけないけど ... )

metadata を使った、別の bot のメッセージに反応して何かする bot 実装例

  • metadata 付きのメッセージを通知する Ruby アプリ
  • metadata 付きのメッセージに反応する Python Bolt アプリ

を例に作ってみました

metadata 付きのメッセージを通知する Ruby アプリ

↑ のようなメッセージを POST する単純なコードです。metadata も付けています。

#!/usr/bin/env ruby

require 'slack-ruby-client'
require 'dotenv'

Dotenv.load

params = {
  channel: '#dev',
  username: 'live-migration-notifier',
  text: ":arrow_forward: live-migrationが開始されました",
  blocks: [
    {
      type: :section,
      text: {
        type: :plain_text,
        text: ":arrow_forward: live-migrationが開始されました",
        emoji: true
      }
    },
    {
      type: :divider,
    },
    {
      type: :context,
      elements: [
        {
          type: :mrkdwn,
          text: ":desktop_computer: *example.com*\n:pencil2: 7ef9111c-0000-0000-0000-1717950e45cf",
          verbatim: true
        }
      ]
    },
    {
      type: :context,
      elements: [
        {
          type: :plain_text,
          text: ":outbox_tray: host000",
          emoji: true
        },
        {
          type: :plain_text,
          text: ":inbox_tray: host001",
          emoji: true
        }
      ]
    },
    {
      type: :divider,
    },
    {
      type: :context,
      elements: [
        {
          type: :plain_text,
          text: "req-b23c7b63-0000-0000-0000-00000000",
          emoji: false
        },
        {
          type: :plain_text,
          text: "by instance-migrator",
          emoji: false
        }
      ]
    }
  ]
}

# こんな感じで 構造化したデータを突っ込める
metadata = {
  event_type: "live_migration_started",
  event_payload: {
    source_host: "host000",
    dest_host: "host001",
    server: {
      name: "example.com",
      uuid: "7ef9111c-0000-0000-0000-1717950e45cf",
    },
  }
}

# metadata には JSON を入れる
params[:metadata] = metadata.to_json

client = Slack::Web::Client.new(token: ENV['SLACK_API_TOKEN'])
client.chat_postMessage(params)

metadata 付きのメッセージに反応する Python Bolt アプリ

先のメッセージに反応する Python Bolt アプリです。

"""_summary_
"""
import os

import dotenv
import slack_bolt
from slack_bolt.adapter.socket_mode import SocketModeHandler

dotenv.load_dotenv()

app = slack_bolt.App(token=os.environ["SLACK_BOT_TOKEN"])


@app.event("message")
def handle_message(event, say):
    metadata = event.get("metadata")
    if metadata is None:
        return

    if metadata["event_type"] == "live_migration_started":
        payload = metadata["event_payload"]

        print(payload)

        server_name = payload["server"]["name"]
        server_uuid = payload["server"]["uuid"]
        say("{0} {1} started live-migration".format(server_name, server_uuid))


if __name__ == "__main__":
    handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
    handler.start()

動作の例

二つの bot が連携して動作している例です

Python Bolt アプリでは、以下のように metadata を受け取っています

 $ poetry run python bot.py
⚡️ Bolt app is running!
{'source_host': 'host000', 'dest_host': 'host001', 'server': {'name': 'example.com', 'uuid': '7ef9111c-0000-0000-0000-1717950e45cf'}}

意図した通りにデータを扱うことができています。

Proof Of Concent なコードなので、特に利用価値がある実装ではありません。 metadata を通して依存関係を持っている、ってのがポイントですね。

感想

他のユースケース

  • Emoji で Reaction 付けたら、メッセージの metadata ( event_type, event_payload ) を取り出して何か操作するってコード書けそう

metadata の 技術的な疑問点

  • OSS なんかで汎用的に使う bot の場合、 event_type命名はどう扱うのがいいのだろうか?
    • あまり単純な名前をつけると、別の bot と衝突するだろう
    • namespace の規約があるといいようには思う

そもそもの話

  • live-migration-notifier の通知を別チャンネルにも転送したいユースケースがあった
  • live-migration-notifier に機能を追加するよりも、別の bot で転送機能実装したらいいかと思って 「 別の bot のメッセージに反応して何かする bot 」を書いていた

*1:何らかの理由で 元のメッセージを出す bot の実装を変えたくない/変えられない場合

*2:同僚の id:buty4649 が書いた OpenStackのLiveMigrationの通知するくん のメッセージです github.com

*3: このエントリを書いている時点では open beta の機能です