📆

[Mattermost Integrations] Plugin (Server API)

2021/06/19に公開

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

本記事について

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

本記事では、Mattermost Server Plugin の開発に利用できる API について紹介します。

Mattermost Plugin についての公式ドキュメントは下記になります。
https://developers.mattermost.com/extend/plugins/overview/

Mattermost Plugin API

Mattermost Server Plugin の開発に利用できる API の一覧は下記にあります。

https://developers.mattermost.com/extend/plugins/server/reference/

ユーザーやチャンネル、投稿などの操作については REST API とほぼ同様の機能を有しています。数が多いため全ては紹介しませんが、Server Plugin 特有の処理に関する API を紹介していこうと思います。

GetConfig

GetConfigは、Mattermost Server のシステムコンソールの設定情報を取得します。

siteURL := p.API.GetConfig().ServiceSettings.SiteURL

p.API.GetConfig()下記のOnConfigurationChange内で実行し、Plugin 構造体のフィールドとして保持しておくのが良いそうです。
https://github.com/mattermost/mattermost-plugin-starter-template/blob/master/server/configuration.go

OpenInteractiveDialog

OpenInteractiveDialogは、第 16 日目の記事でも紹介した Interactive Dialog を Plugin から開くための API です。Interactive Dialog を開くにはTriggerIdというパラメータが必須であり、TriggerIdは Slash Command 実行時、もしくは Interactive Message Button/Menu 実行時に送信されるリクエストからしか取得できません。

以下の例は Slash Command 実行時に Interactive Dialog を開き、入力された情報を整形して投稿を作成するコードです。

plugin.go
func (p *SamplePlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
	appErr := p.API.OpenInteractiveDialog(model.OpenDialogRequest{
		TriggerId: args.TriggerId,
		URL:       fmt.Sprintf("%s/plugins/com.github.kaakaa.mattermost-plugin-starter-template/callback", *p.API.GetConfig().ServiceSettings.SiteURL),
		Dialog: model.Dialog{
			Title:       "Sample Plugin Dialog",
			SubmitLabel: "Submit",
			Elements: []model.DialogElement{
				{
					DisplayName: "タイトル",
					Name:        "title",
					Placeholder: "Title",
					Type:        "text",
				},
				{
					DisplayName: "Snippet",
					Name:        "snippet",
					Type:        "textarea",
				},
			},
		},
	})
	if appErr != nil {
		return nil, appErr
	}
	return &model.CommandResponse{}, nil
}

Interactive Dialog のリクエスト送信先を

		URL:       fmt.Sprintf("%s/plugins/com.github.kaakaa.mattermost-plugin-starter-template/callback", *p.API.GetConfig().ServiceSettings.SiteURL),

のようにすることで、Plugin から開いた Interactive Dialog のリクエストを Plugin で処理することもできます。このリクエストの受信先として、Mattermost Plugin Server Hook であるServeHTTPを使って/callbackのエンドポイントを受け取る処理を追加します。

plugin.go
func (p *SamplePlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
	if r.URL.Path == "/callback" {
		defer r.Body.Close()
		request := model.SubmitDialogRequestFromJson(r.Body)
		t := request.Submission["title"]
		s := request.Submission["snippet"]
		p.API.CreatePost(&model.Post{
			UserId:    request.UserId,
			ChannelId: request.ChannelId,
			Message:   fmt.Sprintf("## %s\n\n```\n%s\n```", t, s),
		})
		return
	}
	fmt.Fprint(w, "Hello, world!")
}

このようにすることで、Interactive Dialog の起動からリクエストの処理までを Plugin だけで完結させることができます。

PublishWebSocketEvent

PublishWebSocketEventは、プラグイン独自の WebSoket イベントを送信する API です。

PublishWebSocketEventは、3 つの引数を取ります。

  • event string: WebSocket イベント名を指定します。実際に送信される WebSocket イベントには接頭辞としてcustom_<PluginID>_が付与されます
  • payload map[string]interface{}: 送信されるデータの内容を指定します
  • broadcast *model.WebsocketBroadcast: WebSocket を送信する対象を指定します
    • model.WebsocketBroadcastで、ユーザー、チャンネル、チームを指定したり、受信対象から外すユーザーを指定したりできます

Webapp Plugin API のregisterWebSocketEventHandlerと組み合わせることで、プラグイン内で WebSocket イベントに関する処理を完結させることができます。
この辺りの使用例は第 22 日目以降の記事で紹介する予定です。

SendMail

SendMailは、Mattermost Plugin から HTML メールを送信する API です。システムコンソールで SMTP の設定が完了している必要があります。

Slash Command を実行した際にメールを送信する例は以下のようになります。

plugin.go
func (p *SamplePlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
	appErr := p.API.SendMail(
		"test@example.com",
		"Sample Title",
		"<h1>Mail from plugin</h1><div><p>Sample Mail</p></div>")
	if appErr != nil {
		return nil, appErr
	}

	return &model.CommandResponse{}, nil
}

KVGet / KVSet

KVGet, KVSetは、プラグインごとに割り当てられた Key Value ストアから値を取得、または値を設定する API です。Key Value ストアには[]byte型のデータを格納できるため、格納用データの構造体を作成し、json.Marshal[]byte型のデータ化したものを出し入れするのが主な使い方になるのではないかと思います。

Key Value ストアを使用してカウンターを実装した例が以下になります。

plugin.go
type counter struct {
	Count     int    `json:"count"`
	CreatedAt string `json:"created_at"`
}

func (p *SamplePlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
	// read data
	b, appErr := p.API.KVGet("counter")
	if appErr != nil {
		return nil, appErr
	}
	var ct counter
	if b == nil {
		// init if data is not found
		ct = counter{
			Count:     0,
			CreatedAt: time.Now().Format(time.RFC3339),
		}
	} else {
		if err := json.Unmarshal(b, &ct); err != nil {
			return nil, model.NewAppError("where", "id", nil, err.Error(), http.StatusInternalServerError)
		}
	}
	// count up
	ct.Count = ct.Count + 1
	b, err := json.Marshal(ct)
	if err != nil {
		return nil, model.NewAppError("where", "id", nil, err.Error(), http.StatusInternalServerError)
	}
	// set data
	if appErr := p.API.KVSet("counter", b); appErr != nil {
		return nil, appErr
	}

	return &model.CommandResponse{Text: fmt.Sprintf("counter: %d", ct.Count)}, nil
}

Key Value ストアを操作する API はKVGet, KVSet以外にも数多くあります。データを削除するKVDeleteや、プラグイン内で保存済みのデータのkeyを取得するKVList、期間を指定して値を格納できるKVSetWithExpiryなどがあります。また、同時にKVSetが実行された場合に不整合が起きないようにするため、Atomic な Key Value ストアの操作を強制するためのKVCompareAndSetなどもあります。

API の一覧については公式ドキュメントを参照してください。
https://developers.mattermost.com/extend/plugins/server/reference/

Helpers

Mattermost Plugin API は数多くあるため、Mattermost に対するほとんどの操作を実行することはできますが、より簡単に Plugin API を実行するための Helper 関数がいくつか存在します。

ここでは、Helper 関数のうち一部を紹介します。

Helper 関数の一覧は下記の公式ドキュメントにあります。
https://developers.mattermost.com/extend/plugins/server/reference/#Helpers

KVGetJSON / KVSetJSON

先ほどのKVGetKVSetのコードでは、構造体を[]byteに変換するためにKVSetを呼ぶ前にjson.Marshalを、KVGetを呼んだ後にjson.Unmarshalを呼んでいましたが、KVGetJSONKVSetJSONを使用することで、その必要がなくなります。

そのため、先ほどの処理を多少すっきりと書くことができます。

plugin.go
type counter struct {
	Count     int    `json:"count"`
	CreatedAt string `json:"created_at"`
}

func (p *SamplePlugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
	// read data
	var ct counter
	exists, err := p.Helpers.KVGetJSON("counter", &ct)
	if err != nil {
		return nil, model.NewAppError("where", "id", nil, err.Error(), http.StatusInternalServerError)
	}
	if !exists {
		ct = counter{
			Count:     0,
			CreatedAt: time.Now().Format(time.RFC3339),
		}
	}

	// count up
	ct.Count = ct.Count + 1

	// set data
	if appErr := p.Helpers.KVSetJSON("counter", ct); appErr != nil {
		return nil, model.NewAppError("where", "id", nil, err.Error(), http.StatusInternalServerError)
	}

	return &model.CommandResponse{Text: fmt.Sprintf("counter: %d", ct.Count)}, nil
}

CheckRequiredServerConfiguration

CheckRequiredServerConfigurationは、システムコンソールの設定をチェックするための Helper 関数です。

引数に指定したmodel.Configと同じ設定になっているかをチェックし、異なる設定だった場合はfalseを返します。

システムコンソール > Bot アカウント > Bot アカウントの作成を有効にするの設定が有効になっていない場合にプラグインの起動を停止するような例は以下のようになります。

plugin.go
func toPtr(v bool) *bool {
	return &v
}

func (p *SamplePlugin) OnActivate() error {
	b, err := p.Helpers.CheckRequiredServerConfiguration(&model.Config{
		ServiceSettings: model.ServiceSettings{
			EnableBotAccountCreation: toPtr(true),
		},
	})
	if err != nil {
		return err
	}
	if !b {
		return errors.New("EnableBotAccountCreation must be true.")
	}

    ...

プラグイン起動時、サーバーのログには以下のようなログが出力されます。

2020-12-13T00:16:01.409+0900    error   mlog/log.go:229 Unable to restart plugin on upgrade.    {"path": "/api/v4/plugins", "request_id": "u31bzwuyhbyibratzznjxgxzrr", "ip_addr": "::1", "user_id": "87x93uo8pfnzdro9ktcmobpa1r", "method": "POST", "err_where": "installExtractedPlugin", "http_code": 500, "err_details": "EnableBotAccountCreation must be true."}

さいごに

本日は、Mattermost Plugin の Server API について紹介しました。
明日からは、Mattermost Plugin のWebappサイドの実装について紹介します。

Discussion