Mattermostプラグインの作り方
(追記 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 の開発知識がある前提で書いています)
この記事で紹介しているコードは、下記のリポジトリで公開しています。
(Mattermost v5.6 を対象に動作確認を行なっています)Step 0: Mattermost サーバーの設定
Mattermost は、デフォルトではシステムコンソールからのプラグインアップロード機能が無効化されています。この機能を有効にするにはconfig.json
を編集する必要があります。
...
},
"PluginSettings": {
"Enable": true,
"EnableUploads": true,
"Directory": "./plugins",
...
Mattermost 設定ファイル内のPluginSettings
にあるEnable
とEnableUploads
をtrue
にし、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、名前、バージョンや実行バイナリのパスなどのメタ情報を記述するファイルです。
マニフェストファイル記述できる内容については、下記のリファレンスを参照してください。
まず、下記のようにマニフェストファイルを作成します。
{
"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 の システムコンソール > プラグイン(ベータ版) > 管理画面からプラグインをアップロードします。
ファイルを選択する ボタンを押し、先ほど作成したdist/org.kaakaa.mattermost-plugin-first_0.0.1.tar.gz
を選択し、アップロードボタンを押すことでプラグインがアップロードされます。
プラグインが正常にアップロードされるとインストール済みプラグインに表示されます。
アップロードされたプラグインの有効にするボタンを押すことでプラグインを有効にすることができますが、現在はプラグインの実装が行われていないため、エラーメッセージ「このプラグインを起動できませんでした。エラーに関するシステムログを確認してください。」が表示されるはずです。
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 を実装する必要があります。
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
を実行することでサーバーサイドの実装を含むプラグインファイルが作成されます。
プラグインアップロード・有効化
先ほどと同様、システムコンソール > プラグイン(ベータ版) > 管理 よりプラグインファイルをアップロードし、同画面から有効にするリンクをクリックしてプラグインを有効化します。今度は「このプラグインは起動中です。」というメッセージが表示されると思います。
動作確認
まとめ
以上のように、Mattermost プラグインのサーバーサイドの実装を行うことができます。
今回は触れませんが、サーバーサイドの拡張ポイント(Hooks)は他にも数多くあるのでユースケースに合致する組み合わせを探して見てください。
-
OnActivate / OnDeactivate
- プラグインを有効化/無効化した時に呼ばれるメソッド
- このメソッド内でRegisterCommand/UnregisterCommandの API を実行し、スラッシュコマンドを追加するようなプラグインで利用することができます (
ExecutedCommand
の Hooks も実装する必要がある)
-
ExecutedCommand
- プラグインを通じて追加されたコマンドが実行された時に呼ばれるメソッド
-
ServeHTTP
- プラグイン専用のエンドポイントを追加することができます
- 追加されたエンドポイントのルートは
${MATTERMOST_SITEURL}/plugins/${PLUGIN_ID}/
になります
-
OnConfigurationChange
- プラグインの設定が変更された時に呼び出されるメソッドです
- マニフェストファイルにsetting_schemaセクションを追加することで、プラグインの設定画面が出現します
Step 3: フロントエンドプラグインの実装
最後にフロントエンドプラグインの実装について見ていきます。
フロントエンドの拡張ポイントは下記に挙げられています。
今回は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 要素を指定します。
Mattermost プラグインでは React Bootstrap が export されているため、const {Glyphicon} = window.ReactBootstrap;
と書くことで React Bootstrap の機能を import することができます。(export されているライブラリ一覧はExported Libraries and Functions)。
第2引数にボタンが押された時の動作を記述します。今回は投稿を作成するcreatePluginPost
という処理を actions として実装しました。
第3引数には、複数のプラグインがregisterChannelHeaderButtonAction
を実装していた場合に、ドロップダウンメニューとして使われるテキストです。
第4引数にはチャンネルヘッダボタンのツールチップテキストです。
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)
プラグインアップロード・有効化
先ほどと同様、システムコンソール > プラグイン(ベータ版) > 管理 よりプラグインファイルをアップロードし、プラグインを有効化します。
動作確認
まとめ
以上のように、Mattermost プラグインのフロントエンドの実装を行うことができます。
他にも多くの拡張ポイントがあるので、実装方法については公式のサンプルリポジトリの実装を参照してみてください。
おわりに
以上が Mattermost プラグインの開発方法になります。
今までは Mattermost を他のシステムと連携する場合、連携機能専用のサーバーを立ち上げなければならず、連携機能が増えるたびにサーバーを運用する手間が増えていましたが、プラグイン機能を使うことで運用の手間を Mattermost インスタンスに一元化することができます。
また、プラグイン機能では今まで以上に細かな動作にまで手を加えることができるようになりました。
例えば、mattermost/mattermost-plugin-antivirusのようにファイルがアップロードされた際に、そのファイルに対してセキュリティのチェックをかけたりできるようになります。
Slack とは違い、オンプレミスでの運用もメインターゲットとなる Mattermost では、プラグイン機能によるインフラ運用コストの削減や、企業特有のポリシーを実装するための細かなカスタマイズ性は非常に大きなメリットとなります。
プラグイン実装にまつわる本体のアップデートの追従などのメンテナンスコストは今後問題となる可能性がありますが、その辺りをどのようにクリアしていくかを含め、開発に協力しながらウォッチしていければと思います。
参考資料
開発者向けドキュメント
このサイトでは、プラグイン開発についてだけでなく、その他の統合機能であるウェブフックやスラッシュコマンドなどの開発方法や、Mattermost へのコントリビュート手順などについてもまとめられています。
サンプルプラグイン
公式で開発されているデモ用のプラグインです。
プラグイン向けに提供されている拡張ポイントの多くを利用しているので、実装する際の参考になると思います。
このリポジトリでは、Official/Unofficial 問わず Mattermost プラグイン開発プロジェクトの GitHub リポジトリがまとめられています。
文書よりも動くコードのほうが分かりやすいという方には参考になると思います。
Discussion