見出し画像

LINE Messaging APIとGASを用いた手書きメッセージングサービスの開発【柏の葉高校情報理数科】

課題研究の技術ブログを公開します!

このブログは、情報理数科3年「課題研究」という授業のまとめブログです。
来年以降の情報理数科の皆さんに向けて18期生からのメッセージだと思ってください!

今回は手書きでメッセージを送れるメッセージングサービスを開発しました。
特にご高齢の方や、スマホをつかい慣れていないご家族をもつ方がLINEを使用してメッセージのやり取りを行うことができたらいいと考え、このアプリケーションを作りました。
ご高齢の方や、使い慣れない方のために、手書きでメッセージを送信できるものになっています。


GASを使って開発

今回は、GAS(Google Apps Script)を使ってアプリケーション開発をしました!

GASのはじめかた

① Googleドライブを開く。
② 左上の”新規”からスプレッドシートを作成する。
③ スプレッドシート上のツールバーから”拡張機能”を選択し、”Apps Script”を選択する。

LINE Messaging APIとは?

LINEのチャンネルを作ることができ、メッセージの送受信やアクセストークンを使用することで、プログラムとの連携ができます。

① LINE公式アカウントを作成

LINE Business IDからGoogle アカウントでLINE公式アカウントを作ります。

② LINE Developerでチャネルを作成

LINE公式アカウントを作成すると、LINE Developerでチャネルの作成をすることができます。LINE Messaging APIでチャネルの作成を行います。

③ チャネルの設定

チャネル作成後、Messaging API設定からwebhookの指定を行います。webhooknに入力するURLは、GASでデプロイしたものになります。※/execで終わるものを使用します。

次にチャネルアクセストークンを発行します。アクセストークンはチャネル自体のアクセス権なので他者に見られないようにしてください。

手書きメッセージングサービスのシステム

ファイルの作成

今回の手書きメッセージングサービスのシステムでは、htmlファイル1個とgsファイル2個を使用します。具体的な用途ファイル名の対応は以下の通りです。

htmlファイル 

  • canvas.html:手書きできる場所のCSSや、表示するものの制御

gsファイル

  • LINEに送信する&HTML動かす用ファイル:キャンバスやデータの処理を行う用

  • 受け取る用ファイル:ラインの情報を取得する用

htmlファイルにもコメントで記載してありますが、GASでJSやCSSを使う際には、htmlファイル内で記述する必要があります。具体的にはJSはscriptタグの中に、CSSはstyleタグの中に記述する必要があります。ここまでの手順を踏めば、コーディングする準備はOKです。 

コードの解説

最初に2つのgsファイルのコードの解説をしていきます。

LINEに送信する&HTML動かす用ファイル

function showHandwritingWindow() {
  const html = HtmlService.createHtmlOutputFromFile('canvas')
    .setWidth(1600)
    .setHeight(1200);
  SpreadsheetApp.getUi().showModalDialog(html, '手書き入力');
}

// スプレッドシートからテキストを取得する関数
function getTextFromSheet() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const text = sheet.getRange('A2').getValue(); // セル A2 のテキストを取得
  return text;
}

// Base64データをスプレッドシートに保存する関数
function saveImageToSheet(base64Data) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  sheet.getRange('A1').setValue(base64Data); // 画像データをA1に保存

  // 画像をGoogle Driveに保存し、そのURLを取得
  const imageUrl = saveImageToDrive(base64Data);

  // 画像をLINEに送信
  sendImageToLine(imageUrl);
}

function saveImageToDrive(base64Data) {
  // 画像データのBase64部分を取得
  const base64Content = base64Data.replace(/^data:image\/(png|jpeg);base64,/, "");
  const blob = Utilities.newBlob(Utilities.base64Decode(base64Content), 'image/png', 'handwritten_image.png');
  
  // Google Driveに画像を保存
  const file = DriveApp.createFile(blob);
  file.setSharing(DriveApp.Access.ANYONE, DriveApp.Permission.VIEW); // 共有設定を公開に変更
  
  // ファイルIDを取得してLINE用URLに変換
  const fileId = file.getId();
  const lineImageUrl = `https://drive.google.com/uc?export=view&id=${fileId}`;
  
  return lineImageUrl; // LINE用のURLを返す
}

function sendImageToLine(imageUrl) {
  const token = '発行したアクセストークンをここにいれる; // LINEのチャネルアクセストークンを入力
  
  const payload = {
    to: 'チャネルに登録している誰に送信したいのかラインIDをいれる', // メッセージを送信する相手のユーザーID
    messages: [
      {
        type: 'image',
        originalContentUrl: imageUrl, // 変換後の画像URL
        previewImageUrl: imageUrl     // プレビュー画像のURL
      }
    ]
  };

  const options = {
    method: 'post',
    contentType: 'application/json',
    headers: {
      Authorization: 'Bearer ' + token
    },
    payload: JSON.stringify(payload)
  };

  UrlFetchApp.fetch('https://api.line.me/v2/bot/message/push', options);
}

このコードでは、手書きをできるキャンバスを用意し、データ送信ボタンが押されたとき書かれたものを画像として保存、送信を同時に行っています。
高齢者の方がタブレットにタッチペンなどで手書きし、それを画像として保存し、送信しているわけですね。

また、スプレッドシート内に保存する際、テキストデータのみでの保存なのでbase64というテキストデータに変換しています。

base64に変換後、base64をblobというバイナリーデータに変換しています。変換する理由はGoogleドライブに送信する際バイナリーデータが画適してからです。

Googleドライブに送信後URLとしてデータを受け取ります。LINEで画像を送る際、URLのみで送信することが可能になります。

受け取る用ファイル

const SPREADSHEET_ID = 'データを保管するためのスプレッドシートのID';

function doPost(e) {
  // LINEからのPOSTリクエストを受信
  const json =JSON.parse(e.postData.contents);
  const events = json.events;

  //Logger.log('test------');確認用で書いたけど使いません

  // スプレッドシートの取得
  const spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);
  const sheet = spreadsheet.getActiveSheet();

  events.forEach(event => {
    if (event.type === 'message' && event.message.type === 'text') {
      const userId = event.source.userId;
      const message = event.message.text;
      const timestamp = new Date(event.timestamp);

      // シートにデータを追加
      sheet.getRange('A2').setValue(message);
      sheet.getRange('B2').setValue(userId);
      sheet.getRange('C2').setValue(timestamp);
    }
  });

  // LINEに正常応答
  return ContentService.createTextOutput(JSON.stringify({status: 'success'})).setMimeType(ContentService.MimeType.JSON);
}

このdoPost関数でLINEにメッセージが来たとき、イベントが実行されるようになっています。メッセージのデータ、送り主のLINE ID、送られてきた時刻が保存されるようになっています。

スプレッドシートのIDは、スプレッドシートの上のURLの/d/の後からすべてになります。

今回スプレッドシートを開ているものではなく指定したものを使用した理由として、LINE IDの受け取り先を変更するためです。※キャンバスが表示される場所に出てしまうと誰でも見ることのできる状態になってしまうため、データの送り先を制限するためです。 


次にhtmlファイルについて解説します。

HTMLファイル

canvas.html

<!DOCTYPE html>
<html>
  <head>
    <style>
      body, html {
        margin: 0;
        padding: 0;
        overflow: hidden;
        width: 100%;
        height: 100%;
      }
      canvas {
        display: block;
      }
      #textDisplay {
        position: absolute;
        bottom: 10px;
        left: 10px;
        z-index: 1;
        padding: 1rem 2rem;
        border: 3px solid #000;
        background-color: rgba(255, 255, 255, 0.8);
        font-size: 14px;
        text-align: center;
        font-weight: bold;
        color: #333;
        min-width: 160px; /* 最小幅 */
        max-width: 850px; /* 最大幅を850pxに変更 */
        width: auto; /* 幅を自動調整 */
        white-space: normal; /* テキストの折り返し */
        word-wrap: break-word; /* 単語が長くても折り返す */
      }
      #textDisplay:before {
        content: '家族からのメッセージ';
        font-size: 0.75rem; /* Smaller font size */
        position: absolute;
        top: -24px;
        width: 120px;
        left: 0;
        height: 24px;
        padding: 0 0.5em;
        color: #fff;
        background: #000;
        line-height: 24px;
        border-radius: 5px;
      }
      
      .btn--blue {
        color: #fff;
        background-color: #00c0ff;
        border: none;
        border-bottom: 5px solid #0099cc;
        padding: 10px 20px;
        font-size: 16px;
        font-weight: bold;
        cursor: pointer;
        transition: all 0.2s ease;
        position: absolute;
        top: 10px;
        right: 10px;
        z-index: 1;
      }
      .btn--blue:hover {
        margin-top: 3px;
        background-color: #00aaff;
        border-bottom: 2px solid #007799;
      }

      .btn--red {
        color: #fff;
        background-color: #ff4d4d;
        border: none;
        border-bottom: 5px solid #cc0000;
        padding: 10px 20px;
        font-size: 16px;
        font-weight: bold;
        cursor: pointer;
        transition: all 0.2s ease;
        position: absolute;
        top: 10px;
        right: 100px;
        z-index: 1;
      }
      .btn--red:hover {
        margin-top: 3px;
        background-color: #ff3333;
        border-bottom: 2px solid #990000;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <button onclick="saveImage()" class="btn--blue">送信</button>
    <button onclick="clearCanvas()" class="btn--red">消す</button>

    <!-- Text display area with adjusted label position -->
    <div id="textDisplay">
      <span id="messageText">テキストを読み込み中...</span>
    </div>

    <script>
      const canvas = document.getElementById('canvas');
      const ctx = canvas.getContext('2d');
      let previousText = "";

      function resizeCanvas() {
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
      }

      window.addEventListener('load', () => {
        resizeCanvas();
        loadTextFromGAS();
        setInterval(checkForTextUpdate, 2000);
      });
      window.addEventListener('resize', resizeCanvas);

      let isDrawing = false;
      canvas.addEventListener('mousedown', (event) => {
        isDrawing = true;
        ctx.beginPath();
        ctx.moveTo(event.clientX, event.clientY);
      });

      canvas.addEventListener('mousemove', (event) => {
        if (!isDrawing) return;
        draw(event.clientX, event.clientY);
      });

      canvas.addEventListener('mouseup', () => {
        isDrawing = false;
        ctx.closePath();
      });

      canvas.addEventListener('touchstart', (event) => {
        isDrawing = true;
        const touch = event.touches[0];
        ctx.beginPath();
        ctx.moveTo(touch.clientX, touch.clientY);
      });

      canvas.addEventListener('touchmove', (event) => {
        if (!isDrawing) return;
        const touch = event.touches[0];
        draw(touch.clientX, touch.clientY);
        event.preventDefault();
      });

      canvas.addEventListener('touchend', () => {
        isDrawing = false;
        ctx.closePath();
      });

      function draw(x, y) {
        ctx.lineWidth = 2;
        ctx.lineCap = 'round';
        ctx.strokeStyle = 'black';
        ctx.lineTo(x, y);
        ctx.stroke();
      }

      function clearCanvas() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
      }

      function saveImage() {
        const base64Data = canvas.toDataURL('image/png');
        google.script.run.saveImageToSheet(base64Data);
        alert('画像が送信されました!');//画像保存確認用alert
      }

      function loadTextFromGAS() {
        google.script.run.withSuccessHandler((text) => {
          document.getElementById('messageText').innerText = text;
          previousText = text;
        }).getTextFromSheet();
      }

      function checkForTextUpdate() {
        google.script.run.withSuccessHandler((text) => {
          if (text !== previousText) {
            document.getElementById('messageText').innerText = text;
            previousText = text;
          }
        }).getTextFromSheet();
      }
    </script>
  </body>
</html>

補足

HTMLでは主にキャンバスのボタンの色、アニメーションや送られてきた文章の段落構成、また文章が送信されたときの更新方法を指定しています。キャンバス内のデータリザルト表示なども行っています。

これですべてになります。デプロイし、実行ボタンを押すことで使用が可能です。