(追記 2020/04/12)

本記事でも使用しているdepがもうメンテナンスされておらず、本記事で紹介している内容で Mattermost の最新版を利用したプラグインの開発が困難になっているようです。

今後 Mattermost プラグイン開発を始める場合、Mattermost プラグイン用の GitHub リポジトリテンプレートを使ってみる - Qiita でも紹介していますが、Mattermost 公式チームがメンテナンスしているテンプレートリポジトリ https://github.com/mattermost/mattermost-plugin-starter-template を利用することを推奨します。

(追記おわり)


はじめに

2017/11/16 にリリースされた Mattermost v4.4.0 で、Mattermost 自体の機能拡張を行うためのプラグイン機能が追加されました。 その後、2018/08/16 にリリースされた Mattermost v5.2.0 で、破壊的変更となるプラグインアーキテクチャの一新が行われ、現在も新たな拡張ポイントや API が追加され続けています。

Mattermost プラグインでは、サーバーサイド(Go 言語)とフロントエンド(React.js)両方の拡張が行えます。サーバーサイドを拡張したい場合は Go 言語、フロントエンドを拡張したい場合は Javascript(React.js)を書くことになります。 Mattermost プラグインを実装することで、新たなユーザーがチーム/チャンネルに参加した際の処理を追加したり、投稿を表す React コンポーネントを自作のものに置き換えるなど、今まで手を入れることができなかった部分にまで機能追加を行うことができるようになります。

今回は、簡単な実装例を交えながら Mattermost プラグインの作成方法について紹介していきたいと思います。(Go、Javascript の開発知識がある前提で書いています)

この記事で紹介しているコードは、下記のリポジトリで公開しています。 https://github.com/kaakaa/mattermost-plugin-first (Mattermost v5.6 を対象に動作確認を行なっています)

Step 0: Mattermost サーバーの設定

Mattermost は、デフォルトではシステムコンソールからのプラグインアップロード機能が無効化されています。この機能を有効にするにはconfig.jsonを編集する必要があります。


...
    },
    "PluginSettings": {
        "Enable": true,
        "EnableUploads": true,
        "Directory": "./plugins",
...

Mattermost 設定ファイル内のPluginSettingsにあるEnableEnableUploadstrueにし、Mattermost サーバーを再起動します。

他にもプラグインを直接サーバーに配置したり、REST API 経由でプラグインをアップロードすることもできますが、システムコンソールからのアップロードが最も簡単なためこの手順で説明していきます。

Step 1: プラグイン開発・アップロード

まず、何も機能を持たない空のプラグインを作成し、そのプラグインをアップロードするまでのフローを見ていきます。

root/
  ├ dist/
  │  └ org.kaakaa.mattermost-plugin-first_0.0.2.tar.gz
  ├ Makefile
  └ plugin.json

マニフェストファイル

Mattermost プラグインの実体は.tar.gz形式のファイルとなります。 .tar.gzファイルの中身として最低限必要なファイルはマニフェストファイルだけです。

JSON 形式(plugin.json)か YAML 形式(plugin.yaml)で、プラグインの ID、名前、バージョンや実行バイナリのパスなどのメタ情報を記述するファイルです。

マニフェストファイル記述できる内容については、下記のリファレンスを参照してください。 https://developers.mattermost.com/extend/plugins/manifest-reference/

まず、下記のようにマニフェストファイルを作成します。

{
    "id": "org.kaakaa.mattermost-plugin-first",
    "name": "Mattermost Plugin Sample",
    "description": "このプラグインはQiita記事用のMattermostプラグインです。",
    "version": "0.0.1"
}

プラグイン作成

上記のマニフェストファイルを含む.tar.gzファイルを作成するため、下記のような Makefile を作成します。(make の環境がない場合は、手動でコマンドを実行しても構いません)

PLUGIN_ID ?= org.kaakaa.mattermost-plugin-first
PLUGIN_VERSION ?= 0.0.1
BUNDLE_NAME ?= $(PLUGIN_ID)_$(PLUGIN_VERSION).tar.gz

build:
	rm -rf dist/
	mkdir -p dist/$(PLUGIN_ID)
	cp plugin.json dist/$(PLUGIN_ID)/

	cd dist && tar -cvzf $(BUNDLE_NAME) $(PLUGIN_ID)

	@echo plugin built at: dist/$(BUNDLE_NAME)

make buildを実行することでdist/org.kaakaa.mattermost-plugin-first_0.0.1.tar.gzというファイルが作成されます。

プラグインのアップロード

Mattermost の システムコンソール > プラグイン(ベータ版) > 管理画面からプラグインをアップロードします。

スクリーンショット 2019-01-01 10.36.28.png

ファイルを選択する ボタンを押し、先ほど作成したdist/org.kaakaa.mattermost-plugin-first_0.0.1.tar.gzを選択し、アップロードボタンを押すことでプラグインがアップロードされます。 プラグインが正常にアップロードされるとインストール済みプラグインに表示されます。

スクリーンショット 2019-01-01 10.42.33.png

アップロードされたプラグインの有効にするボタンを押すことでプラグインを有効にすることができますが、現在はプラグインの実装が行われていないため、エラーメッセージ「このプラグインを起動できませんでした。エラーに関するシステムログを確認してください。」が表示されるはずです。

スクリーンショット 2019-01-01 10.43.14.png

Step 2: サーバーサイドプラグインの実装

Step 1 で作成したプラグインにサーバーサイドの実装を加えていきます。 serverというディレクトリを作成し、そこに Go コードを書いていきます。

root/
  ├ dist/
  │  └ org.kaakaa.mattermost-plugin-first_0.0.2.tar.gz
  ├ server/
  │  ├ main.go
  │  ├ plugin.go
  │  └ Gopkg.toml
  ├ Makefile
  └ plugin.json

依存ライブラリの管理にはgolang/depを使用しています。

main.go

まず Go 言語の main メソッドで Mattermost プラグインを認識させるためにplugin.ClientMainメソッドを呼び出し、自作の Plugin 用の構造体(FirstPlugin)を読み込ませます。

package main

import "github.com/mattermost/mattermost-server/plugin"

func main() {
	plugin.ClientMain(&FirstPlugin{})
}

この自作の FirstPlugin 構造体にプラグインの実装を追加していきます。

plugin.go

FirstPlugin構造体に最低限求められるのは、plugin.MattermostPluginを継承することです。 plugin.MattermostPluginを継承することで Mattermost プラグインとして認識され、この構造体の持つAPIというフィールドを通じてプラグイン用の各種 APIを実行できるようになります。 実際の API の呼び出し方については、次のhooks.goのセクションで説明しています。

package main

import "github.com/mattermost/mattermost-server/plugin"

type FirstPlugin struct {
	plugin.MattermostPlugin
}

これでサーバーサイドのプラグインを実装することはできましたが、このプラグインはまだ何の機能も持っていません。 Mattermost 本体の動作に影響を及ぼすには、いずれかの Hooks を実装する必要があります。

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

hooks.go

ここでは、MessageWillBePostedの Hooks を実装し、Mattermost 上に Qiita のリンクを含む投稿が行われた際に自動で#Qiitaというハッシュタグを付与する機能を実装していきます。

package main

import (
	"fmt"
	"strings"

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

func (p *FirstPlugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
	if strings.Index(post.Message, "https://qiita.com/") == -1 {
		return post, ""
	}

	p.API.LogDebug("Qiita link is detected.")
	post.Message = fmt.Sprintf("%s #Qiita", post.Message)
	post.Hashtags = fmt.Sprintf("%s #Qiita", post.Hashtags)
	return post, ""
}

MessageWillBePostedメソッドを実装することで、ユーザーから投稿されたメッセージが DB に保存される直前に実行される処理を記述できます。 ここでは、投稿内容にhttps://qiita.com/という URL を含む場合に、自動で#Qiitaというハッシュタグを付与するよう実装になっています。

途中でp.API.LogDebugというメソッドを利用してログ出力を行なっています。このメソッドを通じて出力されたログは下記のように システムコンソール > ログ に出力されます。

2018-12-31T21:42:59.988+0900 debug app/plugin_api.go:623 Qiita link is detected. {“plugin_id”: “org.kaakaa.mattermost-plugin-first”}

Gopkg.toml

ここまで書けたら、依存ライブラリを取得するためにdepコマンドを使用します。

serverディレクトリ配下でdep initを実行することで、依存ライブラリの検出・取得を行うことができます。

$ cd server
$ dep init

ビルド

サーバーサイドの実装を含むプラグイン .tar.gzファイルを作成していきます。

まず、plugin.jsonにサーバーサイドのプラグインを認識させるためにserverというセクションを追加します。

{
    "id": "org.kaakaa.mattermost-plugin-first",
    "name": "Mattermost Plugin Sample",
    "description": "このプラグインはQiita記事用のMattermostプラグインです。",
    "version": "0.0.2",
    "server": {
        "executables": {
            "linux-amd64": "server/dist/plugin-linux-amd64",
            "darwin-amd64": "server/dist/plugin-darwin-amd64",
            "windows-amd64": "server/dist/plugin-windows-amd64.exe"
        },
        "executable": ""
    }
}

serverセクションには、.tar.gz ファイル内の Go バイナリの位置を指定します。executableセクションでは各 OS ごとのバイナリを指定することもできます。

plugin.jsonに書かれた構成通りに.tar.gzファイルが作成されるよう、Makefileを下記のように修正します。

PLUGIN_ID ?= org.kaakaa.mattermost-plugin-first
PLUGIN_VERSION ?= 0.0.2
BUNDLE_NAME ?= $(PLUGIN_ID)_$(PLUGIN_VERSION).tar.gz

build:
	mkdir -p server/dist
	cd server && env GOOS=linux GOARCH=amd64 GO build -o dist/plugin-linux-amd64
	cd server && env GOOS=darwin GOARCH=amd64 GO build -o dist/plugin-darwin-amd64
	cd server && env GOOS=windows GOARCH=amd64 GO build -o dist/plugin-windows-amd64.exe

	rm -rf dist/
	mkdir -p dist/$(PLUGIN_ID)
	cp plugin.json dist/$(PLUGIN_ID)/

	mkdir -p dist/$(PLUGIN_ID)/server/dist
	cp -r server/dist/* dist/$(PLUGIN_ID)/server/dist

	cd dist && tar -cvzf $(BUNDLE_NAME) $(PLUGIN_ID)

	@echo plugin built at: dist/$(BUNDLE_NAME)

make buildを実行することでサーバーサイドの実装を含むプラグインファイルが作成されます。

プラグインアップロード・有効化

先ほどと同様、システムコンソール > プラグイン(ベータ版) > 管理 よりプラグインファイルをアップロードし、同画面から有効にするリンクをクリックしてプラグインを有効化します。今度は「このプラグインは起動中です。」というメッセージが表示されると思います。

スクリーンショット 2019-01-01 10.59.34.png

動作確認

plugin.mov.gif

まとめ

以上のように、Mattermost プラグインのサーバーサイドの実装を行うことができます。 今回は触れませんが、サーバーサイドの拡張ポイント(Hooks)は他にも数多くあるのでユースケースに合致する組み合わせを探して見てください。

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

Step 3: フロントエンドプラグインの実装

最後にフロントエンドプラグインの実装について見ていきます。

フロントエンドの拡張ポイントは下記に挙げられています。 https://developers.mattermost.com/extend/plugins/webapp/reference/#registry

今回はregisterChannelHeaderButtonActionを使い、チャンネルヘッダーボタンから投稿を作成するサンプルを作っていきます。

root/
  ├ dist/
  │  └ org.kaakaa.mattermost-plugin-first_0.0.3.tar.gz
  ├ server/
  │  ├ ...
  ├ webapp
  │  ├ src/
  │  │  ├ index.js
  │  │  └ action.js
  │  ├ package.json
  │  └ webpack.config.js
  ├ Makefile
  └ plugin.json

index.js

プラグインの読み込みと実装をindex.jsというファイルに書いていきます。

const React = window.React;
const {Glyphicon} = window.ReactBootstrap;

import {createPluginPost} from './action'

window.registerPlugin("org.kaakaa.mattermost-plugin-first", new Plugin());

class Plugin {
    initialize(registry, store) {
        registry.registerChannelHeaderButtonAction(
            <Glyphicon glyph="pencil" />,
            (channel) => {
                createPluginPost(channel.id)(store.dispatch, store.getState)
            },
            'First Plugin',
            'This is plugin tooltip',
        )
    }
}

フロントエンドプラグインはwindow.registerPluginメソッドにプラグイン ID とプラグインを実装したクラスのインスタンスを与えることで読み込まれます。 プラグイン実装クラス(Plugin)はinitializeメソッドを持ち、このメソッドの第一引数であるregistryが持つメソッドを呼び出すことで機能を追加することができます。

今回はチャンネルヘッダーボタンを追加する部分を実装するため、registry.registerChannelHeaderButtonActionを呼び出しています。

このメソッドの第1引数には、ボタンのアイコンとなる React 要素を指定します。

スクリーンショット 2019-01-01 11.13.12.png

Mattermost プラグインでは React Bootstrap が export されているため、const {Glyphicon} = window.ReactBootstrap;と書くことで React Bootstrap の機能を import することができます。(export されているライブラリ一覧はExported Libraries and Functions)。

第2引数にボタンが押された時の動作を記述します。今回は投稿を作成するcreatePluginPostという処理を actions として実装しました。 第3引数には、複数のプラグインがregisterChannelHeaderButtonActionを実装していた場合に、ドロップダウンメニューとして使われるテキストです。 第4引数にはチャンネルヘッダボタンのツールチップテキストです。

スクリーンショット 2019-01-01 11.14.30.png

action.js

チャンネルヘッダーボタンを押した時に行われる処理をaction.jsに書いています。 Mattermost の Javascript ドライバーであるmattermost-reduxを利用して、簡単なメッセージを投稿する機能を下記のように実装しています。

import {createPost} from 'mattermost-redux/actions/posts';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';

export function createPluginPost(channelId) {
    return async (dispatch, getState) => {
        const state = getState();
        const userId = getCurrentUserId(state)
        const post = {
            channel_id: channelId,
            user_id: userId,
            message: "Post from webapp plugin",
        }
        return await dispatch(createPost(post));
    }
}

ビルド

まず、plugin.jsonにフロントエンドのプラグインを認識させるためにwebappというセクションを追加します。

{
    "id": "org.kaakaa.mattermost-plugin-first",
    "name": "Mattermost Plugin Sample",
    "description": "このプラグインはQiita記事用のMattermostプラグインです。",
    "version": "0.0.3",
    "server": {
        "executables": {
            "linux-amd64": "server/dist/plugin-linux-amd64",
            "darwin-amd64": "server/dist/plugin-darwin-amd64",
            "windows-amd64": "server/dist/plugin-windows-amd64.exe"
        },
        "executable": ""
    },
    "webapp": {
        "bundle_path": "webapp/dist/main.js"
    }
}

実装してきた.jsファイルを、マニフェストファイルで指定したしたwebapp/dist/main.jsにバンドルされるようにwebpack.config.jsを用意します。 (フロントエンドに明るくないので公式のサンプルをベースにしています)

var path = require('path');

module.exports = {
    entry: [
        './src/index.js',
    ],
    resolve: {
        modules: [
            'src',
            'node_modules',
        ],
        extensions: ['*', '.js', '.jsx'],
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env','@babel/preset-react'],
                        plugins: [
                            'transform-class-properties',
                            '@babel/plugin-proposal-object-rest-spread'
                        ],
                    },
                },
            },
        ],
    },
    externals: {
        react: 'React',
        redux: 'Redux',
        'react-redux': 'ReactRedux',
    },
    output: {
        path: path.join(__dirname, '/dist'),
        publicPath: '/',
        filename: 'main.js',
    },
};

下記のようなpackage.jsonを用意し、npm installしてからnpm run buildを実行し、正常にコマンドが完了すれば準備は完了です。

{
  "name": "webapp",
  "version": "0.0.3",
  "description": "Post today's metal from channel header button",
  "main": "src/index.js",
  "scripts": {
    "build": "webpack --mode=production",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "mattermost-redux": "^5.6.2"
  },
  "devDependencies": {
    "@babel/core": "^7.2.2",
    "@babel/plugin-proposal-object-rest-spread": "^7.2.0",
    "@babel/preset-env": "^7.2.3",
    "@babel/preset-react": "^7.0.0",
    "babel-loader": "^8.0.4",
    "babel-plugin-transform-class-properties": "^6.24.1",
    "babel-plugin-transform-object-rest-spread": "^6.26.0",
    "webpack": "^4.28.3",
    "webpack-cli": "^3.1.2"
  }
}

最後にルートディレクトリにある Makefile を下記のように編集し、でmake buildを実行することで、フロントエンドプラグインを含むプラグイン.tar.gzファイルがdistディレクトリ内に作成されるはずです。

PLUGIN_ID ?= org.kaakaa.mattermost-plugin-first
PLUGIN_VERSION ?= 0.0.3
BUNDLE_NAME ?= $(PLUGIN_ID)_$(PLUGIN_VERSION).tar.gz

build:
	mkdir -p server/dist
	cd server && env GOOS=linux GOARCH=amd64 GO build -o dist/plugin-linux-amd64
	cd server && env GOOS=darwin GOARCH=amd64 GO build -o dist/plugin-darwin-amd64
	cd server && env GOOS=windows GOARCH=amd64 GO build -o dist/plugin-windows-amd64.exe

	cd webapp && npm run build;

	rm -rf dist/
	mkdir -p dist/$(PLUGIN_ID)
	cp plugin.json dist/$(PLUGIN_ID)/

	mkdir -p dist/$(PLUGIN_ID)/server/dist
	cp -r server/dist/* dist/$(PLUGIN_ID)/server/dist
	mkdir -p dist/$(PLUGIN_ID)/webapp/dist;
	cp -r webapp/dist/* dist/$(PLUGIN_ID)/webapp/dist/;

	cd dist && tar -cvzf $(BUNDLE_NAME) $(PLUGIN_ID)

	@echo plugin built at: dist/$(BUNDLE_NAME)

プラグインアップロード・有効化

先ほどと同様、システムコンソール > プラグイン(ベータ版) > 管理 よりプラグインファイルをアップロードし、プラグインを有効化します。

動作確認

webapp.mov.gif

まとめ

以上のように、Mattermost プラグインのフロントエンドの実装を行うことができます。 他にも多くの拡張ポイントがあるので、実装方法については公式のサンプルリポジトリの実装を参照してみてください。 https://github.com/mattermost/mattermost-plugin-demo

おわりに

以上が Mattermost プラグインの開発方法になります。

今までは Mattermost を他のシステムと連携する場合、連携機能専用のサーバーを立ち上げなければならず、連携機能が増えるたびにサーバーを運用する手間が増えていましたが、プラグイン機能を使うことで運用の手間を Mattermost インスタンスに一元化することができます。

また、プラグイン機能では今まで以上に細かな動作にまで手を加えることができるようになりました。 例えば、mattermost/mattermost-plugin-antivirusのようにファイルがアップロードされた際に、そのファイルに対してセキュリティのチェックをかけたりできるようになります。

Slack とは違い、オンプレミスでの運用もメインターゲットとなる Mattermost では、プラグイン機能によるインフラ運用コストの削減や、企業特有のポリシーを実装するための細かなカスタマイズ性は非常に大きなメリットとなります。 プラグイン実装にまつわる本体のアップデートの追従などのメンテナンスコストは今後問題となる可能性がありますが、その辺りをどのようにクリアしていくかを含め、開発に協力しながらウォッチしていければと思います。

参考資料

開発者向けドキュメント

このサイトでは、プラグイン開発についてだけでなく、その他の統合機能であるウェブフックやスラッシュコマンドなどの開発方法や、Mattermost へのコントリビュート手順などについてもまとめられています。

サンプルプラグイン

公式で開発されているデモ用のプラグインです。 プラグイン向けに提供されている拡張ポイントの多くを利用しているので、実装する際の参考になると思います。

このリポジトリでは、Official/Unofficial 問わず Mattermost プラグイン開発プロジェクトの GitHub リポジトリがまとめられています。 文書よりも動くコードのほうが分かりやすいという方には参考になると思います。

comments powered by Disqus