levix-bot 3.3.0-rc.2 → 4.0.0-beta

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 (118) hide show
  1. package/README.md +11 -7
  2. package/app.cjs +42 -45
  3. package/package.json +9 -1
  4. package/public/dashboard/assets/index-C1b7ijfu.js +391 -0
  5. package/public/dashboard/assets/index-pYaivHON.css +1 -0
  6. package/public/dashboard/index.html +19 -0
  7. package/public/dashboard.css +391 -66
  8. package/public/dashboard.js +593 -42
  9. package/scheduler.cjs +9 -14
  10. package/src/auth/use-database-auth-state.js +2 -4
  11. package/src/bootstrap/core.js +2 -5
  12. package/src/bootstrap/panel.js +10 -5
  13. package/src/cli.js +3 -5
  14. package/src/commands/autoschedule.cjs +1 -4
  15. package/src/commands/blacklist.cjs +22 -20
  16. package/src/commands/block.cjs +2 -4
  17. package/src/commands/calc.cjs +9 -16
  18. package/src/commands/digit.cjs +97 -0
  19. package/src/commands/gemini.cjs +147 -161
  20. package/src/commands/group/add.cjs +7 -16
  21. package/src/commands/group/antiSpam.cjs +5 -11
  22. package/src/commands/group/antilink.cjs +34 -28
  23. package/src/commands/group/approveall.cjs +3 -11
  24. package/src/commands/group/clearwarns.cjs +2 -5
  25. package/src/commands/group/demote.cjs +3 -8
  26. package/src/commands/group/kick.cjs +12 -15
  27. package/src/commands/group/media.cjs +17 -24
  28. package/src/commands/group/members.cjs +1 -2
  29. package/src/commands/group/promote.cjs +4 -12
  30. package/src/commands/group/removeall.cjs +2 -5
  31. package/src/commands/group/setpp.cjs +1 -1
  32. package/src/commands/group/setrules.cjs +2 -8
  33. package/src/commands/group/setwarn.cjs +4 -2
  34. package/src/commands/group/tagadmins.cjs +2 -5
  35. package/src/commands/group/warn.cjs +26 -24
  36. package/src/commands/group/warns.cjs +1 -2
  37. package/src/commands/group/welcome.cjs +13 -17
  38. package/src/commands/group.cjs +7 -10
  39. package/src/commands/help.cjs +6 -12
  40. package/src/commands/lang.cjs +104 -0
  41. package/src/commands/listschedules.cjs +2 -6
  42. package/src/commands/loop.cjs +6 -13
  43. package/src/commands/memory.cjs +19 -32
  44. package/src/commands/mod.cjs +97 -35
  45. package/src/commands/perm.cjs +18 -56
  46. package/src/commands/ping.cjs +1 -5
  47. package/src/commands/poll.cjs +1 -1
  48. package/src/commands/rand.cjs +2 -5
  49. package/src/commands/schedule.cjs +4 -8
  50. package/src/commands/score.cjs +8 -2
  51. package/src/commands/setprefix.cjs +2 -2
  52. package/src/commands/shortlink.cjs +7 -15
  53. package/src/commands/shutdown.cjs +1 -1
  54. package/src/commands/sticker.cjs +222 -0
  55. package/src/commands/stt.cjs +136 -68
  56. package/src/commands/todo.cjs +9 -15
  57. package/src/commands/tts.cjs +10 -16
  58. package/src/commands/unblock.cjs +2 -6
  59. package/src/commands/weather.cjs +5 -12
  60. package/src/config/ai-persona.md +5 -3
  61. package/src/config/available-models.json +1156 -0
  62. package/src/config/baileys.config.js +2 -1
  63. package/src/config/brand.esm.js +1 -10
  64. package/src/config/constants.js +1 -1
  65. package/src/config/defaults.cjs +66 -66
  66. package/src/config/lock.cjs +2 -2
  67. package/src/config/modelCapabilities.cjs +172 -0
  68. package/src/config/runtime-config.cjs +8 -13
  69. package/src/config/secrets.cjs +18 -10
  70. package/src/config/settings.cjs +94 -10
  71. package/src/config/vault.cjs +107 -0
  72. package/src/core/connection.js +13 -12
  73. package/src/core/events.js +41 -23
  74. package/src/core/proxy.js +7 -7
  75. package/src/core/session.js +91 -48
  76. package/src/core/socket.js +12 -15
  77. package/src/db/db.cjs +14 -6
  78. package/src/db/store.cjs +221 -87
  79. package/src/domain/caddy.js +1 -1
  80. package/src/domain/command.js +40 -19
  81. package/src/domain/detect.js +1 -1
  82. package/src/domain/system.js +2 -5
  83. package/src/handlers/command.handler.js +22 -43
  84. package/src/handlers/group.handler.js +13 -13
  85. package/src/handlers/message.handler.js +11 -21
  86. package/src/index.js +8 -4
  87. package/src/middleware/antispam.middleware.js +46 -23
  88. package/src/middleware/blacklist.middleware.js +14 -9
  89. package/src/middleware/forward-tracking.middleware.js +3 -2
  90. package/src/middleware/permissions.middleware.js +7 -10
  91. package/src/panel/login-throttle.cjs +43 -0
  92. package/src/panel/session-auth.cjs +17 -0
  93. package/src/routes/dashboard.api.esm.js +394 -72
  94. package/src/services/aiAgent.cjs +136 -49
  95. package/src/services/aiProviders.cjs +193 -47
  96. package/src/services/aiRouter.cjs +137 -0
  97. package/src/services/aiTools.cjs +305 -125
  98. package/src/services/llmDiscovery.cjs +481 -0
  99. package/src/services/modelRegistry.cjs +412 -0
  100. package/src/utils/datetime.cjs +2 -2
  101. package/src/utils/lid-helper.esm.js +7 -7
  102. package/src/utils/logger.cjs +1 -1
  103. package/src/utils/markdownParser.cjs +159 -0
  104. package/src/utils/memory.cjs +43 -13
  105. package/src/utils/messageContent.cjs +40 -0
  106. package/src/utils/normalizeJid.esm.js +7 -7
  107. package/src/utils/numerals.cjs +75 -0
  108. package/src/utils/permissions.esm.js +10 -15
  109. package/src/utils/providerUrl.cjs +281 -0
  110. package/src/utils/recurrence.cjs +7 -11
  111. package/src/utils/sendBotMessage.esm.js +4 -14
  112. package/src/utils/statusMessage.cjs +5 -13
  113. package/src/utils/storage.cjs +10 -0
  114. package/src/utils/storage.esm.js +11 -0
  115. package/src/utils/textDecode.cjs +4 -9
  116. package/src/utils/thumbnail.cjs +8 -14
  117. package/src/utils/typing.esm.js +12 -12
  118. package/views/dashboard.ejs +134 -106
@@ -208,7 +208,9 @@ function listMemory({ scope = "chat", chatId } = {}) {
208
208
  */
209
209
  function addMemory({ scope = "chat", chatId, content, by, chatName } = {}) {
210
210
  const normalizedScope = normalizeScope(scope);
211
- const text = String(content || "").trim().slice(0, MAX_ENTRY_CHARS);
211
+ const text = String(content || "")
212
+ .trim()
213
+ .slice(0, MAX_ENTRY_CHARS);
212
214
  if (!text) throw new Error("مفيش محتوى أحفظه");
213
215
 
214
216
  const filePath = memoryFilePath(normalizedScope, chatId);
@@ -236,10 +238,7 @@ function addMemory({ scope = "chat", chatId, content, by, chatName } = {}) {
236
238
  ].join("\n");
237
239
 
238
240
  writeRaw(filePath, `${raw}${body}\n`);
239
- logger.info(
240
- { scope: normalizedScope, chatId, id },
241
- "[memory] saved a new entry"
242
- );
241
+ logger.info({ scope: normalizedScope, chatId, id }, "[memory] saved a new entry");
243
242
  return { id, content: text, scope: normalizedScope, file: filePath };
244
243
  }
245
244
 
@@ -292,9 +291,44 @@ function clearMemory({ scope = "chat", chatId } = {}) {
292
291
  return entries.length;
293
292
  }
294
293
 
294
+ /**
295
+ * Remove account-derived AI memory when the WhatsApp account is unlinked.
296
+ * A later pairing is a different security principal, so neither chat files nor
297
+ * global facts may silently cross that account boundary.
298
+ */
299
+ function clearAllMemoryFiles() {
300
+ let removed = 0;
301
+ try {
302
+ if (fs.existsSync(GLOBAL_FILE)) {
303
+ fs.unlinkSync(GLOBAL_FILE);
304
+ removed += 1;
305
+ }
306
+ } catch (err) {
307
+ logger.warn({ err }, "[memory] failed to remove global memory during unlink");
308
+ }
309
+
310
+ try {
311
+ if (fs.existsSync(CHATS_DIR)) {
312
+ for (const name of fs.readdirSync(CHATS_DIR)) {
313
+ if (!name.endsWith(".md")) continue;
314
+ const filePath = path.join(CHATS_DIR, name);
315
+ const stat = fs.lstatSync(filePath);
316
+ if (!stat.isFile() || stat.isSymbolicLink()) continue;
317
+ fs.unlinkSync(filePath);
318
+ removed += 1;
319
+ }
320
+ }
321
+ } catch (err) {
322
+ logger.warn({ err }, "[memory] failed to remove chat memory during unlink");
323
+ }
324
+ return removed;
325
+ }
326
+
295
327
  /** Case-insensitive substring search across one scope. */
296
328
  function searchMemory(query, { scope = "chat", chatId } = {}) {
297
- const needle = String(query || "").trim().toLowerCase();
329
+ const needle = String(query || "")
330
+ .trim()
331
+ .toLowerCase();
298
332
  const entries = listMemory({ scope, chatId });
299
333
  if (!needle) return entries;
300
334
  return entries.filter((entry) => entry.content.toLowerCase().includes(needle));
@@ -326,16 +360,11 @@ function buildMemoryContext(chatId, { limitChars = maxContextChars() } = {}) {
326
360
  const blocks = [];
327
361
 
328
362
  if (globalEntries.length) {
329
- blocks.push(
330
- `### ذاكرة عامة (global.md)\n${renderEntries(globalEntries, perScope)}`
331
- );
363
+ blocks.push(`### ذاكرة عامة (global.md)\n${renderEntries(globalEntries, perScope)}`);
332
364
  }
333
365
  if (chatEntries.length) {
334
366
  blocks.push(
335
- `### ذاكرة المحادثة دي (${safeFileName(chatId)}.md)\n${renderEntries(
336
- chatEntries,
337
- perScope
338
- )}`
367
+ `### ذاكرة المحادثة دي (${safeFileName(chatId)}.md)\n${renderEntries(chatEntries, perScope)}`,
339
368
  );
340
369
  }
341
370
 
@@ -378,6 +407,7 @@ module.exports = {
378
407
  addMemory,
379
408
  removeMemory,
380
409
  clearMemory,
410
+ clearAllMemoryFiles,
381
411
  searchMemory,
382
412
  buildMemoryContext,
383
413
  memoryStats,
@@ -0,0 +1,40 @@
1
+ // Unwrap WhatsApp envelopes the same way command routing already does for
2
+ // captions: view-once / ephemeral / document-caption / SKDM-first.
3
+
4
+ function unwrapMessage(message) {
5
+ let inner = message || {};
6
+ for (let i = 0; i < 6; i++) {
7
+ if (inner.viewOnceMessage?.message) inner = inner.viewOnceMessage.message;
8
+ else if (inner.viewOnceMessageV2?.message) inner = inner.viewOnceMessageV2.message;
9
+ else if (inner.viewOnceMessageV2Extension?.message) inner = inner.viewOnceMessageV2Extension.message;
10
+ else if (inner.ephemeralMessage?.message) inner = inner.ephemeralMessage.message;
11
+ else if (inner.documentWithCaptionMessage?.message) inner = inner.documentWithCaptionMessage.message;
12
+ else if (inner.editedMessage?.message) inner = inner.editedMessage.message;
13
+ else break;
14
+ }
15
+ return inner;
16
+ }
17
+
18
+ function visibleText(message) {
19
+ const inner = unwrapMessage(message);
20
+ return (
21
+ inner.conversation ||
22
+ inner.extendedTextMessage?.text ||
23
+ inner.imageMessage?.caption ||
24
+ inner.videoMessage?.caption ||
25
+ inner.documentMessage?.caption ||
26
+ ""
27
+ );
28
+ }
29
+
30
+ const MEDIA_KEYS = ["imageMessage", "videoMessage", "stickerMessage", "audioMessage", "documentMessage"];
31
+
32
+ function mediaType(message) {
33
+ const inner = unwrapMessage(message);
34
+ for (const key of MEDIA_KEYS) {
35
+ if (inner[key]) return key.replace("Message", "").toLowerCase();
36
+ }
37
+ return "";
38
+ }
39
+
40
+ module.exports = { unwrapMessage, visibleText, mediaType };
@@ -3,17 +3,17 @@ export default function normalizeJid(jid) {
3
3
  if (!jid) return jid;
4
4
 
5
5
  // V7: Handle LID format (@lid)
6
- if (jid.includes('@lid')) {
6
+ if (jid.includes("@lid")) {
7
7
  // LID format: keep as-is
8
- if (jid.includes(':')) {
9
- return jid.split(':')[0] + '@lid';
8
+ if (jid.includes(":")) {
9
+ return jid.split(":")[0] + "@lid";
10
10
  }
11
11
  return jid;
12
12
  }
13
13
 
14
14
  // Handle PN format (@s.whatsapp.net)
15
- if (jid.includes(':')) {
16
- return jid.split(':')[0] + '@' + jid.split('@')[1];
15
+ if (jid.includes(":")) {
16
+ return jid.split(":")[0] + "@" + jid.split("@")[1];
17
17
  }
18
18
 
19
19
  return jid;
@@ -22,13 +22,13 @@ export default function normalizeJid(jid) {
22
22
  // V7: Helper to check if JID is a Phone Number (PN)
23
23
  export function isPnJid(jid) {
24
24
  if (!jid) return false;
25
- return jid.includes('@s.whatsapp.net');
25
+ return jid.includes("@s.whatsapp.net");
26
26
  }
27
27
 
28
28
  // V7: Helper to check if JID is a LID
29
29
  export function isLidJid(jid) {
30
30
  if (!jid) return false;
31
- return jid.includes('@lid');
31
+ return jid.includes("@lid");
32
32
  }
33
33
 
34
34
  // V7: Helper to check if JID is a user (either PN or LID)
@@ -0,0 +1,75 @@
1
+ // Digit-script conversion: Western 0-9 <-> Eastern Arabic / Persian.
2
+
3
+ const ARABIC_DIGITS = ["٠", "١", "٢", "٣", "٤", "٥", "٦", "٧", "٨", "٩"];
4
+ const PERSIAN_DIGITS = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"];
5
+ const ENGLISH_DIGITS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
6
+
7
+ function convertNumerals(text, targetLang) {
8
+ if (!text || typeof text !== "string") return text;
9
+
10
+ if (targetLang === "ar") {
11
+ return text.replace(/[0-9]/g, (d) => ARABIC_DIGITS[parseInt(d, 10)]);
12
+ }
13
+
14
+ if (targetLang === "en") {
15
+ let result = text;
16
+ for (let i = 0; i < 10; i++) {
17
+ result = result.split(ARABIC_DIGITS[i]).join(ENGLISH_DIGITS[i]);
18
+ result = result.split(PERSIAN_DIGITS[i]).join(ENGLISH_DIGITS[i]);
19
+ }
20
+ return result;
21
+ }
22
+
23
+ return text;
24
+ }
25
+
26
+ function classifyNumerals(text) {
27
+ const source = String(text || "");
28
+ const hasEn = /[0-9]/.test(source);
29
+ const hasAr = /[٠-٩۰-۹]/.test(source);
30
+ if (hasEn && hasAr) return "mixed";
31
+ if (hasEn) return "en";
32
+ if (hasAr) return "ar";
33
+ return "none";
34
+ }
35
+
36
+ function oppositeNumeralLang(kind) {
37
+ if (kind === "en") return "ar";
38
+ if (kind === "ar") return "en";
39
+ return null;
40
+ }
41
+
42
+ function getQuotedText(msg) {
43
+ const quoted = msg.message?.extendedTextMessage?.contextInfo?.quotedMessage;
44
+ if (!quoted) return null;
45
+ return (
46
+ quoted.conversation ||
47
+ quoted.extendedTextMessage?.text ||
48
+ quoted.imageMessage?.caption ||
49
+ quoted.videoMessage?.caption ||
50
+ null
51
+ );
52
+ }
53
+
54
+ function isExplicitAr(token) {
55
+ const t = String(token || "")
56
+ .toLowerCase()
57
+ .trim();
58
+ return t === "ar" || t === "عربي" || t === "ع";
59
+ }
60
+
61
+ function isExplicitEn(token) {
62
+ const t = String(token || "")
63
+ .toLowerCase()
64
+ .trim();
65
+ return t === "en" || t === "انجليزي" || t === "إنجليزي";
66
+ }
67
+
68
+ module.exports = {
69
+ convertNumerals,
70
+ classifyNumerals,
71
+ oppositeNumeralLang,
72
+ getQuotedText,
73
+ isExplicitAr,
74
+ isExplicitEn,
75
+ };
@@ -32,18 +32,15 @@
32
32
  // sameUser(a, b)
33
33
  // True if two identifiers refer to the same person (LID/PN/phone equiv).
34
34
 
35
- import normalizeJid, {
36
- isLidJid,
37
- isPnJid,
38
- } from "./normalizeJid.esm.js";
35
+ import normalizeJid, { isLidJid, isPnJid } from "./normalizeJid.esm.js";
39
36
  import {
40
- getAllOwners,
41
- getAllBotAdmins,
42
- isUserOwner as dbIsOwner,
43
37
  isUserBotAdmin as dbIsBotAdmin,
44
- setUserRole,
38
+ isUserOwner as dbIsOwner,
39
+ getAllBotAdmins,
40
+ getAllOwners,
45
41
  getLidForPn,
46
42
  getPnForLid,
43
+ setUserRole,
47
44
  } from "./storage.esm.js";
48
45
 
49
46
  // Two rosters, same mechanics:
@@ -96,7 +93,9 @@ export function getBootstrapOwners() {
96
93
  export function bootstrapAdmins(values) {
97
94
  bootstrapAdminSet.clear();
98
95
  if (!Array.isArray(values)) return;
99
- values.forEach((value) => pushInto(bootstrapAdminSet, value));
96
+ values.forEach((value) => {
97
+ pushInto(bootstrapAdminSet, value);
98
+ });
100
99
  }
101
100
 
102
101
  export function addBootstrapAdmin(value) {
@@ -162,9 +161,7 @@ function hasRole(jid, { set, dbCheck, listAll }) {
162
161
  try {
163
162
  for (const entry of listAll()) {
164
163
  const entryPhone =
165
- digitsOnly(entry.phone) ||
166
- digitsOnly(entry.jid) ||
167
- digitsOnly(entry.lid);
164
+ digitsOnly(entry.phone) || digitsOnly(entry.jid) || digitsOnly(entry.lid);
168
165
  if (entryPhone && entryPhone === phone) return true;
169
166
  }
170
167
  } catch {}
@@ -308,9 +305,7 @@ export function isAdminInGroup(groupMetadata, jid) {
308
305
  return groupMetadata.participants.some((p) => {
309
306
  if (!["admin", "superadmin"].includes(p.admin)) return false;
310
307
  const participantIds = [p.id, p.lid, p.phoneNumber, p.jid].filter(Boolean);
311
- return participantIds.some(
312
- (pid) => normalizeJid(pid) === target || sameUser(pid, jid)
313
- );
308
+ return participantIds.some((pid) => normalizeJid(pid) === target || sameUser(pid, jid));
314
309
  });
315
310
  }
316
311
 
@@ -0,0 +1,281 @@
1
+ // Security boundary for operator-configured AI provider endpoints.
2
+ //
3
+ // Local loopback is an intentional product feature (Ollama / LM Studio and
4
+ // local compatibility gateways). Other private, link-local, metadata,
5
+ // multicast, and reserved destinations must never receive a vaulted API key.
6
+
7
+ const dns = require("node:dns").promises;
8
+ const net = require("node:net");
9
+ const { Agent, fetch: undiciFetch, Response: UndiciResponse } = require("undici");
10
+
11
+ function ipv4Bytes(host) {
12
+ const parts = String(host).split(".");
13
+ if (
14
+ parts.length !== 4 ||
15
+ !parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255)
16
+ ) {
17
+ return null;
18
+ }
19
+ return parts.map(Number);
20
+ }
21
+
22
+ function ipv6Bytes(host) {
23
+ let value = String(host).toLowerCase().split("%")[0];
24
+ const dotted = value.match(/(?:^|:)(\d+\.\d+\.\d+\.\d+)$/)?.[1];
25
+ if (dotted) {
26
+ const bytes = ipv4Bytes(dotted);
27
+ if (!bytes) return null;
28
+ value = value.slice(0, -dotted.length) + `${(bytes[0] << 8) | bytes[1]}:${
29
+ (bytes[2] << 8) | bytes[3]
30
+ }`;
31
+ }
32
+
33
+ const halves = value.split("::");
34
+ if (halves.length > 2) return null;
35
+ const left = halves[0] ? halves[0].split(":") : [];
36
+ const right = halves[1] ? halves[1].split(":") : [];
37
+ const missing = 8 - left.length - right.length;
38
+ if (missing < 0 || (halves.length === 1 && missing !== 0)) return null;
39
+ const words = [...left, ...Array(missing).fill("0"), ...right];
40
+ if (words.length !== 8 || !words.every((word) => /^[0-9a-f]{1,4}$/.test(word))) return null;
41
+
42
+ const out = [];
43
+ for (const word of words) {
44
+ const number = Number.parseInt(word, 16);
45
+ out.push(number >> 8, number & 0xff);
46
+ }
47
+ return out;
48
+ }
49
+
50
+ function isIpv4Loopback(bytes) {
51
+ return bytes?.[0] === 127;
52
+ }
53
+
54
+ function isForbiddenIpv4(bytes, { allowLoopback = false } = {}) {
55
+ if (!bytes) return true;
56
+ const [a, b] = bytes;
57
+ if (isIpv4Loopback(bytes)) return !allowLoopback;
58
+ return (
59
+ a === 0 ||
60
+ a === 10 ||
61
+ (a === 100 && b >= 64 && b <= 127) ||
62
+ (a === 169 && b === 254) ||
63
+ (a === 172 && b >= 16 && b <= 31) ||
64
+ (a === 192 && b === 168) ||
65
+ (a === 198 && (b === 18 || b === 19)) ||
66
+ a >= 224
67
+ );
68
+ }
69
+
70
+ function embeddedIpv4(bytes) {
71
+ if (!bytes || bytes.length !== 16) return null;
72
+ const prefixIsZero = bytes.slice(0, 10).every((byte) => byte === 0);
73
+ if (prefixIsZero && bytes[10] === 0xff && bytes[11] === 0xff) return bytes.slice(12);
74
+ const translatedPrefix =
75
+ bytes.slice(0, 8).every((byte) => byte === 0) &&
76
+ bytes[8] === 0xff &&
77
+ bytes[9] === 0xff &&
78
+ bytes[10] === 0 &&
79
+ bytes[11] === 0;
80
+ if (translatedPrefix) return bytes.slice(12);
81
+
82
+ if (
83
+ bytes[0] === 0x00 &&
84
+ bytes[1] === 0x64 &&
85
+ bytes[2] === 0xff &&
86
+ bytes[3] === 0x9b &&
87
+ (bytes.slice(4, 12).every((byte) => byte === 0) ||
88
+ (bytes[4] === 0 && bytes[5] === 1 && bytes.slice(6, 12).every((byte) => byte === 0)))
89
+ ) {
90
+ return bytes.slice(12);
91
+ }
92
+ return null;
93
+ }
94
+
95
+ function isIpv6Loopback(bytes) {
96
+ return bytes?.slice(0, 15).every((byte) => byte === 0) && bytes[15] === 1;
97
+ }
98
+
99
+ function isForbiddenIpv6(bytes, { allowLoopback = false } = {}) {
100
+ if (!bytes) return true;
101
+ if (isIpv6Loopback(bytes)) return !allowLoopback;
102
+ if (bytes.every((byte) => byte === 0)) return true;
103
+
104
+ const embedded = embeddedIpv4(bytes);
105
+ if (embedded) return isForbiddenIpv4(embedded, { allowLoopback });
106
+
107
+ const first = bytes[0];
108
+ const second = bytes[1];
109
+ if ((first & 0xfe) === 0xfc) return true;
110
+ if (first === 0xfe && (second & 0xc0) === 0x80) return true;
111
+ if (first === 0xfe && (second & 0xc0) === 0xc0) return true;
112
+ if (first === 0xff) return true;
113
+ if (first === 0x20 && second === 0x02) return true;
114
+ return false;
115
+ }
116
+
117
+ function isForbiddenIp(host, options = {}) {
118
+ const family = net.isIP(host);
119
+ if (family === 4) return isForbiddenIpv4(ipv4Bytes(host), options);
120
+ if (family === 6) return isForbiddenIpv6(ipv6Bytes(host), options);
121
+ return false;
122
+ }
123
+
124
+ function parseProviderUrl(raw, { allowLoopback = true, allowQuery = false } = {}) {
125
+ const text = String(raw ?? "").trim();
126
+ if (!text) throw new Error("Base URL is required");
127
+
128
+ let url;
129
+ try {
130
+ url = new URL(text);
131
+ } catch {
132
+ throw new Error("Base URL must be a valid http(s) URL");
133
+ }
134
+ if (!/^https?:$/.test(url.protocol)) throw new Error("Base URL must be http or https");
135
+ if (!url.hostname) throw new Error("Base URL must include a host");
136
+ if (url.username || url.password) throw new Error("Base URL cannot contain credentials");
137
+ if ((!allowQuery && url.search) || url.hash) {
138
+ throw new Error("Base URL cannot contain a query or fragment");
139
+ }
140
+
141
+ const host = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
142
+ const isLoopback =
143
+ host === "localhost" ||
144
+ (net.isIP(host) === 4 && isIpv4Loopback(ipv4Bytes(host))) ||
145
+ (net.isIP(host) === 6 && isIpv6Loopback(ipv6Bytes(host)));
146
+
147
+ if (/^(metadata|metadata\.google\.internal|instance-data)$/i.test(host)) {
148
+ throw new Error("Base URL cannot point at cloud metadata");
149
+ }
150
+ if (net.isIP(host) && isForbiddenIp(host, { allowLoopback })) {
151
+ throw new Error("Base URL cannot point at a private, link-local, or reserved address");
152
+ }
153
+ if (isLoopback && !allowLoopback) throw new Error("Loopback provider URLs are not allowed");
154
+ if (url.protocol === "http:" && !isLoopback) {
155
+ throw new Error("Public provider URLs must use HTTPS");
156
+ }
157
+ return url;
158
+ }
159
+
160
+ function assertProviderBaseUrl(raw, options = {}) {
161
+ const url = parseProviderUrl(raw, options);
162
+ return url.toString().replace(/\/+$/, "");
163
+ }
164
+
165
+ async function resolveProviderRequestUrl(raw, options = {}) {
166
+ const url = parseProviderUrl(raw, { ...options, allowQuery: true });
167
+ const host = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
168
+ if (net.isIP(host)) {
169
+ return { url, host, addresses: [{ address: host, family: net.isIP(host) }] };
170
+ }
171
+
172
+ let answers;
173
+ try {
174
+ answers = await dns.lookup(host, { all: true, verbatim: true });
175
+ } catch {
176
+ throw new Error("Provider host could not be resolved");
177
+ }
178
+ const explicitLocalhost = host === "localhost";
179
+ if (
180
+ !answers.length ||
181
+ answers.some(({ address }) =>
182
+ isForbiddenIp(address, { allowLoopback: explicitLocalhost && options.allowLoopback !== false }),
183
+ )
184
+ ) {
185
+ throw new Error("Provider host resolves to a private, link-local, or reserved address");
186
+ }
187
+ return { url, host, addresses: answers };
188
+ }
189
+
190
+ async function assertProviderRequestUrl(raw, options = {}) {
191
+ const { url } = await resolveProviderRequestUrl(raw, options);
192
+ return url;
193
+ }
194
+
195
+ function pinnedLookup(expectedHost, addresses) {
196
+ return (hostname, lookupOptions, callback) => {
197
+ if (String(hostname).toLowerCase() !== expectedHost) {
198
+ const error = new Error("Provider connection attempted an unexpected host");
199
+ error.code = "EAI_FAIL";
200
+ return callback(error);
201
+ }
202
+ const requestedFamily =
203
+ typeof lookupOptions === "number" ? lookupOptions : lookupOptions?.family;
204
+ const candidates = requestedFamily
205
+ ? addresses.filter(({ family }) => family === requestedFamily)
206
+ : addresses;
207
+ if (!candidates.length) {
208
+ const error = new Error("Provider host has no address in the requested family");
209
+ error.code = "EAI_ADDRFAMILY";
210
+ return callback(error);
211
+ }
212
+ if (typeof lookupOptions === "object" && lookupOptions?.all) {
213
+ return callback(null, candidates.map(({ address, family }) => ({ address, family })));
214
+ }
215
+ return callback(null, candidates[0].address, candidates[0].family);
216
+ };
217
+ }
218
+
219
+ function responseWithDispatcherCleanup(response, dispatcher) {
220
+ let released = false;
221
+ const release = (destroy = false) => {
222
+ if (released) return;
223
+ released = true;
224
+ const done = destroy ? dispatcher.destroy() : dispatcher.close();
225
+ Promise.resolve(done).catch(() => dispatcher.destroy());
226
+ };
227
+
228
+ if (!response.body) {
229
+ release();
230
+ return response;
231
+ }
232
+
233
+ const reader = response.body.getReader();
234
+ const body = new ReadableStream({
235
+ async pull(controller) {
236
+ try {
237
+ const chunk = await reader.read();
238
+ if (chunk.done) {
239
+ controller.close();
240
+ release();
241
+ } else {
242
+ controller.enqueue(chunk.value);
243
+ }
244
+ } catch (error) {
245
+ controller.error(error);
246
+ release(true);
247
+ }
248
+ },
249
+ async cancel(reason) {
250
+ try {
251
+ await reader.cancel(reason);
252
+ } finally {
253
+ release(true);
254
+ }
255
+ },
256
+ });
257
+ return new UndiciResponse(body, {
258
+ status: response.status,
259
+ statusText: response.statusText,
260
+ headers: response.headers,
261
+ });
262
+ }
263
+
264
+ async function safeProviderFetch(raw, init = {}, options = {}) {
265
+ const { url, host, addresses } = await resolveProviderRequestUrl(raw, options);
266
+ const dispatcher = new Agent({ connect: { lookup: pinnedLookup(host, addresses) } });
267
+ try {
268
+ const response = await undiciFetch(url, { ...init, redirect: "manual", dispatcher });
269
+ return responseWithDispatcherCleanup(response, dispatcher);
270
+ } catch (error) {
271
+ Promise.resolve(dispatcher.destroy()).catch(() => {});
272
+ throw error;
273
+ }
274
+ }
275
+
276
+ module.exports = {
277
+ assertProviderBaseUrl,
278
+ assertProviderRequestUrl,
279
+ safeProviderFetch,
280
+ isForbiddenIp,
281
+ };
@@ -44,15 +44,7 @@ const DAY_ALIASES = new Map([
44
44
  ["سبت", 6],
45
45
  ]);
46
46
 
47
- const WEEKDAYS = [
48
- "Sunday",
49
- "Monday",
50
- "Tuesday",
51
- "Wednesday",
52
- "Thursday",
53
- "Friday",
54
- "Saturday",
55
- ];
47
+ const WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
56
48
 
57
49
  function normalizeDay(value) {
58
50
  return String(value ?? "")
@@ -84,7 +76,9 @@ function parseTime(value) {
84
76
  }
85
77
 
86
78
  function parseRecurringArgs(args) {
87
- const type = String(args[0] ?? "").trim().toLowerCase();
79
+ const type = String(args[0] ?? "")
80
+ .trim()
81
+ .toLowerCase();
88
82
 
89
83
  if (type === "daily") {
90
84
  const time = parseTime(args[1]);
@@ -120,7 +114,9 @@ function parseRecurringArgs(args) {
120
114
 
121
115
  function describeScheduledJob(job, timezone) {
122
116
  if (job.type === "recurring") {
123
- const parts = String(job.cronString ?? "").trim().split(/\s+/);
117
+ const parts = String(job.cronString ?? "")
118
+ .trim()
119
+ .split(/\s+/);
124
120
  if (parts.length === 5) {
125
121
  const [minuteText, hourText, dayOfMonth, month, dayText] = parts;
126
122
  const minute = Number(minuteText);
@@ -69,13 +69,7 @@ async function safePresence(sock, jid, presence) {
69
69
  * @returns {Promise<object>} Baileys send result
70
70
  */
71
71
  export async function sendBotMessage(sock, jid, content, options = {}) {
72
- const {
73
- replyTo = null,
74
- quoted = null,
75
- quote = true,
76
- typing = true,
77
- delayMs,
78
- } = options;
72
+ const { replyTo = null, quoted = null, quote = true, typing = true, delayMs } = options;
79
73
 
80
74
  // Respect the per-chat cooldown so multiple replies don't pile up instantly.
81
75
  const now = Date.now();
@@ -109,7 +103,7 @@ export async function sendBotMessage(sock, jid, content, options = {}) {
109
103
  } catch (err) {
110
104
  logger.error(
111
105
  { err, jid, contentKeys: Object.keys(content || {}) },
112
- "[sendBotMessage] sock.sendMessage failed"
106
+ "[sendBotMessage] sock.sendMessage failed",
113
107
  );
114
108
  throw err;
115
109
  }
@@ -127,12 +121,8 @@ export async function sendBotMessage(sock, jid, content, options = {}) {
127
121
  */
128
122
  export async function sendBotError(sock, jid, err, arabicLead = "حدث خطأ", options = {}) {
129
123
  const name = err?.name || (typeof err === "string" ? "Error" : "Error");
130
- const message =
131
- err?.message || (typeof err === "string" ? err : JSON.stringify(err));
132
- const text = `❌ *${arabicLead}*\n\n*النوع:* \`${name}\`\n*التفاصيل:* ${message}`.slice(
133
- 0,
134
- 1500
135
- );
124
+ const message = err?.message || (typeof err === "string" ? err : JSON.stringify(err));
125
+ const text = `❌ *${arabicLead}*\n\n*النوع:* \`${name}\`\n*التفاصيل:* ${message}`.slice(0, 1500);
136
126
  return sendBotMessage(sock, jid, { text }, options);
137
127
  }
138
128