@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.
Files changed (89) hide show
  1. package/LICENSE +191 -0
  2. package/dist/adapters/bluebubbles.d.ts +63 -0
  3. package/dist/adapters/bluebubbles.d.ts.map +1 -0
  4. package/dist/adapters/bluebubbles.js +197 -0
  5. package/dist/adapters/bluebubbles.js.map +1 -0
  6. package/dist/adapters/discord.d.ts +27 -0
  7. package/dist/adapters/discord.d.ts.map +1 -0
  8. package/dist/adapters/discord.js +202 -0
  9. package/dist/adapters/discord.js.map +1 -0
  10. package/dist/adapters/email.d.ts +39 -0
  11. package/dist/adapters/email.d.ts.map +1 -0
  12. package/dist/adapters/email.js +359 -0
  13. package/dist/adapters/email.js.map +1 -0
  14. package/dist/adapters/googlechat.d.ts +77 -0
  15. package/dist/adapters/googlechat.d.ts.map +1 -0
  16. package/dist/adapters/googlechat.js +232 -0
  17. package/dist/adapters/googlechat.js.map +1 -0
  18. package/dist/adapters/matrix.d.ts +37 -0
  19. package/dist/adapters/matrix.d.ts.map +1 -0
  20. package/dist/adapters/matrix.js +262 -0
  21. package/dist/adapters/matrix.js.map +1 -0
  22. package/dist/adapters/signal.d.ts +32 -0
  23. package/dist/adapters/signal.d.ts.map +1 -0
  24. package/dist/adapters/signal.js +216 -0
  25. package/dist/adapters/signal.js.map +1 -0
  26. package/dist/adapters/slack.d.ts +29 -0
  27. package/dist/adapters/slack.d.ts.map +1 -0
  28. package/dist/adapters/slack.js +202 -0
  29. package/dist/adapters/slack.js.map +1 -0
  30. package/dist/adapters/teams.d.ts +66 -0
  31. package/dist/adapters/teams.d.ts.map +1 -0
  32. package/dist/adapters/teams.js +227 -0
  33. package/dist/adapters/teams.js.map +1 -0
  34. package/dist/adapters/telegram.d.ts +28 -0
  35. package/dist/adapters/telegram.d.ts.map +1 -0
  36. package/dist/adapters/telegram.js +170 -0
  37. package/dist/adapters/telegram.js.map +1 -0
  38. package/dist/adapters/twilio.d.ts +63 -0
  39. package/dist/adapters/twilio.d.ts.map +1 -0
  40. package/dist/adapters/twilio.js +193 -0
  41. package/dist/adapters/twilio.js.map +1 -0
  42. package/dist/adapters/whatsapp.d.ts +99 -0
  43. package/dist/adapters/whatsapp.d.ts.map +1 -0
  44. package/dist/adapters/whatsapp.js +218 -0
  45. package/dist/adapters/whatsapp.js.map +1 -0
  46. package/dist/adapters/zalo.d.ts +64 -0
  47. package/dist/adapters/zalo.d.ts.map +1 -0
  48. package/dist/adapters/zalo.js +216 -0
  49. package/dist/adapters/zalo.js.map +1 -0
  50. package/dist/index.d.ts +15 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +16 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/manager.d.ts +35 -0
  55. package/dist/manager.d.ts.map +1 -0
  56. package/dist/manager.js +127 -0
  57. package/dist/manager.js.map +1 -0
  58. package/dist/types.d.ts +71 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +2 -0
  61. package/dist/types.js.map +1 -0
  62. package/package.json +32 -0
  63. package/src/adapters/bluebubbles.ts +294 -0
  64. package/src/adapters/discord.ts +253 -0
  65. package/src/adapters/email.ts +457 -0
  66. package/src/adapters/googlechat.ts +364 -0
  67. package/src/adapters/matrix.ts +376 -0
  68. package/src/adapters/signal.ts +313 -0
  69. package/src/adapters/slack.ts +252 -0
  70. package/src/adapters/teams.ts +320 -0
  71. package/src/adapters/telegram.ts +208 -0
  72. package/src/adapters/twilio.ts +256 -0
  73. package/src/adapters/whatsapp.ts +342 -0
  74. package/src/adapters/zalo.ts +319 -0
  75. package/src/index.ts +78 -0
  76. package/src/manager.ts +180 -0
  77. package/src/types.ts +84 -0
  78. package/tests/bluebubbles.test.ts +438 -0
  79. package/tests/email.test.ts +136 -0
  80. package/tests/googlechat.test.ts +439 -0
  81. package/tests/matrix.test.ts +564 -0
  82. package/tests/signal.test.ts +404 -0
  83. package/tests/slack.test.ts +343 -0
  84. package/tests/teams.test.ts +429 -0
  85. package/tests/twilio.test.ts +269 -0
  86. package/tests/whatsapp.test.ts +530 -0
  87. package/tests/zalo.test.ts +499 -0
  88. package/tsconfig.json +8 -0
  89. 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, '&amp;')
243
+ .replace(/</g, '&lt;')
244
+ .replace(/>/g, '&gt;')
245
+ .replace(/"/g, '&quot;')
246
+ .replace(/'/g, '&apos;');
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
+ }