Slack RTM Bot Engine: eure/bobo

Takuma Morikawa
Eureka Engineering
Published in
18 min readDec 12, 2019

この記事は 「eureka Advent Calendar 2019」 12日目の記事です。
11日目はBIチームの栗村による「BigQueryとAirflowを活用したDataPlatform運用の10のケース・スタディ」でした。

彼の趣味はバイクです。アメリカンです。実家のバイクで走り出します。

彼は元々Androidエンジニアですが、今年からデータサイエンス的なムーブメントをしています。
でもこの2週間はドロイダーとしてまたKotlinコードを書いているみたいです。
そのくらいAndroidエンジニアが足りません。本当に足りないのでぜひ今年中に入社してください。

さて、みなさまいかがお過ごしでしょうか。
エウレカの森川と申します。

気がつけばもう12月、暑い夏、謎に暑すぎた秋も終わり、寒さが全身を襲い尽くす季節がやってきました。
そう、冬です。これはポケモンGoが辛い季節でもあります。軒並みカーブボールが外れてしまうほどの寒さが遍いてます。

そんなわけで、この記事ではGo言語で作るSlack RTM Botについて記載いたします。

対象者(前提条件)

  • Go言語である程度何か書いたことがある人
  • 既にSlack BotのTokenを取得している人

できるようになること/できるようにならないこと

  • RTM APIを利用したSlack Botに対してGo言語でかんたんに機能追加できるようになります。
  • ❌ RTM以外の方式のSlack Botはつくれません
  • ❌ Go言語に対する深いナレッジやインサイトは得られません

eure/bobo について

というわけで、Slack botつくってますか?
Goだと nlopes/slack 使ったり、GoではないですがBoltを使ったりするのかなと思います。
Goで一からシコシコ作るのでもいいんですが、色んなbot作ってると、毎回似たような処理ばかりでめんどくさいですよね。
そんな甘ちゃんのあなた(わたし)に eure/bobo を紹介させてください。(といってもコード内部ではnlopes/slackを利用させてもらってます)

以下の手順で簡単にオリジナルSlackBotが作れちゃいます。

まずは適当なディレクトリにcloneするか、go get しちゃってください。

# $ go get -u github.com/eure/bobo
$ git clone https://github.com/eure/bobo.git
$ cd bobo

そして、SlackTokenを与えて起動すればサンプルのbotが動きます。

$ make build
$ SLACK_RTM_TOKEN=xoxb-0000... ./bin/bobo
2019/12/10 21:24:06 [INFO] [bobo] pid=[11055]

そして話しかけると…

サンプルのSlackBot

きちんと反応してくれますネ。

まあこんなゴミのような機能しかないゴミBotはゴミかSelf Pleasure以外のナニモノでもないので、もっとCOOLな機能を入れたいですよね。
安心してください、普段からGoを書いているあなたならきっと簡単にできます。

まずはエントリポイントのコードを見てみましょう。

entrypoint

不要なコードを取り除いてシンプルにすると↓みたいになります

entrypoint (simple)

コマンドの作成と追加は↓のようにします
(以下、READMEからほぼコピペ)

echoコマンド

作ったコマンドを追加してみます.

これを起動すると…

echoコマンドが使えるSlackBot (下部2メッセージ)

うまく動きました👾

まあこれもいわゆるTHE・ゴミ機能ですね。

G・O・M・I

ゴミばっか生産している人間というレッテルを貼られる前にそろそろ現実的な機能を入れたいところですが、一旦コマンド周りのコード構成を確認したいと思います。

CommandData と Task

まず、コマンド作成時に引数となっている CommandData ですが、以下のようなフィールドが存在しています。

CommandData構造体 について

コマンド内ではこれらのデータを利用しつつ処理を行い、複数のタスクを生成します。

タスク自体はinterfaceで以下の2つのメソッドが実装されていれば何でも構いません。

Task interface について

例としてSlackの指定チャンネルにメッセージを投稿する replyEngineTask を見てみましょう.

replyEngineTaskについて

NewReplyEngineTask で必要なデータが入り、Run 内で engine.Reply だけが実行されています。

なんとなく分かってきたでしょうか。
最低限必要な内容を説明したので、ここからは実際に何かを作って理解を深めていきます。

例1. 指定画像の顔を合成するコマンド

まずはみんなが好きそうな顔を合成するコマンドを作成します。
自分で処理を書くのは非常にしんどいので外部のAPIを利用します。

ここでは中国Megvii社のFace++というサービスを使います。現在のところ秒間1クエリまでは無料なので気軽に試すことができます。
ただし中国政府の監視カメラにも利用されている企業サービスなので、データの扱いには注意して利用してください。

ここでは解説しませんが、事前にサインアップして、 API KeyAPI Secret を取得しておいてください。発行はシンプルなので迷うことは少ないと思います。
Face++のGo用クライアントとしては evalphobia/go-face-plusplus を使います。

(Face++のMergeFaceの利用例)

さて、これを実際に利用可能なコマンドとして追加していきます。

まずは顔画像を合成する関数を作成します。(↑の例とほぼ同じなので解説は省きます)

mergeFaceImage関数の作成

次にこの関数を利用するコマンドを作成していきます。

MergeCommandの作成

ちょっと長いですが、コードを見ればなんとなく何をしているかわかるかと思います。

// 一旦喋っておく の箇所では作成したタスクをコマンドへ c.Add() せずに、すぐに Run() しています。
これは GenerateFn の内容が先に実行されて、その他のタスクは最後にまとめて実行されるため、すぐに発言しておきたいものがタスクとしてコマンド内に追加されてしまうと以下の順番で実行されてしまいます。

  1. [GenerateFn] 画像マージAPIの実行
  2. [Task] Slackへの発言 「合成中…」
  3. [Task] 合成済み顔画像ファイルのアップロード

本来は 1 と 2 の順番が逆なので、以下のいずれかの形に書き換える必要があります。

  • a) 2をその場でRunする
  • b) 1をタスクとして追加して、結果をどうにかして3へ渡す
  • c) 1と3を同じタスク内に定義・実行する

(このコードではaにしています)

どうせSlackBotなので、数百人でいじることもないだろうし各々書きやすい方法で書いてください。

さて、最後に作成したコマンドをCommandSetへ追加し、Botを起動させてみます。
(注: SlackBotには files.upload の権限が必要です)

# 起動コマンド例
$ FACEPP_API_KEY=xxx \
FACEPP_API_SECRET=yyy \
SLACK_RTM_TOKEN=xoxb-... \
go run ./cmd/main.go
テクノロジーの勢いが感じられる合成結果

うーん、テクノロジーの勢いを感じますね。

もう一つやってみます。

怒ってる...? 喜んでる…?

いい感じに合成されましたっ☆ミ

ここまでは出来レースです。

しかし、サングラスはどうでしょうか…

サングラスもギリギリいける

ギリギリのラインです。初見なら失笑してくれそうだから良しとしましょう。

これでようやくゴミBotから脱却できましたんじゃないでしょうか。

Special Thanks to Yu.Akasaka, Kento.Yamashita, Takeru.Kawashita, Koya Suzuki, Hirokazu Nakamura

例2. AWSの1日の料金を取得するコマンド

例1では(無料で)現代テクノロジーの風を感じることができましたが、まだまだ給料泥棒感は否めません。
得られたものはちょっとした社内の笑いだけで、それ以外はゴミです。

なんら生産性がありません。

年末なので評価面談も近いし、ちゃんと働いているところを見せるためにも、何か人の役に立つ機能を入れたいところです。
そこで次は、みんな大好きAWSの料金を取得するコマンドを作成します。

まずは完成形のイメージです。

頼れる男 KentyKenty

上記画像のように(全て$0.00ですが..)、1日のAWS合計コストと各サービスのコストを列挙してくれるようにしていきます。

ちょっとコードが長い上にAWSの知識がちょこっと必要なので、本当はコードのURLだけ貼って終わりにしたいところではありますが、
会社のアドベントなカレンダーなのである程度説明を加えつつ泣く泣く進めていきます(/_;)

今回は例1とは違い、 command.BasicCommandTemplate を使わずにオリジナルなコマンドテンプレートを作成してみます。

command.BasicCommandTemplate という構造体は CommandTemplate というinterfaceを満たしています。
CommandTemplate を満たすためには以下の5つのメソッドが必要です。

CommandTemplate interface について

この5つのメソッドの内、Exec 以外の4つは固定値を返却するだけで大丈夫です。

今回は @bot awscost でAWSのコストを出力してくれるコマンドを作成することにします。

awscost コマンドの実装(処理は空)

まだ処理は空ですが、これで command.CommandTemplate を満たすことができました。
AWSCostCommand にはServicesというフィールドがありますが、これを使ってコスト表示するAWSサービス名を指定できるようにしてみます。

また一番上の行は interfaceを満たすかどうかコンパイル時に判定できるので、利用するinterfaceが決まっている場合は極力記述するようにしましょう。

var _ command.CommandTemplate = AWSCostCommand{}

(テストコード中に書いてしまうとテストを実行するまで分かりませんが、これなら起動前にエラーが分かります)

ガワができたので、まずは大雑把に必要なロジックを書きます。
コード内に説明を入れながら、処理を順次足していきます。

AWSコスト取得 メインロジックの実装

と大きく4つの処理に分けて実行するようにしました。
順番に処理を作っていきます。

(1) AWS CloudWatchクライアント

まずはAWSのCloudWatch用のクライアント周りの処理を作ります。
「Cost Explorer APIじゃないんかい!」みたいなツッコミもあるかと思いますが、ここは古き良きのCloudWatch経由で取得していきます。

定形処理とRequest/Responseを楽に扱うために evalphobia/aws-sdk-go-wrapper を使用しています。

CloudWatchクライアントの生成と取得

これで(1)の処理は完成です。

(2) 日時のパース

次にコストを計算したい日時を返却する処理を作成します。
(簡単なのでコードだけ載せて終わりでもいいでしょうか…)

対象日時を返却する処理

(3–1) コスト取得: 対象となるサービスリストの取得

一番の肝となるコスト取得の前に、対象となるAWSサービスを返却する処理を追加します。
サービスが指定されていない場合はデフォルトのリストを返却してあげるメソッドを追加してみましょう。

計算対象となるAWSサービス名のリスト

(3–2) コスト取得

では、メインのコスト取得処理になります。

まずはコスト取得・計算周りの全体のロジックから書いていきます。
最初に全コスト合計値を取得し、その後のループ内で各AWSサービスごとのコストを取得します。

コスト取得処理 その1

次に実際にAWS CloudWatch APIから各コストを取得する処理を書きます。

コスト取得処理 その2

ちょっと長いですが、上記の処理でコストデータが取得できるはずです。おそらく…

fetchCosts では指定日時(+指定サービス)のコストデータを取得しています
(ClowdWatchのAPIの話になるので詳細な解説は省略します)

fetchCosts ではCloudWatchの時系列データが返却されるため、 getFirstMaximum を使ってデータの先頭のMaximumを取得しています。
利用していないサービスでは、 そもそも何も返却されない = 時系列データが空になる と思うので、0が返却されます。

(4) コストの整形

最後はテキスト整形処理です。
何も考えずに fmt.Sprintf("%+v", cost) とかしてもいいんですが、なるべく実用的にしたいところです。

ここでは、コストが高い順にソートして出力するようにしてみます。

テキストのをいい感じに整形する処理

最後にソートの処理を書きます。
(通常のソート処理なので、詳細な解説は省略します。。。)

sort.Interfaceを満たすための処理をKVListへ実装

こんな感じで完了です。

コマンド追加と実行

作成したコマンドを追加してボットを起動してみましょう。

awscostコマンドを追加
# 起動コマンド例
$ AWS_ACCESS_KEY_ID=xxx \
AWS_SECRET_ACCESS_KEY=yyy \
SLACK_RTM_TOKEN=xoxb-... \
go run ./cmd/main.go

そして awscost コマンドを実行すると...

無事に表示されたでしょうか?

同じ画像貼るだけなんですが、画像再掲を省略してみました。

例3. GoogleCalendarの予定を取得するコマンド

これはおまけです。

Google Calendar APIを利用するには credentials と oauth token が必要になります。

以下のチュートリアル等を参考にしてなんとか取得してください。。。

https://developers.google.com/calendar/quickstart/go

ここでは当日の予定を教えてくれるコマンドを作ります。

まずは完成形のイメージです。

ぼくもお昼ごはん食べながら輪読会してみたいナア

こんな感じで、単に @bot calendar とした場合は自分のカレンダーから予定を表示し、 @bot calendar @user とした場合は他の人のカレンダーから予定を表示するようにしていきます。

まずは大雑把に必要なロジックを書きます。

calendarコマンド メインロジックの実装

今回も大きく4つの処理に分けて実行するようにしました。
順番に処理を作っていきます。

(1) Google Calendarクライアント

まずはGoogle Calendar用のクライアント周りの処理を作ります。

(こちらも楽に扱うために evalphobia/google-api-go-wrapper を使用しています)

Google Calendarクライアントの生成と取得

これで(1)の処理は完成です。

(credential関連は環境変数で起動時に指定します)

(2) メールアドレスの取得

次に対象者のメールアドレスを取得するようにします。

SlackではユーザーIDからユーザーのメアドを取得することができるのでそうします。

メールアドレスの取得

(3) Google Calendarから予定を取得

ここは追加の実装はいりません。APIをそのまま使えばOKです。

calendarCli.EventList(email, maxEvent) のようにメアドと取得したい予定数だけ入れれば取得できます。

日時を指定したい場合や、その他の細かいオプションを指定したい場合は calendarCli.EventListWithOption(email, option) を使っても良いです。

EventListOption

(4) 予定の整形

見やすくなるようにテキストを整形してあげます。

こんな感じで完了です。(3回目)

コマンド追加と実行

作成したコマンドを追加してボットを起動してみましょう。(3回目)

# 起動コマンド例
$ GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json \
GOOGLE_API_OAUTH_TOKEN_FILE=/path/to/oauth_token.json \
SLACK_RTM_TOKEN=xoxb-... \
go run ./cmd/main.go
(再掲) ぼくもジムにいきたいよ

これで気になるあのコの予定も確認し放題になりました!

ここまでできればコマンド追加手順はバッチリですね。
(色々と変なものを追加しすぎてクビにならないように気をつけてください)

なお、少し手直ししたものは以下のレポジトリに掲載しており、謎コマンドを随時追加していく予定です。

おわり

現在エウレカではBotは募集していませんが、やっぱりエンジニアを募集しています。

サーバーサイドエンジニアは決済とか暗号化とか検索アルゴリズムとか辛いことはたくさんありますが、得られるものも大きいので興味がある方はぜひ応募してください :)

お待ちしております♥❤❥٩(♡ε♡ )۶❥❤♥

--

--

Takuma Morikawa
Eureka Engineering

Software Engineer at eureka, Inc. | Gopher, Payment, Search algorithm, Love❤maker | Whole lotta love ❤