いまさらGASでLineBotを作る【Drive画像保存・キュー処理】

◆ Live配信スケジュール ◆
サイオステクノロジーでは、Microsoft MVPの武井による「わかりみの深いシリーズ」など、定期的なLive配信を行っています。
⇒ 詳細スケジュールはこちらから
⇒ 見逃してしまった方はYoutubeチャンネルをご覧ください
【5/21開催】Azure OpenAI ServiceによるRAG実装ガイドを公開しました
生成AIを活用したユースケースで最も一番熱いと言われているRAGの実装ガイドを公開しました。そのガイドの紹介をおこなうイベントです!!
https://tech-lab.connpass.com/event/315703/

GASを使用してLINE BOTを作成するシリーズの第二弾!GASの特徴であるGoogleのサービスと連携してLINE上で送信した画像をGoogle Driveに保存する方法について紹介します。また、トリガー実行とキャッシュを利用してキュー処理を実現しています。

初めに

どもども!先日、上司をお誘いして飲み会を開いてルンルンで酔っぱらって帰ってきた龍ちゃんです。もともと決めていた予定を上書きして、歓迎会にして初めましての先輩とも飲んで帰ってきました。学びと楽しみのある飲み会でした。

さて!今回は「今更GASでLINE BOTを作るシリーズ」の第二弾です。前回はオウム返しのひな形を作成しました。今回は以下の二つの記事を使用してプログラムを構築しています。

今回作成するBOTの基本コンセプトは「画像を送信したらキャッシュに情報を保存し、ステッカーを送信したらトリガーを作成して、トリガーはキャッシュの情報を基にGoogle Driveに画像を保存して保存名を送信する」です。

それではコードの解説を進めていきます。

コーディング解説

設計から実際のコーディングまで解説をすすめていきたいと思います。

設計

基本コンセプトは「画像を送信したらキャッシュに情報を保存し、ステッカーを送信したらトリガーを作成して、トリガーはキャッシュの情報を基にGoogle Driveに画像を保存して保存名を送信する」になります。ユーザーからのアクションは「画像」と「スタンプ」になります。

LINE-GAS Drive画像保存ボット処理フロー

処理フローだけを抽出して拡大表示します。

LINE-GAS Google Drive画像保存処理フロー拡大図

LINE BOTの処理の受け取りとしては、「doPost」で処理を受け取ります。LINE BOTの制限として「ユーザーからのアクションに対して1秒以内に返答」というものがあります。そのため、ユーザーからのアクションを受け取る部分に関しては単一の処理のみを登録しています。Google Driveに画像を保存する処理に関しては、GASのトリガーを作成して別スレッドで処理を行います。トリガーは1分後に処理を行います。トリガーではキャッシュの情報を元に画像を取得し、Google Driveに保存する処理を行います。

キュー処理

今回はGASのキャッシュ機能を利用して、キュー処理を実現しました。その肝となる部分は以下になります。キャッシュを保存する概念としては『JobList』とその内部に保存されているKey名で保存されているJSONデータになります。

キュー処理 構成図

キャッシュを呼び出すためには、Key名がわからないといけません。Keyは重複がないようにuuidを生成します。その情報を『JobList』に保存して、実際のデータはuuidをKey名としてJSONデータを保存します。今回は、JSON DataとしてLINEから送信されたデータを保存しています。

この処理はコード内の『setJobData』で実装しています。

コーディング

それでは、コーディングについて解説をしていきたいと思います。作成した関数としては以下になります。

  • doPost:LINE BOT受け取り
    • setJobData:送信された画像取得用Tokenを含めた情報を保存(キュー処理)
    • ScriptApp.newTrigger:1分後に「startJob」を処理するための関数
  • startJob:画像取得~Google Driveに画像を保存~LINE返信処理

コードとしては以下になります。

GASのプロジェクトを作成して、LINE TokenとGoogle Driveのフォルダ名をスクリプトプロパティとして保存することで動作します。

// LINE developersのメッセージ送受信設定に記載のアクセストークン
const LINE_TOKEN = PropertiesService.getScriptProperties().getProperty("LINE_TOKEN"); // Messaging API設定の一番下で発行できるLINE Botのアクセストークン
const FOLDER_ID = PropertiesService.getScriptProperties().getProperty("FOLDER_ID");
const LINE_URL = '<https://api.line.me/v2/bot/message/reply>';

//ユーザーがメッセージを送信した時に下記を実行する
function doPost(e) {

  const json = JSON.parse(e.postData.contents);

  //replyToken…イベントへの応答に使用するトークン(Messaging APIリファレンス)
  // <https://developers.line.biz/ja/reference/messaging-api/#message-event>
  const reply_token = json.events[0].replyToken;
  const messageId = json.events[0].message.id;
  const messageType = json.events[0].message.type;
  const messageText = json.events[0].message.text;

  // 検証で200を返すための取り組み
  if (typeof reply_token === 'underfined') {
    return;
  }

  switch (messageType) {
    case "text":
      return;
    case "image":
      // 受け取った情報がimageの場合は、保存用にジョブを保存しておく
      const job = {
        "reply_token": reply_token,
        "messageId": messageId
      }
      setJobData(job)
      return
    case "sticker":
			// 一分後にstartJobを実行するTriggerの作成
      const triggers = ScriptApp.getProjectTriggers()
      let jobTriggerFlag = true;
      triggers.forEach((trigger) => {
        if (trigger.getHandlerFunction() == "startJob") jobTriggerFlag = false
      })
      if (jobTriggerFlag) ScriptApp.newTrigger("startJob").timeBased().after(1000).create();
      return
    default:
      // 受け取った情報がimage以外の場合、通知を行う
      const message = "今回のBOTは写真を保存するためだけです。別のBOTはまた用意します。";
      // request fetch all 要のプロパティ
      const request = createRequestForTextMessage(message, reply_token)
      UrlFetchApp.fetchAll([request])
      return;
  }

}
const createRequestForTextMessage = (message, reply_token) => {
  return {
    "url": LINE_URL,
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + LINE_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': reply_token,
      'messages': [{
        'type': 'text',
        'text': message,
      }],
    }),
  }
}

// キュー処理の保存
const setJobData = (job) => {
  const cache = CacheService.getScriptCache();
  const uid = getUuid()
  const jobList = JSON.parse(cache.get("jobList"))
  const newJobList = jobList ? [...jobList, uid] : [uid]
  cache.put("jobList", JSON.stringify(newJobList))
  cache.put(uid, JSON.stringify(job))
}

const getUuid = () => {
  return Utilities.getUuid()
}

const startJob = () => {
	// キャッシュを取得する
  const cache = CacheService.getScriptCache();
  const triggers = ScriptApp.getProjectTriggers()
  for (const trigger of triggers) {
    if (trigger.getHandlerFunction() == "startJob") {
      ScriptApp.deleteTrigger(trigger)
    }
  }
  const jobList = JSON.parse(cache.get("jobList"))
  if (jobList) {
    const replyTokenList = []
		// 画像取得用request生成
    const requests = jobList.map((value) => {
      const job = JSON.parse(cache.get(value));
      const messageId = job.messageId;
      replyTokenList.push(job.reply_token)
      return {
        "url": "<https://api-data.line.me/v2/bot/message/>" + messageId + "/content",
        'headers': {
          'Content-Type': 'application/json; charset=UTF-8',
          'Authorization': 'Bearer ' + LINE_TOKEN,
        },
        'method': 'get',
      }
    })
		// 画像取得
    const imageList = UrlFetchApp.fetchAll(requests)
    const imageBlobs = imageList.map((value) => value.getBlob().getAs("image/png").setName("LINE画像_" + getUuid() + ".png"))

		// 画像保存
    try {
      const folder = DriveApp.getFolderById(FOLDER_ID)
      const replayRequests = imageBlobs.map((value) => {
        folder.createFile(value);
        const replyToken = replyTokenList[0]
        replyTokenList.shift()
        return createRequestForTextMessage(`${value.getName()}で保存しました`, replyToken)
      })
      UrlFetchApp.fetchAll(replayRequests)
    } catch (e) {
      UrlFetchApp.fetchAll([createRequestForTextMessage("何かしらの問題が発生しました。頑張ってデバックしてくれ" + JSON.stringify(e), replyTokenList[0])])
      Logger.log(e)
    }
		// キャッシュの開放
    cache.removeAll(jobList)
    cache.remove("jobList")
  }

『startJob』の処理は以下の処理に分割することができます。

  • LINEから送信された情報を基に画像を取得する
  • Google Driveに画像を保存する
  • 保存した画像のタイトルをLINEで返信する

画像データはBlobでやりとりされます。Google DriveではBlob形式のデータをランダムな命名のPNG形式で保存しています。また命名の結果をLINEに返信します。

終わりに

今回は、LINE BOTの第二弾として「LINE上で送信した画像データをGoogle Driveに保存するBOT」を作成しました。今回使用しているGASの機能は『Google Apps Scriptを最大限活用していきたいなぁ~』にて紹介しています。

コーディングをどこまで解説するべきか悩みますね。今回は、概念だけ説明してコードはぺらっと張っておきました。もし、実装上の問題があったらTwitterで問い合わせしてもらうと気づきやすいです。まだ、投稿する内容が決まっていなくて全然発信できていないんですけどね。

それではまた~

アバター画像
About 龍:Ryu 107 Articles
2022年入社で主にフロントエンドの業務でTailwindと遊ぶ日々。お酒とうまいご飯が好きで、運動がちょっと嫌いなエンジニアです。しゃべれるエンジニアを目指しておしゃべりとブログ執筆に注力中(業務もね)//
ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。


ご覧いただきありがとうございます。
ブログの最新情報はSNSでも発信しております。
ぜひTwitterのフォロー&Facebookページにいいねをお願い致します!



>> 雑誌等の執筆依頼を受付しております。
   ご希望の方はお気軽にお問い合わせください!

Be the first to comment

Leave a Reply

Your email address will not be published.


*


質問はこちら 閉じる