[Mattermost Integrations] Interactive Dialog
Mattermost 記事まとめ: https://blog.kaakaa.dev/tags/mattermost/
本記事について
Mattermost の統合機能アドベントカレンダーの第 16 日目の記事です。
本記事では、Mattermost でユーザーの入力を受け付けるダイアログを表示する Interactive Dialog の機能について紹介します。
Interactive Dialog の概要
Interactive Dialog は、Slash Command や Interactive Message などのアクションを起点に、Mattermost 上にユーザー入力を受け付けるダイアログ(モーダルウィンドウ)を表示する機能です。
(画像は公式ドキュメントから)
Interactive Dialog に関する公式ドキュメントは下記になります。
Interactie Dialog は、何度か Mattermost とインタラクションをしながら動作するもののため、動作が複雑になります。今までのようにcurl
だけで動作させることは難しいため、Go のコードで書いたものを断片的に紹介していきます。
今回は、Interactive Dialog の入力内容から Message Attachments のメッセージを作成するような例を考えてみます。
Trigger ID の取得
Interactive Dialog を起動するには、まず、Mattermost 内部で生成される Trigger ID というものが必要です。Trigger ID は Slash Command や Interactive Message のアクションを実行した時に、Mattermost から送信されるリクエストに含まれています。Slash Command 実行時のリクエストから Trigger ID を取得する場合、Slash Command 実行時に送信されるリクエストを処理するサーバーで、以下のように Trigger ID を取得することができます。
http.HandleFunc("/command", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
// (1) Slash Command実行時に送信されるリクエストから "Trigger ID" を取得
triggerId := r.Form.Get("trigger_id")
...
Interactive Message Button などのアクションから取得する際は、PostActionIntegrationRequest.TriggerId
から Trigger ID を取得できます。
Interactive Dialog の起動
先ほど取得した Trigger ID を使って、Mattermost へ Interactive Dialog 起動のリクエストを投げます。
Trigger ID を取得するコードに続けて、/api/v4/actions/dialogs/open
にOpenDialogRequest
で定義されるリクエストを送信することで Interactive Dialog を起動することができます。
http.HandleFunc("/command", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
// (1) Slash Command実行時に送信されるリクエストから "Trigger ID" を取得
triggerId := r.Form.Get("trigger_id")
// (2) Interactive Dialogを起動するためのリクエストを構築
request := model.OpenDialogRequest{
TriggerId: triggerId,
URL: "http://localhost:8080/actions/dialog",
Dialog: model.Dialog{
Title: "Sample Interactive Dialog",
Elements: []model.DialogElement{{
DisplayName: "Title",
Name: "title",
Type: "text",
}, {
DisplayName: "Message",
Name: "message",
Type: "textarea",
}},
},
}
// (3) Interactive Dialogを開く
b, _ := json.Marshal(request)
req, _ := http.NewRequest(http.MethodPost, "http://localhost:8065/api/v4/actions/dialogs/open", bytes.NewReader(b))
req.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
...
(2)
で構築しているOpenDialogRequest
にどのようなダイアログを表示するかという情報も指定するのですが、詳しくは後述します。
(3)
で/actions/dialogs/open
にリクエストを送信していますが、ここでは AccessToken などが必要ありません。これは Trigger ID 自体の利用可能期限が 3 秒と短く、悪用の心配がないためだと思われます。この点は、Trigger ID を取得してからダイアログを開く前に時間のかかる処理などを入れないよう注意する必要があるということも意味します。
/actions/dialogs/open
へのリクエストが正常に完了すると、Mattermost 上で Interactive Dialog が表示されます。
Interactive Dialog 起動時のパラメータ
Interactive Dialog を起動する際に送信するOpenDialogRequest
に与えることができるパラメータは下記の通りです。
-
TriggerId
: Slash Command や Interactive Message のアクションを実行した時に Mattermost 内部で生成される Interactive Dialog 起動用の ID を指定します -
URL
: Interactive Dialog に入力された情報の送信先 URL を指定します -
Dialog
: Interactive Dialog 上に表示される要素を指定します-
CallbackId
: 統合機能で設定される ID です。Slash Command の場合はCommandArgs.RootId
、Interactive Message の場合はPostActionIntegrationRequest.PostId
を指定している気がしますが、何に使われているかはいまいちわかりません。 -
Title
: Interactive Dialog のタイトル部分に表示されるテキストを指定します -
IntroductionText
:Title
の下に表示されるダイアログの説明文を指定します -
IconURL
: ダイアログに表示されるアイコンの URL を指定します -
SubmitLabel
: ダイアログの決定ボタンのラベルを指定します -
NotifyOnCancel
: ダイアログのキャンセルボタンが押された時に、サーバーにその旨を通知するかを選択します。true
の場合、キャンセル通知がサーバーに送信されます -
State
: 統語機能によって処理の状態を管理したい場合に設定される任意のフィールドです -
Elements
: ダイアログ上の入力フィールドを指定します。利用可能なElement
については公式ドキュメントを参照してください。
-
Interactive Dialog からのリクエスト受信
Interactive Dialog の送信ボタンが押されると、OpenDialogRequest
のURL
フィールドに指定した URL へリクエストが送信されます。
// (2) Interactive Dialogを起動するためのリクエストを構築
request := model.OpenDialogRequest{
TriggerId: triggerId,
URL: "http://localhost:8080/actions/dialog",
...
送信されるリクエストは Mattermost のコードではSubmitDialogRequest
として定義されています。
type SubmitDialogRequest struct {
Type string `json:"type"`
URL string `json:"url,omitempty"`
CallbackId string `json:"callback_id"`
State string `json:"state"`
UserId string `json:"user_id"`
ChannelId string `json:"channel_id"`
TeamId string `json:"team_id"`
Submission map[string]interface{} `json:"submission"`
Cancelled bool `json:"cancelled"`
}
ユーザーが Interactive Dialog 上で入力したデータは Submission
に格納されています。Submission
はOpenDialogRequest
内のDialogElement
のName
を key、入力データを value とした map 形式のデータです。
今回の Interactive Dialog では、title
とmessage
というName
を持つDialogElement
を指定しているため、Submission
からはこれらの値をキーとする Value が格納されています。
...
Elements: []model.DialogElement{{
DisplayName: "Title",
Name: "title",
Type: "text",
}, {
DisplayName: "Message",
Name: "message",
Type: "textarea",
}},
...
以上より、Interactive Dialog からのリクエストを受信し、入力内容から Message Attachment のメッセージを作るアプリケーションは以下のようになります。
...
http.HandleFunc("/actions/dialog", func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
// (4) リクエストデータの読み出し
b, _ := ioutil.ReadAll(r.Body)
var payload model.SubmitDialogRequest
json.Unmarshal(b, &payload)
title, ok := payload.Submission["title"].(string)
if !ok {
resp := model.SubmitDialogResponse{Error: "failed to get title"}
w.Header().Add("Content-Type", "application/json")
io.WriteString(w, string(resp.ToJson()))
return
}
msg, ok := payload.Submission["message"].(string)
if !ok {
resp := model.SubmitDialogResponse{Error: "failed to get message"}
w.Header().Add("Content-Type", "application/json")
io.WriteString(w, string(resp.ToJson()))
return
}
// (5) Message Attachmentsインスタンス作成
post := &model.Post{
ChannelId: payload.ChannelId,
Props: model.StringInterface{
"attachments": []*model.SlackAttachment{{
Title: title,
Text: msg,
}},
},
}
// (6) REST APIによるメッセージ投稿
req, _ := http.NewRequest(http.MethodPost, "http://localhost:8065/api/v4/posts", strings.NewReader(post.ToJson()))
req.Header.Add("Authorization", "Bearer "+MattermostAccessToken)
req.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
// (7) エラー処理
dialogResp := model.SubmitDialogResponse{}
if err != nil {
dialogResp.Error = err.Error()
}
if resp.StatusCode != http.StatusCreated {
dialogResp.Error = fmt.Sprintf("failed to request: %s", resp.Status)
}
w.Header().Add("Content-Type", "application/json")
io.WriteString(w, string(dialogResp.ToJson()))
})
...
Interactive Dialog からのリクエストを受け取ったら、(4)
でリクエストを SubmitDialogRequest
形式で読み込みます。そして、SubmitDialogRequest
のSubmission
からtitle
、message
をキーに持つ値を取得します。Submission
の Value はinterface{}
型なので、文字列の場合はキャストが必要です。
データを読み出せたら (5)
で、読み出したデータを使って Message Attachments を含むPost
インスタンスを作成し、(6)
で REST API 経由で投稿を作成しています。REST API を実行するため、Mattermost のアクセストークン(MattermostAccessToken
)を事前に取得しておく必要があります。
最後に (7)
で REST API の実行結果をチェックし、エラーが発生している場合はSubmitDialogResponse
形式のデータを返却します。
type SubmitDialogResponse struct {
Error string `json:"error,omitempty"`
Errors map[string]string `json:"errors,omitempty"`
}
SubmitDialogResponse
のError
には Interactive Dialog 全体のエラーとして表示される文字列、Errors
にはDialogElement
の要素ごとのエラーメッセージを指定します。Errors
はSubmission
と同じくDialogElement
のName
を key とする map 形式でエラーメッセージを指定します。
試しに、以下のようなSubmitDialogResponse
を返したときの結果を紹介します。
dialogResp.Errors = map[string]string{
"title": "title error",
"message": "message error",
}
dialogResp.Error = "error"
w.Header().Add("Content-Type", "application/json")
io.WriteString(w, string(dialogResp.ToJson()))
以上のように Interactive Dialog からのリクエストを処理できます。
さいごに
本日は、Interactive Dialog の使い方について紹介しました。
明日からは、Mattermost のプラグイン機能について紹介していきます。
Discussion