n8n-nodes-zalo-bot-stephen 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -0
- package/dist/credentials/ZaloApi.credentials.d.ts +8 -0
- package/dist/credentials/ZaloApi.credentials.js +61 -0
- package/dist/gulpfile.d.ts +5 -0
- package/dist/gulpfile.js +9 -0
- package/dist/icons/zalo.svg +6 -0
- package/dist/nodes/ZaloBot/ZaloBot.node.d.ts +6 -0
- package/dist/nodes/ZaloBot/ZaloBot.node.js +505 -0
- package/dist/nodes/ZaloBot/ZaloBotInterface.d.ts +129 -0
- package/dist/nodes/ZaloBot/ZaloBotInterface.js +2 -0
- package/dist/nodes/ZaloBot/ZaloBotTrigger.node.d.ts +12 -0
- package/dist/nodes/ZaloBot/ZaloBotTrigger.node.js +212 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# n8n-nodes-zalo-bot
|
|
2
|
+
|
|
3
|
+
Custom n8n nodes for integrating with Zalo Bot Platform.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install n8n-nodes-zalo-bot
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or install directly in n8n:
|
|
12
|
+
|
|
13
|
+
1. Go to Settings > Community Nodes
|
|
14
|
+
2. Click "Install a community node"
|
|
15
|
+
3. Enter: `n8n-nodes-zalo-bot`
|
|
16
|
+
4. Click "Install"
|
|
17
|
+
|
|
18
|
+
## Nodes
|
|
19
|
+
|
|
20
|
+
### Zalo Bot Trigger
|
|
21
|
+
|
|
22
|
+
A webhook trigger node that starts your workflow when Zalo Bot receives a message.
|
|
23
|
+
|
|
24
|
+
**Features:**
|
|
25
|
+
- Automatically configures webhook URL when activated
|
|
26
|
+
- Validates incoming webhook requests using secret token
|
|
27
|
+
- Handles webhook lifecycle (add, check, remove)
|
|
28
|
+
|
|
29
|
+
**Configuration:**
|
|
30
|
+
1. Add your Zalo Bot API credentials
|
|
31
|
+
2. The node will automatically set up the webhook when activated
|
|
32
|
+
3. Incoming messages will trigger the workflow
|
|
33
|
+
|
|
34
|
+
### Zalo Bot
|
|
35
|
+
|
|
36
|
+
An action node for sending messages and interacting with Zalo Bot.
|
|
37
|
+
|
|
38
|
+
**Resources:**
|
|
39
|
+
- **Message**: Send text messages, photos, stickers, and chat actions
|
|
40
|
+
- **Bot**: Get bot information
|
|
41
|
+
- **Webhook**: Manage webhook configuration
|
|
42
|
+
|
|
43
|
+
**Operations:**
|
|
44
|
+
|
|
45
|
+
#### Message Operations
|
|
46
|
+
- **Send Message**: Send a text message to a chat
|
|
47
|
+
- **Send Photo**: Send a photo (supports URL or binary data)
|
|
48
|
+
- **Send Sticker**: Send a sticker by sticker ID
|
|
49
|
+
- **Send Chat Action**: Send typing indicators or upload status
|
|
50
|
+
|
|
51
|
+
#### Bot Operations
|
|
52
|
+
- **Get Me**: Get information about your bot
|
|
53
|
+
|
|
54
|
+
#### Webhook Operations
|
|
55
|
+
- **Get Webhook Info**: Get current webhook configuration
|
|
56
|
+
- **Set Webhook**: Configure webhook URL manually
|
|
57
|
+
- **Delete Webhook**: Remove webhook configuration
|
|
58
|
+
|
|
59
|
+
## Credentials
|
|
60
|
+
|
|
61
|
+
### Zalo Bot API
|
|
62
|
+
|
|
63
|
+
1. Go to [Zalo Bot Platform](https://bot.zaloplatforms.com/)
|
|
64
|
+
2. Create a bot and get your Bot Token
|
|
65
|
+
3. Add the credential in n8n with your Bot Token
|
|
66
|
+
|
|
67
|
+
## Usage Examples
|
|
68
|
+
|
|
69
|
+
### Example 1: Simple Message Echo Bot
|
|
70
|
+
|
|
71
|
+
1. Add **Zalo Bot Trigger** node
|
|
72
|
+
2. Add **Zalo Bot** node
|
|
73
|
+
3. Configure Zalo Bot node:
|
|
74
|
+
- Resource: Message
|
|
75
|
+
- Operation: Send Message
|
|
76
|
+
- Chat ID: `{{ $json.message.chat.id }}`
|
|
77
|
+
- Text: `{{ $json.message.text }}`
|
|
78
|
+
|
|
79
|
+
### Example 2: Send Photo from URL
|
|
80
|
+
|
|
81
|
+
1. Add **Zalo Bot** node
|
|
82
|
+
2. Configure:
|
|
83
|
+
- Resource: Message
|
|
84
|
+
- Operation: Send Photo
|
|
85
|
+
- Chat ID: `123456789`
|
|
86
|
+
- Photo: `https://example.com/image.jpg`
|
|
87
|
+
- Caption: `Check out this image!`
|
|
88
|
+
|
|
89
|
+
### Example 3: Send Photo from Binary Data
|
|
90
|
+
|
|
91
|
+
1. Add a node that provides binary data (e.g., HTTP Request to download image)
|
|
92
|
+
2. Add **Zalo Bot** node
|
|
93
|
+
3. Configure:
|
|
94
|
+
- Resource: Message
|
|
95
|
+
- Operation: Send Photo
|
|
96
|
+
- Chat ID: `123456789`
|
|
97
|
+
- Binary Data: `true`
|
|
98
|
+
- Binary Property: `data` (or your binary property name)
|
|
99
|
+
- Caption: `Image from workflow`
|
|
100
|
+
|
|
101
|
+
## API Reference
|
|
102
|
+
|
|
103
|
+
For detailed API documentation, visit: [Zalo Bot Platform Docs](https://bot.zaloplatforms.com/docs/)
|
|
104
|
+
|
|
105
|
+
## Development
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Install dependencies
|
|
109
|
+
npm install
|
|
110
|
+
|
|
111
|
+
# Build
|
|
112
|
+
npm run build
|
|
113
|
+
|
|
114
|
+
# Lint
|
|
115
|
+
npm run lint
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT
|
|
121
|
+
|
|
122
|
+
## Support
|
|
123
|
+
|
|
124
|
+
For issues and feature requests, please visit the [GitHub repository](https://github.com/your-username/n8n-nodes-zalo-bot).
|
|
125
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ICredentialType, INodeProperties, ICredentialsDecrypted, INodeCredentialTestRequest, INodeCredentialTestResult } from 'n8n-workflow';
|
|
2
|
+
export declare class ZaloApi implements ICredentialType {
|
|
3
|
+
name: string;
|
|
4
|
+
displayName: string;
|
|
5
|
+
documentationUrl: string;
|
|
6
|
+
properties: INodeProperties[];
|
|
7
|
+
test(this: INodeCredentialTestRequest, credentials: ICredentialsDecrypted): Promise<INodeCredentialTestResult>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ZaloApi = void 0;
|
|
4
|
+
class ZaloApi {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.name = 'zaloApi';
|
|
7
|
+
this.displayName = 'Zalo Bot API';
|
|
8
|
+
this.documentationUrl = 'https://bot.zaloplatforms.com/docs/';
|
|
9
|
+
this.properties = [
|
|
10
|
+
{
|
|
11
|
+
displayName: 'Bot Token',
|
|
12
|
+
name: 'botToken',
|
|
13
|
+
type: 'string',
|
|
14
|
+
typeOptions: {
|
|
15
|
+
password: true,
|
|
16
|
+
},
|
|
17
|
+
default: '',
|
|
18
|
+
required: true,
|
|
19
|
+
description: 'Your Zalo Bot API token',
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
async test(credentials) {
|
|
24
|
+
const botToken = credentials.botToken;
|
|
25
|
+
if (!botToken) {
|
|
26
|
+
return {
|
|
27
|
+
status: 'Error',
|
|
28
|
+
message: 'Bot Token is required',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const options = {
|
|
32
|
+
url: `https://bot-api.zaloplatforms.com/bot${botToken}/getMe`,
|
|
33
|
+
method: 'GET',
|
|
34
|
+
headers: {
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
const response = await this.helpers.httpRequest(options);
|
|
40
|
+
if (response.ok) {
|
|
41
|
+
return {
|
|
42
|
+
status: 'OK',
|
|
43
|
+
message: 'Authentication successful',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
return {
|
|
48
|
+
status: 'Error',
|
|
49
|
+
message: response.description || 'Authentication failed',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
return {
|
|
55
|
+
status: 'Error',
|
|
56
|
+
message: error?.message || 'Failed to authenticate',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.ZaloApi = ZaloApi;
|
package/dist/gulpfile.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildIcons = exports.build = void 0;
|
|
4
|
+
const gulp_1 = require("gulp");
|
|
5
|
+
function copyIcons() {
|
|
6
|
+
return (0, gulp_1.src)('icons/**/*').pipe((0, gulp_1.dest)('dist/icons'));
|
|
7
|
+
}
|
|
8
|
+
exports.build = copyIcons;
|
|
9
|
+
exports.buildIcons = copyIcons;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
2
|
+
<rect width="100" height="100" rx="20" fill="#0068FF"/>
|
|
3
|
+
<path d="M30 35 L50 50 L70 35 M30 65 L50 50 L70 65" stroke="white" stroke-width="4" stroke-linecap="round" fill="none"/>
|
|
4
|
+
<circle cx="50" cy="50" r="25" fill="none" stroke="white" stroke-width="3"/>
|
|
5
|
+
</svg>
|
|
6
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
export declare class ZaloBot implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
5
|
+
private makeRequestWithRetry;
|
|
6
|
+
}
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ZaloBot = void 0;
|
|
7
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
8
|
+
const form_data_1 = __importDefault(require("form-data"));
|
|
9
|
+
class ZaloBot {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.description = {
|
|
12
|
+
displayName: 'Zalo Bot',
|
|
13
|
+
name: 'zaloBot',
|
|
14
|
+
icon: 'file:zalo.svg',
|
|
15
|
+
group: ['transform'],
|
|
16
|
+
version: 1,
|
|
17
|
+
subtitle: '={{$parameter["resource"] + "/" + $parameter["operation"]}}',
|
|
18
|
+
description: 'Send messages and interact with Zalo Bot',
|
|
19
|
+
defaults: {
|
|
20
|
+
name: 'Zalo Bot',
|
|
21
|
+
},
|
|
22
|
+
inputs: ['main'],
|
|
23
|
+
outputs: ['main'],
|
|
24
|
+
credentials: [
|
|
25
|
+
{
|
|
26
|
+
name: 'zaloApi',
|
|
27
|
+
required: true,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
properties: [
|
|
31
|
+
{
|
|
32
|
+
displayName: 'Resource',
|
|
33
|
+
name: 'resource',
|
|
34
|
+
type: 'options',
|
|
35
|
+
noDataExpression: true,
|
|
36
|
+
options: [
|
|
37
|
+
{
|
|
38
|
+
name: 'Message',
|
|
39
|
+
value: 'message',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'Bot',
|
|
43
|
+
value: 'bot',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'Webhook',
|
|
47
|
+
value: 'webhook',
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
default: 'message',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
displayName: 'Operation',
|
|
54
|
+
name: 'operation',
|
|
55
|
+
type: 'options',
|
|
56
|
+
noDataExpression: true,
|
|
57
|
+
displayOptions: {
|
|
58
|
+
show: {
|
|
59
|
+
resource: ['message'],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
options: [
|
|
63
|
+
{
|
|
64
|
+
name: 'Send Message',
|
|
65
|
+
value: 'sendMessage',
|
|
66
|
+
description: 'Send a text message',
|
|
67
|
+
action: 'Send a text message',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'Send Photo',
|
|
71
|
+
value: 'sendPhoto',
|
|
72
|
+
description: 'Send a photo',
|
|
73
|
+
action: 'Send a photo',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'Send Sticker',
|
|
77
|
+
value: 'sendSticker',
|
|
78
|
+
description: 'Send a sticker',
|
|
79
|
+
action: 'Send a sticker',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'Send Chat Action',
|
|
83
|
+
value: 'sendChatAction',
|
|
84
|
+
description: 'Send a chat action (typing, uploading, etc.)',
|
|
85
|
+
action: 'Send a chat action',
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
default: 'sendMessage',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
displayName: 'Operation',
|
|
92
|
+
name: 'operation',
|
|
93
|
+
type: 'options',
|
|
94
|
+
noDataExpression: true,
|
|
95
|
+
displayOptions: {
|
|
96
|
+
show: {
|
|
97
|
+
resource: ['bot'],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
options: [
|
|
101
|
+
{
|
|
102
|
+
name: 'Get Me',
|
|
103
|
+
value: 'getMe',
|
|
104
|
+
description: 'Get bot information',
|
|
105
|
+
action: 'Get bot information',
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
default: 'getMe',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
displayName: 'Operation',
|
|
112
|
+
name: 'operation',
|
|
113
|
+
type: 'options',
|
|
114
|
+
noDataExpression: true,
|
|
115
|
+
displayOptions: {
|
|
116
|
+
show: {
|
|
117
|
+
resource: ['webhook'],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
options: [
|
|
121
|
+
{
|
|
122
|
+
name: 'Get Webhook Info',
|
|
123
|
+
value: 'getWebhookInfo',
|
|
124
|
+
description: 'Get webhook information',
|
|
125
|
+
action: 'Get webhook information',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'Set Webhook',
|
|
129
|
+
value: 'setWebhook',
|
|
130
|
+
description: 'Set webhook URL',
|
|
131
|
+
action: 'Set webhook URL',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'Delete Webhook',
|
|
135
|
+
value: 'deleteWebhook',
|
|
136
|
+
description: 'Delete webhook',
|
|
137
|
+
action: 'Delete webhook',
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
default: 'getWebhookInfo',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
displayName: 'Chat ID',
|
|
144
|
+
name: 'chatId',
|
|
145
|
+
type: 'string',
|
|
146
|
+
required: true,
|
|
147
|
+
displayOptions: {
|
|
148
|
+
show: {
|
|
149
|
+
resource: ['message'],
|
|
150
|
+
operation: ['sendMessage', 'sendPhoto', 'sendSticker', 'sendChatAction'],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
default: '',
|
|
154
|
+
description: 'The chat ID to send the message to',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
displayName: 'Text',
|
|
158
|
+
name: 'text',
|
|
159
|
+
type: 'string',
|
|
160
|
+
required: true,
|
|
161
|
+
displayOptions: {
|
|
162
|
+
show: {
|
|
163
|
+
resource: ['message'],
|
|
164
|
+
operation: ['sendMessage'],
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
default: '',
|
|
168
|
+
description: 'The message text',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
displayName: 'Photo',
|
|
172
|
+
name: 'photo',
|
|
173
|
+
type: 'string',
|
|
174
|
+
required: true,
|
|
175
|
+
displayOptions: {
|
|
176
|
+
show: {
|
|
177
|
+
resource: ['message'],
|
|
178
|
+
operation: ['sendPhoto'],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
default: '',
|
|
182
|
+
description: 'Photo URL or file_id. Leave empty to use binary data.',
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
displayName: 'Binary Data',
|
|
186
|
+
name: 'binaryData',
|
|
187
|
+
type: 'boolean',
|
|
188
|
+
displayOptions: {
|
|
189
|
+
show: {
|
|
190
|
+
resource: ['message'],
|
|
191
|
+
operation: ['sendPhoto'],
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
default: false,
|
|
195
|
+
description: 'Whether to use binary data from previous node',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
displayName: 'Binary Property',
|
|
199
|
+
name: 'binaryPropertyName',
|
|
200
|
+
type: 'string',
|
|
201
|
+
displayOptions: {
|
|
202
|
+
show: {
|
|
203
|
+
resource: ['message'],
|
|
204
|
+
operation: ['sendPhoto'],
|
|
205
|
+
binaryData: [true],
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
default: 'data',
|
|
209
|
+
description: 'Name of the binary property containing the image',
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
displayName: 'Caption',
|
|
213
|
+
name: 'caption',
|
|
214
|
+
type: 'string',
|
|
215
|
+
displayOptions: {
|
|
216
|
+
show: {
|
|
217
|
+
resource: ['message'],
|
|
218
|
+
operation: ['sendPhoto'],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
default: '',
|
|
222
|
+
description: 'Photo caption',
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
displayName: 'Sticker ID',
|
|
226
|
+
name: 'stickerId',
|
|
227
|
+
type: 'string',
|
|
228
|
+
required: true,
|
|
229
|
+
displayOptions: {
|
|
230
|
+
show: {
|
|
231
|
+
resource: ['message'],
|
|
232
|
+
operation: ['sendSticker'],
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
default: '',
|
|
236
|
+
description: 'The sticker ID to send',
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
displayName: 'Action',
|
|
240
|
+
name: 'action',
|
|
241
|
+
type: 'options',
|
|
242
|
+
required: true,
|
|
243
|
+
displayOptions: {
|
|
244
|
+
show: {
|
|
245
|
+
resource: ['message'],
|
|
246
|
+
operation: ['sendChatAction'],
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
options: [
|
|
250
|
+
{
|
|
251
|
+
name: 'Typing',
|
|
252
|
+
value: 'typing',
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: 'Upload Photo',
|
|
256
|
+
value: 'upload_photo',
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: 'Upload Video',
|
|
260
|
+
value: 'upload_video',
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: 'Upload Document',
|
|
264
|
+
value: 'upload_document',
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: 'Find Location',
|
|
268
|
+
value: 'find_location',
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'Record Video',
|
|
272
|
+
value: 'record_video',
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: 'Record Audio',
|
|
276
|
+
value: 'record_audio',
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: 'Upload Audio',
|
|
280
|
+
value: 'upload_audio',
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
default: 'typing',
|
|
284
|
+
description: 'The chat action to send',
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
displayName: 'Webhook URL',
|
|
288
|
+
name: 'webhookUrl',
|
|
289
|
+
type: 'string',
|
|
290
|
+
required: true,
|
|
291
|
+
displayOptions: {
|
|
292
|
+
show: {
|
|
293
|
+
resource: ['webhook'],
|
|
294
|
+
operation: ['setWebhook'],
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
default: '',
|
|
298
|
+
description: 'The webhook URL to set',
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
displayName: 'Secret Token',
|
|
302
|
+
name: 'secretToken',
|
|
303
|
+
type: 'string',
|
|
304
|
+
required: true,
|
|
305
|
+
displayOptions: {
|
|
306
|
+
show: {
|
|
307
|
+
resource: ['webhook'],
|
|
308
|
+
operation: ['setWebhook'],
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
typeOptions: {
|
|
312
|
+
password: true,
|
|
313
|
+
},
|
|
314
|
+
default: '',
|
|
315
|
+
description: 'Secret token (8-256 characters) for webhook validation',
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
async execute() {
|
|
321
|
+
const items = this.getInputData();
|
|
322
|
+
const returnData = [];
|
|
323
|
+
const credentials = await this.getCredentials('zaloApi');
|
|
324
|
+
const botToken = credentials.botToken;
|
|
325
|
+
const baseUrl = `https://bot-api.zaloplatforms.com/bot${botToken}`;
|
|
326
|
+
const resource = this.getNodeParameter('resource', 0);
|
|
327
|
+
const operation = this.getNodeParameter('operation', 0);
|
|
328
|
+
for (let i = 0; i < items.length; i++) {
|
|
329
|
+
try {
|
|
330
|
+
let response;
|
|
331
|
+
if (resource === 'message') {
|
|
332
|
+
if (operation === 'sendMessage') {
|
|
333
|
+
const chatId = this.getNodeParameter('chatId', i);
|
|
334
|
+
const text = this.getNodeParameter('text', i);
|
|
335
|
+
const body = {
|
|
336
|
+
chat_id: chatId,
|
|
337
|
+
text,
|
|
338
|
+
};
|
|
339
|
+
response = await this.makeRequestWithRetry.call(this, `${baseUrl}/sendMessage`, 'POST', body);
|
|
340
|
+
}
|
|
341
|
+
else if (operation === 'sendPhoto') {
|
|
342
|
+
const chatId = this.getNodeParameter('chatId', i);
|
|
343
|
+
const useBinary = this.getNodeParameter('binaryData', i, false);
|
|
344
|
+
const caption = this.getNodeParameter('caption', i, '');
|
|
345
|
+
let photoData;
|
|
346
|
+
let isMultipart = false;
|
|
347
|
+
let filename = 'photo.jpg';
|
|
348
|
+
let contentType = 'image/jpeg';
|
|
349
|
+
if (useBinary) {
|
|
350
|
+
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i, 'data');
|
|
351
|
+
const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName);
|
|
352
|
+
photoData = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
|
|
353
|
+
// Get filename and contentType from binary data properties
|
|
354
|
+
filename = binaryData.fileName || binaryData.file || 'photo.jpg';
|
|
355
|
+
contentType = binaryData.mimeType || binaryData.mime || 'image/jpeg';
|
|
356
|
+
isMultipart = true;
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
photoData = this.getNodeParameter('photo', i);
|
|
360
|
+
}
|
|
361
|
+
if (isMultipart) {
|
|
362
|
+
const formData = new form_data_1.default();
|
|
363
|
+
formData.append('chat_id', chatId);
|
|
364
|
+
formData.append('photo', photoData, {
|
|
365
|
+
filename: filename,
|
|
366
|
+
contentType: contentType,
|
|
367
|
+
});
|
|
368
|
+
if (caption) {
|
|
369
|
+
formData.append('caption', caption);
|
|
370
|
+
}
|
|
371
|
+
response = await this.makeRequestWithRetry.call(this, `${baseUrl}/sendPhoto`, 'POST', formData, true);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
const body = {
|
|
375
|
+
chat_id: chatId,
|
|
376
|
+
photo: photoData,
|
|
377
|
+
};
|
|
378
|
+
if (caption) {
|
|
379
|
+
body.caption = caption;
|
|
380
|
+
}
|
|
381
|
+
response = await this.makeRequestWithRetry.call(this, `${baseUrl}/sendPhoto`, 'POST', body);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
else if (operation === 'sendSticker') {
|
|
385
|
+
const chatId = this.getNodeParameter('chatId', i);
|
|
386
|
+
const stickerId = this.getNodeParameter('stickerId', i);
|
|
387
|
+
const body = {
|
|
388
|
+
chat_id: chatId,
|
|
389
|
+
sticker_id: stickerId,
|
|
390
|
+
};
|
|
391
|
+
response = await this.makeRequestWithRetry.call(this, `${baseUrl}/sendSticker`, 'POST', body);
|
|
392
|
+
}
|
|
393
|
+
else if (operation === 'sendChatAction') {
|
|
394
|
+
const chatId = this.getNodeParameter('chatId', i);
|
|
395
|
+
const action = this.getNodeParameter('action', i);
|
|
396
|
+
const body = {
|
|
397
|
+
chat_id: chatId,
|
|
398
|
+
action: action,
|
|
399
|
+
};
|
|
400
|
+
response = await this.makeRequestWithRetry.call(this, `${baseUrl}/sendChatAction`, 'POST', body);
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown operation: ${operation}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
else if (resource === 'bot') {
|
|
407
|
+
if (operation === 'getMe') {
|
|
408
|
+
response = await this.makeRequestWithRetry.call(this, `${baseUrl}/getMe`, 'GET');
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown operation: ${operation}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
else if (resource === 'webhook') {
|
|
415
|
+
if (operation === 'getWebhookInfo') {
|
|
416
|
+
response = await this.makeRequestWithRetry.call(this, `${baseUrl}/getWebhookInfo`, 'GET');
|
|
417
|
+
}
|
|
418
|
+
else if (operation === 'setWebhook') {
|
|
419
|
+
const webhookUrl = this.getNodeParameter('webhookUrl', i);
|
|
420
|
+
const secretToken = this.getNodeParameter('secretToken', i);
|
|
421
|
+
const body = {
|
|
422
|
+
url: webhookUrl,
|
|
423
|
+
secret_token: secretToken,
|
|
424
|
+
};
|
|
425
|
+
response = await this.makeRequestWithRetry.call(this, `${baseUrl}/setWebhook`, 'POST', body);
|
|
426
|
+
}
|
|
427
|
+
else if (operation === 'deleteWebhook') {
|
|
428
|
+
response = await this.makeRequestWithRetry.call(this, `${baseUrl}/deleteWebhook`, 'POST');
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown operation: ${operation}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown resource: ${resource}`);
|
|
436
|
+
}
|
|
437
|
+
if (!response.ok) {
|
|
438
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), response.description || 'API request failed', { code: response.error_code?.toString() });
|
|
439
|
+
}
|
|
440
|
+
returnData.push({
|
|
441
|
+
json: response.result || response,
|
|
442
|
+
pairedItem: {
|
|
443
|
+
item: i,
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
if (this.continueOnFail()) {
|
|
449
|
+
returnData.push({
|
|
450
|
+
json: {
|
|
451
|
+
error: error.message,
|
|
452
|
+
},
|
|
453
|
+
pairedItem: {
|
|
454
|
+
item: i,
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
throw error;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return [returnData];
|
|
463
|
+
}
|
|
464
|
+
async makeRequestWithRetry(url, method, body, isMultipart = false, maxRetries = 3) {
|
|
465
|
+
let lastError = null;
|
|
466
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
467
|
+
try {
|
|
468
|
+
const options = {
|
|
469
|
+
method,
|
|
470
|
+
url,
|
|
471
|
+
headers: {},
|
|
472
|
+
};
|
|
473
|
+
if (isMultipart && body instanceof form_data_1.default) {
|
|
474
|
+
options.body = body;
|
|
475
|
+
options.headers = body.getHeaders();
|
|
476
|
+
}
|
|
477
|
+
else if (body) {
|
|
478
|
+
options.body = body;
|
|
479
|
+
options.headers['Content-Type'] = 'application/json';
|
|
480
|
+
options.json = true;
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
options.headers['Content-Type'] = 'application/json';
|
|
484
|
+
}
|
|
485
|
+
const response = await this.helpers.httpRequest(options);
|
|
486
|
+
return response;
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
lastError = error;
|
|
490
|
+
// Don't retry on client errors (4xx)
|
|
491
|
+
if (error.response?.status >= 400 && error.response?.status < 500) {
|
|
492
|
+
throw error;
|
|
493
|
+
}
|
|
494
|
+
// Retry on network errors or server errors (5xx)
|
|
495
|
+
if (attempt < maxRetries) {
|
|
496
|
+
// Exponential backoff: wait 1s, 2s, 4s
|
|
497
|
+
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt - 1) * 1000));
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
throw lastError || new Error('Request failed after retries');
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
exports.ZaloBot = ZaloBot;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zalo Bot API Response Structure
|
|
3
|
+
*/
|
|
4
|
+
export interface ZaloApiResponse<T = any> {
|
|
5
|
+
ok: boolean;
|
|
6
|
+
result?: T;
|
|
7
|
+
error_code?: number;
|
|
8
|
+
description?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Bot Information from getMe
|
|
12
|
+
*/
|
|
13
|
+
export interface ZaloBotInfo {
|
|
14
|
+
id: string;
|
|
15
|
+
is_bot: boolean;
|
|
16
|
+
first_name?: string;
|
|
17
|
+
last_name?: string;
|
|
18
|
+
username?: string;
|
|
19
|
+
can_join_groups?: boolean;
|
|
20
|
+
can_read_all_group_messages?: boolean;
|
|
21
|
+
supports_inline_queries?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Webhook Information
|
|
25
|
+
*/
|
|
26
|
+
export interface ZaloWebhookInfo {
|
|
27
|
+
url?: string;
|
|
28
|
+
has_custom_certificate?: boolean;
|
|
29
|
+
pending_update_count?: number;
|
|
30
|
+
last_error_date?: number;
|
|
31
|
+
last_error_message?: string;
|
|
32
|
+
max_connections?: number;
|
|
33
|
+
allowed_updates?: string[];
|
|
34
|
+
updated_at?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Message from Zalo
|
|
38
|
+
*/
|
|
39
|
+
export interface ZaloMessage {
|
|
40
|
+
message_id: number;
|
|
41
|
+
from?: {
|
|
42
|
+
id: number;
|
|
43
|
+
is_bot: boolean;
|
|
44
|
+
first_name?: string;
|
|
45
|
+
last_name?: string;
|
|
46
|
+
username?: string;
|
|
47
|
+
};
|
|
48
|
+
chat: {
|
|
49
|
+
id: number;
|
|
50
|
+
type: 'private' | 'group' | 'supergroup' | 'channel';
|
|
51
|
+
title?: string;
|
|
52
|
+
username?: string;
|
|
53
|
+
first_name?: string;
|
|
54
|
+
last_name?: string;
|
|
55
|
+
};
|
|
56
|
+
date: number;
|
|
57
|
+
text?: string;
|
|
58
|
+
location?: {
|
|
59
|
+
latitude: number;
|
|
60
|
+
longitude: number;
|
|
61
|
+
};
|
|
62
|
+
photo?: Array<{
|
|
63
|
+
file_id: string;
|
|
64
|
+
file_unique_id: string;
|
|
65
|
+
width: number;
|
|
66
|
+
height: number;
|
|
67
|
+
file_size?: number;
|
|
68
|
+
}>;
|
|
69
|
+
sticker?: {
|
|
70
|
+
file_id: string;
|
|
71
|
+
file_unique_id: string;
|
|
72
|
+
width: number;
|
|
73
|
+
height: number;
|
|
74
|
+
emoji?: string;
|
|
75
|
+
set_name?: string;
|
|
76
|
+
file_size?: number;
|
|
77
|
+
};
|
|
78
|
+
document?: {
|
|
79
|
+
file_id: string;
|
|
80
|
+
file_unique_id: string;
|
|
81
|
+
file_name?: string;
|
|
82
|
+
mime_type?: string;
|
|
83
|
+
file_size?: number;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Update from getUpdates or Webhook
|
|
88
|
+
*/
|
|
89
|
+
export interface ZaloUpdate {
|
|
90
|
+
update_id: number;
|
|
91
|
+
message?: ZaloMessage;
|
|
92
|
+
edited_message?: ZaloMessage;
|
|
93
|
+
channel_post?: ZaloMessage;
|
|
94
|
+
edited_channel_post?: ZaloMessage;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Request Interfaces for API Calls
|
|
98
|
+
*/
|
|
99
|
+
export interface SendMessageRequest {
|
|
100
|
+
chat_id: string | number;
|
|
101
|
+
text: string;
|
|
102
|
+
parse_mode?: 'HTML' | 'Markdown' | 'MarkdownV2';
|
|
103
|
+
disable_web_page_preview?: boolean;
|
|
104
|
+
disable_notification?: boolean;
|
|
105
|
+
reply_to_message_id?: number;
|
|
106
|
+
}
|
|
107
|
+
export interface SendPhotoRequest {
|
|
108
|
+
chat_id: string | number;
|
|
109
|
+
photo: string;
|
|
110
|
+
caption?: string;
|
|
111
|
+
parse_mode?: 'HTML' | 'Markdown' | 'MarkdownV2';
|
|
112
|
+
disable_notification?: boolean;
|
|
113
|
+
reply_to_message_id?: number;
|
|
114
|
+
}
|
|
115
|
+
export interface SendStickerRequest {
|
|
116
|
+
chat_id: string | number;
|
|
117
|
+
sticker_id: string;
|
|
118
|
+
disable_notification?: boolean;
|
|
119
|
+
reply_to_message_id?: number;
|
|
120
|
+
}
|
|
121
|
+
export interface SendChatActionRequest {
|
|
122
|
+
chat_id: string | number;
|
|
123
|
+
action: 'typing' | 'upload_photo' | 'upload_video' | 'upload_document' | 'find_location' | 'record_video' | 'record_audio' | 'upload_audio';
|
|
124
|
+
}
|
|
125
|
+
export interface SetWebhookRequest {
|
|
126
|
+
url: string;
|
|
127
|
+
secret_token: string;
|
|
128
|
+
allowed_updates?: string[];
|
|
129
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { IWebhookFunctions, INodeType, INodeTypeDescription, IWebhookResponseData } from 'n8n-workflow';
|
|
2
|
+
export declare class ZaloBotTrigger implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
webhookMethods: {
|
|
5
|
+
default: {
|
|
6
|
+
checkExists(this: IWebhookFunctions): Promise<boolean>;
|
|
7
|
+
create(this: IWebhookFunctions): Promise<boolean>;
|
|
8
|
+
delete(this: IWebhookFunctions): Promise<boolean>;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
webhook(this: IWebhookFunctions): Promise<IWebhookResponseData>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.ZaloBotTrigger = void 0;
|
|
27
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
28
|
+
const crypto = __importStar(require("crypto"));
|
|
29
|
+
class ZaloBotTrigger {
|
|
30
|
+
constructor() {
|
|
31
|
+
this.description = {
|
|
32
|
+
displayName: 'Zalo Bot Trigger',
|
|
33
|
+
name: 'zaloBotTrigger',
|
|
34
|
+
icon: 'file:zalo.svg',
|
|
35
|
+
group: ['trigger'],
|
|
36
|
+
version: 1,
|
|
37
|
+
subtitle: '={{$parameter["event"]}}',
|
|
38
|
+
description: 'Starts workflow when Zalo Bot receives a message',
|
|
39
|
+
defaults: {
|
|
40
|
+
name: 'Zalo Bot Trigger',
|
|
41
|
+
},
|
|
42
|
+
inputs: [],
|
|
43
|
+
outputs: ['main'],
|
|
44
|
+
credentials: [
|
|
45
|
+
{
|
|
46
|
+
name: 'zaloApi',
|
|
47
|
+
required: true,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
webhooks: [
|
|
51
|
+
{
|
|
52
|
+
name: 'default',
|
|
53
|
+
httpMethod: 'POST',
|
|
54
|
+
responseMode: 'onReceived',
|
|
55
|
+
path: 'webhook',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
properties: [
|
|
59
|
+
{
|
|
60
|
+
displayName: 'Webhook Path',
|
|
61
|
+
name: 'webhookPath',
|
|
62
|
+
type: 'string',
|
|
63
|
+
default: 'default',
|
|
64
|
+
required: true,
|
|
65
|
+
description: 'The path to listen for webhooks',
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
this.webhookMethods = {
|
|
70
|
+
default: {
|
|
71
|
+
async checkExists() {
|
|
72
|
+
const credentials = await this.getCredentials('zaloApi');
|
|
73
|
+
const botToken = credentials.botToken;
|
|
74
|
+
try {
|
|
75
|
+
const response = await this.helpers.httpRequest({
|
|
76
|
+
method: 'GET',
|
|
77
|
+
url: `https://bot-api.zaloplatforms.com/bot${botToken}/getWebhookInfo`,
|
|
78
|
+
headers: {
|
|
79
|
+
'Content-Type': 'application/json',
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
if (response.ok && response.result?.url) {
|
|
83
|
+
const webhookUrl = this.getNodeWebhookUrl('default');
|
|
84
|
+
return response.result.url === webhookUrl;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
async create() {
|
|
93
|
+
const credentials = await this.getCredentials('zaloApi');
|
|
94
|
+
const botToken = credentials.botToken;
|
|
95
|
+
const webhookUrl = this.getNodeWebhookUrl('default');
|
|
96
|
+
// Generate secret token (32-64 characters, alphanumeric + special chars)
|
|
97
|
+
const secretToken = crypto
|
|
98
|
+
.randomBytes(32)
|
|
99
|
+
.toString('base64')
|
|
100
|
+
.replace(/[^a-zA-Z0-9]/g, '')
|
|
101
|
+
.substring(0, 48);
|
|
102
|
+
// Store secret token in node state for validation
|
|
103
|
+
const nodeState = this.getWorkflowStaticData('node');
|
|
104
|
+
nodeState.secretToken = secretToken;
|
|
105
|
+
const body = {
|
|
106
|
+
url: webhookUrl,
|
|
107
|
+
secret_token: secretToken,
|
|
108
|
+
};
|
|
109
|
+
try {
|
|
110
|
+
const response = await this.helpers.httpRequest({
|
|
111
|
+
method: 'POST',
|
|
112
|
+
url: `https://bot-api.zaloplatforms.com/bot${botToken}/setWebhook`,
|
|
113
|
+
headers: {
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
},
|
|
116
|
+
body,
|
|
117
|
+
json: true,
|
|
118
|
+
});
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to set webhook: ${response.description || 'Unknown error'}`, { code: response.error_code?.toString() });
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (error instanceof n8n_workflow_1.NodeOperationError) {
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to set webhook: ${error.message || 'Unknown error'}`);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
async delete() {
|
|
132
|
+
const credentials = await this.getCredentials('zaloApi');
|
|
133
|
+
const botToken = credentials.botToken;
|
|
134
|
+
try {
|
|
135
|
+
const response = await this.helpers.httpRequest({
|
|
136
|
+
method: 'POST',
|
|
137
|
+
url: `https://bot-api.zaloplatforms.com/bot${botToken}/deleteWebhook`,
|
|
138
|
+
headers: {
|
|
139
|
+
'Content-Type': 'application/json',
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to delete webhook: ${response.description || 'Unknown error'}`, { code: response.error_code?.toString() });
|
|
144
|
+
}
|
|
145
|
+
// Clear secret token from node state
|
|
146
|
+
const nodeState = this.getWorkflowStaticData('node');
|
|
147
|
+
delete nodeState.secretToken;
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (error instanceof n8n_workflow_1.NodeOperationError) {
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to delete webhook: ${error.message || 'Unknown error'}`);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async webhook() {
|
|
161
|
+
const req = this.getRequestObject();
|
|
162
|
+
const headers = this.getHeaderData();
|
|
163
|
+
const body = this.getBodyData();
|
|
164
|
+
// Validate secret token from header
|
|
165
|
+
const nodeState = this.getWorkflowStaticData('node');
|
|
166
|
+
const expectedSecretToken = nodeState.secretToken;
|
|
167
|
+
if (expectedSecretToken) {
|
|
168
|
+
const receivedSecretToken = headers['x-bot-api-secret-token'];
|
|
169
|
+
if (!receivedSecretToken) {
|
|
170
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Missing secret token in webhook request', { httpCode: 401 });
|
|
171
|
+
}
|
|
172
|
+
// Use timing-safe comparison to prevent timing attacks
|
|
173
|
+
// Convert strings to Buffers for timingSafeEqual
|
|
174
|
+
const expectedBuffer = Buffer.from(expectedSecretToken, 'utf8');
|
|
175
|
+
const receivedBuffer = Buffer.from(receivedSecretToken, 'utf8');
|
|
176
|
+
// timingSafeEqual requires buffers of equal length
|
|
177
|
+
if (expectedBuffer.length !== receivedBuffer.length) {
|
|
178
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Invalid secret token in webhook request', { httpCode: 401 });
|
|
179
|
+
}
|
|
180
|
+
if (!crypto.timingSafeEqual(expectedBuffer, receivedBuffer)) {
|
|
181
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Invalid secret token in webhook request', { httpCode: 401 });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Parse incoming update
|
|
185
|
+
let update;
|
|
186
|
+
if (typeof body === 'object' && body !== null) {
|
|
187
|
+
update = body;
|
|
188
|
+
}
|
|
189
|
+
else if (typeof body === 'string') {
|
|
190
|
+
try {
|
|
191
|
+
update = JSON.parse(body);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Invalid JSON in webhook body', { httpCode: 400 });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Invalid webhook body format', { httpCode: 400 });
|
|
199
|
+
}
|
|
200
|
+
// Return the update data to trigger the workflow
|
|
201
|
+
return {
|
|
202
|
+
workflowData: [
|
|
203
|
+
[
|
|
204
|
+
{
|
|
205
|
+
json: update,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
exports.ZaloBotTrigger = ZaloBotTrigger;
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-zalo-bot-stephen",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "n8n custom node for Zalo Bot Platform integration",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"n8n-community-node-package",
|
|
7
|
+
"zalo",
|
|
8
|
+
"bot",
|
|
9
|
+
"messaging"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"homepage": "https://github.com/your-username/n8n-nodes-zalo-bot",
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "Your Name",
|
|
15
|
+
"email": "your.email@example.com"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/your-username/n8n-nodes-zalo-bot.git"
|
|
20
|
+
},
|
|
21
|
+
"main": "index.js",
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc && gulp build:icons",
|
|
24
|
+
"dev": "tsc --watch",
|
|
25
|
+
"lint": "eslint nodes credentials --ext .ts",
|
|
26
|
+
"lintfix": "eslint nodes credentials --ext .ts --fix",
|
|
27
|
+
"prepublishOnly": "echo Skipping build for publish"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"n8n": {
|
|
34
|
+
"n8nNodesApiVersion": 2,
|
|
35
|
+
"nodes": [
|
|
36
|
+
"dist/nodes/ZaloBot/ZaloBot.node.js",
|
|
37
|
+
"dist/nodes/ZaloBot/ZaloBotTrigger.node.js"
|
|
38
|
+
],
|
|
39
|
+
"credentials": [
|
|
40
|
+
"dist/credentials/ZaloApi.credentials.js"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/gulp": "^4.0.18",
|
|
45
|
+
"@types/node": "^25.0.3",
|
|
46
|
+
"@typescript-eslint/parser": "^5.45.0",
|
|
47
|
+
"eslint-plugin-n8n-nodes-base": "~1.11.0",
|
|
48
|
+
"gulp": "^4.0.2",
|
|
49
|
+
"n8n-workflow": "*",
|
|
50
|
+
"typescript": "~5.3.0"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"form-data": "^4.0.0",
|
|
54
|
+
"n8n-workflow": "^2.0.0"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"n8n-workflow": "^2.0.0"
|
|
58
|
+
}
|
|
59
|
+
}
|