@peopl-health/nexus 1.0.2

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.
@@ -0,0 +1,180 @@
1
+ const { MessageProvider } = require('../core/MessageProvider');
2
+
3
+ /**
4
+ * Baileys WhatsApp messaging provider
5
+ */
6
+ class BaileysProvider extends MessageProvider {
7
+ constructor(config) {
8
+ super(config);
9
+ this.waSocket = null;
10
+ this.authState = config.authState || 'auth_info_baileys';
11
+ this.mongoUri = config.mongoUri;
12
+ this.userDbMongo = config.userDbMongo;
13
+ }
14
+
15
+ async initialize() {
16
+ try {
17
+ const { default: makeWASocket, useMultiFileAuthState } = require('baileys');
18
+ const { useMongoDBAuthState } = require('../utils/mongoAuthConfig');
19
+ const pino = require('pino');
20
+
21
+ const logger = pino({ level: 'warn' });
22
+ let state, saveCreds;
23
+
24
+ if (this.mongoUri && this.userDbMongo) {
25
+ ({ state, saveCreds } = await useMongoDBAuthState(
26
+ this.mongoUri,
27
+ this.userDbMongo,
28
+ 'baileys'
29
+ ));
30
+ } else {
31
+ ({ state, saveCreds } = await useMultiFileAuthState(this.authState));
32
+ }
33
+
34
+ this.waSocket = makeWASocket({
35
+ auth: state,
36
+ keepAliveIntervalMs: 30000,
37
+ logger,
38
+ printQRInTerminal: true,
39
+ });
40
+
41
+ this.waSocket.ev.on('connection.update', async (update) => {
42
+ await this.handleConnectionUpdate(update);
43
+ });
44
+
45
+ this.waSocket.ev.on('creds.update', saveCreds);
46
+
47
+ } catch (error) {
48
+ throw new Error(`Failed to initialize Baileys: ${error.message}`);
49
+ }
50
+ }
51
+
52
+ async handleConnectionUpdate(update) {
53
+ const { connection, lastDisconnect, qr } = update || {};
54
+
55
+ if (qr) {
56
+ // Emit QR code event for consumer to handle
57
+ this.emit?.('qr', qr);
58
+ }
59
+
60
+ if (connection === 'close' && lastDisconnect) {
61
+ const { DisconnectReason } = require('baileys');
62
+ const shouldReconnect = lastDisconnect?.error?.output?.statusCode !== DisconnectReason.loggedOut;
63
+ if (shouldReconnect) {
64
+ await this.initialize();
65
+ }
66
+ } else if (connection === 'open') {
67
+ this.isConnected = true;
68
+ }
69
+ }
70
+
71
+ formatCode(code) {
72
+ return code.includes('@s.whatsapp.net') ? code : `${code}@s.whatsapp.net`;
73
+ }
74
+
75
+ formatMessage(message) {
76
+ return message.replace(/\\n/g, '\n');
77
+ }
78
+
79
+ async sendMessage(messageData) {
80
+ if (!this.isConnected || !this.waSocket) {
81
+ throw new Error('Baileys provider not connected');
82
+ }
83
+
84
+ const { to, message, fileUrl, fileType, hidePreview = false } = messageData;
85
+
86
+ if (!to) {
87
+ throw new Error('Recipient is required');
88
+ }
89
+
90
+ if (!message && !fileUrl) {
91
+ throw new Error('Message or file URL is required');
92
+ }
93
+
94
+ const code = this.formatCode(to);
95
+ let sendOptions = {};
96
+ let formattedMessage = this.formatMessage(message || '');
97
+
98
+ // Handle mentions
99
+ const regex = /@\d+/g;
100
+ const matches = formattedMessage.match(regex);
101
+ if (matches) {
102
+ const processedNumbers = matches.map(match => {
103
+ return match.replace('@', '') + '@s.whatsapp.net';
104
+ });
105
+ sendOptions.mentions = processedNumbers;
106
+ }
107
+
108
+ // Handle link preview
109
+ const keywords = ['meet.google.com', 'airtable', 'zoom'];
110
+ if (keywords.some(keyword => formattedMessage.includes(keyword)) || hidePreview) {
111
+ sendOptions.linkPreview = false;
112
+ }
113
+
114
+ // Set message content based on type
115
+ if (!fileUrl || fileType === 'text') {
116
+ sendOptions.text = formattedMessage;
117
+ } else {
118
+ if (fileType === 'image') {
119
+ sendOptions.image = { url: fileUrl };
120
+ sendOptions.mimetype = 'image/png';
121
+ sendOptions.caption = formattedMessage;
122
+ } else if (fileType === 'document') {
123
+ sendOptions.document = { url: fileUrl };
124
+ sendOptions.mimetype = 'application/pdf';
125
+ sendOptions.caption = formattedMessage;
126
+ } else if (fileType === 'audio') {
127
+ sendOptions.audio = { url: fileUrl };
128
+ sendOptions.mimetype = 'audio/mpeg';
129
+ } else {
130
+ throw new Error(`Unsupported file type: ${fileType}`);
131
+ }
132
+ }
133
+
134
+ try {
135
+ const result = await this.waSocket.sendMessage(code, sendOptions);
136
+ return {
137
+ success: true,
138
+ messageId: result?.key?.id,
139
+ provider: 'baileys',
140
+ result
141
+ };
142
+ } catch (error) {
143
+ throw new Error(`Baileys send failed: ${error.message}`);
144
+ }
145
+ }
146
+
147
+ async sendScheduledMessage(scheduledMessage) {
148
+ const { sendTime, timeZone } = scheduledMessage;
149
+ const delay = this.calculateDelay(sendTime, timeZone);
150
+
151
+ setTimeout(async () => {
152
+ try {
153
+ await this.sendMessage(scheduledMessage);
154
+ console.log('Scheduled message sent successfully');
155
+ } catch (error) {
156
+ console.error(`Scheduled message failed: ${error.message}`);
157
+ }
158
+ }, delay);
159
+ }
160
+
161
+ calculateDelay(sendTime) {
162
+ const now = new Date();
163
+ const targetTime = new Date(sendTime);
164
+ return Math.max(0, targetTime.getTime() - now.getTime());
165
+ }
166
+
167
+ getConnectionStatus() {
168
+ return this.waSocket?.user ? true : false;
169
+ }
170
+
171
+ async disconnect() {
172
+ if (this.waSocket) {
173
+ await this.waSocket.logout();
174
+ this.waSocket = null;
175
+ }
176
+ this.isConnected = false;
177
+ }
178
+ }
179
+
180
+ module.exports = { BaileysProvider };
@@ -0,0 +1,118 @@
1
+ const { MessageProvider } = require('../core/MessageProvider');
2
+
3
+ /**
4
+ * Twilio WhatsApp messaging provider
5
+ */
6
+ class TwilioProvider extends MessageProvider {
7
+ constructor(config) {
8
+ super(config);
9
+ this.twilioClient = null;
10
+ this.accountSid = config.accountSid;
11
+ this.authToken = config.authToken;
12
+ this.whatsappNumber = config.whatsappNumber;
13
+ }
14
+
15
+ async initialize() {
16
+ if (!this.accountSid || !this.authToken) {
17
+ throw new Error('Twilio credentials (accountSid, authToken) are required');
18
+ }
19
+
20
+ try {
21
+ const twilio = require('twilio');
22
+ this.twilioClient = twilio(this.accountSid, this.authToken);
23
+ this.isConnected = true;
24
+ } catch (error) {
25
+ throw new Error(`Failed to initialize Twilio: ${error.message}`);
26
+ }
27
+ }
28
+
29
+ ensureWhatsAppFormat(number) {
30
+ if (!number) return null;
31
+ if (number.startsWith('whatsapp:')) return number;
32
+ if (number.startsWith('+')) return `whatsapp:${number}`;
33
+ return `whatsapp:+${number}`;
34
+ }
35
+
36
+ async sendMessage(messageData) {
37
+ if (!this.isConnected || !this.twilioClient) {
38
+ throw new Error('Twilio provider not initialized');
39
+ }
40
+
41
+ const { to, message, fileUrl, fileType, variables, contentSid } = messageData;
42
+
43
+ const formattedFrom = this.ensureWhatsAppFormat(this.whatsappNumber);
44
+ const formattedTo = this.ensureWhatsAppFormat(to);
45
+
46
+ if (!formattedFrom || !formattedTo) {
47
+ throw new Error('Invalid sender or recipient number');
48
+ }
49
+
50
+ const messageParams = {
51
+ from: formattedFrom,
52
+ to: formattedTo
53
+ };
54
+
55
+ // Handle template messages
56
+ if (contentSid) {
57
+ messageParams.contentSid = contentSid;
58
+ if (variables && Object.keys(variables).length > 0) {
59
+ const formattedVariables = {};
60
+ Object.keys(variables).forEach(key => {
61
+ const numericString = key.replace(/[^0-9]/g, '');
62
+ const numericKey = parseInt(numericString, 10).toString();
63
+ formattedVariables[numericKey] = variables[key];
64
+ });
65
+ messageParams.contentVariables = JSON.stringify(formattedVariables);
66
+ }
67
+ } else if (message) {
68
+ messageParams.body = message;
69
+ }
70
+
71
+ // Handle media messages
72
+ if (fileUrl && fileType !== 'text') {
73
+ messageParams.mediaUrl = [fileUrl];
74
+ if (!messageParams.body || messageParams.body.trim() === '') {
75
+ delete messageParams.body;
76
+ }
77
+ }
78
+
79
+ // Validate message has content
80
+ if (!messageParams.body && !messageParams.mediaUrl && !messageParams.contentSid) {
81
+ throw new Error('Message must have body, media URL, or content SID');
82
+ }
83
+
84
+ try {
85
+ const result = await this.twilioClient.messages.create(messageParams);
86
+ return {
87
+ success: true,
88
+ messageId: result.sid,
89
+ provider: 'twilio',
90
+ result
91
+ };
92
+ } catch (error) {
93
+ throw new Error(`Twilio send failed: ${error.message}`);
94
+ }
95
+ }
96
+
97
+ async sendScheduledMessage(scheduledMessage) {
98
+ const { sendTime, timeZone } = scheduledMessage;
99
+ const delay = this.calculateDelay(sendTime, timeZone);
100
+
101
+ setTimeout(async () => {
102
+ try {
103
+ await this.sendMessage(scheduledMessage);
104
+ console.log('Scheduled message sent successfully');
105
+ } catch (error) {
106
+ console.error(`Scheduled message failed: ${error.message}`);
107
+ }
108
+ }, delay);
109
+ }
110
+
111
+ calculateDelay(sendTime) {
112
+ const now = new Date();
113
+ const targetTime = new Date(sendTime);
114
+ return Math.max(0, targetTime.getTime() - now.getTime());
115
+ }
116
+ }
117
+
118
+ module.exports = { TwilioProvider };
@@ -0,0 +1,7 @@
1
+ const { TwilioProvider } = require('./TwilioProvider');
2
+ const { BaileysProvider } = require('./BaileysProvider');
3
+
4
+ module.exports = {
5
+ TwilioProvider,
6
+ BaileysProvider
7
+ };
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Abstract base class for messaging providers
3
+ */
4
+ class MessageProvider {
5
+ constructor(config = {}) {
6
+ this.config = config;
7
+ this.isConnected = false;
8
+ }
9
+
10
+ /**
11
+ * Initialize the provider connection
12
+ * @returns {Promise<void>}
13
+ */
14
+ async initialize() {
15
+ throw new Error('initialize() must be implemented by provider');
16
+ }
17
+
18
+ /**
19
+ * Connect the provider
20
+ * @returns {Promise<void>}
21
+ */
22
+ async connect() {
23
+ throw new Error('connect() must be implemented by provider');
24
+ }
25
+
26
+ /**
27
+ * Send a message
28
+ * @param {Object} messageData - Message data
29
+ * @param {string} messageData.to - Recipient
30
+ * @param {string} messageData.message - Message text
31
+ * @param {string} messageData.fileUrl - Optional file URL
32
+ * @param {string} messageData.fileType - File type (text, image, document, audio)
33
+ * @param {Object} messageData.variables - Template variables
34
+ * @param {string} messageData.contentSid - Template content SID
35
+ * @returns {Promise<Object>} Message result
36
+ */
37
+ async sendMessage(messageData) {
38
+ // eslint-disable-next-line no-unused-vars
39
+ messageData;
40
+ throw new Error('sendMessage() must be implemented by provider');
41
+ }
42
+
43
+ /**
44
+ * Send a scheduled message
45
+ * @param {Object} scheduledMessage - Scheduled message data
46
+ * @returns {Promise<void>}
47
+ */
48
+ async sendScheduledMessage(scheduledMessage) {
49
+ // eslint-disable-next-line no-unused-vars
50
+ scheduledMessage;
51
+ throw new Error('sendScheduledMessage() must be implemented by provider');
52
+ }
53
+
54
+ /**
55
+ * Check if provider is connected
56
+ * @returns {boolean}
57
+ */
58
+ getConnectionStatus() {
59
+ return this.isConnected;
60
+ }
61
+
62
+ /**
63
+ * Disconnect the provider
64
+ * @returns {Promise<void>}
65
+ */
66
+ async disconnect() {
67
+ throw new Error('disconnect() must be implemented by provider');
68
+ }
69
+ }
70
+
71
+ module.exports = { MessageProvider };
@@ -0,0 +1,231 @@
1
+ const { TwilioProvider } = require('../adapters/TwilioProvider');
2
+ const { BaileysProvider } = require('../adapters/BaileysProvider');
3
+
4
+ /**
5
+ * Core messaging class that manages providers and message handling
6
+ */
7
+ class NexusMessaging {
8
+ constructor(config = {}) {
9
+ this.config = config;
10
+ this.provider = null;
11
+ this.messageStorage = null;
12
+ this.handlers = {
13
+ onMessage: null,
14
+ onInteractive: null,
15
+ onMedia: null,
16
+ onCommand: null,
17
+ onKeyword: null,
18
+ onFlow: null
19
+ };
20
+ }
21
+
22
+ /**
23
+ * Initialize messaging with specified provider
24
+ * @param {string} providerType - 'twilio' or 'baileys'
25
+ * @param {Object} providerConfig - Provider-specific configuration
26
+ */
27
+ async initializeProvider(providerType, providerConfig) {
28
+ switch (providerType.toLowerCase()) {
29
+ case 'twilio':
30
+ this.provider = new TwilioProvider(providerConfig);
31
+ break;
32
+ case 'baileys':
33
+ this.provider = new BaileysProvider(providerConfig);
34
+ break;
35
+ default:
36
+ throw new Error(`Unsupported provider: ${providerType}`);
37
+ }
38
+
39
+ await this.provider.initialize();
40
+ }
41
+
42
+ /**
43
+ * Set message storage interface
44
+ * @param {Object} storage - Storage interface with save methods
45
+ */
46
+ setMessageStorage(storage) {
47
+ this.messageStorage = storage;
48
+ }
49
+
50
+ /**
51
+ * Register event handlers for different message types
52
+ * @param {Object} handlers - Handler functions
53
+ */
54
+ setHandlers(handlers) {
55
+ this.handlers = { ...this.handlers, ...handlers };
56
+ }
57
+
58
+ /**
59
+ * Register a single message handler
60
+ * @param {Function} handler - Message handler function
61
+ */
62
+ onMessage(handler) {
63
+ this.handlers.onMessage = handler;
64
+ }
65
+
66
+ /**
67
+ * Register an interactive message handler
68
+ * @param {Function} handler - Interactive handler function
69
+ */
70
+ onInteractive(handler) {
71
+ this.handlers.onInteractive = handler;
72
+ }
73
+
74
+ /**
75
+ * Register a media handler
76
+ * @param {Function} handler - Media handler function
77
+ */
78
+ onMedia(handler) {
79
+ this.handlers.onMedia = handler;
80
+ }
81
+
82
+ /**
83
+ * Register a command handler
84
+ * @param {Function} handler - Command handler function
85
+ */
86
+ onCommand(handler) {
87
+ this.handlers.onCommand = handler;
88
+ }
89
+
90
+ /**
91
+ * Register a keyword handler
92
+ * @param {Function} handler - Keyword handler function
93
+ */
94
+ onKeyword(handler) {
95
+ this.handlers.onKeyword = handler;
96
+ }
97
+
98
+ /**
99
+ * Register a flow handler
100
+ * @param {Function} handler - Flow handler function
101
+ */
102
+ onFlow(handler) {
103
+ this.handlers.onFlow = handler;
104
+ }
105
+
106
+ /**
107
+ * Send a message through the active provider
108
+ * @param {Object} messageData - Message data
109
+ */
110
+ async sendMessage(messageData) {
111
+ if (!this.provider) {
112
+ throw new Error('No provider initialized');
113
+ }
114
+
115
+ const result = await this.provider.sendMessage(messageData);
116
+
117
+ // Store message if storage is configured
118
+ if (this.messageStorage) {
119
+ await this.messageStorage.saveMessage({
120
+ ...messageData,
121
+ messageId: result.messageId,
122
+ provider: result.provider,
123
+ timestamp: new Date(),
124
+ fromMe: true
125
+ });
126
+ }
127
+
128
+ return result;
129
+ }
130
+
131
+ /**
132
+ * Send a scheduled message
133
+ * @param {Object} scheduledMessage - Scheduled message data
134
+ */
135
+ async sendScheduledMessage(scheduledMessage) {
136
+ if (!this.provider) {
137
+ throw new Error('No provider initialized');
138
+ }
139
+
140
+ return await this.provider.sendScheduledMessage(scheduledMessage);
141
+ }
142
+
143
+ /**
144
+ * Process incoming message with configured handlers
145
+ * @param {Object} messageData - Incoming message data
146
+ */
147
+ async processIncomingMessage(messageData) {
148
+ // Store incoming message
149
+ if (this.messageStorage) {
150
+ await this.messageStorage.saveMessage({
151
+ ...messageData,
152
+ timestamp: new Date(),
153
+ fromMe: false
154
+ });
155
+ }
156
+
157
+ // Determine message type and call appropriate handler
158
+ if (messageData.interactive) {
159
+ return await this.handleInteractive(messageData);
160
+ } else if (messageData.media) {
161
+ return await this.handleMedia(messageData);
162
+ } else if (messageData.command) {
163
+ return await this.handleCommand(messageData);
164
+ } else if (messageData.keyword) {
165
+ return await this.handleKeyword(messageData);
166
+ } else if (messageData.flow) {
167
+ return await this.handleFlow(messageData);
168
+ } else {
169
+ return await this.handleMessage(messageData);
170
+ }
171
+ }
172
+
173
+ async handleMessage(messageData) {
174
+ if (this.handlers.onMessage) {
175
+ return await this.handlers.onMessage(messageData, this);
176
+ }
177
+ }
178
+
179
+ async handleInteractive(messageData) {
180
+ // Store interactive message
181
+ if (this.messageStorage) {
182
+ await this.messageStorage.saveInteractive(messageData);
183
+ }
184
+
185
+ if (this.handlers.onInteractive) {
186
+ return await this.handlers.onInteractive(messageData, this);
187
+ }
188
+ }
189
+
190
+ async handleMedia(messageData) {
191
+ if (this.handlers.onMedia) {
192
+ return await this.handlers.onMedia(messageData, this);
193
+ }
194
+ }
195
+
196
+ async handleCommand(messageData) {
197
+ if (this.handlers.onCommand) {
198
+ return await this.handlers.onCommand(messageData, this);
199
+ }
200
+ }
201
+
202
+ async handleKeyword(messageData) {
203
+ if (this.handlers.onKeyword) {
204
+ return await this.handlers.onKeyword(messageData, this);
205
+ }
206
+ }
207
+
208
+ async handleFlow(messageData) {
209
+ if (this.handlers.onFlow) {
210
+ return await this.handlers.onFlow(messageData, this);
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Get provider connection status
216
+ */
217
+ isConnected() {
218
+ return this.provider ? this.provider.getConnectionStatus() : false;
219
+ }
220
+
221
+ /**
222
+ * Disconnect the provider
223
+ */
224
+ async disconnect() {
225
+ if (this.provider) {
226
+ await this.provider.disconnect();
227
+ }
228
+ }
229
+ }
230
+
231
+ module.exports = { NexusMessaging };
@@ -0,0 +1,7 @@
1
+ const { NexusMessaging } = require('./NexusMessaging');
2
+ const { MessageProvider } = require('./MessageProvider');
3
+
4
+ module.exports = {
5
+ NexusMessaging,
6
+ MessageProvider
7
+ };