「調整さん」を(日程調整ではなく)出欠の管理に使っている前提で、出欠登録のリマインダをLINE BOTとして作ってみたので、そのメモ。
[9/29追記]LINE BOT APIはDeprecatedとなり、Messaging APIに置き換わりました。同時にSDKもMessaging API対応のものに更新されていますので、ご注意ください。
[10/23追記]LINE BOT API Trial Accountは、11/16に完全削除を予定されていると発表されました。
新しいMessaging APIへの置き換えについては、次のエントリを参照してください。
環境
- Google App Engine SDK for Go 1.9.40
- LINE BOT API Trial SDK / Go Version
仕様
- 調整さんのイベントをクロールし、3日後に開催される予定がある場合、参加人数などを通知する
- 通知は、BOTと友だち登録した人全員に送られる(cronで毎朝8:00に起動)
- 監視対象の調整さんイベントは、ハッシュを
app.yaml
に記述しておく - LINE BOTのシークレットキーなども
app.yaml
に記述
GAE/Goのプロジェクト作成
Google Cloud Platformコンソールで新規プロジェクトを作成。プロジェクトIDを確定させる。
LINE BOT(チャンネル)の開設・設定
- BOT API Trial Accountを開設
- 開設したチャンネルの"Basic Information"にある"Callback URL"に、AppEngine側のコールバックを受け取るURLを設定
- "https:// YOUR_PROJECT_ID .appspot.com:443/line/callback"
- "Server IP Whitelist"は設定しない。設定するとAppEngineのIPアドレス変動に対応する必要があるため
必要なパッケージのインストール
$ goapp get github.com/line/line-bot-sdk-go/linebot
$ goapp get golang.org/x/text/encoding/japanese
$ goapp get golang.org/x/text/transform
LINE BOTの基本部分を実装
LINEからのコールバック用のハンドラをfunc init()
に定義する。パスは上で定義したもの。
func init() {
http.HandleFunc("/line/callback", lineCallback)
}
lineCallback()
の中身は、ほぼBOT SDKのサンプルをコピペ。ただし、App Engineではhttp.Clientとしてappengine/urlfetch
を使用する必要があるため、LINE BOT Clientからもこれを使うように、下記のように初期化する。
bot, err := linebot.NewClient(channelID, channelSecret, channelMID,
linebot.WithHTTPClient(urlfetch.Client(c)))
ここで一度goapp deploy
して動作確認。BOTを友達登録して、トークで送った内容をオウム返ししてくれればok.
友だち登録時にMIDを取得してデータストアに格納する
LINEへのメッセージ送信には、toにMID(LINEユーザの固有ID)を指定する必要がある。友だちに一斉配信するためには、あらかじめ友だち登録時に相手のMIDを取得しデータストアに保存する。
友だち登録および解除は、メッセージとは別フォーマットのオペレーション・イベントが通知される。パラメタのOpType
によって、追加(ブロック解除も等価)、削除を判別できる。
なお、メッセージとは送信者のMIDが格納されている位置が異なる。OperationContent.Params[0]
に入っているので注意。
if content.IsOperation {
//オペレーションイベント受信
opContent, err := content.OperationContent()
if content.OpType == linebot.OpTypeAddedAsFriend {
task := taskqueue.NewPOSTTask("/task/addfriend", url.Values{
"mid": {opContent.Params[0]},
})
taskqueue.Add(c, task, "default")
} else if content.OpType == linebot.OpTypeBlocked {
task := taskqueue.NewPOSTTask("/task/removefriend", url.Values{
"mid": {opContent.Params[0]},
})
taskqueue.Add(c, task, "default")
}
} else if content.IsMessage && content.ContentType == linebot.ContentTypeText {
//テキストメッセージ受信
(snip)
}
Task Queue/task/addfriend
は送信者のMIDをデータストアに保存、/task/removefriend
は送信者のMIDをデータストアから削除するもの。
メッセージを全員に送信
テキストメッセージを受信したとき処理を、送信者にオウム返しするもの(サンプルの実装)から、データストアに保存されている全MIDへの送信に変更する。
//データストアから購読者のMIDを取得
q := datastore.NewQuery("Subscriber")
var subscribers []Subscriber
if _, err := q.GetAll(c, &subscribers); err != nil {
return err
}
mids := make([]string, len(subscribers))
for i, current := range subscribers {
mids[i] = current.MID
}
//全員に送信
bot.SendText(mids, message)
調整さんリマインダ処理
調整さんリマインダは、下記処理をcronで毎朝起動されるようにする。
調整さんのイベントをクロール
cronからキックされるハンドラをfunc init()
に追加する。
http.HandleFunc("/cron/crawlchouseisan", crawlChouseisan)
以下、crawlChouseisan()
の実装。
調整さんのAPIは公開されていないが、イベント作成者にのみ表示される「出欠表をダウンロード」リンクのURLから、csv形式のレスポンスが得られるのでこれを利用する。
調整さんのイベントハッシュは、イベント作成時にコピーしておき、app.yaml
に定義したものを取得する。
url := "https://chouseisan.com/schedule/List/createCsv?h=" + os.Getenv("CHOUSEISAN_EVENT_HASH")
c := appengine.NewContext(r)
client := urlfetch.Client(c)
res, err := client.Get(url)
csvをパースする。MS932なのでjapanese.ShiftJIS
デコーダを使用している。
また、encoding/csv
のReader
は、1レコード目と異なるカラム数のレコードをエラーと判断する。調整さんのcsvは1レコード目がタイトルのみとなっており、素直に読むとデータ行で"wrong number of fields in line"というエラーメッセージが出てしまうので、下記にようにErrFieldCount
は無視するようにした。
reader := csv.NewReader(transform.NewReader(csvBody, japanese.ShiftJIS.NewDecoder()))
for {
row, err := reader.Read()
if err == io.EOF {
break
} else if e2, ok := err.(*csv.ParseError); ok && e2.Err == csv.ErrFieldCount {
//フィールド数エラーは無視
} else if err != nil {
log.Errorf(c, "Read chouseisan's csv failed. err: %v", err)
return nil
}
(snip)
日付などのパース処理は割愛。日付をキーにしたMapに詰める。
3日後の予定があれば通知
AddDate()
で3日後を指すDate
を作り、Mapにその日のValueが存在すれば通知対象とする。なお、App Engine実行環境ではUTCが使われるため、JSTを明示的に使用する。
tz, _ := time.LoadLocation("Asia/Tokyo")
today := time.Now().In(tz)
(snip)
//3日後の予定をピック
after3days := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, tz).AddDate(0, 0, 3)
obj, exist := m[after3days.String()]
obj, exist := m[after3days]
if !exist {
log.Infof(c, "Not found schedule at 3 days after.")
return
}
//メッセージを組み立てて送信
(snip)
cron.yamlを作成
作成したハンドラを、毎朝8:00 JSTに起動されるようにcronを定義。
cron:
- description: crawl chouseisan every day
url: /cron/crawlchouseisan
schedule: every day 08:00
timezone: Asia/Tokyo
以上をデプロイして、今のところ想定通り動作している模様。
途中、シネマカリテで『不思議惑星キン・ザ・ザ』<デジタル・リマスター版>を観た影響で、ひたすら「クー」と返す "キン・ザ・ザBOT" に作り変えたくなったけど、よく我慢した。
ソース
下記リポジトリで公開しています(Apache License 2.0)
GitHub - nowsprinting/ChouseisanReminder: Reminder line-bot for chouseisan.com
はじめてのGoでありAppEngine/Goアプリなので、作法とかなっとらん自覚はあります。なので参考程度に。『みんなのGo言語』ポチったので届いたら読んで改善したい。
参考
- Go on Google App Engine | App Engine Documentation | Google Cloud
- http://line.github.io/line-bot-api-doc/ja/
- GitHub - line/line-bot-sdk-go: LINE Messaging API SDK for Go
- LINE BOT API 実は友達追加時にもCallbackされてた & 署名確認をしよう。
- Go言語でCSVの読み書き(sjis、euc、utf8対応)
- encoding/csv error handling question
- [Go] UTCの時刻を日本時間に変換する
- ATOM で Go — プログラミング言語 Go
*1:りんなはグループに登録できるので、トライアルの仕様?