@xenterprises/fastify-xtwilio 1.0.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/.dockerignore +63 -0
- package/.env.example +257 -0
- package/API.md +973 -0
- package/CHANGELOG.md +189 -0
- package/LICENSE +15 -0
- package/README.md +261 -0
- package/SECURITY.md +721 -0
- package/index.d.ts +999 -0
- package/package.json +55 -0
- package/server/app.js +88 -0
- package/src/services/conversations.js +328 -0
- package/src/services/email.js +362 -0
- package/src/services/rcs.js +284 -0
- package/src/services/sms.js +268 -0
- package/src/xTwilio.js +37 -0
- package/test/xTwilio.test.js +511 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
// src/services/email.js
|
|
2
|
+
import sgMail from "@sendgrid/mail";
|
|
3
|
+
import sgClient from "@sendgrid/client";
|
|
4
|
+
|
|
5
|
+
export async function setupEmail(fastify, options) {
|
|
6
|
+
if (options.active === false) return;
|
|
7
|
+
|
|
8
|
+
// Validate required credentials
|
|
9
|
+
if (!options.apiKey) {
|
|
10
|
+
throw new Error("SendGrid apiKey must be provided for Email service.");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!options.fromEmail) {
|
|
14
|
+
throw new Error("fromEmail must be provided for Email service.");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Initialize SendGrid clients
|
|
18
|
+
sgMail.setApiKey(options.apiKey);
|
|
19
|
+
sgClient.setApiKey(options.apiKey);
|
|
20
|
+
|
|
21
|
+
fastify.decorate("email", {
|
|
22
|
+
/**
|
|
23
|
+
* Send an email
|
|
24
|
+
* @param {string|string[]} to - Recipient email(s)
|
|
25
|
+
* @param {string} subject - Email subject
|
|
26
|
+
* @param {string} html - HTML content
|
|
27
|
+
* @param {string} text - Plain text content (optional)
|
|
28
|
+
* @param {object} extraOptions - Additional SendGrid options
|
|
29
|
+
* @returns {Promise<object>} Send result
|
|
30
|
+
*/
|
|
31
|
+
send: async (to, subject, html, text = null, extraOptions = {}) => {
|
|
32
|
+
try {
|
|
33
|
+
const msg = {
|
|
34
|
+
to,
|
|
35
|
+
from: options.fromEmail,
|
|
36
|
+
subject,
|
|
37
|
+
html,
|
|
38
|
+
text: text || html.replace(/<[^>]*>/g, ""), // Strip HTML tags if no text provided
|
|
39
|
+
...extraOptions,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const response = await sgMail.send(msg);
|
|
43
|
+
return { success: true, statusCode: response[0].statusCode, messageId: response[0].headers["x-message-id"] };
|
|
44
|
+
} catch (error) {
|
|
45
|
+
fastify.log.error("Email send failed:", error);
|
|
46
|
+
throw new Error("Failed to send email.");
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Send an email using a template
|
|
52
|
+
* @param {string|string[]} to - Recipient email(s)
|
|
53
|
+
* @param {string} subject - Email subject
|
|
54
|
+
* @param {string} templateId - SendGrid template ID
|
|
55
|
+
* @param {object} dynamicData - Template variables
|
|
56
|
+
* @param {object} extraOptions - Additional SendGrid options
|
|
57
|
+
* @returns {Promise<object>} Send result
|
|
58
|
+
*/
|
|
59
|
+
sendTemplate: async (to, subject, templateId, dynamicData = {}, extraOptions = {}) => {
|
|
60
|
+
try {
|
|
61
|
+
const msg = {
|
|
62
|
+
to,
|
|
63
|
+
from: options.fromEmail,
|
|
64
|
+
subject,
|
|
65
|
+
templateId,
|
|
66
|
+
dynamicTemplateData: { ...dynamicData, subject },
|
|
67
|
+
...extraOptions,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const response = await sgMail.send(msg);
|
|
71
|
+
return { success: true, statusCode: response[0].statusCode, messageId: response[0].headers["x-message-id"] };
|
|
72
|
+
} catch (error) {
|
|
73
|
+
fastify.log.error("Email sendTemplate failed:", error);
|
|
74
|
+
throw new Error("Failed to send template email.");
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Send an email with attachments
|
|
80
|
+
* @param {string|string[]} to - Recipient email(s)
|
|
81
|
+
* @param {string} subject - Email subject
|
|
82
|
+
* @param {string} html - HTML content
|
|
83
|
+
* @param {Array<{content: string, filename: string, type: string}>} attachments - Attachments
|
|
84
|
+
* @returns {Promise<object>} Send result
|
|
85
|
+
*/
|
|
86
|
+
sendWithAttachments: async (to, subject, html, attachments) => {
|
|
87
|
+
try {
|
|
88
|
+
const msg = {
|
|
89
|
+
to,
|
|
90
|
+
from: options.fromEmail,
|
|
91
|
+
subject,
|
|
92
|
+
html,
|
|
93
|
+
attachments: attachments.map((att) => ({
|
|
94
|
+
content: att.content, // Base64 encoded
|
|
95
|
+
filename: att.filename,
|
|
96
|
+
type: att.type,
|
|
97
|
+
disposition: att.disposition || "attachment",
|
|
98
|
+
})),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const response = await sgMail.send(msg);
|
|
102
|
+
return { success: true, statusCode: response[0].statusCode };
|
|
103
|
+
} catch (error) {
|
|
104
|
+
fastify.log.error("Email sendWithAttachments failed:", error);
|
|
105
|
+
throw new Error("Failed to send email with attachments.");
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Send bulk emails (multiple recipients, same content)
|
|
111
|
+
* @param {string[]} to - Array of recipient emails
|
|
112
|
+
* @param {string} subject - Email subject
|
|
113
|
+
* @param {string} html - HTML content
|
|
114
|
+
* @returns {Promise<object>} Bulk send result
|
|
115
|
+
*/
|
|
116
|
+
sendBulk: async (to, subject, html) => {
|
|
117
|
+
try {
|
|
118
|
+
const msg = {
|
|
119
|
+
to,
|
|
120
|
+
from: options.fromEmail,
|
|
121
|
+
subject,
|
|
122
|
+
html,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const response = await sgMail.sendMultiple(msg);
|
|
126
|
+
return { success: true, count: to.length, statusCode: response[0].statusCode };
|
|
127
|
+
} catch (error) {
|
|
128
|
+
fastify.log.error("Email sendBulk failed:", error);
|
|
129
|
+
throw new Error("Failed to send bulk emails.");
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Send personalized bulk emails (different content per recipient)
|
|
135
|
+
* @param {Array<{to: string, subject: string, html: string}>} messages - Array of message objects
|
|
136
|
+
* @returns {Promise<object[]>} Array of send results
|
|
137
|
+
*/
|
|
138
|
+
sendPersonalizedBulk: async (messages) => {
|
|
139
|
+
try {
|
|
140
|
+
const mailMessages = messages.map((msg) => ({
|
|
141
|
+
to: msg.to,
|
|
142
|
+
from: options.fromEmail,
|
|
143
|
+
subject: msg.subject,
|
|
144
|
+
html: msg.html,
|
|
145
|
+
text: msg.text,
|
|
146
|
+
}));
|
|
147
|
+
|
|
148
|
+
const responses = await Promise.allSettled(mailMessages.map((msg) => sgMail.send(msg)));
|
|
149
|
+
|
|
150
|
+
return responses.map((result, index) => {
|
|
151
|
+
if (result.status === "fulfilled") {
|
|
152
|
+
return {
|
|
153
|
+
success: true,
|
|
154
|
+
to: messages[index].to,
|
|
155
|
+
statusCode: result.value[0].statusCode,
|
|
156
|
+
};
|
|
157
|
+
} else {
|
|
158
|
+
return {
|
|
159
|
+
success: false,
|
|
160
|
+
to: messages[index].to,
|
|
161
|
+
error: result.reason.message,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
} catch (error) {
|
|
166
|
+
fastify.log.error("Email sendPersonalizedBulk failed:", error);
|
|
167
|
+
throw new Error("Failed to send personalized bulk emails.");
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Validate an email address using SendGrid Validation API
|
|
173
|
+
* @param {string} email - Email to validate
|
|
174
|
+
* @returns {Promise<object>} Validation result
|
|
175
|
+
*/
|
|
176
|
+
validate: async (email) => {
|
|
177
|
+
try {
|
|
178
|
+
const request = {
|
|
179
|
+
url: `/v3/validations/email`,
|
|
180
|
+
method: "POST",
|
|
181
|
+
body: { email },
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const [response, body] = await sgClient.request(request);
|
|
185
|
+
|
|
186
|
+
if (response.statusCode === 200) {
|
|
187
|
+
return {
|
|
188
|
+
email,
|
|
189
|
+
valid: body.result?.verdict === "Valid",
|
|
190
|
+
verdict: body.result?.verdict,
|
|
191
|
+
score: body.result?.score,
|
|
192
|
+
result: body.result,
|
|
193
|
+
};
|
|
194
|
+
} else {
|
|
195
|
+
throw new Error(body.errors ? body.errors.map((err) => err.message).join(", ") : "Validation failed");
|
|
196
|
+
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
fastify.log.error("Email validation failed:", error);
|
|
199
|
+
return {
|
|
200
|
+
email,
|
|
201
|
+
valid: false,
|
|
202
|
+
verdict: "Unknown",
|
|
203
|
+
error: error.message,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Add or update a contact in SendGrid
|
|
210
|
+
* @param {string} email - Contact email
|
|
211
|
+
* @param {object} data - Contact data (first_name, last_name, custom fields)
|
|
212
|
+
* @param {string[]} listIds - Array of list IDs to add contact to
|
|
213
|
+
* @returns {Promise<object>} Contact result
|
|
214
|
+
*/
|
|
215
|
+
addContact: async (email, data = {}, listIds = []) => {
|
|
216
|
+
try {
|
|
217
|
+
const request = {
|
|
218
|
+
url: `/v3/marketing/contacts`,
|
|
219
|
+
method: "PUT",
|
|
220
|
+
body: {
|
|
221
|
+
list_ids: listIds,
|
|
222
|
+
contacts: [
|
|
223
|
+
{
|
|
224
|
+
email,
|
|
225
|
+
first_name: data.firstName || data.first_name,
|
|
226
|
+
last_name: data.lastName || data.last_name,
|
|
227
|
+
...data.customFields,
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const [response, body] = await sgClient.request(request);
|
|
234
|
+
|
|
235
|
+
if (response.statusCode === 200 || response.statusCode === 202) {
|
|
236
|
+
return {
|
|
237
|
+
success: true,
|
|
238
|
+
jobId: body.job_id,
|
|
239
|
+
email,
|
|
240
|
+
};
|
|
241
|
+
} else {
|
|
242
|
+
throw new Error("Failed to add contact");
|
|
243
|
+
}
|
|
244
|
+
} catch (error) {
|
|
245
|
+
fastify.log.error("Email addContact failed:", error);
|
|
246
|
+
throw new Error("Failed to add contact.");
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Search for a contact by email
|
|
252
|
+
* @param {string} email - Contact email
|
|
253
|
+
* @returns {Promise<object>} Contact data
|
|
254
|
+
*/
|
|
255
|
+
searchContact: async (email) => {
|
|
256
|
+
try {
|
|
257
|
+
const request = {
|
|
258
|
+
url: `/v3/marketing/contacts/search/emails`,
|
|
259
|
+
method: "POST",
|
|
260
|
+
body: { emails: [email] },
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const [response, body] = await sgClient.request(request);
|
|
264
|
+
|
|
265
|
+
if (response.statusCode === 200 && body.result && body.result[email]) {
|
|
266
|
+
return {
|
|
267
|
+
found: true,
|
|
268
|
+
contact: body.result[email].contact,
|
|
269
|
+
};
|
|
270
|
+
} else {
|
|
271
|
+
return { found: false };
|
|
272
|
+
}
|
|
273
|
+
} catch (error) {
|
|
274
|
+
fastify.log.error("Email searchContact failed:", error);
|
|
275
|
+
throw new Error("Failed to search contact.");
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Delete a contact by ID
|
|
281
|
+
* @param {string} contactId - Contact ID
|
|
282
|
+
* @returns {Promise<boolean>} Success status
|
|
283
|
+
*/
|
|
284
|
+
deleteContact: async (contactId) => {
|
|
285
|
+
try {
|
|
286
|
+
const request = {
|
|
287
|
+
url: `/v3/marketing/contacts`,
|
|
288
|
+
method: "DELETE",
|
|
289
|
+
qs: { ids: contactId },
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const [response] = await sgClient.request(request);
|
|
293
|
+
return response.statusCode === 202 || response.statusCode === 200;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
fastify.log.error("Email deleteContact failed:", error);
|
|
296
|
+
throw new Error("Failed to delete contact.");
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Create a new contact list
|
|
302
|
+
* @param {string} name - List name
|
|
303
|
+
* @returns {Promise<object>} List object
|
|
304
|
+
*/
|
|
305
|
+
createList: async (name) => {
|
|
306
|
+
try {
|
|
307
|
+
const request = {
|
|
308
|
+
url: `/v3/marketing/lists`,
|
|
309
|
+
method: "POST",
|
|
310
|
+
body: { name },
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const [response, body] = await sgClient.request(request);
|
|
314
|
+
return { success: true, list: body };
|
|
315
|
+
} catch (error) {
|
|
316
|
+
fastify.log.error("Email createList failed:", error);
|
|
317
|
+
throw new Error("Failed to create list.");
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get all contact lists
|
|
323
|
+
* @returns {Promise<object[]>} Array of lists
|
|
324
|
+
*/
|
|
325
|
+
getLists: async () => {
|
|
326
|
+
try {
|
|
327
|
+
const request = {
|
|
328
|
+
url: `/v3/marketing/lists`,
|
|
329
|
+
method: "GET",
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const [response, body] = await sgClient.request(request);
|
|
333
|
+
return body.result || [];
|
|
334
|
+
} catch (error) {
|
|
335
|
+
fastify.log.error("Email getLists failed:", error);
|
|
336
|
+
throw new Error("Failed to get lists.");
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Delete a contact list
|
|
342
|
+
* @param {string} listId - List ID
|
|
343
|
+
* @returns {Promise<boolean>} Success status
|
|
344
|
+
*/
|
|
345
|
+
deleteList: async (listId) => {
|
|
346
|
+
try {
|
|
347
|
+
const request = {
|
|
348
|
+
url: `/v3/marketing/lists/${listId}`,
|
|
349
|
+
method: "DELETE",
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const [response] = await sgClient.request(request);
|
|
353
|
+
return response.statusCode === 202 || response.statusCode === 200 || response.statusCode === 204;
|
|
354
|
+
} catch (error) {
|
|
355
|
+
fastify.log.error("Email deleteList failed:", error);
|
|
356
|
+
throw new Error("Failed to delete list.");
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
console.info(" ✅ Email Service (SendGrid) Enabled");
|
|
362
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// src/services/rcs.js
|
|
2
|
+
import Twilio from "twilio";
|
|
3
|
+
|
|
4
|
+
export async function setupRCS(fastify, options) {
|
|
5
|
+
if (options.active === false) return;
|
|
6
|
+
|
|
7
|
+
// Validate required credentials
|
|
8
|
+
if (!options.accountSid || !options.authToken) {
|
|
9
|
+
throw new Error("Twilio accountSid and authToken must be provided for RCS.");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!options.messagingServiceSid) {
|
|
13
|
+
throw new Error("messagingServiceSid is required for RCS messaging.");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Initialize Twilio client
|
|
17
|
+
const twilioClient = Twilio(options.accountSid, options.authToken);
|
|
18
|
+
|
|
19
|
+
fastify.decorate("rcs", {
|
|
20
|
+
/**
|
|
21
|
+
* Send a basic RCS text message
|
|
22
|
+
* @param {string} to - Recipient phone number (E.164 format)
|
|
23
|
+
* @param {string} body - Message content
|
|
24
|
+
* @param {object} extraOptions - Optional parameters
|
|
25
|
+
* @returns {Promise<object>} Message result
|
|
26
|
+
*/
|
|
27
|
+
send: async (to, body, extraOptions = {}) => {
|
|
28
|
+
try {
|
|
29
|
+
const message = await twilioClient.messages.create({
|
|
30
|
+
body,
|
|
31
|
+
to,
|
|
32
|
+
messagingServiceSid: options.messagingServiceSid,
|
|
33
|
+
contentRetention: "retain",
|
|
34
|
+
...extraOptions,
|
|
35
|
+
});
|
|
36
|
+
return message;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
fastify.log.error("RCS send failed:", error);
|
|
39
|
+
throw new Error("Failed to send RCS message.");
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Send an RCS message with media
|
|
45
|
+
* @param {string} to - Recipient phone number
|
|
46
|
+
* @param {string} body - Message content
|
|
47
|
+
* @param {string|string[]} mediaUrl - Media URL(s)
|
|
48
|
+
* @returns {Promise<object>} Message result
|
|
49
|
+
*/
|
|
50
|
+
sendMedia: async (to, body, mediaUrl) => {
|
|
51
|
+
try {
|
|
52
|
+
const message = await twilioClient.messages.create({
|
|
53
|
+
body,
|
|
54
|
+
to,
|
|
55
|
+
messagingServiceSid: options.messagingServiceSid,
|
|
56
|
+
mediaUrl: Array.isArray(mediaUrl) ? mediaUrl : [mediaUrl],
|
|
57
|
+
});
|
|
58
|
+
return message;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
fastify.log.error("RCS sendMedia failed:", error);
|
|
61
|
+
throw new Error("Failed to send RCS media message.");
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Send an RCS message using Content Template
|
|
67
|
+
* @param {string} to - Recipient phone number
|
|
68
|
+
* @param {string} contentSid - Twilio Content Template SID
|
|
69
|
+
* @param {object} contentVariables - Template variables
|
|
70
|
+
* @returns {Promise<object>} Message result
|
|
71
|
+
*/
|
|
72
|
+
sendTemplate: async (to, contentSid, contentVariables = {}) => {
|
|
73
|
+
try {
|
|
74
|
+
const message = await twilioClient.messages.create({
|
|
75
|
+
to,
|
|
76
|
+
messagingServiceSid: options.messagingServiceSid,
|
|
77
|
+
contentSid,
|
|
78
|
+
contentVariables: JSON.stringify(contentVariables),
|
|
79
|
+
});
|
|
80
|
+
return message;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
fastify.log.error("RCS sendTemplate failed:", error);
|
|
83
|
+
throw new Error("Failed to send RCS template message.");
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Send an RCS rich card message
|
|
89
|
+
* @param {string} to - Recipient phone number
|
|
90
|
+
* @param {object} card - Rich card configuration
|
|
91
|
+
* @param {string} card.title - Card title
|
|
92
|
+
* @param {string} card.description - Card description
|
|
93
|
+
* @param {string} card.mediaUrl - Card media URL
|
|
94
|
+
* @param {Array} card.actions - Card actions/buttons
|
|
95
|
+
* @returns {Promise<object>} Message result
|
|
96
|
+
*/
|
|
97
|
+
sendRichCard: async (to, card) => {
|
|
98
|
+
try {
|
|
99
|
+
// Build content for rich card using Twilio Content API
|
|
100
|
+
const contentData = {
|
|
101
|
+
types: {
|
|
102
|
+
"twilio/card": {
|
|
103
|
+
title: card.title,
|
|
104
|
+
subtitle: card.description,
|
|
105
|
+
media: card.mediaUrl ? [card.mediaUrl] : [],
|
|
106
|
+
actions: card.actions || [],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Create content template if contentSid not provided
|
|
112
|
+
let contentSid = card.contentSid;
|
|
113
|
+
if (!contentSid) {
|
|
114
|
+
const content = await twilioClient.content.v1.contents.create({
|
|
115
|
+
friendly_name: `RCS Card ${Date.now()}`,
|
|
116
|
+
types: contentData.types,
|
|
117
|
+
});
|
|
118
|
+
contentSid = content.sid;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const message = await twilioClient.messages.create({
|
|
122
|
+
to,
|
|
123
|
+
messagingServiceSid: options.messagingServiceSid,
|
|
124
|
+
contentSid,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return message;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
fastify.log.error("RCS sendRichCard failed:", error);
|
|
130
|
+
throw new Error("Failed to send RCS rich card.");
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Send an RCS carousel message
|
|
136
|
+
* @param {string} to - Recipient phone number
|
|
137
|
+
* @param {object[]} cards - Array of card objects
|
|
138
|
+
* @returns {Promise<object>} Message result
|
|
139
|
+
*/
|
|
140
|
+
sendCarousel: async (to, cards) => {
|
|
141
|
+
try {
|
|
142
|
+
const contentData = {
|
|
143
|
+
types: {
|
|
144
|
+
"twilio/list-picker": {
|
|
145
|
+
body: "Please select an option",
|
|
146
|
+
items: cards.map((card) => ({
|
|
147
|
+
id: card.id || `card_${Date.now()}`,
|
|
148
|
+
title: card.title,
|
|
149
|
+
description: card.description,
|
|
150
|
+
media: card.mediaUrl,
|
|
151
|
+
})),
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const content = await twilioClient.content.v1.contents.create({
|
|
157
|
+
friendly_name: `RCS Carousel ${Date.now()}`,
|
|
158
|
+
types: contentData.types,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const message = await twilioClient.messages.create({
|
|
162
|
+
to,
|
|
163
|
+
messagingServiceSid: options.messagingServiceSid,
|
|
164
|
+
contentSid: content.sid,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return message;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
fastify.log.error("RCS sendCarousel failed:", error);
|
|
170
|
+
throw new Error("Failed to send RCS carousel.");
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Send an RCS message with suggested quick replies
|
|
176
|
+
* @param {string} to - Recipient phone number
|
|
177
|
+
* @param {string} body - Message content
|
|
178
|
+
* @param {Array<{text: string, payload: string}>} replies - Quick reply options
|
|
179
|
+
* @returns {Promise<object>} Message result
|
|
180
|
+
*/
|
|
181
|
+
sendQuickReplies: async (to, body, replies) => {
|
|
182
|
+
try {
|
|
183
|
+
const contentData = {
|
|
184
|
+
types: {
|
|
185
|
+
"twilio/quick-reply": {
|
|
186
|
+
body,
|
|
187
|
+
actions: replies.map((reply) => ({
|
|
188
|
+
type: "quick-reply",
|
|
189
|
+
title: reply.text,
|
|
190
|
+
id: reply.payload || reply.text,
|
|
191
|
+
})),
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const content = await twilioClient.content.v1.contents.create({
|
|
197
|
+
friendly_name: `RCS Quick Reply ${Date.now()}`,
|
|
198
|
+
types: contentData.types,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const message = await twilioClient.messages.create({
|
|
202
|
+
to,
|
|
203
|
+
messagingServiceSid: options.messagingServiceSid,
|
|
204
|
+
contentSid: content.sid,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return message;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
fastify.log.error("RCS sendQuickReplies failed:", error);
|
|
210
|
+
throw new Error("Failed to send RCS quick replies.");
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get RCS message status
|
|
216
|
+
* @param {string} messageSid - Message SID
|
|
217
|
+
* @returns {Promise<object>} Message status
|
|
218
|
+
*/
|
|
219
|
+
getStatus: async (messageSid) => {
|
|
220
|
+
try {
|
|
221
|
+
const message = await twilioClient.messages(messageSid).fetch();
|
|
222
|
+
return {
|
|
223
|
+
sid: message.sid,
|
|
224
|
+
status: message.status,
|
|
225
|
+
errorCode: message.errorCode,
|
|
226
|
+
errorMessage: message.errorMessage,
|
|
227
|
+
channel: message.channel,
|
|
228
|
+
};
|
|
229
|
+
} catch (error) {
|
|
230
|
+
fastify.log.error("RCS getStatus failed:", error);
|
|
231
|
+
throw new Error("Failed to get RCS message status.");
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* List RCS content templates
|
|
237
|
+
* @param {object} options - List options
|
|
238
|
+
* @returns {Promise<object[]>} List of content templates
|
|
239
|
+
*/
|
|
240
|
+
listTemplates: async (listOptions = {}) => {
|
|
241
|
+
try {
|
|
242
|
+
const contents = await twilioClient.content.v1.contents.list({
|
|
243
|
+
limit: listOptions.limit || 50,
|
|
244
|
+
});
|
|
245
|
+
return contents;
|
|
246
|
+
} catch (error) {
|
|
247
|
+
fastify.log.error("RCS listTemplates failed:", error);
|
|
248
|
+
throw new Error("Failed to list RCS templates.");
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get a content template by SID
|
|
254
|
+
* @param {string} contentSid - Content template SID
|
|
255
|
+
* @returns {Promise<object>} Content template
|
|
256
|
+
*/
|
|
257
|
+
getTemplate: async (contentSid) => {
|
|
258
|
+
try {
|
|
259
|
+
const content = await twilioClient.content.v1.contents(contentSid).fetch();
|
|
260
|
+
return content;
|
|
261
|
+
} catch (error) {
|
|
262
|
+
fastify.log.error("RCS getTemplate failed:", error);
|
|
263
|
+
throw new Error("Failed to get RCS template.");
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Delete a content template
|
|
269
|
+
* @param {string} contentSid - Content template SID
|
|
270
|
+
* @returns {Promise<boolean>} Success status
|
|
271
|
+
*/
|
|
272
|
+
deleteTemplate: async (contentSid) => {
|
|
273
|
+
try {
|
|
274
|
+
await twilioClient.content.v1.contents(contentSid).remove();
|
|
275
|
+
return true;
|
|
276
|
+
} catch (error) {
|
|
277
|
+
fastify.log.error("RCS deleteTemplate failed:", error);
|
|
278
|
+
throw new Error("Failed to delete RCS template.");
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
console.info(" ✅ RCS Service Enabled");
|
|
284
|
+
}
|