無料(タダ)でゴミ出しLINEBOTを作ってみた

# はじめに
この度一人暮らしを始めたのですが、その居住地域のゴミの日が全然把握できないし、朝は意外と時間がないので(ギリギリまで寝ていたい)ついついゴミ出しを忘れてしまいがちでした。。。  
というわけで、LINEBOTを作ればいいじゃない！となった次第です。特徴としては、ただのゴミ出しを通知するだけのボットではなく、ちょっとしたレスポンスが返ってくるような機能もつけてみました。

## どういったゴミ出しLINEBOTか
毎日、曜日に合わせてゴミの種類を通知してくれるLINEBOTです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1939085/2cacda39-df03-c649-2a6c-306b879511de.png)
この機能を持つLINEBOTは、ググれば結構色々な方がやっています。僕はそれにflex message機能を使ったちょっと洒落たことがしたかったので、「ゴミの日一覧」というワードに対してリアクションしてくれる機能を実装してみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1939085/56163b7c-5839-2423-8809-0ec327ce25c3.png)
毎日通知してくれる機能とflex messageを返してくれる機能のいずれにも工夫した点がありまして。それは、**対象のスプレッドシートを編集すると、その編集した結果が反映される**という点です。ググって出てくるLINEBOTは、大体がコード側に直接ゴミ出しの情報を書き込んでいるため、もし居住地域が変わって、コードを編集するたびにデプロイするとなると不便だなぁと思いましたので、スプレッドシートとコードを連携させることで解消しました。上画像がそうですが、僕の居住地域がバレないようにスプレッドシートを適当に編集して、意味不明なflex messageが返ってくるようにしました。


## 必要な準備
準備するのはたった2つだけです。
* LINEアカウントの作成
    * 当然ですが、LINEアカウントが必要になります
* googleアカウントの作成
    * 今回はGAS(**G**oogle **A**pp **S**cript)というJavaScriptを模した(同じ？)言語を使います
    * googleDrive上にGASファイルとgoogleスプレッドシートを用意するだけです

もう少し細かい設定まで話します。  
### 1. LINE側の設定に関して
LINE側の設定と言っても普段使うLINEの設定ではなくて、[LINE Developers](https://developers.line.biz/ja/)というLINEBOTを作るには絶対に必要な設定です。以下画像がLINE Developersの画面になります。お持ちのLINEアカウントでログインまで済ましてください。
![スクリーンショット 2023-05-13 14.52.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1939085/78b048a0-1fe2-0705-c8ef-a635167c901d.png)
![スクリーンショット 2023-05-13 14.52.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1939085/712a7712-3699-56e6-00f5-47145f882064.png)
そうすると以下の画面に遷移すると思うので、ProviderをCreateしてください(僕は既に作ってあるので表示されていますが、初期状態は何も表示がされないはずです)。  
Createできたら、作成したProviderをクリック→`Create a new channel`をクリック→`Messaging API`を選択。
![スクリーンショット 2023-05-13 14.58.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1939085/215eef4b-3a10-af71-4563-8a32719d8a2d.png)
![スクリーンショット 2023-05-13 15.00.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1939085/a185d1fb-7036-6bcf-d818-a072f219d42b.png)
![スクリーンショット 2023-05-13 15.07.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1939085/7756d57c-80ab-4e9b-70b1-555726bad271.png)
`Messaging API`を選択後、色々とLINEBOTの名前だとか詳細説明とか色々設定が必要なので、それに従って適当に埋めてください。optionとある項目は必須でないので飛ばしてもらっても構いません。僕は`Category`の項目を「個人」にしました。埋める項目を全て埋めてcreateしてもらえると、無事にchannelが作成できているはずです。 これでLINE側の準備はOKです。

### google側の設定に関して
自身のgoogleDriveでGASファイルとgoogleスプレッドシートをそれぞれ新規作成してください。どちらも右クリックで新規作成できると思います。同じフォルダ内に作ってしまって問題ないです(GASファイルとgoogleスプレッドシートの名称はなんでも良いです)。
GASに関してですが、少し見づらいところにあります。以下画像を参考にしてください。
![スクリーンショット 2023-05-13 14.42.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1939085/3f9ebe1e-bade-bac5-9a35-fa290505cae3.png)
これでgoogle側の設定もOKです。  

次はコーディング周りの内容です。

## GAS編
以下に記載するGASコードを、先ほど作成してもらったGASファイルにコピペするだけです。GASファイルを開いて、以下のコードをそれぞれ`ファイルを追加`→`スクリプト`の順でそれぞれ書き込んでください。  

(本来GASの拡張子は`.gs`ですが、`.js`としたほうがコードが見やすいと思ったので`.js`を使っています。ですが、気にせずに`.gs`でファイルを追加するようにお願いします)

<details><summary>main.gs</summary>

```.js
//LINEAPIのトークン
const TOKEN = '置き換える';
//LINEのpushAPI
const PUSH_URL = 'https://api.line.me/v2/bot/message/push';
//LINEのreplyAPI
const REPLY_URL = "https://api.line.me/v2/bot/message/reply";
//googleSpreadSheetのURL
const SPREADSHEET_URL = '置き換える';
//googleSPreadSheetのシート名
const SHEET_NAME_GARBAGE = "シート1";

function doPost(e) {
  const responseLine = e.postData.getDataAsString();
  const responseLineJson = JSON.parse(responseLine).events[0];
  //イベントへの応答に使用するトークンを取得
  const replyToken = responseLineJson.replyToken;
  
  //メッセージイベントの場合
  if (responseLineJson.type == 'message') {
    replyMessage(responseLineJson, TOKEN, replyToken);
  }
}
```
</details>

* main.gs
    * TOKEN
        * 先ほど作成したLINEBOTのchannelページに行くと、「Messaging API」というタブがありますので、そこの最下部にある「Channel access token」にある「issue」をクリックして表示された文字列がTOKENになります。確か末尾が=だった気がします。
    * SPREADSHEET_URL
        * 先ほど作成したgoogleスプレッドシートのURLに注目してください。そのURLの真ん中あたり(<foo>https</foo>://docs.google.com/spreadsheets/d/ここ/edit#gid=0)が対象となる文字列になります。
    * SHEET_NAME_GARBAGE
        * googleスプレッドシートのシート名です。デフォルトで「シート1」になっていると思いますが、変える場合には、こちらを変更したシート名にしてください。

<details><summary>get_SpreadSheet.gs</summary>

```.js
//スプレッドシートからデータを獲得する
function getSpreadSheet(alpha, x, sheet_name) {
  var spreadsheet = SpreadsheetApp.openById(SPREADSHEET_URL);
  var sheet = spreadsheet.getSheetByName(sheet_name);
  var getVal = sheet.getRange(alpha + x).getDisplayValue();
  
  return getVal;
}

//スプレッドシートの最終行までの数を取得
function getLastNum(sheet_name) {
  var spreadsheet = SpreadsheetApp.openById(SPREADSHEET_URL);
  var sheet = spreadsheet.getSheetByName(sheet_name);
  var last = sheet.getLastRow();
  
  return last;
}
```
</details>

<details><summary>make_textInfo.gs</summary>

```.js
function make_textInfo() {
  var post_code = getSpreadSheet('A', 1, SHEET_NAME_ADDRESS);
  var address = getSpreadSheet('A', 2, SHEET_NAME_ADDRESS);
  var text = post_code + "\n" + address;
  var messages = [{
    "type": "text",
    "text": text,
  }]

  return messages;
}
```
</details>

<details><summary>make_GarbageInfo.gs</summary>

```.js
//スプレッドシートから辞書を作成
function makeGarbageDic() {
  var garbageDic = {};
  for(var i=1;i<getLastNum(SHEET_NAME_GARBAGE);i++) {
    var date = getSpreadSheet('A', i+1, SHEET_NAME_GARBAGE); //スプレッドシートから曜日の取得
    var garbageInfo_str = getSpreadSheet('B', i+1, SHEET_NAME_GARBAGE); //スプレッドシートからゴミの情報を取得
    var garbageInfo_list = garbageInfo_str.split('、'); //str -> 配列
    garbageDic[date] = garbageInfo_list;
  }

  return garbageDic;
}

//当日の曜日と第n週目を取得
function getDateNum() {
  //日付の取得
  var today = new Date();
  var day = today.getDate();
  //曜日の取得
  const dates = ['日','月','火','水','木','金','土'];
  var thisDay = today.getDay(); //曜日のインデックス(0-index)
  var date = dates[thisDay]; //曜日
  var week_n = Math.floor(day / 7); //第n週

  return {date, week_n};
}

//ゴミの情報(文字列)を作成
function makeGarbageInfo_str() {
  var garbageInfoDic = makeGarbageDic();
  var flag = true;
  var {date, week_n} = getDateNum();
  //第1,3週目ならフラグを立てる
  if(week_n % 2 != 0 && week_n < 5 && date == '木') flag = false;
  var garbageInfo = "";
  if(flag) garbageInfo = garbageInfoDic[date];

  return garbageInfo;
}

//いらすとや様にお世話になりますmm
function make_GarbageIconDic() {
  var dic = {
    "燃えるゴミ": "https://2.bp.blogspot.com/-SN24R4K0Omo/VtoffvSKf0I/AAAAAAAA4X0/AU8wIcv_4IU/s800/gomi_mark01_moeru.png",
    "燃えないごみ": "https://4.bp.blogspot.com/-M_FMq1RWBtM/VtofgVUhJiI/AAAAAAAA4X4/e2sddJ_poXU/s800/gomi_mark02_moenai.png",
    "古紙": "https://2.bp.blogspot.com/-JuG4yaiLI9o/VtofkMlVTUI/AAAAAAAA4Yc/dxFpn5LTdyg/s800/gomi_mark11_kami.png",
    "プラスチック": "https://3.bp.blogspot.com/-wMset6stW4U/Vtofh0BpVmI/AAAAAAAA4YI/C2geoLnn9qY/s800/gomi_mark06_plastic.png",
    "缶": "https://2.bp.blogspot.com/-jDPGGg_w214/VtofhCIc0dI/AAAAAAAA4YA/TN4N490_QMM/s800/gomi_mark04_can.png",
    "ビン": "https://1.bp.blogspot.com/-8nJCEO1bCX0/VtofhHaXeOI/AAAAAAAA4X8/1V6lVN524dM/s800/gomi_mark03_bin.png",
    "ペットボトル": "https://1.bp.blogspot.com/-F_hmSswVCz8/VtofhY4f40I/AAAAAAAA4YE/fg09VtK3tss/s800/gomi_mark05_petbottle.png"
  }

  return dic;
}

//ゴミの情報(flexMessage)を作成
function make_body() {
  var garbageDic = makeGarbageDic();
  var dates = Object.keys(garbageDic);
  var iconDic = make_GarbageIconDic();

  var bodyContents = [];
  for(var i=0;i<dates.length;i++) {
    var iconBody = [];
    const date = dates[i];
    for(var j=0;j<garbageDic[date].length;j++) {
      var offsetStart = (j+1) * 33;
      var iconURL = iconDic[garbageDic[date][j]];
      if(iconURL == null) continue;
      var childIconBody = {};
      childIconBody["type"] = "icon";
      childIconBody["url"] = iconURL;
      childIconBody["size"] = "3xl";
      childIconBody["position"] = "absolute";
      childIconBody["offsetStart"] = offsetStart.toString() + "px";
      childIconBody["offsetTop"] = "5px";
      iconBody.push(childIconBody);
    }

    var contentsDic = {}
    contentsDic["type"] = "box";
    contentsDic["layout"] = "baseline";
    if(iconBody.length != 0) {
      contentsDic["contents"] = [
        {
          "type": "text",
          "text": date,
          "size": "xxl"
        },
        ...iconBody
      ]
    } else {
      contentsDic["contents"] = [
        {
          "type": "text",
          "text": date,
          "size": "xxl"
        }
      ]
    }
    bodyContents.push(contentsDic);
  }

  return bodyContents;
}

//flex messageを作成
function make_flexMessage() {
  var hero_contents = make_body();
  var payload = [{
    "type": "flex",
    "altText": "ゴミの日一覧", //送信するテキストメッセージ
    "contents": {
      "type": "bubble",
      "styles": {
        "header": {
          "backgroundColor": "#ffdbed" //ピンク
        },
        "hero": {
          "backgroundColor": "#ffffff" //白色
        },
        "body": {
          "backgroundColor": "#ffffff" //白色
        },
        "footer": {
          "backgroundColor": "#ffdbed" //ピンク
        }
      },
      "header": {
        "type": "box",
        "layout": "vertical",
        "contents": [
          {
            "type": "text",
            "text": "ゴミの曜日一覧",
            "size": "xl",
            "align": "center"
          }
        ]
      },
      "hero": {
        "type": "image",
        "url": "https://4.bp.blogspot.com/-m9JWfaE6TKw/W0mF1cenVwI/AAAAAAABNU8/7a-nlu-eA34PTL8kTscE614f0C49XeETwCLcBGAs/s800/gomidashi_woman.png", //ゴミ出し byいらすとや
        "size": "full",
        "aspectRatio": "2:1"
      },
      "body": {
        "type": "box",
        "layout": "vertical",
        "contents": hero_contents
      },
      "footer": {
        "type": "box",
        "layout": "vertical",
        "contents": [{
          "type": "button",
          "style": "primary",
          "action": {
            "type": "uri",
            "label": "詳細情報はここをタップ",
            "uri": "ゴミ出しカレンダーのURL" //
          }
        }]
      }
    }
  }]

  return payload;
}
```
</details>


* make_GarbageInfo.gs
    * function makeGarbageInfo_str()
        * フラグを立てるコードがありますが、こちらは自分の居住地域に合わせて変更してください。基本的に毎週ゴミの日の曜日は同じですが、僕の住んでいる地域は第1,3週木曜だけの日、があるのでここでフラグを立てて判定しています。例えばそれが火曜日なら「date == '火'」にするなどしてください。また、第2,4週目の場合は、「week_n % 2 != 1」に変更してください。
    * function make_flexMessage()
        * 最下部あたりに「uri: 」とありますが、こちらには居住地域のゴミ出しカレンダー的なURLを貼るようにしています。不要な場合は「footer」ごと消してもらえれば、バグることなく削除できると思います。

<details><summary>push_GarbageInfo.gs</summary>

```.js
function push_GarbageInfo() {
  var garbageInfo = makeGarbageInfo_str(); //メッセージの取得
  var text = "";
  if(garbageInfo == "") text = "今日はゴミ出しがない日だね〜";
  else text = "今日は\n" + garbageInfo + "\nの日だね〜";

  //メッセージを作成
  var template = {
    "headers": {
      "Content-Type": "application/json; charset=UTF-8",
      "Authorization": "Bearer " + TOKEN
    },
    "method": "POST",
    "payload": JSON.stringify({
      "to": GROUP_ID,
      "messages": [{
        "type": "text",
        "text": text
      }]
    })
  }

  //メッセージを送信
  var response = UrlFetchApp.fetch(PUSH_URL, template);

  return response.getResponseCode();
}
```
</details>

* push_GarbageINfo.gs
    * function push_GarbageINfo()
        * この関数の上部のif文の箇所で`text=`という変数に文字列を入れています。これが、毎日通知してくれる文面になります。
        * ifがtrueの場合は、ゴミ出しがないパターンの文面になります。
        * ifがfalseの場合は、何かしらのゴミ出しがあるパターンの文面になります。
        * 通知してくれる文面を変更したい場合は、こちらを編集してください。

<details><summary>reply_GarbageInfo.gs</summary>

```.js
//特定の単語に対してリプライを送信
function replyMessage(event, token, replyToken) {
  //メッセージを取得
  const message = event.message;
  //本文を取得
  const text = message.text;

  if(text.match("ゴミの日一覧")) {
    var messages = make_flexMessage();
    var reply_message = {
      "headers": {
        "Content-Type": "application/json; charset=UTF-8",
        "Authorization": "Bearer " + token
      },
      "method": "POST",
      "payload": JSON.stringify({
        "replyToken": replyToken,
        "messages": messages
      })
    };

    UrlFetchApp.fetch(REPLY_URL, reply_message);
  }
}
```
</details>

* reply_GarbageInfo.gs
    * replyMessage()
        * 中央あたりに記述されている`"Authorization": "Bearer " + token`ですが、**わざとBearerの後ろに半角スペースを入れています**。要するに文字列の連結をしているということで、タイポじゃないです。こちらの半角スペースを消すだけで実行できなくなりますので、そのままでお願いしますmm

以上6つのGASファイルがあればとりあえず実行はできるはずです。

次にトリガーを設定します。トリガーとは、指定した時間帯や週、月の様々なペースで特定のGASファイル内の関数を実行してくれる機能です。
![スクリーンショット 2023-05-13 16.33.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1939085/d2e7d3c2-2d71-60b0-188b-879209dcab33.png)
上画像の位置にあるタイマーのマークをクリック→右下にある`トリガーを追加`から項目を埋めます。
* 実行する関数を選択
    * push_GarbageInfo
* 実行するデプロイを選択
    * Head
* イベントのソースを選択
    * 時間主導型
* 時間ベースのトリガーのタイプを選択
    * 日付ベースのタイマー
* 時刻を選択
    * 好きな時間帯を選択してください。僕は午前6〜7時にしています。おそらく全部そうだと思いますが、大体指定した時間帯のちょうど真ん中の時刻(僕の場合は6時30分くらい)で通知が来ます。

これでGASの操作は終了です。

## googleスプレッドシート編
スプレッドシートのシート1は、以下のような感じで記述します。
![スクリーンショット 2023-05-13 16.47.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1939085/b9ccb3a4-95d0-8641-2b62-4a9c8a17cfaa.png)

* A列
    * 曜日が記載されています。特に変更箇所はないです
* B列
   * `燃えるゴミ、燃えないごみ、古紙、プラスチック、缶、ビン、ペットボトル`のいずれかを記載する必要があります。複数列挙する場合は、`、`(=読点)で区切ってください(今気づいたけれど、ゴミがひらがなとカタカナあるの良くないな...)
   * 細かい内容は先ほどの[make_GarbageInfo.gs](https://qiita.com/T_death/items/d9c1ff31e69b42d75c6b#:~:text=make_textInfo.gs-,make_GarbageInfo,-.gs)を見ていただけると分かると思いますが、文字列と対応する画像のペアとなる辞書を持っているため、スプレッドシートには辞書のkeyと同じ文字列をB列に記載する必要があります(それが上に列挙した7つです)
* C列
    * A・B列はGASの動作に影響を与えますが、C列以降の列は影響を与えないので、備考などを書いています

これでgoogleスプレッドシートの操作は終了です。

## LINE Developers編
作成したchannelの「Messaging API」タブの中を見ると、`Webhook settings`という箇所があります。ここの「Webhook URL」にある`Edit`をクリックするとURLを入力を求められます。ここには、作成したGASファイルの右上部にある`デプロイ`ボタンから得られるURLを入力する必要があります。  
作成したGASファイルの`デプロイ`をクリック→「新しいデプロイ」を選択。「説明」は空欄で大丈夫ですし、「ウェブアプリ」はおそらくデフォルトで自分のメールアドレスになっているはずです。「アクセスできるユーザー」で**必ず全員**を選択してください(そうしないと動かないです)。その後`デプロイ`をクリック。  

<details><summary>もしも、アクセスを承認と出たら</summary>

`アクセスを承認`をクリック→自身のアカウントを選択→左下にある「詳細」をクリック→画面下部に表示される「安全ではないページへ移動」→`許可`をクリック。これでOKです。
</details>

「webアプリ」という表示の下にあるURLをコピー→先ほどのLINE developersのchannelの`Edit`に貼り付ける→`Verify`をクリック→「success」と表示されたらOKです。そして、そのすぐ下にある「Use webhook」をonにしてください(緑色にすれば良いです)。  

これでLINE Developersの操作は終了です。  
同時に、全てのやることが終わりました！何もなければ動作するはずです。

## おまけ
GAS編、googleスプレッドシート編、LINE Developoers編の3つが完了したら、動作できるはずです。今回実装した機能としては2つ。  
1つ目は、指定した時間にゴミ出しの情報を通知してくれる機能。2つ目は、「ゴミの日一覧」という言葉に反応して、ゴミ出しの情報を提供してくれる機能。この2つ目の機能ですが、毎回LINEで「ゴミの日一覧」と入力するのは面倒なので、リッチメニューというLINEの機能を活用することで、ボタン1つで勝手に文字を入力する機能を付与できます(企業の公式アカウントとかで使われている機能です)。そのためには、[LINE Official Account Manager](https://manager.line.biz/)から自身のLINEでログインして、リッチメニューを作成します。リッチメニューの実装は技術とか要らないはずなので割愛します。結構簡単にできます。  

# おわりに
僕は一人暮らしを始めたばかりなのでゴミ捨てを忘れがちなのですが、実はもう一つ忘れてしまうものがありまして。それは住所です(あ、郵便番号も)。Amazonで注文するにも、何か重要な資料を書く際にも、意外と住所を求められる場面が多いです。
というわけで、実は今回のLINEBOTには住所をレスポンスしてくれる機能もつけています。が、今回は紹介していません。もし追加したい場合も、特に追加の`.gs`ファイルが必要とかはなくて、特定の単語に対してリアクションを返す関数を実行するだけなので、試してみたい方はぜひやってみてください。  

今後は、生活周りの情報を整理してくれるBOTとして色々改良していくつもりです。