Skip to content

Commit

Permalink
new API to create gist contains line msg
Browse files Browse the repository at this point in the history
  • Loading branch information
taichunmin committed Aug 14, 2023
1 parent 81df5ae commit 1eae3ae
Show file tree
Hide file tree
Showing 15 changed files with 1,222 additions and 1,087 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- name: 安裝 Node.js 與 yarn
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
cache: 'yarn'
- name: install, lint, test
run: |
Expand All @@ -34,7 +34,7 @@ jobs:
- name: 安裝 Node.js 與 yarn
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
cache: 'yarn'
- id: 'auth'
uses: google-github-actions/auth@v0
Expand Down
41 changes: 41 additions & 0 deletions api/createLineMsgGist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const _ = require('lodash')
const { beautifyFlex, getenv, log } = require('../libs/helper')
const { octokit } = require('../libs/octokit')
const dayjs = require('dayjs')
const JSON5 = require('json5')
const Line = require('../libs/linebotsdk').Client

const LINE_MESSAGING_TOKEN = getenv('LINE_MESSAGING_TOKEN')

module.exports = async (ctx, next) => {
const { req, res } = ctx
if (_.isNil(LINE_MESSAGING_TOKEN) || _.isNil(octokit)) return await next() // 未設定 TOKEN
if (!_.isArray(req.body) && !_.isPlainObject(req.body)) return await next() // 轉交給下一個 middleware 處理

try {
ctx.line = new Line({ channelAccessToken: LINE_MESSAGING_TOKEN })

// verify message
const tmp1 = _.castArray(req.body)
for (let i = 0; i < tmp1.length; i++) {
if (_.includes(['bubble', 'carousel'], tmp1[i]?.type)) tmp1[i] = { altText: 'altText', contents: tmp1[i], type: 'flex' }
const tmp2 = tmp1[i]
if (!_.isNil(tmp2?.replyToken)) throw new Error(`msg[${i}].replyToken is not allowed`)
if (!_.includes(['text', 'sticker', 'image', 'video', 'audio', 'location', 'imagemap', 'template', 'flex'], tmp2?.type)) throw new Error(`msg[${i}].type = ${JSON5.stringify(tmp2?.type)} is invalid`)
}
await ctx.line.validateReplyMessageObjects(tmp1) // 先透過 messaging api 驗證內容
.catch(err => { throw _.merge(err, { status: err?.originalError?.response?.status ?? 500, ..._.pick(err?.originalError?.response?.data, ['message', 'details']) }) })

// 上傳到 gist
const nowts = dayjs()
const filename = `gcf-line-devbot-${+nowts}.json5`
const gist = await octokit.request('POST /gists', {
description: `Upload by line-devbot /gist/createLineMessage at ${nowts.format('YYYY-MM-DD HH:mm:ss')}`,
files: { [filename]: { content: JSON5.stringify(beautifyFlex(req.body)) } },
})
res.json({ rawUrl: gist.data.files[filename].raw_url })
} catch (err) {
log('ERROR', err)
res.status(err.status ?? 500).json(_.pick(err, ['message', 'details']))
}
}
14 changes: 14 additions & 0 deletions api/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const _ = require('lodash')

const apis = new Map([
['GET /mp/collect', require('./mpCollect')],
['POST /api/createLineMsgGist', require('./createLineMsgGist')],
['POST /api/lineVerifyReplyMsg', require('./lineVerifyReplyMsg')],
])

module.exports = async (ctx, next) => {
const { req } = ctx
const apiMethodPath = `${_.toUpper(req.method)} ${req.path}`
if (!apis.has(apiMethodPath)) return await next()
return await apis.get(apiMethodPath)(ctx, next)
}
22 changes: 22 additions & 0 deletions api/lineVerifyReplyMsg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const _ = require('lodash')
const { getenv } = require('../libs/helper')
const { log } = require('../libs/helper')
const Line = require('../libs/linebotsdk').Client

const LINE_MESSAGING_TOKEN = getenv('LINE_MESSAGING_TOKEN')

module.exports = async (ctx, next) => {
const { req, res } = ctx
if (_.isNil(LINE_MESSAGING_TOKEN)) return await next() // 未設定 TOKEN
if (!_.isArray(req.body) && !_.isPlainObject(req.body)) return await next() // 轉交給下一個 middleware 處理

try {
ctx.line = new Line({ channelAccessToken: LINE_MESSAGING_TOKEN })
await ctx.line.validateReplyMessageObjects(req.body) // 先透過 messaging api 驗證內容
.catch(err => { throw _.merge(err, { status: err?.originalError?.response?.status ?? 500, ..._.pick(err?.originalError?.response?.data, ['message', 'details']) }) })
res.json({})
} catch (err) {
log('ERROR', err)
res.status(err.status ?? 500).json(_.pick(err, ['message', 'details']))
}
}
3 changes: 1 addition & 2 deletions gtag.js → api/mpCollect.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable camelcase */
const _ = require('lodash')
const { log, parseJsonOrDefault } = require('./libs/helper')
const { log, parseJsonOrDefault } = require('../libs/helper')
const axios = require('axios')
const createError = require('http-errors')

Expand All @@ -12,7 +12,6 @@ const reqToJson = req => {

module.exports = async (ctx, next) => {
const { req, res } = ctx
if (req.method !== 'GET' || req.path !== '/mp/collect') return await next()
try {
const { api_secret, measurement_id } = req.query
if (!api_secret || !measurement_id) throw createError(400, 'invalid request')
Expand Down
9 changes: 5 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
require('dotenv').config()

const _ = require('lodash')
const { log } = require('./libs/helper')
const { middlewareCompose } = require('./libs/helper')
const functions = require('@google-cloud/functions-framework')

const handler = middlewareCompose([
const handlers = middlewareCompose([
require('./cors'),
require('./gtag'),
require('./api/index'),
require('./line/handler/index'),
])

functions.http('main', async (req, res) => {
try {
await handler({ req, res })
await handlers({ req, res })
} catch (err) {
log('ERROR', err)
res.status(err.status || 500).send(err.message)
res.status(err.status ?? 500).json(_.pick(err, ['message']))
}
})
11 changes: 6 additions & 5 deletions libs/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const jsonStringify = obj => {
try {
const preventCircular = new Set()
return JSON.stringify(obj, (key, value) => {
if (value instanceof Map) return { dataType: 'Map', value: [...value.entries()] }
if (value instanceof Map) return _.fromPairs([...value.entries()])
if (value instanceof Set) return [...value.values()]
if (_.isObject(value) && !_.isEmpty(value)) {
if (preventCircular.has(value)) return '[Circular]'
preventCircular.add(value)
Expand Down Expand Up @@ -62,13 +63,13 @@ exports.log = (() => {
}
})()

exports.middlewareCompose = middleware => {
exports.middlewareCompose = middlewares => {
// 型態檢查
if (!_.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
if (!_.every(middleware, _.isFunction)) throw new TypeError('Middleware must be composed of functions!')
if (!_.isArray(middlewares)) throw new TypeError('Middleware stack must be an array!')
if (!_.every(middlewares, _.isFunction)) throw new TypeError('Middleware must be composed of functions!')

return async (context = {}, next) => {
const cloned = [...middleware, ...(_.isFunction(next) ? [next] : [])]
const cloned = [...middlewares, ...(_.isFunction(next) ? [next] : [])]
if (!cloned.length) return
const executed = _.times(cloned.length + 1, () => 0)
const dispatch = async cur => {
Expand Down
10 changes: 6 additions & 4 deletions libs/linemsgapi.js → libs/linebotsdk.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
const _ = require('lodash')
const { Client } = require('@line/bot-sdk')
const axios = require('axios')

const LINE_API_APIBASE = 'https://api.line.me'

exports.validateReplyMessage = async (line, msg) => {
Client.prototype.validateReplyMessageObjects = async function (msg) {
try {
if (!_.isArray(msg)) msg = [msg]
return await axios.post(`${LINE_API_APIBASE}/v2/bot/message/validate/reply`, { messages: msg }, {
headers: {
Authorization: `Bearer ${line.config.channelAccessToken}`,
Authorization: `Bearer ${this.config.channelAccessToken}`,
},
})
} catch (err) {
err.message = err?.response?.data?.message ?? err.message
throw err
throw _.merge(new Error('Failed to validate reply message objects'), { originalError: err })
}
}

exports.Client = Client
2 changes: 1 addition & 1 deletion line/handler/cmd/gistReplaceAltText.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ module.exports = async (ctx, next) => {
if (_.has(last, 'altText')) last.altText = altText

log({ message: `reply flex from text, altText: ${last.altText}`, msg }) // 回傳前先記錄一次
await ctx.line.validateReplyMessage(msg) // 先驗證 messaging api 的內容正確
await ctx.line.validateReplyMessageObjects(msg) // 先驗證 messaging api 的內容正確

msg = await tryAddShareBtn(ctx, msg) // 嘗試新增透過 LINE 數位版名片分享的按鈕
await ctx.replyMessage(msg)
Expand Down
2 changes: 1 addition & 1 deletion line/handler/cmd/replySticker.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module.exports = async (ctx, next) => {
title: 'RESULTS OF /replySticker',
stickers: await Promise.all(_.map(stickers, async ([packageId, stickerId]) => {
try {
await ctx.line.validateReplyMessage({
await ctx.line.validateReplyMessageObjects({
packageId: _.toSafeInteger(packageId),
stickerId: _.toSafeInteger(stickerId),
type: 'sticker',
Expand Down
8 changes: 2 additions & 6 deletions line/handler/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
const _ = require('lodash')
const { log, middlewareCompose } = require('../../libs/helper')
const Line = require('@line/bot-sdk').Client
const linemsgapi = require('../../libs/linemsgapi')
const Line = require('../../libs/linebotsdk').Client

// 模仿 Koajs 的 middleware
const lineEventHander = middlewareCompose([
Expand All @@ -20,14 +19,11 @@ module.exports = async (ctx, next) => {
if (!/^[a-zA-Z0-9+/=]+$/.test(channelAccessToken)) throw new Error('invalid channel access token')
const line = new Line({ channelAccessToken })

// 註冊新的 LINE Messaging API
line.validateReplyMessage = async msg => linemsgapi.validateReplyMessage(line, msg)

// 處理 events
const ctx = { line, req }
const events = _.get(req, 'body.events', [])
await Promise.all(_.map(events, event => lineEventHander({ ...ctx, event })))
res.status(200).send('OK')
res.status(200).send({})
} catch (err) {
log('ERROR', err)
res.status(err.status || 500).send(err.message)
Expand Down
2 changes: 1 addition & 1 deletion line/handler/replyEventJson.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const eventToMsgsHandlers = [
if (_.get(ctx, 'event.message.type') !== 'sticker') return
const msg = _.last(ctx.msgs)
_.update(msg, 'quickReply.items', items => {
items = _.toArray(items)
items = items ?? []
const { stickerId, packageId } = ctx.event.message
items.push({
type: 'action',
Expand Down
2 changes: 1 addition & 1 deletion line/handler/replyFlexFromText.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ module.exports = async (ctx, next) => {
if (isPartialFlex) msg = { altText: '缺少替代文字', contents: msg, type: 'flex' }

log({ message: `reply flex from text, altText: ${getAltText(msg)}`, msg }) // 回傳前先記錄一次
await ctx.line.validateReplyMessage(msg) // 先驗證 messaging api 的內容正確
await ctx.line.validateReplyMessageObjects(msg) // 先透過 messaging api 驗證內容

msg = await tryAddShareBtn(ctx, msg) // 嘗試新增透過 LINE 數位版名片分享的按鈕
await ctx.replyMessage(msg)
Expand Down
24 changes: 12 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,29 @@
"author": "taichunmin <[email protected]>",
"license": "MIT",
"dependencies": {
"@google-cloud/functions-framework": "^3.1.3",
"@google-cloud/functions-framework": "^3.3.0",
"@josephg/resolvable": "^1.0.1",
"@line/bot-sdk": "^7.5.2",
"axios": "^1.3.4",
"axios": "^1.4.0",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.7",
"dotenv": "^16.0.3",
"dayjs": "^1.11.9",
"dotenv": "^16.3.1",
"json5": "^2.2.3",
"lodash": "^4.17.21",
"octokit": "^2.0.14",
"qs": "^6.11.1"
"octokit": "^3.1.0",
"qs": "^6.11.2"
},
"devDependencies": {
"eslint": "^8.36.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^15.6.1",
"eslint": "^8.47.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.28.0",
"eslint-plugin-n": "^16.0.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.1.1",
"jest": "^29.5.0"
"jest": "^29.6.2"
},
"scripts": {
"deploy": "gcloud functions deploy gcf-line-devbot --allow-unauthenticated --entry-point=main --env-vars-file=.env.yaml --gen2 --max-instances=1 --memory=128Mi --no-user-output-enabled --region=us-central1 --runtime=nodejs16 --timeout=60s --trigger-http && gcloud run services update gcf-line-devbot --region=us-central1 --cpu 1 --concurrency 80",
"deploy": "gcloud functions deploy gcf-line-devbot --allow-unauthenticated --entry-point=main --env-vars-file=.env.yaml --gen2 --max-instances=1 --memory=128Mi --no-user-output-enabled --region=us-central1 --runtime=nodejs18 --timeout=60s --trigger-http && gcloud run services update gcf-line-devbot --region=us-central1 --cpu 1 --concurrency 80",
"lint": "eslint --ext .js --fix .",
"localhost-run": "autossh -M 0 -o ServerAliveInterval=60 -o ServerAliveCountMax=3 -o StrictHostKeyChecking=no -R 80:localhost:3000 [email protected]",
"repl": "node --experimental-repl-await repl.js",
Expand Down
Loading

0 comments on commit 1eae3ae

Please sign in to comment.