🚧

単純な Slack Bot を単純に書くための Go のライブラリをメンテした

2022/06/29に公開

概要

Slack Bot を作るためのちゃんとした Go のライブラリは slack-go があると思うのですが、単に Slack からメッセージを受けて返すだけの用途でいいときなどに使える雑なライブラリを用意して使っていました。

https://github.com/ikawaha/slackbot

これは Slack の RTM(Real Time Messaging)という機能を利用して websocket で Slack と双方向に通信できるようになっていたのですが、権限などの問題があるとのことで、Slack から「RTM は廃止するぞ」とお達しが(ずいぶん前から)来ていました。

RTM は websocket 通信の API なので、エンドポイントを公開したようなサーバを立てる必要がなく、手元の開発環境からでも簡単に bot を立ち上げられて便利でした。残念ながら Slack からいよいよ最後通告がきたので、Slack が新しく用意した Socket Mode というのに移行してみました、という防備録です。

試行錯誤メモなので Slack が使用している用語と異なる言葉の使い方をしちゃってるところとかあるかもです。詳しくはドキュメントを読んで下さい。たくさんドキュメントがあって、なかなかに混乱します😇。

RTM から Socket Mode への移行で変わるところ

新しい Socket Mode も websocket 通信の API です。なので、公開されたエンドポイントを必要とするサーバが不要、という点は変わりません。しかしながら次のような点が異なるようです。

RTM の利用 Socket Mode での変更
通信 双方向 Socket Mode は受信のみ。送信は EventAPI を利用する
トークン user token のみ app-level token と bot (もしくは user) token

必要な権限を持ったアプリを Slack に登録する

RTM は、API token だけで利用可能でしたが、Socket Mode を利用するためには app-level token が必要になります。手順はなかなかにややこしいので、最後に付録としてのせておきます。ここでは作業が済んだものとして話を進めます。

移行のポイント

Socket Mode でのイベントの受信

接続の手順

接続には app-level token が必要です。apps.connections.open というエンドポイントがあるので、そこに token を投げつけます。そうすると websocket のエンドポイントを送り返してくれるので、ふたたび websocket プロトコルでダイアルします。

イベント通知

Socket Mode は公開したエンドポイントを用意しなくても(手元の計算機などからでも)手軽に bot を機能させられる便利なモードですが、このモードでは、Slack からのイベントを受け取ることしか出来ません。

イベントは、Evelope と Payload に包まれていて、Envelope のタイプでまずどんなイベントが起こったかを判別して、Payload に入っているイベントを取り出すという感じです。

まず、Envelope をみて、EnvelopeID が入っていたら、すぐにお返事をしないといけません。返事をしないと Slack はメッセージを再送します。なので、2回同じメッセージを受け取ることになります。

また、Envelope のタイプを見ると、それが通常のメッセージイベントなのか、スラッシュコマンドなのか分かるようになっています。サーバ側の都合で通信を切断する必要がある場合には disconnect というのが送られてきます。これが送られてきたら、通信を再接続してやる必要があります。

イベント

イベントの通知のタイプが判別したら、イベントを取り出します。イベントPayload に包まれているので、ここから取り出します。

ややこしいことに、スラッシュコマンドの場合は Payload にイベントが入っていません。ややこしい(ややこしいので、ライブラリではイベントとして扱うようにしています)。

イベントには、イベントのタイプとかチャンネルのID、発言内容、発言したユーザーなのど情報が入っています。これに応じて bot に発言させればよさそうです。ただし発言は Socket Mode ではなく Web API を利用して行う必要があります。

Web API でのメッセージ送信

Web API でのメッセージの送信は chat.postMessage のようなエンドポイントが用意されているので、それを利用すればよいです。

Web API の各エンドポイントを利用するにはそれに応じた権限が必要になるので、必要な権限を bot token なり user token に付与してやる必要があります。権限がないとメッセージをくれるエンドポイントもあれば、沈黙してなにも返してこないエンドポイントもあるのでややこしいです。

Bot としてつかうのもややこしい

「呼びかけると応答してくれる」これがやりたいことです。この単純なことを実現するのもちょっとややこしいです。

君の名は?

まず、bot を生成したときに、bot のユーザーIDが分かりません。bot を生成するキーは、app-level token と bot token ですが、この情報と Slack からのレスポンスでは、Slack 上の bot のユーザーIDは判別できません(たぶん)。app-level token の prefix には app ID が付いているので、これを目印にすればよさそうに見えますが、bot に呼びかける際に、@kagome-bot のようにメンションすると、この@kagome-bot は送られてくるメッセージの中で <@U03MADG7LPP> のような Slack のユーザーIDに変換されるので、app ID では判別できません。

ユーザーIDを知るには、users.list のエンドポイントにアクセスしてユーザー一覧を取得するなりして、bot のユーザーIDを取得するしかなさそうに思えます。bot のユーザーIDを知るには bot の名前から引く必要があるので、bot 作成に必要な情報は app-level token と bot token と bot の名前という事になりそうです。

そんなバカな・・・と思うのですが、いい方法が分からないので bot 生成時にユーザーIDが必要なら取得するようにオプションを用意してしまいました。いい方法を知りたい・・・。

そうだスラッシュコマンドがある

bot のユーザーIDが分からなくても、スラッシュコマンドを設定しておけば、bot からの送信であるを判別するまでもなさそうです。スラッシュコマンドで用が足りるなら、スラッシュコマンドを用意するのがよさそうです。

スラッシュコマンドにはレスポンス用の URL が含まれていて、ここにメッセージを json にして送りつければ bot のレスポンスとして表示してくれます。json は複雑な構造を取ることも可能で、凝った使い方も出来るようです。時々 Slack でみる「このメッセージはあなたにしか見えてません」的なメッセージは、この URL に json を送りつけるときのオプションで選択できます。

スラッシュコマンドには発生したチャンネルのIDが含まれているので、レスポンス用の URL を無視して、chat.postMessage でメッセージを送りつけてもいいのかも知れません。

形態素解析 bot で実証してみる

Slack に常駐して形態素解析する bot として検証してみました。

https://github.com/ikawaha/kagome-bot

もっと単純なエコーのサンプルは以下のようになります:

イベントがある度にイベントを返してくれる ReceiveMessage という関数があるので、それにイベントを処理するハンドラを用意してください。単純なことがしたいので、単純なインターフェースです。頭にボットのメンションを付けて呼ぶか、スラッシュコマンドでオウム返しします。

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"strings"

	"github.com/ikawaha/slackbot"
)

// Bot your bot
type Bot struct {
	*slackbot.Client
}

// NewBot creates a Slack bot.
func NewBot(appToken, botToken, botName string) (*Bot, error) {
	c, err := slackbot.New(appToken, botToken, slackbot.SetBotID(botName), slackbot.Debug())
	if err != nil {
		return nil, err
	}
	return &Bot{Client: c}, err
}

func main() {
	if len(os.Args) != 4 {
		fmt.Fprintf(os.Stderr, "usage: bot app-level-token slack-bot-token bot-name\n")
		os.Exit(1)
	}
	// set your app-level-token, bot token and bot name!
	bot, err := NewBot(os.Args[1], os.Args[2], os.Args[3])
	if err != nil {
		log.Fatal(err)
	}
	defer bot.Close()
	fmt.Println("^C exits")

	callPrefix := "<@" + bot.ID + ">"
	for {
		if err := bot.ReceiveMessage(context.TODO(), func(ctx context.Context, e *slackbot.Event) error {
			switch slackbot.EventType(e.Type) {
			case slackbot.Message:
				u, ok := bot.User(e.UserID)
				log.Printf("!!! user: %+v", u)
				if !ok || u.IsBot {
					return nil
				}
				if !strings.HasPrefix(e.Text, callPrefix) {
					return nil
				}
				msg := "Hi, " + u.Name + ": " + strings.TrimPrefix(e.Text, callPrefix)
				if err := bot.PostMessage(ctx, e.Channel, msg); err != nil {
					return err
				}
			case slackbot.SlashCommand:
				if err := bot.RespondToCommand(ctx, e.ResponseURL, e.Text, true); err != nil {
					return err
				}
			}
			return nil
		}); err != nil {
			log.Printf("%v", err)
		}
	}
}

付録:必要な権限を持ったアプリを Slack に登録する

RTM は、API token だけで利用可能でしたが、Socket Mode を利用するためには app-level token が必要になります。手順はなかなかにややこしいので、最後に付録としてのせておきます。ここでは作業が済んだものとして話を進めます。

  1. https://api.slack.com/apps にアクセスして右上の [Create New App] のボタンを押す
  2. Create a Slack App というダイアログが出てくるので、App Name と対象とする Slack のワークスペースを選ぶ
  3. 左に Settings というペインがあるので、ここから「SocketMode」を選ぶ
    • Enable Socket Mode のトグルを ON にする
    • Token Name を設定するダイアログが出てくるので適当に名前を付ける(名前は何でもよさそう。今回はここで付けた名前を使うことはなかった)
    • Token が表示されたダイアログが出てくる。これが app-level token です。記録しておきます
    • Fetures affected という項目が下の方にあるので(左のペインからEvent Subscriptionsを選んでも同じ)クリック
      • Enable Events でトグルを ON にする
      • 下に Subscribe to bot event という項目があるので開いて、[Add Workspace Event]で下記を追加する(これでメッセージが読めるようになる)
        • message.channels(チャンネルに書き込まれたメッセージのイベントが送られてくる)
        • message.im(ダイレクトメッセージに書き込まれたメッセージのイベントが送られてくる)
        • [Save Changes] のボタンを押すのを忘れずに
  4. 左のペインの Features から「OAuth&Permissions」を選択
    • Bot token scopes に権限を追加(上の2つは Subscribe to bot event で選んだため追加されているはず)
      • channels:history (チャンネルのメッセージを読む)
      • im:history (ダイレクトメッセージを読む。botとのダイレクトメッセージを利用のため)
      • chat:write (チャンネルに書き込む)
      • channels:join (チャンネルに参加する)
      • app_mentions:read(bot をメンションしたときのメッセージを読む)
      • files:write(ファイルを書き込む:画像の表示用)
  5. 左のペインの Settings から Install App を選択
    • [Install to Workspace]を選択して Allow する
    • Bot User OAuth Token が表示されるので記録しておきます

bot と直接会話するための設定

Slack の Apps に登録した bot を選んでもメッセージを書き込む入力フォームありません。ここの入力フォームを有効にするには次の設定が必要です

  1. 左のペインの Features から「App Home」をえらぶ

Happy hacking!

Discussion