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のプラグイン機能について紹介していきます。