高校生3人で作ったLINEBot「カップ麺タイマー」の技術的まとめと、開発の感想

<img width="1309" alt="スクリーンショット 2020-05-09 12.22.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488939/2c6f2a43-1715-e9f4-7505-d529ca738e5d.png">

こんにちは。最近高校生のDeveloper友達が増えてきて楽しくなってきているようかんです。

昨日、リリースした「[カップ麺タイマー](https://osumen.nucumo.com/)」を開発するにあたって、LINEBotのバックエンドを担当させてもらったので、開発の中で使った技術・検討したサービスなどなど、裏側を全部公開します！

プロダクトが始まった頃は**GitHubを使ったことが無い**、**非同期同期処理を知らない**、**自分で１からコードを書いたことが無い**という超絶初心者でした。ですが、[SGG](https://qiita.com/organizations/sgg?page=1)のみんなに質問しまくり、いっぱい学び、いっぱい調べて成長したプロダクト。僕をコードが一応かける人に変えてくれた感謝いっぱいのプロダクトです！！

気持ちを込めて真剣に書きましたのでぜひご覧ください！
間違ったことが書いてあったらコメントいただけると幸いです！
#この記事を読んで欲しい人
・プロダクトに興味を持ってくださった人
・LINEBotを使って何か作りたい人
・LINEBotの可能性を模索している人

#ProjectMember
当初は[@nztm](https://qiita.com/nztm)と2人でプロジェクトを進めていたのですが、配色やイラスト面で途中から[@yuki384](https://qiita.com/yuki384)さんも加わり3人で開発を進めました。

#「カップ麺タイマー」 is 何
![１.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488939/3a4269e7-898e-fc67-0031-a62768bc2d40.png)

「カップ麺タイマー」はLINEBotです。
LINEのアプリ内で利用することができるのでアプリ等のインストールの必要が無く、ユーザーに気軽に利用してもらうことができます。トーク上のメニューにある「カップ麺をつくる」ボタンを押し、1分から10分の待ち時間を選択します。選択すると同時にタイマーが起動したことを通知するメッセージとその待ち時間内に読める記事を1つレコメンドしてくれます。ランダムでレコメンドすることで普段は自分が目にしないような分野の記事に出会うことができるので、ユーザーが有意義な時間を過ごしてもらえるのではないか。ということをコンセプトにしたプロダクトです。

詳しいコンセプトについては[@nztm](https://qiita.com/nztm)君が書いた[カップ麺を待つ3分に新しい可能性をもたらす「カップ麺タイマー」をLINE Botで作った話](https://qiita.com/nztm/items/54b17abbaf31cdd4bfb1)を見てください。
使い方については僕が昨日投稿した[note](https://note.com/inoue2002/n/n8c367df5a2e0)を是非ご覧ください。
#開発の流れ
Slackのワークスペースを活用し、やりとりをしました。
ミーティングには[Zoom](https://zoom.us/)、
最初のアイデア出しには[Miro](https://miro.com/)を、
ミーティングで出てきた意見は[Notion](https://www.notion.so/)にまとめました。

#工夫した点・仕組みなど

##Bot構成
言語はNode.js
開発の最初の頃は[Heroku](https://jp.heroku.com/)や[now](https://zeit.co/)を使って運用しようと考えていたのですが、レコメンド記事をDBに保存することになりどこにDBを置こうかという問題の結果、最近興味を持っていたLambda＋DynamoDBでいいんじゃない？ということになり、AWSを使ってサーバレスLINEBotをつくることにしました。
<img width="380" alt="スクリーンショット 2020-05-09 13.51.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488939/a502e7b0-10a7-02eb-e6f7-ac9192aafa5f.png">


###タイマーが動いてpushする仕組み
userが1分から10分までのボタンをタップした時、記事をリプライして、同時にsetTimeoutを動かします。
しかし、同じLambda関数でこれらを実行すると、全てのイベントに対して関数を最大で起動する10分で起動しないといけなくなり、請求額がとんでもないことになりました。

なので、userの必要に応じた起動時間で関数を動かす必要があるため[invoke](https://qiita.com/inoue2002/items/8206ef1443ebb08c6ebd)を使いました
invokeのやり方詳細は上記記事をみていただければわかります。

Timerが起動すると同時にやることは、
###1.Timerのスタート、終了のイラストを決める(pictureIDの決定）
最初と最後でイラストを合わせる為、ただランダムで取得するだけでなくIDとして発行します
![アートボード 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488939/373636f8-a10b-c764-a4b8-35e94c8530ac.png)
###2.レコメンドする記事を決める(DBに保存されている記事を1個取得する）
###3.タイマースタートメッセージと記事をリプライ
###4.３桁の乱数でTimerIDを作成してDBに保存
<img width="367" alt="スクリーンショット 2020-05-10 11.15.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488939/c280e438-4a76-997a-d9ee-2385a35dcf9a.png">
###5.invokeする


```index.js
const params = {
          FunctionName: `${Min}min_function`,
          InvocationType: 'Event',
          Payload: JSON.stringify({
                  id: event.source.userId,
                  Ms: Ms,
                  timerID: timerID,
                  pictureId: pictureId
                  })
               }
   // 呼び出される側のLambda関数を実行する
   lambda.invoke(params).promise()
```
mainの関数からTimerが起動すると上記のような方法でinvokeしています。
Lambda関数を1分~10分で作ってあるので、`${Min}min_function`をしてinvoke先を決めます
pushする時に使う`userId`、setTimeoutに使う`Ms`、乱数生成した`TimerID`、最初と最後のイラストを合わせる為の`pictureId`をparamsに入れています。

invoke先では、

```index.js
exports.handler = async (event) => {
  if(event.pictureID==='1'){
    pictureURL = 'xxxxxx'
  }
  setTimeout(async () => {
  //ここでuserIdを使って、DBを一度確認しに行き、DBに保存されたTimerIDとevent.timerIDが一致した時にAxiosを使ってuserにpush。それ以外は何もしない
  }, event.Ms)
}
```
`pictureID`によってそれに応じた`pictureURL`を決定します。pushするときのメッセージで利用します。
あとはsetTimeoutの処理を待って、時間になったらDBを確認し、Timerが起動したときと同じTimerIDがDBにあればpushするといった仕組みです。

##Lambda関数の無料枠を超えない工夫
Lambda関数はリクエストの数とコードの実行時間に基づいて課金されるようになっています。だからいくら1か月ごとに100万件の無料リクエストがあったとしても毎リクエスト10分起動していると一瞬で課金に突入してしまいます。その為、各待ち時間によって関数を作成し、
<img width="1343" alt="スクリーンショット 2020-05-10 11.26.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488939/b28504d2-bdce-311c-2bd6-64883322382b.png">
<img width="664" alt="スクリーンショット 2020-05-10 11.26.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488939/bcd43a49-7b95-d0d2-28d3-f16fc8bf9f68.png">
<img width="646" alt="スクリーンショット 2020-05-10 11.27.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488939/9c36c400-e5b8-7fbb-0506-6af65f8c1fe6.png">
それぞれの関数によって起動時間を変えることにしました。

##タイマーが2重に作動しない仕組み
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">カップ麺を作っている時は他のタイマーを動かすことはできないよ！<br> <a href="https://twitter.com/hashtag/%E3%82%AB%E3%83%83%E3%83%97%E9%BA%BA%E3%82%BF%E3%82%A4%E3%83%9E%E3%83%BC?src=hash&amp;ref_src=twsrc%5Etfw">#カップ麺タイマー</a> <a href="https://t.co/8n7MszOy0s">pic.twitter.com/8n7MszOy0s</a></p>&mdash; ようかん (Yosuke Inoue) / linebot作成中 (@inoue2002) <a href="https://twitter.com/inoue2002/status/1259311514541887491?ref_src=twsrc%5Etfw">May 10, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
タイマーをすでに動かしている時に新しくタイマーを動かそうとしても`タイマーはすでに作動しています`と返されTimerの2重稼働ができない仕組みになっています。こちらに関してもただDBを使っていると言ってしまえばそれだけなのですが、「カップ麺をつくる」が押された時にDBを一度確認しに行って`OFF`の時しか次の処理に移れないようになっています。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">タイマーは動いてる時しか止められないよ！↩︎当たり前<a href="https://twitter.com/hashtag/%E3%82%AB%E3%83%83%E3%83%97%E9%BA%BA%E3%82%BF%E3%82%A4%E3%83%9E%E3%83%BC?src=hash&amp;ref_src=twsrc%5Etfw">#カップ麺タイマー</a> <a href="https://t.co/WILSitQ6sS">pic.twitter.com/WILSitQ6sS</a></p>&mdash; ようかん (Yosuke Inoue) / linebot作成中 (@inoue2002) <a href="https://twitter.com/inoue2002/status/1259340147960672258?ref_src=twsrc%5Etfw">May 10, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
タイマーをストップさせる機能にも同様にDBを確認する処理をしています。

##スタートと終了のイラストが同じになる仕組み
invokeする仕組みのところで少し紹介したのですが、![アートボード 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488939/f613b7a7-950a-c125-18c9-3205e2e11c18.png)
このように、スタートの時のイラストと終了時のイラストが同じになるようになっています。
この上記2種類のイラスト以外にも実装されているのでぜひ使って探してみてください！
##リアルタイムの友達追加数がわかる仕組み
![アートボード 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488939/b687f0af-1980-da1e-6008-be2c1ec9055c.png)
LINE公式アカウントはプロフィール欄を見ると現在の登録者数を確認することができます。しかし、友達にアカウントを紹介する時に表示される友達登録者数と数が異なっています。これはミスではなくあえてこうなっています。
LINE側で表示される友達追加数は一度追加すると増えますが、ブロックされてもその数が減ることはありません。
しかし、右の数は実際にDBを確認しに行き、今実際に追加してくれている友達の数を出しています。
LINEのAPIの中に[友だち数を取得する](https://developers.line.biz/ja/reference/messaging-api/#get-number-of-delivery-messages)というのがあるのですが、これを使うと反映が遅かったり、実際のアクティブユーザー数とは異なる場合があるため、DBを活用して表示しています。
<img width="138" alt="スクリーンショット 2020-05-10 12.02.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488939/dde42661-349d-8f25-bed1-53690cb8db97.png">
DBですが、先ほどから使っているDBを全スキャンしてそのItem数を取得しても現在の友達追加数にすることもできますが、何回も大勢の項目をスキャンするということがDynamoDBの無料枠を超える可能性が高くなるので、別に友達登録者数だけを記録するDBを作成し、追加されたら+1,ブロックされると-1でアップデートするようにしています。
##公式アカウントを簡単に友達に送信できる仕組み
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">LINE友達への共有が一瞬<a href="https://twitter.com/hashtag/%E3%82%AB%E3%83%83%E3%83%97%E9%BA%BA%E3%82%BF%E3%82%A4%E3%83%9E%E3%83%BC?src=hash&amp;ref_src=twsrc%5Etfw">#カップ麺タイマー</a> <a href="https://t.co/6ghaoKBjtS">pic.twitter.com/6ghaoKBjtS</a></p>&mdash; ようかん (Yosuke Inoue) / linebot作成中 (@inoue2002) <a href="https://twitter.com/inoue2002/status/1259327369950621697?ref_src=twsrc%5Etfw">May 10, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
メニューからその他を押すと、友達に紹介するという項目があります。そこには友達追加できるQRコードと「LINEで送信する」というボタンが表示されます。そのボタンを押すと実際のLINE友達の送信先が出てきて、一瞬で紹介することができるようになっています。
これにはLINE URLスキームというものを利用しています。実際にここで利用しているURLは[https://line.me/R/nv/recommendOA/@649wudig](https://line.me/R/nv/recommendOA/@649wudig)
参考：[LINE公式アカウントをシェアする](https://developers.line.biz/ja/docs/line-login/using-line-url-scheme/#line%E3%81%A7%E5%88%A9%E7%94%A8%E3%81%A7%E3%81%8D%E3%82%8Burl%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%A0)

##フレックスメッセージのこだわり
![アートボード 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488939/3d184232-c26f-d49e-63c5-607fa1dd5c91.png)

以前作った僕のBotと比較してみました。
日常で使っているLINEではテキストメッセージがメインなのでテキストが通知されますが、フレックスメッセージを使うとデフォルトのままだと、`Flex Message`という名前で通知が飛んでしまうことになります。以下のようにJSONの`altText`というプロパティにそのフレックスメッセージが何を伝えたいのかを入れておくとユーザーにとってはより便利なものになると思います。

```message.json
{
  "type": "flex",
  "altText": "Flex Message",  //このaltTextを変更する→タイマーが終了しました
  "contents": {
   }
}
```
##自作QRコードについて
個人的に開発をしていて知った知識を書いておきます。
QRコードって一部分を隠しても復元してくれるらしいですね。復元訂正レベルというのがあるらしいので今後調べたいと思います。ということで真ん中にアイコンを入れたQRコードを作ってみました。確かにこれぐらいQRコードを隠しても読み取ることができました。アイコンが入っていると何のQRコードか一目でわかるのでいいですね！
![アートボード 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488939/d763e263-dcd1-0cc8-c826-31403fe4a732.png)


##spredsheetとDynamoDBを同期する
<img width="550" alt="スクリーンショット 2020-05-10 12.30.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488939/dc86acab-dc42-22e3-ad94-c6a6ea8377c2.png">
開発の終わり頃に、ユーザーからもレコメンド記事を募集しよう！ということになりました。
googleformで募集を始めたのですが、それをそのままBotに反映すると不適切な記事があったとしても反映される可能性があります。なので、おすすめ記事が追加されていたら、運営で確認をして、spredsheetの上部にあるDynamoDBのボタンを押すとDynamoDBと同期できる仕組みをgasを使って作りました。一瞬で同期してくれるので便利です。（セキュリティは怪しそう）近々そこも詳しく書く予定なのでお楽しみに。
参考にした記事：[DynamoDBとGoogleスプレッドシートを相互同期するやつ](https://so-wh.at/entry/2016/09/01/DynamoDB%E3%81%A8Google%E3%82%B9%E3%83%97%E3%83%AC%E3%83%83%E3%83%89%E3%82%B7%E3%83%BC%E3%83%88%E3%82%92%E7%9B%B8%E4%BA%92%E5%90%8C%E6%9C%9F%E3%81%99%E3%82%8B%E3%82%84%E3%81%A4)

##使おうとしてやめたSheetDBについて
DBをどこに置こうかという問題で、一時期[SheetDB](https://sheetdb.io/)使って実装していました。
Google Spreadsheetにあるデータを簡単にAPI化できるということで、これを使ってAxiosで記事を取得していたのですが、無料枠だと、月に500リクエストしか無く、有料で月2000円くらいかかるとのことなので、運用は厳しいとの判断でやめました。でも、気軽にAPI化できて便利だったので書いておきます。知らなかった人はぜひ使ってみてください！



##致命的なバグがあった話（修正済み）
タイマーが起動した時に乱数を発行せず、ただON/OFFをDBに保存していた時期の出来事です。
実装できたー！と喜んでテストをしていたらしっかりタイマーは動いているのですが、一度タイマーストップの処理を挟むとBotが狂い出すバグがありました。どう狂ったのかというと、10分を選択したのに2分後に終了メッセージが届いた。みたいな感じ。
これはなぜ起こったかというと、

```
//その時実装していた流れ
タイマーが起動される→DBに`ON`を記録
タイマーストップが起動→DBに`OFF`を記録
setTimeoutが終了したらDBをみてONならpush、OFFなら何もしない
```

####1. 5分タイマーを起動
####2. 3分後ぐらいにストップを起動させる
この時点で5分タイマーのメッセージは来なくなるはずです。
####3. 10分タイマーを起動
この時点から10分後にメッセージがpushされれば成功ですが、その時は2分後にメッセージが届きました。
原因としては、pushする際の確認はタイマーがONかOFFかだったので、10分タイマーを起動している間に一番最初に起動した5分タイマーが終了し、DBを確認→ON→pushという処理になってしまったのです。
このバグを受けてTimerIDを生成し、タイマースタート時と終了時でIDが異なっていないか確認をして処理を分けるという方法で解決しました。


##結果的に使ったDBの数
これに関してはもっと綺麗に作れただろうな。と今では反省しています。
今実際に動かしているDBの数は4つ。
- 友達登録者数を記録するDB
- レコメンドする記事を保存するDB
- userがTimerを動かしているかどうかを記録するDB
- userが記事のレコメンド設定の状態を記録するDB


#終わりに
ここまでお読みいただきありがとうございます。

冒頭にも書きましたが、このプロダクトを始めたことは何にも知らない超絶初心者でした。そこから3ヶ月。色々な刺激を受け、勉強をしてなんとかリリースまでもって行くことができました。[野崎くん](https://qiita.com/nztm)と梅田で何時間もだべったあの日が懐かしいなぁ。そして、[三橋さん](https://qiita.com/yuki384)とまさか一緒にプロダクトを動かすことになるとも夢にも思いませんでした。本当に感謝しています。
昨日の18:30にリリースしましたが、1日で50人以上の方に使っていただき今の時点で70人を突破しています。登録してくださった皆さんにも感謝したいと思います。
僕の学校では「開発する」とか「リリースする」とかを一切聞かない環境なので、この休校期間を使って、普段はなかなかできない活動ができて僕としてはいい経験ができました。これを基にこれからも色々な物を生み出して行きたいと思います。ありがとうございました。
今後もこのサービスをよりよくするため、色々な追加機能を相談中です。ぜひそちらもお楽しみに！！！
面白かったなと思っていただけたらLGTMやSNSへの拡散をお忘れなく！次への開発のモチベーションになります！
#関連記事
リリースツイート　-開発者3人-
https://twitter.com/nztm_tw/status/1259053313854599174?s=20
https://twitter.com/YukiMihashi/status/1259055152792657920?s=20
https://twitter.com/inoue2002/status/1259053187836739585?s=20
公式サイト -公式-
https://cupmenbot.nztm.io/
Qiita　-コンセプト-
https://qiita.com/nztm/items/54b17abbaf31cdd4bfb1
Qiita -技術的まとめ-
https://qiita.com/drafts/7e47283ba9affa0fac82
note -カップ麺タイマーの使い方-
https://note.com/inoue2002/n/n8c367df5a2e0

LINE×Lambda -プログラミングを知らなくてもサーバレスLINEBotが作れる記事-
https://qiita.com/inoue2002/items/a87df2b520f8b6e37f42