🗂

Google Apps Scriptでプリクラを作ってみた(サーバ側実装編)

2022/12/24に公開約6,500字

この記事はGoogle Apps Script Advent Calendar 2022の24日目の記事です

学校の文化祭でプリクラを展示したい!

というのを高校生が言っていたので、サンプルとして作成してみました。が、そのうち本番運用することになってしまいました。

要件

  • Chromebookで動作する必要がある(Giga School構想というので、一人1台端末を持っている)
  • Webカメラで撮影する
  • LINEを通じて画像がスマートフォンに送られる
  • ペンで落書きができる
  • スタンプ(画像を追加できる)
  • Undo機能が要る

ということで、Google Apps Scriptで作ります。

Webアプリケーションとして公開するための準備

まず、Chromebookで動作させるために、Webアプリケーションである必要があったので、以下のようにdoGet()を実装しました。1画面のみだったので、固定でindex.htmlを返しています。

コード.gs
function doGet(e) {
    return HtmlService.createTemplateFromFile("index").evaluate().setTitle("文化祭プリクラシステム");
}

デプロイ(公開)するときは、スクリプトエディタの右上のデプロイ→新しいデプロイでデプロイすることができます。

画像データをGoogle Driveに保存する

画像データをbase64エンコードした文字列で受け取ってBlobに変換してGoogle Driveに保存しています。もしかしたらバイナリを直接受け取れたかもしれませんが、いろいろな経緯があって、エンコードした文字列を受け取るようにしました。

コード.gs
function saveDrive(sendData) {
  Logger.log(sendData);

  let now = new Date();
  let nowString = Utilities.formatDate(now, "JST", "yyyyMMddHHmmss");

  let data = sendData.replace("data:image/jpeg;base64,", "");
  data = data.replace(" ", "+");
  let image = Utilities.base64Decode(data);
  image = Utilities.newBlob(image);
  image.setName(nowString + ".jpg");

  let folder = DriveApp.getFolderById("[画像保存用のフォルダID]");
  let file = DriveApp.createFile(image);
  file.moveTo(folder);

  // LINEの公式アカウントを通じて写真を送る
  sendMessage(file.getDownloadUrl());
}

LINE Messaging APIを使ってメッセージと画像を送る

LINEのサービスには各種サービスがありますが、今回は、Messaging APIを使ってメッセージを送りました。

LINEの公式アカウントを作る

LINEの公式アカウントはLINE Official Account Managerを使って作ります。無料プランだと、公開時点では5000件までメッセージが送信できましたが、今は、1000件までとなっています。

友だち追加時のWebhookに対応する

LINEの公式アカウントを友だち追加されると、LINEのサーバからPOSTのリクエストが送信されます。このリクエストに200を返す必要があります。が、Google Apps Scriptの場合は基本200が返されるので、そんなに難しくはありません。

doGet()、doPost()を実装したら(?)ログが見られなくなった!

https://ryjkmr.com/google-apps-script-console-log/の記事のようにWebアプリケーションとして公開した後か、UrlFetchAppを利用した時あたりから、Logger.log()の出力が見られなくなって困りました。console.logを利用するとか。という情報があったので試してみましたが、見ることができませんでした。
正式には、GoogleCloudのプロジェクトと紐づけると見られるようになるのですが、動画を作って送るなど、時間がないことと、短命で終わるプロジェクトに対して労力を割くのが面倒だったので、Google Documentに出力するという荒業で回避しました。

友だち追加したユーザー情報を取得する。

LINEのプロフィール情報の取得を使うと、表示名、UserID、画像URL、ステータスメッセージが取得できます。トラブル発生時に備えて、UserIDと表示名を取得して保存しておこうと思いましたが、個人情報の取り扱い関係で面倒な感じだったので、取得するのはやめました。(コメントアウトしています)したがって、今回は最後に追加した一人に対して画像を送ることにしました。SpreadSheetに保存するようにしましたが、データは1件分なので、PropertiesServiceを使えばよかったと思います。

コード.gs
// LINE Messaging API用アクセストークン
var accessToken = "[取得したアクセストークン]";

function doPost(e) {
  let doc = DocumentApp.create('Postlog');
  let ss = SpreadsheetApp.openById('[SpreadSheetのID]');
  let sheet = ss.getSheetByName('master');
  let body = doc.getBody();

  // Logger.log("doPost");
  log(body, "doPost");
  if(e.contextPath.length == 0) {
    // LINEからのリクエスト
    // console.log(e.postData);
    logJSON(body, e.postData);

    let contents = JSON.parse(e.postData.contents);
    console.log(contents);
    if(contents.events.length > 0) {
      for(let i = 0; i < contents.events.length; i++) {
        // console.log(contents.events[i]);
        logJSON(body, contents.events[i]);
        // 友だち追加
        if(contents.events[i].type == "follow") {
          log(body, "User: " + contents.events[i].source.userId);
          sheet.getRange(1, 1).setValue(contents.events[i].source.userId);
          // let response = UrlFetchApp.fetch("https://api.line.me/v2/bot/profile/" + contents.events[i].source.userId
          //                                 , {
          //                                   'method' : 'get',
          //                                   'headers' : {
          //                                     'Authorization' : "Bearer " + "{" + accessToken + "}"
          //                                   }
          //                                 });
          // log(body, response.getContentText());
          // log(body, parseInt(response.getResponseCode()));
          // if(response.getResponseCode() == 200) {
          //   // 正常時は表示名とIdを控えておく
          //   let user = JSON.parse(response.getContentText());
          //   log(body, "User: " + user.userId + " displayName: " + user.displayName);

          //   let values = [[user.userId, user.displayName]];
          //   let row = sheet.getLastRow() + 1;
          //   sheet.getRange(row, 1, 1, 2).setValues(values);
          // }
        }
      }
    } else {
      // Webhookの検証ボタンか不正リクエスト
      // console.log("Webhookの検証");
      log(body, "Webhookの検証");
    }
  } else {
    // その他のリクエスト
  }
}

function log(body, str) {
  body.appendParagraph(Utilities.formatDate(new Date(), "JST", "yyyy-MM-dd HH:mm:ss.SSS") + " : " + str);
}

function logJSON(body, obj) {
  body.appendParagraph(Utilities.formatDate(new Date(), "JST", "yyyy-MM-dd HH:mm:ss.SSS") + " : " + JSON.stringify(obj));
}

LINEアプリにメッセージを送る

最後にLINEにメッセージを送ります。UrlFetchAppで送信するときにpayloadの作成でハマったので、気を付けた方が良いです。JSON文字列を直接作るようにするよりは、オブジェクトを変換すると正常に送信できました。ただ、LINEのサーバからGoogle Driveの画像データを取得するためにアクセスするため、Google Driveのフォルダは共有設定を「リンクを知っている全員」などとして公開しておかなければいけません。(LINEアプリ側で画像がないという感じの×表示になる)

コード.gs
function sendMessage(fileUrl) {
  Logger.log(fileUrl);

  let ss = SpreadsheetApp.openById('[撮影した画像が保存されたフォルダのID');
  let sheet = ss.getSheetByName('master');

  // プレビュー画像:1MB、オリジナル画像:10MB
  let data = {to: sheet.getRange(1,1).getValue(),
              messages: [
                {type: "text", text: "作成したプリクラ画像が完成したのでお渡しします!"},
                {type: "image", originalContentUrl: fileUrl, previewImageUrl: fileUrl}
              ]};

  let response = UrlFetchApp.fetch("https://api.line.me/v2/bot/message/push"
                    , {
                        'method' : 'post',
                        'contentType' : 'application/json',
                        'headers' : {
                          'Authorization' : "Bearer " + "{" + accessToken + "}"
                        },
                        'payload' : JSON.stringify(data)
                      });

  Logger.log(response);
}

まとめ

Webアプリケーションは自由度が上がってきて文化祭の展示作品などにも使えるようになっています。小規模のアプリケーションならGoogle Apps Scriptで作って運用するのは手軽で便利ではないかと思ったりしています。大規模なものはだめです。きちんとGCEやら、GAEやらを使ってください。
クライアント側実装編も時間がある時に、また書きます。

Discussion

ログインするとコメントできます