友だち数50万人を誇るbotリマインくん詳解

この投稿は [LINEBot&Clova Advent Calendar 2018](https://qiita.com/advent-calendar/2018/linebot)の4日目の投稿です。前日は[K_M95](https://qiita.com/K_M95)さんの[PythonでClovaスキルを作ってみた](https://qiita.com/K_M95/items/d980e228da834d7e10e9)でした。step-by-stepでClovaスキルの作り方が学べます！

本投稿では拙作のLINE bot、[リマインくん](http://remine.akira108.com)について技術的な説明をします。これを見てLINE botに興味を持ってくだされば幸いです！

# リマインくんとは
リマインくんは友だち数**50万人**を超えるLINEのリマインダーbotです。用件と時間を送ると、その日時にリマインドしてくれます。よくエゴサして、わりと使ってくれているな、とにこにこ眺めています。リマインドというタスクとchatbotの親和性は高いなー、と思います。

![](http://remine.akira108.com/assets/img/ss1.png)

- **「打ち合わせ」「明日の15時から」**
- **「燃えるごみ」「毎週水曜日の朝8時」**
- **「歯医者さん」「2月20日 10:00」**
- **「カップラーメン」「3分後」**

といったような形で、多種多様な日本語で日時を指定したり繰り返したりできるのがポイントです。私自身がリマインダーアプリはあまり見ないけどLINEはぜったい見るな、というモチベでつくりました。

# 構成

- Kotlin
- Spring Boot 2.x（Webフレームワーク）
- MyBatis（O/R マッパー）
- MySQL
- Redis
- [Messaging API SDK for Java](https://github.com/line/line-bot-sdk-java)
- Mackerel (Monitoring)
- さくらのクラウド App1台・DB1台

50万人くらいなら素直に実装してサーバーの数も少なくてもいけるんだな、という学びがありました。

# 仕組み

特に複雑なことはしておらず、Kotlin + Spring Bootでガシガシ書いています。割と最近までRuby + Sinatra + ActiveRecord + Sidekiq （ジョブキュー）で動いていましたが、長大な3篇の`.rb`で動いておりメンテ不能になったので書き直しました。Kotlin最高！（私は業務ではAndroidアプリを作っています）

各部位を見ていきましょう！

## Messaging API

これも普通に使っています。[SDKのキッチンシンク](https://github.com/line/line-bot-sdk-java/tree/master/sample-spring-boot-kitchensink)を見ればほとんどすべての使い方がわかります。

リマインくんで使っている機能としては

- Push Message（リマインド）
- Reply Message（ふつうの応答）
- Flex Message（繰り返し予定の一覧）
- Quick Reply（スヌーズの選択肢）

が挙げられます。Rich Menu APIも使ってみたい。

## 日本語→`yyyy/MM/dd HH:mm:ss`変換

リマインくんの一番のキモがこの部分です。ユーザーからの日本語のメッセージを受け取り、そこからリマインドするの日時の情報を抽出します。**ネタバレですが、AIではありません。**

ではどうするかというと、[ANTLR(ANother Tool for Language Recognition）](https://www.antlr.org)を使っています。ANTLRはJavaで実装されているパーサジェネレータです。単語リスト(字句解析機、lexer)と文法(構文解析器、parser)を与えると、入力された文字列をパースして木構造を作り、その木構造を巡るソースコード(Java, Python, Go, etc.)を生成してくれるものです。

わかりやすい例をあげますと、
**「明後日の14時」**と入力された場合には、このような木構造で解釈されます：

<img width="311" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/8012/acd3f2c6-80ca-e9c1-782b-c63177035507.png">

これはつまり、lexer中の

```ruby
fragment DIGIT : ('0' .. '9' ) ;
NUMBER: DIGIT+;
HOUR    : '時' | 'じ' ;
DAY_AFTER_TOMMOROW : '明後日' | 'あさって';

```

を含む文法の

```ruby

relative_date: 
             | DAY_AFTER_TOMMOROW DELIMITER? time # relativeDateDayAfterTomorrow

```
にマッチしたということです（`time`は時刻を表す、さらに色んな文法からなる文法です）。

そしてこの木構造を巡ったときに呼ばれるメソッドで日付の計算をしてあげればよいわけです。

```java
@Override
public void exitRelativeDateDayAfterTomorrow(Parser.RelativeDateDayAfterTomorrowContext ctx) {
    // 明後日
    offsetDay += 2;
}
```

実際には日付や時刻関係の単語や文法がもっとうじゃーと書いてあり、**「来月の5日の12時」**などでも地味に計算をしまくっています。そしてたくさんのテストコードで検証しています。とある事情で一晩でつくらなきゃいけなくて、涙がちょちょぎれました。

繰り返しのときは、似たように**「毎日〜」**のような文法を定義して、根性でCron Expressionに変換しています。あとはSpringに、そのCron Expressionで表される日時のうち、現在の次の日時を求めるクラスがあるのでそれを使っています。

## 指定時刻の通知

指定時刻の通知は、上記の方法で得られた日時情報をunix timeに変換して、unix timeをkey、リマインダーのidをvalueとして、Redisのソート済みセットに入れて`ZREVRANGE`で引きまくって、現在時刻に等しいまたは過去のがあればリマインドするキューに積む感じです。
[Redis in Actionの6.4.2 Delayed Tasks](https://redislabs.com/ebook/part-2-core-concepts/chapter-6-application-components-in-redis/6-4-task-queues/6-4-2-delayed-tasks/)のコードをKotlinで書き直しただけです。

# キャラ

[ツイッターでエゴサ](https://twitter.com/search?f=tweets&vertical=default&q=リマインくん&src=typd)すると、リマインくんはそのキャラクターが愛されていることがわかり大変うれしいです。
ご覧いただけるように、ただのリマインダーだけではなく応援したり気遣ったりするようにしています。普通のアプリではなくて、友だちとLINEするかのように使えるLINE botだからこそ、ユーザーが仲の良い友だちに思ってくれるようなbotを作っていけるといいと思います。

# むすび

リマインくんのキモとなる部分を説明してみました。楽しんでいただけたら幸いです。
このようなシンプルなbotでもみなさんに使っていただけている理由は、

- **誰もが使える王道な機能をちゃんとつくる**
- **+αのキャラクター性**

なのかなと個人的には思います。
ぜひ皆さんにも便利でかつ楽しいbotをつくっていただけたらと思います。UIつくらなくていいしすぐに使ってもらえるので、楽ちんで楽しいですよ。冬休みにぜひ！