OTOBANK Engineering Blog

オトバンクはコンテンツが大好きなエンジニアを募集しています!

TrelloとHubotの美味しい連携 ~ Hubotにランチを決めさせてみよう

こんにちは。麦芽系エンジニアでおなじみasmzです。そろそろビールが美味しい季節になってきましたね。ちなみにうちにはこないだコストコで大量に金麦が補充されてとても喜んでいます。(奥さんに気づかれないペースでしれっと消費している)

Trelloで会社周辺のランチ情報管理

さて皆さん、「Trello」ってご存知でしょうか?

「Trello」とは、いわゆる「カンバン方式」でタスク管理を行うことができるWebサービスです。

このように各所で取り上げられており、タスク管理ツールとして近年注目されつつあります。

オトバンクでも昨年からTrelloを導入し、サービスやプロジェクト毎にboardを用意してタスク管理に利用しています。またcardの登録、修正内容はSlackに流れるので、タスクの進捗状況の「見える化」にも繋がっています。

まぁここまでは他の会社でも割とやっているところは多いと思うんですが、オトバンクではさらに業務以外にも以下のように一風変わった使い方をしているboardがあります。

このboardには会社周辺(本郷~御茶ノ水エリア)で発掘したランチ営業情報や料理の写真などが有志によって適宜登録され、ランチのお店を探すときに使われます。

ちなみにcardのAttachmentsに料理写真が追加されると、連動するSlackのランチ用チャンネルにその料理写真が垂れ流されるという、とても恐ろしい飯テロboardでもあります。

ランチ選びの問題点

このランチ管理boardの内容が増えてくるにつれ、こんな声が聞こえて来た気がしなくもないです。(この話はフィクションが含まれています)

オトバンク女子A「お腹空いたー」
オトバンク女子B「今日のランチどこ行こうかー?」
オトバンク女子C「xxxはこないだ行ったしー」


オトバンク女子A「どうするー?」
オトバンク女子B「どうしよっかー?」
オトバンク女子C「どうしようねー?」

(※繰り返し)

むむむ、これではせっかくランチ情報がTrelloで管理されているというのに、お店探しに十分活用されているとは言い難い。

問題点は、たくさんランチ情報は管理・蓄積されていても、その中から今日どこに行くか誰か決めてくれないと、結局悩み続けてしまって決まらないことです。その結果貴重なランチ時間が削られてしまうのは勿体無いので、迷いなくサクッとお店選べるといいですよね。

そこで(またつい勢い余って)「ランチのお店誰も決めてくれないなら、otobot(弊社のHubot)に決めて貰えばいいじゃない」と、悩めるオトバンク女子を救うべく、いち麦芽系エンジニアが立ち上がったのです。頑張れ麦芽系!頑張れotobot!

Hubotランチ選び方針

私は昔からノッてくると「話が長い」とよく言われます。特に飲み会でいい感じに酔っ払ってきた時間帯とかはアレですよね。で、このブログもいつも前置きが長すぎてなかなか本題に入れないことが多いので、そろそろ本題に入ります。

Hubotに適当に選ばせるなら、スクリプト内に幾つかランチ情報をベタ書きして、ランダムでどれか一つを返却させるだけでも出来なくはないのですが、上述の通りせっかくTrelloにランチ情報が日々蓄積されており今回はそれを活用したいので、以下の様な感じでTrelloとHubotを連動させます。

  1. Trelloのランチ用boardに、前もってある程度のランチ情報をcard登録しておく
  2. HubotからTrelloAPIを実行し、ランチ用boardのcard取得
  3. 取得したcardのうち営業時間中のcardのみに絞り込み、その中からランダムで1つ取得してタイムラインに出力

なお、cardのAttachmentsに料理写真が添付されている場合は、自動でそのURLも出力するようにします。otobotはSlack上で動作しているので、写真URLは自動的に画像展開(飯テロ)される仕組みです。ふふふ。

Trelloランチ用board準備

データの取得元となるboardは、スクリプト処理が出来るよう一定のルールを設定しておく必要があります。今回は最低限以下のルールを設定しました。

  • 必須ルール
  • ランチ専用として1つのboardを用意
  • card1つに対して1ランチ情報を登録
  • 任意ルール
  • cardのDescription欄に規定フォーマットでランチタイム情報を記載
  • 料理の写真などはcardのAttachmentsに登録(複数可、ただしSlack自動展開させるには2MB以下のファイルサイズである必要あり)
  • 各cardには「和食」「カレー」など料理ジャンルのlabelを付与(候補の絞り込みに使用)

cardのDescription欄には、以下のフォーマットでランチタイムを記載するようにしています。

open HH:MM
close HH:MM
...(その他自由記入)

後述しますが、これはそのお店のランチ営業時間を調べる際に使用します。ただランチタイム時間が不明(未調査)の場合を考慮して、ない場合でもエラーにはしないようスクリプト側で処理します。

Trello API Key、API Token、ランチ用boardのIDを取得

HubotからTrelloのcard内容を取得するためにはAPIを利用します。APIの実行には事前にKeyとTokenを取得しておく必要がありますが、この取得の仕方が結構判りづらかったので、自分のメモも兼ねて手順を記載しておきます。

なお、基本的にのboardはprivate設定で会社内のみで使用していることが多いかと思うので、ここではその前提で記載します。

API Keyの取得

Trelloにログインした状態でここにアクセスします。 https://trello.com/1/appKey/generate

ここのKey:がAPI Keyになりますので、記載の内容をメモっておいて下さい。

API Tokenの取得

以下のURLへアクセスします。

https://trello.com/1/authorize?key=<api_key(上記でメモったやつ)>&name=hubot-trello-lunch&expiration=never&response_type=token&scope=read

name=hubot-trello-lunchは連携アプリ名、expiration=neverは無期限、scope=readはデータの読み取り権限のみ、という指定になります。必要に応じて内容は読み替えて下さい。

なお今回はやりませんが、Hubotから新規card作成とかしたい場合はここでscope=read,writeとか指定できます。

アクセスするとこんな画面が出ます。

連携アプリの権限など記載されているので、問題なければ「Allow」をクリックします。

クリックすると表示される以下のページで、伏せ字にしているところがAPI Tokenになりますので、こちらもメモっておいて下さい。

Board ID の取得

ここまでのKeyとTokenでAPI自体は実行可能です。ただし今回は「ランチ専用のboard」に対してHubotスクリプト処理を行うため、そのboardのIDを事前に取得しておきます。

以下の様な感じで自分が所属している組織のboard一覧をAPIで取得できます。

$ curl -s -G "https://trello.com/1/members/me/boards?key=<api_key>&token=<api_token>&fields=name" | jq "."
[
  ...
  {
    "name": "OTO Lunch",
    "id": "zzzzz"
  },
  ...
]

fields=nameはTrelloのAPIで用意されているフィルタリングで、いろんな情報のうち名前とIDだけ絞って表示してくれます。また、jqコマンドは前回のエントリで導入したやつで、見た目を整形したかっただけです。

ここで表示されたidがboardのIDとなりますので、これもメモっておきましょう。

今回作成したHubotスクリプト

今回作成したHubotスクリプトはこちらに置いてあります。実際はこれをHubotのscriptディレクトリに放り込んで動作させています。

trello-lunch.coffee

  • hubot lunch me … 営業時間中の全ランチ情報からランダムで一つ回答
  • hubot lunch me <label> … 営業時間中で、かつ指定した<label>がついてるランチ情報からランダムで一つ回答

例によってここでは抜粋して解説しますので、詳細はソースを直接ご参照ください。

node-trelloの使用

今回TrelloAPIを実行するに当たり、既にNodeラッパーが存在しているのでこちらを利用することにしています。

node-trello

こちらのラッパーは、「API Key」「API Token」をインスタンス生成時に渡してあげるだけで、その後API実行の際にTrelloとの通信をよろしくやってくれます。

Trello = require 'node-trello'
...
trello = new Trello process.env.HUBOT_LUNCH_TRELLO_KEY, process.env.HUBOT_LUNCH_TRELLO_TOKEN

今回メモっておいたKey、Tokenを予め環境変数に設定しておき、Hubot実行時にそれぞれ環境変数から取得して、インスタンス生成するようにしています。

Trelloのboardより全card取得

以下の様な感じで、ランチ用boardから全cardを取得します。取得APIにはBoard IDが必要となりますので、ここでもメモっておいたBoard IDを環境変数経由で渡すようにしています。

# Get trello cards
trello.get "/1/boards/#{process.env.HUBOT_LUNCH_TRELLO_BOARD}", {cards: "open"}, (err, data) ->
  if err
    msg.send "あ、今ちょっとTrelloエラー"
    return

APIが正常に終了すると、card情報が配列でdataに詰めこまれます。

なお、ここでdataに入るcard情報は以下の様な内容の配列となっています。(項目は抜粋)

...
{
  "id": "xxxxxxxxxxxxx",
  "badges": {
    ...
    "attachments": 2,
  },
  "dateLastActivity": "2015-05-08T09:13:24.539Z",
  "desc": "open 11:00\nclose 15:00\nうどんのお店",
  ...
  "labels": [
    {
      "id": "xxxxxxxxxxxxx",
      "idBoard": "xxxxxxxxxxxxx",
      "name": "うどん",
      ...
    }
  ],
  ...
  "name": "甚八",
  "shortUrl": "https://trello.com/c/qwerty",
  ...
},
...

descがcardのDescription欄、labelsがそのcardに設定されているラベル内容です。この辺の項目はスクリプト処理で使用することになります。

現在営業中のcardのみに絞り込み

前述した通り、cardのDescription欄にはランチタイムの営業時間を設定するルールとしていますので、その情報を元にその時営業中のランチ情報のみに絞り込みます。

こちらは関数として実装しました。

# Select cards by open now
selectCardsByOpenNow = (cards) ->
  selectedCards = []
  for card in cards
    open = card.desc.match(/open\s(\d+):(\d+)/)
    opentime = if open then ("0" + open[1]).slice(-2) + ("0" + open[2]).slice(-2) else "0000"
    
    close = card.desc.match(/close\s(\d+):(\d+)/)
    closetime= if close then ("0" + close[1]).slice(-2) + ("0" + close[2]).slice(-2) else "2359"
    
    now = new Date
    nowtime = ("0" + now.getHours()).slice(-2) + ("0" + now.getMinutes()).slice(-2)
    
    selectedCards.push(card) if opentime <= nowtime && nowtime < closetime
  return selectedCards

card.descのテキストから正規表現でopen HH:MMclose HH:MMの文字列を引っ掛けて開始時間、終了時間を取得し、今の時間が営業時間内のもののみ新たな配列に詰め替えて返却しています。

まぁ正規表現なので、ここのフォーマットはルールさえ決めてしまえば割と何でもいいんですが、一応Description欄は人の目に触れるので、そのまま表示されても違和感ないものにしています。あとはお好みで。

なおこれと同じように、ラベル指定時はそのラベルでの絞り込み処理も別途入れています。

回答候補よりランダムで1つのcardを選択

絞り込んだ結果残ったcardが回答候補となりますが、ここで複数回答してしまっては結局オトバンク女子を悩ませるだけなので、最終的に1つに絞ります。

これは以下の通り、単純にランダムで1つ選んでいます。

card = msg.random cards

Attachments(料理写真)の取得

1つに絞られたcardに付属するAttachmentsをAPIで取得します。

# Get attachments
trello.get "/1/cards/#{card.id}/attachments", {cards: "open"}, (err, attachments) ->
  if err
    msg.send "あ、今ちょっとTrelloエラー"
    return
  imageUrl = if attachments.length > 0 then (msg.random attachments).url else ""

もしAttachmentsがあれば、attachmentsに配列でURLが返却されるので、ここでもまたランダムで1つ選択しています。

回答をタイムラインに出力

ここまでで習得したランチ情報、写真URLを最終的にタイムラインに返却します。

answer = "こことかどうかな〜?"
answer += "\n#{card.name} - #{card.shortUrl}"
answer += "\n#{card.desc}" if card.desc
answer += "\n#{imageUrl}" if imageUrl
msg.send answer

これが一連のスクリプト処理となります。node-trelloを使用しているので、API実行時のHTTP通信周りを一切書かなくて済むのは良いことですね!

実行結果

このHubotが稼働しているSlackで動かしてみると、こんな感じになります。

こ、これはうまそうなやつやーー。

やってみて分かったんですが、何かこれは料理写真の品質次第で実際に行くかどうか左右されそうな気がしないでもないですね。

利用者の喜びの声

このHubotスクリプト導入後、利用者から続々と喜びの声が届いております。

最後の人だけ何かタイムスリップしてるのとか、いい大人は気にしちゃいけません。


以上がTrelloとHubotの美味しい連携でした。似たような作り方で他にもいろんな連携の仕方ありそうな気がするので、まぁ今回のこれはTrello☓Hubot連携例の一つとして誰かのご参考になれば幸いです。

なお、今回のHubotスクリプトはだいぶTrelloの作りに依存する部分が多くあまり汎用的ではないのでnpm公開とかはしていないですが、依存ライブラリのインストールとか楽にするために気が向いたら用意しておきます。

まとめ

そうそう、ちょっと話だけでも聞いてみたい!という人は、↓ここに「ランチをご馳走します」って書いてあるので、弊社の豊富なランチ情報からHubotが厳選したランチに連れてってもらえますよ!

ちなみにここまで書いておいて何ですが、自分は少ないお小遣いでなんとかやりくりする必要あるので、奥さんのお弁当派です。(そもそもランチに出かけてない)