n8n-nodes-linq 4.0.2 → 4.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# n8n-nodes-linq
|
|
2
2
|
|
|
3
|
-
Community node for the Linq Partner API v3 in n8n.
|
|
3
|
+
Community node for the Linq Partner API v3 in n8n. The current implementation covers chats, messages, attachments, phone numbers, webhook subscriptions, and Linq webhooks from your workflows.
|
|
4
4
|
|
|
5
5
|
**Ownership:** The Linq Partner API is owned and operated by Linq (https://linqapp.com/). This repository is community-maintained by Everyday Workflows.
|
|
6
6
|
|
|
@@ -12,14 +12,15 @@ Community node for the Linq Partner API v3 in n8n. Use this node to manage chats
|
|
|
12
12
|
|
|
13
13
|
- Usage guide: [docs/usage.md](docs/usage.md)
|
|
14
14
|
- API reference (v3): [docs/api-reference.md](docs/api-reference.md)
|
|
15
|
-
-
|
|
15
|
+
- Official Linq docs: https://apidocs.linqapp.com/documentation/api-reference
|
|
16
|
+
- Cached OpenAPI snapshot: [V3.yaml](V3.yaml)
|
|
16
17
|
- Contributing: [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
17
18
|
- License: [LICENSE.md](LICENSE.md)
|
|
18
19
|
|
|
19
20
|
## Requirements
|
|
20
21
|
|
|
21
22
|
- Node.js >= 20.15
|
|
22
|
-
- n8n compatible with `n8n-workflow` ^1.120.
|
|
23
|
+
- n8n compatible with `n8n-workflow` ^1.120.10
|
|
23
24
|
|
|
24
25
|
## Installation (in n8n)
|
|
25
26
|
|
|
@@ -54,66 +55,72 @@ Add the **Linq** node to your workflow. Operations map to Linq Partner API v3 en
|
|
|
54
55
|
|
|
55
56
|
**Chat**
|
|
56
57
|
|
|
57
|
-
- Get Many → `GET /v3/chats` (
|
|
58
|
+
- Get Many → `GET /v3/chats` (supports optional `from`, `to`, cursor, and limit)
|
|
58
59
|
- Get One → `GET /v3/chats/{chatId}`
|
|
59
|
-
- Find → `GET /v3/chats`
|
|
60
|
+
- Find (legacy helper) → `GET /v3/chats` using `to` plus the older `phone_numbers[]` fallback when multiple handles are supplied
|
|
60
61
|
- Create → `POST /v3/chats`
|
|
62
|
+
- Leave → `POST /v3/chats/{chatId}/leave`
|
|
61
63
|
- Share Contact → `POST /v3/chats/{chatId}/share_contact_card`
|
|
64
|
+
- Start Typing → `POST /v3/chats/{chatId}/typing`
|
|
65
|
+
- Stop Typing → `DELETE /v3/chats/{chatId}/typing`
|
|
66
|
+
- Mark as Read → `POST /v3/chats/{chatId}/read`
|
|
67
|
+
- Send Voice Memo → `POST /v3/chats/{chatId}/voicememo`
|
|
68
|
+
|
|
69
|
+
Create and send operations support:
|
|
70
|
+
|
|
71
|
+
- text parts
|
|
72
|
+
- direct media URLs
|
|
73
|
+
- pre-uploaded `attachment_id` values
|
|
74
|
+
- rich link URLs
|
|
75
|
+
- threaded replies (`reply_to`)
|
|
76
|
+
- iMessage effects
|
|
77
|
+
- preferred service selection
|
|
78
|
+
- idempotency keys
|
|
62
79
|
|
|
63
80
|
**Chat Message**
|
|
64
81
|
|
|
65
82
|
- Get Many → `GET /v3/chats/{chatId}/messages`
|
|
66
83
|
- Get One → `GET /v3/messages/{messageId}`
|
|
67
84
|
- Create → `POST /v3/chats/{chatId}/messages`
|
|
85
|
+
- Edit → `PATCH /v3/messages/{messageId}`
|
|
68
86
|
- Delete → `DELETE /v3/messages/{messageId}`
|
|
69
87
|
- React → `POST /v3/messages/{messageId}/reactions`
|
|
88
|
+
- Get Thread → `GET /v3/messages/{messageId}/thread`
|
|
70
89
|
|
|
71
90
|
**Phone Number**
|
|
72
91
|
|
|
73
|
-
- Get Many → `GET /v3/
|
|
74
|
-
- Update → `PUT /v3/phonenumbers/{phoneNumberId}`
|
|
92
|
+
- Get Many → `GET /v3/phone_numbers`
|
|
75
93
|
|
|
76
94
|
**Webhook Subscription**
|
|
77
95
|
|
|
96
|
+
- Get Events → `GET /v3/webhook-events`
|
|
78
97
|
- Get Many → `GET /v3/webhook-subscriptions`
|
|
79
98
|
- Get One → `GET /v3/webhook-subscriptions/{subscriptionId}`
|
|
80
99
|
- Create → `POST /v3/webhook-subscriptions`
|
|
81
100
|
- Update → `PUT /v3/webhook-subscriptions/{subscriptionId}`
|
|
82
101
|
- Delete → `DELETE /v3/webhook-subscriptions/{subscriptionId}`
|
|
83
102
|
|
|
84
|
-
**Contact (extension)**
|
|
85
|
-
|
|
86
|
-
- Create → `POST /v3/contacts`
|
|
87
|
-
- Get One → `GET /v3/contacts/{contactId}`
|
|
88
|
-
- Update → `PUT /v3/contacts/{contactId}`
|
|
89
|
-
- Delete → `DELETE /v3/contacts/{contactId}`
|
|
90
|
-
|
|
91
|
-
**Chat Message (extension)**
|
|
92
|
-
|
|
93
|
-
- Edit → `POST /v3/messages/{messageId}/edit`
|
|
94
|
-
- Get Reaction → `GET /v3/chat_message_reactions/{reactionId}`
|
|
95
|
-
|
|
96
|
-
> **Note:** The “extension” endpoints above are currently implemented in the node but are not present in `V3.yaml`. They will be aligned once the spec is updated.
|
|
97
|
-
|
|
98
103
|
## Linq Trigger Node
|
|
99
104
|
|
|
100
105
|
Add the **Linq Trigger** node to start workflows when Linq events occur. The trigger automatically registers a webhook with Linq when the workflow is activated.
|
|
101
106
|
|
|
102
107
|
**Supported Events**
|
|
103
108
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
- `message
|
|
107
|
-
- `
|
|
108
|
-
- `
|
|
109
|
-
- `
|
|
110
|
-
- `
|
|
109
|
+
The trigger supports the current documented webhook event families, including:
|
|
110
|
+
|
|
111
|
+
- `message.*`
|
|
112
|
+
- `participant.*`
|
|
113
|
+
- `reaction.*`
|
|
114
|
+
- `chat.*`
|
|
115
|
+
- `phone_number.status_updated`
|
|
116
|
+
- `call.*`
|
|
111
117
|
|
|
112
118
|
**Security**
|
|
113
119
|
|
|
114
120
|
- The trigger verifies Linq webhooks using HMAC-SHA256
|
|
115
121
|
- Uses `X-Webhook-Timestamp` + raw request body for the signature payload: `{timestamp}.{payload}`
|
|
116
122
|
- Requires the **Webhook Signing Secret** from credentials
|
|
123
|
+
- Accepts both the current documented webhook envelope (`event_type` + `data`) and the older legacy envelope (`event` + `payload`)
|
|
117
124
|
|
|
118
125
|
## Examples
|
|
119
126
|
|
|
@@ -4,7 +4,7 @@ exports.LinqApi = void 0;
|
|
|
4
4
|
class LinqApi {
|
|
5
5
|
name = 'linqApi';
|
|
6
6
|
displayName = 'Linq API';
|
|
7
|
-
documentationUrl = 'https://apidocs.linqapp.com/
|
|
7
|
+
documentationUrl = 'https://apidocs.linqapp.com/documentation/getting-started';
|
|
8
8
|
properties = [
|
|
9
9
|
{
|
|
10
10
|
displayName: 'API Token',
|
|
@@ -38,7 +38,7 @@ class LinqApi {
|
|
|
38
38
|
test = {
|
|
39
39
|
request: {
|
|
40
40
|
baseURL: 'https://api.linqapp.com/api/partner',
|
|
41
|
-
url: '/v3/
|
|
41
|
+
url: '/v3/phone_numbers',
|
|
42
42
|
method: 'GET',
|
|
43
43
|
},
|
|
44
44
|
};
|
|
@@ -7,6 +7,86 @@ function formatPhoneNumber(phoneNumber) {
|
|
|
7
7
|
// Only remove space characters; do not modify digits/symbols or add prefixes
|
|
8
8
|
return phoneNumber.trim().replaceAll(' ', '');
|
|
9
9
|
}
|
|
10
|
+
function splitCommaSeparated(value) {
|
|
11
|
+
return value
|
|
12
|
+
.split(',')
|
|
13
|
+
.map(item => item.trim())
|
|
14
|
+
.filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
function parseHandles(value) {
|
|
17
|
+
return splitCommaSeparated(value).map(formatPhoneNumber);
|
|
18
|
+
}
|
|
19
|
+
function buildMessageParts(text, attachmentUrlsString, attachmentIdsString, linkUrl) {
|
|
20
|
+
const attachmentUrls = splitCommaSeparated(attachmentUrlsString).map(url => {
|
|
21
|
+
if (!isValidUrl(url)) {
|
|
22
|
+
throw new n8n_workflow_1.ApplicationError(`Invalid attachment URL: "${url}". URLs must be valid HTTP/HTTPS URLs.`);
|
|
23
|
+
}
|
|
24
|
+
return url;
|
|
25
|
+
});
|
|
26
|
+
const attachmentIds = splitCommaSeparated(attachmentIdsString);
|
|
27
|
+
const trimmedLinkUrl = linkUrl.trim();
|
|
28
|
+
if (trimmedLinkUrl) {
|
|
29
|
+
if (!isValidUrl(trimmedLinkUrl)) {
|
|
30
|
+
throw new n8n_workflow_1.ApplicationError(`Invalid link URL: "${trimmedLinkUrl}". URLs must be valid HTTP/HTTPS URLs.`);
|
|
31
|
+
}
|
|
32
|
+
if (text || attachmentUrls.length > 0 || attachmentIds.length > 0) {
|
|
33
|
+
throw new n8n_workflow_1.ApplicationError('Link URL must be the only message part. Remove text, attachment URLs, and attachment IDs before sending a rich link.');
|
|
34
|
+
}
|
|
35
|
+
return [{ type: 'link', value: trimmedLinkUrl }];
|
|
36
|
+
}
|
|
37
|
+
const parts = [];
|
|
38
|
+
if (text) {
|
|
39
|
+
parts.push({ type: 'text', value: text });
|
|
40
|
+
}
|
|
41
|
+
attachmentUrls.forEach(url => {
|
|
42
|
+
parts.push({ type: 'media', url });
|
|
43
|
+
});
|
|
44
|
+
attachmentIds.forEach(attachmentId => {
|
|
45
|
+
parts.push({ type: 'media', attachment_id: attachmentId });
|
|
46
|
+
});
|
|
47
|
+
if (!parts.length) {
|
|
48
|
+
throw new n8n_workflow_1.ApplicationError('A message requires text, attachment URLs, attachment IDs, or a link URL.');
|
|
49
|
+
}
|
|
50
|
+
return parts;
|
|
51
|
+
}
|
|
52
|
+
function buildMessageContent(context, i) {
|
|
53
|
+
const messageText = context.getNodeParameter('messageText', i, '');
|
|
54
|
+
const attachmentUrls = context.getNodeParameter('attachmentUrls', i, '');
|
|
55
|
+
const attachmentIds = context.getNodeParameter('attachmentIds', i, '');
|
|
56
|
+
const linkUrl = context.getNodeParameter('linkUrl', i, '');
|
|
57
|
+
const idempotencyKey = context.getNodeParameter('idempotencyKey', i, '');
|
|
58
|
+
const preferredService = context.getNodeParameter('preferredService', i, '');
|
|
59
|
+
const effectType = context.getNodeParameter('effectType', i, '');
|
|
60
|
+
const effectName = context.getNodeParameter('effectName', i, '');
|
|
61
|
+
const replyToMessageId = context.getNodeParameter('replyToMessageId', i, '');
|
|
62
|
+
const replyToPartIndex = context.getNodeParameter('replyToPartIndex', i, 0);
|
|
63
|
+
const content = {
|
|
64
|
+
parts: buildMessageParts(messageText, attachmentUrls, attachmentIds, linkUrl),
|
|
65
|
+
};
|
|
66
|
+
if (idempotencyKey) {
|
|
67
|
+
content.idempotency_key = idempotencyKey;
|
|
68
|
+
}
|
|
69
|
+
if (preferredService) {
|
|
70
|
+
content.preferred_service = preferredService;
|
|
71
|
+
}
|
|
72
|
+
if (effectType || effectName) {
|
|
73
|
+
if (!effectType || !effectName) {
|
|
74
|
+
throw new n8n_workflow_1.ApplicationError('Both Effect Type and Effect Name are required when sending a message effect.');
|
|
75
|
+
}
|
|
76
|
+
content.effect = {
|
|
77
|
+
type: effectType,
|
|
78
|
+
name: effectName,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (replyToMessageId) {
|
|
82
|
+
const replyTo = { message_id: replyToMessageId };
|
|
83
|
+
if (replyToPartIndex > 0) {
|
|
84
|
+
replyTo.part_index = replyToPartIndex;
|
|
85
|
+
}
|
|
86
|
+
content.reply_to = replyTo;
|
|
87
|
+
}
|
|
88
|
+
return content;
|
|
89
|
+
}
|
|
10
90
|
class Linq {
|
|
11
91
|
description = {
|
|
12
92
|
displayName: 'Linq',
|
|
@@ -84,6 +164,11 @@ class Linq {
|
|
|
84
164
|
value: 'getOne',
|
|
85
165
|
action: 'Get one chat',
|
|
86
166
|
},
|
|
167
|
+
{
|
|
168
|
+
name: 'Leave',
|
|
169
|
+
value: 'leave',
|
|
170
|
+
action: 'Leave a group chat',
|
|
171
|
+
},
|
|
87
172
|
{
|
|
88
173
|
name: 'Mark as Read',
|
|
89
174
|
value: 'markAsRead',
|
|
@@ -141,15 +226,20 @@ class Linq {
|
|
|
141
226
|
action: 'Delete a chat message',
|
|
142
227
|
},
|
|
143
228
|
{
|
|
144
|
-
name: '
|
|
145
|
-
value: '
|
|
146
|
-
action: '
|
|
229
|
+
name: 'Edit',
|
|
230
|
+
value: 'edit',
|
|
231
|
+
action: 'Edit a chat message',
|
|
147
232
|
},
|
|
148
233
|
{
|
|
149
234
|
name: 'Get Many',
|
|
150
235
|
value: 'getAll',
|
|
151
236
|
action: 'Get many chat messages',
|
|
152
237
|
},
|
|
238
|
+
{
|
|
239
|
+
name: 'Get One',
|
|
240
|
+
value: 'get',
|
|
241
|
+
action: 'Get a chat message',
|
|
242
|
+
},
|
|
153
243
|
{
|
|
154
244
|
name: 'Get Thread',
|
|
155
245
|
value: 'getThread',
|
|
@@ -197,6 +287,11 @@ class Linq {
|
|
|
197
287
|
value: 'delete',
|
|
198
288
|
action: 'Delete a webhook subscription',
|
|
199
289
|
},
|
|
290
|
+
{
|
|
291
|
+
name: 'Get Events',
|
|
292
|
+
value: 'getEvents',
|
|
293
|
+
action: 'Get available webhook events',
|
|
294
|
+
},
|
|
200
295
|
{
|
|
201
296
|
name: 'Get Many',
|
|
202
297
|
value: 'getAll',
|
|
@@ -251,6 +346,7 @@ class Linq {
|
|
|
251
346
|
resource: ['chat'],
|
|
252
347
|
operation: [
|
|
253
348
|
'getOne',
|
|
349
|
+
'leave',
|
|
254
350
|
'shareContact',
|
|
255
351
|
'update',
|
|
256
352
|
'addParticipant',
|
|
@@ -271,7 +367,15 @@ class Linq {
|
|
|
271
367
|
type: 'string',
|
|
272
368
|
displayOptions: { show: { resource: ['chat'], operation: ['getAll'] } },
|
|
273
369
|
default: '',
|
|
274
|
-
description: '
|
|
370
|
+
description: 'Optional: filter chats by one of your Linq phone numbers (from)',
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
displayName: 'Participant Handle',
|
|
374
|
+
name: 'participantHandle',
|
|
375
|
+
type: 'string',
|
|
376
|
+
displayOptions: { show: { resource: ['chat'], operation: ['getAll'] } },
|
|
377
|
+
default: '',
|
|
378
|
+
description: 'Optional: filter chats by a participant phone number or email address',
|
|
275
379
|
},
|
|
276
380
|
{
|
|
277
381
|
displayName: 'From Phone Number',
|
|
@@ -293,7 +397,9 @@ class Linq {
|
|
|
293
397
|
displayName: 'Cursor',
|
|
294
398
|
name: 'cursor',
|
|
295
399
|
type: 'string',
|
|
296
|
-
displayOptions: {
|
|
400
|
+
displayOptions: {
|
|
401
|
+
show: { resource: ['chat', 'chatMessage'], operation: ['getAll', 'getThread'] },
|
|
402
|
+
},
|
|
297
403
|
default: '',
|
|
298
404
|
description: 'Cursor for pagination',
|
|
299
405
|
},
|
|
@@ -304,7 +410,9 @@ class Linq {
|
|
|
304
410
|
typeOptions: {
|
|
305
411
|
minValue: 1,
|
|
306
412
|
},
|
|
307
|
-
displayOptions: {
|
|
413
|
+
displayOptions: {
|
|
414
|
+
show: { resource: ['chat', 'chatMessage'], operation: ['getAll', 'getThread'] },
|
|
415
|
+
},
|
|
308
416
|
default: 50,
|
|
309
417
|
description: 'Max number of results to return',
|
|
310
418
|
},
|
|
@@ -355,7 +463,89 @@ class Linq {
|
|
|
355
463
|
type: 'string',
|
|
356
464
|
displayOptions: { show: { resource: ['chat'], operation: ['create'] } },
|
|
357
465
|
default: '',
|
|
358
|
-
description: 'Comma-separated list of attachment URLs',
|
|
466
|
+
description: 'Comma-separated list of attachment URLs to send directly as media parts',
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
displayName: 'Attachment IDs',
|
|
470
|
+
name: 'attachmentIds',
|
|
471
|
+
type: 'string',
|
|
472
|
+
displayOptions: {
|
|
473
|
+
show: { resource: ['chat', 'chatMessage'], operation: ['create'] },
|
|
474
|
+
},
|
|
475
|
+
default: '',
|
|
476
|
+
description: 'Comma-separated list of pre-uploaded Linq attachment IDs to send as media parts',
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
displayName: 'Link URL',
|
|
480
|
+
name: 'linkUrl',
|
|
481
|
+
type: 'string',
|
|
482
|
+
displayOptions: {
|
|
483
|
+
show: { resource: ['chat', 'chatMessage'], operation: ['create'] },
|
|
484
|
+
},
|
|
485
|
+
default: '',
|
|
486
|
+
description: 'Optional rich link preview URL. Must be the only message part when provided.',
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
displayName: 'Preferred Service',
|
|
490
|
+
name: 'preferredService',
|
|
491
|
+
type: 'options',
|
|
492
|
+
displayOptions: {
|
|
493
|
+
show: { resource: ['chat', 'chatMessage'], operation: ['create'] },
|
|
494
|
+
},
|
|
495
|
+
options: [
|
|
496
|
+
{ name: 'Default', value: '' },
|
|
497
|
+
{ name: 'iMessage', value: 'iMessage' },
|
|
498
|
+
{ name: 'RCS', value: 'RCS' },
|
|
499
|
+
{ name: 'SMS', value: 'SMS' },
|
|
500
|
+
],
|
|
501
|
+
default: '',
|
|
502
|
+
description: 'Optional preferred messaging service for the message',
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
displayName: 'Effect Type',
|
|
506
|
+
name: 'effectType',
|
|
507
|
+
type: 'options',
|
|
508
|
+
displayOptions: {
|
|
509
|
+
show: { resource: ['chat', 'chatMessage'], operation: ['create'] },
|
|
510
|
+
},
|
|
511
|
+
options: [
|
|
512
|
+
{ name: 'None', value: '' },
|
|
513
|
+
{ name: 'Screen', value: 'screen' },
|
|
514
|
+
{ name: 'Bubble', value: 'bubble' },
|
|
515
|
+
],
|
|
516
|
+
default: '',
|
|
517
|
+
description: 'Optional iMessage effect type',
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
displayName: 'Effect Name',
|
|
521
|
+
name: 'effectName',
|
|
522
|
+
type: 'string',
|
|
523
|
+
displayOptions: {
|
|
524
|
+
show: { resource: ['chat', 'chatMessage'], operation: ['create'] },
|
|
525
|
+
},
|
|
526
|
+
default: '',
|
|
527
|
+
description: 'Optional effect name such as confetti, fireworks, slam, loud, gentle, or invisible',
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
displayName: 'Reply To Message ID',
|
|
531
|
+
name: 'replyToMessageId',
|
|
532
|
+
type: 'string',
|
|
533
|
+
displayOptions: {
|
|
534
|
+
show: { resource: ['chat', 'chatMessage'], operation: ['create'] },
|
|
535
|
+
},
|
|
536
|
+
default: '',
|
|
537
|
+
description: 'Optional message ID to reply to',
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
displayName: 'Reply To Part Index',
|
|
541
|
+
name: 'replyToPartIndex',
|
|
542
|
+
type: 'number',
|
|
543
|
+
typeOptions: { minValue: 0 },
|
|
544
|
+
displayOptions: {
|
|
545
|
+
show: { resource: ['chat', 'chatMessage'], operation: ['create'] },
|
|
546
|
+
},
|
|
547
|
+
default: 0,
|
|
548
|
+
description: 'Optional part index to reply to when sending a threaded reply',
|
|
359
549
|
},
|
|
360
550
|
// Update Chat parameters
|
|
361
551
|
{
|
|
@@ -394,6 +584,14 @@ class Linq {
|
|
|
394
584
|
default: '',
|
|
395
585
|
description: 'The URL of the voice memo to send',
|
|
396
586
|
},
|
|
587
|
+
{
|
|
588
|
+
displayName: 'Voice Memo Attachment ID',
|
|
589
|
+
name: 'voiceMemoAttachmentId',
|
|
590
|
+
type: 'string',
|
|
591
|
+
displayOptions: { show: { resource: ['chat'], operation: ['sendVoiceMemo'] } },
|
|
592
|
+
default: '',
|
|
593
|
+
description: 'Optional pre-uploaded Linq attachment ID for the voice memo',
|
|
594
|
+
},
|
|
397
595
|
// Chat Message parameters
|
|
398
596
|
{
|
|
399
597
|
displayName: 'Chat Message ID',
|
|
@@ -402,7 +600,7 @@ class Linq {
|
|
|
402
600
|
displayOptions: {
|
|
403
601
|
show: {
|
|
404
602
|
resource: ['chatMessage'],
|
|
405
|
-
operation: ['
|
|
603
|
+
operation: ['get', 'edit', 'delete', 'react', 'getThread'],
|
|
406
604
|
},
|
|
407
605
|
},
|
|
408
606
|
default: '',
|
|
@@ -438,17 +636,70 @@ class Linq {
|
|
|
438
636
|
displayName: 'Idempotency Key',
|
|
439
637
|
name: 'idempotencyKey',
|
|
440
638
|
type: 'string',
|
|
441
|
-
displayOptions: {
|
|
639
|
+
displayOptions: {
|
|
640
|
+
show: { resource: ['chat', 'chatMessage'], operation: ['create'] },
|
|
641
|
+
},
|
|
442
642
|
default: '',
|
|
443
643
|
description: 'Optional idempotency key for message creation',
|
|
444
644
|
},
|
|
445
645
|
{
|
|
446
|
-
displayName: '
|
|
646
|
+
displayName: 'Edit Text',
|
|
647
|
+
name: 'editText',
|
|
648
|
+
type: 'string',
|
|
649
|
+
displayOptions: { show: { resource: ['chatMessage'], operation: ['edit'] } },
|
|
650
|
+
default: '',
|
|
651
|
+
description: 'The new text to apply to the selected message part',
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
displayName: 'Reaction Type',
|
|
447
655
|
name: 'reaction',
|
|
448
656
|
type: 'string',
|
|
449
657
|
displayOptions: { show: { resource: ['chatMessage'], operation: ['react'] } },
|
|
450
658
|
default: '',
|
|
451
|
-
description: 'The reaction to add',
|
|
659
|
+
description: 'The reaction type to add or remove (for example love, like, custom)',
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
displayName: 'Reaction Operation',
|
|
663
|
+
name: 'reactionOperation',
|
|
664
|
+
type: 'options',
|
|
665
|
+
displayOptions: { show: { resource: ['chatMessage'], operation: ['react'] } },
|
|
666
|
+
options: [
|
|
667
|
+
{ name: 'Add', value: 'add' },
|
|
668
|
+
{ name: 'Remove', value: 'remove' },
|
|
669
|
+
],
|
|
670
|
+
default: 'add',
|
|
671
|
+
description: 'Whether to add or remove the reaction',
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
displayName: 'Custom Emoji',
|
|
675
|
+
name: 'customEmoji',
|
|
676
|
+
type: 'string',
|
|
677
|
+
displayOptions: { show: { resource: ['chatMessage'], operation: ['react'] } },
|
|
678
|
+
default: '',
|
|
679
|
+
description: 'Optional custom emoji value when reaction type is custom',
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
displayName: 'Message Part Index',
|
|
683
|
+
name: 'messagePartIndex',
|
|
684
|
+
type: 'number',
|
|
685
|
+
typeOptions: { minValue: 0 },
|
|
686
|
+
displayOptions: {
|
|
687
|
+
show: { resource: ['chatMessage'], operation: ['edit', 'react'] },
|
|
688
|
+
},
|
|
689
|
+
default: 0,
|
|
690
|
+
description: 'Optional message part index for editing or reacting to a specific part',
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
displayName: 'Order',
|
|
694
|
+
name: 'threadOrder',
|
|
695
|
+
type: 'options',
|
|
696
|
+
displayOptions: { show: { resource: ['chatMessage'], operation: ['getThread'] } },
|
|
697
|
+
options: [
|
|
698
|
+
{ name: 'Ascending', value: 'asc' },
|
|
699
|
+
{ name: 'Descending', value: 'desc' },
|
|
700
|
+
],
|
|
701
|
+
default: 'asc',
|
|
702
|
+
description: 'Sort order for thread messages',
|
|
452
703
|
},
|
|
453
704
|
// Webhook Subscription parameters
|
|
454
705
|
{
|
|
@@ -481,15 +732,35 @@ class Linq {
|
|
|
481
732
|
default: '',
|
|
482
733
|
description: 'Comma-separated list of events',
|
|
483
734
|
},
|
|
735
|
+
{
|
|
736
|
+
displayName: 'Phone Numbers',
|
|
737
|
+
name: 'webhookPhoneNumbers',
|
|
738
|
+
type: 'string',
|
|
739
|
+
displayOptions: {
|
|
740
|
+
show: { resource: ['webhookSubscription'], operation: ['create', 'update'] },
|
|
741
|
+
},
|
|
742
|
+
default: '',
|
|
743
|
+
description: 'Optional comma-separated list of phone numbers to filter webhook deliveries for',
|
|
744
|
+
},
|
|
484
745
|
{
|
|
485
746
|
displayName: 'Active',
|
|
486
747
|
name: 'active',
|
|
487
748
|
type: 'boolean',
|
|
488
749
|
displayOptions: {
|
|
489
|
-
show: { resource: ['webhookSubscription'], operation: ['
|
|
750
|
+
show: { resource: ['webhookSubscription'], operation: ['update'] },
|
|
490
751
|
},
|
|
491
752
|
default: true,
|
|
492
|
-
description: 'Whether the webhook is active',
|
|
753
|
+
description: 'Whether the webhook subscription is active',
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
displayName: 'Clear Phone Number Filter',
|
|
757
|
+
name: 'clearWebhookPhoneNumbers',
|
|
758
|
+
type: 'boolean',
|
|
759
|
+
displayOptions: {
|
|
760
|
+
show: { resource: ['webhookSubscription'], operation: ['update'] },
|
|
761
|
+
},
|
|
762
|
+
default: false,
|
|
763
|
+
description: 'Whether to remove any existing phone number filter on the subscription',
|
|
493
764
|
},
|
|
494
765
|
// Attachment parameters
|
|
495
766
|
{
|
|
@@ -530,7 +801,7 @@ class Linq {
|
|
|
530
801
|
type: 'string',
|
|
531
802
|
displayOptions: { show: { resource: ['attachment'], operation: ['updateStatus'] } },
|
|
532
803
|
default: '',
|
|
533
|
-
description: 'The status of the attachment (
|
|
804
|
+
description: 'The status of the attachment (pending, complete, or failed)',
|
|
534
805
|
},
|
|
535
806
|
],
|
|
536
807
|
};
|
|
@@ -577,35 +848,6 @@ class Linq {
|
|
|
577
848
|
}
|
|
578
849
|
}
|
|
579
850
|
exports.Linq = Linq;
|
|
580
|
-
async function uploadAttachment(context, url) {
|
|
581
|
-
const response = await context.helpers.httpRequest({
|
|
582
|
-
method: 'GET',
|
|
583
|
-
url,
|
|
584
|
-
encoding: 'arraybuffer',
|
|
585
|
-
});
|
|
586
|
-
const buffer = Buffer.from(response);
|
|
587
|
-
const contentType = 'application/octet-stream';
|
|
588
|
-
const uploadConfig = await context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
|
|
589
|
-
method: 'POST',
|
|
590
|
-
url: 'https://api.linqapp.com/api/partner/v3/attachments',
|
|
591
|
-
headers: {
|
|
592
|
-
'Content-Type': 'application/json',
|
|
593
|
-
Accept: 'application/json',
|
|
594
|
-
},
|
|
595
|
-
body: {
|
|
596
|
-
content_type: contentType,
|
|
597
|
-
},
|
|
598
|
-
});
|
|
599
|
-
await context.helpers.httpRequest({
|
|
600
|
-
method: 'PUT',
|
|
601
|
-
url: uploadConfig.upload_url,
|
|
602
|
-
headers: {
|
|
603
|
-
'Content-Type': contentType,
|
|
604
|
-
},
|
|
605
|
-
body: buffer,
|
|
606
|
-
});
|
|
607
|
-
return uploadConfig.id;
|
|
608
|
-
}
|
|
609
851
|
function isValidUrl(urlString) {
|
|
610
852
|
try {
|
|
611
853
|
const url = new URL(urlString);
|
|
@@ -615,44 +857,16 @@ function isValidUrl(urlString) {
|
|
|
615
857
|
return false;
|
|
616
858
|
}
|
|
617
859
|
}
|
|
618
|
-
async function createParts(context, text, attachmentUrlsString) {
|
|
619
|
-
const parts = [];
|
|
620
|
-
if (text) {
|
|
621
|
-
parts.push({ type: 'text', value: text });
|
|
622
|
-
}
|
|
623
|
-
if (attachmentUrlsString) {
|
|
624
|
-
const urls = attachmentUrlsString
|
|
625
|
-
.split(',')
|
|
626
|
-
.map(u => u.trim())
|
|
627
|
-
.filter(Boolean)
|
|
628
|
-
.filter(u => {
|
|
629
|
-
if (!isValidUrl(u)) {
|
|
630
|
-
throw new n8n_workflow_1.ApplicationError(`Invalid attachment URL: "${u}". URLs must be valid HTTP/HTTPS URLs.`);
|
|
631
|
-
}
|
|
632
|
-
return true;
|
|
633
|
-
});
|
|
634
|
-
// Upload attachments in batches to limit concurrency and avoid OOM
|
|
635
|
-
const CONCURRENCY_LIMIT = 3;
|
|
636
|
-
const attachmentIds = [];
|
|
637
|
-
for (let i = 0; i < urls.length; i += CONCURRENCY_LIMIT) {
|
|
638
|
-
const chunk = urls.slice(i, i + CONCURRENCY_LIMIT);
|
|
639
|
-
const chunkResults = await Promise.all(chunk.map(url => uploadAttachment(context, url)));
|
|
640
|
-
attachmentIds.push(...chunkResults);
|
|
641
|
-
}
|
|
642
|
-
attachmentIds.forEach(id => {
|
|
643
|
-
parts.push({ type: 'media', attachment_id: id });
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
return parts;
|
|
647
|
-
}
|
|
648
860
|
async function chatGetAll(context, i) {
|
|
649
861
|
const phoneNumber = context.getNodeParameter('phoneNumber', i, '');
|
|
862
|
+
const participantHandle = context.getNodeParameter('participantHandle', i, '');
|
|
650
863
|
const cursor = context.getNodeParameter('cursor', i, '');
|
|
651
864
|
const limit = context.getNodeParameter('limit', i);
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
865
|
+
const qs = {};
|
|
866
|
+
if (phoneNumber)
|
|
867
|
+
qs.from = formatPhoneNumber(phoneNumber);
|
|
868
|
+
if (participantHandle)
|
|
869
|
+
qs.to = formatPhoneNumber(participantHandle);
|
|
656
870
|
if (cursor)
|
|
657
871
|
qs.cursor = cursor;
|
|
658
872
|
if (limit && limit !== 50)
|
|
@@ -673,12 +887,19 @@ async function chatGetOne(context, i) {
|
|
|
673
887
|
async function chatFind(context, i) {
|
|
674
888
|
const fromPhoneNumber = context.getNodeParameter('fromPhoneNumber', i, '');
|
|
675
889
|
const phoneNumbers = context.getNodeParameter('phoneNumbers', i, '');
|
|
676
|
-
|
|
677
|
-
|
|
890
|
+
const handles = parseHandles(phoneNumbers);
|
|
891
|
+
if (!fromPhoneNumber && handles.length === 0) {
|
|
892
|
+
throw new n8n_workflow_1.ApplicationError('Provide a from phone number or at least one participant handle');
|
|
893
|
+
}
|
|
894
|
+
const qs = {};
|
|
895
|
+
if (fromPhoneNumber) {
|
|
896
|
+
qs.from = formatPhoneNumber(fromPhoneNumber);
|
|
897
|
+
}
|
|
898
|
+
if (handles.length === 1) {
|
|
899
|
+
qs.to = handles[0];
|
|
678
900
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
qs['phone_numbers[]'] = phoneNumbers.split(',').map(p => formatPhoneNumber(p.trim()));
|
|
901
|
+
else if (handles.length > 1) {
|
|
902
|
+
qs['phone_numbers[]'] = handles;
|
|
682
903
|
}
|
|
683
904
|
return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
|
|
684
905
|
method: 'GET',
|
|
@@ -690,27 +911,44 @@ async function chatCreate(context, i) {
|
|
|
690
911
|
const sendFrom = context.getNodeParameter('sendFrom', i);
|
|
691
912
|
const displayName = context.getNodeParameter('displayName', i);
|
|
692
913
|
const phoneNumbers = context.getNodeParameter('phoneNumbers', i);
|
|
693
|
-
const messageText = context.getNodeParameter('messageText', i);
|
|
694
|
-
const attachmentUrls = context.getNodeParameter('attachmentUrls', i, '');
|
|
695
914
|
if (!sendFrom) {
|
|
696
915
|
throw new n8n_workflow_1.ApplicationError('send_from is required by Linq when creating chats');
|
|
697
916
|
}
|
|
698
|
-
const
|
|
917
|
+
const recipients = parseHandles(phoneNumbers);
|
|
918
|
+
if (!recipients.length) {
|
|
919
|
+
throw new n8n_workflow_1.ApplicationError('At least one recipient phone number or email address is required');
|
|
920
|
+
}
|
|
699
921
|
const body = {
|
|
700
922
|
from: formatPhoneNumber(sendFrom),
|
|
701
|
-
to:
|
|
702
|
-
message:
|
|
923
|
+
to: recipients,
|
|
924
|
+
message: buildMessageContent(context, i),
|
|
703
925
|
};
|
|
704
|
-
|
|
705
|
-
body.display_name = displayName;
|
|
706
|
-
return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
|
|
926
|
+
const response = (await context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
|
|
707
927
|
method: 'POST',
|
|
708
928
|
url: 'https://api.linqapp.com/api/partner/v3/chats',
|
|
709
929
|
headers: {
|
|
710
930
|
'Content-Type': 'application/json',
|
|
711
931
|
},
|
|
712
932
|
body,
|
|
713
|
-
});
|
|
933
|
+
}));
|
|
934
|
+
if (displayName) {
|
|
935
|
+
const createdChat = response.chat;
|
|
936
|
+
const createdChatId = createdChat?.id;
|
|
937
|
+
if (!createdChatId) {
|
|
938
|
+
throw new n8n_workflow_1.ApplicationError('Linq created the chat but did not return a chat ID for the follow-up display name update.');
|
|
939
|
+
}
|
|
940
|
+
await context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
|
|
941
|
+
method: 'PUT',
|
|
942
|
+
url: `https://api.linqapp.com/api/partner/v3/chats/${createdChatId}`,
|
|
943
|
+
headers: {
|
|
944
|
+
'Content-Type': 'application/json',
|
|
945
|
+
},
|
|
946
|
+
body: {
|
|
947
|
+
display_name: displayName,
|
|
948
|
+
},
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
return response;
|
|
714
952
|
}
|
|
715
953
|
async function chatShareContact(context, i) {
|
|
716
954
|
const chatId = context.getNodeParameter('chatId', i);
|
|
@@ -747,6 +985,16 @@ async function chatUpdate(context, i) {
|
|
|
747
985
|
json: true,
|
|
748
986
|
});
|
|
749
987
|
}
|
|
988
|
+
async function chatLeave(context, i) {
|
|
989
|
+
const chatId = context.getNodeParameter('chatId', i);
|
|
990
|
+
if (!chatId) {
|
|
991
|
+
throw new n8n_workflow_1.ApplicationError('chatId is required to leave a chat');
|
|
992
|
+
}
|
|
993
|
+
return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
|
|
994
|
+
method: 'POST',
|
|
995
|
+
url: `https://api.linqapp.com/api/partner/v3/chats/${chatId}/leave`,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
750
998
|
async function chatAddParticipant(context, i) {
|
|
751
999
|
const chatId = context.getNodeParameter('chatId', i);
|
|
752
1000
|
const handle = context.getNodeParameter('handle', i);
|
|
@@ -817,12 +1065,26 @@ async function chatMarkAsRead(context, i) {
|
|
|
817
1065
|
}
|
|
818
1066
|
async function chatSendVoiceMemo(context, i) {
|
|
819
1067
|
const chatId = context.getNodeParameter('chatId', i);
|
|
820
|
-
const voiceMemoUrl = context.getNodeParameter('voiceMemoUrl', i);
|
|
1068
|
+
const voiceMemoUrl = context.getNodeParameter('voiceMemoUrl', i, '');
|
|
1069
|
+
const voiceMemoAttachmentId = context.getNodeParameter('voiceMemoAttachmentId', i, '');
|
|
821
1070
|
if (!chatId) {
|
|
822
1071
|
throw new n8n_workflow_1.ApplicationError('chatId is required to send a voice memo');
|
|
823
1072
|
}
|
|
824
|
-
if (!voiceMemoUrl) {
|
|
825
|
-
throw new n8n_workflow_1.ApplicationError('
|
|
1073
|
+
if (!voiceMemoUrl && !voiceMemoAttachmentId) {
|
|
1074
|
+
throw new n8n_workflow_1.ApplicationError('Either Voice Memo URL or Voice Memo Attachment ID is required to send a voice memo');
|
|
1075
|
+
}
|
|
1076
|
+
if (voiceMemoUrl && voiceMemoAttachmentId) {
|
|
1077
|
+
throw new n8n_workflow_1.ApplicationError('Provide either Voice Memo URL or Voice Memo Attachment ID, but not both.');
|
|
1078
|
+
}
|
|
1079
|
+
const body = {};
|
|
1080
|
+
if (voiceMemoUrl) {
|
|
1081
|
+
if (!isValidUrl(voiceMemoUrl)) {
|
|
1082
|
+
throw new n8n_workflow_1.ApplicationError('Voice Memo URL must be a valid HTTP/HTTPS URL');
|
|
1083
|
+
}
|
|
1084
|
+
body.voice_memo_url = voiceMemoUrl;
|
|
1085
|
+
}
|
|
1086
|
+
if (voiceMemoAttachmentId) {
|
|
1087
|
+
body.attachment_id = voiceMemoAttachmentId;
|
|
826
1088
|
}
|
|
827
1089
|
return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
|
|
828
1090
|
method: 'POST',
|
|
@@ -830,7 +1092,7 @@ async function chatSendVoiceMemo(context, i) {
|
|
|
830
1092
|
headers: {
|
|
831
1093
|
'Content-Type': 'application/json',
|
|
832
1094
|
},
|
|
833
|
-
body
|
|
1095
|
+
body,
|
|
834
1096
|
json: true,
|
|
835
1097
|
});
|
|
836
1098
|
}
|
|
@@ -864,8 +1126,8 @@ async function attachmentRequestUpload(context, i) {
|
|
|
864
1126
|
'Content-Type': 'application/json',
|
|
865
1127
|
},
|
|
866
1128
|
body: {
|
|
867
|
-
|
|
868
|
-
|
|
1129
|
+
filename: fileName,
|
|
1130
|
+
size_bytes: fileSize,
|
|
869
1131
|
content_type: contentType,
|
|
870
1132
|
},
|
|
871
1133
|
json: true,
|
|
@@ -904,6 +1166,8 @@ async function handleChatOperation(context, i, operation) {
|
|
|
904
1166
|
return chatShareContact(context, i);
|
|
905
1167
|
case 'update':
|
|
906
1168
|
return chatUpdate(context, i);
|
|
1169
|
+
case 'leave':
|
|
1170
|
+
return chatLeave(context, i);
|
|
907
1171
|
case 'addParticipant':
|
|
908
1172
|
return chatAddParticipant(context, i);
|
|
909
1173
|
case 'removeParticipant':
|
|
@@ -947,17 +1211,10 @@ async function chatMessageGetOne(context, i) {
|
|
|
947
1211
|
}
|
|
948
1212
|
async function chatMessageCreate(context, i) {
|
|
949
1213
|
const chatId = context.getNodeParameter('chatId', i);
|
|
950
|
-
const messageText = context.getNodeParameter('messageText', i);
|
|
951
|
-
const attachmentUrls = context.getNodeParameter('attachmentUrls', i, '');
|
|
952
|
-
const idempotencyKey = context.getNodeParameter('idempotencyKey', i, '');
|
|
953
1214
|
if (!chatId) {
|
|
954
1215
|
throw new n8n_workflow_1.ApplicationError('chatId is required to create a chat message');
|
|
955
1216
|
}
|
|
956
|
-
const
|
|
957
|
-
const body = { message: { parts } };
|
|
958
|
-
if (idempotencyKey) {
|
|
959
|
-
body.message.idempotency_key = idempotencyKey;
|
|
960
|
-
}
|
|
1217
|
+
const body = { message: buildMessageContent(context, i) };
|
|
961
1218
|
return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
|
|
962
1219
|
method: 'POST',
|
|
963
1220
|
url: `https://api.linqapp.com/api/partner/v3/chats/${chatId}/messages`,
|
|
@@ -989,40 +1246,85 @@ async function chatMessageDelete(context, i) {
|
|
|
989
1246
|
async function chatMessageReact(context, i) {
|
|
990
1247
|
const chatMessageId = context.getNodeParameter('chatMessageId', i);
|
|
991
1248
|
const reaction = context.getNodeParameter('reaction', i);
|
|
1249
|
+
const reactionOperation = context.getNodeParameter('reactionOperation', i, 'add');
|
|
1250
|
+
const customEmoji = context.getNodeParameter('customEmoji', i, '');
|
|
1251
|
+
const messagePartIndex = context.getNodeParameter('messagePartIndex', i, 0);
|
|
992
1252
|
if (!chatMessageId) {
|
|
993
1253
|
throw new n8n_workflow_1.ApplicationError('chatMessageId is required to react to a message');
|
|
994
1254
|
}
|
|
995
1255
|
if (!reaction) {
|
|
996
1256
|
throw new n8n_workflow_1.ApplicationError('reaction is required to react to a message');
|
|
997
1257
|
}
|
|
1258
|
+
const body = { operation: reactionOperation, type: reaction };
|
|
1259
|
+
if (customEmoji) {
|
|
1260
|
+
body.custom_emoji = customEmoji;
|
|
1261
|
+
}
|
|
1262
|
+
if (messagePartIndex > 0) {
|
|
1263
|
+
body.part_index = messagePartIndex;
|
|
1264
|
+
}
|
|
998
1265
|
return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
|
|
999
1266
|
method: 'POST',
|
|
1000
1267
|
url: `https://api.linqapp.com/api/partner/v3/messages/${chatMessageId}/reactions`,
|
|
1001
1268
|
headers: {
|
|
1002
1269
|
'Content-Type': 'application/json',
|
|
1003
1270
|
},
|
|
1004
|
-
body
|
|
1271
|
+
body,
|
|
1272
|
+
json: true,
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
async function chatMessageEdit(context, i) {
|
|
1276
|
+
const chatMessageId = context.getNodeParameter('chatMessageId', i);
|
|
1277
|
+
const editText = context.getNodeParameter('editText', i);
|
|
1278
|
+
const messagePartIndex = context.getNodeParameter('messagePartIndex', i, 0);
|
|
1279
|
+
if (!chatMessageId) {
|
|
1280
|
+
throw new n8n_workflow_1.ApplicationError('chatMessageId is required to edit a message');
|
|
1281
|
+
}
|
|
1282
|
+
if (!editText) {
|
|
1283
|
+
throw new n8n_workflow_1.ApplicationError('editText is required to edit a message');
|
|
1284
|
+
}
|
|
1285
|
+
return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
|
|
1286
|
+
method: 'PATCH',
|
|
1287
|
+
url: `https://api.linqapp.com/api/partner/v3/messages/${chatMessageId}`,
|
|
1288
|
+
headers: {
|
|
1289
|
+
'Content-Type': 'application/json',
|
|
1290
|
+
},
|
|
1291
|
+
body: {
|
|
1292
|
+
part_index: messagePartIndex,
|
|
1293
|
+
text: editText,
|
|
1294
|
+
},
|
|
1005
1295
|
json: true,
|
|
1006
1296
|
});
|
|
1007
1297
|
}
|
|
1008
1298
|
async function chatMessageGetThread(context, i) {
|
|
1009
1299
|
const chatMessageId = context.getNodeParameter('chatMessageId', i);
|
|
1300
|
+
const cursor = context.getNodeParameter('cursor', i, '');
|
|
1301
|
+
const limit = context.getNodeParameter('limit', i, 50);
|
|
1302
|
+
const threadOrder = context.getNodeParameter('threadOrder', i, 'asc');
|
|
1010
1303
|
if (!chatMessageId) {
|
|
1011
1304
|
throw new n8n_workflow_1.ApplicationError('chatMessageId is required to get message thread');
|
|
1012
1305
|
}
|
|
1306
|
+
const qs = { order: threadOrder };
|
|
1307
|
+
if (cursor)
|
|
1308
|
+
qs.cursor = cursor;
|
|
1309
|
+
if (limit && limit !== 50)
|
|
1310
|
+
qs.limit = limit;
|
|
1013
1311
|
return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
|
|
1014
1312
|
method: 'GET',
|
|
1015
1313
|
url: `https://api.linqapp.com/api/partner/v3/messages/${chatMessageId}/thread`,
|
|
1314
|
+
qs,
|
|
1016
1315
|
});
|
|
1017
1316
|
}
|
|
1018
1317
|
async function handleChatMessageOperation(context, i, operation) {
|
|
1019
1318
|
switch (operation) {
|
|
1020
1319
|
case 'getAll':
|
|
1021
1320
|
return chatMessageGetAll(context, i);
|
|
1321
|
+
case 'getOne':
|
|
1022
1322
|
case 'get':
|
|
1023
1323
|
return chatMessageGetOne(context, i);
|
|
1024
1324
|
case 'create':
|
|
1025
1325
|
return chatMessageCreate(context, i);
|
|
1326
|
+
case 'edit':
|
|
1327
|
+
return chatMessageEdit(context, i);
|
|
1026
1328
|
case 'delete':
|
|
1027
1329
|
return chatMessageDelete(context, i);
|
|
1028
1330
|
case 'react':
|
|
@@ -1036,7 +1338,7 @@ async function handleChatMessageOperation(context, i, operation) {
|
|
|
1036
1338
|
async function phoneNumberGetAll(context) {
|
|
1037
1339
|
return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
|
|
1038
1340
|
method: 'GET',
|
|
1039
|
-
url: 'https://api.linqapp.com/api/partner/v3/
|
|
1341
|
+
url: 'https://api.linqapp.com/api/partner/v3/phone_numbers',
|
|
1040
1342
|
});
|
|
1041
1343
|
}
|
|
1042
1344
|
async function handlePhoneNumberOperation(context, _i, operation) {
|
|
@@ -1053,6 +1355,12 @@ async function webhookSubscriptionGetAll(context) {
|
|
|
1053
1355
|
url: 'https://api.linqapp.com/api/partner/v3/webhook-subscriptions',
|
|
1054
1356
|
});
|
|
1055
1357
|
}
|
|
1358
|
+
async function webhookSubscriptionGetEvents(context) {
|
|
1359
|
+
return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
|
|
1360
|
+
method: 'GET',
|
|
1361
|
+
url: 'https://api.linqapp.com/api/partner/v3/webhook-events',
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1056
1364
|
async function webhookSubscriptionGetOne(context, i) {
|
|
1057
1365
|
const webhookSubscriptionId = context.getNodeParameter('webhookSubscriptionId', i);
|
|
1058
1366
|
return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
|
|
@@ -1063,13 +1371,17 @@ async function webhookSubscriptionGetOne(context, i) {
|
|
|
1063
1371
|
async function webhookSubscriptionCreate(context, i) {
|
|
1064
1372
|
const webhookUrl = context.getNodeParameter('webhookUrl', i);
|
|
1065
1373
|
const events = context.getNodeParameter('events', i);
|
|
1066
|
-
const
|
|
1374
|
+
const phoneNumbers = parseHandles(context.getNodeParameter('webhookPhoneNumbers', i, ''));
|
|
1375
|
+
const subscribedEvents = splitCommaSeparated(events);
|
|
1376
|
+
if (!subscribedEvents.length) {
|
|
1377
|
+
throw new n8n_workflow_1.ApplicationError('At least one webhook event is required when creating a subscription');
|
|
1378
|
+
}
|
|
1067
1379
|
const body = {
|
|
1068
1380
|
target_url: webhookUrl,
|
|
1069
|
-
|
|
1381
|
+
subscribed_events: subscribedEvents,
|
|
1070
1382
|
};
|
|
1071
|
-
if (
|
|
1072
|
-
body.
|
|
1383
|
+
if (phoneNumbers.length) {
|
|
1384
|
+
body.phone_numbers = phoneNumbers;
|
|
1073
1385
|
}
|
|
1074
1386
|
return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
|
|
1075
1387
|
method: 'POST',
|
|
@@ -1083,13 +1395,21 @@ async function webhookSubscriptionCreate(context, i) {
|
|
|
1083
1395
|
async function webhookSubscriptionUpdate(context, i) {
|
|
1084
1396
|
const webhookSubscriptionId = context.getNodeParameter('webhookSubscriptionId', i);
|
|
1085
1397
|
const webhookUrl = context.getNodeParameter('webhookUrl', i);
|
|
1086
|
-
const events = context.getNodeParameter('events', i);
|
|
1398
|
+
const events = context.getNodeParameter('events', i, '');
|
|
1087
1399
|
const active = context.getNodeParameter('active', i);
|
|
1088
|
-
const
|
|
1400
|
+
const clearWebhookPhoneNumbers = context.getNodeParameter('clearWebhookPhoneNumbers', i, false);
|
|
1401
|
+
const phoneNumbers = parseHandles(context.getNodeParameter('webhookPhoneNumbers', i, ''));
|
|
1402
|
+
const body = { is_active: active };
|
|
1089
1403
|
if (webhookUrl)
|
|
1090
1404
|
body.target_url = webhookUrl;
|
|
1091
1405
|
if (events) {
|
|
1092
|
-
body.subscribed_events = events
|
|
1406
|
+
body.subscribed_events = splitCommaSeparated(events);
|
|
1407
|
+
}
|
|
1408
|
+
if (clearWebhookPhoneNumbers) {
|
|
1409
|
+
body.phone_numbers = [];
|
|
1410
|
+
}
|
|
1411
|
+
else if (phoneNumbers.length) {
|
|
1412
|
+
body.phone_numbers = phoneNumbers;
|
|
1093
1413
|
}
|
|
1094
1414
|
return context.helpers.httpRequestWithAuthentication.call(context, 'linqApi', {
|
|
1095
1415
|
method: 'PUT',
|
|
@@ -1109,6 +1429,8 @@ async function webhookSubscriptionDelete(context, i) {
|
|
|
1109
1429
|
}
|
|
1110
1430
|
async function handleWebhookSubscriptionOperation(context, i, operation) {
|
|
1111
1431
|
switch (operation) {
|
|
1432
|
+
case 'getEvents':
|
|
1433
|
+
return webhookSubscriptionGetEvents(context);
|
|
1112
1434
|
case 'getAll':
|
|
1113
1435
|
return webhookSubscriptionGetAll(context);
|
|
1114
1436
|
case 'getOne':
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { IWebhookFunctions, INodeType, INodeTypeDescription, IWebhookResponseData } from 'n8n-workflow';
|
|
1
|
+
import { IWebhookFunctions, INodeType, INodeTypeDescription, IWebhookResponseData, IDataObject } from 'n8n-workflow';
|
|
2
|
+
export declare function extractSignatureHex(signatureHeader: string): string;
|
|
3
|
+
export declare function extractIncomingEvent(bodyData: IDataObject, headers: IDataObject): string | undefined;
|
|
4
|
+
export declare function extractWebhookPayload(bodyData: IDataObject): IDataObject;
|
|
2
5
|
export declare class LinqTrigger implements INodeType {
|
|
3
6
|
description: INodeTypeDescription;
|
|
4
7
|
webhook(this: IWebhookFunctions): Promise<IWebhookResponseData>;
|
|
@@ -34,8 +34,32 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.LinqTrigger = void 0;
|
|
37
|
+
exports.extractSignatureHex = extractSignatureHex;
|
|
38
|
+
exports.extractIncomingEvent = extractIncomingEvent;
|
|
39
|
+
exports.extractWebhookPayload = extractWebhookPayload;
|
|
37
40
|
const n8n_workflow_1 = require("n8n-workflow");
|
|
38
41
|
const crypto = __importStar(require("node:crypto"));
|
|
42
|
+
function extractSignatureHex(signatureHeader) {
|
|
43
|
+
const trimmedSignature = signatureHeader.trim();
|
|
44
|
+
const [, prefixedValue] = trimmedSignature.split('=', 2);
|
|
45
|
+
const hexValue = prefixedValue ?? trimmedSignature;
|
|
46
|
+
if (!/^[a-fA-F0-9]+$/.test(hexValue)) {
|
|
47
|
+
throw new n8n_workflow_1.ApplicationError('Invalid signature format');
|
|
48
|
+
}
|
|
49
|
+
return hexValue;
|
|
50
|
+
}
|
|
51
|
+
function extractIncomingEvent(bodyData, headers) {
|
|
52
|
+
return (bodyData.event_type ??
|
|
53
|
+
bodyData.event ??
|
|
54
|
+
headers['x-webhook-event']);
|
|
55
|
+
}
|
|
56
|
+
function extractWebhookPayload(bodyData) {
|
|
57
|
+
const nestedPayload = (bodyData.data ?? bodyData.payload);
|
|
58
|
+
if (nestedPayload && typeof nestedPayload === 'object' && !Array.isArray(nestedPayload)) {
|
|
59
|
+
return nestedPayload;
|
|
60
|
+
}
|
|
61
|
+
return bodyData;
|
|
62
|
+
}
|
|
39
63
|
class LinqTrigger {
|
|
40
64
|
description = {
|
|
41
65
|
displayName: 'Linq Trigger',
|
|
@@ -71,6 +95,38 @@ class LinqTrigger {
|
|
|
71
95
|
name: 'events',
|
|
72
96
|
type: 'multiOptions',
|
|
73
97
|
options: [
|
|
98
|
+
{
|
|
99
|
+
name: 'Call Answered',
|
|
100
|
+
value: 'call.answered',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'Call Declined',
|
|
104
|
+
value: 'call.declined',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'Call Ended',
|
|
108
|
+
value: 'call.ended',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'Call Failed',
|
|
112
|
+
value: 'call.failed',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'Call Initiated',
|
|
116
|
+
value: 'call.initiated',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'Call No Answer',
|
|
120
|
+
value: 'call.no_answer',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'Call Ringing',
|
|
124
|
+
value: 'call.ringing',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'Chat Created',
|
|
128
|
+
value: 'chat.created',
|
|
129
|
+
},
|
|
74
130
|
{
|
|
75
131
|
name: 'Chat Group Icon Update Failed',
|
|
76
132
|
value: 'chat.group_icon_update_failed',
|
|
@@ -87,10 +143,22 @@ class LinqTrigger {
|
|
|
87
143
|
name: 'Chat Group Name Updated',
|
|
88
144
|
value: 'chat.group_name_updated',
|
|
89
145
|
},
|
|
146
|
+
{
|
|
147
|
+
name: 'Chat Typing Indicator Started',
|
|
148
|
+
value: 'chat.typing_indicator.started',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'Chat Typing Indicator Stopped',
|
|
152
|
+
value: 'chat.typing_indicator.stopped',
|
|
153
|
+
},
|
|
90
154
|
{
|
|
91
155
|
name: 'Message Delivered',
|
|
92
156
|
value: 'message.delivered',
|
|
93
157
|
},
|
|
158
|
+
{
|
|
159
|
+
name: 'Message Edited',
|
|
160
|
+
value: 'message.edited',
|
|
161
|
+
},
|
|
94
162
|
{
|
|
95
163
|
name: 'Message Failed',
|
|
96
164
|
value: 'message.failed',
|
|
@@ -115,6 +183,10 @@ class LinqTrigger {
|
|
|
115
183
|
name: 'Participant Removed',
|
|
116
184
|
value: 'participant.removed',
|
|
117
185
|
},
|
|
186
|
+
{
|
|
187
|
+
name: 'Phone Number Status Updated',
|
|
188
|
+
value: 'phone_number.status_updated',
|
|
189
|
+
},
|
|
118
190
|
{
|
|
119
191
|
name: 'Reaction Added',
|
|
120
192
|
value: 'reaction.added',
|
|
@@ -148,10 +220,7 @@ class LinqTrigger {
|
|
|
148
220
|
if (Math.abs(now - timestamp) > 300) {
|
|
149
221
|
throw new n8n_workflow_1.ApplicationError('Request timestamp is too old');
|
|
150
222
|
}
|
|
151
|
-
const
|
|
152
|
-
if (scheme !== 'sha256' || !signature) {
|
|
153
|
-
throw new n8n_workflow_1.ApplicationError('Invalid signature format');
|
|
154
|
-
}
|
|
223
|
+
const signature = extractSignatureHex(signatureHeader);
|
|
155
224
|
// 2. Verify HMAC Signature
|
|
156
225
|
const credentials = await this.getCredentials('linqApi');
|
|
157
226
|
const signingSecret = credentials.webhookSigningSecret;
|
|
@@ -176,22 +245,34 @@ class LinqTrigger {
|
|
|
176
245
|
}
|
|
177
246
|
// 3. Event Filtering
|
|
178
247
|
const events = this.getNodeParameter('events', []);
|
|
179
|
-
const incomingEvent = bodyData
|
|
248
|
+
const incomingEvent = extractIncomingEvent(bodyData, headers);
|
|
249
|
+
if (!incomingEvent) {
|
|
250
|
+
throw new n8n_workflow_1.ApplicationError('Unable to determine webhook event type');
|
|
251
|
+
}
|
|
180
252
|
// Filter if events are specified
|
|
181
253
|
if (events.length > 0 && !events.includes(incomingEvent)) {
|
|
182
254
|
return {};
|
|
183
255
|
}
|
|
184
|
-
// 4. Extract
|
|
185
|
-
|
|
186
|
-
const
|
|
256
|
+
// 4. Extract payload from both legacy and current webhook envelopes
|
|
257
|
+
const payload = extractWebhookPayload(bodyData);
|
|
258
|
+
const json = {
|
|
259
|
+
event: incomingEvent,
|
|
260
|
+
api_version: bodyData.api_version,
|
|
261
|
+
webhook_version: bodyData.webhook_version,
|
|
262
|
+
event_id: bodyData.event_id,
|
|
263
|
+
trace_id: bodyData.trace_id,
|
|
264
|
+
created_at: bodyData.created_at,
|
|
265
|
+
partner_id: bodyData.partner_id,
|
|
266
|
+
payload,
|
|
267
|
+
};
|
|
268
|
+
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
|
269
|
+
Object.assign(json, payload);
|
|
270
|
+
}
|
|
187
271
|
return {
|
|
188
272
|
workflowData: [
|
|
189
273
|
[
|
|
190
274
|
{
|
|
191
|
-
json
|
|
192
|
-
event: incomingEvent,
|
|
193
|
-
...payload,
|
|
194
|
-
},
|
|
275
|
+
json,
|
|
195
276
|
},
|
|
196
277
|
],
|
|
197
278
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-linq",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Linq API integration for n8n",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|
|
@@ -45,17 +45,17 @@
|
|
|
45
45
|
]
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@types/node": "^24.
|
|
49
|
-
"@typescript-eslint/parser": "~8.
|
|
50
|
-
"eslint": "^8.57.
|
|
51
|
-
"eslint-plugin-n8n-nodes-base": "^1.16.
|
|
52
|
-
"gulp": "^5.0.
|
|
53
|
-
"prettier": "^3.
|
|
54
|
-
"typescript": "^5.
|
|
55
|
-
"vitest": "^
|
|
48
|
+
"@types/node": "^24.12.0",
|
|
49
|
+
"@typescript-eslint/parser": "~8.58.0",
|
|
50
|
+
"eslint": "^8.57.1",
|
|
51
|
+
"eslint-plugin-n8n-nodes-base": "^1.16.6",
|
|
52
|
+
"gulp": "^5.0.1",
|
|
53
|
+
"prettier": "^3.8.1",
|
|
54
|
+
"typescript": "^5.9.3",
|
|
55
|
+
"vitest": "^4.1.2"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"n8n-workflow": "^1.120.
|
|
58
|
+
"n8n-workflow": "^1.120.10"
|
|
59
59
|
},
|
|
60
60
|
"overrides": {
|
|
61
61
|
"form-data": "^4.0.5",
|