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.
Files changed (118) hide show
  1. package/README.md +13 -9
  2. package/app.cjs +49 -46
  3. package/package.json +9 -1
  4. package/public/dashboard/assets/index-BBBXWzM0.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 -128
  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
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) {
@@ -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");
@@ -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) => io.emit(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 targetUser = groupMetadata.participants.find(
69
- (p) => p.id === normalizedTargetJid
70
- );
71
- if (targetUser?.admin)
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.includes(normalizedTargetJid))
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.includes(normalizedTargetJid))
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 !== normalizedTargetJid
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
  };
@@ -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
- msg.message?.extendedTextMessage?.contextInfo?.mentionedJid?.[0];
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) {
@@ -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 ? Math.pow(x, 1 / n) : -Math.pow(-x, 1 / n)) },
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 Math.pow(base, parseUnary()); // right-associative
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
+ };