LoginSignup
1
2

More than 3 years have passed since last update.

LINE Messaging API x Google Apps Script x kintone で作る画像データベース

Last updated at Posted at 2020-06-21

LINEからkintoneに撮影した写真を送って保存する画像データベースを作ってみました。
LINE Messaging API と Google Apps Script、kintoneを組み合わせています。

前提知識

  • kintoneでアプリが作成できる
  • LINE Messaging API の設定ができる
  • Google Apps Script の設定ができる

動作イメージ

LINEの画面です。
IMG_1247.PNG

kintoneの画面です。
スクリーンショット 2020-06-21 14.25.46.png

概要

LINE Messaging API から送信した画像を ボットサーバー(Google Apps Script(GAS)で作成して公開)で受信します。受信した画像をkintoneにファイルアップロードします。

  1. kintoneアプリを作成
  2. LINE Messaging APIを設定
  3. Google Apps Script でWebアプリを作成
  4. Google Apps Script の公開URLをLINE Messaging API のWebhookに設定します

1.と2.、4.は拙稿をご覧ください。

3. Google Apps Script でWebアプリを作成

参考にコードを載せておきます。
いつものようにデバッグ用のコードはそのままですし、書き方も動きを確認する程度の雑な実装ですが、ご容赦ください。
また、APIトークンなどは、変数に良しなにセットされている前提です。

Code.ts
// 環境変数のセット
const scriptProperties = PropertiesService.getScriptProperties();
const DOMAIN = scriptProperties.getProperty('DOMAIN'); // kintone ドメイン名
const APP_ID = scriptProperties.getProperty('APP_ID'); // kintone ユーザーID
const API_TOKEN = scriptProperties.getProperty('API_TOKEN'); // kintone APIトークン
const SHEET_ID = scriptProperties.getProperty('SHEET_ID'); // デバッグ用 Spreadsheet ID
const CHANNEL_ACCESS_TOKEN = scriptProperties.getProperty('CHANNEL_ACCESS_TOKEN'); // LINEチャネルアクセストークン
const lineReplyUrl: string = 'https://api.line.me/v2/bot/message/reply';

function doPost(e: { postData: { contents: string; }; }) {
  appendLogToSpreadsheet('--- debug start', SHEET_ID)
  let lineEvents = []
  const botUserId: string = JSON.parse(e.postData.contents).destination
  lineEvents = JSON.parse(e.postData.contents).events
  appendLogToSpreadsheet(botUserId, SHEET_ID)
  appendLogToSpreadsheet(JSON.stringify(lineEvents), SHEET_ID)
  if (!lineEvents.length) {
    return ContentService.createTextOutput(JSON.stringify({'content': '200 ok'})).setMimeType(ContentService.MimeType.JSON);
  }
  lineEvents.forEach(event => appendLogToSpreadsheet(JSON.stringify(event), SHEET_ID))
  let replyToken: string = ''
  let userMessage: string = ''
  const replyPackageId: string = '11537'
  const replyStickerId: string = '52002740' // LINEスタンプID
  let replyMessage = {}
  let kintoneResult = {}
  lineEvents.forEach( async (event) => {
    if (event.type === 'message') {
      if (event.message.type === 'text') { // テキストメッセージ
        replyToken = event.replyToken
        userMessage = event.message.text
        replyMessage = {
          type: 'text',
          text: userMessage
        }
        replayMessage(lineReplyUrl, CHANNEL_ACCESS_TOKEN, replyToken, replyMessage)
        kintoneResult = await LoggerTokintone (userMessage, event.source.userId, event.message.type,DOMAIN, APP_ID, API_TOKEN)
      } else if (event.message.type === 'sticker') { // LINEスタンプ
        replyToken = event.replyToken
        replyMessage = {
          type: 'sticker',
          packageId: replyPackageId,
          stickerId: replyStickerId
        }
        replayMessage(lineReplyUrl, CHANNEL_ACCESS_TOKEN, replyToken, replyMessage)
        kintoneResult = await LoggerTokintone (`sticker packageId: ${event.message.packageId}, stickerId: ${event.message.stickerId}`, event.source.userId, event.message.type,DOMAIN, APP_ID, API_TOKEN)
      } else if (event.message.type === 'image') { // 画像
        replyToken = event.replyToken
        userMessage = '画像が送信されました'
        replyMessage = {
          type: 'text',
          text: userMessage
        }
        replayMessage(lineReplyUrl, CHANNEL_ACCESS_TOKEN, replyToken, replyMessage)
        const lineResult = await getUserContent(CHANNEL_ACCESS_TOKEN, event.message.id)
        const fileBlob = lineResult.getBlob()
        // kintoneに画像をアップロードする
        const fileKey = await uploadFileTokintone(fileBlob, DOMAIN, API_TOKEN, fileBlob.getName())
        // kintoneにアップロードした画像の情報をkintoneに登録する
        kintoneResult = await LoggerTokintone (event.message.id, event.source.userId, event.message.type, DOMAIN, APP_ID, API_TOKEN)
        appendLogToSpreadsheet('Blob fileKey', SHEET_ID)
        appendLogToSpreadsheet(fileKey, SHEET_ID)
        const attachedResult = await attachedFile(fileKey, DOMAIN, APP_ID, kintoneResult.id, API_TOKEN)
        appendLogToSpreadsheet('attached file', SHEET_ID)
        appendLogToSpreadsheet(JSON.stringify(attachedResult), SHEET_ID)
      } else {
        replyToken = event.replyToken
        userMessage = 'テキスト、画像又はスタンプ以外が送信されました'
        replyMessage = {
          type: 'text',
          text: userMessage
        }
        replayMessage(lineReplyUrl, CHANNEL_ACCESS_TOKEN, replyToken, replyMessage)
        kintoneResult = await LoggerTokintone (userMessage, event.source.userId, '',DOMAIN, APP_ID, API_TOKEN)
      }
    } else {
      replyToken = event.replyToken
      userMessage = 'メッセージ以外が送信されました'
      replyMessage = {
        type: 'text',
        text: userMessage
      }
      replayMessage(lineReplyUrl, CHANNEL_ACCESS_TOKEN, replyToken, replyMessage)
      kintoneResult = await LoggerTokintone (userMessage, event.source.userId, '',DOMAIN, APP_ID, API_TOKEN)
    }
    appendLogToSpreadsheet('result', SHEET_ID)
    appendLogToSpreadsheet(kintoneResult, SHEET_ID)
  })
  if (typeof replyToken === 'undefined') {
    return ContentService.createTextOutput(JSON.stringify({'content': '200 ok'})).setMimeType(ContentService.MimeType.JSON);
  }
  // 200 OKを返す
  return ContentService.createTextOutput(JSON.stringify({'content': '200 ok'})).setMimeType(ContentService.MimeType.JSON);
}

// LINEにメッセージを返信する
function replayMessage(url = 'https://api.line.me/v2/bot/message/reply', channelAccessToken, replyToken, replyMessage) {
  const response =  UrlFetchApp.fetch(url, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': `Bearer ${channelAccessToken}` ,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': replyToken,
      'messages': [replyMessage],
    }),
  });
  return JSON.parse(response.getContentText())
}

// LINEから送信した画像を取得する
function getUserContent(chanelAccessToken: string, messageId: string) {
  const url = `https://api-data.line.me/v2/bot/message/${messageId}/content`
  const option =   {
    'headers': {
        'Authorization': `Bearer ${chanelAccessToken}`
      },
      'method': 'get',
    }
  return new Promise((resolve, reject) => {
    try {
      const result = UrlFetchApp.fetch(
        url,
        option
      )
      resolve(result)
    } catch (error) {
      reject(error)
    }
  })
}

// Spreadsheetにログを書き出す
function appendLogToSpreadsheet(log: String, sheetId: string, sheetName: string = 'sheet1') {
  const spreadSheet = SpreadsheetApp.openById(sheetId)
  spreadSheet.getSheetByName(sheetName).appendRow(
    [new Date(), log]
  );
  SpreadsheetApp.flush()
}

// kintoneにログを追加する
function LoggerTokintone (message: string, lineId: string, messageType: string, subDomain: string, appId: Number, apiToken: string) {
  const payload = {
    app: appId,
    record: { 
      'messageType': {
        'value': messageType
      },      
      'message': {
        'value': message
      },
      'lineId': {
        'value': lineId
      },
    }
  }
  const option = {
    method: "post",
    contentType: "application/json",
    headers: { "X-Cybozu-API-Token": apiToken },
    muteHttpExceptions: true,
    payload: JSON.stringify(payload)
  };
  appendLogToSpreadsheet('function LoggerTokintone option', SHEET_ID)
  appendLogToSpreadsheet(JSON.stringify(option), SHEET_ID)

  // kintoneにレコード追加
  return new Promise((resolve, reject) => {
    try {
      const result = UrlFetchApp.fetch(
        `https://${subDomain}.cybozu.com/k/v1/record.json`,
        option
      )
      appendLogToSpreadsheet('function LoggerTokintone UrlFetch', SHEET_ID)
      appendLogToSpreadsheet(result.getContentText(), SHEET_ID)
      resolve(JSON.parse(result.getContentText()))
    } catch (error) {
      reject(error)
    }
  })
}

// kintoneにアップロードした画像をkintoneのレコードに紐付ける
function attachedFile(fileKey: string, subDomain: string, appId: number, recordId: number, apiToken: string) {
  const data = {
    'app': appId,
    'id': recordId,
    'record': { 
      'image': {
        'value': [
          {'fileKey': fileKey}
        ]
      }
    }
  }
  const option = {
    method: "put",
    contentType: "application/json",
    headers: {"X-Cybozu-API-Token": apiToken},
    muteHttpExceptions: true,
    payload: JSON.stringify(data)
  };
  appendLogToSpreadsheet('function attachedFile option', SHEET_ID)
  appendLogToSpreadsheet(option, SHEET_ID)

  // kintoneにレコード追加
  return new Promise((resolve, reject) => {
    try {
      const result = UrlFetchApp.fetch(
        `https://${subDomain}.cybozu.com/k/v1/record.json`,
        option
      )
      appendLogToSpreadsheet('function attachedFile UrlFetch', SHEET_ID)
      appendLogToSpreadsheet(JSON.parse(result.getContentText()), SHEET_ID)
      resolve(JSON.parse(result.getContentText()))
    } catch (error) {
      appendLogToSpreadsheet('function attachedFile UrlFetch', SHEET_ID)
      appendLogToSpreadsheet(error.getContentText(), SHEET_ID)
      reject(error)
    }
  })
}

// 画像をkintoneにアップロードする
function uploadFileTokintone(file, subDomain, apiToken, fileName = 'sample.jpeg') {
  const boundary = 'xxxxxxxxxxxxxxx';
  const blob = file.copyBlob()
  appendLogToSpreadsheet(`${blob.getContentType()}, ${fileName}`, SHEET_ID)

  let data = "";
  data += "--" + boundary + "\r\n"
  data += "Content-Disposition: form-data; name=\"file\"; filename=\"" + fileName + "\"\r\n";
  data += "Content-Type:" + blob.getContentType() + "\r\n\r\n";
  let payload = Utilities.newBlob(data).getBytes()
    .concat(blob.getBytes())
    .concat(Utilities.newBlob("\r\n--" + boundary + "--").getBytes());
  let options = {
    method: "post",
    contentType: "multipart/form-data; boundary=" + boundary,
    headers: {
      "X-Cybozu-API-Token": apiToken
    },
    muteHttpExceptions: true,
    payload: payload
  };
  appendLogToSpreadsheet('function uploadFile option', SHEET_ID)
  appendLogToSpreadsheet(JSON.stringify(options), SHEET_ID)

  // kintoneにレコード追加
  return new Promise((resolve, reject) => {
    try {
      const result = UrlFetchApp.fetch(
        `https://${subDomain}.cybozu.com/k/v1/file.json`,
        options
      )
      const json = JSON.parse(result.getContentText());
      appendLogToSpreadsheet('file upload result', SHEET_ID)
      appendLogToSpreadsheet(json, SHEET_ID)
      resolve(json.fileKey)
    } catch (error) {
      appendLogToSpreadsheet('file upload error', SHEET_ID)
      appendLogToSpreadsheet(JSON.stringify(error), SHEET_ID)
      reject(error)
    }
  })
}

参考

GASからkintoneに画像ファイルをアップロードする方法については、下記を参考にさせていただきました。ありがとうございます。
Google フォームとkintoneを連携させる方法(添付ファイル編)
https://gist.github.com/tanaikech/d595d30a592979bbf0c692d1193d260c

GAS関連リンク

今後の展開

  • kintoneに登録した画像をLINEから取得して表示する
1
2
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
1
2