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を開き、入力された情報を整形して投稿を作成するコードです。

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のエンドポイントを受け取る処理を追加します。

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つの引数を取ります。

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

SendMail

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

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

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ストアを使用してカウンターを実装した例が以下になります。

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を使用することで、その必要がなくなります。

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

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アカウントの作成を有効にするの設定が有効になっていない場合にプラグインの起動を停止するような例は以下のようになります。

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サイドの実装について紹介します。

comments powered by Disqus