13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

プロトアウトスタジオAdvent Calendar 2020

Day 11

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

Last updated at Posted at 2020-12-11

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

はじめに

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

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

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

作ったもの

LINEBotの下にボタンがあって、それらを押すと、あらかじめスプレッドシートに登録してある情報を検索し、位置情報を返してくれるBotです!
スクリーンショット 2020-12-11 22.00.45.png

QRコードはこちらです
スクリーンショット 2020-12-10 11.07.03.png

作り方

準備するもの

・Googleアカウント
・LINEのアカウント
・パソコン(macでもwindowsでも大丈夫です!)

準備しなくていいもの

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

①LINEBotの準備

・LINE公式の「Messaging APIを始めよう」を読んで「チャネル」を作成します
・チャネルが作成できたら「ボットを作成する」を読んで、チャネルアクセストークンを発行して、メモっておいてください。
「Webhook URLを設定する」は②のあとで設定します。

②GASの準備

自分のグーグルアカウントにログインし、適当にスプレッドシートを作ってください。
データは後ほど入れます。
スクリーンショット 2020-12-11 22.18.35.png

「拡張機能」から「AppScript」を選択します
スクリーンショット 2022-04-02 18.14.35.png

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

コード.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

スクリーンショット 2022-04-02 18.17.56.png

種類の選択 => ウェブアプリ
次のユーザーとして実行 => 自分
アクセスできるユーザー => 全員
スクリーンショット 2022-04-02 18.18.27.png

アクセスを承認をクリック
スクリーンショット 2022-04-02 18.18.54.png

認証するアカウントを選択
スクリーンショット 2022-04-02 18.19.01.png

警告が出ますが、「詳細」を押して「(安全ではないページ)に移動」のリンクをクリック

  • リスクがあるのでデベロッパーが第三者の場合は十分注意してください
    スクリーンショット 2022-04-02 18.19.15.png

許可をクリック
スクリーンショット 2022-04-02 18.19.23.png

この画面までいったらURLをコピーしておきます
スクリーンショット 2022-04-02 18.19.35.png

①LINEBotの準備の続き

webhook url にURLを貼り付け「update」と「verify」を行います
スクリーンショット 2020-12-04 1.46.05.png

うまく出来ていれば、入力された値をそのまま返すBotになっています。
スクリーンショット 2020-12-07 7.06.09.png

②にちゃんとした実装

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

コード.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

デプロイを管理を選択
スクリーンショット 2022-04-02 18.39.44.png

編集をクリック
スクリーンショット 2022-04-02 18.40.13.png

新バージョンを選択し、あとは初回デプロイ時と同様に認証を行う
スクリーンショット 2022-04-02 18.40.20.png
URLは変更されないので、LINEbotのWebhookのURLは変更不要です

周りの人の反応

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

おわりに

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

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

13
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?