@auxiora/channels 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/LICENSE +191 -0
- package/dist/adapters/bluebubbles.d.ts +63 -0
- package/dist/adapters/bluebubbles.d.ts.map +1 -0
- package/dist/adapters/bluebubbles.js +197 -0
- package/dist/adapters/bluebubbles.js.map +1 -0
- package/dist/adapters/discord.d.ts +27 -0
- package/dist/adapters/discord.d.ts.map +1 -0
- package/dist/adapters/discord.js +202 -0
- package/dist/adapters/discord.js.map +1 -0
- package/dist/adapters/email.d.ts +39 -0
- package/dist/adapters/email.d.ts.map +1 -0
- package/dist/adapters/email.js +359 -0
- package/dist/adapters/email.js.map +1 -0
- package/dist/adapters/googlechat.d.ts +77 -0
- package/dist/adapters/googlechat.d.ts.map +1 -0
- package/dist/adapters/googlechat.js +232 -0
- package/dist/adapters/googlechat.js.map +1 -0
- package/dist/adapters/matrix.d.ts +37 -0
- package/dist/adapters/matrix.d.ts.map +1 -0
- package/dist/adapters/matrix.js +262 -0
- package/dist/adapters/matrix.js.map +1 -0
- package/dist/adapters/signal.d.ts +32 -0
- package/dist/adapters/signal.d.ts.map +1 -0
- package/dist/adapters/signal.js +216 -0
- package/dist/adapters/signal.js.map +1 -0
- package/dist/adapters/slack.d.ts +29 -0
- package/dist/adapters/slack.d.ts.map +1 -0
- package/dist/adapters/slack.js +202 -0
- package/dist/adapters/slack.js.map +1 -0
- package/dist/adapters/teams.d.ts +66 -0
- package/dist/adapters/teams.d.ts.map +1 -0
- package/dist/adapters/teams.js +227 -0
- package/dist/adapters/teams.js.map +1 -0
- package/dist/adapters/telegram.d.ts +28 -0
- package/dist/adapters/telegram.d.ts.map +1 -0
- package/dist/adapters/telegram.js +170 -0
- package/dist/adapters/telegram.js.map +1 -0
- package/dist/adapters/twilio.d.ts +63 -0
- package/dist/adapters/twilio.d.ts.map +1 -0
- package/dist/adapters/twilio.js +193 -0
- package/dist/adapters/twilio.js.map +1 -0
- package/dist/adapters/whatsapp.d.ts +99 -0
- package/dist/adapters/whatsapp.d.ts.map +1 -0
- package/dist/adapters/whatsapp.js +218 -0
- package/dist/adapters/whatsapp.js.map +1 -0
- package/dist/adapters/zalo.d.ts +64 -0
- package/dist/adapters/zalo.d.ts.map +1 -0
- package/dist/adapters/zalo.js +216 -0
- package/dist/adapters/zalo.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/manager.d.ts +35 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/manager.js +127 -0
- package/dist/manager.js.map +1 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +32 -0
- package/src/adapters/bluebubbles.ts +294 -0
- package/src/adapters/discord.ts +253 -0
- package/src/adapters/email.ts +457 -0
- package/src/adapters/googlechat.ts +364 -0
- package/src/adapters/matrix.ts +376 -0
- package/src/adapters/signal.ts +313 -0
- package/src/adapters/slack.ts +252 -0
- package/src/adapters/teams.ts +320 -0
- package/src/adapters/telegram.ts +208 -0
- package/src/adapters/twilio.ts +256 -0
- package/src/adapters/whatsapp.ts +342 -0
- package/src/adapters/zalo.ts +319 -0
- package/src/index.ts +78 -0
- package/src/manager.ts +180 -0
- package/src/types.ts +84 -0
- package/tests/bluebubbles.test.ts +438 -0
- package/tests/email.test.ts +136 -0
- package/tests/googlechat.test.ts +439 -0
- package/tests/matrix.test.ts +564 -0
- package/tests/signal.test.ts +404 -0
- package/tests/slack.test.ts +343 -0
- package/tests/teams.test.ts +429 -0
- package/tests/twilio.test.ts +269 -0
- package/tests/whatsapp.test.ts +530 -0
- package/tests/zalo.test.ts +499 -0
- package/tsconfig.json +8 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { Bot, Context } from 'grammy';
|
|
2
|
+
import { audit } from '@auxiora/audit';
|
|
3
|
+
import type {
|
|
4
|
+
ChannelAdapter,
|
|
5
|
+
InboundMessage,
|
|
6
|
+
OutboundMessage,
|
|
7
|
+
SendResult,
|
|
8
|
+
} from '../types.js';
|
|
9
|
+
|
|
10
|
+
export interface TelegramAdapterConfig {
|
|
11
|
+
token: string;
|
|
12
|
+
webhookUrl?: string;
|
|
13
|
+
allowedChats?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const MAX_MESSAGE_LENGTH = 4096;
|
|
17
|
+
|
|
18
|
+
export class TelegramAdapter implements ChannelAdapter {
|
|
19
|
+
readonly type = 'telegram' as const;
|
|
20
|
+
readonly name = 'Telegram';
|
|
21
|
+
|
|
22
|
+
private bot: Bot;
|
|
23
|
+
private config: TelegramAdapterConfig;
|
|
24
|
+
private messageHandler?: (message: InboundMessage) => Promise<void>;
|
|
25
|
+
private errorHandler?: (error: Error) => void;
|
|
26
|
+
private connected = false;
|
|
27
|
+
|
|
28
|
+
constructor(config: TelegramAdapterConfig) {
|
|
29
|
+
this.config = config;
|
|
30
|
+
this.bot = new Bot(config.token);
|
|
31
|
+
this.setupEventHandlers();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private setupEventHandlers(): void {
|
|
35
|
+
this.bot.on('message:text', async (ctx: Context) => {
|
|
36
|
+
const message = ctx.message;
|
|
37
|
+
if (!message || !message.text) return;
|
|
38
|
+
|
|
39
|
+
// Check chat allowlist
|
|
40
|
+
if (
|
|
41
|
+
this.config.allowedChats?.length &&
|
|
42
|
+
!this.config.allowedChats.includes(String(message.chat.id))
|
|
43
|
+
) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const inbound = this.toInboundMessage(ctx);
|
|
48
|
+
|
|
49
|
+
audit('message.received', {
|
|
50
|
+
channelType: 'telegram',
|
|
51
|
+
senderId: inbound.senderId,
|
|
52
|
+
channelId: inbound.channelId,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (this.messageHandler) {
|
|
56
|
+
try {
|
|
57
|
+
await this.messageHandler(inbound);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
this.errorHandler?.(error instanceof Error ? error : new Error(String(error)));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
this.bot.catch((error) => {
|
|
65
|
+
audit('channel.error', { channelType: 'telegram', error: error.message });
|
|
66
|
+
this.errorHandler?.(error);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private toInboundMessage(ctx: Context): InboundMessage {
|
|
71
|
+
const message = ctx.message!;
|
|
72
|
+
const from = message.from!;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
id: String(message.message_id),
|
|
76
|
+
channelType: 'telegram',
|
|
77
|
+
channelId: String(message.chat.id),
|
|
78
|
+
senderId: String(from.id),
|
|
79
|
+
senderName: from.first_name + (from.last_name ? ` ${from.last_name}` : ''),
|
|
80
|
+
content: message.text || '',
|
|
81
|
+
timestamp: message.date * 1000,
|
|
82
|
+
replyToId: message.reply_to_message
|
|
83
|
+
? String(message.reply_to_message.message_id)
|
|
84
|
+
: undefined,
|
|
85
|
+
raw: message,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async connect(): Promise<void> {
|
|
90
|
+
if (this.config.webhookUrl) {
|
|
91
|
+
await this.bot.api.setWebhook(this.config.webhookUrl);
|
|
92
|
+
} else {
|
|
93
|
+
// Start polling
|
|
94
|
+
this.bot.start({
|
|
95
|
+
onStart: () => {
|
|
96
|
+
audit('channel.connected', { channelType: 'telegram' });
|
|
97
|
+
this.connected = true;
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
this.connected = true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async disconnect(): Promise<void> {
|
|
105
|
+
await this.bot.stop();
|
|
106
|
+
this.connected = false;
|
|
107
|
+
audit('channel.disconnected', { channelType: 'telegram' });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
isConnected(): boolean {
|
|
111
|
+
return this.connected;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async send(channelId: string, message: OutboundMessage): Promise<SendResult> {
|
|
115
|
+
try {
|
|
116
|
+
const chatId = parseInt(channelId, 10);
|
|
117
|
+
|
|
118
|
+
// Chunk long messages
|
|
119
|
+
const chunks = this.chunkMessage(message.content);
|
|
120
|
+
let lastMessageId: number | undefined;
|
|
121
|
+
|
|
122
|
+
for (const chunk of chunks) {
|
|
123
|
+
const sent = await this.bot.api.sendMessage(chatId, chunk, {
|
|
124
|
+
parse_mode: message.formatting?.markdown ? 'MarkdownV2' : undefined,
|
|
125
|
+
reply_parameters: message.replyToId
|
|
126
|
+
? { message_id: parseInt(message.replyToId, 10) }
|
|
127
|
+
: undefined,
|
|
128
|
+
});
|
|
129
|
+
lastMessageId = sent.message_id;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
audit('message.sent', {
|
|
133
|
+
channelType: 'telegram',
|
|
134
|
+
channelId,
|
|
135
|
+
messageId: lastMessageId,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return { success: true, messageId: String(lastMessageId) };
|
|
139
|
+
} catch (error) {
|
|
140
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
141
|
+
audit('channel.error', {
|
|
142
|
+
channelType: 'telegram',
|
|
143
|
+
action: 'send',
|
|
144
|
+
error: errorMessage,
|
|
145
|
+
});
|
|
146
|
+
return { success: false, error: errorMessage };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private chunkMessage(content: string): string[] {
|
|
151
|
+
if (content.length <= MAX_MESSAGE_LENGTH) {
|
|
152
|
+
return [content];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const chunks: string[] = [];
|
|
156
|
+
let remaining = content;
|
|
157
|
+
|
|
158
|
+
while (remaining.length > 0) {
|
|
159
|
+
if (remaining.length <= MAX_MESSAGE_LENGTH) {
|
|
160
|
+
chunks.push(remaining);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let breakPoint = remaining.lastIndexOf('\n', MAX_MESSAGE_LENGTH);
|
|
165
|
+
if (breakPoint === -1 || breakPoint < MAX_MESSAGE_LENGTH / 2) {
|
|
166
|
+
breakPoint = remaining.lastIndexOf(' ', MAX_MESSAGE_LENGTH);
|
|
167
|
+
}
|
|
168
|
+
if (breakPoint === -1 || breakPoint < MAX_MESSAGE_LENGTH / 2) {
|
|
169
|
+
breakPoint = MAX_MESSAGE_LENGTH;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
chunks.push(remaining.slice(0, breakPoint));
|
|
173
|
+
remaining = remaining.slice(breakPoint).trimStart();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return chunks;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async startTyping(channelId: string): Promise<() => void> {
|
|
180
|
+
const chatId = parseInt(channelId, 10);
|
|
181
|
+
// Send immediately, then repeat every 4s (Telegram typing expires after ~5s)
|
|
182
|
+
let stopped = false;
|
|
183
|
+
this.bot.api.sendChatAction(chatId, 'typing').catch((e: Error) => {
|
|
184
|
+
audit('channel.error', { channelType: 'telegram', action: 'typing', error: e.message });
|
|
185
|
+
});
|
|
186
|
+
const interval = setInterval(() => {
|
|
187
|
+
if (stopped) return;
|
|
188
|
+
this.bot.api.sendChatAction(chatId, 'typing').catch(() => {});
|
|
189
|
+
}, 4000);
|
|
190
|
+
return () => {
|
|
191
|
+
stopped = true;
|
|
192
|
+
clearInterval(interval);
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Webhook handler for serverless deployments
|
|
197
|
+
async handleWebhook(body: unknown): Promise<void> {
|
|
198
|
+
await this.bot.handleUpdate(body as Parameters<typeof this.bot.handleUpdate>[0]);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
onMessage(handler: (message: InboundMessage) => Promise<void>): void {
|
|
202
|
+
this.messageHandler = handler;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
onError(handler: (error: Error) => void): void {
|
|
206
|
+
this.errorHandler = handler;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import Twilio from 'twilio';
|
|
2
|
+
import { audit } from '@auxiora/audit';
|
|
3
|
+
import type {
|
|
4
|
+
ChannelAdapter,
|
|
5
|
+
InboundMessage,
|
|
6
|
+
OutboundMessage,
|
|
7
|
+
SendResult,
|
|
8
|
+
} from '../types.js';
|
|
9
|
+
|
|
10
|
+
export interface TwilioAdapterConfig {
|
|
11
|
+
accountSid: string;
|
|
12
|
+
authToken: string;
|
|
13
|
+
phoneNumber: string; // Your Twilio phone number for SMS
|
|
14
|
+
whatsappNumber?: string; // WhatsApp-enabled number (format: whatsapp:+1234567890)
|
|
15
|
+
webhookUrl?: string; // For incoming messages
|
|
16
|
+
allowedNumbers?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TwilioWebhookBody {
|
|
20
|
+
MessageSid: string;
|
|
21
|
+
AccountSid: string;
|
|
22
|
+
From: string;
|
|
23
|
+
To: string;
|
|
24
|
+
Body: string;
|
|
25
|
+
NumMedia?: string;
|
|
26
|
+
MediaUrl0?: string;
|
|
27
|
+
MediaContentType0?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const MAX_SMS_LENGTH = 1600; // SMS segment limit
|
|
31
|
+
const MAX_WHATSAPP_LENGTH = 4096;
|
|
32
|
+
|
|
33
|
+
export class TwilioAdapter implements ChannelAdapter {
|
|
34
|
+
readonly type = 'twilio' as const;
|
|
35
|
+
readonly name = 'Twilio';
|
|
36
|
+
|
|
37
|
+
private client: Twilio.Twilio;
|
|
38
|
+
private config: TwilioAdapterConfig;
|
|
39
|
+
private messageHandler?: (message: InboundMessage) => Promise<void>;
|
|
40
|
+
private errorHandler?: (error: Error) => void;
|
|
41
|
+
private connected = false;
|
|
42
|
+
|
|
43
|
+
constructor(config: TwilioAdapterConfig) {
|
|
44
|
+
this.config = config;
|
|
45
|
+
this.client = Twilio(config.accountSid, config.authToken);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async connect(): Promise<void> {
|
|
49
|
+
// Verify credentials by fetching account info
|
|
50
|
+
try {
|
|
51
|
+
await this.client.api.accounts(this.config.accountSid).fetch();
|
|
52
|
+
this.connected = true;
|
|
53
|
+
audit('channel.connected', { channelType: 'twilio' });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new Error(`Failed to connect to Twilio: ${error instanceof Error ? error.message : error}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async disconnect(): Promise<void> {
|
|
60
|
+
this.connected = false;
|
|
61
|
+
audit('channel.disconnected', { channelType: 'twilio' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
isConnected(): boolean {
|
|
65
|
+
return this.connected;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Handle incoming webhook from Twilio
|
|
70
|
+
* Call this from your HTTP server when receiving POST to your webhook URL
|
|
71
|
+
*/
|
|
72
|
+
async handleWebhook(body: TwilioWebhookBody): Promise<string | null> {
|
|
73
|
+
const isWhatsApp = body.From.startsWith('whatsapp:');
|
|
74
|
+
|
|
75
|
+
// Check allowed numbers (strip whatsapp: prefix for matching)
|
|
76
|
+
const senderNumber = isWhatsApp ? body.From.replace('whatsapp:', '') : body.From;
|
|
77
|
+
if (this.config.allowedNumbers?.length && !this.config.allowedNumbers.includes(senderNumber)) {
|
|
78
|
+
audit('message.filtered', { channelType: 'twilio', senderId: body.From, reason: 'number_not_allowed' });
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const inbound: InboundMessage = {
|
|
83
|
+
id: body.MessageSid,
|
|
84
|
+
channelType: 'twilio',
|
|
85
|
+
channelId: isWhatsApp ? 'whatsapp' : 'sms',
|
|
86
|
+
senderId: body.From,
|
|
87
|
+
content: body.Body,
|
|
88
|
+
timestamp: Date.now(),
|
|
89
|
+
attachments: body.NumMedia && parseInt(body.NumMedia, 10) > 0
|
|
90
|
+
? [{
|
|
91
|
+
type: body.MediaContentType0?.startsWith('image/') ? 'image' : 'file',
|
|
92
|
+
url: body.MediaUrl0,
|
|
93
|
+
mimeType: body.MediaContentType0,
|
|
94
|
+
}]
|
|
95
|
+
: undefined,
|
|
96
|
+
raw: body,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
audit('message.received', {
|
|
100
|
+
channelType: 'twilio',
|
|
101
|
+
senderId: inbound.senderId,
|
|
102
|
+
channelId: inbound.channelId,
|
|
103
|
+
isWhatsApp,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (this.messageHandler) {
|
|
107
|
+
try {
|
|
108
|
+
await this.messageHandler(inbound);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
this.errorHandler?.(error instanceof Error ? error : new Error(String(error)));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Return null to indicate no TwiML response needed
|
|
115
|
+
// Or return TwiML XML string if you want to respond immediately
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Send message to a phone number
|
|
121
|
+
* @param channelId Phone number in E.164 format (+1234567890) or whatsapp:+1234567890
|
|
122
|
+
*/
|
|
123
|
+
async send(channelId: string, message: OutboundMessage): Promise<SendResult> {
|
|
124
|
+
try {
|
|
125
|
+
const isWhatsApp = channelId.startsWith('whatsapp:');
|
|
126
|
+
const fromNumber = isWhatsApp
|
|
127
|
+
? this.config.whatsappNumber || `whatsapp:${this.config.phoneNumber}`
|
|
128
|
+
: this.config.phoneNumber;
|
|
129
|
+
|
|
130
|
+
if (!fromNumber) {
|
|
131
|
+
return { success: false, error: 'No phone number configured for this channel type' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const maxLength = isWhatsApp ? MAX_WHATSAPP_LENGTH : MAX_SMS_LENGTH;
|
|
135
|
+
const chunks = this.chunkMessage(message.content, maxLength);
|
|
136
|
+
let lastMessageSid: string | undefined;
|
|
137
|
+
|
|
138
|
+
for (const chunk of chunks) {
|
|
139
|
+
const sent = await this.client.messages.create({
|
|
140
|
+
from: fromNumber,
|
|
141
|
+
to: channelId,
|
|
142
|
+
body: chunk,
|
|
143
|
+
});
|
|
144
|
+
lastMessageSid = sent.sid;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
audit('message.sent', {
|
|
148
|
+
channelType: 'twilio',
|
|
149
|
+
channelId,
|
|
150
|
+
messageId: lastMessageSid,
|
|
151
|
+
isWhatsApp,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return { success: true, messageId: lastMessageSid };
|
|
155
|
+
} catch (error) {
|
|
156
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
157
|
+
audit('channel.error', {
|
|
158
|
+
channelType: 'twilio',
|
|
159
|
+
action: 'send',
|
|
160
|
+
error: errorMessage,
|
|
161
|
+
});
|
|
162
|
+
return { success: false, error: errorMessage };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Send SMS to a phone number
|
|
168
|
+
*/
|
|
169
|
+
async sendSMS(to: string, message: string): Promise<SendResult> {
|
|
170
|
+
// Ensure proper format
|
|
171
|
+
const phoneNumber = to.startsWith('+') ? to : `+${to}`;
|
|
172
|
+
return this.send(phoneNumber, { content: message });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Send WhatsApp message
|
|
177
|
+
*/
|
|
178
|
+
async sendWhatsApp(to: string, message: string): Promise<SendResult> {
|
|
179
|
+
// Ensure proper format
|
|
180
|
+
const phoneNumber = to.replace('whatsapp:', '');
|
|
181
|
+
const whatsappTo = `whatsapp:${phoneNumber.startsWith('+') ? phoneNumber : `+${phoneNumber}`}`;
|
|
182
|
+
return this.send(whatsappTo, { content: message });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private chunkMessage(content: string, maxLength: number): string[] {
|
|
186
|
+
if (content.length <= maxLength) {
|
|
187
|
+
return [content];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const chunks: string[] = [];
|
|
191
|
+
let remaining = content;
|
|
192
|
+
|
|
193
|
+
while (remaining.length > 0) {
|
|
194
|
+
if (remaining.length <= maxLength) {
|
|
195
|
+
chunks.push(remaining);
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let breakPoint = remaining.lastIndexOf('\n', maxLength);
|
|
200
|
+
if (breakPoint === -1 || breakPoint < maxLength / 2) {
|
|
201
|
+
breakPoint = remaining.lastIndexOf(' ', maxLength);
|
|
202
|
+
}
|
|
203
|
+
if (breakPoint === -1 || breakPoint < maxLength / 2) {
|
|
204
|
+
breakPoint = maxLength;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
chunks.push(remaining.slice(0, breakPoint));
|
|
208
|
+
remaining = remaining.slice(breakPoint).trimStart();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return chunks;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Validate Twilio webhook signature
|
|
216
|
+
*/
|
|
217
|
+
validateWebhookSignature(
|
|
218
|
+
signature: string,
|
|
219
|
+
url: string,
|
|
220
|
+
params: Record<string, string>
|
|
221
|
+
): boolean {
|
|
222
|
+
return Twilio.validateRequest(
|
|
223
|
+
this.config.authToken,
|
|
224
|
+
signature,
|
|
225
|
+
url,
|
|
226
|
+
params
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Generate TwiML response for immediate reply
|
|
232
|
+
*/
|
|
233
|
+
static generateTwiML(message: string): string {
|
|
234
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
235
|
+
<Response>
|
|
236
|
+
<Message>${this.escapeXml(message)}</Message>
|
|
237
|
+
</Response>`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private static escapeXml(text: string): string {
|
|
241
|
+
return text
|
|
242
|
+
.replace(/&/g, '&')
|
|
243
|
+
.replace(/</g, '<')
|
|
244
|
+
.replace(/>/g, '>')
|
|
245
|
+
.replace(/"/g, '"')
|
|
246
|
+
.replace(/'/g, ''');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
onMessage(handler: (message: InboundMessage) => Promise<void>): void {
|
|
250
|
+
this.messageHandler = handler;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
onError(handler: (error: Error) => void): void {
|
|
254
|
+
this.errorHandler = handler;
|
|
255
|
+
}
|
|
256
|
+
}
|