📆

[Mattermost Integrations] Slash Command 応用編

2021/06/19に公開

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

本記事について

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

本日の記事は、昨日まで紹介してきた Slash Command を使ったサンプルアプリの紹介です。

背景

コロナ影響により WFH(Work From Home)が定着してきましたが、毎日自宅で作業していると同じ景色しか見えないため気持ちが滅入ってくることがあります。自分は少しでも日常に変化を付けようと音楽を流しながら作業していることが多いので、Mattermost から Spotify のプレイリストを探すことができる統合機能を作ってみました。(普段は AppleMusic 派ですが...)

動作例

movie

/music [search_term]で、[search_term]を検索キーとしてプレイリストを検索します。[search_term]を指定しない場合、workfromhomeを検索キーとして検索します。
コマンドを実行すると、見つかったプレイリストの情報をリンク付きで Mattermost に投稿します。この時、card機能を使って投稿からプレイリストの内容を確認できるようにしています。

コード

ソースコードは下記のリポジトリにコミットしてあります。

https://github.com/kaakaa/blog/tree/master/code/mattermost/2020-advent-calendar/slash-command-spotify

Mattermost 連携部分の動作コードは下記になります。これとは別に Spotify クライアント用のコードもありますが、そちらの紹介は割愛します。

package main

import (
	"fmt"
	"io"
	"log"
	"math/rand"
	"net/http"
	"strings"

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

const (
	// (1) 定数の設定
	MattermostToken     = "arzoyh4i57dw5x57yumenuktqw"
	SpotifyClientID     = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
	SpotifyClientSecret = "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"
)

func main() {
	// (2) Spotify クライアントの初期化
	client, err := NewSpotifyClient(SpotifyClientID, SpotifyClientSecret)
	if err != nil {
		log.Fatalf("failed to set up spotify client: %s", err.Error())
	}
	http.HandleFunc("/spotify", func(w http.ResponseWriter, r *http.Request) {
		r.ParseForm()
		// (3) トークンによるリクエストの検証
		if MattermostToken != r.Form.Get("token") {
			log.Printf("received token is invalid: %s", r.Form.Get("token"))
			return
		}

		// (4) serach_termの取得
		term := "workfromhome"
		if t := r.Form.Get("text"); len(t) > 0 {
			term = t
		}

		// (5) プレイリストの検索
		playlist, err := client.findRandomPlaylist(term)
		if err != nil {
			io.WriteString(w, fmt.Sprintf("failed to serach playlist: %s", err.Error()))
			return
		}

		// (6) プレイリスト内のトラック情報の取得
		tracks, err := client.getTracks(playlist.Id)
		if err != nil {
			io.WriteString(w, fmt.Sprintf("failed to get tracks: %s", err.Error()))
			return
		}
		contextInfo := []string{"Tracks"}
		for _, t := range tracks {
			contextInfo = append(contextInfo, fmt.Sprintf("1. %s", t))
		}

		// (7) Mattermostへのレスポンスの構築
		response := model.CommandResponse{
			ResponseType: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL,
			Username:     "Music Provider",
			Props: map[string]interface{}{
				"card": strings.Join(contextInfo, "\n"),
			},
			Attachments: playlist.ToMessage(),
		}

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

	// Start server
	http.ListenAndServe(":8080", nil)
}

(1) 定数の設定

プログラム内で使用する定数を宣言しています。

Mattermost 関係は Slash Command から送信されるリクエスト検証用のトークン文字列。
Spotify は、アプリケーションのクライアント ID/クライアントシークレットです。これらの値の取得方法については Spotify Developer Platform: Spotify API アクセスしてデータ取得してみてみた - Qiita などを参照ください。Client Credential Flowで認証するため、Redirect URI などの設定は必要無いかと思います。

(2) Spotify クライアントの初期化

Spotify の API を実行するためのインスタンスを構築しています。内容については割愛します。

(3) トークンによるリクエストの検証

Mattermost の Custom Slash Command によるリクエストを受信したら、まずはトークンによる検証を行います。

(4) serach_term の取得

Custom Slash Command のリクエストから、Slash Command の引数として指定されたテキストを取得します。これが Spotify への検索キーワードになります。
Custom Slash Command のリクエスト内容については公式ドキュメントを参照ください。

(5) プレイリストの検索

(2) で初期化した Spotify クライアントを使ってプレイリストの検索を行います。詳細は割愛しますが、Search for an Item | Spotify for Developersの API を実行し、ランダムに選んだ一つのプレイリストを返すようになっています。

(6) プレイリスト内のトラック情報の取得

こちらも Spotify クライアントを使って(5)で取得したプレイリストのトラックを取得しています。内部ではGet a Playlist's Items | Spotify for Developersの API を実行しています。
一度の API リクエストで取得できる最大のトラックス数が 100 であり、100 トラック以上あるプレイリストについては、最初の 100 トラックだけを取得しています。

(7) Mattermost へのレスポンスの構築

今まで取得してきた情報を使って Mattermost へのレスポンスを構築しています。attachmentsフィールドはMessageAttachmentsの機能を使っていますが、MessageAttachmentsについては後日紹介します。


さいごに

以上のように、Custom Slash Command によるリクエストを受信したサーバーから、外部の API を実行し、結果を Mattermost に返すようなアプリケーションを構築することができます。外部 API の実行に時間がかかる恐れがある場合は、Delayed Responseを利用すると Slash Command を実行したユーザーへフィードバックをすぐに返せるため便利です。Delayed Response の機能については昨日の記事で紹介しました。

明日は、Message Attachments について紹介します。

Discussion