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.
- package/README.md +11 -7
- package/app.cjs +42 -45
- package/package.json +9 -1
- package/public/dashboard/assets/index-C1b7ijfu.js +391 -0
- package/public/dashboard/assets/index-pYaivHON.css +1 -0
- package/public/dashboard/index.html +19 -0
- package/public/dashboard.css +391 -66
- package/public/dashboard.js +593 -42
- package/scheduler.cjs +9 -14
- package/src/auth/use-database-auth-state.js +2 -4
- package/src/bootstrap/core.js +2 -5
- package/src/bootstrap/panel.js +10 -5
- package/src/cli.js +3 -5
- package/src/commands/autoschedule.cjs +1 -4
- package/src/commands/blacklist.cjs +22 -20
- package/src/commands/block.cjs +2 -4
- package/src/commands/calc.cjs +9 -16
- package/src/commands/digit.cjs +97 -0
- package/src/commands/gemini.cjs +147 -161
- package/src/commands/group/add.cjs +7 -16
- package/src/commands/group/antiSpam.cjs +5 -11
- package/src/commands/group/antilink.cjs +34 -28
- package/src/commands/group/approveall.cjs +3 -11
- package/src/commands/group/clearwarns.cjs +2 -5
- package/src/commands/group/demote.cjs +3 -8
- package/src/commands/group/kick.cjs +12 -15
- package/src/commands/group/media.cjs +17 -24
- package/src/commands/group/members.cjs +1 -2
- package/src/commands/group/promote.cjs +4 -12
- package/src/commands/group/removeall.cjs +2 -5
- package/src/commands/group/setpp.cjs +1 -1
- package/src/commands/group/setrules.cjs +2 -8
- package/src/commands/group/setwarn.cjs +4 -2
- package/src/commands/group/tagadmins.cjs +2 -5
- package/src/commands/group/warn.cjs +26 -24
- package/src/commands/group/warns.cjs +1 -2
- package/src/commands/group/welcome.cjs +13 -17
- package/src/commands/group.cjs +7 -10
- package/src/commands/help.cjs +6 -12
- package/src/commands/lang.cjs +104 -0
- package/src/commands/listschedules.cjs +2 -6
- package/src/commands/loop.cjs +6 -13
- package/src/commands/memory.cjs +19 -32
- package/src/commands/mod.cjs +97 -35
- package/src/commands/perm.cjs +18 -56
- package/src/commands/ping.cjs +1 -5
- package/src/commands/poll.cjs +1 -1
- package/src/commands/rand.cjs +2 -5
- package/src/commands/schedule.cjs +4 -8
- package/src/commands/score.cjs +8 -2
- package/src/commands/setprefix.cjs +2 -2
- package/src/commands/shortlink.cjs +7 -15
- package/src/commands/shutdown.cjs +1 -1
- package/src/commands/sticker.cjs +222 -0
- package/src/commands/stt.cjs +136 -68
- package/src/commands/todo.cjs +9 -15
- package/src/commands/tts.cjs +10 -16
- package/src/commands/unblock.cjs +2 -6
- package/src/commands/weather.cjs +5 -12
- package/src/config/ai-persona.md +5 -3
- package/src/config/available-models.json +1156 -0
- package/src/config/baileys.config.js +2 -1
- package/src/config/brand.esm.js +1 -10
- package/src/config/constants.js +1 -1
- package/src/config/defaults.cjs +66 -66
- package/src/config/lock.cjs +2 -2
- package/src/config/modelCapabilities.cjs +172 -0
- package/src/config/runtime-config.cjs +8 -13
- package/src/config/secrets.cjs +18 -10
- package/src/config/settings.cjs +94 -10
- package/src/config/vault.cjs +107 -0
- package/src/core/connection.js +13 -12
- package/src/core/events.js +41 -23
- package/src/core/proxy.js +7 -7
- package/src/core/session.js +91 -48
- package/src/core/socket.js +12 -15
- package/src/db/db.cjs +14 -6
- package/src/db/store.cjs +221 -87
- package/src/domain/caddy.js +1 -1
- package/src/domain/command.js +40 -19
- package/src/domain/detect.js +1 -1
- package/src/domain/system.js +2 -5
- package/src/handlers/command.handler.js +22 -43
- package/src/handlers/group.handler.js +13 -13
- package/src/handlers/message.handler.js +11 -21
- package/src/index.js +8 -4
- package/src/middleware/antispam.middleware.js +46 -23
- package/src/middleware/blacklist.middleware.js +14 -9
- package/src/middleware/forward-tracking.middleware.js +3 -2
- package/src/middleware/permissions.middleware.js +7 -10
- package/src/panel/login-throttle.cjs +43 -0
- package/src/panel/session-auth.cjs +17 -0
- package/src/routes/dashboard.api.esm.js +394 -72
- package/src/services/aiAgent.cjs +136 -49
- package/src/services/aiProviders.cjs +193 -47
- package/src/services/aiRouter.cjs +137 -0
- package/src/services/aiTools.cjs +305 -125
- package/src/services/llmDiscovery.cjs +481 -0
- package/src/services/modelRegistry.cjs +412 -0
- package/src/utils/datetime.cjs +2 -2
- package/src/utils/lid-helper.esm.js +7 -7
- package/src/utils/logger.cjs +1 -1
- package/src/utils/markdownParser.cjs +159 -0
- package/src/utils/memory.cjs +43 -13
- package/src/utils/messageContent.cjs +40 -0
- package/src/utils/normalizeJid.esm.js +7 -7
- package/src/utils/numerals.cjs +75 -0
- package/src/utils/permissions.esm.js +10 -15
- package/src/utils/providerUrl.cjs +281 -0
- package/src/utils/recurrence.cjs +7 -11
- package/src/utils/sendBotMessage.esm.js +4 -14
- package/src/utils/statusMessage.cjs +5 -13
- package/src/utils/storage.cjs +10 -0
- package/src/utils/storage.esm.js +11 -0
- package/src/utils/textDecode.cjs +4 -9
- package/src/utils/thumbnail.cjs +8 -14
- package/src/utils/typing.esm.js +12 -12
- package/views/dashboard.ejs +134 -106
package/src/utils/memory.cjs
CHANGED
|
@@ -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 || "")
|
|
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 || "")
|
|
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(
|
|
6
|
+
if (jid.includes("@lid")) {
|
|
7
7
|
// LID format: keep as-is
|
|
8
|
-
if (jid.includes(
|
|
9
|
-
return jid.split(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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) =>
|
|
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
|
+
};
|
package/src/utils/recurrence.cjs
CHANGED
|
@@ -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] ?? "")
|
|
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 ?? "")
|
|
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
|
-
|
|
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
|
|