@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.
- package/CHANGELOG.md +55 -0
- package/LICENSE +21 -0
- package/MIGRATION_GUIDE.md +388 -0
- package/README.md +532 -0
- package/examples/.env.example +24 -0
- package/examples/assistants/BaseAssistant.js +242 -0
- package/examples/assistants/ExampleAssistant.js +111 -0
- package/examples/assistants/index.js +7 -0
- package/examples/basic-usage.js +109 -0
- package/examples/consumer-server.js +206 -0
- package/lib/adapters/BaileysProvider.js +180 -0
- package/lib/adapters/TwilioProvider.js +118 -0
- package/lib/adapters/index.js +7 -0
- package/lib/core/MessageProvider.js +71 -0
- package/lib/core/NexusMessaging.js +231 -0
- package/lib/core/index.js +7 -0
- package/lib/index.d.ts +276 -0
- package/lib/index.js +204 -0
- package/lib/models/index.js +9 -0
- package/lib/models/messageModel.js +91 -0
- package/lib/models/threadModel.js +20 -0
- package/lib/storage/MongoStorage.js +183 -0
- package/lib/storage/index.js +5 -0
- package/lib/utils/AssistantManager.js +218 -0
- package/lib/utils/DefaultLLMProvider.js +22 -0
- package/lib/utils/MessageParser.js +249 -0
- package/lib/utils/index.js +22 -0
- package/lib/utils/logger.js +22 -0
- package/lib/utils/mongoAuthConfig.js +139 -0
- package/lib/utils/twilioHelper.js +77 -0
- package/lib/utils/whatsappHelper.js +68 -0
- package/package.json +82 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const pino = require('pino');
|
|
2
|
+
|
|
3
|
+
const createLogger = (config = {}) => {
|
|
4
|
+
const {
|
|
5
|
+
level = process.env.NODE_ENV === 'production' ? 'info' : 'debug'
|
|
6
|
+
} = config;
|
|
7
|
+
|
|
8
|
+
return pino({
|
|
9
|
+
level,
|
|
10
|
+
transport: process.env.NODE_ENV !== 'production' ? {
|
|
11
|
+
target: 'pino-pretty',
|
|
12
|
+
options: {
|
|
13
|
+
colorize: true
|
|
14
|
+
}
|
|
15
|
+
} : undefined
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Default logger instance
|
|
20
|
+
const logger = createLogger();
|
|
21
|
+
|
|
22
|
+
module.exports = { logger, createLogger };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const { MongoClient } = require('mongodb');
|
|
2
|
+
|
|
3
|
+
const { proto, Curve, signedKeyPair, generateRegistrationId } = require('baileys');
|
|
4
|
+
const { randomBytes } = require('crypto');
|
|
5
|
+
|
|
6
|
+
async function connectToMongo(mongoClient) {
|
|
7
|
+
try {
|
|
8
|
+
await mongoClient.connect();
|
|
9
|
+
console.log('Connected to Mongo database successfully.');
|
|
10
|
+
} catch (err) {
|
|
11
|
+
console.error('Error connecting to Mongo database:', err.message);
|
|
12
|
+
throw err;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function initAuthCreds() {
|
|
17
|
+
const identityKey = Curve.generateKeyPair();
|
|
18
|
+
return {
|
|
19
|
+
noiseKey: Curve.generateKeyPair(),
|
|
20
|
+
signedIdentityKey: identityKey,
|
|
21
|
+
signedPreKey: signedKeyPair(identityKey, 1),
|
|
22
|
+
registrationId: generateRegistrationId(),
|
|
23
|
+
advSecretKey: randomBytes(32).toString('base64'),
|
|
24
|
+
processedHistoryMessages: [],
|
|
25
|
+
nextPreKeyId: 1,
|
|
26
|
+
firstUnuploadedPreKeyId: 1,
|
|
27
|
+
accountSettings: {
|
|
28
|
+
unarchiveChats: false,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const BufferJSON = {
|
|
34
|
+
replacer: (k, value) => {
|
|
35
|
+
if (
|
|
36
|
+
Buffer.isBuffer(value) ||
|
|
37
|
+
value instanceof Uint8Array ||
|
|
38
|
+
value?.type === 'Buffer'
|
|
39
|
+
) {
|
|
40
|
+
return {
|
|
41
|
+
type: 'Buffer',
|
|
42
|
+
data: Buffer.from(value?.data || value).toString('base64'),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return value;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
reviver: (_, value) => {
|
|
50
|
+
if (
|
|
51
|
+
typeof value === 'object' &&
|
|
52
|
+
!!value &&
|
|
53
|
+
(value.buffer === true || value.type === 'Buffer')
|
|
54
|
+
) {
|
|
55
|
+
const val = value.data || value.value;
|
|
56
|
+
return typeof val === 'string'
|
|
57
|
+
? Buffer.from(val, 'base64')
|
|
58
|
+
: Buffer.from(val || []);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return value;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
async function useMongoDBAuthState(uri, dbName, sessionId) {
|
|
66
|
+
const client = new MongoClient(uri);
|
|
67
|
+
await connectToMongo(client);
|
|
68
|
+
|
|
69
|
+
const db = client.db(dbName);
|
|
70
|
+
const collection = db.collection('auth_sessions');
|
|
71
|
+
|
|
72
|
+
const writeData = async (data, key) => {
|
|
73
|
+
const informationToStore = JSON.parse(
|
|
74
|
+
JSON.stringify(data, BufferJSON.replacer)
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const update = {
|
|
78
|
+
$set: {
|
|
79
|
+
...informationToStore,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
return collection.updateOne({ _id: `${sessionId}-${key}` }, update, { upsert: true });
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const readData = async (key) => {
|
|
86
|
+
try {
|
|
87
|
+
const data = JSON.stringify(await collection.findOne({ _id: `${sessionId}-${key}` }));
|
|
88
|
+
return JSON.parse(data, BufferJSON.reviver);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.log(err);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const removeData = async (key) => {
|
|
96
|
+
try {
|
|
97
|
+
await collection.deleteOne({ _id: `${sessionId}-${key}` });
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.log(err);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const creds = (await readData('creds')) || (0, initAuthCreds)();
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
state: {
|
|
107
|
+
creds,
|
|
108
|
+
keys: {
|
|
109
|
+
get: async (type, ids) => {
|
|
110
|
+
const data = {};
|
|
111
|
+
await Promise.all(ids.map(async (id) => {
|
|
112
|
+
let value = await readData(`${type}-${id}`);
|
|
113
|
+
if (type === 'app-state-sync-key') {
|
|
114
|
+
value = proto.Message.AppStateSyncKeyData.fromObject(data);
|
|
115
|
+
}
|
|
116
|
+
data[id] = value;
|
|
117
|
+
}));
|
|
118
|
+
return data;
|
|
119
|
+
},
|
|
120
|
+
set: async (data) => {
|
|
121
|
+
const tasks = [];
|
|
122
|
+
for (const category in data) {
|
|
123
|
+
for (const id in data[category]) {
|
|
124
|
+
const value = data[category][id];
|
|
125
|
+
const key = `${category}-${id}`;
|
|
126
|
+
tasks.push(value ? writeData(value, key) : removeData(key));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
await Promise.all(tasks);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
saveCreds: async () => {
|
|
134
|
+
await writeData(creds, 'creds');
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = { useMongoDBAuthState };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const { v4: uuidv4 } = require('uuid');
|
|
3
|
+
|
|
4
|
+
function convertTwilioToInternalFormat(twilioMessage) {
|
|
5
|
+
const from = twilioMessage.From || '';
|
|
6
|
+
const to = twilioMessage.To || '';
|
|
7
|
+
const fromMe = to === from;
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
key: {
|
|
11
|
+
id: twilioMessage.MessageSid || uuidv4(),
|
|
12
|
+
fromMe: fromMe,
|
|
13
|
+
remoteJid: fromMe ? to : from
|
|
14
|
+
},
|
|
15
|
+
pushName: twilioMessage.ProfileName || '',
|
|
16
|
+
message: {
|
|
17
|
+
conversation: twilioMessage.Body || ''
|
|
18
|
+
},
|
|
19
|
+
messageTimestamp: Math.floor(Date.now() / 1000)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function downloadMediaFromTwilio(mediaUrl, credentials) {
|
|
24
|
+
try {
|
|
25
|
+
console.log(`Downloading media from: ${mediaUrl}`);
|
|
26
|
+
|
|
27
|
+
const response = await axios({
|
|
28
|
+
method: 'GET',
|
|
29
|
+
url: mediaUrl,
|
|
30
|
+
responseType: 'arraybuffer',
|
|
31
|
+
headers: {
|
|
32
|
+
'Authorization': `Basic ${Buffer.from(
|
|
33
|
+
`${credentials.accountSid}:${credentials.authToken}`
|
|
34
|
+
).toString('base64')}`
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return Buffer.from(response.data);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error(`Failed to download media: ${error.message}`);
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getMediaTypeFromContentType(contentType) {
|
|
46
|
+
if (contentType.startsWith('image/')) return 'imageMessage';
|
|
47
|
+
if (contentType.startsWith('audio/')) return 'audioMessage';
|
|
48
|
+
if (contentType.startsWith('video/')) return 'videoMessage';
|
|
49
|
+
if (contentType.startsWith('application/')) return 'documentMessage';
|
|
50
|
+
return 'unknownMessage';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function extractTitle(message, mediaType) {
|
|
54
|
+
if (mediaType === 'documentMessage' && message.MediaUrl0) {
|
|
55
|
+
const urlParts = message.MediaUrl0.split('/');
|
|
56
|
+
return urlParts[urlParts.length - 1] || null;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const ensureWhatsAppFormat = (phoneNumber) => {
|
|
62
|
+
if (!phoneNumber) return null;
|
|
63
|
+
|
|
64
|
+
if (phoneNumber.startsWith('whatsapp:')) {
|
|
65
|
+
return phoneNumber;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return `whatsapp:${phoneNumber}`;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
convertTwilioToInternalFormat,
|
|
73
|
+
downloadMediaFromTwilio,
|
|
74
|
+
getMediaTypeFromContentType,
|
|
75
|
+
extractTitle,
|
|
76
|
+
ensureWhatsAppFormat
|
|
77
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const moment = require('moment-timezone');
|
|
2
|
+
|
|
3
|
+
function delay(ms) {
|
|
4
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function formatCode(codeBase) {
|
|
8
|
+
|
|
9
|
+
const [number, domain] = codeBase.split('@');
|
|
10
|
+
|
|
11
|
+
if (!number || !domain) {
|
|
12
|
+
throw new Error('Invalid code format: missing number or domain');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (domain !== 's.whatsapp.net') {
|
|
16
|
+
throw new Error('Invalid domain: must be s.whatsapp.net');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let formattedNumber = number;
|
|
20
|
+
|
|
21
|
+
if (formattedNumber.endsWith('-2')) {
|
|
22
|
+
formattedNumber = formattedNumber.slice(0, -2);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (formattedNumber.length === 10) {
|
|
26
|
+
formattedNumber = '52' + formattedNumber;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (formattedNumber.startsWith('52') && formattedNumber.length === 12) {
|
|
30
|
+
formattedNumber = formattedNumber.substring(0, 2) + '1' + formattedNumber.substring(2);
|
|
31
|
+
}
|
|
32
|
+
return `${formattedNumber}@s.whatsapp.net`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function calculateDelay(sendTime, timeZone) {
|
|
36
|
+
if (sendTime !== undefined && timeZone !== undefined) {
|
|
37
|
+
const sendMoment = moment.tz(sendTime, timeZone);
|
|
38
|
+
|
|
39
|
+
if (!sendMoment.isValid()) {
|
|
40
|
+
return { error: 'Invalid time format' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const now = moment().tz(timeZone);
|
|
44
|
+
const randomDelay = Math.floor(Math.random() * 15001) + 15000;
|
|
45
|
+
const delay = sendMoment.diff(now) + randomDelay;
|
|
46
|
+
|
|
47
|
+
console.log(
|
|
48
|
+
'Scheduled Time:', sendMoment.format(),
|
|
49
|
+
'Current Time:', now.format(),
|
|
50
|
+
'Delay (minutes):', delay / 60000,
|
|
51
|
+
'Remaining Seconds:', delay % 60000
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (delay <= 0) {
|
|
55
|
+
return 2500;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return delay;
|
|
59
|
+
} else {
|
|
60
|
+
return 2500;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
delay,
|
|
66
|
+
formatCode,
|
|
67
|
+
calculateDelay
|
|
68
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@peopl-health/nexus",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Core messaging and assistant library for WhatsApp communication platforms",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"main": "lib/index.js",
|
|
9
|
+
"types": "lib/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./lib/index.js",
|
|
13
|
+
"require": "./lib/index.js",
|
|
14
|
+
"types": "./lib/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./adapters": {
|
|
17
|
+
"import": "./lib/adapters/index.js",
|
|
18
|
+
"require": "./lib/adapters/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./core": {
|
|
21
|
+
"import": "./lib/core/index.js",
|
|
22
|
+
"require": "./lib/core/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./utils": {
|
|
25
|
+
"import": "./lib/utils/index.js",
|
|
26
|
+
"require": "./lib/utils/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc",
|
|
31
|
+
"dev": "tsc --watch",
|
|
32
|
+
"test": "jest",
|
|
33
|
+
"lint": "eslint lib/**/*.js",
|
|
34
|
+
"prepublishOnly": "npm test && npm run lint",
|
|
35
|
+
"version": "npm run prepublishOnly && git add -A lib",
|
|
36
|
+
"postversion": "git push && git push --tags"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"whatsapp",
|
|
40
|
+
"messaging",
|
|
41
|
+
"ai-assistant",
|
|
42
|
+
"twilio",
|
|
43
|
+
"baileys",
|
|
44
|
+
"communication"
|
|
45
|
+
],
|
|
46
|
+
"author": "PEOPL Health Tech",
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"mongoose": "^7.5.0",
|
|
50
|
+
"moment-timezone": "^0.5.43",
|
|
51
|
+
"pino": "^8.15.0",
|
|
52
|
+
"pino-pretty": "^10.2.0",
|
|
53
|
+
"uuid": "^9.0.0",
|
|
54
|
+
"axios": "^1.5.0"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"baileys": "^6.4.0",
|
|
58
|
+
"twilio": "^4.15.0",
|
|
59
|
+
"openai": "^4.0.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/node": "^20.5.0",
|
|
63
|
+
"typescript": "^5.1.6",
|
|
64
|
+
"jest": "^29.6.2",
|
|
65
|
+
"eslint": "^8.47.0"
|
|
66
|
+
},
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": ">=20.0.0"
|
|
69
|
+
},
|
|
70
|
+
"repository": {
|
|
71
|
+
"type": "git",
|
|
72
|
+
"url": "https://github.com/peopl-health/nexus.git"
|
|
73
|
+
},
|
|
74
|
+
"files": [
|
|
75
|
+
"lib/",
|
|
76
|
+
"examples/",
|
|
77
|
+
"README.md",
|
|
78
|
+
"LICENSE",
|
|
79
|
+
"CHANGELOG.md",
|
|
80
|
+
"MIGRATION_GUIDE.md"
|
|
81
|
+
]
|
|
82
|
+
}
|