今日のランチを決めてくれるLINE Bot を作ったら盛り上がった話

:::note info
2022/4/2 最新のGASのUIでスクショを更新
:::

# はじめに
はじめまして。最近、GASにハマっていて、いろいろ作ってみたい今日この頃です。みなさまいかがお過ごしでしょうか？

さて、今回は最近作ったLINEBotの紹介をしたいと思います！

会社に出社したときに、毎回会社の人とランチに行くんですが、毎回お店を決めるのがまぁまぁ面倒ですよねーという話から、今日行くランチを決めてくれるLINE Botを作ってみました。
ソースコードも載せておきます。
びっくりするほど簡単に作成できるので、試してみてね！

# 作ったもの
LINEBotの下にボタンがあって、それらを押すと、あらかじめスプレッドシートに登録してある情報を検索し、位置情報を返してくれるBotです！
![スクリーンショット 2020-12-11 22.00.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/d801da3d-80cc-707b-8681-01de5c5f35a3.png)

QRコードはこちらです
![スクリーンショット 2020-12-10 11.07.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/c4457f45-3b6d-7eaa-c018-3fd7bc6137bc.png)


# 作り方
### 準備するもの
・Googleアカウント
・LINEのアカウント
・パソコン（macでもwindowsでも大丈夫です！）

### 準備しなくていいもの
開発環境（地味に準備に一番時間がかかりますよね。。。）
サーバー
なにかのインストール（依存関係でうまくいかないとかあるあるです。。。）
お金

## ①LINEBotの準備
・LINE公式の「[Messaging APIを始めよう](https://developers.line.biz/ja/docs/messaging-api/getting-started/#using-console)」を読んで「チャネル」を作成します
・チャネルが作成できたら「[ボットを作成する](https://developers.line.biz/ja/docs/messaging-api/building-bot/)」を読んで、チャネルアクセストークンを発行して、メモっておいてください。
「Webhook URLを設定する」は②のあとで設定します。

## ②GASの準備
自分のグーグルアカウントにログインし、適当にスプレッドシートを作ってください。
データは後ほど入れます。
![スクリーンショット 2020-12-11 22.18.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/2d09dda0-fab0-2bb3-0ce2-4a4f313dd042.png)

「拡張機能」から「AppScript」を選択します
![スクリーンショット 2022-04-02 18.14.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/a4fec243-73fa-c4e0-88fb-574836e4ee70.png)


とりあえず連携するために、単純なオウム返しのプログラムにします。
コピペで貼り付けて、1行目だけを書き換えましょう。

```js:コード.gs
var CHANNEL_ACCESS_TOKEN = '①で発行したチャネルアクセストークンを貼り付け'; 
var line_endpoint = 'https://api.line.me/v2/bot/message/reply';

//ポストで送られてくるので、ポストデータ取得
function doPost(e) {
  //JSONをパースする
  var json = JSON.parse(e.postData.contents);

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

  //送られたLINEメッセージを取得
  var user_message = json.events[0].message.text;  

  //送られたメッセージをそのままオウム返し
  var reply_messages = [user_message];
 
  // メッセージを返信
  var messages = reply_messages.map(function (v) {
    return {'type': 'text', 'text': v};    
  });
  UrlFetchApp.fetch(line_endpoint, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': reply_token,
      'messages': messages,
    }),
  });
  return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}
```

準備が出来たら、デプロイ（WEBアプリケーションとして公開）します
![スクリーンショット 2022-04-02 18.17.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/29d13da4-bac2-830c-ea09-d82b0b38efb7.png)

![スクリーンショット 2022-04-02 18.17.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/91de926b-b8ef-009c-9427-a65a2ab79513.png)

種類の選択 => ウェブアプリ
次のユーザーとして実行 => 自分
アクセスできるユーザー => 全員
![スクリーンショット 2022-04-02 18.18.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/8ed1dbbb-f427-90e1-67b9-c5e6f679fc5e.png)

アクセスを承認をクリック
![スクリーンショット 2022-04-02 18.18.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/88cf6a57-6f1b-946b-104f-13dd50eddb81.png)

認証するアカウントを選択
![スクリーンショット 2022-04-02 18.19.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/1c266379-b44e-2d12-6326-db5142ed9308.png)

警告が出ますが、「詳細」を押して「(安全ではないページ)に移動」のリンクをクリック
* リスクがあるのでデベロッパーが第三者の場合は十分注意してください
![スクリーンショット 2022-04-02 18.19.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/ffe0e71d-ad7e-33ce-03fa-dacb70bd739b.png)

許可をクリック
![スクリーンショット 2022-04-02 18.19.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/16d4cb9b-bfb5-aa94-682f-273f21118e47.png)

この画面までいったらURLをコピーしておきます
![スクリーンショット 2022-04-02 18.19.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/c2ed21bc-6983-dd18-a824-3de63de7dc44.png)

## ①LINEBotの準備の続き
webhook url にURLを貼り付け「update」と「verify」を行います
![スクリーンショット 2020-12-04 1.46.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/0f6eee81-2eda-b1d6-d5b9-96a166042060.png)

うまく出来ていれば、入力された値をそのまま返すBotになっています。
![スクリーンショット 2020-12-07 7.06.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/17b74d0a-6fd6-72a8-b355-1043b9073e9a.png)

## ②にちゃんとした実装

実装はシンプルに以下のようになっています。
（スプレッドシートのシート名を「お店」に変更してください）

```js:コード.gs
//ポストで送られてくるので、ポストデータ取得
function doPost(e) {
  //JSONをパースする
  var json = JSON.parse(e.postData.contents);

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

  //送られたLINEメッセージを取得
  var user_message = json.events[0].message.text;  

  // シート名を指定してsheetとして持っておきます
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('お店');

  var target_rows
  var address
  var messages
  
  if(user_message == 'おまかせ'){
    // 「おまかせ」というメッセージが送られた場合は、シートの3列目の「おすすめ度」で検索を行います
    // searchDataByScoreという関数を作り、69点より上の点数のお店を検索します
    target_rows = searchDataByScore(sheet, 69, 3);
    target_rows = randomSelect(target_rows) // 検索結果からランダムに1つを選択する関数を作って呼び出します

    // シートからお店の住所の情報を取り出します
    address = sheet.getRange(target_rows[0],4).getValue()
    // 住所がなかったらお店の名前だけ表示します
    if(!address){
      messages = [{'type': 'text', 'text': "店名：" + sheet.getRange(target_rows[0],1).getValue()}];
    } else {
      // 住所から緯度軽度を計算します
      var
        geocoder = Maps.newGeocoder() // Creates a new Geocoder object.
        , geocoder = geocoder.setLanguage('ja') // Use Japanese
        , response = geocoder.geocode(address).results[0]; // ets the approximate geographic points for a given address.
      // ユーザーに返すメッセージを組み立てます
      messages = [{
          "type": "location",
          "title": sheet.getRange(target_rows[0],1).getValue(),
          "address": address,
          "latitude": response.geometry.location.lat,
          "longitude": response.geometry.location.lng
        }];
    }    
  }else{
    target_rows = searchData(sheet, user_message, 2); // 2列目は「ジャンル」
    target_rows = randomSelect(target_rows) // ランダムに一つを選択
    
    address = sheet.getRange(target_rows[0],4).getValue()
    if(!address){
      messages = [{'type': 'text', 'text': "店名：" + sheet.getRange(target_rows[0],1).getValue()}];
    } else {
      var
        geocoder = Maps.newGeocoder() // Creates a new Geocoder object.
        , geocoder = geocoder.setLanguage('ja') // Use Japanese
        , response = geocoder.geocode(address).results[0]; // ets the approximate geographic points for a given address.

      messages = [{
          "type": "location",
          "title": sheet.getRange(target_rows[0],1).getValue(),
          "address": address,
          "latitude": response.geometry.location.lat,
          "longitude": response.geometry.location.lng
        }];
    }
  }
    
  UrlFetchApp.fetch(line_endpoint, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': reply_token,
      'messages': messages,
    }),
  });
  return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}

// 文字でお店を検索します
function searchData(sheet, val, col){
  var dat = sheet.getDataRange().getValues();
  var target_rows = []; 
  for(var i = 1; i < dat.length; i++) {
    if(dat[i][col-1].indexOf(val) != -1){
      target_rows.push(i+1);
    }
  }
  return target_rows;
}

// 点数でお店を検索します
function searchDataByScore(sheet, score, col){
  var dat = sheet.getDataRange().getValues();
  var target_rows = []; 
  for(var i = 1; i < dat.length; i++) {
    if(dat[i][col-1] > score){
      target_rows.push(i+1);
    }
  }
  return target_rows;
}

function randomSelect(array){
  var randomSelect = [];
  while(randomSelect.length < 1 && array.length > 0){
    const rand = Math.floor(Math.random() * array.length);
    randomSelect.push(array[rand]);
    array.slice(rand, 1); // 結果が重複しないように、元の配列からは削除
  }
  return randomSelect;
}
```

（汎用的に作ったため、多少冗長なところがあります）

デプロイを更新します
ファイル未保存のときは、オレンジの丸が付くので保存してください
![スクリーンショット 2022-04-02 18.39.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/a730b410-7bc2-a32f-2587-96b73c9f14fa.png)

デプロイを管理を選択
![スクリーンショット 2022-04-02 18.39.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/a4968569-1340-9efb-201a-ed9989d8ef0c.png)

編集をクリック
![スクリーンショット 2022-04-02 18.40.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/d7a6876e-434f-e9e2-1807-a26094f28716.png)

新バージョンを選択し、あとは初回デプロイ時と同様に認証を行う
![スクリーンショット 2022-04-02 18.40.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27192/c079453c-15a3-296f-b74d-524ca2629e28.png)
URLは変更されないので、LINEbotのWebhookのURLは変更不要です


# 周りの人の反応
・お店で迷うことがなくなりました！
・エンジニアではない人からは、どういう風に動いているのか質問されるなどプログラムに興味を持ってもらえました！
・スプレッドシートにランチのお店を書き出すと、あれも追加しなきゃ、これも追加しなきゃとワイワイ盛り上がった！
・こういうのはデータを集めるのが大変なんだけど、社内にスプレッドシートを公開したことで、協力してくれる人がたくさん集まって、自発的に編集してくれるようになった
・自分はこういう機能が欲しい、など意見をもらえました！（やっぱり食に関することは食いつきがいいですね！）

# おわりに
プロトタイピングで何か作るのに、GASは本当に考えることが減ってとても素早く実装ができました！（今回の実装も2時間程度です）

次のプロトアウトスタジオのアドベントカレンダーははちょっと空いて12/14に記事公開だそうです！