📆

[Mattermost Integrations] Interactive Message Button

10 min read

Mattermost 記事まとめ: https://blog.kaakaa.dev/tags/mattermost/

本記事について

Mattermost の統合機能アドベントカレンダーの第 14 日目の記事です。

本記事では、Mattermost の投稿にユーザーが操作できるボタンを追加する Interactive Message Button の機能について紹介します。

Interactive Message Button 概要

Interactive Message Button は、Mattermost の投稿にボタンを表示し、ボタンがクリックされるとどのボタンが押されたのかという情報がサーバーへ送信される機能です。

official example
(画像は公式ドキュメントから)

Interactive Message に関する公式ドキュメントは下記になります。

作成

Interactive Message Button を含む投稿を作成するには、Message Attachments 機能と同様attachmentsに Button に関する情報を追加します。

Incoming Webhook(内向きのウェブフック)を使って作成する例は下記のようになります。

BODY='{
  "attachments": [{
    "title": "Echo text",
    "actions": [{
	  "id": "echo",
      "name": "Echo",
      "integration": {
        "url": "http://localhost:8080/actions/echo",
        "context": {
          "text": "sample text"
		}
	  }
	}, {
      "name": "Reject",
	  "style": "danger",
      "integration": {
        "url": "http://localhost:8080/actions/reject"
	  }
	}]
  }]
}'

curl \
  -H "Content-Type: application/json" \
  -d "$BODY"  \
  http://localhost:8065/hooks/ucw5qjw86jgeum77o1uw8197jr

上記のリクエストを実行すると、下記のようなボタンを含む投稿が作成されます。

first example

リクエスト内容と見ると、actionsフィールドに含まれる下記の一つのオブジェクトが一つのボタンに関連していることが分かります。

{
  "id": "echo",
  "name": "Echo",
  "integration": {
    "url": "http://localhost:8080/actions/echo",
    "context": {
      "text": "sample text"
    }
  }
}

idはボタンの ID、nameは投稿上に表示されるボタンのラベルを指定します。integration内には、ボタンを押した時に送信されるリクエストの情報を記述しており、urlにはリクエスト送信先の URL を、contextにはリクエストに含まれる任意の情報を指定することができます。

ボタンが押された時に何か処理を行うには、リクエストを受け取るサーバーを用意する必要がありますが、それについては後述します。

パラメータ

actionsフィールドに指定できるパラメータは、Mattermost のサーバー側のコードにPostAction構造体として宣言されています。

https://github.com/mattermost/mattermost-server/blob/master/model/integration_action.go#L37
  • id: 1 投稿内でユニークとなるボタンの ID を指定します。指定しない場合、Mattermost 側で一意の ID が付与されます。
  • type: Interactive Message の種別を指定します。selectbuttonを指定でき、何も指定しないとbuttonが指定されます。
  • name: Mattermost 内でボタンのラベルとして表示される文字列を指定します。
  • disabled: ボタンを無効化するオプションです。trueを指定するとボタンが押せなくなります。
  • style: ボタンの色を指定できます。default, primary, success, good, warning, danger, もしくは任意の Hex color("#FF9900"など)を指定できます。
    Outgoing WebHook を利用するには、システムコンソール > 統合機能管理 > 外向きのウェブフックを有効にする の設定が有効になっている必要があります。
  • data_source: Interactive Message Buttonでは使用しません。Interactie Message Menuでのみ使用します。
  • options: Interactive Message Buttonでは使用しません。Interactie Message Menuでのみ使用します。
  • default_option: Interactive Message Buttonでは使用しません。Interactie Message Menuでのみ使用します。
  • integration: アクションを実行した際に送信されるリクエストに関する情報を指定します。
    • url: リクエスト送信先の URL を指定します。
    • context: 送信されるリクエストに含まれる追加情報を指定します
    • integrationに指定した情報はリクエスト送信時にしか読み出されないため、(https などで通信経路のセキュリティが担保されていれば)アクセストークンのような秘密情報なども含めることができます。

実行

Interactive Message Button を機能させるには、ボタンが押された時に送信されるリクエストを処理するサーバーを立てておく必要があります。今回は、slash command の引数に指定したテキストを Echo するような Interactive Message Button について考えます。

video

slash command、Interactive Message Button からのリクエストを受け取るサーバーアプリのサンプルコードは下記のようになります。

(今回の例では slash command、Interactive Message Button 共に、Mattermost からlocalhostに対してリクエストが送信されるため、システムコンソール > 開発者 > 信頼されていない内部接続を許可するの設定に、リクエスト送信先のサーバーを記述しておく必要があります。詳しくは公式ドキュメントを参照ください。)

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"

	"github.com/mattermost/mattermost-server/v5/model"
)

func main() {
	// (1) Interactive Message Button作成用slash command
	http.HandleFunc("/command", func(w http.ResponseWriter, r *http.Request) {
		r.ParseForm()

		response := model.CommandResponse{
			ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL,
			Attachments: []*model.SlackAttachment{{
				Title: "Echo text",
				Actions: []*model.PostAction{{
					// (2) Echoボタン
					Id:   "echo",
					Name: "Echo",
					Integration: &model.PostActionIntegration{
						URL: "http://localhost:8080/actions/echo",
						Context: map[string]interface{}{
							"text": r.Form.Get("text"),
						},
					},
				}, {
					// (3) Rejectボタン
					Name:  "Reject",
					Style: "danger",
					Integration: &model.PostActionIntegration{
						URL: "http://localhost:8080/actions/reject",
					},
				}},
			}},
		}

		// Need to set header. if not, just json string will be posted.
		w.Header().Add("Content-Type", "application/json")
		io.WriteString(w, response.ToJson())
	})

	// (4) Echo Buttonが押されたときの処理
	http.HandleFunc("/actions/echo", func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		// (5) リクエストデータの読み出し
		b, _ := ioutil.ReadAll(r.Body)
		var payload model.PostActionIntegrationRequest
		json.Unmarshal(b, &payload)
		text, ok := payload.Context["text"].(string)
		if !ok {
			resp := &model.PostActionIntegrationResponse{EphemeralText: "invalid request. Context['text'] is not found."}
			fmt.Fprint(w, resp.ToJson())
			return
		}

		// (6) レスポンスの構築
		response := &model.PostActionIntegrationResponse{
			Update: &model.Post{
				Message: text,
				Props:   model.StringInterface{},
			},
		}

		w.Header().Add("Content-Type", "application/json")
		io.WriteString(w, string(response.ToJson()))
	})

	// (7) Rejectボタンが押されたときの処理
	http.HandleFunc("/actions/reject", func(w http.ResponseWriter, r *http.Request) {
		// (8) レスポンスの構築
		response := &model.PostActionIntegrationResponse{
			Update: &model.Post{
				Props: model.StringInterface{},
			},
			EphemeralText: "Echoing was rejected.",
		}

		w.Header().Add("Content-Type", "application/json")
		io.WriteString(w, string(response.ToJson()))
	})

	http.ListenAndServe(":8080", nil)
}

(1)は slash command のリクエストを処理するロジックです。http://localhost:8080/commandへリクエストを送信する slash command を事前に登録しておく必要があります。slash command の登録方法については 6 日目の記事を参照ください。
(2)では、slash command 実行時の引数情報を Echo するEchoボタンを定義しています。Integration.URLにリクエスト送信先としてhttp://localhost:8080/actions/echoを、Integration.Contextに、slash command の引数として指定された文字列を保持しています。(3)では、Echo 処理を注意するためのRejectボタンを定義しています。リクエスト送信先はhttp://localhost:8080/actions/rejectにしてあります。また、Rejectボタンには"style": "danger"をしているため、ボタンが赤く表示されます。

slash command を実行すると、下記のような投稿が作成されます。

example

(4)で、Echoボタンが押された時に送信されるリクエストを処理しています。まず、(5)で送信されたリクエストを読み出しています。Interactive Message Button から送信されるリクエストは Mattermost のコードではPostActionIntegrationRequestとして定義されています。

type PostActionIntegrationRequest struct {
	UserId      string                 `json:"user_id"`
	UserName    string                 `json:"user_name"`
	ChannelId   string                 `json:"channel_id"`
	ChannelName string                 `json:"channel_name"`
	TeamId      string                 `json:"team_id"`
	TeamName    string                 `json:"team_domain"`
	PostId      string                 `json:"post_id"`
	TriggerId   string                 `json:"trigger_id"`
	Type        string                 `json:"type"`
	DataSource  string                 `json:"data_source"`
	Context     map[string]interface{} `json:"context,omitempty"`
}

今回はContext内にtextをキーとして slash command 実行時の引数が格納されているため、その文字列を読み出しています。
そして、(6)で読み出した文字列を使ってレスポンスを構築しています。レスポンスの形式は Mattermost のコードでPostActionIntegrationResponseとして定義されています。

type PostActionIntegrationResponse struct {
	Update           *Post  `json:"update"`
	EphemeralText    string `json:"ephemeral_text"`
	SkipSlackParsing bool   `json:"skip_slack_parsing"` // Set to `true` to skip the Slack-compatibility handling of Text.
}

PostActionIntegrationResponseUpdateフィールドに指定したPostのインスタンスで、Interactive Message Button を置き換えることができます。今回は slash command の引数として与えられた文字列をメッセージに持つ投稿に置き換えています。Props: model.StringInterface{},PostPropsを空にしていますが、これをしないと Interactive Message Button がずっと残り続けてしまうため、ボタンを削除したい場合は必ず空に設定する必要があります。Mattermost v5.10 以前では、明示的にPropsを空にしなくても Interactive Message Button が削除されていましたが、Mattermost v5.11 以上では、明示的に空に設定しないと削除されないようになったようです。(https://docs.mattermost.com/developer/interactive-messages.html#how-do-i-manage-properties-of-an-interactive-message)

(7)は、Rejectボタンが押されたときの処理です。(8)でレスポンスを構築していますが、ボタンが押された時に Interactive Message Button を含む投稿を削除したい場合は、このようにPropsを空に設定しただけのPostインスタンスを指定します。EphemeralTextに指定した文字列は、ボタンを押した人だけに見えるメッセージになります。

さいごに

本日は、Interactive Message Button の使い方について紹介しました。
明日は、Interactive Message Menu について紹介します。

Discussion

ログインするとコメントできます