📆

[Mattermost Integrations] Matterpoll Plugin (Webapp)

2021/06/19に公開

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

本記事について

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

以前から開発に参加しているMatterpollが Mattermost 公式の Plugin Marketplace で Community Plugin として公開されました。

昨日の記事では Matterpoll Plugin の Server 側の実装について紹介しましたが、本日の記事では Matterpoll Plugin の Webapp 側の機能について紹介します。

Webapp 側の処理

Webapp 側では、ユーザー毎に投票の見え方を変えるための処理を実装しています。

独自の PostType による投票済み回答のハイライト

matterpoll-posttype.dio.png

Mattermos で投稿したメッセージは、Mattermost 上では Post 構造体のデータとして扱われています。そして、Post構造体のType フィールドの値によって投稿の種別を判別しています。Post.Typeはデフォルトでは大きく分けて二つの種類があり、一つは通常ユーザーが投稿するメッセージ( Post.Type が空文字(POST_DEFAULT)。もう一つは、チャンネル参加時などにシステムによって投稿されるメッセージ(Post.Typesystem_ で始まる)です。

2 つのPost.Typeの投稿

Mattermost 上のメッセージの見た目は Message Attachements の機能を使う事である程度変更することはできますが、 Message Attachments では Mattermost ユーザー全員に対して同じ見た目しか提供できないため、ユーザー毎にメッセージの一部を変更したい場合などはプラグインで処理する必要があります。Matterpoll の場合「自分が投票した回答がどれなのか表示したい」というようなユーザー毎に表示を変更する要求があったため、この部分をプラグインで処理しています。

プラグインで投稿の見た目を変えたい場合、まず Server 側で投稿を作成する際に、独自のPost.Typeを設定する必要があります。Matterpoll ではスラッシュコマンド /poll が実装された際に投票用の投稿 (Post) を作成するのですが、その際に Post.Typecustom_matterpoll という値を設定しています。

server/plugin/plugin.go
...
const (
  ...
	// MatterpollPostType is post_type of posts generated by Matterpoll
	MatterpollPostType = "custom_matterpoll"
)
...

https://github.com/matterpoll/matterpoll/blob/45f095875a98fb1d4f3f166851c86f41b987493e/server/plugin/plugin.go#L53

server/plugin/command.go
...
  post := &model.Post{
		UserId:    p.botUserID,
		ChannelId: args.ChannelId,
		RootId:    args.RootId,
		Type:      MatterpollPostType,
		Props: map[string]interface{}{
			"poll_id": newPoll.ID,
		},
	}
	model.ParseSlackAttachment(post, actions)

	rPost, appErr := p.API.CreatePost(post)
...

https://github.com/matterpoll/matterpoll/blob/45f095875a98fb1d4f3f166851c86f41b987493e/server/plugin/command.go#L191

上記のようにPost.Type にプラグイン独自の値を設定しましたが、Webapp 側でcustom_matterpollというPost.Typeに対する処理を追加しない限り、通常の投稿と同じように表示されてしまいます。Mattermost プラグインでは、WebApp プラグイン API のregisterPostTypeComponent を使い Post.Typecustom_matterpoll が設定された場合のレンダリング方法を指定しています。

registerPostTypeComponent は第一引数にPost.Type の文字列を指定し、第二引数に React コンポーネントを指定します。Matterpoll の場合は、Server 側で Post.Typecustom_matterpoll を指定しているため、下記のように第一引数に custom_matterpoll を、第二引数に独自の React コンポーネントを指定して registerPostTypeComponent を呼び出しています。

webapp/src/actions/config.js
import PostType from 'components/post_type';
...

    if (data.experimentalui) {
        registeredComponentId = registry.registerPostTypeComponent('custom_matterpoll', PostType);
    } else {
       registry.unregisterPostTypeComponent(registeredComponentId);
       registeredComponentId = '';
    }
...

https://github.com/matterpoll/matterpoll/blob/fa1f3aa8489deb24706937beac7d9976b0528ed7/webapp/src/actions/config.js#L11

registerPostTypeComponent は戻り値として登録された React コンポーネントに対応する ID 文字列を返しますが、これは登録したコンポーネントをunregisterPostTypeComponentを利用して解除する場合に必要なため state に保持しています。

Matterpoll では、Webapp 側の機能はまだ Experimental な機能として設定で On/Off を切り替えられるようにしているため、設定変更によってコンポーネントを登録/解除できるようにしています。

このように、プラグイン独自に投稿の見た目を変えたい場合、自作の React コンポーネントを作成する必要があります。この時、Mattermost 本体が構築する投稿に関する DOM の一部だけ表示を変更するなどができると良いのですが、Mattermost の React コンポーネントは外部から参照できる形で公開されていないため、一から自分で作る必要がありなかなか難儀しています...。ただ、React コンポーネントを開発する際のユーティリティ関数がいくつか利用可能なため、この辺りを使っいます。

https://developers.mattermost.com/extend/plugins/webapp/reference/#post-utils

プラグイン独自の WebSocket イベント

matterpoll-ws.dio.png

Mattermost の Server と Webapp の間は WebSocket によりやり取りが行われています。

Webapp 側で WebSocket イベントを受信した時の独自処理を登録するのが registerWebSocketEventHandlerメソッドです。このメソッドは、第一引数に WebSocket イベント名、第二引数に WebSocket イベントを受け取ったときに実行される処理を関数として登録します。

Mattermost がデフォルトで Publish している WebSocket イベントはAPI ドキュメントに一覧でまとめられており、メッセージの投稿/削除/編集やチャンネルの作成/削除、参加/脱退など、ユーザー操作に対するイベントが一通り網羅されています。また、Server 側からプラグインごとに独自の WebSocket イベントを Publish することもでき、Matterpoll では、Matterpoll プラグインの設定が変更された時と、ユーザーが投票に回答したときに独自の WebSocket イベントを送信しています。

プラグイン独自の WebSocket イベントを Publish するには、Server 側でプラグイン API のPublishWebSocketEventを実行するだけでよく、Matterpoll では下記のようなメソッドを作成し、投票データに変更があった時にこの処理を呼び出すことで、投票に関する最新のデータを Webapp に送信しています。

server/plugin/api.go
func (p *MatterpollPlugin) publishPollMetadata(poll *poll.Poll, userID string) {
	hasAdminPermission, appErr := p.HasAdminPermission(poll, userID)
	if appErr != nil {
		p.API.LogWarn("Failed to check admin permission", "userID", userID, "pollID", poll.ID, "error", appErr.Error())
		hasAdminPermission = false
	}
	metadata := poll.GetMetadata(userID, hasAdminPermission)
	p.API.PublishWebSocketEvent("has_voted", metadata.ToMap(), &model.WebsocketBroadcast{UserId: userID})
}

https://github.com/matterpoll/matterpoll/blob/45f095875a98fb1d4f3f166851c86f41b987493e/server/plugin/api.go#L411

プラグイン API である PublishWebSocketEvent は 3 つの引数を取り、第一引数で Publish する WebSocket イベントのイベント名を指定します。実際には、ここで指定されたイベント名の先頭に custom_<pluginid>_ を付与したイベント名としてやり取りされるため、第 1 引数を "has_voted" としている今回の例では custom_com.github.matterpoll.matterpoll_has_votedという WebSocket イベント名となります。イベント名にプラグイン ID が入っているため、プラグイン間のイベント名の競合については考慮する必要がありません。

第 2 引数には Publish するデータを指定します。map 形式で value には interface{} 型が指定できますが、この値には Go のプリミティブ型か mattermost-server/model しか指定できません。それ以外の値を指定してしまうと、実行時にエンコードエラーとなります。(Mattermost サーバーのログを確認しないと分からないためハマりがちです)

第 3 引数では、この WebSocket イベントをどのユーザーに対して送信するかを決定します。model.WebsocketBroadcastの定義を見ると、ユーザー単位以外にもチャンネルやチーム単位で指定することもできそうです(試したことはないですが)。

次に、Server 側から Publish された WebSocket イベント custome_com.github.matterpoll.matterpoll_has_voted を Webapp 側で処理するために、前述の registerWebSocketEventHandler を実行します。

webapp/src/plugin.jsx
export default class MatterPollPlugin {
    async initialize(registry, store) {
        ...
        registry.registerWebSocketEventHandler(
            'custom_' + pluginId + '_has_voted',
            (message) => {
                store.dispatch(websocketHasVoted(message.data));
            },
        );
        ...
    }

https://github.com/matterpoll/matterpoll/blob/0b797025cfbf319c43464fcf457d4cdfe5086188/webapp/src/plugin.jsx#L17

上記のように、Webapp 側のプラグイン起動時に WebSocket を受信した時の Handler を登録しておくことで、Mattermost 上の操作に応じて Webapp 側の表示を変えるなどの処理を行えるようになります。

さいごに

本日は、Mattermost 上で投票を行えるようにする Matterpoll Plugin の Webapp 側の実装について紹介しました。
明日は、Matterpoll Plugin 開発におけるテストや CI について紹介します。

Discussion