📆

[Mattermost Integrations] Interactive Message Menu

2021/06/19に公開

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

本記事について

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

本記事では、Mattermost の投稿にユーザーが操作できるセレクトボックスを追加する Interactive Message Menu の機能について紹介します。

Interactive Message Menu 概要

Interactive Message Menu は、昨日紹介した Interactive Message Buttont と同様の機能です。

Interactive Message Button は、Mattermost の投稿にボタンを表示する機能でしたが、Interactive Message Menu では、Mattermost の投稿にセレクトボックスを表示することができます。

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

作成

Interactive Message Menu を含む投稿を作成する方法は Interactive Message Button の時とほぼ同じです。

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

BODY='{
  "attachments": [{
    "title": "Echo text",
    "actions": [{
	  "id": "echo",
      "name": "Would you like to echo?",
      "integration": {
        "url": "http://localhost:8080/actions/echo",
        "context": {
          "text": "sample text"
		}
	  },
	  "type": "select",
	  "options": [{
		  "text": "Echo",
		  "value": "echo"
	  }, {
		  "text": "Reject",
		  "value": "reject"
	  }]
	}]
  }]
}'

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

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

first exmple

optionsに指定したオプションがセレクトボックスから選べるようになっています。

first example2

セレクトボックスから要素を選択すると、対応するオプションのvalueに指定した値を含むリクエストがIntegrationに指定した URL へ送信されることになります。

パラメータ

actionsフィールドに指定できるパラメータは、Interactive Message Button と同じ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 Menu のみで使用できるパラメータはdata_source, options, default_optionになります。

optionsは、先ほどの例にも示したようにセレクトボックスで選択する要素を定義するフィールドです。
default_optionは、デフォルトで選択されているオプションを指定することができます。options内のいずれかのオプションのvalueの値を指定します。

data_sourceは特殊なオプションです。userschannelsを指定でき、これを指定するとセレクトボックスから選択できるオプションがそれぞれ Mattermost 上のユーザーか Mattermost 上の公開チャンネルになります。data_sourceを指定した場合、optionsに指定されたオプションは無視されます。

data source

BODY='{
  "attachments": [{
    "title": "Echo text",
    "actions": [{
	  "id": "echo",
      "name": "Would you like to echo?",
      "integration": {
        "url": "http://localhost:8080/actions/echo",
        "context": {
          "text": "sample text"
		}
	  },
	  "type": "select",
	  "data_source": "channels"
	}]
  }]
}'

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

実行

今回は、slash command の引数に指定したテキストをセレクトボックスで選択されたチャンネルへ Echo するような Interactive Message Menu を紹介します。

video

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

(今回の例では 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() {
	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 server",
				Actions: []*model.PostAction{{
					// (1) Echoメニュー
					Name: "Echo",
					Integration: &model.PostActionIntegration{
						URL: "http://localhost:8080/actions/echo",
						Context: map[string]interface{}{
							"text": r.Form.Get("text"),
						},
					},
					Type: model.POST_ACTION_TYPE_SELECT,
					Options: []*model.PostActionOptions{{
						Text:  "Echo",
						Value: "echo",
					}, {
						Text:  "Reject",
						Value: "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())
	})

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

		// (3) リクエストデータの読み出し
		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
		}

		selected, ok := payload.Context["selected_option"].(string)
		if !ok {
			resp := &model.PostActionIntegrationResponse{EphemeralText: "invalid request. Context['selected_option'] is not found."}
			fmt.Fprint(w, resp.ToJson())
			return
		}

		// (4) レスポンスの構築
		response := &model.PostActionIntegrationResponse{}
		switch selected {
		case "echo":
			response.Update = &model.Post{
				Message: text,
				Props:   model.StringInterface{},
			}
		case "reject":
			response.Update = &model.Post{Props: model.StringInterface{}}
			response.EphemeralText = "Echoing was rejected"
		default:
			response.EphemeralText = "invalid operation"
		}

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

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

(1)では Interactive Message Menu の内容を定義しています。Integration.URLにリクエスト送信先としてhttp://localhost:8080/actions/echoを、Integration.Contextに、slash command の引数として指定された文字列を保持しています。また、EchoRejectのオプションをoptionsに定義しています。

(2)で、セレクトボックスからオプションが選択された時に送信されるリクエストを処理しています。(3)で送信されたリクエストを読み出しています。送信されるリクエストの形式は、Interactive Message Menu から送信されるリクエストも Interactive Message Button と同じく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 実行時の引数と、selected_optionをキーとして格納されている選択されたオプションを読み出しています。
そして、(4)で読み出したselected_optionの値に応じてレスポンスを構築しています。レスポンスの形式も Interactie Message Button と同じく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.
}

さいごに

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

Discussion