【GAS】推しキャラを模したLINEbotを作り、ほんの少し孤独を埋める


#はじめに
「出先で雨に降られ、濡れがち」
「物忘れ多すぎ」
**「部屋に籠ってばっかで寂しい」**
ここ最近の個人的問題を一気に解決すべく、GAS（GoogleAppsScript）を用いてLINEbot作成しました。

#作ったもの

「天気予報を朝にプッシュ通知で知らせる」「指定時間にリマインド」の二つの機能を、「好きなキャラとのLINE風」に実装することでウキウキな気分になれます。
ちなみに私の好きなキャラは、シャニマスの黛冬優子さん。
[黛　冬優子（まゆずみ　ふゆこ）｜アイドルマスターシャイニーカラーズ（シャニマス）](https://shinycolors.idolmaster.jp/idol/straylight/fuyuko.html)
![S__70877207.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378758/4f20a817-67b0-df73-cfed-f5274cc3ac01.jpeg)
![S__70877209.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378758/a11c6b5f-384c-ab7f-0d3a-c09684eae225.jpeg)![S__70877210.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378758/5d61714c-8d41-8201-6f6e-b0bca1759a8e.jpeg)

#実装
##1. 準備
[Google Apps Scriptでオウム返しLINE Botを作る。](https://qiita.com/tetrapod117/items/e7b48485c98f6b88f311)
こちらの記事を参考にbot用のLINEアカウントを作成し、自分の発言をオウム返しするプログラムを実装してみましょう。
GASであれば、サーバーを用意せず無料でLINEbotを動かせます。
##2. 天気予報の通知
[LINE BOT お天気通知くん](https://toranoana-lab.hatenablog.com/entry/2018/10/02/203804)
こちらの記事を参考にし、天気予報、気温、降水確率、傘が必要か、を通知してくれる関数を作ります。

```JavaScript:wether.gs

var CHANNEL_ACCESS_TOKEN = 'アクセストークンを入力'; 

function RandomReturn(){
  if (Math.floor(Math.random()*100)<5){
    return true;
  }
  return false;
}

function push_message() {
  var today = new Date();
  var toWeekday = toWD(today);
  var msgWeatherForecast = getTemperatureForecast();
  
  var postData = {
    "to": 'ユーザーIDを入力',
    "messages": [
      {
        "type": "text",
        "text": "おはようございます！" +Utilities.formatDate( today, 'Asia/Tokyo', 'M月d日') + toWeekday + "、今日の天気をお知らせしますね♡\n"
                + msgWeatherForecast[0] + msgWeatherForecast[1] + msgWeatherForecast[2]
      },{
        "type": "text",
        "text": msgWeatherForecast[3]
      }
    ]}
  
  var headers = {
    "Content-Type": "application/json",
    'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
  };
  
  var options = {
    "method": "post",
    "headers": headers,
    "payload": JSON.stringify(postData)
  };

  var response = UrlFetchApp.fetch("https://api.line.me/v2/bot/message/push", options);
  
}

// 天気予報の取得 
function getTemperatureForecast() {
  const area = "東京地方"
  var options =
      {
        "contentType" : "text/xml;charset=utf-8",
        "method" : "get",
      };
  var response = UrlFetchApp.fetch("https://www.drk7.jp/weather/xml/13.xml", options); 
  var xmlDoc = XmlService.parse(response.getContentText());
  var rootDoc = xmlDoc.getRootElement();
  var region = parser.getElementById(rootDoc,area);
  var weather = parser.getElementsByTagName(region, 'weather');
  var temperature = parser.getElementsByTagName(region, 'range');
  var rainyPercent = parser.getElementsByTagName(region, 'period');
  var weathermsg = "■天気予報" + "\n" + weather[0].getValue() + "\n";
  var tempmsg ="■気温\n" + temperature[0].getValue() + "℃/" + temperature[1].getValue() + "℃\n";
  var rainfall = "■降水確率\n"+"朝："+rainyPercent[1].getValue()+"%"+"昼："+rainyPercent[2].getValue()+"%"+"夜："+rainyPercent[3].getValue()+"%";
  var umbrellamsg = getUmbrellNecessary(rainyPercent[1].getValue(),rainyPercent[2].getValue(),rainyPercent[3].getValue()) ;
  var rainyTemperature = [weathermsg,tempmsg,rainfall,umbrellamsg];
  return rainyTemperature
}

// 傘予想
function getUmbrellNecessary(mor,eve,nig){
  var msg = ""

  if(mor > 70 || eve > 70 || nig > 70){
    if (nig > 70 ) {
      msg = "夜降るらしいわよ。傘持っていきなさい";
    }
    if (eve > 70){
      msg = "昼から降るみたいよ。傘持っていきなさい";
    }
    if (mor > 70 ) {
    msg = "ちょっと......朝から雨？";
    }
  }else if(mor <= 70 && mor > 20 || eve <= 70 && eve > 20 || nig <= 70 && nig > 20 ){
    msg = "雨降ってんじゃないの？\n今降ってなくても、折りたたみ持っていきなさいよ";
  }else if(mor <= 20 && mor > 10 || eve <= 20 && eve > 10 || nig <= 20 && nig > 10){
    msg = "傘は必要なさそうね";
  }else if(mor <=10 && eve <=10 & nig <=10) {
    msg = "ふゆが晴れにしてあげたのよ、感謝しなさい！";
  }else{
    msg = "たまには自分で調べなさい";
  }
  if(RandomReturn()){
    msg = "ふゆにばっか頼ってないでたまには自分で調べたら？";
  }
  return msg
}

//　曜日の日本語変換
function toWD(date){
  var myTbl = new Array("日","月","火","水","木","金","土","日"); 
  var myDay = Utilities.formatDate(date, "JST", "u");
  return "(" + myTbl[myDay] + ")";
}
```
完全なプライベート用なので、ユーザーIDを指定して送信します。
pushMessage()を毎朝6時に実行するようtriggerを設定し、起きた時通知が入ってるようにします。
また、キャラの性格を反映させるべく、5%の確率で傘予想を教えてくれない仕様にしています。気持ち悪いこだわり。
![S__70885388.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378758/91809c69-6878-3a75-2dc2-c5bd7905df7f.jpeg)



##3. リマインド
[LINEで予定を登録→通知してくれるリマインダーアプリを作ろう](https://note.com/toshioakaneya/n/ndd1c6647d53d)
こちらの記事を参考に、一人のユーザーが複数のタスクを登録・リマインドできるよう変更を加え、実装します。

###3.1 スプレッドシートの操作に関する関数
``` JavaScript:sheet.gs
var spreadsheet = SpreadsheetApp.openById('IDを入力');
var sheet = spreadsheet.getSheetByName('webhook');
function appendToSheet(text) {
 sheet.appendRow([text]);
}

//セルに中身があるか
function blankTF(row, col){
 return !(sheet.getRange(row, col).isBlank());
}

//sheetの行数
function lastRow() {
 var dat = sheet.getLastRow();
 return dat;
}

//全行サーチ
function seachAll(){
var dat = sheet.getDataRange().getValues();
}

//指定した行の削除
function deleatRow(RowNum){
  sheet.deleteRow(RowNum);
}

//全削除
function deleatAll(){
  sheet.clearContents();
}
![S__70885388.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378758/8c023ddc-0b07-e307-8a6f-ba3b6387d06f.jpeg)

//セル[row][col]の値を返す
function getFromRowCol(row, col) {
 var dat = sheet.getDataRange().getValues();
 return dat[row][col];
}

//セル[row][col]にvalを入れる
function setFromRowCol(val, row, col) {
 sheet.getRange(row , col).setValue(val);
}

//row行1列のデータ（内容）を取得
function getTodoCell(row) {
 return sheet.getRange(row , 1).getValue();
}

//row行2列のデータ（日付）を取得
function getDateCell(row) {
 return sheet.getRange(row , 2).getValue();
}

//row行3列のデータ（trigger）を取得
function getTriggerCell(row) {
 return sheet.getRange(row , 3).getValue();
}
```
スプレッドシート使って、リマインドしたいタスクを管理しています。
１列目にリマインド内容、２列目にリマインドする日付、３列目にtriggerIDを格納します。
GASには指定したタイミングで関数を実行するtriggerという機能があり、セットした時に割り振られる固有のIDがtriggerIDです。

###3.2 Triggerをセット、削除する関数
``` JavaScript:trigger.gs
function setTrigger(row, date) {
 var triggerDay = moment(date,'YYYY年MM月DD日H時m分').toDate(); 
 var trigger =  ScriptApp.newTrigger("remind").timeBased().at(triggerDay).create();
 setFromRowCol(trigger.getUniqueId(), row, 3);
}

function deleteTrigger(uniqueId) {
 var triggers = ScriptApp.getProjectTriggers();
 for(var i=0; i < triggers.length; i++) {
   if (triggers[i].getUniqueId() === uniqueId) {
     ScriptApp.deleteTrigger(triggers[i]);
   }
 }
}
```
ここは参考記事まんまです。
setTriggerは、格納された日時になると関数を呼び出すtriggerをセットします。

##3.3 リマインド本体

``` JavaScript:remind.gs
var moment = Moment.load();

  //ポストで送られてくるので、送られてきたJSONをパース
function doPost(e) {
  var json = JSON.parse(e.postData.contents);
  var replyText = 'null';
  
  var filledDo = false;
  var filledDate = true;
  if(lastRow()>0){
  filledDo = blankTF(lastRow(),1);
  filledDate = blankTF(lastRow(),2);
  }

  var userId = json.events[0].source.userId;
  //返信するためのトークン取得
  var replyToken= json.events[0].replyToken;
  if (typeof replyToken === 'undefined') {
    return;
  }
  var message = json.events[0].message.text; 

      switch (message) {
       case 'キャンセル':
         replyText = cancel();
         break;
        case '天気':
          replyText = '';
          push_message();
          break;
  case '確認':
     if (lastRow()>0) {
       replyText = confirm();
     } else {
       replyText = 'まだ何にも登録してないでしょ';
     }
     break;
       default: 
          if(message.match(/削除/)){
            replyText = del(message);
          }
         else if(filledDate){
           replyText = setDo(message);
           break;
         }else{
           replyText = setDate(message);
           break;
         }
         break;
 }

   // メッセージを返信    
  UrlFetchApp.fetch(line_endpoint, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': replyToken,
      'messages': [{
        'type': 'text',
        'text': replyText,
      }],
    }),
  });
  return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);

}


//データの削除
function del(m){
  m=m.replace('削除', '');
  if (m==''){
  return '「削除+リマインダー番号」で指定しなさい'
  }
   m = m.replace(/[Ａ-Ｚａ-ｚ０-９]/g, function (s) {
   return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
 });
  if(m>lastRow()){
    return '番号間違ってるわよ\nもう一回確認しなさい';
  }
  var s =  getTodoCell(m);
  var deltriggerID = getTriggerCell(m);
  deleteTrigger(deltriggerID);
  deleatRow(m);
  return '「'+s+'」'+'を削除したわ';
}

//データの確認
function confirm(){
  var replyText = 'まだ何にも登録してないでしょ';
      if(lastRow()>0){
        replyText = '';
   for (var i = 1; i <= lastRow(); i++) {
     replyText += i+'；'+ getDateCell(i) + 'に「' + getTodoCell(i) + '」\n';
   }
  }
  replyText+='以上よ';
  return replyText;
}

//入力のキャンセル
function cancel(){
  if(lastRow()>0){
  deleatRow(lastRow());
  return 'キャンセルしたわ\nまたなんかあったら言って';  
  }else{
  return 'キャンセルするものなんてないわよ';
  }
}

//日付の入力
function  setDate(m){
 // 全角英数を半角に変換
 m = m.replace(/[Ａ-Ｚａ-ｚ０-９]/g, function (s) {
   return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
 });
 var date = Moment.moment(m, 'M月D日H時m分', true).format('YYYY年MM月DD日H時m分');
 if (date === 'Invalid date') {
   var match = m.match(/\d+/g);
   if (match !== null) {
     date = Moment.moment().add(+match[0], 'minutes').format('YYYY年MM月DD日H時m分');
   }
 }
 if (date === 'Invalid date') {
   return '「何分後」「」'
 } 
 setTrigger(lastRow(), date);
 setFromRowCol(date,lastRow(), 2);
 return 'はいはい\n'+date + 'に連絡するわ';
}

//データの入力
function  setDo(m){
  if(RandomReturn()){
    return 'めんどくさぁ......';
  }
  setFromRowCol(m, lastRow()+1, 1);
return '「' + m + '」ね、いつ連絡すればいいのよ\n「キャンセル」って言ってくれればやめるけど';
}

function getnowDate(){
  return Moment.moment().format('YYYY年MM月DD日H時m分');
}

function remind(e){
  var nowDate = getnowDate();
  var replyText = 'リマインド登録されてないわよ';
  
    if(lastRow()>0){
   for (var i = 1; i <= lastRow(); i++) {
     replyText = getDateCell(i);
     if(getDateCell(i)==Moment.moment().format('YYYY年MM月DD日H時m分')){
       replyText = getTodoCell(i);
       var deltriggerID = getTriggerCell(i);
       deleteTrigger(deltriggerID);
       deleatRow(i);
     }
   }
  }
  
var postData = {
    "to": 'ユーザーID',
    "messages": [
      {
        "type": "text",
        "text": replyText+'\nふゆがわざわざリマインドしてあげたんだから、忘れるんじゃないわよ'
      }
    ]}
  
  var headers = {
    "Content-Type": "application/json",
    'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
  };
  
  var options = {
    "method": "post",
    "headers": headers,
    "payload": JSON.stringify(postData)
  };

  var response = UrlFetchApp.fetch("https://api.line.me/v2/bot/message/push", options); 
}

```
![S__70877212.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378758/5ef591e9-0233-e611-c29c-447bb833e0a3.jpeg)
![S__70877211 (2).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378758/6d53ece5-cf34-6c0a-8c68-8e7b8ae17aa1.jpeg)


送られてきたテキスト内容によって、天気予報のwether()、タスクを削除するdel()、登録してあるタスクを確認するconfirm()、現在の入力をキャンセルするcancel()、リマインドする日時を格納するsetDate()、リマインドするタスクを格納するsetDo()のどれかにシフトするようになっています。
ここでもタスクを登録する際、5%の確率で面倒くさがられます。
![S__70893571.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378758/73d6996e-6c9e-dfce-633c-bd89fa0e0d9e.jpeg)