📆

[Mattermost Integrations] Slash Command 発展編

2021/06/19に公開

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

本記事について

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

前回の記事では、Mattermost で/で始まるコマンドを実行することで、特定の処理を実行することができる Slash Command の機能について紹介しました。また、Custom Slash Command により外部アプリケーションにリクエストを送信する方法を紹介しました。本記事では、Custome Slash Command のリクエストが送信された外部アプリケーション側から Mattermost 側へ返却するレスポンス内容について紹介します。

Slash Command のレスポンスとして投稿を作成する

Slash Command によるリクエストを受け付けた外部アプリケーションから、レスポンスとして Mattermost に投稿を作成する方法について紹介します。

以下のコードは昨日のコードにレスポンスを返すコードを追加したものです。

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

const (
	WebHookToken = "8w7foap4ufrsfczda8uez51yxo"
)

func main() {
	http.HandleFunc("/command", func(w http.ResponseWriter, r *http.Request) {
		log.Printf("%#v", r.Form)
		r.ParseForm()

		log.Printf("token: %v", r.Form.Get("token"))
		// (1) Tokenをチェック
		if r.Form.Get("token") != WebHookToken {
			log.Printf("received an invalid request with token: %s", r.Form.Get("token"))
			return
		}

		// (2) レスポンスとしてMattermostへ投稿を作成
		w.Header().Add("Content-Type", "application/json")
		io.WriteString(w, fmt.Sprintf(`{"text": "**%s**"}`, r.Form.Get("text")))
	})
	http.ListenAndServe(":8080", nil)
}

Outgoing WebHook の時とほぼ同じですが、(1) でトークンを利用したリクエストの検証を行い、(2) で Slash Command のレスポンスとして投稿を作成しています。レスポンスヘッダーへのContent-Type: application/jsonの設定は必須です。textフィールドに指定したメッセージが Mattermost に投稿されます。

このサーバに対して/サンプルコマンド TESTという Slash Command を送信した時の画面が下記になります。

response

作成された投稿には、BOTというラベルに加え、(あなただけが見ることができます)というメッセージがついています。Slash Command のレスポンスとして作成した投稿は、デフォルトでは Slash Command を実行したユーザーしか見ることができない投稿になり、他のユーザーからは見ることができません。チャンネルの全員が閲覧可能な投稿を作成するには、後述のresponse_typeパラメータを設定します。

Slash Command レスポンスパラメータ

text

投稿されるメッセージであり、後述のattachmentsを指定しない場合は必須のパラメータです。Markdown 記法や、@によるメンションを投稿することもできます。

io.WriteString(w, `{"text": "**TEST**"}`)

response_type

Slash Command のレスポンスにより作成される投稿の種別を指定します。デフォルトではephemeralであり、Slash Command を実行したユーザーにしか見えない投稿になります。チャンネル全員が見えるようにするにはresponse_typein_channelを指定します。

io.WriteString(w, `{"text": "**TEST**", "response_type": "in_channel"}`)

username, icon_url

Slash Command のレスポンスとして作成される投稿のユーザー名とアイコンの表示を変更することができます。デフォルトでは、ウェブフックを作成したユーザーになります。

このパラメータを使用する場合、システムコンソールから下記の設定をそれぞれ有効にする必要があります。

  • システムコンソール > 統合機能管理 > 統合機能によるユーザー名の上書きを許可する
  • システムコンソール > 統合機能管理 > 統合機能によるプロフィール画像アイコンの上書きを許可する

(この設定を有効にすると、usernameicon_urlを指定しない場合の投稿の作成者がデフォルトのアイコン、ユーザー名(webhook)に変更されます)

io.WriteString(w, `{
	"text": "**TEST**",
	"username": "new-bot",
	"icon_emoji": "http://www.mattermost.org/wp-content/uploads/2016/04/icon_WS.png"
}`)

use overwrite

channel_id

投稿を作成するチャンネルを指定できます。ここで指定するチャンネル ID は、チャンネルメニュー > 情報を表示するから確認できます。

channel info view

channel info view

io.WriteString(w, `{
	"text": "**TEST**",
	"channel_id": "u6ijofzg8bnsie5u8c1eumtc1h"
}`)

goto_location

Slash Command 特有のパラメータです。goto_locationフィールドに URL を指定しておくと、Slash Command が実行された際に指定された URL に移動します。この時textなどで指定した投稿も作成されます。

io.WriteString(w, `{
	"text": "**TEST**",
	"goto_location": "https://github.com/mattermost"
}`)

movie

goto_locationにはhttpsだけでなく、sshmailtoのプロトコルも使用できるため、例えばスラッシュコマンドを実行した場合に「メーラーでメール作成画面を開く」というような動作をさせることも可能です。

io.WriteString(w, `{
	"text": "**TEST**",
	"goto_location": "mailto:test@example.com"
}`)

attachments

attachmentsは、Mattermost にリッチな投稿を作成することができるMessageAttachmentsを指定できる機能です。MessageAttachmentsについては、別途紹介記事を用意する予定です。

type

typeは、Mattermost の投稿の種別を指定するパラメータですが、基本的に使用することはありません。Plugin(Webapp)にてプラグイン独自の投稿種別を指定した場合のみに使用するものかと思います。Plugin(Webapp)については、別途紹介記事を用意する予定です。

extra_responses

extra_responses配列としてレスポンスの JSON データを記述することで複数の投稿を作成することができます。
extra_responsesが導入される Mattermost v5.6 以前は、Slash Command のレスポンスとして複数の投稿を作成したい場合は REST API を実行するなどをしなくてはなりませんでしたが、extra_responsesによって REST API を実行するためのコードなどが必要なくなりました。

Slash Command のリクエストに対して下記のレスポンスを返却すると、「チャンネル全員が閲覧できる投稿を作りつつ、コマンド実行ユーザーのみにしか見えない投稿も合わせて作成し、さらに、別のチャンネルにコマンドが実行されたことを通知する」ということができるようになります。

io.WriteString(w, `{
	"text": "**TEST**",
	"response_type": "in_channel",
	"extra_responses": [
		{
			"text": "**Secret Info**"
		},
		{
			"text": "Notify in other channel",
			"channel_id": "u6ijofzg8bnsie5u8c1eumtc1h"
		}
	]
}`)

extra_responses内にさらにextra_responsesを指定することはできず、また、goto_locationも使用できません。

skip_slack_parsing

Mattermost の統合機能は Slack 記法との互換性を意識して作成されていますが、skip_slack_parsingtrueを指定すると、レスポンスとして返却したtextを Slack 互換の記法として解析しないようになります。あまり使うことは無いかと思いますが、レスポンスが想定どおりに投稿されない場合などに利用を検討してみても良いかもしれません。

このパラメータが追加されたきっかけとなったのは下記の Issue です。
slack-compatibility-layer seems to break results of MM-only integration · Issue #12702 · mattermost/mattermost-server

props

Incoming WebHook などと同様、投稿のメタデータを格納するフィールドです。Incoming WebHook と同様、cardも利用することができます。

Slash Command で時間のかかる処理を実行する場合

単に Slash Command のレスポンスとして投稿を作成できるだけだと、外部アプリケーションで時間のかかる処理を実行し、その結果を返したいという場合、Mattermost 上のユーザーはコマンドを実行してから結果が返るまで何の反応も得ることができません。それを回避するため、Slash Command 実行時に送信されるリクエストには、レスポンスを一度返した後で投稿を作成ことができる URL であるresponse_urlフィールドを持っています。

response_urlを使ったコードは下記のようになります。

main.go
package main

import (
	"io"
	"log"
	"net/http"
	"strings"
	"time"
)

const (
	WebHookToken = "8w7foap4ufrsfczda8uez51yxo"
)

func main() {
	http.HandleFunc("/command", func(w http.ResponseWriter, r *http.Request) {
		r.ParseForm()
		if r.Form.Get("token") != WebHookToken {
			log.Printf("received an invalid request with token: %s", r.Form.Get("token"))
			return
		}

		w.Header().Add("Content-Type", "application/json")
		io.WriteString(w, `{"text": "Request is recieved. Results will return later."}`)

		// (1) 5秒後に "response_url" に対してリクエストを送信
		go func(url string) {
			time.Sleep(5 * time.Second)
			body := strings.NewReader(`{"text": "This is the result.", "response_type": "in_channel"}`)
			http.Post(url, "application/json", body)
		}(r.Form.Get("response_url"))
	})
	http.ListenAndServe(":8080", nil)
}

今までと同じようにレスポンスを返した後、5 秒 Sleep した後にresponse_urlの URL に対してリクエストを送信しています

movie

response_urlに送信するリクエストには、goto_locationは使えませんでしたが、response_typeextra_responsesなどは使えるようです。

また、response_urlが有効なのは、元の Slash Command が実行されてから 30 分間のみで、5 つまでの投稿を作成することができます。処理が 30 分以上かかる場合や、5 つ以上の投稿を作成したい場合は、REST API を利用するか、Plugin を利用する必要がありそうです。

さいごに

本日は、Slash Command の詳細な使い方について紹介しました。
明日は、REST API の使い方について紹介していきます。

Discussion