【LINEBot】投票しろ【SINoALICE】

# 投票しろBotをつくる
## 開発の経緯
始業前に偶然見かけた
[プログラマーとして初めて親の役に立つものを作った話(LINEbot)](https://qiita.com/enomotoakira/items/64bf4efeccc0248958f1)
に触発されました。

実務約半年。JavaSciptは一応触れる。開発環境入れなくてもいいなら家のPCでも作業ができる。
やらない手はないな？？？

というわけで手始めに上記の記事を参考に、ゴミ出し日を通知する個人用Botを作ってみました。
なるほど、動くじゃないかと。

さて、せっかく年末年始で時間もあるし、もう一つ何か作ろうと思いまして。
でも実際自分が欲しくて他人の役に立つもの…？
どんな通知が来たらありがたいだろう…？
てかせっかくGAS使ってるんだからその辺のもの有効活用したい…したくない？

とか諸々考えた結果、
私が２年半続けているスマートフォンアプリ「SINoALICE」の投票しろBotを作ろうとなったわけです。
## 結局投票しろBotって何ぞ？
SINoALICEではギルド対ギルドのバトルが毎日１回開催されており、また、毎月１回、１週間の間「グランコロシアム」という特別なギルドバトルが開催されます。
その期間中限定コンテンツとして、

```
期間中配られる投票チケットを賭ける
→結果に応じてメダルが手に入る
→メダルで魔晶石(いわゆるガチャ石)やその他ゲーム内アイテムと交換できる
```

という「ギシアン酒場」というものがあります。
ただし期間中限定コンテンツのため、
**「最終日に全部賭けようと思っていたのに忘れてたせいで期間中のメダルが全部無駄になった」**
という人が後を絶ちません。~~私も賭け忘れ常習犯です~~
じゃあ**あらかじめ最終日をGoogleカレンダーとかに放り込んどいてLINEで通知させればよくね？**
GASがGoogleなんちゃらっていうくらいだしカレンダーとの連携もできるやろ！
そんな感じの軽いノリで作りはじめました。

# 機能要件
* 投票できる最終日に最大３回「投票しろ」とメッセージを送信する。(朝8時前、倍率ボーナスがなくなる18時前、投票期限の20時前)
* リプライが来たらちゃんと投票したものとしてその日はそれ以上メッセージを送信しない。ついでにちゃんと投票できたねって褒めてあげる。

# 完成したもの
## 完成品
![demo](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/989366/2e683db0-1b7e-23e4-b346-49b92f451f27.jpeg)

## ソースコード

```javascript:コード.gs
const CALENDER_ID = '参照するカレンダー@group.calendar.google.com';
const TOKEN = 'Botのアクセストークン';
const SPREAD = SpreadsheetApp.openById('スプレッドシートのID');
// ユーザーIDと投票したかどうかが入るシート
const USER_SHEET = SPREAD.getSheetByName('users');
// 褒め言葉一覧を入れてあるシート
const EULOGY_SHEET = SPREAD.getSheetByName('eulogy');
const HEADERS = {
  'Content-Type': 'application/json; charset=UTF-8',
  'Authorization': 'Bearer ' + TOKEN,
};
// 通知を飛ばすかどうか判定するフラグ
let push_flag = false;

// 毎日投票状況をリセットする(4時~5時にトリガーで起動)
function refresh() {
    USER_SHEET.getRange(2, 2, USER_SHEET.getLastRow()-1, 1).clearContent();
}
// 今日のイベントを取得する(=今日が最終日かチェックする)
function get_calender_events() {
  const calender = CalendarApp.getCalendarById(CALENDER_ID);
  const today = new Date();
  const events = calender.getEventsForDay(today);
    for (let i in events) {
      push_flag = true;
  }
}

// 「投票しろ」って通知する(7時~8時、17時~18時、19時~20時にトリガーで起動)
function vote_vote() {
  get_calender_events();
  if(push_flag){
    let last_row = USER_SHEET.getLastRow();
    for(let i = last_row; i > 1; i--) {
      if(USER_SHEET.getRange(i,2).getValue() == '') {
        let push_data = {
          'to': USER_SHEET.getRange(i,1).getValue(),
          'messages': [{
            'type': 'text',
            'text': '＿人人人人人人＿\n＞　投票しろ　＜\n￣Y^Y^Y^Y^Y￣',
          }]
        };
      let options = {
        'method': 'post',
        'headers': HEADERS,
        'payload': JSON.stringify(push_data)
      };
      UrlFetchApp.fetch('https://api.line.me/v2/bot/message/push', options);
      }
    }
  }  
}

// 何かしらリアクションがあったらdoPost関数が動くようになってる、らしい
function doPost(e){
  //ポストで送られてきたJSONをパース
  let event = JSON.parse(e.postData.contents).events[0];
  let user_id = event.source.userId
  let eventType = event.type
  let last_row = USER_SHEET.getLastRow();
  let reply_token= event.replyToken;
  // botが友達追加されたらユーザーIDを保存する
  if(eventType == 'follow') {
    for(let i = last_row; i >= 1; i--) {
      if(USER_SHEET.getRange(i,1).getValue() != '') {
        let j = i + 1;
        USER_SHEET.getRange(j,1).setValue(user_id);
        USER_SHEET.getDataRange().removeDuplicates([1]);
        break;
      }
    }
// 投票最終日の期限内(5:00~19:59)にメッセージが来たら１回だけ褒める
  } else if(eventType == 'message') {
    get_calender_events();
    let reply_time = new Date(event.timestamp);
    let reply_hour = reply_time.getHours();
    if(push_flag) {
      if(reply_hour > 4 && reply_hour < 20) {
        for(let i = last_row; i >= 1; i--) {
          if(USER_SHEET.getRange(i,1).getValue() == user_id && USER_SHEET.getRange(i,2).getValue() == '') {
            // 褒め言葉をランダムに選択
            let eulogies = EULOGY_SHEET.getRange(1, 1, EULOGY_SHEET.getLastRow());
            let random_number = Math.round(Math.random() * EULOGY_SHEET.getLastRow());
            let text = eulogies.getValues()[random_number][0];
            // メッセージを返信
            let reply_data = {
                'replyToken': reply_token,
                'messages': [{
                  'type': 'text',
                  'text': text,
                }],
            }
            UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', {
              'method': 'post',
              'headers': HEADERS,
              'payload': JSON.stringify(reply_data),
            });
            // 褒めたときにスプレッドシートにチェックを入れておく
            USER_SHEET.getRange(i,2).setValue('OK');
          }
        break;
        }
      }
    }
  }
}
```

## 苦労したこと
### スプレッドシートに書き込めない
スプレッドシートも共有しなきゃいけないのか？でもユーザーIDってバリバリ個人情報だから公開できんよな…ってなってたのですが、どうやら**GoogleDriveから直接GASを開くのではなくスプレッドシート側からGASを開かないといけなかった**ようで。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/989366/4df6cd17-b66f-ea3a-3e5c-075b101090d4.png)
これが原因だと気が付くまでに結構な時間が溶けました。
### 動かしてみたらなんかエラー吐く
いやログ見ろよって話なんですが、なんかログが重たいのかうちのパソコンが重たいのかでなかなか見れず、原因の特定にめっちゃ時間かかりました。やはりパソコンは変え時か…
結局タイポだったんだと思います。
でもここで「だったんだと思います」じゃ同じことがあったときにまた同じだけ時間がかかってしまうので、**失敗したことはきちんとどこかに残しておかないといけない**ですね。反省。

# 今後の課題
## ３か月に１回あるコロシアム・シンへの対応をどうするか
期間中毎日チケットが配られるグランコロシアムと違いチケットの配布が１回のみ、かつ倍率ボーナスが段階的に変わるためどのようにして落とし込むかまだ答えが出ていません。
## 毎日トリガー起動させてるの無駄な気がする
通知日が基本的に月１回なので、そのために毎日トリガー発動させてるのすごく勿体ない気がします。
私や利用者に支障がなくてもGoogle的に支障がありそうなので、なんとかできるならなんとかしたいなと思っています。
## コードがまだ汚い気がする
今まで資格を取る勉強ばかりしてきてまともな実務はまだやり始めて半年、JavaSciptに関しては実務で初めて触ったというところを考えればまあまだまあって感じもしなくはないですが…
業務やり始めて実感したのですが、読みにくいコードというのはみんなの生産性も下げてしまうので非常によくないと思います。
これが今の自分のベストではあるんですが、プログラマは成長を止めてはいけないと勝手に思っているので、今後も精進していきたいなと改めて思いました。

# 参考文献
[プログラマーとして初めて親の役に立つものを作った話(LINEbot)](https://qiita.com/enomotoakira/items/64bf4efeccc0248958f1)
[GASを用いLINE Messaging APIのフォローイベントでuserIdをスプレッドシートに転記する](https://qiita.com/s_home/items/50bf4834189cf7ad1318)
[Google Apps Scriptでオウム返しLINE Botを作る。](https://qiita.com/tetrapod117/items/e7b48485c98f6b88f311)
[GASとLINEAPIでハマったときの対処法](https://qiita.com/hiropoon/items/e8054e6f8a556c65f215)