golangで書かれたSlack bot でエンジニアに話題提供しよう

こんにちは、id:hakobe932 です。はてなエンジニアアドベントカレンダーの18日目として、はてな社内で導入をためしている話題提供Slack botの機能と実装について紹介します。昨日はid:astj による Herokuとwerckerによる継続的インテグレーション・自動デプロイでperlのwebアプリケーションを開発するでした。

#enginnerで技術交換

はてなではメインのチャットツールとしてSlackを活用しています。チームや職種などの単位のたくさんのチャンネルがあり、それぞれのチャンネルでコミュニケーションが行われているのですが、もっぱら技術的な議論を行っているのが #enginner というチャンネルです。#engineer では、チームをまたいだ技術的な相談のほか、新技術や勉強会の紹介など、技術に関わるさまざまな話題で情報交換しています。

もっと技術の話題でわいわいしたい

基本的にはにぎやかな#engineerですが、どうしても業務で利用している技術の話題に偏りがちです。社内のエンジニアが興味があって調べている技術や、専門にしている技術などを自然に話題に取り上げられないかと考えていました。

engineerkun登場

そこで、最近はengineerkunという名前の、自動的に話題を提供してくれるSlack botを#engineerに常駐させています。engineerkunは、はてなブックマークは指定したタグで検索して、適度に人気で新鮮なエントリのURLを定期的に発言してくれます。

f:id:hakobe932:20141216085704p:plain:h400

単に定期的にURLを発言するだけだと人間の会話の邪魔になるので、以下のように適度に遠慮して活動するのが特徴です。

  • 人間が会話している時には遠慮して発言しない
  • 人間が会話していなくても連続で数回発言したらだまる (朝来たらログがうまっているということがない)

会話が途切れた時に適度におもしろURLが投稿されることで、多様な話題によるコミュニケーションがわいわいと起こることを期待しつつ動かしてみています。

engineerkun の使い方

engineerkunはbotの愛称で、実装は拙作のpresentというツールです。presentを使うと、engineerkunのような話題提供botを簡単に作ることができます。


hakobe/present · GitHub


golangで書かれているので、go getコマンドを利用するとすぐに利用することができます。

$ go get github.com/hakobe/present
$ PRESENT_SLACK_INCOMMING_URL="https://hooks.slack.com/services/ABCD1234/EFGH5679/abcdefghijk123456" \
  PRESENT_DB_DSN="id:pass@tcp(mysqldhost:3306)/dbname?parseTime=true&charset=utf8" \
  PRESENT_NAME=engineerkun \          # コマンドを実行するときに呼ぶbotの名前
  PRESENT_WAIT=900 \                  # URLを発言する頻度(秒)
  PRESENT_NOOP_LIMIT=3 \              # この回数だけ連続して発言したら一時停止する
  PORT="8080" \                       # WebHooksを待ち受けるHTTPサーバのポート
  $GOPATH/bin/present

presentは、発言用とチャンネルのログの監視のためにSlackのIncomming WebHooksとOutgoing WebHooksを一つづ利用します。また、タグや検索したURLを保存するストレージとしてMySQLが必要です。くわしくはREADMEで説明しているので参照してください。

heroku-buildpack-goを利用すると Heroku 上でも動作させることができます(適宜Godepsをリポジトリに含める必要があります。heroku-buildpack-goのドキュメントをご参照ください。)。

チェックするはてなブックマークのタグは、botを常駐させたチャンネルでengineerkun add perlのように発言すると追加することができます。例えばはてなでは以下のようなタグを設定しています。

f:id:hakobe932:20141218011016p:plain

この状態で放っておくと、環境変数で設定したPRESENT_WAIT秒後に、設定したタグに関する人気記事がbotを常駐させたチャンネルに投稿されます。

f:id:hakobe932:20141218011012p:plain

人間が会話している時には、botが適当に遠慮してURLが投稿されませんので、動作を検証したいときにはengineerkun plzのようにお願いするとよいでしょう。

f:id:hakobe932:20141218113311p:plain

もちろん、技術に関係のないタグを登録することもできますから、engineerkun add animeのようにして、ひたすらアニメ情報をウォッチするのもよいでしょう。

そのほかの機能については、engineerkun helpと発言するか、READMEを参照してください。

実装の紹介

presentはgolangを使って実装されています。ほとんど標準ライブラリのみを利用しています(MySQLのドライバだけはgo-sql-driver/mysqlを使っています)。いくつかおもしろポイントを紹介します。

goroutineを活用した設計

はてなブックマークを定期的にチェックするgoroutineSlackのOutgoing WebHooksを受け付けるWebサーバのgoroutine、そしてそれらを管理する スケジューラのメインルーチン の3つが協調して動作します。

それぞれのgroutineは自分のメインループを持ち、各々自分の仕事しています。goroutineの外部へのインターフェースとしてはchanelを公開するようにします。goroutineの中ではプログラムは逐次的に動作しておりデータ競合について考慮する必要はありません。たとえば 、はてなブックマークを定期的にチェックするgoroutineでは、概ね以下の様なメソッドをつかって新しいgoroutineを作っています。

func Start() (<-chan *RssEntry, chan<- []string) {
	ticker := time.Tick(10 * time.Minute)
	out := make(chan *RssEntry)
	newTags := make(chan []string)

	collect := func(tags []string, out chan *RssEntry) {
		...  
		out <- fetchedEntry // 結果をチャンネルに返す
	}

	go func() {
		tags := make([]string, 0)
		for {
			select {
			case <-ticker:
				collect(tags, out)
			case ts := <- newTags: // 新しいタグの一覧を受け取る
				tags = ts // 逐次的に動作するので同期化せずに状態を変更できる
				collect(tags, out)
			}
		}
	}()

	// インターフェースを公開する
	return out, newTags
}

このような機能を実装する場合、オブジェクト志向プログラミングでは、機能の持つ状態を抽象化したオブジェクトを定義することが多いですが、golangではプロセス( = goroutine)を使って処理を中心に機能を抽象化することができます。並行処理と相性が良く、Erlangのようなプロセスを中心にプログラムを行う言語ではよく利用されている方法です。

DB操作

golangの標準ライブラリである、database/sqlを使うとRDBMSに接続してSQLを実行することができます。コネクションプーリングを備えていたり、goroutineをまたいで使っても競合が起きない(= groutine safe)になっているなど、golangらしい特徴も備えます。素朴にSELECT文を実行すると以下のようになります。PerlのDBIのようにシンプルです。

func All(db *sql.DB) ([]string, error) {
	sql := `
		SELECT tag FROM tags ORDER BY tag ASC
	`

	rows, err := db.Query(sql)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	tags := make([]string, 0)
	for rows.Next() {
		var tag string
		if err := rows.Scan(&tag); err != nil {
			return nil, err
		}
		tags = append(tags, tag)
	}
	if err = rows.Err(); err != nil {
		return nil, err
	}

	return tags, nil
}

MySQLに接続するにはgo-sql-driver/mysql: Go-MySQL-Driver is a lightweight and fast MySQL-Driver for Go's (golang) database/sql packageが必要です。PerlのDBIと同じようなDSNを設定して接続するのでPerl使いにも馴染みがありますね。

import (
	"database/sql"
	_ "github.com/go-sql-driver/mysql"
)
db, err := sql.Open("mysql", "id:pass@tcp(mysqldhost:3306)/dbname?parseTime=true&charset=utf8")

まとめ

以上 engineerkunをその実装である present の紹介でした。presentを使うとエンジニア向けに限らない話題提供botを気楽につくることができます。 はてな社内での評判はまずまずですが、だいたいうまく機能しているのでぜひおためしください!

はてなエンジニアアドベントカレンダー の明日の担当は id:wtatsuru さんです!

はてなでは、golangやエンジニア同士の交流の仕方に興味のあるエンジニアを募集しています。