levix-bot 3.3.0-rc.2 → 4.0.0
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 +13 -9
- package/app.cjs +49 -46
- package/package.json +9 -1
- package/public/dashboard/assets/index-BBBXWzM0.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 -128
- 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/scheduler.cjs
CHANGED
|
@@ -78,7 +78,7 @@ function initializeScheduledJobs(sock) {
|
|
|
78
78
|
logger.info("[Scheduler] Running periodic media cleanup...");
|
|
79
79
|
trimMediaDirectory();
|
|
80
80
|
},
|
|
81
|
-
{ timezone: settings.get("bot_timezone") }
|
|
81
|
+
{ timezone: settings.get("bot_timezone") },
|
|
82
82
|
);
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -97,7 +97,7 @@ function scheduleAt(whenMs, run) {
|
|
|
97
97
|
const remaining = whenMs - Date.now();
|
|
98
98
|
timer = setTimeout(
|
|
99
99
|
remaining > MAX_TIMEOUT_MS ? arm : run,
|
|
100
|
-
Math.min(Math.max(remaining, 0), MAX_TIMEOUT_MS)
|
|
100
|
+
Math.min(Math.max(remaining, 0), MAX_TIMEOUT_MS),
|
|
101
101
|
);
|
|
102
102
|
};
|
|
103
103
|
arm();
|
|
@@ -140,7 +140,7 @@ function recordDelivery(jobId, status, runAt, error = null) {
|
|
|
140
140
|
} catch (stateError) {
|
|
141
141
|
logger.error(
|
|
142
142
|
{ err: stateError, jobId },
|
|
143
|
-
"[Scheduler] failed to record scheduled delivery state"
|
|
143
|
+
"[Scheduler] failed to record scheduled delivery state",
|
|
144
144
|
);
|
|
145
145
|
}
|
|
146
146
|
}
|
|
@@ -164,10 +164,7 @@ async function deliverScheduledJob(sock, job) {
|
|
|
164
164
|
} catch (error) {
|
|
165
165
|
const message = deliveryErrorMessage(error);
|
|
166
166
|
recordDelivery(job.id, "failed", runAt, message);
|
|
167
|
-
logger.error(
|
|
168
|
-
{ err: error, jobId: job.id },
|
|
169
|
-
"[Scheduler] failed to send scheduled message"
|
|
170
|
-
);
|
|
167
|
+
logger.error({ err: error, jobId: job.id }, "[Scheduler] failed to send scheduled message");
|
|
171
168
|
return { ok: false, reason: "delivery_failed", error: message };
|
|
172
169
|
} finally {
|
|
173
170
|
deliveriesInFlight.delete(job.id);
|
|
@@ -183,7 +180,7 @@ async function executeScheduledJob(sock, job) {
|
|
|
183
180
|
} catch (error) {
|
|
184
181
|
logger.error(
|
|
185
182
|
{ err: error, jobId: job.id },
|
|
186
|
-
"[Scheduler] failed to record one-off schedule status"
|
|
183
|
+
"[Scheduler] failed to record one-off schedule status",
|
|
187
184
|
);
|
|
188
185
|
}
|
|
189
186
|
}
|
|
@@ -197,7 +194,7 @@ function scheduleNewJob(sock, job) {
|
|
|
197
194
|
if (job.type === "recurring") {
|
|
198
195
|
if (!job.cronString || !cron.validate(job.cronString)) {
|
|
199
196
|
logger.error(
|
|
200
|
-
`[Scheduler] Job ${job.id} has an invalid cron string (${job.cronString}) — skipping it
|
|
197
|
+
`[Scheduler] Job ${job.id} has an invalid cron string (${job.cronString}) — skipping it`,
|
|
201
198
|
);
|
|
202
199
|
updateJobStatus(job.id, "invalid");
|
|
203
200
|
return false;
|
|
@@ -207,7 +204,7 @@ function scheduleNewJob(sock, job) {
|
|
|
207
204
|
job.id,
|
|
208
205
|
cron.schedule(job.cronString, () => executeScheduledJob(sock, job), {
|
|
209
206
|
timezone: settings.get("bot_timezone"),
|
|
210
|
-
})
|
|
207
|
+
}),
|
|
211
208
|
);
|
|
212
209
|
} else {
|
|
213
210
|
const when = new Date(job.date).getTime();
|
|
@@ -233,7 +230,7 @@ function scheduleNewJob(sock, job) {
|
|
|
233
230
|
} finally {
|
|
234
231
|
runningTasks.delete(job.id);
|
|
235
232
|
}
|
|
236
|
-
})
|
|
233
|
+
}),
|
|
237
234
|
);
|
|
238
235
|
}
|
|
239
236
|
|
|
@@ -247,9 +244,7 @@ async function retryScheduledJob(sock, jobId) {
|
|
|
247
244
|
|
|
248
245
|
const retryable =
|
|
249
246
|
job.status === "failed" ||
|
|
250
|
-
(job.type === "recurring" &&
|
|
251
|
-
job.status === "active" &&
|
|
252
|
-
job.lastDeliveryStatus === "failed");
|
|
247
|
+
(job.type === "recurring" && job.status === "active" && job.lastDeliveryStatus === "failed");
|
|
253
248
|
if (!retryable) return { ok: false, reason: "not_failed", job };
|
|
254
249
|
|
|
255
250
|
const result = await executeScheduledJob(sock, job);
|
|
@@ -55,7 +55,7 @@ export async function useDatabaseAuthState(storageType = null) {
|
|
|
55
55
|
const key = `${type}-${id}`;
|
|
56
56
|
const val = await readData(key);
|
|
57
57
|
if (val !== null && val !== undefined) out[id] = val;
|
|
58
|
-
})
|
|
58
|
+
}),
|
|
59
59
|
);
|
|
60
60
|
return out;
|
|
61
61
|
},
|
|
@@ -63,9 +63,7 @@ export async function useDatabaseAuthState(storageType = null) {
|
|
|
63
63
|
set: async (data) => {
|
|
64
64
|
try {
|
|
65
65
|
const topKeys = Object.keys(data);
|
|
66
|
-
const looksLikeTyped = topKeys.some(
|
|
67
|
-
(k) => typeof data[k] === "object" && data[k] !== null
|
|
68
|
-
);
|
|
66
|
+
const looksLikeTyped = topKeys.some((k) => typeof data[k] === "object" && data[k] !== null);
|
|
69
67
|
|
|
70
68
|
if (looksLikeTyped) {
|
|
71
69
|
for (const type of topKeys) {
|
package/src/bootstrap/core.js
CHANGED
|
@@ -13,13 +13,10 @@
|
|
|
13
13
|
|
|
14
14
|
import { createRequire } from "module";
|
|
15
15
|
import { WhatsAppSession } from "../core/session.js";
|
|
16
|
-
import { loadCommands, getLoadedCommands } from "../handlers/command.handler.js";
|
|
17
16
|
import { initStore } from "../db/store.esm.js";
|
|
17
|
+
import { getLoadedCommands, loadCommands } from "../handlers/command.handler.js";
|
|
18
18
|
import { deleteQrCode } from "../utils/storage.esm.js";
|
|
19
|
-
import {
|
|
20
|
-
hasPairedCredentials,
|
|
21
|
-
sessionStartupPolicy,
|
|
22
|
-
} from "./session-startup-policy.js";
|
|
19
|
+
import { hasPairedCredentials, sessionStartupPolicy } from "./session-startup-policy.js";
|
|
23
20
|
|
|
24
21
|
const require = createRequire(import.meta.url);
|
|
25
22
|
const logger = require("../utils/logger.cjs");
|
package/src/bootstrap/panel.js
CHANGED
|
@@ -40,6 +40,7 @@ export async function bootstrapPanel({ core } = {}) {
|
|
|
40
40
|
requireLoginApi,
|
|
41
41
|
noStore,
|
|
42
42
|
installFinalHandlers,
|
|
43
|
+
disconnectInvalidPanelSockets,
|
|
43
44
|
} = require("../../app.cjs");
|
|
44
45
|
|
|
45
46
|
const { default: dashboardApiRoutes, setSession } = await import(
|
|
@@ -54,7 +55,13 @@ export async function bootstrapPanel({ core } = {}) {
|
|
|
54
55
|
// Everything the bot reports goes out to the browsers watching the panel.
|
|
55
56
|
// One direction only: a browser attaching or leaving cannot reach back and
|
|
56
57
|
// change the WhatsApp session's lifecycle. See src/core/session.js.
|
|
57
|
-
attach((event, payload) =>
|
|
58
|
+
attach((event, payload) => {
|
|
59
|
+
// A CLI reset-password runs in another process, so the in-memory session
|
|
60
|
+
// store cannot receive a destroy call. Revalidate the password epoch before
|
|
61
|
+
// every server push; stale sockets are removed before QR/pairing data moves.
|
|
62
|
+
disconnectInvalidPanelSockets();
|
|
63
|
+
io.emit(event, payload);
|
|
64
|
+
});
|
|
58
65
|
|
|
59
66
|
// The routes read the live socket off the session manager, so a reconnect
|
|
60
67
|
// needs no re-wiring here.
|
|
@@ -77,7 +84,7 @@ export async function bootstrapPanel({ core } = {}) {
|
|
|
77
84
|
`Port ${port} is already taken.\n\n` +
|
|
78
85
|
` Levix is probably already running — open http://localhost:${port}/ and\n` +
|
|
79
86
|
` check. If something else owns the port, change it in the control panel\n` +
|
|
80
|
-
` under Settings -> Server, or start with a different one
|
|
87
|
+
` under Settings -> Server, or start with a different one.`,
|
|
81
88
|
);
|
|
82
89
|
friendly.code = "EADDRINUSE";
|
|
83
90
|
return reject(friendly);
|
|
@@ -119,9 +126,7 @@ export function resolveBindAddress() {
|
|
|
119
126
|
/** The address a person should type, given how this install is reachable. */
|
|
120
127
|
export function panelUrl({ port, firstRun = false } = {}) {
|
|
121
128
|
const domain = settings.get("public_domain");
|
|
122
|
-
const host =
|
|
123
|
-
domain ||
|
|
124
|
-
(isAndroidHost() ? "127.0.0.1" : "localhost");
|
|
129
|
+
const host = domain || (isAndroidHost() ? "127.0.0.1" : "localhost");
|
|
125
130
|
const base = domain ? `https://${host}` : `http://${host}:${port}`;
|
|
126
131
|
return firstRun ? `${base}/setup` : base;
|
|
127
132
|
}
|
package/src/cli.js
CHANGED
|
@@ -27,7 +27,7 @@ function checkNode() {
|
|
|
27
27
|
"",
|
|
28
28
|
" Get it from https://nodejs.org (pick the LTS build).",
|
|
29
29
|
"",
|
|
30
|
-
].join("\n")
|
|
30
|
+
].join("\n"),
|
|
31
31
|
);
|
|
32
32
|
process.exit(1);
|
|
33
33
|
}
|
|
@@ -54,7 +54,7 @@ function help() {
|
|
|
54
54
|
" Everything else is configured from the control panel itself,",
|
|
55
55
|
" which prints its address when the bot starts.",
|
|
56
56
|
"",
|
|
57
|
-
].join("\n")
|
|
57
|
+
].join("\n"),
|
|
58
58
|
);
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -110,9 +110,7 @@ export async function main(argv = process.argv) {
|
|
|
110
110
|
const { close } = await import("./db/db.cjs");
|
|
111
111
|
store.deleteBotSetting("auth:password");
|
|
112
112
|
close();
|
|
113
|
-
console.log(
|
|
114
|
-
"\n Password cleared. Start the bot and open the panel to choose a new one.\n"
|
|
115
|
-
);
|
|
113
|
+
console.log("\n Password cleared. Start the bot and open the panel to choose a new one.\n");
|
|
116
114
|
return 0;
|
|
117
115
|
}
|
|
118
116
|
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
const {
|
|
2
|
-
scheduleNewJob,
|
|
3
|
-
saveScheduledJob,
|
|
4
|
-
} = require("../../scheduler.cjs");
|
|
1
|
+
const { scheduleNewJob, saveScheduledJob } = require("../../scheduler.cjs");
|
|
5
2
|
const { randomUUID } = require("node:crypto");
|
|
6
3
|
const { defaultTimezone } = require("../utils/datetime.cjs");
|
|
7
4
|
const { parseRecurringArgs } = require("../utils/recurrence.cjs");
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
const { getGroupSettings, saveGroupSettings } = require("../utils/storage.cjs");
|
|
3
3
|
const logger = require("../utils/logger.cjs");
|
|
4
4
|
const normalizeJid = require("../utils/normalizeJid.esm.js").default;
|
|
5
|
+
const { isAdminInGroupSync, sameUserSync } = require("../utils/permissions.cjs");
|
|
6
|
+
|
|
7
|
+
function sameIdentity(a, b) {
|
|
8
|
+
return normalizeJid(a) === normalizeJid(b) || sameUserSync(a, b);
|
|
9
|
+
}
|
|
5
10
|
|
|
6
11
|
module.exports = {
|
|
7
12
|
name: "blacklist",
|
|
@@ -65,14 +70,20 @@ module.exports = {
|
|
|
65
70
|
const normalizedTargetJid = normalizeJid(targetJid);
|
|
66
71
|
|
|
67
72
|
if (action === "add") {
|
|
68
|
-
const
|
|
69
|
-
(
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
const targetIsAdmin =
|
|
74
|
+
isAdminInGroupSync(groupMetadata, normalizedTargetJid) ||
|
|
75
|
+
groupMetadata.participants.some(
|
|
76
|
+
(participant) =>
|
|
77
|
+
participant.admin &&
|
|
78
|
+
[participant.id, participant.lid, participant.phoneNumber, participant.jid]
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.some((id) => sameIdentity(id, normalizedTargetJid)),
|
|
81
|
+
);
|
|
82
|
+
if (targetIsAdmin)
|
|
72
83
|
return await sock.sendMessage(groupId, {
|
|
73
84
|
text: "لا يمكن حظر المشرفين.",
|
|
74
85
|
});
|
|
75
|
-
if (settings.blacklist.
|
|
86
|
+
if (settings.blacklist.some((jid) => sameIdentity(jid, normalizedTargetJid)))
|
|
76
87
|
return await sock.sendMessage(groupId, {
|
|
77
88
|
text: `العضو @${normalizedTargetJid.split("@")[0]} محظور بالفعل.`,
|
|
78
89
|
mentions: [normalizedTargetJid],
|
|
@@ -80,37 +91,28 @@ module.exports = {
|
|
|
80
91
|
|
|
81
92
|
settings.blacklist.push(normalizedTargetJid);
|
|
82
93
|
await sock.sendMessage(groupId, {
|
|
83
|
-
text: `🚫 تم إضافة @${
|
|
84
|
-
normalizedTargetJid.split("@")[0]
|
|
85
|
-
} إلى القائمة السوداء بنجاح.`,
|
|
94
|
+
text: `🚫 تم إضافة @${normalizedTargetJid.split("@")[0]} إلى القائمة السوداء بنجاح.`,
|
|
86
95
|
mentions: [normalizedTargetJid],
|
|
87
96
|
});
|
|
88
97
|
} else if (action === "remove") {
|
|
89
|
-
if (!settings.blacklist.
|
|
98
|
+
if (!settings.blacklist.some((jid) => sameIdentity(jid, normalizedTargetJid)))
|
|
90
99
|
return await sock.sendMessage(groupId, {
|
|
91
|
-
text: `العضو @${
|
|
92
|
-
normalizedTargetJid.split("@")[0]
|
|
93
|
-
} ليس في القائمة السوداء أصلاً.`,
|
|
100
|
+
text: `العضو @${normalizedTargetJid.split("@")[0]} ليس في القائمة السوداء أصلاً.`,
|
|
94
101
|
mentions: [normalizedTargetJid],
|
|
95
102
|
});
|
|
96
103
|
|
|
97
104
|
settings.blacklist = settings.blacklist.filter(
|
|
98
|
-
(jid) => jid
|
|
105
|
+
(jid) => !sameIdentity(jid, normalizedTargetJid),
|
|
99
106
|
);
|
|
100
107
|
await sock.sendMessage(groupId, {
|
|
101
|
-
text: `✅ تم إزالة @${
|
|
102
|
-
normalizedTargetJid.split("@")[0]
|
|
103
|
-
} من القائمة السوداء.`,
|
|
108
|
+
text: `✅ تم إزالة @${normalizedTargetJid.split("@")[0]} من القائمة السوداء.`,
|
|
104
109
|
mentions: [normalizedTargetJid],
|
|
105
110
|
});
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
saveGroupSettings(groupId, settings);
|
|
109
114
|
} catch (error) {
|
|
110
|
-
logger.error(
|
|
111
|
-
{ err: error, command: "blacklist" },
|
|
112
|
-
"Error in !blacklist command"
|
|
113
|
-
);
|
|
115
|
+
logger.error({ err: error, command: "blacklist" }, "Error in !blacklist command");
|
|
114
116
|
}
|
|
115
117
|
},
|
|
116
118
|
};
|
package/src/commands/block.cjs
CHANGED
|
@@ -21,10 +21,8 @@ module.exports = {
|
|
|
21
21
|
}
|
|
22
22
|
// Case 2: A target is specified (mention, reply, or number)
|
|
23
23
|
else {
|
|
24
|
-
const mentionedJid =
|
|
25
|
-
|
|
26
|
-
const repliedToJid =
|
|
27
|
-
msg.message?.extendedTextMessage?.contextInfo?.participant;
|
|
24
|
+
const mentionedJid = msg.message?.extendedTextMessage?.contextInfo?.mentionedJid?.[0];
|
|
25
|
+
const repliedToJid = msg.message?.extendedTextMessage?.contextInfo?.participant;
|
|
28
26
|
const numberArg = args[0];
|
|
29
27
|
|
|
30
28
|
if (mentionedJid) {
|
package/src/commands/calc.cjs
CHANGED
|
@@ -50,7 +50,7 @@ const FUNCTIONS = {
|
|
|
50
50
|
rad: { arity: 1, fn: (x) => x * DEG },
|
|
51
51
|
fact: { arity: 1, fn: (x) => factorial(x) },
|
|
52
52
|
pow: { arity: 2, fn: Math.pow },
|
|
53
|
-
root: { arity: 2, fn: (x, n) => (n % 2 === 0 || x >= 0 ?
|
|
53
|
+
root: { arity: 2, fn: (x, n) => (n % 2 === 0 || x >= 0 ? x ** (1 / n) : -((-x) ** (1 / n))) },
|
|
54
54
|
hypot: { arity: -1, fn: (...a) => Math.hypot(...a) },
|
|
55
55
|
min: { arity: -1, fn: (...a) => Math.min(...a) },
|
|
56
56
|
max: { arity: -1, fn: (...a) => Math.max(...a) },
|
|
@@ -96,10 +96,7 @@ function normalize(input) {
|
|
|
96
96
|
.replace(/(?<=\d),(?=\d{3}(?!\d))/g, "");
|
|
97
97
|
|
|
98
98
|
// «20% من 300» / «20% of 300» -> (20/100)*300, so % stays modulo elsewhere.
|
|
99
|
-
text = text.replace(
|
|
100
|
-
/(\d+(?:\.\d+)?)\s*%\s*(?:of|من)\s*/gi,
|
|
101
|
-
(_, value) => `(${value}/100)*`
|
|
102
|
-
);
|
|
99
|
+
text = text.replace(/(\d+(?:\.\d+)?)\s*%\s*(?:of|من)\s*/gi, (_, value) => `(${value}/100)*`);
|
|
103
100
|
|
|
104
101
|
return text.trim();
|
|
105
102
|
}
|
|
@@ -219,7 +216,7 @@ function parse(tokens) {
|
|
|
219
216
|
|
|
220
217
|
function parsePower() {
|
|
221
218
|
const base = parsePostfix();
|
|
222
|
-
if (eat("^")) return
|
|
219
|
+
if (eat("^")) return base ** parseUnary(); // right-associative
|
|
223
220
|
return base;
|
|
224
221
|
}
|
|
225
222
|
|
|
@@ -267,7 +264,7 @@ function parse(tokens) {
|
|
|
267
264
|
expect(")", `قوس \`${name}\` مقفلش`);
|
|
268
265
|
if (spec.arity !== -1 && args.length !== spec.arity) {
|
|
269
266
|
throw new CalcError(
|
|
270
|
-
`الدالة \`${name}\` بتاخد ${spec.arity} ${spec.arity === 1 ? "قيمة" : "قيم"}
|
|
267
|
+
`الدالة \`${name}\` بتاخد ${spec.arity} ${spec.arity === 1 ? "قيمة" : "قيم"}`,
|
|
271
268
|
);
|
|
272
269
|
}
|
|
273
270
|
if (!args.length) throw new CalcError(`الدالة \`${name}\` من غير قيم`);
|
|
@@ -348,8 +345,7 @@ module.exports = {
|
|
|
348
345
|
|
|
349
346
|
// Fall back to the quoted message so «رد على الرقم واكتب !calc» works.
|
|
350
347
|
const quoted = msg.message?.extendedTextMessage?.contextInfo?.quotedMessage;
|
|
351
|
-
const quotedText =
|
|
352
|
-
quoted?.conversation || quoted?.extendedTextMessage?.text || "";
|
|
348
|
+
const quotedText = quoted?.conversation || quoted?.extendedTextMessage?.text || "";
|
|
353
349
|
|
|
354
350
|
const expression = args.join(" ").trim() || quotedText.trim();
|
|
355
351
|
|
|
@@ -363,20 +359,17 @@ module.exports = {
|
|
|
363
359
|
sock,
|
|
364
360
|
chatId,
|
|
365
361
|
{
|
|
366
|
-
text:
|
|
367
|
-
`🧮 \`${normalized}\`\n` +
|
|
368
|
-
`*=* ${formatNumber(value)}`,
|
|
362
|
+
text: `🧮 \`${normalized}\`\n` + `*=* ${formatNumber(value)}`,
|
|
369
363
|
},
|
|
370
|
-
{ replyTo: msg }
|
|
364
|
+
{ replyTo: msg },
|
|
371
365
|
);
|
|
372
366
|
} catch (error) {
|
|
373
|
-
const reason =
|
|
374
|
-
error instanceof CalcError ? error.message : "المعادلة مش مفهومة";
|
|
367
|
+
const reason = error instanceof CalcError ? error.message : "المعادلة مش مفهومة";
|
|
375
368
|
return sendBotMessage(
|
|
376
369
|
sock,
|
|
377
370
|
chatId,
|
|
378
371
|
{ text: `❌ ${reason}\n\nاكتب \`!calc\` لوحده عشان تشوف الأمثلة.` },
|
|
379
|
-
{ replyTo: msg }
|
|
372
|
+
{ replyTo: msg },
|
|
380
373
|
);
|
|
381
374
|
}
|
|
382
375
|
},
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Convert digits between Western 0-9 and Eastern Arabic / Persian numerals.
|
|
2
|
+
const runtimeConfig = require("../config/runtime-config.cjs");
|
|
3
|
+
const {
|
|
4
|
+
classifyNumerals,
|
|
5
|
+
convertNumerals,
|
|
6
|
+
getQuotedText,
|
|
7
|
+
isExplicitAr,
|
|
8
|
+
isExplicitEn,
|
|
9
|
+
oppositeNumeralLang,
|
|
10
|
+
} = require("../utils/numerals.cjs");
|
|
11
|
+
|
|
12
|
+
function mixedDigitsNotice(prefix) {
|
|
13
|
+
return `⚠️ الأرقام مختلطة (عربي وإنجليزي). حدد الاتجاه:\n\`${prefix}digit ar ...\` أو \`${prefix}digit en ...\`.\nDigits are mixed Arabic and English. Specify \`${prefix}digit ar\` or \`${prefix}digit en\`.`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function helpText(prefix) {
|
|
17
|
+
return `🔢 *تحويل الأرقام / Digit converter*
|
|
18
|
+
|
|
19
|
+
• \`${prefix}digit 12345\` ➔ ١٢٣٤٥ (يُكتشف تلقائياً / auto-detected)
|
|
20
|
+
• \`${prefix}digit ١٢٣٤٥\` ➔ 12345
|
|
21
|
+
• \`${prefix}digit ar 12345\` ➔ ١٢٣٤٥
|
|
22
|
+
• \`${prefix}digit en ١٢٣٤٥\` ➔ 12345
|
|
23
|
+
_أو رد على رسالة فيها أرقام بـ \`${prefix}digit\` — يحوّل للاتجاه المعاكس._
|
|
24
|
+
_إذا اختلطت الأرقام العربية والإنجليزية يجب تحديد الاتجاه: \`${prefix}digit ar\` أو \`${prefix}digit en\`._`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
name: "digit",
|
|
29
|
+
aliases: ["digits", "num", "numbers", "numeral", "ارقام", "رقم"],
|
|
30
|
+
description: "Convert numbers between Arabic and English digits",
|
|
31
|
+
usage: "digit [en|ar] [number/text] (or reply to a message)",
|
|
32
|
+
chat: "all",
|
|
33
|
+
|
|
34
|
+
async execute(sock, msg, args) {
|
|
35
|
+
const chatId = msg.key.remoteJid;
|
|
36
|
+
const prefix = runtimeConfig.getPrefix();
|
|
37
|
+
const quotedText = getQuotedText(msg);
|
|
38
|
+
const firstArg = args?.[0] ? String(args[0]).toLowerCase().trim() : "";
|
|
39
|
+
const targetAr = isExplicitAr(firstArg);
|
|
40
|
+
const targetEn = isExplicitEn(firstArg);
|
|
41
|
+
const hasExplicitTarget = targetAr || targetEn;
|
|
42
|
+
|
|
43
|
+
if (!args || args.length === 0) {
|
|
44
|
+
if (quotedText && classifyNumerals(quotedText) !== "none") {
|
|
45
|
+
const kind = classifyNumerals(quotedText);
|
|
46
|
+
if (kind === "mixed") {
|
|
47
|
+
await sock.sendMessage(chatId, { text: mixedDigitsNotice(prefix) }, { quoted: msg });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const converted = convertNumerals(quotedText, oppositeNumeralLang(kind));
|
|
51
|
+
await sock.sendMessage(chatId, { text: converted }, { quoted: msg });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
await sock.sendMessage(chatId, { text: helpText(prefix) }, { quoted: msg });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const inlineText = hasExplicitTarget ? args.slice(1).join(" ").trim() : args.join(" ").trim();
|
|
59
|
+
const textToConvert = inlineText || quotedText;
|
|
60
|
+
|
|
61
|
+
if (!textToConvert) {
|
|
62
|
+
await sock.sendMessage(
|
|
63
|
+
chatId,
|
|
64
|
+
{
|
|
65
|
+
text: `❌ اكتب رقماً بعد الأمر، أو رد على رسالة.\nType a number after the command, or reply to a message.\nمثال: \`${prefix}digit 12345\` أو \`${prefix}digit ar 12345\``,
|
|
66
|
+
},
|
|
67
|
+
{ quoted: msg },
|
|
68
|
+
);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (hasExplicitTarget) {
|
|
73
|
+
const converted = convertNumerals(textToConvert, targetAr ? "ar" : "en");
|
|
74
|
+
await sock.sendMessage(chatId, { text: converted }, { quoted: msg });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const kind = classifyNumerals(textToConvert);
|
|
79
|
+
if (kind === "mixed") {
|
|
80
|
+
await sock.sendMessage(chatId, { text: mixedDigitsNotice(prefix) }, { quoted: msg });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (kind === "none") {
|
|
84
|
+
await sock.sendMessage(
|
|
85
|
+
chatId,
|
|
86
|
+
{
|
|
87
|
+
text: `❌ لا توجد أرقام للتحويل.\nNo digits found to convert.\nاستخدم:\n• \`${prefix}digit 12345\`\n• \`${prefix}digit ar\` / \`${prefix}digit en\` مع النص أو بالرد على رسالة.`,
|
|
88
|
+
},
|
|
89
|
+
{ quoted: msg },
|
|
90
|
+
);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const converted = convertNumerals(textToConvert, oppositeNumeralLang(kind));
|
|
95
|
+
await sock.sendMessage(chatId, { text: converted }, { quoted: msg });
|
|
96
|
+
},
|
|
97
|
+
};
|