LINEからkintoneに撮影した写真を送って保存する画像データベースを作ってみました。
LINE Messaging API と Google Apps Script、kintoneを組み合わせています。
前提知識
- kintoneでアプリが作成できる
- LINE Messaging API の設定ができる
- Google Apps Script の設定ができる
動作イメージ
概要
LINE Messaging API から送信した画像を ボットサーバー(Google Apps Script(GAS)で作成して公開)で受信します。受信した画像をkintoneにファイルアップロードします。
- kintoneアプリを作成
- LINE Messaging APIを設定
- Google Apps Script でWebアプリを作成
- Google Apps Script の公開URLをLINE Messaging API のWebhookに設定します
1.と2.、4.は拙稿をご覧ください。
- LINE と Google Apps Script で作成したBotの情報をkintoneに保存する
- LINE Messaging API を使って、リッチメニューを作ってスマホのカメラを起動するまでやってみた
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関連リンク
- Class File | Apps Script https://developers.google.com/apps-script/reference/drive/file
- Class Blob | Apps Script https://developers.google.com/apps-script/reference/base/blob
- URL Fetch Service | Apps Script https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app
- Class HTTPResponse | Apps Script https://developers.google.com/apps-script/reference/url-fetch/http-response
今後の展開
- kintoneに登録した画像をLINEから取得して表示する