AWS
S3
lambda
Mattermost
CloudWatchEvents

Mattermostにランチボットをサーバレスで作ってみた話

はじめに

 会社のOrganizationsに所属しているアカウントで、業務外の内容を投稿することに若干の抵抗を感じているのですが、Mattermostでランチボットを作ってみた話を書いてみたいと思います。
 チームメンバと毎日一緒にランチに行っているのですが、毎日どこに行くのか考えるのが面倒だったのと、Qiitaを調べてみてもSlackにランチボットを作ってみた投稿は見つかるのですが、Mattermostにランチボットを作ってみた投稿はなかったので、試しにやってみましたという内容を書いてみたいと思います。会社のルールでSlackは使えないけど、Slackでやっているようなキラキラしたことをやってみたい人達の参考になれば良いかなと思っています。

Mattermostって何?

 ※Mattermostを知っている人は読み飛ばしてください。

 一言で言うと「Slackクローン」です。OSSとして公開されていることから、社内ルールでSlackが使えないような環境において、Slack代替としてオンプレにMattermostを立てるケースが多いと思います。
 また、他システム連携(Integrations)の機能も用意されており、「Incoming Webhook」「Outgoing Webhook」「Slash Command」の連携機能が提供されています。弊社内の開発環境でもIncoming Webhookを利用したGitlabとの連携を行っており、Issueの新規登録やクローズしたタイミングなどに、Mattermostに通知されるようにしています。

2018-05-07_153204.png

システム構成

 タイトルに記載した「サーバレスで作ってみた」部分について、簡単に説明したいと思います。
 まず、このランチボットの利用シナリオは、以下の通りです。

  • 毎日11:30にオススメの店が3つボットから投稿される
  • Emojiリアクションを使って、行きたいお店をチームメンバに選択してもらう
  • 投票結果の一番多いお店にチームメンバで出発する

 この「毎日11:30にオススメの店が3つボットから投稿される」を実現するため、当初は適当なLinuxサーバからcronを使ってMattermostのIncoming Webhookをcurlコマンドで叩いてもらおうと思ったのですが、せっかくなのでイマドキにAWS Lambdaを使ってサーバレスで作ってみました。
 簡単なシステム構成図は以下の通りです。Amazon CloudWatch Eventsがcron式を記述できるので、これをトリガにAWS Lambdaをキックして、Amazon S3に格納されたJSONファイルからランダムに3つ候補を取得し、Incoming WebhookからMattermostに投稿しています。(上記の説明ではなんのこっちゃ分からないと思うので、次章で少し詳しく説明したいと思います)

スライド1.PNG

サーバレスで作ってみた話

Incoming Webhook

 まずはMattermost側にボットからの投稿を受け付けるための口を用意します。
 「Integrations」->「Incoming Webhook」から「Add Incoming Webhook」ボタンを押下してください。以下の値を入力し「Save」ボタンを押下します。

  • Title: 任意の名前
  • Channel: ボットからの投稿を表示したいチャネル名
  • Username: ボットが投稿された際に表示されるユーザ名
  • Profile Picture: ボットが投稿された際に表示されるアイコン画像のURL

2018-05-07_153604.png

 これでボットからの投稿を受け付けるための準備ができたのですが、表示されたURLはAWS Lambdaから叩くときに利用しますので、適当な場所(テキストファイルなど)に控えておいてください。

Amazon S3

 次にランダムに表示されるお店の情報を用意します。ここでは、以下のようなJSONファイルをAmazon S3に格納しています。

ybp_lunch_list.json
{
    "far": [
        "とんとん(餃子)",
        "光家(ラーメン)",
        "すぱいす工房Sido(カレー)",
        "ほどがや千成鮨(寿司)",
        "佐久良家(蕎麦)",
        "立花鮨(寿司)",
        "ばらの木(喫茶)",
        "エベレストキッチン(カレー)"
    ],
    "near": [
        "やきとりの拓(からあげ)",
        "スパイスプラザ(カレー)",
        "再会楼(中華)",
        "スパイシードラゴン(肉)",
        "あまんじゃく(お好み焼き)",
        "きたみ(とんかつ)",
        "田中家(そば)",
        "和醸良酒(和食)",
        "のんで~こ(納豆)",
        "HAWAIIAN DINING BAR MAHALOHA(ハワイ料理)",
        "グジェール(洋食)",
        "バーガーズニューヨーク(ハンバーガー)",
        "かんぎ(寿司)",
        "ハナウタ(タパスダイニング)",
        "こころ(とんかつ)"
    ]
}

 このJSONファイルでは、near・farというお店の分類をしていますが、これらは精神的な距離を表しており、near:「良く行くお店」、far:「元気な時にたまに行くお店」に分類しています。

AWS Lambda

 次に、Amazon S3に格納されたJSONファイルからお店の情報を取得し、3つのお店をランダムに選択し、MattermostのIncoming Webhookを呼び出す、AWS Lambdaの関数を作成します。なんとなく良い感じにお店を選択しているくれるように、near:「良く行くお店」から2つ、far:「元気な時にたまに行くお店」から1つ、ランダムに選択するようにしています。また、ランタイムには「Node.js 4.3」を利用しています。

index.js
console.log('Loading function');

var aws = require('aws-sdk');
var s3 = new aws.S3();
aws.config.region = 'ap-northeast-1';

const http = require('http');
const url = require('url');

exports.handler = function(event, context) {

    const slack_url = event['webhook'];
    const slack_req_opts = url.parse(slack_url);
    slack_req_opts.method = 'POST';
    slack_req_opts.headers = {'Content-Type': 'application/json'};

    var bucket = event['bucket'];
    var key = event['key'];

    var params = {
        Bucket: bucket,
        Key: key
    };

    s3.getObject(params, function(err, data) {
        if (err) {
            console.log(err, err.stack);
        } else {
            var json = JSON.parse(data.Body.toString());

            var near_shops = json['near'];
            var far_shops = json['far'];

            var a = near_shops[Math.floor(Math.random() * near_shops.length)];
            var b = near_shops[Math.floor(Math.random() * near_shops.length)];
            if (b == a){
                b = near_shops[Math.floor(Math.random() * near_shops.length)];
            }
            var c = far_shops[Math.floor(Math.random() * far_shops.length)];

            var candidate_message = [
                "そろそろランチにしませんか?",
                "ランチの時間ですよ!",
                "今日のおすすめはこちらです。",
                "お腹すきましたね。",
                "お昼です!",
                "お昼の時間です!",
                "ご飯行きましょう!!",
            ];

            var message = candidate_message[Math.floor(Math.random() * candidate_message.length)];

            message += "\n\n:one: " + a + "\n:two: " + b + "\n:three: " + c;
            message += "\n\n行きたいところに :one: :two: :three: のリアクションをしてください。";

            var req = http.request(slack_req_opts, function (res) {
                if (res.statusCode === 200) {
                    context.succeed('posted to mattermost');
                } else {
                    context.fail('status code: ' + res.statusCode);
                }
            });

            req.on('error', function(e) {
                console.log('problem with request: ' + e.message);
                context.fail(e.message);
            });

            req.write(JSON.stringify({text: message}));
            req.end();

        }
    });

};

 詳細な説明は後述しますが、MattermostのIncoming WebhookのURLやS3バケット名などは、event連想配列としてAmazon CloudWatch Eventsから渡しています。

Amazon CloudWatch Events

 最後に、毎日11:30に投稿するためのcron式の設定と、AWS Lambdaに渡すevent連想配列の設定を、Amazon CloudWatch Eventsに実施します。
 AWS Lambdaのコンソールのトリガーの追加から「CloudWatch Events」を選択し、以下の値を入力したトリガーを作成してください。

  • イベントソース: スケジュール
    • Cron式: 30 2 ? * 2-6 *
  • ターゲット: Lambda関数
    • 入力の設定(定数(JSONテキスト)): {"webhook": "※※Incoming Webhookを作成した際に表示されるURL※※", "bucket": "※※JSONファイルを格納したS3バケット名※※", "key": "ybp_lunch_list.json"}

 ※cron式に表示される時刻はUTCであるため、日本時間の場合には投稿したい日時に対して-9時間する必要があります。
 ※JSONテキストには、Incoming WebhookのURL、JSONファイルを格納したS3バケット名、JSONファイル名を記載しています。これらの値は、event連想配列としてAWS Lambdaの関数から読まれます。

ランチボットを実行した結果

 投稿結果は以下の通りです。利用シナリオに記載した通り、行きたいお店のEmojiリアクションをしてもらえれば簡易なアンケートもできて、今日のランチに迷うこともなくなるはずです。

2018-05-07_154002.png

 ランチボットが投稿する機能までは比較的簡単にアーキテクチャを考えることができたのですが、チームメンバにどうやって行きたい店を投票してもらうかは、個人的には少し悩みました。Emojiリアクション機能を使えば良いというのは個人的はブレイクスルーで、このアイデアに気付いたときに一気にランチボットを仕上げることができました。

まとめ

 本記事では、Mattermostにランチボットをサーバレスで作ってみた話を書いてみました。
 最初に作ったボットなので、cronで定期的に実行するだけの非常に簡単なものだったのですが、毎日運用していく中でお店のリストを更新したい欲求が出てきたので、現在ではSlash Command機能とAmazon API Gatewayを使って、JSONファイルのお店の追加や削除もできるようにしています。また、新しいお店を発見するためにyelpのAPIを使って、週一でJSONファイル内のリストを更新する運用も行っています。これらの内容だけでも、それぞれQiita記事を書けそうな気がするので、時間ができたときにまた投稿してみたいと思います。