0

HubotでYoのwebhookを受信する

Yoは最近はダッシュボードからbotアカウントを作れるようになっていて、俺もいくつか作って使っている。

botがYoを受信するとあらかじめ指定しておいたコールバックURLにHTTP-GETでwebhookしてくれる。

https://自分のhubot.herokuapp.com/yo/webhook?token=kazudon とか指定しておくといい。GETのパラメータでusername, user_ip, tokenが来る。


Hubotで受信するとしたらこんな感じ。Hubotにはexpressが内蔵されていてrobot.routerで使える。
環境変数YO_WEBHOOK_TOKENを設定する必要がある。

本当にYoのサーバーからのリクエストかを確かめるのにコールバックURLにtokenを含めるのと、HTTPSを使う。
誰でもYoからプログラム実行できても困るので、許可するYoユーザーをあらかじめ持っておく。

# Description:
# YoのWebhookを受信して何かする
#
# Author:
# @shokai

module.exports = (robot) ->

unless process.env.YO_WEBHOOK_TOKEN
robot.logger.error 'ENV var "YO_WEBHOOK_TOKEN" is missing'
return

yo_users = ['SHOKAI', 'user1', 'user2'] # Yo許可するユーザー

robot.router.get '/yo/webhook', (req, res) ->
ip = req.query.user_ip

unless who = req.query.username
return res.status(400).end 'invalid request'
if yo_users.indexOf(who) < 0
res.status(400).end "bad user: #{who}"
robot.send {room: "#general"}, "不正なユーザー #{who}(#{ip})がYoしてきました"
return
if req.query.token isnt process.env.YO_WEBHOOK_TOKEN
res.status(400).end "bad token"
robot.send {room: "#general"}, "#{who}(#{ip})が不正なtoken#{req.query.token}でYoしてきました"
return

res.end 'ok'
robot.send {room: "#general"}, "#{who}(#{ip})がYoしてきました"
## 〜〜ここで何か処理〜〜 ##

0

slackでふぁぼったのをhubotで通知する

hubot-slackアダプタ v3の中で使われているnode-slack-clientを見ていたら、slackで発言に星を付けた時に通知が来ていたのでそれを別のroomに流すようにしてみた。

slackには自分のstarを見る画面はあっても他人のstarを見れる画面がない。API使ってstars.listが取れるけど、starまとめ的なページを作るにはユーザの数だけ定期的にクロールしなきゃだめなのかー・・・と思ってたけど、hubotにサーバープッシュされて来ていた。



starsというroomをあらかじめ作って、hubotを/inviteしておく必要ある
https://gist.github.com/shokai/e52dd7fdd5d2592878b0

# Description:
# notify "star_added" event for slack.com
#
# Author:
# @shokai

debug = require('debug')('hubot:slack-star')

module.exports = (robot) ->

robot.adapter.client?.on? 'raw_message', (msg) ->
return unless msg.type is 'star_added'
debug msg
return unless msg.item.message.permalink
user = robot.adapter.client.getUserByID msg.user
text = ":star: @#{user.name} added star #{msg.item.message.permalink}"
debug text
robot.send {room: 'stars'}, text
raw_messageイベントは、node-slack-clientがサーバーからwebsocketでJSONを受信してparseしてすぐemitされる。
これは通常のSlackのAPI(HTTPで使うやつ)ではなくhubot用のwebsocketので、一方通行に受信してるだけなので、こっちからslackに送信できるのはbotの発言命令だけのようだ。hubotにstarつけさせたり、発言を削除したりはできない。

getUserByIDはnode-slack-client内部でcacheされてるので呼び出しまくっても問題ない。

もしstar通知が多くて邪魔になってきたら、3人以上starした発言、とかで通知するroom作ったりしてみようかな

0

hubotのbrainが爆発した

brainのmergeが内容もタイミングもヤバイという話を読んでたら、ちょうどタイミングよくbrainが爆発した。


redis-brainに保存する度にエラーがでて、保存失敗する。hubotのbrainはオンメモリにキャッシュされているので、一見保存できてて動いているんだけどプロセス再起動するとデータ巻き戻ってる、という事態に見舞われた。

node_redis: no callback to send error: OOM command not allowed when used memory > 'maxmemory'.
[Fri Jan 09 2015 16:12:33 GMT+0900 (JST)] ERROR Error: OOM command not allowed when used memory > 'maxmemory'.
at ReplyParser. (/app/node_modules/hubot-scripts/node_modules/redis/index.js:279:27)
at ReplyParser.emit (events.js:95:17)
at ReplyParser.send_error (/app/node_modules/hubot-scripts/node_modules/redis/lib/parser/javascript.js:296:10)
at ReplyParser.execute (/app/node_modules/hubot-scripts/node_modules/redis/lib/parser/javascript.js:181:22)
at RedisClient.on_data (/app/node_modules/hubot-scripts/node_modules/redis/index.js:504:27)
at Socket. (/app/node_modules/hubot-scripts/node_modules/redis/index.js:82:14)
at Socket.emit (events.js:95:17)
at Socket. (_stream_readable.js:765:14)
at Socket.emit (events.js:92:17)
at emitReadable_ (_stream_readable.js:427:10)
at emitReadable (_stream_readable.js:423:5)
at readableAddChunk (_stream_readable.js:166:9)
at Socket.Readable.push (_stream_readable.js:128:10)
at TCP.onread (net.js:529:21)


原因

RedisToGoのサーバー側のmaxmemoryを超えた大きさのデータを書き込みにいっているのが悪いらしい。
nanoプランを使っているんだけど、その時の使用量はちょうど31%(約1.5MB)になっていた。

研究室のwikiの更新差分を通知するためにページの本文を比較的たくさん保存していて、そのせいでだんだん容量増えていたんだけどそれでもまだたった1.5MBしか保存してない。それでも書き込みエラーがでまくった。

実装を読んでみると、hubotのredis-brainはRedisの1つのキーに全データをシリアライズして保存している事に気づいた。そのせいで1回の通信でRedisと全データのやりとりが行われて、maxmemoryを超えるらしい。

最初、Redisじゃなくmongoにでも乗り替えるか・・Heroku+mongolab無料枠(500MBぐらい)も何度か使ったことあるし・・・
と思ってたんだけど、ちょっと気になってredis-brainの実装読んだら1キーに全部保存してる事に気づいた。
そしてmongo-brainもmongolab-brainも、1つのdocumentに全データ保存してた。何故そうする・・


解決方法


最初からついてるredis-brainを捨てて、hubot-brain-redis-hashに乗り換えた。

hubot-brain-redis-hashはちゃんとRedisをKVSとして使ってて、更新のあったキーだけ書き込むとかやってくれてるように見える。
とりあえずちゃんと動いてる。


redis-brainのデータ消す


移行する必要もなかったのでとりあえず全部消した

'use strict'

Redis = require 'redis'
Url = require 'url'

info = Url.parse process.env.REDISTOGO_URL
client = Redis.createClient(info.port, info.hostname)
prefix = info.path?.replace('/', '') or 'hubot'

if info.auth
client.auth info.auth.split(":")[1]

client.on 'error', (err) ->
console.error err

client.on 'connect', ->
console.log 'connect!'

client.get "#{prefix}:storage", (err, reply) ->
return console.error err if err

if reply
data = JSON.parse reply.toString()
console.log Object.keys data._private

client.del "#{prefix}:storage", (err) ->
return console.error err if err
console.log 'deleted'

0

hubot-slackアダプタv3にアップデートしたら色々辛かった

hubot-slackアダプタを今までv2.x系を使っていたんだけど、先月ぐらいに出てたv3系にアップデートした。
hubot-slack アダプタ v2 から v3 へ | 半月記が参考になった。

hubot-rss-readerがv3で動かない機能があったりしたのも直した。


結論

v3系は色々おかしい気がするのでまだv3にアップデートしないほうがいいと思う。
とにかく今v2のhubot integrationをslackで使ってるなら、設定画面から削除はしないほうがいい。消したらv3しか使えなくなる。

slackの設定

slackに新規にhubot integrationを追加するとv3じゃないと使えないようになっている。
今まで使っていたintegration消して、新しいintegration作ってつなげた。
新規作成時に、hubotにアカウントを設定されるようになった。

/invite @hubotとかして予めチャットメンバーに追加しておかなければならないが、そのかわりprivate roomに参加させられる。


アダプタv2とv3の違い

通信がHTTPからwebsocketになった。
hubot側がclientなので、外に丸見えのサーバーじゃなくてもbotが動かせるのは良いと思う。


エラーの内容が読めない

なんかエラー出力がtoString使ってるので[object Object]とか言われて何だかわからない。
[Sun Jan 11 2015 16:51:34 GMT+0900 (JST)] ERROR Received error [object Object]
これはプルリク送っておいた

mergeしてもらえればこういうエラーになるはず
[Sun Jan 11 2015 16:59:42 GMT+0900 (JST)] ERROR Received error {"code":-1,"msg":"slow down, too many messages..."}

マジで辛かったのは、これとbrainの爆発が同時に起こってて本当に何がなんだかわからなかった。

いきなりexitする

websocketが切断されたり、単にエラーが起こるだけでprocess.exitが呼ばれるのは豪快すぎる。

存在しないroomに送信しようとするとエラー起こってprocess.exitする。

そうじゃなくても連続でsendしたらclientが落ちてプロセスも落ちる。hubot-rss-readerはqueueを作って500msecごとにsendするようにした。

いきなりexitする件は#127で議論されていて、slackのサーバー側が返すエラーコードに応じて自動再接続するか処理を分岐したいらしい。


room名の頭に#が付けられない

robot.send {room: "#hoge"}, "こんにちは"
じゃだめで
robot.send {room: "news"}, "こんにちは"
とする必要がある。
自分が使ってたスクリプトも#つけてるのが結構あったので、ここでエラーになってた。#つけても大丈夫にするプルリク送っておいた


websocket接続するまでrobot.sendが無い


起動した時に通知を送るようにしている。
v2まではHTTPだったから、これでよかったけど
module.exports = (robot) ->

robot.send {room: "news"}, "Hubot、起動しました"

v3ではwebsocketが接続するまでrobot.send関数が存在しないので、sendが生えてくるまで待つようにした。
module.exports = (robot) ->

cid = setInterval ->
return if typeof robot?.send isnt 'function'
robot.send {room: "news"}, "Hubot、起動しました"
clearInterval cid
, 1000

hubotって、まずrobot.adapterがチャットサービスに接続してconnectedイベントをemitして、それを受けてからscripts以下のcoffeeやjsが読み込まれるはずなんだけど、slackアダプタはなぜかscripts読み込まれた後でもまだadapterが接続されてない。connectedとは一体何なのか。

これはhubot-slackの下で使われてるnode-slack-clientの問題だったので、プルリクしておいた

0

hubotでgithubにissue立てる

1ヶ月ぐらい前だけどhubotからgithubにissueを建てるのを作った。

チャットからissueのリストを見たり作ったりできるので手軽。なんか雑用を思いついたらToDoというリポジトリにissue立てるみたいな運用されてる。




githubot


hubotからgithub apiを使う場合、githubotというnpmが便利だった。
githubotはbranchのリストを取る便利メソッドとかはあるけど、基本的にGitHubのAPIをgithubot.get(url,data,callback)とgithubot.post(url,data,callback)で直接扱う。

以下はgithubot 1.0.0-beta2を使っている。


セットアップ


HUBOT_GITHUB_TOKENという環境変数を使うので、

curl -i https://api.github.com/authorizations -d '{"scopes":["repo"]}' -u "自分のユーザー名"
を実行してAPI tokenをもらって、herokuに環境変数セットする。

heroku config:set HUBOT_GITHUB_TOKEN=a1b2cdef3456

なおissueはtokenを作ったユーザー名で建てられるようになる。


実装


Issues | GitHub APIをgithubotのpostに渡すだけでissueが作れた。

githubのAPIが綺麗に規則的にできているので、githubotもよくあるかっちりしたAPIラッパーである必要がなく、tokenとかパラメータの渡し方の部分だけよしなにやってくれる薄いライブラリになっている。
なんか下手にAPIリクエスト先のpathを規則的にメソッド名に変換しているとか、そういうのじゃなくてURLをそのまま書く方がわかりやすい感じがする。APIとライブラリ両方のドキュメントを突き合わせて読まずに済むのはよい。

https://github.com/masuilab/slack-hubot/blob/master/scripts/github_issue.coffee
debug = require('debug')('hubot-github-issue')
repo = process.env.HUBOT_GITHUB_ISSUE_REPO or "masuilab/todo"

module.exports = (robot) ->
github = require('githubot')(robot)

## list issue
robot.respond /issue$/i, (msg) ->
debug "get issues list (#{repo})"
github.get "https://api.github.com/repos/#{repo}/issues", {}, (issues) ->
issues = issues.sort (a,b) -> a.number > b.number
texts = ["https://github.com/#{repo}/issues"]
for i in issues
texts.push "[#{i.number}] #{i.title}"
debug texts
msg.send texts.join '\n'

## create issue
robot.respond /issue (.+)$/mi, (msg) ->
who = msg.message.user.name
body = msg.match[1]
debug "create issue #{body} (#{repo})"
query_param =
title: body
body: "#{body}\n\ncreated by #{who} & hubot"
labels: ["fromHubot"]
github.post "https://api.github.com/repos/#{repo}/issues", query_param, (issue) ->
text = "issue created\n#{issue.html_url}" or "issue create error"
debug text
msg.send text