@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,268 @@
|
|
|
1
|
+
// src/services/sms.js
|
|
2
|
+
import Twilio from "twilio";
|
|
3
|
+
|
|
4
|
+
export async function setupSMS(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 SMS.");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!options.phoneNumber && !options.messagingServiceSid) {
|
|
13
|
+
throw new Error("Either phoneNumber or messagingServiceSid must be provided for SMS.");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Initialize Twilio client
|
|
17
|
+
const twilioClient = Twilio(options.accountSid, options.authToken);
|
|
18
|
+
|
|
19
|
+
fastify.decorate("sms", {
|
|
20
|
+
/**
|
|
21
|
+
* Send an SMS message
|
|
22
|
+
* @param {string} to - Recipient phone number (E.164 format recommended)
|
|
23
|
+
* @param {string} body - Message content
|
|
24
|
+
* @param {object} options - Optional parameters (statusCallback, etc.)
|
|
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
|
+
from: options.phoneNumber,
|
|
33
|
+
messagingServiceSid: options.messagingServiceSid,
|
|
34
|
+
...extraOptions,
|
|
35
|
+
});
|
|
36
|
+
return message;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
fastify.log.error("SMS send failed:", error);
|
|
39
|
+
throw new Error("Failed to send SMS.");
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Send an MMS 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
|
+
* @param {object} options - Optional parameters
|
|
49
|
+
* @returns {Promise<object>} Message result
|
|
50
|
+
*/
|
|
51
|
+
sendMMS: async (to, body, mediaUrl, extraOptions = {}) => {
|
|
52
|
+
try {
|
|
53
|
+
const message = await twilioClient.messages.create({
|
|
54
|
+
body,
|
|
55
|
+
to,
|
|
56
|
+
from: options.phoneNumber,
|
|
57
|
+
messagingServiceSid: options.messagingServiceSid,
|
|
58
|
+
mediaUrl: Array.isArray(mediaUrl) ? mediaUrl : [mediaUrl],
|
|
59
|
+
...extraOptions,
|
|
60
|
+
});
|
|
61
|
+
return message;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
fastify.log.error("MMS send failed:", error);
|
|
64
|
+
throw new Error("Failed to send MMS.");
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Schedule an SMS message (requires Messaging Service)
|
|
70
|
+
* @param {string} to - Recipient phone number
|
|
71
|
+
* @param {string} body - Message content
|
|
72
|
+
* @param {Date|string} sendAt - When to send the message (ISO 8601 format)
|
|
73
|
+
* @returns {Promise<object>} Message result
|
|
74
|
+
*/
|
|
75
|
+
schedule: async (to, body, sendAt) => {
|
|
76
|
+
try {
|
|
77
|
+
if (!options.messagingServiceSid) {
|
|
78
|
+
throw new Error("messagingServiceSid is required for scheduled messages.");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const message = await twilioClient.messages.create({
|
|
82
|
+
body,
|
|
83
|
+
to,
|
|
84
|
+
messagingServiceSid: options.messagingServiceSid,
|
|
85
|
+
scheduleType: "fixed",
|
|
86
|
+
sendAt: sendAt instanceof Date ? sendAt.toISOString() : sendAt,
|
|
87
|
+
});
|
|
88
|
+
return message;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
fastify.log.error("SMS schedule failed:", error);
|
|
91
|
+
throw new Error("Failed to schedule SMS.");
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Cancel a scheduled message
|
|
97
|
+
* @param {string} messageSid - Message SID
|
|
98
|
+
* @returns {Promise<object>} Updated message
|
|
99
|
+
*/
|
|
100
|
+
cancelScheduled: async (messageSid) => {
|
|
101
|
+
try {
|
|
102
|
+
const message = await twilioClient.messages(messageSid).update({
|
|
103
|
+
status: "canceled",
|
|
104
|
+
});
|
|
105
|
+
return message;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
fastify.log.error("SMS cancel scheduled failed:", error);
|
|
108
|
+
throw new Error("Failed to cancel scheduled SMS.");
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get message by SID
|
|
114
|
+
* @param {string} messageSid - Twilio message SID
|
|
115
|
+
* @returns {Promise<object>} Message object
|
|
116
|
+
*/
|
|
117
|
+
get: async (messageSid) => {
|
|
118
|
+
try {
|
|
119
|
+
const message = await twilioClient.messages(messageSid).fetch();
|
|
120
|
+
return message;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
fastify.log.error("SMS get failed:", error);
|
|
123
|
+
throw new Error("Failed to get SMS.");
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get message status
|
|
129
|
+
* @param {string} messageSid - Twilio message SID
|
|
130
|
+
* @returns {Promise<object>} Message status
|
|
131
|
+
*/
|
|
132
|
+
getStatus: async (messageSid) => {
|
|
133
|
+
try {
|
|
134
|
+
const message = await twilioClient.messages(messageSid).fetch();
|
|
135
|
+
return {
|
|
136
|
+
sid: message.sid,
|
|
137
|
+
status: message.status,
|
|
138
|
+
errorCode: message.errorCode,
|
|
139
|
+
errorMessage: message.errorMessage,
|
|
140
|
+
dateCreated: message.dateCreated,
|
|
141
|
+
dateUpdated: message.dateUpdated,
|
|
142
|
+
dateSent: message.dateSent,
|
|
143
|
+
};
|
|
144
|
+
} catch (error) {
|
|
145
|
+
fastify.log.error("SMS getStatus failed:", error);
|
|
146
|
+
throw new Error("Failed to get SMS status.");
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* List messages
|
|
152
|
+
* @param {object} filters - Filter options (dateSent, from, to, limit)
|
|
153
|
+
* @returns {Promise<object[]>} List of messages
|
|
154
|
+
*/
|
|
155
|
+
list: async (filters = {}) => {
|
|
156
|
+
try {
|
|
157
|
+
const messages = await twilioClient.messages.list({
|
|
158
|
+
from: filters.from,
|
|
159
|
+
to: filters.to,
|
|
160
|
+
dateSentAfter: filters.dateSentAfter,
|
|
161
|
+
dateSentBefore: filters.dateSentBefore,
|
|
162
|
+
limit: filters.limit || 50,
|
|
163
|
+
});
|
|
164
|
+
return messages;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
fastify.log.error("SMS list failed:", error);
|
|
167
|
+
throw new Error("Failed to list SMS messages.");
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Delete a message
|
|
173
|
+
* @param {string} messageSid - Message SID
|
|
174
|
+
* @returns {Promise<boolean>} Success status
|
|
175
|
+
*/
|
|
176
|
+
delete: async (messageSid) => {
|
|
177
|
+
try {
|
|
178
|
+
await twilioClient.messages(messageSid).remove();
|
|
179
|
+
return true;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
fastify.log.error("SMS delete failed:", error);
|
|
182
|
+
throw new Error("Failed to delete SMS.");
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get media from a message
|
|
188
|
+
* @param {string} messageSid - Message SID
|
|
189
|
+
* @returns {Promise<object[]>} List of media items
|
|
190
|
+
*/
|
|
191
|
+
getMedia: async (messageSid) => {
|
|
192
|
+
try {
|
|
193
|
+
const media = await twilioClient.messages(messageSid).media.list();
|
|
194
|
+
return media;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
fastify.log.error("SMS getMedia failed:", error);
|
|
197
|
+
throw new Error("Failed to get media.");
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Validate a phone number using Lookup API
|
|
203
|
+
* @param {string} phoneNumber - Phone number to validate
|
|
204
|
+
* @param {object} options - Lookup options (type, addOns)
|
|
205
|
+
* @returns {Promise<object>} Validation result
|
|
206
|
+
*/
|
|
207
|
+
validatePhoneNumber: async (phoneNumber, lookupOptions = {}) => {
|
|
208
|
+
try {
|
|
209
|
+
const validation = await twilioClient.lookups.v2
|
|
210
|
+
.phoneNumbers(phoneNumber)
|
|
211
|
+
.fetch(lookupOptions);
|
|
212
|
+
|
|
213
|
+
const isValid = validation.valid !== undefined ? validation.valid : true;
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
phoneNumber: validation.phoneNumber,
|
|
217
|
+
nationalFormat: validation.nationalFormat,
|
|
218
|
+
countryCode: validation.countryCode,
|
|
219
|
+
valid: isValid,
|
|
220
|
+
validation,
|
|
221
|
+
};
|
|
222
|
+
} catch (error) {
|
|
223
|
+
fastify.log.error("Phone number validation failed:", error);
|
|
224
|
+
return {
|
|
225
|
+
phoneNumber,
|
|
226
|
+
valid: false,
|
|
227
|
+
validation: {},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Send bulk SMS messages
|
|
234
|
+
* @param {Array<{to: string, body: string}>} messages - Array of messages to send
|
|
235
|
+
* @returns {Promise<object[]>} Array of message results
|
|
236
|
+
*/
|
|
237
|
+
sendBulk: async (messages) => {
|
|
238
|
+
try {
|
|
239
|
+
const promises = messages.map((msg) =>
|
|
240
|
+
twilioClient.messages.create({
|
|
241
|
+
body: msg.body,
|
|
242
|
+
to: msg.to,
|
|
243
|
+
from: options.phoneNumber,
|
|
244
|
+
messagingServiceSid: options.messagingServiceSid,
|
|
245
|
+
})
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const results = await Promise.allSettled(promises);
|
|
249
|
+
return results.map((result, index) => {
|
|
250
|
+
if (result.status === "fulfilled") {
|
|
251
|
+
return { success: true, message: result.value };
|
|
252
|
+
} else {
|
|
253
|
+
return {
|
|
254
|
+
success: false,
|
|
255
|
+
error: result.reason.message,
|
|
256
|
+
to: messages[index].to,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
} catch (error) {
|
|
261
|
+
fastify.log.error("SMS sendBulk failed:", error);
|
|
262
|
+
throw new Error("Failed to send bulk SMS.");
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
console.info(" ✅ SMS Service Enabled");
|
|
268
|
+
}
|
package/src/xTwilio.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/xTwilio.js
|
|
2
|
+
import fp from "fastify-plugin";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Services
|
|
6
|
+
*/
|
|
7
|
+
import { setupSMS } from "./services/sms.js";
|
|
8
|
+
import { setupConversations } from "./services/conversations.js";
|
|
9
|
+
import { setupRCS } from "./services/rcs.js";
|
|
10
|
+
import { setupEmail } from "./services/email.js";
|
|
11
|
+
|
|
12
|
+
async function xTwilio(fastify, options) {
|
|
13
|
+
const {
|
|
14
|
+
twilio: twilioOptions = {},
|
|
15
|
+
sendgrid: sendgridOptions = {},
|
|
16
|
+
} = options;
|
|
17
|
+
|
|
18
|
+
console.info("\n 📱 Starting xTwilio...\n");
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* Twilio Services
|
|
22
|
+
*/
|
|
23
|
+
await setupSMS(fastify, twilioOptions);
|
|
24
|
+
await setupConversations(fastify, twilioOptions);
|
|
25
|
+
await setupRCS(fastify, twilioOptions);
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* SendGrid Email
|
|
29
|
+
*/
|
|
30
|
+
await setupEmail(fastify, sendgridOptions);
|
|
31
|
+
|
|
32
|
+
console.info("\n 📱 xTwilio Ready!\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default fp(xTwilio, {
|
|
36
|
+
name: "xTwilio",
|
|
37
|
+
});
|