├── .gitignore ├── README.md ├── database.rules ├── firebase.json ├── firestore.rules └── functions ├── .eslintrc.json ├── index.js ├── line.js ├── package.json ├── template.js └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | */npm-debug.log 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | firebase-debug.log* 9 | firebase-debug.*.log* 10 | **/firebase-debug.log 11 | **/ui-debug.log 12 | **/database-debug.log 13 | **/pubsub-debug.log 14 | 15 | # Diagnostic reports (https://nodejs.org/api/report.html) 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | 18 | # Firebase cache 19 | **/.firebase 20 | 21 | # Firebase config 22 | **/.firebaserc 23 | 24 | # Runtime data 25 | pids 26 | *.pid 27 | *.seed 28 | *.pid.lock 29 | 30 | # Directory for instrumented libs generated by jscoverage/JSCover 31 | lib-cov 32 | 33 | # Coverage directory used by tools like istanbul 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | .nyc_output 39 | 40 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 41 | .grunt 42 | 43 | # Bower dependency directory (https://bower.io/) 44 | bower_components 45 | 46 | # node-waf configuration 47 | .lock-wscript 48 | 49 | # Compiled binary addons (http://nodejs.org/api/addons.html) 50 | build/Release 51 | 52 | # Dependency directories 53 | **/node_modules/* 54 | **/jspm_packages/* 55 | 56 | # TypeScript v1 declaration files 57 | typings/ 58 | 59 | # TypeScript cache 60 | *.tsbuildinfo 61 | 62 | # Optional npm cache directory 63 | .npm 64 | 65 | # Optional eslint cache 66 | .eslintcache 67 | 68 | # Microbundle cache 69 | .rpt2_cache/ 70 | .rts2_cache_cjs/ 71 | .rts2_cache_es/ 72 | .rts2_cache_umd/ 73 | 74 | # Optional REPL history 75 | .node_repl_history 76 | 77 | # Output of 'npm pack' 78 | *.tgz 79 | 80 | # Yarn Integrity file 81 | .yarn-integrity 82 | yarn.lock 83 | 84 | # dotenv environment variables file 85 | .env 86 | .env.test 87 | 88 | # parcel-bundler cache (https://parceljs.org/) 89 | .cache 90 | 91 | # Next.js build output 92 | .next 93 | 94 | # Nuxt.js build / generate output 95 | .nuxt 96 | dist 97 | 98 | # Gatsby files 99 | .cache/ 100 | 101 | # vuepress build output 102 | .vuepress/dist 103 | 104 | # Serverless directories 105 | .serverless/ 106 | 107 | # FuseBox cache 108 | .fusebox/ 109 | 110 | # DynamoDB Local files 111 | .dynamodb/ 112 | 113 | # TernJS port file 114 | .tern-port 115 | 116 | .idea 117 | **/.runtimeconfig.json 118 | **/package-lock.json 119 | **/tsconfig-compile.json 120 | service-account.json 121 | service-account-credentials.json 122 | 123 | .DS_Store 124 | Thumbs.db -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LINE Bot 101 by Messaging API and Cloud Functions for Firebase 2 | Code sample for creating LINE Bot by Messaging API and Cloud Functions for Firebase for beginner 3 | 4 | ### What's Cloud Functions for Firebase? 5 | 6 | Cloud Functions is a hosted, private, and scalable Node.js environment where you can run JavaScript code. [Cloud Functions for Firebase](https://firebase.google.com/features/functions) integrates the Firebase platform by letting you write code that responds to events and invokes functionality exposed by other Firebase features. 7 | 8 | ## Prerequisites 9 | * [Create a channel on the LINE Developers console](https://developers.line.biz/en/docs/messaging-api/getting-started/) 10 | * To learn how to get started with Cloud Functions for Firebase by having a look at our [Getting Started Guide](https://firebase.google.com/docs/functions/get-started) 11 | * Select Blaze plan for your project in [Firebase Console](https://console.firebase.google.com/) 12 | 13 | ## Features 14 | * Reply Message 15 | * Push Message 16 | * Multicast Message 17 | * Broadcast Message 18 | 19 | ## Blog 20 | * [สร้าง LINE Bot ด้วย Messaging API และ Cloud Functions for Firebase](https://medium.com/linedevth/20d284edea1b) 21 | 22 | ## Screenshots 23 | 24 | 25 | 26 | 27 | 28 |
29 | -------------------------------------------------------------------------------- /database.rules: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".read": true, 4 | ".write": true 5 | } 6 | } -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "predeploy": [ 4 | "npm --prefix \"$RESOURCE_DIR\" run lint" 5 | ], 6 | "source": "functions" 7 | }, 8 | "database": { 9 | "rules": "database.rules" 10 | }, 11 | "firestore": { 12 | "rules": "firestore.rules" 13 | }, 14 | "emulators": { 15 | "functions": { 16 | "port": 5001 17 | }, 18 | "firestore": { 19 | "port": 8080 20 | }, 21 | "database": { 22 | "port": 9000 23 | }, 24 | "ui": { 25 | "enabled": true 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | service cloud.firestore { 2 | match /databases/{database}/documents { 3 | match /{document=**} { 4 | allow read, write; 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /functions/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | // Required for certain syntax usages 4 | "ecmaVersion": 2017 5 | }, 6 | "plugins": [ 7 | "promise" 8 | ], 9 | "extends": "eslint:recommended", 10 | "rules": { 11 | // Removed rule "disallow the use of console" from recommended eslint rules 12 | "no-console": "off", 13 | 14 | // Removed rule "disallow multiple spaces in regular expressions" from recommended eslint rules 15 | "no-regex-spaces": "off", 16 | 17 | // Removed rule "disallow the use of debugger" from recommended eslint rules 18 | "no-debugger": "off", 19 | 20 | // Removed rule "disallow unused variables" from recommended eslint rules 21 | "no-unused-vars": "off", 22 | 23 | // Removed rule "disallow mixed spaces and tabs for indentation" from recommended eslint rules 24 | "no-mixed-spaces-and-tabs": "off", 25 | 26 | // Removed rule "disallow the use of undeclared variables unless mentioned in /*global */ comments" from recommended eslint rules 27 | "no-undef": "off", 28 | 29 | // Warn against template literal placeholder syntax in regular strings 30 | "no-template-curly-in-string": 1, 31 | 32 | // Warn if return statements do not either always or never specify values 33 | "consistent-return": 1, 34 | 35 | // Warn if no return statements in callbacks of array methods 36 | "array-callback-return": 1, 37 | 38 | // Require the use of === and !== 39 | "eqeqeq": 2, 40 | 41 | // Disallow the use of alert, confirm, and prompt 42 | "no-alert": 2, 43 | 44 | // Disallow the use of arguments.caller or arguments.callee 45 | "no-caller": 2, 46 | 47 | // Disallow null comparisons without type-checking operators 48 | "no-eq-null": 2, 49 | 50 | // Disallow the use of eval() 51 | "no-eval": 2, 52 | 53 | // Warn against extending native types 54 | "no-extend-native": 1, 55 | 56 | // Warn against unnecessary calls to .bind() 57 | "no-extra-bind": 1, 58 | 59 | // Warn against unnecessary labels 60 | "no-extra-label": 1, 61 | 62 | // Disallow leading or trailing decimal points in numeric literals 63 | "no-floating-decimal": 2, 64 | 65 | // Warn against shorthand type conversions 66 | "no-implicit-coercion": 1, 67 | 68 | // Warn against function declarations and expressions inside loop statements 69 | "no-loop-func": 1, 70 | 71 | // Disallow new operators with the Function object 72 | "no-new-func": 2, 73 | 74 | // Warn against new operators with the String, Number, and Boolean objects 75 | "no-new-wrappers": 1, 76 | 77 | // Disallow throwing literals as exceptions 78 | "no-throw-literal": 2, 79 | 80 | // Require using Error objects as Promise rejection reasons 81 | "prefer-promise-reject-errors": 2, 82 | 83 | // Enforce “for” loop update clause moving the counter in the right direction 84 | "for-direction": 2, 85 | 86 | // Enforce return statements in getters 87 | "getter-return": 2, 88 | 89 | // Disallow await inside of loops 90 | "no-await-in-loop": 2, 91 | 92 | // Disallow comparing against -0 93 | "no-compare-neg-zero": 2, 94 | 95 | // Warn against catch clause parameters from shadowing variables in the outer scope 96 | "no-catch-shadow": 1, 97 | 98 | // Disallow identifiers from shadowing restricted names 99 | "no-shadow-restricted-names": 2, 100 | 101 | // Enforce return statements in callbacks of array methods 102 | "callback-return": 2, 103 | 104 | // Require error handling in callbacks 105 | "handle-callback-err": 2, 106 | 107 | // Warn against string concatenation with __dirname and __filename 108 | "no-path-concat": 1, 109 | 110 | // Prefer using arrow functions for callbacks 111 | "prefer-arrow-callback": 1, 112 | 113 | // Return inside each then() to create readable and reusable Promise chains. 114 | // Forces developers to return console logs and http calls in promises. 115 | "promise/always-return": 2, 116 | 117 | //Enforces the use of catch() on un-returned promises 118 | "promise/catch-or-return": 2, 119 | 120 | // Warn against nested then() or catch() statements 121 | "promise/no-nesting": 1 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require("firebase-functions"); 2 | const util = require("./util"); 3 | const runtimeOpts = { timeoutSeconds: 8, memory: "1GB", minInstances: 1 }; 4 | const region = "asia-northeast1"; 5 | 6 | exports.LineProxy = functions.region(region).runWith(runtimeOpts).https.onRequest(async (req, res) => { 7 | if (req.method === "POST") { 8 | if (!util.verifySignature(req.headers["x-line-signature"], req.body)) { 9 | return res.status(401).send("Unauthorized"); 10 | } 11 | 12 | let event = req.body.events[0]; 13 | if (event === undefined) { 14 | return res.end(); 15 | } 16 | 17 | if (event.type === "message") { 18 | if (event.message.type !== "text") { 19 | await util.reply(event.replyToken, { type: "text", text: JSON.stringify(event) }); 20 | } else { 21 | await util.postToDialogflow(req); 22 | } 23 | } 24 | } 25 | return res.send(req.method); 26 | }); 27 | 28 | const line = require('./line'); 29 | exports.lineBotReply = line.botReply; 30 | exports.lineBotPush = line.botPush; 31 | exports.lineBotMulticast = line.botMulticast; 32 | exports.lineBotBroadcast = line.botBroadcast; -------------------------------------------------------------------------------- /functions/line.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | const functions = require("firebase-functions"); 4 | const axios = require("axios"); 5 | const util = require("./util"); 6 | 7 | const runtimeOpts = { timeoutSeconds: 8, memory: "1GB", maxInstances: 1 }; 8 | const region = "asia-northeast1"; 9 | const LINE_USER_ID = functions.config().lineoa.jirawatee.userid; 10 | const OPENWEATHER_API = "https://api.openweathermap.org/data/2.5/weather/"; 11 | const OPENWEATHER_APPID = functions.config().openweather.appid; 12 | 13 | exports.botReply = functions.region(region).runWith(runtimeOpts).https.onRequest((req, res) => { 14 | if (req.method === "POST") { 15 | util.reply( 16 | req.body.events[0].replyToken, 17 | { type: "text", text: JSON.stringify(req.body) } 18 | ) 19 | } 20 | return res.status(200).send(req.method) 21 | }); 22 | 23 | exports.botPush = functions.region(region).runWith(runtimeOpts).https.onRequest(async (req, res) => { 24 | const response = await axios({ 25 | method: 'get', 26 | url: `${OPENWEATHER_API}?appid=${OPENWEATHER_APPID}&units=metric&type=accurate&zip=10330,th` 27 | }) 28 | const msg = `City: ${response.data.name}\nWeather: ${response.data.weather[0].description}\nTemperature: ${response.data.main.temp}` 29 | util.push(LINE_USER_ID, { type: "text", text: msg }) 30 | return res.status(200).send({ message: `Push: ${msg}` }) 31 | }); 32 | 33 | exports.botMulticast = functions.region(region).runWith(runtimeOpts).https.onRequest((req, res) => { 34 | const msg = req.query.msg 35 | if (msg !== undefined && msg.trim() !== "") { 36 | util.multicast({ type: "text", text: msg }) 37 | return res.status(200).send({ message: `Multicast: ${msg}` }) 38 | } else { 39 | return res.status(400).send({ message: "Text not found" }) 40 | } 41 | }); 42 | 43 | exports.botBroadcast = functions.region(region).runWith(runtimeOpts).https.onRequest((req, res) => { 44 | const msg = req.query.msg 45 | if (msg !== undefined && msg.trim() !== "") { 46 | util.broadcast({ type: "text", text: msg }) 47 | return res.status(200).send({ message: `Broadcast: ${msg}` }) 48 | } else { 49 | const ret = { message: "Text not found" } 50 | return res.status(400).send(ret) 51 | } 52 | }); -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "LINE Bot 101 by Messaging API and Cloud Functions for Firebase", 4 | "scripts": { 5 | "lint": "eslint .", 6 | "serve": "firebase emulators:start --only functions", 7 | "shell": "firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "dependencies": { 13 | "axios": "^0.21.1", 14 | "cheerio": "^1.0.0-rc.9", 15 | "firebase-admin": "^9.8.0", 16 | "firebase-functions": "^3.14.1", 17 | "uuid-v4": "^0.1.0" 18 | }, 19 | "devDependencies": { 20 | "eslint": "^7.27.0", 21 | "eslint-plugin-promise": "^5.1.0" 22 | }, 23 | "engines": { 24 | "node": "14" 25 | }, 26 | "private": true 27 | } 28 | -------------------------------------------------------------------------------- /functions/template.js: -------------------------------------------------------------------------------- 1 | const linepay = () => ({ 2 | "type": "flex", 3 | "altText": "Flex Message", 4 | "contents": { 5 | "type": "bubble", 6 | "body": { 7 | "type": "box", 8 | "layout": "vertical", 9 | "spacing": "md", 10 | "contents": [ 11 | { 12 | "type": "box", 13 | "layout": "vertical", 14 | "spacing": "sm", 15 | "contents": [ 16 | { 17 | "type": "box", 18 | "layout": "baseline", 19 | "contents": [ 20 | { 21 | "type": "text", 22 | "text": "Burger", 23 | "weight": "bold", 24 | "margin": "sm", 25 | "flex": 0 26 | }, 27 | { 28 | "type": "text", 29 | "text": "1฿", 30 | "size": "sm", 31 | "align": "end", 32 | "color": "#aaaaaa" 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | ] 39 | }, 40 | "footer": { 41 | "type": "box", 42 | "layout": "vertical", 43 | "contents": [ 44 | { 45 | "type": "button", 46 | "style": "primary", 47 | "color": "#905c44", 48 | "action": { 49 | "type": 'postback', 50 | "label": "burger", 51 | "data": "item=burger" 52 | } 53 | } 54 | ] 55 | } 56 | } 57 | }); 58 | 59 | const linehack = (data) => ({ 60 | "type": "flex", 61 | "altText": "Flex Message", 62 | "contents": { 63 | "type": "bubble", 64 | "size": "giga", 65 | "body": { 66 | "type": "box", 67 | "layout": "vertical", 68 | "contents": [ 69 | { 70 | "type": "box", 71 | "layout": "horizontal", 72 | "contents": [ 73 | { 74 | "type": "image", 75 | "url": "https://mokmoon.com/images/LINEDEV.png", 76 | "aspectRatio": "3:1", 77 | "size": "lg", 78 | "align": "start", 79 | "flex": 1 80 | }, 81 | { 82 | "type": "image", 83 | "url": "https://mokmoon.com/images/GDGBKK.png", 84 | "aspectRatio": "3:1", 85 | "size": "lg", 86 | "align": "start", 87 | "flex": 2, 88 | "margin": "none" 89 | } 90 | ], 91 | "paddingStart": "12%" 92 | }, 93 | { 94 | "type": "box", 95 | "layout": "vertical", 96 | "contents": [ 97 | { 98 | "type": "text", 99 | "text": "xxx", 100 | "contents": [ 101 | { 102 | "type": "span", 103 | "text": "LINE HACK 2020", 104 | "size": "3xl", 105 | "color": "#ffffff", 106 | "weight": "bold" 107 | }, 108 | { 109 | "type": "span", 110 | "text": ";", 111 | "weight": "bold", 112 | "color": "#0053ea", 113 | "size": "3xl" 114 | } 115 | ] 116 | }, 117 | { 118 | "type": "text", 119 | "text": data.title, 120 | "color": "#00ff00", 121 | "weight": "bold", 122 | "size": "lg", 123 | "margin": "lg" 124 | }, 125 | { 126 | "type": "text", 127 | "text": data.date, 128 | "color": "#ffffff", 129 | "weight": "bold", 130 | "size": "lg" 131 | } 132 | ], 133 | "paddingStart": "12%", 134 | "paddingTop": "4%", 135 | "paddingBottom": "8%", 136 | "paddingEnd": "8%" 137 | }, 138 | { 139 | "type": "box", 140 | "layout": "horizontal", 141 | "contents": [ 142 | { 143 | "type": "box", 144 | "layout": "vertical", 145 | "contents": [], 146 | "height": "32px", 147 | "cornerRadius": "sm", 148 | "background": { 149 | "type": "linearGradient", 150 | "angle": "0deg", 151 | "startColor": "#002e83", 152 | "endColor": "#0053ea", 153 | "centerColor": "#0053ea", 154 | "centerPosition": "32%" 155 | }, 156 | "width": "12%" 157 | }, 158 | { 159 | "type": "box", 160 | "layout": "vertical", 161 | "contents": [], 162 | "height": "32px", 163 | "cornerRadius": "sm", 164 | "background": { 165 | "type": "linearGradient", 166 | "angle": "0deg", 167 | "startColor": "#002e83", 168 | "endColor": "#0053ea", 169 | "centerColor": "#0053ea", 170 | "centerPosition": "32%" 171 | }, 172 | "margin": "48px", 173 | "width": "64%" 174 | } 175 | ], 176 | "margin": "xxl" 177 | }, 178 | { 179 | "type": "box", 180 | "layout": "horizontal", 181 | "contents": [ 182 | { 183 | "type": "box", 184 | "layout": "vertical", 185 | "contents": [], 186 | "width": "24%", 187 | "height": "32px", 188 | "cornerRadius": "sm", 189 | "background": { 190 | "type": "linearGradient", 191 | "angle": "0deg", 192 | "startColor": "#002e83", 193 | "endColor": "#0053ea", 194 | "centerColor": "#0053ea", 195 | "centerPosition": "32%" 196 | } 197 | }, 198 | { 199 | "type": "box", 200 | "layout": "vertical", 201 | "contents": [], 202 | "background": { 203 | "type": "linearGradient", 204 | "angle": "0deg", 205 | "startColor": "#008215", 206 | "endColor": "#00ea23", 207 | "centerColor": "#00ea23", 208 | "centerPosition": "32%" 209 | }, 210 | "height": "32px", 211 | "width": "28%", 212 | "cornerRadius": "sm", 213 | "margin": "48px" 214 | } 215 | ], 216 | "margin": "xl" 217 | }, 218 | { 219 | "type": "box", 220 | "layout": "horizontal", 221 | "contents": [ 222 | { 223 | "type": "box", 224 | "layout": "vertical", 225 | "contents": [], 226 | "width": "25%", 227 | "height": "32px", 228 | "cornerRadius": "sm", 229 | "background": { 230 | "type": "linearGradient", 231 | "angle": "0deg", 232 | "startColor": "#002e83", 233 | "endColor": "#0053ea", 234 | "centerColor": "#0053ea", 235 | "centerPosition": "32%" 236 | } 237 | } 238 | ], 239 | "margin": "xl", 240 | "paddingStart": "12%" 241 | }, 242 | { 243 | "type": "box", 244 | "layout": "vertical", 245 | "contents": [ 246 | { 247 | "type": "text", 248 | "text": "HACK_CONNECT_TOMORROW_", 249 | "weight": "bold", 250 | "color": "#0053ea" 251 | } 252 | ], 253 | "paddingStart": "12%", 254 | "margin": "xxl" 255 | } 256 | ], 257 | "paddingBottom": "8%", 258 | "paddingEnd": "none", 259 | "paddingStart": "none", 260 | "paddingTop": "8%" 261 | }, 262 | "styles": { 263 | "header": { 264 | "backgroundColor": "#1b1b1b" 265 | }, 266 | "hero": { 267 | "backgroundColor": "#1b1b1b" 268 | }, 269 | "body": { 270 | "backgroundColor": "#1b1b1b" 271 | } 272 | } 273 | } 274 | }); 275 | 276 | module.exports = { linehack, linepay }; -------------------------------------------------------------------------------- /functions/util.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | const functions = require("firebase-functions"); 3 | const axios = require("axios"); 4 | const crypto = require("crypto"); 5 | 6 | const LINE_USER_ID = functions.config().lineoa.jirawatee.userid; 7 | const LINE_MESSAGING_API = "https://api.line.me/v2/bot"; 8 | const LINE_CHANNEL_SECRET = functions.config().lineoa.jirawatee.channel_secret; 9 | const LINE_HEADER = { 10 | "Content-Type": "application/json", 11 | Authorization: `Bearer ${functions.config().lineoa.jirawatee.channel_access_token}` 12 | }; 13 | 14 | const getProfile = (userId) => { 15 | return axios({ 16 | method: 'get', 17 | headers: LINE_HEADER, 18 | url: `${LINE_MESSAGING_API}/profile/${userId}` 19 | }); 20 | }; 21 | 22 | const getProfileGroup = (groupId, userId) => { 23 | return axios({ 24 | method: 'get', 25 | headers: LINE_HEADER, 26 | url: `${LINE_MESSAGING_API}/group/${groupId}/member/${userId}` 27 | }); 28 | }; 29 | 30 | const getProfileRoom = (roomId, userId) => { 31 | return axios({ 32 | method: 'get', 33 | headers: LINE_HEADER, 34 | url: `${LINE_MESSAGING_API}/room/${roomId}/member/${userId}` 35 | }); 36 | }; 37 | 38 | const reply = (token, payload) => { 39 | return axios({ 40 | method: "post", 41 | url: `${LINE_MESSAGING_API}/message/reply`, 42 | headers: LINE_HEADER, 43 | data: JSON.stringify({ 44 | replyToken: token, 45 | messages: [payload] 46 | }) 47 | }); 48 | }; 49 | 50 | const push = (targetId, payload) => { 51 | return axios({ 52 | method: "post", 53 | url: `${LINE_MESSAGING_API}/message/push`, 54 | headers: LINE_HEADER, 55 | data: JSON.stringify({ 56 | to: targetId, 57 | messages: [payload] 58 | }) 59 | }); 60 | }; 61 | 62 | const multicast = (payload) => { 63 | return axios({ 64 | method: "post", 65 | url: `${LINE_MESSAGING_API}/message/multicast`, 66 | headers: LINE_HEADER, 67 | data: JSON.stringify({ 68 | to: [LINE_USER_ID], 69 | messages: [payload] 70 | }) 71 | }); 72 | }; 73 | 74 | const broadcast = (payload) => { 75 | return axios({ 76 | method: "post", 77 | url: `${LINE_MESSAGING_API}/message/broadcast`, 78 | headers: LINE_HEADER, 79 | data: JSON.stringify({ 80 | messages: [payload] 81 | }) 82 | }); 83 | }; 84 | 85 | const postToDialogflow = async (req) => { 86 | req.headers.host = "dialogflow.cloud.google.com"; 87 | return axios({ 88 | url: `https://dialogflow.cloud.google.com/v1/integrations/line/webhook/${functions.config().dialogflow.agent_id}`, 89 | headers: req.headers, 90 | method: "post", 91 | data: req.body 92 | }); 93 | }; 94 | 95 | const verifySignature = (originalSignature, body) => { 96 | let text = JSON.stringify(body); 97 | text = text.replace(/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g, (e) => { 98 | return "\\u" + e.charCodeAt(0).toString(16).toUpperCase() + "\\u" + e.charCodeAt(1).toString(16).toUpperCase(); 99 | }); 100 | const signature = crypto.createHmac("SHA256", LINE_CHANNEL_SECRET).update(text).digest("base64").toString(); 101 | if (signature !== originalSignature) { 102 | functions.logger.error("Unauthorized"); 103 | return false; 104 | } 105 | return true; 106 | }; 107 | 108 | module.exports = { reply, push, multicast, broadcast, getProfile, getProfileGroup, getProfileRoom, postToDialogflow, verifySignature }; --------------------------------------------------------------------------------