alexa-ai 2.1.1

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,148 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * JidParser
5
+ * ---------
6
+ * Normalises every WhatsApp identifier shape the bot may hand us.
7
+ *
8
+ * Supported inputs:
9
+ * 78151912841263@lid -> linked-device / privacy id (user)
10
+ * 94771234567@s.whatsapp.net -> classic phone jid (user)
11
+ * 94771234567@c.us -> legacy web jid (user)
12
+ * 94771234567:12@s.whatsapp.net -> jid with device suffix (user)
13
+ * 120363413125431525@g.us -> group (group)
14
+ * xxxxx@broadcast / @newsletter -> broadcast / channel
15
+ *
16
+ * The parser is intentionally forgiving: WhatsApp libraries pass slightly
17
+ * different shapes depending on version, and a memory system must never lose
18
+ * a user just because a `:device` suffix appeared.
19
+ */
20
+ class JidParser {
21
+ static SERVER_LID = 'lid';
22
+ static SERVER_USER = 's.whatsapp.net';
23
+ static SERVER_LEGACY = 'c.us';
24
+ static SERVER_GROUP = 'g.us';
25
+ static SERVER_BROADCAST = 'broadcast';
26
+ static SERVER_NEWSLETTER = 'newsletter';
27
+
28
+ /**
29
+ * Parse any jid into a stable descriptor.
30
+ * @param {string} rawJid
31
+ * @returns {{
32
+ * raw: string, jid: string, local: string, server: string,
33
+ * type: 'lid'|'user'|'group'|'broadcast'|'newsletter'|'unknown',
34
+ * isGroup: boolean, isUser: boolean, isLid: boolean, device: number|null,
35
+ * phone: string|null, valid: boolean
36
+ * }}
37
+ */
38
+ static parse(rawJid) {
39
+ const empty = {
40
+ raw: rawJid == null ? '' : String(rawJid),
41
+ jid: '',
42
+ local: '',
43
+ server: '',
44
+ type: 'unknown',
45
+ isGroup: false,
46
+ isUser: false,
47
+ isLid: false,
48
+ device: null,
49
+ phone: null,
50
+ valid: false,
51
+ };
52
+
53
+ if (rawJid == null) return empty;
54
+
55
+ const raw = String(rawJid).trim();
56
+ if (!raw) return empty;
57
+
58
+ // Strip anything after a space (some libs append push names)
59
+ const cleaned = raw.split(/\s+/)[0];
60
+
61
+ const at = cleaned.lastIndexOf('@');
62
+ let local = at === -1 ? cleaned : cleaned.slice(0, at);
63
+ const server = at === -1 ? '' : cleaned.slice(at + 1).toLowerCase();
64
+
65
+ // Split device suffix -> "94771234567:12" => local 94771234567, device 12
66
+ let device = null;
67
+ const colon = local.indexOf(':');
68
+ if (colon !== -1) {
69
+ const devPart = local.slice(colon + 1);
70
+ local = local.slice(0, colon);
71
+ const parsedDev = Number.parseInt(devPart, 10);
72
+ device = Number.isNaN(parsedDev) ? null : parsedDev;
73
+ }
74
+
75
+ local = local.replace(/[^0-9a-zA-Z_-]/g, '');
76
+
77
+ let type = 'unknown';
78
+ if (server === JidParser.SERVER_GROUP) type = 'group';
79
+ else if (server === JidParser.SERVER_LID) type = 'lid';
80
+ else if (server === JidParser.SERVER_USER || server === JidParser.SERVER_LEGACY) type = 'user';
81
+ else if (server === JidParser.SERVER_BROADCAST) type = 'broadcast';
82
+ else if (server === JidParser.SERVER_NEWSLETTER) type = 'newsletter';
83
+ else if (!server && /^\d{6,}$/.test(local)) type = 'user'; // bare number
84
+
85
+ const isGroup = type === 'group';
86
+ const isLid = type === 'lid';
87
+ const isUser = type === 'user' || isLid;
88
+
89
+ // Only a real phone jid yields a usable phone number. @lid is a privacy
90
+ // id and must NEVER be treated as a phone number.
91
+ const phone = type === 'user' && /^\d{6,}$/.test(local) ? local : null;
92
+
93
+ const normalisedServer = server || (type === 'user' ? JidParser.SERVER_USER : '');
94
+
95
+ return {
96
+ raw,
97
+ jid: normalisedServer ? `${local}@${normalisedServer}` : local,
98
+ local,
99
+ server: normalisedServer,
100
+ type,
101
+ isGroup,
102
+ isUser,
103
+ isLid,
104
+ device,
105
+ phone,
106
+ valid: Boolean(local) && type !== 'unknown',
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Canonical, device-stripped jid used as the DB primary key for a user/group.
112
+ * @param {string} rawJid
113
+ * @returns {string}
114
+ */
115
+ static normalize(rawJid) {
116
+ return JidParser.parse(rawJid).jid;
117
+ }
118
+
119
+ static isGroup(rawJid) {
120
+ return JidParser.parse(rawJid).isGroup;
121
+ }
122
+
123
+ static isUser(rawJid) {
124
+ return JidParser.parse(rawJid).isUser;
125
+ }
126
+
127
+ static isLid(rawJid) {
128
+ return JidParser.parse(rawJid).isLid;
129
+ }
130
+
131
+ /**
132
+ * Build the conversation context key.
133
+ * DM -> "dm:<userJid>"
134
+ * Group -> "group:<groupJid>:<userJid>" (per-user thread inside a group)
135
+ * @param {string} userJid
136
+ * @param {string} [groupJid]
137
+ * @param {boolean} [sharedGroupThread=false] one thread for the whole group
138
+ * @returns {string}
139
+ */
140
+ static contextKey(userJid, groupJid, sharedGroupThread = false) {
141
+ const user = JidParser.normalize(userJid);
142
+ const group = groupJid ? JidParser.normalize(groupJid) : '';
143
+ if (!group) return `dm:${user}`;
144
+ return sharedGroupThread ? `group:${group}` : `group:${group}:${user}`;
145
+ }
146
+ }
147
+
148
+ module.exports = JidParser;
@@ -0,0 +1,235 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Media
5
+ * -----
6
+ * One place that turns "whatever the bot handed us" into the media shape the
7
+ * engine works with:
8
+ *
9
+ * { buffer: Buffer, mimetype: 'image/png', filename: 'image.png' }
10
+ * { url: 'https://…' }
11
+ *
12
+ * Every public method that takes an image (`chat({ image })`, `describeImage`,
13
+ * `editImage`, `upscaleImage`, `detectNsfw`, `ask()`) runs its input through
14
+ * `normalize()`, so all of them accept the same inputs:
15
+ *
16
+ * Buffer · Uint8Array · ArrayBuffer
17
+ * 'data:image/png;base64,…' data URI
18
+ * '<raw base64>' e.g. whatsapp-web.js `media.data`
19
+ * 'https://…' remote URL
20
+ * { buffer, mimetype?, filename? } Baileys downloadMediaMessage()
21
+ * { base64 } · { data } whatsapp-web.js MessageMedia
22
+ * { url }
23
+ *
24
+ * Before this existed each method had its own partial check: a bare Buffer
25
+ * was "unreadable" to `describeImage()`, and `{ base64 }` was silently dropped
26
+ * by the `/api/*` helpers, so the request went out with no image at all.
27
+ */
28
+ class Media {
29
+ static DEFAULT_IMAGE_MIME = 'image/jpeg';
30
+
31
+ /** Extension used for the default filename of each mimetype. */
32
+ static EXTENSIONS = {
33
+ 'image/jpeg': 'jpg',
34
+ 'image/jpg': 'jpg',
35
+ 'image/png': 'png',
36
+ 'image/webp': 'webp',
37
+ 'image/gif': 'gif',
38
+ 'image/bmp': 'bmp',
39
+ 'application/pdf': 'pdf',
40
+ 'text/plain': 'txt',
41
+ 'text/markdown': 'md',
42
+ 'text/csv': 'csv',
43
+ 'application/json': 'json',
44
+ 'application/msword': 'doc',
45
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
46
+ };
47
+
48
+ /**
49
+ * @param {any} input see the list above
50
+ * @returns {{buffer?:Buffer, url?:string, mimetype?:string, filename?:string}|null}
51
+ */
52
+ static normalize(input) {
53
+ if (input == null) return null;
54
+
55
+ // ---- raw bytes -------------------------------------------------------
56
+ const bytes = Media._toBuffer(input);
57
+ if (bytes) return Media._withDefaults({ buffer: bytes });
58
+
59
+ // ---- strings: data URI, URL, raw base64 ---------------------------------
60
+ if (typeof input === 'string') {
61
+ const str = input.trim();
62
+ if (!str) return null;
63
+
64
+ const dataUri = Media._decodeDataUri(str);
65
+ if (dataUri) return Media._withDefaults(dataUri);
66
+
67
+ if (/^https?:\/\//i.test(str)) return { url: str };
68
+
69
+ const decoded = Media._decodeBase64(str);
70
+ if (decoded) return Media._withDefaults({ buffer: decoded });
71
+ return null;
72
+ }
73
+
74
+ if (typeof input !== 'object') return null;
75
+
76
+ // ---- objects -------------------------------------------------------------
77
+ const meta = {
78
+ mimetype: Media._cleanMime(input.mimetype || input.mimeType || input.type),
79
+ filename: Media._cleanName(input.filename || input.fileName || input.name),
80
+ };
81
+
82
+ const inner = Media._toBuffer(input.buffer);
83
+ if (inner) return Media._withDefaults({ ...input, ...meta, buffer: inner });
84
+
85
+ const inline = input.base64 ?? input.data;
86
+ if (typeof inline === 'string' && inline.trim()) {
87
+ const asUri = Media._decodeDataUri(inline.trim());
88
+ if (asUri) {
89
+ return Media._withDefaults({ ...input, ...meta, mimetype: meta.mimetype || asUri.mimetype, buffer: asUri.buffer });
90
+ }
91
+ const decoded = Media._decodeBase64(inline);
92
+ if (decoded) return Media._withDefaults({ ...input, ...meta, buffer: decoded });
93
+ }
94
+ // `data` may also be a nested Buffer/Uint8Array (some libraries do this).
95
+ const nested = Media._toBuffer(input.data);
96
+ if (nested) return Media._withDefaults({ ...input, ...meta, buffer: nested });
97
+
98
+ if (typeof input.url === 'string' && /^https?:\/\//i.test(input.url.trim())) {
99
+ const out = { ...input, url: input.url.trim() };
100
+ delete out.buffer;
101
+ delete out.base64;
102
+ delete out.data;
103
+ return out;
104
+ }
105
+
106
+ return null;
107
+ }
108
+
109
+ /**
110
+ * Shape used for DeepAI's classic `/api/*` family (`runApi`): a URL string
111
+ * is sent as a plain field, bytes as a file upload that keeps its mimetype
112
+ * and filename.
113
+ * @returns {string|{buffer:Buffer, mimetype:string, filename:string}|null}
114
+ */
115
+ static toApiField(input) {
116
+ const media = Media.normalize(input);
117
+ if (!media) return null;
118
+ if (media.url) return media.url;
119
+ return { buffer: media.buffer, mimetype: media.mimetype, filename: media.filename };
120
+ }
121
+
122
+ /** True when the media is a text-bearing document rather than a picture. */
123
+ static isDocument(media) {
124
+ if (!media) return false;
125
+ const mime = String(media.mimetype || '').toLowerCase();
126
+ const name = String(media.filename || '').toLowerCase();
127
+ if (mime.startsWith('image/')) return false;
128
+ return (
129
+ /^(text\/|application\/(pdf|json|xml|rtf|msword|vnd\.))/.test(mime) ||
130
+ /\.(txt|pdf|docx?|csv|md|json|xml|rtf|pptx?|xlsx?|log)$/.test(name)
131
+ );
132
+ }
133
+
134
+ /**
135
+ * Detect the real content type from the first bytes. Bots frequently
136
+ * label everything `image/jpeg`; DeepAI's upload uses the Blob type.
137
+ * @returns {string|null}
138
+ */
139
+ static sniff(buffer) {
140
+ if (!buffer || buffer.length < 4) return null;
141
+ const b = buffer;
142
+ if (b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47) return 'image/png';
143
+ if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) return 'image/jpeg';
144
+ if (b[0] === 0x47 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x38) return 'image/gif';
145
+ if (b[0] === 0x25 && b[1] === 0x50 && b[2] === 0x44 && b[3] === 0x46) return 'application/pdf';
146
+ if (b[0] === 0x42 && b[1] === 0x4d) return 'image/bmp';
147
+ if (
148
+ b.length >= 12 &&
149
+ b[0] === 0x52 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x46 &&
150
+ b[8] === 0x57 && b[9] === 0x45 && b[10] === 0x42 && b[11] === 0x50
151
+ ) {
152
+ return 'image/webp';
153
+ }
154
+ return null;
155
+ }
156
+
157
+ // ------------------------------------------------------------ helpers ---
158
+
159
+ /** @private Buffer | Uint8Array | ArrayBuffer -> Buffer (null otherwise). */
160
+ static _toBuffer(value) {
161
+ if (value == null) return null;
162
+ if (Buffer.isBuffer(value)) return value.length ? value : null;
163
+ if (value instanceof Uint8Array) return value.length ? Buffer.from(value) : null;
164
+ if (value instanceof ArrayBuffer) return value.byteLength ? Buffer.from(value) : null;
165
+ return null;
166
+ }
167
+
168
+ /** @private `data:<mime>;base64,<payload>` -> { buffer, mimetype } */
169
+ static _decodeDataUri(str) {
170
+ const match = /^data:([a-z0-9.+/-]+)?(?:;[a-z0-9=-]+)*;base64,([\s\S]+)$/i.exec(str);
171
+ if (!match) return null;
172
+ const buffer = Media._decodeBase64(match[2]);
173
+ if (!buffer) return null;
174
+ return { buffer, mimetype: Media._cleanMime(match[1]) };
175
+ }
176
+
177
+ /**
178
+ * @private Raw base64 -> Buffer. Only strings that really look like base64
179
+ * qualify, so an ordinary sentence is never mistaken for an image.
180
+ */
181
+ static _decodeBase64(str) {
182
+ const raw = String(str);
183
+ // Real base64 payloads contain no spaces (only line breaks, if
184
+ // anything). A sentence with spaces is prose, not an image.
185
+ if (/[ \t]/.test(raw.trim())) return null;
186
+ const compact = raw.replace(/\s+/g, '');
187
+ if (compact.length < 32) return null;
188
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(compact) && !/^[A-Za-z0-9_-]+={0,2}$/.test(compact)) return null;
189
+ // Base64 of any real file mixes cases and digits; a lower-case word
190
+ // run ("helloworldhelloworld…") is not media.
191
+ if (!/[A-Z]/.test(compact) || !/[a-z]/.test(compact) || !/[0-9+/_-]/.test(compact)) return null;
192
+ try {
193
+ const buffer = Buffer.from(compact, compact.includes('-') || compact.includes('_') ? 'base64url' : 'base64');
194
+ return buffer.length ? buffer : null;
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+
200
+ /** @private fill mimetype (sniffed when missing/generic) and filename. */
201
+ static _withDefaults(media) {
202
+ const out = { ...media };
203
+ delete out.base64;
204
+ delete out.data;
205
+ delete out.url;
206
+
207
+ const sniffed = Media.sniff(out.buffer);
208
+ const declared = Media._cleanMime(out.mimetype);
209
+ out.mimetype = (declared && declared !== 'application/octet-stream' ? declared : null) || sniffed || Media.DEFAULT_IMAGE_MIME;
210
+ // A wrong label ("image/jpeg" for a PNG) is corrected when the bytes say otherwise.
211
+ if (sniffed && declared && declared.startsWith('image/') && sniffed.startsWith('image/') && sniffed !== declared) {
212
+ out.mimetype = sniffed;
213
+ }
214
+
215
+ if (!out.filename) {
216
+ const ext = Media.EXTENSIONS[out.mimetype] || (out.mimetype.split('/')[1] || 'bin').replace(/[^a-z0-9]/gi, '') || 'bin';
217
+ out.filename = `${out.mimetype.startsWith('image/') ? 'image' : 'document'}.${ext}`;
218
+ }
219
+ return out;
220
+ }
221
+
222
+ static _cleanMime(value) {
223
+ if (!value || typeof value !== 'string') return null;
224
+ const mime = value.trim().toLowerCase().split(';')[0];
225
+ return /^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(mime) ? mime : null;
226
+ }
227
+
228
+ static _cleanName(value) {
229
+ if (!value || typeof value !== 'string') return null;
230
+ const name = value.trim().replace(/[\\/:*?"<>|\u0000-\u001F]/g, '_').slice(0, 128);
231
+ return name || null;
232
+ }
233
+ }
234
+
235
+ module.exports = Media;