LoginSignup
43
33

More than 3 years have passed since last update.

Messaging APIとGASを使ったLINE Botでグループチャットの活発化

Last updated at Posted at 2020-05-03

1行概要

Messaging API、GAS、Googleカレンダーの組み合わせで、予定の共有と匿名で投稿ができるBotを実現した。

はじめに

LINEのグループチャット、特にそこまで親しくなかったり、知らない人も入っていたり、大人数だったりするとラインするのに躊躇しますよね。

だったら「Botにラインさせよう」ってことで、LINE Messaging APIGoogle Apps Scriptを使ってグループチャットに匿名で投稿+予定の共有ができるLINE Botを開発しました。
ちなみに、私は4月から勤務している会社の同期グループ向けに作りました。そのため、社名や氏名など画像の一部に隠しを入れています。

完成例

まず、実際の動作の様子を機能別に紹介します。

機能1.予定の追加

グループで共有したい予定を追加します。

①LINE Bot(以下、ボット)と1対1の個人チャット(以下、個チャ)の画面は次の通り。「予定の追加」と「匿名で投稿」というメニューが表示されています。
Screenshot_20200430-071639.png

②「予定の追加」をタップすると、予定の日付、開始時刻、終了時刻、名前と入力案内がされます。最後に予定の確認がされ、「はい」と入力すると予定の追加が完了します。
入力案内のいずれかのフェーズで「やめる」もしくは予定の確認で「いいえ」と入力すると、最初からやり直すようになります。なお、ボットが参加しているグループチャット(以下、グルチャ)ではメニューは表示されませんが、「予定の追加」と手入力すれば同様の手順で予定の追加を行うことはできます。
Screenshot_20200430-071600.png
Screenshot_20200430-071609.png

③追加した予定はGoogleカレンダーに追加されます。
予定の追加.JPG

機能2.予定の通知

グループで共有したい予定の通知をします。
機能1で追加した予定が通知されています。通知のタイミングは任意で設定ができ、このボットの場合は、予定の前日20~21時に通知するようにしています。予定がない場合は通知しません。
Screenshot_20200430-073015.png

機能3.匿名で投稿

グルチャに匿名で投稿ができます。

①「匿名で投稿」をタップすると、投稿内容を入力する案内が表示されます。投稿内容が確認され、「はい」と入力すると予定の追加が完了します。
投稿内容を入力するフェーズで「やめる」もしくは投稿内容の確認で「いいえ」と入力すると、最初からやり直すようになります。なお、ボットが参加しているグルチャではメニューは表示されませんが、「匿名で投稿」と手入力すれば同様の手順で匿名で投稿を行うことはできます。もはや匿名ではありませんが。
Screenshot_20200430-071806.png

②投稿した内容は「匿名投稿:(投稿内容)」として、グルチャにボットが代わりに投稿してくれます。
Screenshot_20200430-073001.png

初心者に向けて

次に、本記事のボット実現のために使ったものを初心者に向けてできるだけ噛み砕いて説明します。
実際は私自身が立ち返って復習をできることが目的です。

LINE Messaging API

ボットを実現する方法はいくつかありますが、基本的にはMessaging APIを使うことになると思います。

「機能2.予定の通知」に限れば、LINE Notifyを使うことでシンプルに実現することができます。が、ボットとやり取りをしたり、グルチャに投稿したりと、より自由度や拡張性が高いのはMessaging APIです。個人的にはボットに任意の名前やアイコンを付けたかったという理由もありますが。

基本的な処理の流れは次の通り。
ちなみに、Webhookとは、WebコールバックやHTTPプッシュAPI、リバースAPIとも呼ばれ、Webアプリケーション(ここで言う、LINEプラットフォーム)でイベントが実行されたときに外部サービス(ここで言う、ボットサーバー)とHTTPで通信する仕組みです。
ちなみにちなみに、API(Application Programming Interface)とは、プログラムの機能やデータなどを外部のプログラムから呼び出して利用するための手順やデータ形式などを定めたもののことです。

1.ユーザーが、LINE公式アカウント(=ボット)にメッセージを送信します。
2.LINEプラットフォームからボットサーバーのWebhook URLに、Webhookイベントが送信されます。
3.Webhookイベントに応じて、ボットサーバーからユーザーにLINEプラットフォームを介して応答します。

messaging-api-architecture.f40bffbb.png

Messaging APIで主にできることは次の通り。
本記事のボットでは「応答メッセージを送る」「プッシュメッセージを送る」「ユーザープロフィールを取得する」を使っています。

・応答メッセージを送る
・プッシュメッセージを送る
・さまざまなタイプのメッセージを送る
 ・テキストメッセージ
 ・スタンプメッセージ
 ・画像メッセージ
 ・動画メッセージ
 ・音声メッセージ
 ・位置情報メッセージ
 ・イメージマップメッセージ
 ・テンプレートメッセージ
 ・Flex Message
・ユーザーが送ったコンテンツを取得する
・ユーザープロフィールを取得する
・グループチャットに参加する
・リッチメニューを使う

Tips
- Messaging API
- LINE Notify
- 【GAS】Googleカレンダーの予定をLINEで受け取る on Qiita
- Webhookって何?を子どもでもわかるように描いてみた

Google Apps Script

APIの次は、Webhookを受け取るボットサーバーと処理を記述するプラットフォームを決める必要があります。
Messaging APIではJava、PHP、Pythonなど様々なプラットフォームで処理を記述することができますが、本記事ではGoogle Apps Script(以下、GAS)を利用します。
GASはその名の通りGoogleが提供するスクリプト開発プラットフォームで、本記事でGASを採用する理由は大きく2つあります。1つ目は予定の追加・管理・取得をするGoogleカレンダーと容易に連携ができるため、2つ目は別にサーバーを用意する必要がないためです。

Tips
- Google Apps Script
- Google Apps Script は何が強くてどんなときに使うべきか自分なりのプラクティスをまとめてみた

簡易プログラムの作成

ボット開発初期段階の慣例となりつつある、「オウム返しボット」の作成手順を解説します。

手順1.チャネルの作成
まずはチャネル(=ボットの外枠)の作成です。

LINE Bussines IDから「LINEアカウントでログイン」をします。
LINEビジネスIDとは、LINEが提供するビジネス向けまたは開発者向けの各種管理画面にログインができる共通認証システムで、通常のLINEアカウントのログイン情報(IDとパスワード)でログインが可能です。
キャプチャaaaaaaaa.JPG

②「プロバイダー」(ボットの開発・提供者の意)を作成します。
キャプチャnuief.JPG

③「チャネル設定」の中から「Messaging API」を選択しチャネルを作成します。
「チャネル名」(=ボット名)「チャネル説明」「大業種」「小業種」「メールアドレス」を入力し、各種利用規約に同意します。
キャプチャncei.JPG
キャプチャbyuaew.JPG

手順2.GASプロジェクトの新規作成
処理を記述するプロジェクトを作成します。

Googleドライブにアクセスします。
このときGoogleアカウントにログインする必要があり、アカウントを所持していない人は新たに作成する必要があります。

②ログイン後、Googleドライブ画面左上の「新規」から、「その他」>「Google Apps Script」を選択します。
キャプチャnferuiJPG.JPG

手順3.処理の記述
実際にボットの処理を記述していきます。

①手順2で作成したGASプロジェクトにもともと記述されているコード(myFunction)は削除し、次のコードを入力してください。以下、プロジェクト名「TEST」、GASファイル名「test.gs」となっています。
コードの細かい中身はこの際気にしなくても大丈夫です。これがボットの基本型になります。
ちなみに、JSON(JavaScript Object Notation)とは文字ベースのデータ送受信用フォーマットです。ボットのメッセージやメッセージ送信のためのフォーマットとして利用しています。

test.gs
// Messaging APIのチャネルアクセストークン
const CHANNEL_ACCESS_TOKEN = "【※】";

/*
 * ボットにイベントが発生したときの(メイン)処理
 * (例)メッセージの受信、フォローされた、アンフォローされた
 */
function doPost(e) {
  const events = JSON.parse(e.postData.contents).events;
  events.forEach((event) => {
    // イベントがメッセージの受信だったとき
    if(event.type == "message") {
      reply(event);
    }
 });
}

/*
 * オウム返しをする処理
 */
function reply(e) {
  // 受信したメッセージをそのまま送信
  const message = {
    "replyToken": e.replyToken,
    "messages": [
      {
        "type": "text",
        "text": e.message.text
      }
    ]
  };
  // 送信のための諸準備
  const replyData = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN
    },
    "payload": JSON.stringify(message)
  };
  // JSON形式でAPIにポスト
  UrlFetchApp.fetch("https://api.line.me/v2/bot/message/reply", replyData);
}

②手順1で作成したチャネルの「Messaging API設定」の最下部にある「チャネルアクセストークン」をコピーし、①のコード中【※】と置き換えます。
チャネルアクセストークンとは、作成したチャネルのMessaging APIを呼び出すときに必要なキーワードのようなものです。他の人に見られないように注意してください。
キャプチャncuek.JPG

手順4.処理の公開
GASプロジェクトを公開し、記述した処理がボットサーバーの役割を果たすようにします。

①処理を記述したプロジェクトの「公開」から「ウェブアプリケーションとして導入...」を選択します。
キャプチャnewfui.JPG

②「Deply as web app」の小ウィンドウが表示されたら、「Project version:」に「New」を、「Who has access to the app:」に「Anyone, even anonymous」を選択してください。
これらを正しく選択しないと、プロジェクトの更新がされなかったり、ボットからボットサーバーにアクセスできなかったりします。
キャプチャnwujie.JPG

③プロジェクトを初めて公開しようとする際には確認の小ウィンドウが表示されます。「許可を確認」で承認してください。
キャプチャc hwe.JPG

④環境によっては下のように「このアプリは確認されていません」という画面が表示されるかもしれません。
問題ありませんので、最下部の「【プロジェクト名】(安全ではないページ)に移動」を選択します。
キャプチャcwen.JPG

⑤「【プロジェクト名】がGoogleアカウントへのアクセスをリクエストしています」と表示されますので、「許可」を選択してください。
キャプチャ dsjk.JPG

⑥次のように「Deploy as web app」として、「Current web app URL:」が表示されれば公開完了です。
キャプチャcmw.JPG

手順5.チャネルのWebhook指定
チャネルのWebhookに、作成したGASプロジェクトを指定します。

①手順1で作成したチャネルの「Messaging API設定」の中央「Webhook設定」の「Webhook URL」に手順4⑥で表示された「Current web app URL:」のURLを入力し、「Webhookの利用」をオンにします。
キャプチャc wei.JPG

②「Webhook URL」の「検証」を行い、「成功」と表示されればWebhookの指定完了です。
私見ですが、Webhookの反映の多少のラグがあるときがあるように思えます。「成功」以外のエラーメッセージが出た場合は、焦らずここまでの手順を再度確認してみてください。
キャプチャc jk.JPG

手順6.ボットの動作確認
最後にボットの動作を確認してみましょう。

①手順1で作成したチャネルの「Messaging API設定」の「QRコード」から友達登録をします。
キャプチャcd.skJPG.JPG

②適当なメッセージを送信し、同じメッセージが返ってくれば成功です。画像では「こんにちは!」とラインして、「こんにちは!」と返って来ているのが分かります。
このとき、フォローしたときの「あいさつメッセージ」とラインする度にくる「応答メッセージ」が表示されていますが、別で任意に設定・解除ができます。
85658.jpg

Tips
- JSONってなにもの?
- チャネルアクセストークン

ここまでが、本記事のボット実現のために使ったものの基本的な説明になります。

実装

ようやく本題です。自分でも長くなってしまったと反省しています。実際に実装したボットについて説明していきます。
なお、コードの大部分はLINE BOTからGoogleカレンダーの予定の取得・追加を行うを参考にさせていただいています。

前準備

処理を記述する前に次の準備をする必要があります。

①新規チャネル、GASプロジェクトの作成
新規にボットを作るため、チャネルとGASプロジェクトを作成します。細かな手順は前述した通りです。

②グルチャへの参加許可
チャネルを作成したら、「Messagin API設定」中の「グループ・複数人チャットへの参加を許可する」を「有効」にします。

③カレンダーIDの取得
予定の追加・管理・取得を行う任意のGoogleカレンダーを作成します。(既存のものでも構いません。)
用意したカレンダーIDをコピーしておきます。
キャプチャ ch.JPG

処理の記述

次のコードが実際のボットの処理の中身です。

8thkun.gs
// Messaging APIのチャネルアクセストークン
const CHANNEL_ACCESS_TOKEN = "【※1】";
// 予定の追加・管理・取得をするカレンダーID
const CALENDER_ID = "【※2】";
// グルチャのグループID
const GROUP_ID = "【※3】";

const dateExp = /(\d{2})\/(\d{2})\s(\d{2}):(\d{2})/;
const dayExp = /(\d+)[\/](\d+)/;
const hourMinExp = /(\d+)[:時](\d+)*/;

/*
 * ボットにイベントが発生したときの(メイン)処理
 */
function doPost(e) {
  let replyToken = JSON.parse(e.postData.contents).events[0].replyToken;
  let lineType = JSON.parse(e.postData.contents).events[0].type
  let userMessage = JSON.parse(e.postData.contents).events[0].message.text;  
  // フォロー、アンフォローイベントは今回無視
  if(typeof replyToken === "undefined" || lineType === "follow" || lineType === "unfollow") {
    return;
  }
  // ボットの状態遷移をtypeという名のキャッシュで管理
  let cache = CacheService.getScriptCache();
  let type = cache.get("type");

  // 状態なし
  if(type === null) {
    // 「予定の追加」メッセージを受け取ったとき
    if(userMessage === "予定の追加") {
      cache.put("type", 1);
      reply(replyToken, "予定の日付を教えてください!\n形式指定:『1/23』『1月23日』\nキャンセル:『やめる』と入力");
    // 「匿名で投稿」メッセージを受け取ったとき
    } else if(userMessage === "匿名で投稿") {
      cache.put("type", 10);
      reply(replyToken, "グルチャに匿名でラインします!投稿内容を教えてください!\nキャンセル:『やめる』と入力");
    // メッセージの投稿に必要なグループIDの取得(後準備で説明)
    } else if(userMessage === "getGroupId") {
      // reply(replyToken, JSON.parse(e.postData.contents).events[0].source.groupId);
    }
  // 状態あり
  } else {
    if(userMessage === "やめる") {
      cache.remove("type");
      reply(replyToken, "キャンセルしました");
      return;
    }

    // 状態1~5は予定の追加、状態10~11は匿名で投稿
    switch(type) {
      // 予定の日付
      case "1":
        let [matched, month, day] = userMessage.match(dayExp);
        cache.put("type", 2);
        cache.put("month", month);
        cache.put("day", day);
        reply(replyToken, "次に開始時刻を教えてください!\n形式指定:『1:23』『12時』『12時34分』\nキャンセル:『やめる』と入力");
        break;
      // 予定の開始時刻
      case "2":
        let [matched, startHour, startMin] = userMessage.match(hourMinExp);
        cache.put("type", 3);
        cache.put("start_hour", startHour);
        if (startMin == null) startMin = "00";
        cache.put("start_min", startMin);
        reply(replyToken, "次に終了時刻を教えてください!\n形式指定:『1:23』『12時』『12時34分』\n\キャンセル:『やめる』と入力");
        break;
      // 予定の終了時刻
      case "3":
        let [matched, endHour, endMin] = userMessage.match(hourMinExp);
        cache.put("type", 4);
        cache.put("end_hour", endHour);
        if (endMin == null) endMin = "00";
        cache.put("end_min", endMin);
        reply(replyToken, "最後に予定の名前を教えてください!\nキャンセル:『やめる』と入力");
        break;
      // 予定の名前
      case "4":
        cache.put("type", 5);
        cache.put("title", userMessage);
        let [title, startDate, endDate] = createEventData(cache);
        reply(replyToken, toEventFormat(title, startDate, endDate) + "\n\nで間違いないでしょうか?よろしければ『はい』を、やり直す場合は『いいえ』と入力してください!");
        break;
      // 予定の確認
      case "5":
        cache.remove("type");
        if (userMessage === "はい") {
          let [title, startDate, endDate] = createEventData(cache);
          CalendarApp.getCalendarById(CALENDER_ID).createEvent(title, startDate, endDate);
          reply(replyToken, "予定を追加しました!");
        } else {
          reply(replyToken, "お手数ですが最初からやり直してください");
        }
        break;

      // 匿名で投稿する内容
      case "10":
        cache.put("type", 11);
        cache.put("post", userMessage);
        let message = createPost(cache);
        reply(replyToken, message + "\n\nで間違いないでしょうか?よろしければ『はい』を、やり直す場合は『いいえ』と入力してください!\nいたずらや誹謗中傷は絶対にやめてください");
        break;
      // 投稿する内容の確認
      case "11":
        cache.remove("type");
        if (userMessage === "はい") {
          let message = createPost(cache);
          pushPost("匿名投稿:\n" + message);
        } else {
          reply(replyToken, "お手数ですが最初からやり直してください");
        }
        cache.remove("post");
        break;
    }
  }
}

/*
 * 追加する予定の日付、開始時刻、終了時刻、名前の作成・保管
 */
function createEventData(cache) {
  const year = new Date().getFullYear();
  const title = cache.get("title");
  const startDate = new Date(year, cache.get("month") - 1, cache.get("day"), cache.get("start_hour"), cache.get("start_min"));
  const endDate = new Date(year, cache.get("month") - 1, cache.get("day"), cache.get("end_hour"), cache.get("end_min"));
  return [title, startDate, endDate];
}

/*
 * 追加する予定の確認のためのフォーマット作成
 */
function toEventFormat(title, startDate, endDate) {
  const start = Utilities.formatDate(startDate, "JST", "MM/dd HH:mm");
  const end = Utilities.formatDate(endDate, "JST", "MM/dd HH:mm");
  const str = title + ": " + start + " ~ " + end;
  return str;
}

/*
 * 匿名で投稿する内容の作成・保管
 */
function createPost(cache){
  const post = cache.get("post");
  return post;
}

/*
 * ボットのメッセージ応答
 */
function reply(replyToken, message) {
  const url = "https://api.line.me/v2/bot/message/reply";
  UrlFetchApp.fetch(url, {
    "headers": {
      "Content-Type": "application/json; charset=UTF-8",
      "Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN,
    },
    "method": "post",
    "payload": JSON.stringify({
      "replyToken": replyToken,
      "messages": [{
        "type": "text",
        "text": message,
      }],
    }),
  });
  return ContentService.createTextOutput(JSON.stringify({"content": "post ok"})).setMimeType(ContentService.MimeType.JSON);
}

/*
 * ボットからのポスト処理
 */
function pushPost(body){
  const url = "https://api.line.me/v2/bot/message/push";

  // 指定のグルチャにPOSTする
  UrlFetchApp.fetch(url, {
    "headers": {
      "Content-Type": "application/json; charset=UTF-8",
      "Authorization": "Bearer " + CHANNEL_ACCESS_TOKEN,
    },
    "method": "post",
    "payload": JSON.stringify({
      "to": GROUP_ID,
      "messages":[{
        "type": "text",
        "text": body,
      }]
     })
   })
}

/*
 * 通知する予定の取得
 */
function getEvents() {
  let date = new Date();
  date.setDate(date.getDate() + 1);
  const events = CalendarApp.getCalendarById(CALENDER_ID).getEventsForDay(date);

  if (events.length !== 0) {
    let body = "明日の予定は\n";
    events.forEach(function(event) {
      const title = event.getTitle();
      const start = toHHmm(event.getStartTime());
      const end = toHHmm(event.getEndTime());
      body += "" + title + ": " + start + " ~ " + end + "\n";
    });
    body += "です!";

    pushPost(body);
  }
}

/*
 * 時刻フォーマットの作成
 */
function toHHmm(date){
  return Utilities.formatDate(date, "JST", "HH:mm");
}

機能別の処理の詳細

冒頭で紹介した機能別の処理の中身を説明していきます。

機能1.予定の追加
本記事のボットは特定のメッセージを受け取ったことをトリガーに、状態遷移を管理するtypeという名のキャッシュの中身を変化させて案内をします。
機能1の場合は、その中身の通り「予定の追加」というキーワードをトリガーにしています。トリガーが発動すると案内の進み具合に応じてtypeを1~5の順に変化させ、案内の課程で入力させた予定の日付、開始時刻、終了時刻、名前をもとにCalendarApp.getCalendarById(CALENDER_ID).createEvent(title, startDate, endDate)で指定のGoogleカレンダーに予定を追加します。

機能2.予定の通知
対して機能2は独立のトリガーで動作します。
GASには任意のタイミング任意の関数を実行させる機能が付いています。本記事のボットでは、追加された予定の前日20~21時の間にグルチャに予定の通知をすることになっていました。具体的には、後述する「日付ベースのタイマー」設定でgetEvents関数を実行することで予定の通知を行います。

機能3.匿名で投稿
機能1に同じで、「匿名で投稿」というキーワードをトリガーにしています。トリガー発動後はtypeが10~11の順に変化し、最終的にpushPost関数でグルチャにボットが匿名で投稿します。

後準備

処理を記述したら、細かい後準備をしていきます。

①チャネルアクセストークン【※1】の入力
例の如く。

②カレンダーID【※2】の入力
前準備で取得したカレンダーIDを【※2】と置き換えます。

③グループID【※3】の取得・入力
ここが少しややこしいです。
メッセージに応答をするのではなく、ボットから直接グルチャに投稿をするためには投稿するグルチャのグループIDが必要になります。
グループIDを取得する方法はいくつかありますが、本記事ではボットがグルチャに参加後、特定のキーワードをトリガーにグループIDを応答するという方法でグループIDを取得します。具体的には、コード中の次の内容が該当します。まず、ボットをグルチャに参加させ、reply以下のコメントアウトを外した後getGroupIdというキーワードでグループIDを応答させます。グループID取得後、reply以下は再びコメントアウトすることで後にgetGroupIdというキーワードでトリガーは発生しなくなります。もう少しましな方法がある気はしますが。

if (userMessage === "getGroupId") {
  // reply(replyToken, JSON.parse(e.postData.contents).events[0].source.groupId);
}

④日付ベースのタイマートリガーの設定
最後に機能2のトリガーを設定します。
GASプロジェクトのトリガー設定から、getEventsを任意のタイミングで発動させるように設定します。本記事のボットでは「日付ベースのタイマー」で「午後8時~9時」に発動させるようにトリガーを設定しました。
キャプチャcwせjか.JPG

以上で処理の記述および諸準備は終わりです。

UIの設定

最後にボットの見た目クオリティを上げます。
ここからは完全に好みの問題になるので、かいつまんで必要な箇所だけ説明します。

①リッチメニュー
ボットとの個チャに表示されるメニューで、LINE Official Account Managerから設定が可能です。
あくまで任意ですが、本記事のボットは特定のキーワードをトリガーに各機能を実行することができるので、メニューを選択するとキーワードを送信するメニューを作成すると良いかもしれません。
キャプチャ絵cj.JPG

②応答メッセージの無効化
重ね重ねになりますが、本記事のボットは特定のキーワードをトリガーに機能を実行します。したがって、受け取ったメッセージ一つひとつに対して応答をする必要はないかと思います。いちいち応答されたらウザいですし。設定から無効化することをおすすめします。

おわりに

本記事では、LINE Messaging APIとGoogle Apps Scriptを使ってグループチャットに匿名で投稿+予定の共有ができるLINE Botを開発しました。
時間にして3時間程度での実現だったので比較的容易に実装できたのではないかと思います。改めてGASは有能だと気づかされました。次はGoogle AppsではなくDBにデータを蓄積・取得していく形式のボットの開発を検討中です。

43
33
2

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
43
33