├── .editorconfig ├── .eslintrc.js ├── .eslintrc.prepublish.js ├── .gitignore ├── .npmignore ├── .prettierrc.js ├── .vscode └── extensions.json ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── credentials ├── LineMessagingAuthApi.credentials.ts └── LineWebhookAuthApi.credentials.ts ├── gulpfile.js ├── images ├── channel_secret.png ├── installation.png ├── line_webhook.png ├── message_node_settings.png ├── message_nodes.png ├── messaging_api_cred.png └── webhook_settings.png ├── index.js ├── nodes ├── LineMessageNode │ ├── LineMessageNode.node.json │ ├── LineMessageNode.node.ts │ ├── LineMessageNodeDescription.ts │ └── line.svg ├── LineMessaging │ ├── LineMessaging.node.json │ ├── LineMessaging.node.ts │ ├── LineMessagingDescription.ts │ └── line.svg └── LineWebhook │ ├── LineWebhook.node.json │ ├── LineWebhook.node.ts │ ├── description.ts │ ├── line.svg │ └── utils.ts ├── package-lock.json ├── package.json ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [package.json] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | 18 | [*.yml] 19 | indent_style = space 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@types/eslint').ESLint.ConfigData} 3 | */ 4 | module.exports = { 5 | root: true, 6 | 7 | env: { 8 | browser: true, 9 | es6: true, 10 | node: true, 11 | }, 12 | 13 | parser: '@typescript-eslint/parser', 14 | 15 | parserOptions: { 16 | project: ['./tsconfig.json'], 17 | sourceType: 'module', 18 | extraFileExtensions: ['.json'], 19 | }, 20 | 21 | ignorePatterns: ['.eslintrc.js', '**/*.js', '**/node_modules/**', '**/dist/**'], 22 | 23 | overrides: [ 24 | { 25 | files: ['package.json'], 26 | plugins: ['eslint-plugin-n8n-nodes-base'], 27 | extends: ['plugin:n8n-nodes-base/community'], 28 | rules: { 29 | 'n8n-nodes-base/community-package-json-name-still-default': 'off', 30 | }, 31 | }, 32 | { 33 | files: ['./credentials/**/*.ts'], 34 | plugins: ['eslint-plugin-n8n-nodes-base'], 35 | extends: ['plugin:n8n-nodes-base/credentials'], 36 | rules: { 37 | 'n8n-nodes-base/cred-class-field-documentation-url-missing': 'off', 38 | 'n8n-nodes-base/cred-class-field-documentation-url-miscased': 'off', 39 | }, 40 | }, 41 | { 42 | files: ['./nodes/**/*.ts'], 43 | plugins: ['eslint-plugin-n8n-nodes-base'], 44 | extends: ['plugin:n8n-nodes-base/nodes'], 45 | rules: { 46 | 'n8n-nodes-base/node-execute-block-missing-continue-on-fail': 'off', 47 | 'n8n-nodes-base/node-resource-description-filename-against-convention': 'off', 48 | 'n8n-nodes-base/node-param-fixed-collection-type-unsorted-items': 'off', 49 | }, 50 | }, 51 | ], 52 | }; 53 | -------------------------------------------------------------------------------- /.eslintrc.prepublish.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@types/eslint').ESLint.ConfigData} 3 | */ 4 | module.exports = { 5 | extends: "./.eslintrc.js", 6 | 7 | overrides: [ 8 | { 9 | files: ['package.json'], 10 | plugins: ['eslint-plugin-n8n-nodes-base'], 11 | rules: { 12 | 'n8n-nodes-base/community-package-json-name-still-default': 'error', 13 | }, 14 | }, 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .tmp 4 | tmp 5 | dist 6 | npm-debug.log* 7 | yarn.lock 8 | .vscode/launch.json 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.tsbuildinfo 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * https://prettier.io/docs/en/options.html#semicolons 4 | */ 5 | semi: true, 6 | 7 | /** 8 | * https://prettier.io/docs/en/options.html#trailing-commas 9 | */ 10 | trailingComma: 'all', 11 | 12 | /** 13 | * https://prettier.io/docs/en/options.html#bracket-spacing 14 | */ 15 | bracketSpacing: true, 16 | 17 | /** 18 | * https://prettier.io/docs/en/options.html#tabs 19 | */ 20 | useTabs: true, 21 | 22 | /** 23 | * https://prettier.io/docs/en/options.html#tab-width 24 | */ 25 | tabWidth: 2, 26 | 27 | /** 28 | * https://prettier.io/docs/en/options.html#arrow-function-parentheses 29 | */ 30 | arrowParens: 'always', 31 | 32 | /** 33 | * https://prettier.io/docs/en/options.html#quotes 34 | */ 35 | singleQuote: true, 36 | 37 | /** 38 | * https://prettier.io/docs/en/options.html#quote-props 39 | */ 40 | quoteProps: 'as-needed', 41 | 42 | /** 43 | * https://prettier.io/docs/en/options.html#end-of-line 44 | */ 45 | endOfLine: 'lf', 46 | 47 | /** 48 | * https://prettier.io/docs/en/options.html#print-width 49 | */ 50 | printWidth: 100, 51 | }; 52 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "EditorConfig.EditorConfig", 5 | "esbenp.prettier-vscode", 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at jan@n8n.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2022 n8n 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # n8n-nodes-linewebhook 2 | 3 | This is an n8n Line support. It lets you create a line chatbot with n8n without any coding. 4 | 5 | [n8n](https://n8n.io/) is a [fair-code licensed](https://docs.n8n.io/reference/license/) workflow automation platform. 6 | 7 | [Installation](#installation) 8 | [Operations](#operations) 9 | [Credentials](#credentials) 10 | 11 | ## Installation 12 | 13 | ### Community Nodes (Recommended) 14 | 15 | 1. Go to Settings > Community Nodes. 16 | 2. Select Install. 17 | 3. Enter n8n-nodes-linewebhook in Enter npm package name. 18 | 4. Agree to the risks of using community nodes: select I understand the risks of installing unverified code from a public source. 19 | 5. Select Install. 20 | After installing the node, you can use it like any other node. n8n displays the node in search results in the Nodes panel. 21 | 22 | ![Community node installation](images/installation.png?raw=true "Community node installation") 23 | 24 | ### Manual installation 25 | 26 | To get started install the package in your n8n root directory: 27 | 28 | ``` 29 | npm install n8n-nodes-linewebhook 30 | ``` 31 | 32 | For Docker-based deployments, add the following line before the font installation command in your n8n Dockerfile: 33 | 34 | RUN cd /usr/local/lib/node_modules/n8n && npm install n8n-nodes-linewebhook 35 | 36 | ## Nodes 37 | 38 | ### Line Webhook 39 | 40 | Supported event types: 41 | 42 | - message 43 | 44 | - text 45 | - audio 46 | - sticker 47 | - image 48 | - video 49 | - location 50 | 51 | - postback 52 | - join 53 | - leave 54 | - member join 55 | - member leave 56 | 57 | ![Line Webhook Node](images/line_webhook.png) 58 | ![Line Webhook Settings](images/webhook_settings.png) 59 | 60 | ### Compose Line Message Types 61 | 62 | - [Text](https://developers.line.biz/en/docs/messaging-api/message-types/#text-messages) 63 | - [Audio](https://developers.line.biz/en/docs/messaging-api/message-types/#audio-messages) 64 | - [Video](https://developers.line.biz/en/docs/messaging-api/message-types/#video-messages) 65 | - [Image](https://developers.line.biz/en/docs/messaging-api/message-types/#image-messages) 66 | - [Location](https://developers.line.biz/en/docs/messaging-api/message-types/#location-messages) 67 | - [Sticker](https://developers.line.biz/en/docs/messaging-api/message-types/#sticker-messages) 68 | - [Flex](https://developers.line.biz/en/docs/messaging-api/message-types/#flex-messages) 69 | 70 | ### Line Messaging APIs 71 | 72 | #### Send Message 73 | 74 | Specify either replyToken to reply message or targetRecipient to send message to a group or a user. You can link the output of the previouse message node to be the content of message property of this node. 75 | 76 | ![Connect message node to api node](images/message_nodes.png) 77 | ![Set up the api node to send message](images/message_node_settings.png) 78 | 79 | #### Get Message Content 80 | 81 | [API document](https://developers.line.biz/en/reference/messaging-api/#get-group-summary) 82 | 83 | When receive multimedia message from webhook, you need to use this node to retrieve the content of the file. For example, if user sends you an image, you can use this node to retrieve the image, and send it to AWS S3. 84 | 85 | #### Get Group Chat Summary 86 | 87 | [API document](https://developers.line.biz/en/reference/messaging-api/#get-group-summary) 88 | 89 | Retrieve the group chat summary with a group chat id. 90 | 91 | #### Get User Profile 92 | 93 | [API document](https://developers.line.biz/en/reference/messaging-api/#get-profile) 94 | 95 | Retrieve the user's profile with a user id. 96 | 97 | ## Credentials 98 | 99 | ### Webhook 100 | 101 | 1. Sign up on [Line Developer Console](https://developers.line.biz/en/) 102 | 2. Create a messaging API channel, and then copy the channel secret 103 | 3. Paste the channel secret in node's credential setting 104 | 4. Configure the Webhook URL in the messaging API settings 105 | 106 | ![Set up channel secret](images/channel_secret.png?raw=true "Set up channel secret") 107 | 108 | ### Messaging API 109 | 110 | 1. Go to page "Messaging API" setting page in the line developer console. 111 | 2. Copy `Channel access token`. Create a new credential in the messaging api node and paste this access token in the "Channel Access Token" property. 112 | 113 | ![Credential example for Line Messaging API node](images/messaging_api_cred.png) 114 | 115 | ## Running Locally 116 | 117 | If you are running n8n locally, you can try some tunnel solutions to simulate the webhook event. You can use [ngrok](https://ngrok.com/) or [Cloudflare Zero Trust](https://www.reddit.com/r/n8n/comments/1igyw0e/comprehensive_guide_secure_n8n_with_cloudflare/) for example. 118 | 119 | -------------------------------------------------------------------------------- /credentials/LineMessagingAuthApi.credentials.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ICredentialType, 3 | INodeProperties, 4 | } from 'n8n-workflow'; 5 | 6 | export class LineMessagingAuthApi implements ICredentialType { 7 | name = 'lineMessagingAuthApi'; 8 | displayName = 'Line Messaging Auth API'; 9 | properties: INodeProperties[] = [ 10 | { 11 | displayName: 'Channel Access Token', 12 | name: 'channel_access_token', 13 | type: 'string', 14 | typeOptions: { password: true }, 15 | default: '', 16 | }, 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /credentials/LineWebhookAuthApi.credentials.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ICredentialType, 3 | INodeProperties, 4 | } from 'n8n-workflow'; 5 | 6 | export class LineWebhookAuthApi implements ICredentialType { 7 | name = 'lineWebhookAuthApi'; 8 | // eslint-disable-next-line n8n-nodes-base/cred-class-field-display-name-missing-api 9 | displayName = 'Line Webhook Auth Credential'; 10 | properties: INodeProperties[] = [ 11 | { 12 | displayName: 'Channel Secret', 13 | name: 'channel_secret', 14 | type: 'string', 15 | typeOptions: { password: true }, 16 | default: '', 17 | }, 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { task, src, dest } = require('gulp'); 3 | 4 | task('build:icons', copyIcons); 5 | 6 | function copyIcons() { 7 | const nodeSource = path.resolve('nodes', '**', '*.{png,svg}'); 8 | const nodeDestination = path.resolve('dist', 'nodes'); 9 | 10 | src(nodeSource).pipe(dest(nodeDestination)); 11 | 12 | const credSource = path.resolve('credentials', '**', '*.{png,svg}'); 13 | const credDestination = path.resolve('dist', 'credentials'); 14 | 15 | return src(credSource).pipe(dest(credDestination)); 16 | } 17 | -------------------------------------------------------------------------------- /images/channel_secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syshen/n8n-nodes-linewebhook/6575bd30c82fbb4b73803a3beb12ac078afdc4d8/images/channel_secret.png -------------------------------------------------------------------------------- /images/installation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syshen/n8n-nodes-linewebhook/6575bd30c82fbb4b73803a3beb12ac078afdc4d8/images/installation.png -------------------------------------------------------------------------------- /images/line_webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syshen/n8n-nodes-linewebhook/6575bd30c82fbb4b73803a3beb12ac078afdc4d8/images/line_webhook.png -------------------------------------------------------------------------------- /images/message_node_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syshen/n8n-nodes-linewebhook/6575bd30c82fbb4b73803a3beb12ac078afdc4d8/images/message_node_settings.png -------------------------------------------------------------------------------- /images/message_nodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syshen/n8n-nodes-linewebhook/6575bd30c82fbb4b73803a3beb12ac078afdc4d8/images/message_nodes.png -------------------------------------------------------------------------------- /images/messaging_api_cred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syshen/n8n-nodes-linewebhook/6575bd30c82fbb4b73803a3beb12ac078afdc4d8/images/messaging_api_cred.png -------------------------------------------------------------------------------- /images/webhook_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syshen/n8n-nodes-linewebhook/6575bd30c82fbb4b73803a3beb12ac078afdc4d8/images/webhook_settings.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syshen/n8n-nodes-linewebhook/6575bd30c82fbb4b73803a3beb12ac078afdc4d8/index.js -------------------------------------------------------------------------------- /nodes/LineMessageNode/LineMessageNode.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "n8n-nodes-base.LineMessageNode", 3 | "nodeVersion": "1.0", 4 | "codexVersion": "1.0", 5 | "categories": ["Communitcation"], 6 | "resources": { 7 | "primaryDocumentation": [ 8 | { 9 | "url": "https://developers.line.biz/en/reference/messaging-api/#message-objects" 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /nodes/LineMessageNode/LineMessageNode.node.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IExecuteFunctions, 3 | INodeExecutionData, 4 | INodeType, 5 | INodeTypeDescription, 6 | } from 'n8n-workflow'; 7 | import { messageTypes } from './LineMessageNodeDescription'; 8 | 9 | export class LineMessageNode implements INodeType { 10 | description: INodeTypeDescription = { 11 | displayName: 'Line Message', 12 | name: 'LineMessageNode', 13 | icon: 'file:line.svg', 14 | group: ['transform'], 15 | version: 1, 16 | description: 'Line Message Node', 17 | defaults: { 18 | name: 'LineMessageNode', 19 | }, 20 | inputs: ['main'], 21 | outputs: ['main'], 22 | credentials: [], 23 | properties: [ 24 | ...messageTypes 25 | ], 26 | }; 27 | 28 | // The execute method will go here 29 | async execute(this: IExecuteFunctions): Promise { 30 | const items = this.getInputData(); 31 | const length = items.length; 32 | const returnData: INodeExecutionData[] = []; 33 | for (let i = 0; i < length; i++) { 34 | const messageType = this.getNodeParameter('operation', i) as string; 35 | let message = null; 36 | if (messageType === 'text') { 37 | const text = this.getNodeParameter('text', i) as string; 38 | message = { 39 | type: 'text', 40 | text 41 | } 42 | } else if (messageType === 'image') { 43 | const originalContentUrl = this.getNodeParameter('originalContentUrl', i) as string; 44 | const previewImageUrl = this.getNodeParameter('previewImageUrl', i) as string; 45 | message = { 46 | type: 'image', 47 | originalContentUrl, 48 | previewImageUrl, 49 | } 50 | } else if (messageType === 'video') { 51 | const originalContentUrl = this.getNodeParameter('originalContentUrl', i) as string; 52 | const previewImageUrl = this.getNodeParameter('previewImageUrl', i) as string; 53 | message = { 54 | type: 'video', 55 | originalContentUrl, 56 | previewImageUrl, 57 | } 58 | } else if (messageType === 'audio') { 59 | const originalContentUrl = this.getNodeParameter('originalContentUrl', i) as string; 60 | const duration = this.getNodeParameter('duration', i) as number; 61 | message = { 62 | type: 'video', 63 | originalContentUrl, 64 | duration, 65 | } 66 | } else if (messageType === 'location') { 67 | const title = this.getNodeParameter('title', i) as string; 68 | const address = this.getNodeParameter('address', i) as string; 69 | const latitude = this.getNodeParameter('latitude', i) as number; 70 | const longitude = this.getNodeParameter('longitude', i) as number; 71 | message = { 72 | type: 'location', 73 | title, 74 | address, 75 | latitude, 76 | longitude 77 | } 78 | } else if (messageType === 'flex') { 79 | const altText = this.getNodeParameter('altText', i) as string; 80 | const flexContent = this.getNodeParameter('flexContent', i) as string; 81 | message = { 82 | type: 'flex', 83 | altText, 84 | contents: JSON.parse(flexContent) 85 | } 86 | } else if (messageType === 'sticker') { 87 | const packageId = this.getNodeParameter('packageId', i) as string; 88 | const stickerId = this.getNodeParameter('stickerId', i) as string; 89 | const quoteToken = this.getNodeParameter('quoteToken', i) as string; 90 | message = { 91 | type: 'sticker', 92 | packageId, 93 | stickerId, 94 | quoteToken 95 | } 96 | } 97 | 98 | returnData.push({ 99 | json: { 100 | message 101 | }, 102 | }); 103 | } 104 | return this.prepareOutputData(returnData); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /nodes/LineMessageNode/LineMessageNodeDescription.ts: -------------------------------------------------------------------------------- 1 | import type { INodeProperties } from 'n8n-workflow'; 2 | 3 | export const messageTypes: INodeProperties[] = [ 4 | { 5 | displayName: 'Message Type', 6 | name: 'operation', 7 | type: 'options', 8 | noDataExpression: true, 9 | options: [ 10 | { 11 | name: 'Audio', 12 | value: 'audio', 13 | description: 'Audio message', 14 | action: 'Create an audio message', 15 | }, 16 | { 17 | name: 'Flex', 18 | value: 'flex', 19 | description: 'Flexible type message', 20 | action: 'Create a flex message', 21 | 22 | }, 23 | { 24 | name: 'Image', 25 | value: 'image', 26 | description: 'Image message', 27 | action: 'Create an image message', 28 | }, 29 | { 30 | name: 'Location', 31 | value: 'location', 32 | description: 'Location message', 33 | action: 'Create a location message', 34 | }, 35 | { 36 | name: 'Sticker', 37 | value: 'sticker', 38 | description: 'Sticker message', 39 | action: 'Create a sticker message', 40 | }, 41 | { 42 | name: 'Text', 43 | value: 'text', 44 | description: 'Plain text message', 45 | action: 'Create a text message', 46 | }, 47 | { 48 | name: 'Video', 49 | value: 'video', 50 | description: 'Video message', 51 | action: 'Create a video message', 52 | }, 53 | ], 54 | default: 'text', 55 | }, 56 | { 57 | displayName: "Text", 58 | name: "text", 59 | type: "string", 60 | default: "", 61 | placeholder: "", 62 | description: "The text message you want to send to user", 63 | displayOptions: { 64 | show: { 65 | operation: ['text'], 66 | }, 67 | } 68 | }, 69 | // Sticker 70 | { 71 | displayName: 'Package ID', 72 | name: 'packageId', 73 | type: 'string', 74 | default: '', 75 | placeholder: '', 76 | required: true, 77 | description: 'Package ID for a set of stickers. For information on package IDs, see the https://developers.line.biz/en/docs/messaging-api/sticker-list/ .', 78 | displayOptions: { 79 | show: { 80 | operation: ['sticker'], 81 | }, 82 | }, 83 | }, 84 | { 85 | displayName: 'Sticker ID', 86 | name: 'stickerId', 87 | type: 'string', 88 | default: '', 89 | placeholder: '', 90 | required: true, 91 | description: 'Sticker ID. For a list of sticker IDs for stickers that can be sent with the Messaging API, see the https://developers.line.biz/en/docs/messaging-api/sticker-list/ .', 92 | displayOptions: { 93 | show: { 94 | operation: ['sticker'], 95 | }, 96 | }, 97 | }, 98 | { 99 | displayName: 'Quote Token', 100 | name: 'quoteToken', 101 | type: 'string', 102 | default: '', 103 | placeholder: '', 104 | description: 'Quote token of the message you want to quote', 105 | displayOptions: { 106 | show: { 107 | operation: ['sticker'], 108 | }, 109 | } 110 | }, 111 | // video, audio, image 112 | { 113 | displayName: 'URL', 114 | name: 'originalContentUrl', 115 | type: 'string', 116 | default: '', 117 | placeholder: '', 118 | required: true, 119 | description: 'The URL for image, video, or audio file (Max character limit: 2000)', 120 | displayOptions: { 121 | show: { 122 | operation: ['image', 'audio', 'video'], 123 | }, 124 | }, 125 | }, 126 | { 127 | displayName: 'Preview Url', 128 | name: 'previewImageUrl', 129 | type: 'string', 130 | default: '', 131 | placeholder: '', 132 | required: true, 133 | description: 'Preview image URL (Max character limit: 2000)', 134 | displayOptions: { 135 | show: { 136 | operation: ['image', 'video'], 137 | }, 138 | }, 139 | }, 140 | { 141 | displayName: 'Tracking ID', 142 | name: 'trackingId', 143 | type: 'string', 144 | default: '', 145 | placeholder: '', 146 | description: 'ID used to identify the video when Video viewing complete event occurs. If you send a video message with trackingId added, the video viewing complete event occurs when the user finishes watching the video.', 147 | displayOptions: { 148 | show: { 149 | operation: ['audio', 'video'], 150 | }, 151 | }, 152 | }, 153 | // audio 154 | { 155 | displayName: "Duration", 156 | name: "duration", 157 | type: "number", 158 | default: 0, 159 | placeholder: "", 160 | required: true, 161 | description: "Audio duration", 162 | displayOptions: { 163 | show: { 164 | operation: ['audio'], 165 | }, 166 | } 167 | }, 168 | // Location 169 | { 170 | displayName: "Title", 171 | name: "title", 172 | type: "string", 173 | default: "", 174 | placeholder: "", 175 | required: true, 176 | description: "Location title, max character limit: 100", 177 | displayOptions: { 178 | show: { 179 | operation: ['location'], 180 | }, 181 | } 182 | }, 183 | { 184 | displayName: "Address", 185 | name: "address", 186 | type: "string", 187 | default: "", 188 | placeholder: "", 189 | required: true, 190 | description: "Location address, max character limit: 100", 191 | displayOptions: { 192 | show: { 193 | operation: ['location'], 194 | }, 195 | } 196 | }, 197 | { 198 | displayName: "Latitude", 199 | name: "latitude", 200 | type: "number", 201 | default: 0, 202 | placeholder: "", 203 | required: true, 204 | description: "Location Latitude", 205 | displayOptions: { 206 | show: { 207 | operation: ['location'], 208 | }, 209 | } 210 | }, 211 | { 212 | displayName: "Longitude", 213 | name: "longitude", 214 | type: "number", 215 | default: 0, 216 | placeholder: "", 217 | required: true, 218 | description: "Location Longtidue", 219 | displayOptions: { 220 | show: { 221 | operation: ['location'], 222 | }, 223 | } 224 | }, 225 | // Flex messaage type options 226 | { 227 | displayName: "Alt Text", 228 | name: "altText", 229 | type: "string", 230 | default: "", 231 | placeholder: "", 232 | required: true, 233 | description: "Alternative text. When a user receives a message, it will appear in the device's notifications, talk list, and quote messages as an alternative to the Flex. Max character limit: 400", 234 | displayOptions: { 235 | show: { 236 | operation: ['flex'], 237 | }, 238 | } 239 | }, 240 | { 241 | displayName: "Flex Content", 242 | name: "flexContent", 243 | type: "json", 244 | default: '', 245 | placeholder: '', 246 | required: true, 247 | description: "The message payload for Flex message. Use Flex simulator to create message payload and paste the JSON code here.", 248 | displayOptions: { 249 | show: { 250 | operation: ['flex'], 251 | }, 252 | } 253 | }, 254 | ]; 255 | -------------------------------------------------------------------------------- /nodes/LineMessageNode/line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/LineMessaging/LineMessaging.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "n8n-nodes-base.LineMessaging", 3 | "nodeVersion": "1.0", 4 | "codexVersion": "1.0", 5 | "categories": ["Communitcation"], 6 | "resources": { 7 | "credentialDocumentation": [ 8 | { 9 | "url": "https://developers.line.biz/en/docs/basics/channel-access-token/" 10 | } 11 | ], 12 | "primaryDocumentation": [ 13 | { 14 | "url": "https://developers.line.biz/en/docs/messaging-api/sending-messages/" 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /nodes/LineMessaging/LineMessaging.node.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IExecuteFunctions, 3 | INodeExecutionData, 4 | INodeType, 5 | INodeTypeDescription, 6 | ICredentialDataDecryptedObject, 7 | NodeApiError, 8 | } from 'n8n-workflow'; 9 | 10 | import { messagingAPIOperations } from './LineMessagingDescription'; 11 | 12 | import { messagingApi } from '@line/bot-sdk'; 13 | const { MessagingApiClient, MessagingApiBlobClient } = messagingApi; 14 | 15 | export class LineMessaging implements INodeType { 16 | description: INodeTypeDescription = { 17 | displayName: 'Line Messaging API', 18 | name: 'LineMessaging', 19 | icon: 'file:line.svg', 20 | group: ['transform'], 21 | version: 1, 22 | description: 'Line Messaging API', 23 | defaults: { 24 | name: 'LineMessaging', 25 | }, 26 | inputs: ['main'], 27 | outputs: ['main'], 28 | credentials: [ 29 | { 30 | name: 'lineMessagingAuthApi', 31 | required: true, 32 | }, 33 | ], 34 | properties: [ 35 | ...messagingAPIOperations 36 | ], 37 | }; 38 | 39 | // The execute method will go here 40 | async execute(this: IExecuteFunctions): Promise { 41 | let expectedCred: ICredentialDataDecryptedObject | undefined; 42 | expectedCred = await this.getCredentials('lineMessagingAuthApi') as { 43 | channel_access_token: string 44 | }; 45 | if (expectedCred === undefined || !expectedCred.channel_access_token) { 46 | // Data is not defined on node so can not authenticate 47 | console.error('No auth provided'); 48 | throw new NodeApiError(this.getNode(), {}); 49 | } 50 | 51 | const client = new MessagingApiClient({ 52 | channelAccessToken: expectedCred.channel_access_token as string, 53 | }); 54 | const blobClient = new MessagingApiBlobClient({ 55 | channelAccessToken: expectedCred.channel_access_token as string, 56 | }) 57 | 58 | const items = this.getInputData(); 59 | const length = items.length; 60 | const returnData: INodeExecutionData[] = []; 61 | 62 | for (let i = 0; i < length; i++) { 63 | const operation = this.getNodeParameter('operation', i) as string; 64 | if (operation === 'message') { 65 | const replyToken = this.getNodeParameter('replyToken', i) as string; 66 | const message = this.getNodeParameter('message', i); 67 | let messages: messagingApi.Message[] = []; 68 | if (message instanceof Array) { 69 | messages = message as messagingApi.Message[]; 70 | } else { 71 | messages = [message as messagingApi.Message]; 72 | } 73 | if (replyToken) { 74 | await client.replyMessage({ 75 | replyToken, 76 | messages: messages, 77 | }); 78 | } else { 79 | const targetRecepient = this.getNodeParameter('targetRecipient', i) as string; 80 | if (targetRecepient) { 81 | await client.pushMessage({ 82 | to: targetRecepient, 83 | messages: messages, 84 | }); 85 | } 86 | } 87 | returnData.push(items[i]); 88 | } else if (operation === 'multicast') { 89 | const targetRecipients = this.getNodeParameter('targetRecipients', i) as string[]; 90 | const message = this.getNodeParameter('message', i); 91 | let messages: messagingApi.Message[] = []; 92 | if (message instanceof Array) { 93 | messages = message as messagingApi.Message[]; 94 | } else { 95 | messages = [message as messagingApi.Message]; 96 | } 97 | await client.multicast({ 98 | to: targetRecipients, 99 | messages: messages, 100 | }); 101 | } else if (operation === 'getMessageContent') { 102 | const messageId = this.getNodeParameter('messageId', i) as string; 103 | const { httpResponse, body } = await blobClient.getMessageContentWithHttpInfo(messageId); 104 | const contentType = httpResponse.headers.get('content-type') as string; 105 | returnData.push({json: {}, 106 | binary: { 107 | data: await this.helpers.prepareBinaryData( 108 | body, 'data', contentType 109 | ), 110 | }}); 111 | } else if (operation === 'getGroupChatSummary') { 112 | const groupId = this.getNodeParameter('groupId', i) as string; 113 | const group_summary_resp = await client.getGroupSummary(groupId); 114 | returnData.push({json: group_summary_resp}); 115 | } else if (operation === 'getGroupChatMemberUserIds') { 116 | const groupId = this.getNodeParameter('groupId', i) as string; 117 | const group_member_user_ids_resp = await client.getGroupMembersIds(groupId); 118 | returnData.push({json: group_member_user_ids_resp}); 119 | } else if (operation === 'getGroupChatMemberProfile') { 120 | const groupId = this.getNodeParameter('groupId', i) as string; 121 | const userId = this.getNodeParameter('userId', i) as string; 122 | const group_member_profile_resp = await client.getGroupMemberProfile(groupId, userId); 123 | returnData.push({json: group_member_profile_resp}); 124 | } else if (operation === 'getProfile') { 125 | const userId = this.getNodeParameter('userId', i) as string; 126 | const user_profile_resp = await client.getProfile(userId); 127 | returnData.push({json: user_profile_resp}); 128 | } 129 | } 130 | return this.prepareOutputData(returnData); 131 | 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /nodes/LineMessaging/LineMessagingDescription.ts: -------------------------------------------------------------------------------- 1 | import type { INodeProperties } from 'n8n-workflow'; 2 | 3 | export const messagingAPIOperations: INodeProperties[] = [ 4 | { 5 | displayName: 'Operations', 6 | name: 'operation', 7 | type: 'options', 8 | noDataExpression: true, 9 | options: [ 10 | { 11 | name: 'Get Group Chat Member Profile', 12 | value: 'getGroupChatMemberProfile', 13 | description: 14 | 'Gets the profile information of a member of a group chat that the LINE Official Account is in if the user ID of the group member is known', 15 | action: 'Get group chat member profile', 16 | }, 17 | { 18 | name: 'Get Group Chat Member User IDs', 19 | value: 'getGroupChatMemberUserIds', 20 | description: 21 | 'Gets the user IDs of the members of a group chat that the LINE Official Account is in. This includes user IDs of users who have not added the LINE Official Account as a friend or has blocked the LINE Official Account.', 22 | action: 'Get group chat member user ids', 23 | }, 24 | { 25 | name: 'Get Group Chat Summary', 26 | value: 'getGroupChatSummary', 27 | description: 28 | 'Gets the group ID, group name, and group icon URL of a group chat where the LINE Official Account is a member', 29 | action: 'Get group chat summary', 30 | }, 31 | { 32 | name: 'Get Message Content', 33 | value: 'getMessageContent', 34 | description: 35 | 'Gets images, videos, audio, and files sent by users using message IDs received via the webhook', 36 | action: 'Get message content', 37 | }, 38 | { 39 | name: 'Get User Profile', 40 | value: 'getProfile', 41 | description: 'You can get the profile information of users', 42 | action: 'Get user profile', 43 | }, 44 | { 45 | name: 'Multicast Message', 46 | value: 'multicast', 47 | description: 'Send a message to multiple users', 48 | action: 'Send a multicast message', 49 | }, 50 | { 51 | name: 'Send Message', 52 | value: 'message', 53 | description: 'Send or reply a message', 54 | action: 'Send a message', 55 | }, 56 | ], 57 | default: 'message', 58 | }, 59 | { 60 | displayName: 'Message', 61 | name: 'message', 62 | type: 'json', 63 | default: '', 64 | placeholder: '', 65 | required: true, 66 | description: 'The message payload', 67 | displayOptions: { 68 | show: { 69 | operation: ['message', 'multicast'], 70 | }, 71 | }, 72 | }, 73 | { 74 | displayName: 'ReplyToken', 75 | name: 'replyToken', 76 | type: 'string', 77 | default: '', 78 | placeholder: '', 79 | description: 'The reply token for reply message', 80 | displayOptions: { 81 | show: { 82 | operation: ['message'], 83 | }, 84 | }, 85 | }, 86 | { 87 | displayName: 'Message ID', 88 | name: 'messageId', 89 | type: 'string', 90 | default: '', 91 | placeholder: '', 92 | description: 'The message ID to retrieve message content', 93 | displayOptions: { 94 | show: { 95 | operation: ['getMessageContent'], 96 | }, 97 | }, 98 | }, 99 | { 100 | displayName: 'Group ID', 101 | name: 'groupId', 102 | type: 'string', 103 | default: '', 104 | placeholder: '', 105 | description: 'Group ID. Found in the source object of webhook event objects.', 106 | displayOptions: { 107 | show: { 108 | operation: ['getGroupChatSummary', 'getGroupChatMemberUserIds', 'getGroupChatMemberProfile'], 109 | }, 110 | }, 111 | }, 112 | { 113 | displayName: 'User ID', 114 | name: 'userId', 115 | type: 'string', 116 | default: '', 117 | placeholder: '', 118 | description: 'User ID that is returned in a webhook event object. Do not use the LINE ID found on LINE.', 119 | displayOptions: { 120 | show: { 121 | operation: ['getProfile', 'getGroupChatMemberProfile'], 122 | }, 123 | }, 124 | }, 125 | { 126 | displayName: 'Target Recipient', 127 | name: 'targetRecipient', 128 | type: 'string', 129 | default: '', 130 | placeholder: '', 131 | description: 'ID of the target recipient. Use a userId, groupId, or roomId to send a message to a user, group, or room.', 132 | displayOptions: { 133 | show: { 134 | operation: ['message'], 135 | }, 136 | }, 137 | }, 138 | { 139 | displayName: 'Target Recipients', 140 | name: 'targetRecipients', 141 | type: 'json', 142 | default: '', 143 | placeholder: '', 144 | description: 'Array of user IDs, group IDs, or room IDs to send a message to', 145 | displayOptions: { 146 | show: { 147 | operation: ['multicast'], 148 | }, 149 | }, 150 | }, 151 | ]; 152 | -------------------------------------------------------------------------------- /nodes/LineMessaging/line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/LineWebhook/LineWebhook.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": "n8n-nodes-base.LineWebhook", 3 | "nodeVersion": "1.0", 4 | "codexVersion": "1.0", 5 | "categories": ["Communitcation"], 6 | "resources": { 7 | "credentialDocumentation": [ 8 | { 9 | "url": "https://developers.line.biz/en/docs/messaging-api/receiving-messages/#verify-signature" 10 | } 11 | ], 12 | "primaryDocumentation": [ 13 | { 14 | "url": "https://developers.line.biz/en/docs/messaging-api/receiving-messages/" 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /nodes/LineWebhook/LineWebhook.node.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IDataObject, 3 | IWebhookFunctions, 4 | IWebhookResponseData, 5 | INodeType, 6 | INodeTypeDescription, 7 | ICredentialDataDecryptedObject, 8 | NodeApiError, 9 | INodeInputConfiguration, 10 | INodeExecutionData, 11 | ConnectionTypes, 12 | } from 'n8n-workflow'; 13 | import { 14 | defaultWebhookDescription, 15 | } from './description'; 16 | import crypto from 'crypto'; 17 | 18 | 19 | function s2b(str: string, encoding: BufferEncoding): Buffer { 20 | return Buffer.from(str, encoding); 21 | } 22 | 23 | function safeCompare(a: Buffer, b: Buffer): boolean { 24 | if (a.length !== b.length) { 25 | return false; 26 | } 27 | return crypto.timingSafeEqual(a, b); 28 | } 29 | 30 | function validateSignature( 31 | body: string | Buffer, 32 | channelSecret: string, 33 | signature: string, 34 | ): boolean { 35 | return safeCompare( 36 | crypto.createHmac("SHA256", channelSecret).update(body.toString('utf8')).digest(), 37 | s2b(signature, "base64"), 38 | ); 39 | } 40 | 41 | function outputs(): INodeInputConfiguration[] { 42 | const messageTypes = ['text', 'audio', 'sticker', 'image', 'video', 'location']; 43 | const eventTypes = ['postback', 'join', 'leave', 'memberJoined', 'memberLeft']; 44 | return [ 45 | ...messageTypes.map((messageType) => ({ 46 | displayName: messageType, 47 | required: false, 48 | type: 'main' as ConnectionTypes, 49 | })), 50 | ...eventTypes.map((eventType) => ({ 51 | displayName: eventType, 52 | required: false, 53 | type: 'main' as ConnectionTypes, 54 | })) 55 | ] 56 | } 57 | 58 | function indexOfOuputs(type: string) { 59 | for (let index = 0; index < outputs().length; index++) { 60 | if (outputs()[index].displayName === type) { 61 | return index; 62 | } 63 | } 64 | return null; 65 | } 66 | 67 | export class LineWebhook implements INodeType { 68 | description: INodeTypeDescription = { 69 | displayName: 'Line Webhook', 70 | name: 'LineWebhook', 71 | icon: 'file:line.svg', 72 | group: ['trigger'], 73 | version: 1, 74 | description: 'Line Webhook', 75 | defaults: { 76 | name: 'LineWebhook', 77 | }, 78 | inputs: ['main'], 79 | outputs: outputs(), 80 | webhooks: [defaultWebhookDescription], 81 | credentials: [ 82 | { 83 | name: 'lineWebhookAuthApi', 84 | required: true, 85 | }, 86 | ], 87 | properties: [ 88 | { 89 | displayName: 'Path', 90 | name: 'path', 91 | type: 'string', 92 | default: '', 93 | placeholder: 'line-webhook', 94 | required: true, 95 | description: 'The path to listen to' 96 | } 97 | ], 98 | }; 99 | 100 | // The execute method will go here 101 | async webhook(this: IWebhookFunctions): Promise { 102 | const headerName = 'x-line-signature'; 103 | const headers = this.getHeaderData(); 104 | const req = this.getRequestObject(); 105 | const body = req.rawBody; 106 | 107 | try { 108 | let expectedCred: ICredentialDataDecryptedObject | undefined; 109 | expectedCred = await this.getCredentials('lineWebhookAuthApi') as { 110 | channel_secret: string 111 | }; 112 | if (expectedCred === undefined || !expectedCred.channel_secret) { 113 | // Data is not defined on node so can not authenticate 114 | console.error('No auth provided'); 115 | throw new NodeApiError(this.getNode(), {}); 116 | } 117 | 118 | if ( 119 | !headers.hasOwnProperty(headerName) || 120 | !validateSignature(body, expectedCred.channel_secret as string, (headers as IDataObject)[headerName] as string) 121 | ) { 122 | // Provided authentication data is wrong 123 | throw new NodeApiError(this.getNode(), {}); 124 | } 125 | } catch(error) { 126 | const resp = this.getResponseObject(); 127 | resp.writeHead(500, { 'WWW-Authenticate': 'Basic realm="Webhook"' }); 128 | resp.end(error.message); 129 | return { noWebhookResponse: true }; 130 | } 131 | 132 | const returnData: IDataObject[][] = []; 133 | for (let index = 0; index < outputs().length; index++) { 134 | returnData.push([]); 135 | } 136 | 137 | const bodyObject = this.getBodyData(); 138 | const destination = bodyObject['destination']; 139 | if (bodyObject['events']) { 140 | for (const event of (bodyObject['events'] as Array)) { 141 | const eventType = (event['type'] as string); 142 | if (eventType === 'message') { 143 | const type = (event['message'] as IDataObject)['type']; 144 | let oi = indexOfOuputs(type as string); 145 | if (oi !== null) { 146 | returnData[oi].push({ 147 | destination, 148 | event 149 | }); 150 | } 151 | } else { 152 | let oi = indexOfOuputs(eventType as string); 153 | if (oi !== null) { 154 | returnData[oi].push({ 155 | destination, 156 | event 157 | }); 158 | } 159 | } 160 | } 161 | } 162 | 163 | const outputData: INodeExecutionData[][] = []; 164 | for (let idx = 0; idx < returnData.length; idx++) { 165 | outputData.push(this.helpers.returnJsonArray(returnData[idx])); 166 | } 167 | 168 | return { 169 | workflowData: outputData, 170 | }; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /nodes/LineWebhook/description.ts: -------------------------------------------------------------------------------- 1 | import type { IWebhookDescription } from 'n8n-workflow'; 2 | 3 | export const defaultWebhookDescription: IWebhookDescription = { 4 | name: 'default', 5 | httpMethod: '={{$parameter["httpMethod"] || "POST"}}', 6 | isFullPath: true, 7 | responseCode: '200', 8 | responseMode: 'responseNode', 9 | responseData: 'allEntries', 10 | responseBinaryPropertyName: '={{$parameter["responseBinaryPropertyName"]}}', 11 | responseContentType: 'application/json', 12 | responsePropertyName: '={{$parameter["options"]["responsePropertyName"]}}', 13 | responseHeaders: '={{$parameter["options"]["responseHeaders"]}}', 14 | path: '={{$parameter["path"]}}', 15 | }; 16 | 17 | -------------------------------------------------------------------------------- /nodes/LineWebhook/line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/LineWebhook/utils.ts: -------------------------------------------------------------------------------- 1 | import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; 2 | import type { IWebhookFunctions, INodeExecutionData, IDataObject } from 'n8n-workflow'; 3 | 4 | type WebhookParameters = { 5 | httpMethod: string; 6 | responseMode: string; 7 | responseData: string; 8 | responseCode?: number; //typeVersion <= 1.1 9 | options?: { 10 | responseData?: string; 11 | responseCode?: { 12 | values?: { 13 | responseCode: number; 14 | customCode?: number; 15 | }; 16 | }; 17 | noResponseBody?: boolean; 18 | }; 19 | }; 20 | 21 | export const getResponseCode = (parameters: WebhookParameters) => { 22 | if (parameters.responseCode) { 23 | return parameters.responseCode; 24 | } 25 | const responseCodeOptions = parameters.options; 26 | if (responseCodeOptions?.responseCode?.values) { 27 | const { responseCode, customCode } = responseCodeOptions.responseCode.values; 28 | 29 | if (customCode) { 30 | return customCode; 31 | } 32 | 33 | return responseCode; 34 | } 35 | return 200; 36 | }; 37 | 38 | export const getResponseData = (parameters: WebhookParameters) => { 39 | const { responseData, responseMode, options } = parameters; 40 | if (responseData) return responseData; 41 | 42 | if (responseMode === 'onReceived') { 43 | const data = options?.responseData; 44 | if (data) return data; 45 | } 46 | 47 | if (options?.noResponseBody) return 'noData'; 48 | 49 | return undefined; 50 | }; 51 | 52 | export const configuredOutputs = (parameters: WebhookParameters) => { 53 | const httpMethod = parameters.httpMethod as string | string[]; 54 | 55 | if (!Array.isArray(httpMethod)) 56 | return [ 57 | { 58 | type: `${NodeConnectionType.Main}`, 59 | displayName: httpMethod, 60 | }, 61 | ]; 62 | 63 | const outputs = httpMethod.map((method) => { 64 | return { 65 | type: `${NodeConnectionType.Main}`, 66 | displayName: method, 67 | }; 68 | }); 69 | 70 | return outputs; 71 | }; 72 | 73 | export const setupOutputConnection = ( 74 | ctx: IWebhookFunctions, 75 | method: string, 76 | additionalData: { 77 | jwtPayload?: IDataObject; 78 | }, 79 | ) => { 80 | const httpMethod = ctx.getNodeParameter('httpMethod', []) as string[] | string; 81 | let webhookUrl = ctx.getNodeWebhookUrl('default') as string; 82 | const executionMode = ctx.getMode() === 'manual' ? 'test' : 'production'; 83 | 84 | if (executionMode === 'test') { 85 | webhookUrl = webhookUrl.replace('/webhook/', '/webhook-test/'); 86 | } 87 | 88 | // multi methods could be set in settings of node, so we need to check if it's an array 89 | if (!Array.isArray(httpMethod)) { 90 | return (outputData: INodeExecutionData): INodeExecutionData[][] => { 91 | outputData.json.webhookUrl = webhookUrl; 92 | outputData.json.executionMode = executionMode; 93 | if (additionalData?.jwtPayload) { 94 | outputData.json.jwtPayload = additionalData.jwtPayload; 95 | } 96 | return [[outputData]]; 97 | }; 98 | } 99 | 100 | const outputIndex = httpMethod.indexOf(method.toUpperCase()); 101 | const outputs: INodeExecutionData[][] = httpMethod.map(() => []); 102 | 103 | return (outputData: INodeExecutionData): INodeExecutionData[][] => { 104 | outputData.json.webhookUrl = webhookUrl; 105 | outputData.json.executionMode = executionMode; 106 | if (additionalData?.jwtPayload) { 107 | outputData.json.jwtPayload = additionalData.jwtPayload; 108 | } 109 | outputs[outputIndex] = [outputData]; 110 | return outputs; 111 | }; 112 | }; 113 | 114 | export const isIpWhitelisted = ( 115 | whitelist: string | string[] | undefined, 116 | ips: string[], 117 | ip?: string, 118 | ) => { 119 | if (whitelist === undefined || whitelist === '') { 120 | return true; 121 | } 122 | 123 | if (!Array.isArray(whitelist)) { 124 | whitelist = whitelist.split(',').map((entry) => entry.trim()); 125 | } 126 | 127 | for (const address of whitelist) { 128 | if (ip && ip.includes(address)) { 129 | return true; 130 | } 131 | 132 | if (ips.some((entry) => entry.includes(address))) { 133 | return true; 134 | } 135 | } 136 | 137 | return false; 138 | }; 139 | 140 | export const checkResponseModeConfiguration = (context: IWebhookFunctions) => { 141 | const responseMode = context.getNodeParameter('responseMode', 'onReceived') as string; 142 | const connectedNodes = context.getChildNodes(context.getNode().name); 143 | 144 | const isRespondToWebhookConnected = connectedNodes.some( 145 | (node) => node.type === 'n8n-nodes-base.respondToWebhook', 146 | ); 147 | 148 | if (!isRespondToWebhookConnected && responseMode === 'responseNode') { 149 | throw new NodeOperationError( 150 | context.getNode(), 151 | new Error('No Respond to Webhook node found in the workflow'), 152 | { 153 | description: 154 | 'Insert a Respond to Webhook node to your workflow to respond to the webhook or choose another option for the “Respond” parameter', 155 | }, 156 | ); 157 | } 158 | 159 | if (isRespondToWebhookConnected && responseMode !== 'responseNode') { 160 | throw new NodeOperationError( 161 | context.getNode(), 162 | new Error('Webhook node not correctly configured'), 163 | { 164 | description: 165 | 'Set the “Respond” parameter to “Using Respond to Webhook Node” or remove the Respond to Webhook node', 166 | }, 167 | ); 168 | } 169 | }; 170 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "n8n-nodes-linewebhook", 3 | "version": "0.1.50", 4 | "description": "Line n8n webhook & messaging nodes", 5 | "keywords": [ 6 | "n8n-community-node-package", 7 | "communication", 8 | "line" 9 | ], 10 | "license": "MIT", 11 | "homepage": "", 12 | "author": { 13 | "name": "Steven Shen", 14 | "email": "sysheen@gmail.com" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git@github.com:syshen/n8n-nodes-linewebhook.git" 19 | }, 20 | "main": "index.js", 21 | "scripts": { 22 | "build": "tsc && gulp build:icons", 23 | "dev": "tsc --watch", 24 | "format": "prettier nodes credentials --write", 25 | "lint": "eslint nodes credentials package.json", 26 | "lintfix": "eslint nodes credentials package.json --fix", 27 | "prepublishOnly": "npm run build && npm run lint -c .eslintrc.prepublish.js nodes credentials package.json" 28 | }, 29 | "files": [ 30 | "dist" 31 | ], 32 | "n8n": { 33 | "n8nNodesApiVersion": 1, 34 | "credentials": [ 35 | "dist/credentials/LineMessagingAuthApi.credentials.js", 36 | "dist/credentials/LineWebhookAuthApi.credentials.js" 37 | ], 38 | "nodes": [ 39 | "dist/nodes/LineMessaging/LineMessaging.node.js", 40 | "dist/nodes/LineWebhook/LineWebhook.node.js", 41 | "dist/nodes/LineMessageNode/LineMessageNode.node.js" 42 | ] 43 | }, 44 | "devDependencies": { 45 | "@types/express": "^4.17.6", 46 | "@types/request-promise-native": "~1.0.15", 47 | "@typescript-eslint/parser": "~5.45", 48 | "eslint-plugin-n8n-nodes-base": "^1.11.0", 49 | "gulp": "^5.0.0", 50 | "n8n-core": "*", 51 | "n8n-workflow": "^1.36.1", 52 | "prettier": "^2.7.1", 53 | "typescript": "~4.8.4" 54 | }, 55 | "dependencies": { 56 | "@line/bot-sdk": "^9.2.2" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "target": "es2019", 7 | "lib": ["es2019", "es2020", "es2022.error"], 8 | "removeComments": true, 9 | "useUnknownInCatchVariables": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "noImplicitAny": true, 12 | "noImplicitReturns": true, 13 | "noUnusedLocals": true, 14 | "strictNullChecks": true, 15 | "preserveConstEnums": true, 16 | "esModuleInterop": true, 17 | "resolveJsonModule": true, 18 | "incremental": true, 19 | "declaration": true, 20 | "sourceMap": true, 21 | "skipLibCheck": true, 22 | "outDir": "./dist/", 23 | }, 24 | "include": [ 25 | "credentials/**/*", 26 | "nodes/**/*", 27 | "nodes/**/*.json", 28 | "package.json", 29 | ], 30 | } 31 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "linterOptions": { 3 | "exclude": [ 4 | "node_modules/**/*" 5 | ] 6 | }, 7 | "defaultSeverity": "error", 8 | "jsRules": {}, 9 | "rules": { 10 | "array-type": [ 11 | true, 12 | "array-simple" 13 | ], 14 | "arrow-return-shorthand": true, 15 | "ban": [ 16 | true, 17 | { 18 | "name": "Array", 19 | "message": "tsstyle#array-constructor" 20 | } 21 | ], 22 | "ban-types": [ 23 | true, 24 | [ 25 | "Object", 26 | "Use {} instead." 27 | ], 28 | [ 29 | "String", 30 | "Use 'string' instead." 31 | ], 32 | [ 33 | "Number", 34 | "Use 'number' instead." 35 | ], 36 | [ 37 | "Boolean", 38 | "Use 'boolean' instead." 39 | ] 40 | ], 41 | "class-name": true, 42 | "curly": [ 43 | true, 44 | "ignore-same-line" 45 | ], 46 | "forin": true, 47 | "jsdoc-format": true, 48 | "label-position": true, 49 | "indent": [ 50 | true, 51 | "tabs", 52 | 2 53 | ], 54 | "member-access": [ 55 | true, 56 | "no-public" 57 | ], 58 | "new-parens": true, 59 | "no-angle-bracket-type-assertion": true, 60 | "no-any": true, 61 | "no-arg": true, 62 | "no-conditional-assignment": true, 63 | "no-construct": true, 64 | "no-debugger": true, 65 | "no-default-export": true, 66 | "no-duplicate-variable": true, 67 | "no-inferrable-types": true, 68 | "ordered-imports": [ 69 | true, 70 | { 71 | "import-sources-order": "any", 72 | "named-imports-order": "case-insensitive" 73 | } 74 | ], 75 | "no-namespace": [ 76 | true, 77 | "allow-declarations" 78 | ], 79 | "no-reference": true, 80 | "no-string-throw": true, 81 | "no-unused-expression": true, 82 | "no-var-keyword": true, 83 | "object-literal-shorthand": true, 84 | "only-arrow-functions": [ 85 | true, 86 | "allow-declarations", 87 | "allow-named-functions" 88 | ], 89 | "prefer-const": true, 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always", 94 | "ignore-bound-class-methods" 95 | ], 96 | "switch-default": true, 97 | "trailing-comma": [ 98 | true, 99 | { 100 | "multiline": { 101 | "objects": "always", 102 | "arrays": "always", 103 | "functions": "always", 104 | "typeLiterals": "ignore" 105 | }, 106 | "esSpecCompliant": true 107 | } 108 | ], 109 | "triple-equals": [ 110 | true, 111 | "allow-null-check" 112 | ], 113 | "use-isnan": true, 114 | "quotes": [ 115 | "error", 116 | "single" 117 | ], 118 | "variable-name": [ 119 | true, 120 | "check-format", 121 | "ban-keywords", 122 | "allow-leading-underscore", 123 | "allow-trailing-underscore" 124 | ] 125 | }, 126 | "rulesDirectory": [] 127 | } 128 | --------------------------------------------------------------------------------