levix-bot 2.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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/SETUP.md +242 -0
  4. package/app.cjs +337 -0
  5. package/bin/levix.js +7 -0
  6. package/deploy/install.sh +182 -0
  7. package/deploy/levix.service +56 -0
  8. package/deploy/nginx/levix.leviro.net.conf +79 -0
  9. package/package.json +78 -0
  10. package/public/brand/apple-touch-icon.png +0 -0
  11. package/public/brand/banner.jpg +0 -0
  12. package/public/brand/banner.webp +0 -0
  13. package/public/brand/icon-16.png +0 -0
  14. package/public/brand/icon-192.png +0 -0
  15. package/public/brand/icon-32.png +0 -0
  16. package/public/brand/icon-512.png +0 -0
  17. package/public/brand/icon-512.webp +0 -0
  18. package/public/brand/mark.png +0 -0
  19. package/public/brand/mark.svg +16 -0
  20. package/public/brand/mark.webp +0 -0
  21. package/public/brand/wordmark.png +0 -0
  22. package/public/brand/wordmark.webp +0 -0
  23. package/public/dashboard.css +881 -0
  24. package/public/dashboard.js +1057 -0
  25. package/public/logo.svg +16 -0
  26. package/public/qrcode.min.js +1 -0
  27. package/public/socket.io.min.js +7 -0
  28. package/scheduler.cjs +197 -0
  29. package/src/auth/auth-storage.cjs +74 -0
  30. package/src/auth/use-database-auth-state.js +130 -0
  31. package/src/bootstrap/core.js +74 -0
  32. package/src/bootstrap/events.cjs +43 -0
  33. package/src/bootstrap/panel.js +97 -0
  34. package/src/cli.js +153 -0
  35. package/src/commands/autoschedule.cjs +84 -0
  36. package/src/commands/blacklist.cjs +116 -0
  37. package/src/commands/block.cjs +70 -0
  38. package/src/commands/calc.cjs +387 -0
  39. package/src/commands/debt.cjs +175 -0
  40. package/src/commands/deleteschedule.cjs +36 -0
  41. package/src/commands/gemini.cjs +727 -0
  42. package/src/commands/group/add.cjs +136 -0
  43. package/src/commands/group/all.cjs +43 -0
  44. package/src/commands/group/antiSpam.cjs +96 -0
  45. package/src/commands/group/antilink.cjs +167 -0
  46. package/src/commands/group/approveall.cjs +54 -0
  47. package/src/commands/group/clearwarns.cjs +41 -0
  48. package/src/commands/group/deletenote.cjs +33 -0
  49. package/src/commands/group/demote.cjs +62 -0
  50. package/src/commands/group/kick.cjs +85 -0
  51. package/src/commands/group/media.cjs +120 -0
  52. package/src/commands/group/members.cjs +59 -0
  53. package/src/commands/group/note.cjs +32 -0
  54. package/src/commands/group/promote.cjs +60 -0
  55. package/src/commands/group/removeall.cjs +42 -0
  56. package/src/commands/group/rules.cjs +31 -0
  57. package/src/commands/group/save.cjs +36 -0
  58. package/src/commands/group/setname.cjs +41 -0
  59. package/src/commands/group/setpp.cjs +51 -0
  60. package/src/commands/group/setrules.cjs +46 -0
  61. package/src/commands/group/setwarn.cjs +58 -0
  62. package/src/commands/group/tagadmins.cjs +50 -0
  63. package/src/commands/group/warn.cjs +105 -0
  64. package/src/commands/group/warns.cjs +66 -0
  65. package/src/commands/group/welcome.cjs +109 -0
  66. package/src/commands/group.cjs +160 -0
  67. package/src/commands/help.cjs +230 -0
  68. package/src/commands/listschedules.cjs +47 -0
  69. package/src/commands/loop.cjs +163 -0
  70. package/src/commands/memory.cjs +201 -0
  71. package/src/commands/mod.cjs +155 -0
  72. package/src/commands/notes.cjs +28 -0
  73. package/src/commands/perm.cjs +236 -0
  74. package/src/commands/ping.cjs +17 -0
  75. package/src/commands/poll.cjs +80 -0
  76. package/src/commands/prayer.cjs +64 -0
  77. package/src/commands/qr.cjs +39 -0
  78. package/src/commands/rand.cjs +130 -0
  79. package/src/commands/restart.cjs +25 -0
  80. package/src/commands/schedule.cjs +86 -0
  81. package/src/commands/score.cjs +130 -0
  82. package/src/commands/setprefix.cjs +40 -0
  83. package/src/commands/shortlink.cjs +74 -0
  84. package/src/commands/shutdown.cjs +45 -0
  85. package/src/commands/status.cjs +75 -0
  86. package/src/commands/stt.cjs +150 -0
  87. package/src/commands/todo.cjs +88 -0
  88. package/src/commands/tts.cjs +170 -0
  89. package/src/commands/unblock.cjs +57 -0
  90. package/src/commands/weather.cjs +79 -0
  91. package/src/config/ai-persona.md +17 -0
  92. package/src/config/baileys.config.js +51 -0
  93. package/src/config/brand.cjs +36 -0
  94. package/src/config/brand.esm.js +17 -0
  95. package/src/config/constants.js +16 -0
  96. package/src/config/defaults.cjs +91 -0
  97. package/src/config/lock.cjs +110 -0
  98. package/src/config/paths.cjs +90 -0
  99. package/src/config/runtime-config.cjs +219 -0
  100. package/src/config/secrets.cjs +130 -0
  101. package/src/config/settings.cjs +386 -0
  102. package/src/core/connection.js +199 -0
  103. package/src/core/events.js +61 -0
  104. package/src/core/socket.js +64 -0
  105. package/src/db/db.cjs +282 -0
  106. package/src/db/store.cjs +837 -0
  107. package/src/db/store.esm.js +17 -0
  108. package/src/domain/caddy.js +120 -0
  109. package/src/domain/command.js +380 -0
  110. package/src/domain/detect.js +173 -0
  111. package/src/domain/nginx.js +138 -0
  112. package/src/domain/system.js +148 -0
  113. package/src/handlers/command.handler.js +414 -0
  114. package/src/handlers/group.handler.js +73 -0
  115. package/src/handlers/message.handler.js +149 -0
  116. package/src/index.js +217 -0
  117. package/src/middleware/antispam.middleware.js +68 -0
  118. package/src/middleware/blacklist.middleware.js +29 -0
  119. package/src/middleware/forward-tracking.middleware.js +49 -0
  120. package/src/middleware/permissions.middleware.js +114 -0
  121. package/src/routes/dashboard.api.esm.js +779 -0
  122. package/src/services/aiAgent.cjs +317 -0
  123. package/src/services/aiTools.cjs +635 -0
  124. package/src/utils/datetime.cjs +61 -0
  125. package/src/utils/lid-helper.esm.js +143 -0
  126. package/src/utils/logger.cjs +72 -0
  127. package/src/utils/memory.cjs +385 -0
  128. package/src/utils/normalizeJid.cjs +65 -0
  129. package/src/utils/normalizeJid.esm.js +37 -0
  130. package/src/utils/openBrowser.cjs +129 -0
  131. package/src/utils/permissions.cjs +154 -0
  132. package/src/utils/permissions.esm.js +375 -0
  133. package/src/utils/recentMessageCache.esm.js +70 -0
  134. package/src/utils/requestOrigin.cjs +102 -0
  135. package/src/utils/sendBotMessage.cjs +43 -0
  136. package/src/utils/sendBotMessage.esm.js +139 -0
  137. package/src/utils/statusMessage.cjs +179 -0
  138. package/src/utils/storage-hub.cjs +49 -0
  139. package/src/utils/storage-hub.esm.js +24 -0
  140. package/src/utils/storage.cjs +80 -0
  141. package/src/utils/storage.esm.js +88 -0
  142. package/src/utils/textDecode.cjs +197 -0
  143. package/src/utils/thumbnail.cjs +591 -0
  144. package/src/utils/typing.esm.js +102 -0
  145. package/views/dashboard.ejs +329 -0
  146. package/views/login.ejs +105 -0
  147. package/views/qr.ejs +108 -0
  148. package/views/setup.ejs +122 -0
@@ -0,0 +1,230 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const logger = require("../utils/logger.cjs");
4
+ const brand = require("../config/brand.cjs");
5
+ const { sendBotMessage } = require("../utils/sendBotMessage.cjs");
6
+ const runtimeConfig = require("../config/runtime-config.cjs");
7
+
8
+ // Recursive function to get all command files
9
+ function getAllCommandFiles(dirPath, fileList = []) {
10
+ const files = fs.readdirSync(dirPath);
11
+
12
+ files.forEach((file) => {
13
+ const filePath = path.join(dirPath, file);
14
+ if (fs.statSync(filePath).isDirectory()) {
15
+ fileList = getAllCommandFiles(filePath, fileList);
16
+ } else if (file.endsWith(".cjs") || file.endsWith(".js")) {
17
+ fileList.push(filePath);
18
+ }
19
+ });
20
+
21
+ return fileList;
22
+ }
23
+
24
+ // Cache the loaded command modules so we don't re-require on every invocation.
25
+ let cached = null;
26
+ function loadAllCommands() {
27
+ if (cached) return cached;
28
+ const commandsPath = path.join(__dirname);
29
+ const files = getAllCommandFiles(commandsPath).filter(
30
+ (file) =>
31
+ path.basename(file) !== "help.cjs" && path.basename(file) !== "help.js"
32
+ );
33
+
34
+ const commands = { general: [], group: [] };
35
+ for (const file of files) {
36
+ try {
37
+ const command = require(file);
38
+ if (!command?.name || !command?.description) continue;
39
+ const relativePath = path.relative(commandsPath, file);
40
+ if (path.dirname(relativePath) === "group") {
41
+ commands.group.push(command);
42
+ } else {
43
+ commands.general.push(command);
44
+ }
45
+ } catch (error) {
46
+ logger.error(error, `Could not load command from ${file}:`);
47
+ }
48
+ }
49
+
50
+ commands.general.push(module.exports);
51
+
52
+ // Build alias index for inverse lookup.
53
+ const aliasIndex = new Map();
54
+ for (const cmd of [...commands.general, ...commands.group]) {
55
+ for (const alias of aliasesOf(cmd)) {
56
+ aliasIndex.set(String(alias).toLowerCase(), cmd);
57
+ }
58
+ }
59
+
60
+ commands.general.sort((a, b) => a.name.localeCompare(b.name));
61
+ commands.group.sort((a, b) => a.name.localeCompare(b.name));
62
+
63
+ cached = { commands, aliasIndex };
64
+ return cached;
65
+ }
66
+
67
+ function findCommand(query, commands, aliasIndex) {
68
+ const q = query.toLowerCase();
69
+ const all = [...commands.general, ...commands.group];
70
+ const direct = all.find((cmd) => cmd.name.toLowerCase() === q);
71
+ if (direct) return direct;
72
+ if (aliasIndex.has(q)) return aliasIndex.get(q);
73
+ return null;
74
+ }
75
+
76
+ // Prefix, aliases and permissions are all changeable at runtime (`!setprefix`,
77
+ // the dashboard), so read the live values rather than the file defaults.
78
+ function currentPrefix() {
79
+ return runtimeConfig.getPrefix();
80
+ }
81
+
82
+ /** Effective aliases: a dashboard override if there is one, else the file's. */
83
+ function aliasesOf(command) {
84
+ return runtimeConfig.getAliases(command.name, command.aliases || []);
85
+ }
86
+
87
+ /**
88
+ * `usage` is authored WITHOUT the prefix ("kick @user [reason]") and may hold
89
+ * several variants separated by newlines. Render each one with the live prefix.
90
+ */
91
+ function usageLines(command, prefix) {
92
+ const raw = command.usage || command.name;
93
+ return String(raw)
94
+ .split("\n")
95
+ .map((line) => line.trim())
96
+ .filter(Boolean)
97
+ .map((line) => `${prefix}${line}`);
98
+ }
99
+
100
+ // Permission level as configured for this command (group sub-commands live in
101
+ // their own map). Shown in the detail view so members know why a command is
102
+ // refusing them.
103
+ const PERMISSION_LABELS = {
104
+ MEMBERS: "الكل",
105
+ ALL: "الكل",
106
+ ADMINS_ONLY: "المشرفين فقط",
107
+ ADMINS_OWNER: "المشرفين والمالك",
108
+ OWNER_ONLY: "المالك فقط",
109
+ };
110
+
111
+ function permissionFor(command, isGroupCommand) {
112
+ const level = runtimeConfig.getPermission(
113
+ isGroupCommand ? `group:${command.name}` : command.name
114
+ );
115
+ return PERMISSION_LABELS[level] || level;
116
+ }
117
+
118
+ const CHAT_LABELS = {
119
+ all: "كل المحادثات",
120
+ group: "المجموعات فقط",
121
+ private: "الخاص فقط",
122
+ };
123
+
124
+ module.exports = {
125
+ name: "help",
126
+ aliases: ["menu", "commands", "h"],
127
+ description: "Shows a list of all commands or info about a specific command.",
128
+ usage: "help [اسم الأمر]",
129
+ chat: "all",
130
+
131
+ async execute(sock, msg, args) {
132
+ const prefix = currentPrefix();
133
+ const { commands, aliasIndex } = loadAllCommands();
134
+
135
+ if (!args[0]) {
136
+ const section = (title, list) => {
137
+ let text = `├─ *${title}*\n`;
138
+ list.forEach((cmd) => {
139
+ const aliases = aliasesOf(cmd);
140
+ const aliasSnippet = aliases.length
141
+ ? ` _(${aliases.map((a) => prefix + a).join(", ")})_`
142
+ : "";
143
+ text += `│ ◦ *${prefix}${cmd.name}*${aliasSnippet}\n`;
144
+ text += `│ ${cmd.description}\n`;
145
+ // The parameters are the whole point of a help list — show them.
146
+ usageLines(cmd, prefix).forEach((line) => {
147
+ text += `│ ⌨️ \`${line}\`\n`;
148
+ });
149
+ });
150
+ return `${text}│\n`;
151
+ };
152
+
153
+ // A command switched off from the dashboard can't run, so it has no
154
+ // business in the menu.
155
+ const live = (list) =>
156
+ list.filter((cmd) => !runtimeConfig.isDisabled(cmd.name));
157
+
158
+ const general = live(commands.general);
159
+ const group = live(commands.group);
160
+
161
+ let helpText = `╭───「 *🤖 ${brand.name} Commands* 」\n│\n`;
162
+ if (general.length) helpText += section("General Commands", general);
163
+ if (group.length) helpText += section("Group Commands", group);
164
+
165
+ helpText += `├─ *الرموز*\n`;
166
+ helpText += `│ \`<...>\` مطلوب · \`[...]\` اختياري · \`a|b\` اختر واحد\n│\n`;
167
+ helpText += `╰───「 اكتب *${prefix}help <أمر>* لتفاصيل أكتر 」`;
168
+
169
+ return sendBotMessage(sock, msg.key.remoteJid, { text: helpText }, { replyTo: msg });
170
+ }
171
+
172
+ const query = args[0].startsWith(prefix) ? args[0].slice(prefix.length) : args[0];
173
+ const command = findCommand(query, commands, aliasIndex);
174
+
175
+ if (!command) {
176
+ return sendBotMessage(
177
+ sock,
178
+ msg.key.remoteJid,
179
+ {
180
+ text:
181
+ `❌ الأمر "*${args[0]}*" غير موجود.\n` +
182
+ `اكتب \`${prefix}help\` لعرض كل الأوامر.`,
183
+ },
184
+ { replyTo: msg }
185
+ );
186
+ }
187
+
188
+ let usageText = `╭───「 *Command Details* 」\n│\n`;
189
+ usageText += `├─ *الأمر:* ${prefix}${command.name}\n`;
190
+
191
+ const commandAliases = aliasesOf(command);
192
+ if (commandAliases.length) {
193
+ usageText += `├─ *اختصارات:* ${commandAliases
194
+ .map((a) => `${prefix}${a}`)
195
+ .join(", ")}\n`;
196
+ }
197
+
198
+ if (command.description) {
199
+ usageText += `├─ *الوصف:* ${command.description}\n`;
200
+ }
201
+
202
+ usageText += `├─ *الاستخدام:*\n`;
203
+ usageLines(command, prefix).forEach((line) => {
204
+ usageText += `│ \`${line}\`\n`;
205
+ });
206
+
207
+ if (command.examples?.length) {
208
+ usageText += `├─ *أمثلة:*\n`;
209
+ command.examples.forEach((example) => {
210
+ usageText += `│ \`${prefix}${example}\`\n`;
211
+ });
212
+ }
213
+
214
+ usageText += `├─ *المكان:* ${CHAT_LABELS[command.chat] || command.chat || "كل المحادثات"}\n`;
215
+ const isGroupCommand = commands.group.includes(command);
216
+ usageText += `├─ *الصلاحية:* ${permissionFor(command, isGroupCommand)}\n`;
217
+
218
+ if (command.userAdminRequired) {
219
+ usageText += `├─ ⚠️ لازم تكون مشرف\n`;
220
+ }
221
+ if (command.botAdminRequired) {
222
+ usageText += `├─ ⚠️ لازم البوت يكون مشرف\n`;
223
+ }
224
+
225
+ usageText += `│\n├─ \`<...>\` مطلوب · \`[...]\` اختياري\n`;
226
+ usageText += `│\n╰───「 ${brand.name} 」`;
227
+
228
+ return sendBotMessage(sock, msg.key.remoteJid, { text: usageText }, { replyTo: msg });
229
+ },
230
+ };
@@ -0,0 +1,47 @@
1
+ // file: /commands/listschedules.js
2
+ const { getScheduledJobs } = require("../../scheduler.cjs");
3
+
4
+ module.exports = {
5
+ name: "listschedules",
6
+ description: "Lists all active and pending scheduled jobs.",
7
+ usage: "listschedules",
8
+ chat: "all",
9
+ userAdminRequired: true,
10
+
11
+ async execute(sock, msg) {
12
+ const jobs = getScheduledJobs();
13
+ const activeJobs = jobs.filter(
14
+ (job) => job.status === "pending" || job.status === "active"
15
+ );
16
+
17
+ if (activeJobs.length === 0) {
18
+ return await sock.sendMessage(msg.key.remoteJid, {
19
+ text: "لا توجد مهام مجدولة حاليًا.",
20
+ });
21
+ }
22
+
23
+ let reply = "*⏰ المهام المجدولة النشطة:*\n\n";
24
+
25
+ activeJobs.forEach((job, index) => {
26
+ reply += `*${index + 1}. المهمة:*\n`;
27
+ reply += `*ID:* \`${job.id}\`\n`;
28
+ reply += `*الرسالة:* "${job.message}"\n`;
29
+
30
+ if (job.type === "recurring") {
31
+ reply += `*التكرار:* ${job.cronString} (يوميًا/أسبوعيًا)\n`;
32
+ } else {
33
+ const jobDate = new Date(job.date).toLocaleString("ar-EG", {
34
+ timeZone: "Africa/Cairo",
35
+ });
36
+ reply += `*الوقت المحدد:* ${jobDate}\n`;
37
+ }
38
+ reply += `*الوجهة:* ${
39
+ job.targetJid.endsWith("@g.us") ? "هذا الجروب" : "محادثة خاصة"
40
+ }\n\n`;
41
+ });
42
+
43
+ reply += "*لحذف مهمة، استخدم:*\n`!deleteschedule <ID>`";
44
+
45
+ await sock.sendMessage(msg.key.remoteJid, { text: reply });
46
+ },
47
+ };
@@ -0,0 +1,163 @@
1
+ // تكرار — !loop
2
+ //
3
+ // Repeats a number range or a piece of text, either bundled into ONE message
4
+ // or sent as SEPARATE messages.
5
+ //
6
+ // !loop 5 اهلا 5 lines in one message
7
+ // !loop 1-10 the numbers 1 … 10
8
+ // !loop 1-20:5 1, 6, 11, 16 (step 5)
9
+ // !loop 3 صباح الخير -s three separate messages
10
+ // !loop 1-5 السطر رقم {n} {n} = current number, {i} = iteration index
11
+ //
12
+ // Hard caps keep an innocent `!loop 100000` from turning the bot into a
13
+ // flood bot (and from getting the account banned).
14
+
15
+ const { sendBotMessage } = require("../utils/sendBotMessage.cjs");
16
+
17
+ const MAX_ITEMS = 100; // total iterations, one-message mode
18
+ const MAX_SPLIT_ITEMS = 15; // total iterations, separate-messages mode
19
+ const MAX_OUTPUT_CHARS = 3500; // WhatsApp starts truncating well past this
20
+ const SPLIT_GAP_MS = 900; // breathing room between separate sends
21
+
22
+ const SPLIT_FLAGS = new Set(["-s", "--split", "split", "منفصل", "منفصلة"]);
23
+ const NO_NUMBER_FLAGS = new Set(["-n", "--nonum", "--plain", "بدون-ترقيم"]);
24
+
25
+ const HELP_TEXT =
26
+ "🔁 *أمر التكرار*\n\n" +
27
+ "*الاستخدام:*\n" +
28
+ "`!loop <عدد> <النص>` — يكرر النص في رسالة واحدة\n" +
29
+ "`!loop <من>-<إلى>` — يطبع الأرقام من/إلى\n" +
30
+ "`!loop <من>-<إلى>:<خطوة>` — بخطوة مخصصة\n\n" +
31
+ "*خيارات:*\n" +
32
+ "`-s` رسائل منفصلة بدل رسالة واحدة\n" +
33
+ "`-n` من غير ترقيم\n\n" +
34
+ "*متغيرات داخل النص:*\n" +
35
+ "`{n}` الرقم الحالي · `{i}` رقم التكرار\n\n" +
36
+ "*أمثلة:*\n" +
37
+ "`!loop 5 اهلا`\n" +
38
+ "`!loop 1-10`\n" +
39
+ "`!loop 0-100:25 العداد {n}`\n" +
40
+ "`!loop 3 صباح الخير -s`\n\n" +
41
+ `_الحد الأقصى: ${MAX_ITEMS} تكرار في رسالة واحدة، ${MAX_SPLIT_ITEMS} رسالة منفصلة._`;
42
+
43
+ /**
44
+ * `5` -> 1..5 · `3-9` -> 3..9 · `0-100:25` -> 0,25,50,75,100
45
+ * Returns null when the token isn't a count/range at all.
46
+ */
47
+ function parseRange(token) {
48
+ if (!token) return null;
49
+
50
+ const match = String(token)
51
+ .replace(/[٠-٩]/g, (d) => String("٠١٢٣٤٥٦٧٨٩".indexOf(d)))
52
+ .match(/^(-?\d+)(?:\s*(?:-|\.\.|إلى|to)\s*(-?\d+))?(?::(\d+))?$/);
53
+ if (!match) return null;
54
+
55
+ const first = Number(match[1]);
56
+ const second = match[2] === undefined ? null : Number(match[2]);
57
+ const step = match[3] ? Number(match[3]) : 1;
58
+ if (!step || step < 1) return null;
59
+
60
+ // A bare number is a repeat count, not a range.
61
+ const from = second === null ? 1 : first;
62
+ const to = second === null ? first : second;
63
+
64
+ return { from, to, step, isCount: second === null };
65
+ }
66
+
67
+ /** How many iterations the range asks for — computed, not enumerated. */
68
+ function countOf({ from, to, step }) {
69
+ return Math.floor(Math.abs(to - from) / step) + 1;
70
+ }
71
+
72
+ function buildNumbers({ from, to, step }, limit) {
73
+ const numbers = [];
74
+ const descending = to < from;
75
+ for (
76
+ let value = from;
77
+ descending ? value >= to : value <= to;
78
+ value += descending ? -step : step
79
+ ) {
80
+ numbers.push(value);
81
+ if (numbers.length >= limit) break;
82
+ }
83
+ return numbers;
84
+ }
85
+
86
+ function renderLine(template, number, index) {
87
+ if (!template) return String(number);
88
+ return template
89
+ .replace(/\{n\}/gi, String(number))
90
+ .replace(/\{i\}/gi, String(index));
91
+ }
92
+
93
+ module.exports = {
94
+ name: "loop",
95
+ aliases: ["repeat", "كرر", "تكرار"],
96
+ description: "يكرر نص أو أرقام — في رسالة واحدة أو رسائل منفصلة.",
97
+ usage:
98
+ "loop <عدد|من-إلى[:خطوة]> [النص] [-s رسائل منفصلة] [-n بدون ترقيم]",
99
+ chat: "all",
100
+
101
+ async execute(sock, msg, args) {
102
+ const chatId = msg.key.remoteJid;
103
+ const reply = (text) => sendBotMessage(sock, chatId, { text }, { replyTo: msg });
104
+
105
+ // Pull the flags out wherever the user put them.
106
+ let split = false;
107
+ let numbered = true;
108
+ const rest = [];
109
+ for (const arg of args) {
110
+ const token = arg.toLowerCase();
111
+ if (SPLIT_FLAGS.has(token)) split = true;
112
+ else if (NO_NUMBER_FLAGS.has(token)) numbered = false;
113
+ else rest.push(arg);
114
+ }
115
+
116
+ const range = parseRange(rest[0]);
117
+ if (!range) return reply(HELP_TEXT);
118
+
119
+ const template = rest.slice(1).join(" ").trim();
120
+ const limit = split ? MAX_SPLIT_ITEMS : MAX_ITEMS;
121
+ const requested = countOf(range);
122
+
123
+ if (requested < 1) return reply("❌ المدى ده مش بيطلع أي أرقام.");
124
+ if (requested > limit) {
125
+ return reply(
126
+ `❌ ${requested} تكرار كتير أوي.\n` +
127
+ `الحد الأقصى ${limit} ${split ? "رسالة منفصلة" : "تكرار في الرسالة الواحدة"}.`
128
+ );
129
+ }
130
+
131
+ const numbers = buildNumbers(range, limit);
132
+ if (!numbers.length) return reply("❌ المدى ده مش بيطلع أي أرقام.");
133
+
134
+ // A bare count with text repeats the text; a range without text prints the
135
+ // numbers. Either way {n}/{i} are substituted per line.
136
+ const lines = numbers.map((number, index) =>
137
+ renderLine(template, number, index + 1)
138
+ );
139
+
140
+ if (split) {
141
+ for (let i = 0; i < lines.length; i++) {
142
+ const body = numbered ? `${i + 1}. ${lines[i]}` : lines[i];
143
+ await sendBotMessage(
144
+ sock,
145
+ chatId,
146
+ { text: body },
147
+ { replyTo: i === 0 ? msg : null, typing: false, delayMs: SPLIT_GAP_MS }
148
+ );
149
+ }
150
+ return;
151
+ }
152
+
153
+ let text = lines
154
+ .map((line, i) => (numbered ? `${i + 1}. ${line}` : line))
155
+ .join("\n");
156
+
157
+ if (text.length > MAX_OUTPUT_CHARS) {
158
+ text = `${text.slice(0, MAX_OUTPUT_CHARS)}\n…\n_(اتقصت عشان طولها)_`;
159
+ }
160
+
161
+ return reply(text);
162
+ },
163
+ };
@@ -0,0 +1,201 @@
1
+ // Long-term memory, straight from WhatsApp.
2
+ //
3
+ // The AI can save/forget things by itself (see services/aiTools.cjs) — this
4
+ // command is the manual door to the same Markdown files:
5
+ //
6
+ // memory/global.md shared by every chat
7
+ // memory/chats/<chat-id>.md this conversation only
8
+ //
9
+ // Anyone can add a fact to the chat they're in; touching the global memory,
10
+ // deleting entries or exporting a file is for admins/owners.
11
+
12
+ const fs = require("fs");
13
+
14
+ const memory = require("../utils/memory.cjs");
15
+ const { sendBotMessage } = require("../utils/sendBotMessage.cjs");
16
+ const {
17
+ isOwnerJidSync,
18
+ isBotAdminUserSync,
19
+ isAdminInGroupSync,
20
+ } = require("../utils/permissions.cjs");
21
+ const logger = require("../utils/logger.cjs");
22
+
23
+ const GLOBAL_WORDS = new Set(["global", "-g", "--global", "عام", "العام", "الكل"]);
24
+
25
+ function isGlobalFlag(value) {
26
+ return GLOBAL_WORDS.has(String(value || "").toLowerCase());
27
+ }
28
+
29
+ /** Pull a `global` flag out of the args wherever the user put it. */
30
+ function extractScope(args) {
31
+ const rest = [];
32
+ let scope = "chat";
33
+ for (const arg of args) {
34
+ if (isGlobalFlag(arg)) scope = "global";
35
+ else rest.push(arg);
36
+ }
37
+ return { scope, rest };
38
+ }
39
+
40
+ function formatEntries(entries, { limit = 30 } = {}) {
41
+ if (!entries.length) return "_(فاضية)_";
42
+ const shown = entries.slice(-limit);
43
+ const lines = shown.map((entry, index) => {
44
+ const number = entries.length - shown.length + index + 1;
45
+ const when = entry.at ? entry.at.slice(0, 10) : "";
46
+ const who = entry.by ? ` · ${entry.by}` : "";
47
+ const meta = when || who ? `\n _${when}${who}_` : "";
48
+ return `*${number}.* ${entry.content}${meta}`;
49
+ });
50
+ const skipped = entries.length - shown.length;
51
+ return (
52
+ (skipped > 0 ? `_(+${skipped} أقدم مش معروضين)_\n\n` : "") + lines.join("\n")
53
+ );
54
+ }
55
+
56
+ module.exports = {
57
+ name: "memory",
58
+ aliases: ["mem", "ذاكرة", "remember"],
59
+ description:
60
+ "الذاكرة الدائمة للبوت (ملفات .md): إضافة، عرض، بحث، حذف، أو تصدير الملف.",
61
+ usage:
62
+ "memory\nmemory add <المعلومة>\nmemory add global <المعلومة>\nmemory search <كلمة>\nmemory forget <رقم|جزء من النص>\nmemory clear [global]\nmemory file [global]",
63
+ chat: "all",
64
+
65
+ async execute(sock, msg, args, body, groupMetadata) {
66
+ const chatId = msg.key.remoteJid;
67
+ const isGroup = chatId.endsWith("@g.us");
68
+ const senderId = isGroup ? msg.key.participant : msg.key.remoteJid;
69
+
70
+ const isOwner = msg.key.fromMe || isOwnerJidSync(senderId);
71
+ const privileged =
72
+ isOwner ||
73
+ isBotAdminUserSync(senderId) ||
74
+ (isGroup && isAdminInGroupSync(groupMetadata, senderId));
75
+
76
+ const action = String(args[0] || "").toLowerCase();
77
+ const { scope, rest } = extractScope(args.slice(1));
78
+
79
+ const reply = (text, extra = {}) =>
80
+ sendBotMessage(sock, chatId, { text, ...extra }, { replyTo: msg });
81
+
82
+ const denied = () =>
83
+ reply("🚫 ده للمشرفين والمالك بس. تقدر تضيف معلومة لذاكرة الشات عادي.");
84
+
85
+ try {
86
+ // ---------------------------------------------------------------- add
87
+ if (["add", "save", "احفظ", "ضيف", "اضف"].includes(action)) {
88
+ const content = rest.join(" ").trim();
89
+ if (!content) {
90
+ return reply(
91
+ "اكتب المعلومة بعد الأمر.\nمثال: `!memory add الاجتماع كل تلات الساعة ٩`"
92
+ );
93
+ }
94
+ if (scope === "global" && !privileged) return denied();
95
+
96
+ const entry = memory.addMemory({
97
+ scope,
98
+ chatId,
99
+ content,
100
+ by: msg.pushName || senderId,
101
+ chatName: groupMetadata?.subject || null,
102
+ });
103
+
104
+ return reply(
105
+ `🧠 اتحفظت في *${scope === "global" ? "الذاكرة العامة" : "ذاكرة الشات"}*.\n` +
106
+ `\`${entry.id}\` — ${content.length > 80 ? content.slice(0, 80) + "…" : content}`
107
+ );
108
+ }
109
+
110
+ // ------------------------------------------------------------- search
111
+ if (["search", "find", "بحث", "دور"].includes(action)) {
112
+ const query = rest.join(" ").trim();
113
+ if (!query) return reply("اكتب الكلمة اللي بتدور عليها.");
114
+ const chatHits = memory.searchMemory(query, { scope: "chat", chatId });
115
+ const globalHits = memory.searchMemory(query, { scope: "global" });
116
+
117
+ if (!chatHits.length && !globalHits.length) {
118
+ return reply(`🔍 مفيش حاجة متسجلة عن "${query}".`);
119
+ }
120
+ return reply(
121
+ `🔍 *نتايج البحث عن* "${query}"\n\n` +
122
+ `*ذاكرة الشات (${chatHits.length}):*\n${formatEntries(chatHits, { limit: 10 })}\n\n` +
123
+ `*الذاكرة العامة (${globalHits.length}):*\n${formatEntries(globalHits, { limit: 10 })}`
124
+ );
125
+ }
126
+
127
+ // ------------------------------------------------------------- forget
128
+ if (["forget", "del", "delete", "remove", "امسح", "انسى"].includes(action)) {
129
+ if (!privileged) return denied();
130
+ const ref = rest.join(" ").trim();
131
+ if (!ref) {
132
+ return reply(
133
+ "حدد اللي عايز تمسحه: رقمه من `!memory` أو جزء من نصه.\n" +
134
+ "مثال: `!memory forget 3`"
135
+ );
136
+ }
137
+ const removed = memory.removeMemory({ scope, chatId, ref });
138
+ return reply(
139
+ removed
140
+ ? `🗑️ اتمسحت: ${removed.content.slice(0, 120)}`
141
+ : `مالقيتش حاجة تطابق "${ref}" في ${
142
+ scope === "global" ? "الذاكرة العامة" : "ذاكرة الشات"
143
+ }.`
144
+ );
145
+ }
146
+
147
+ // -------------------------------------------------------------- clear
148
+ if (["clear", "reset", "wipe", "تصفير"].includes(action)) {
149
+ if (!privileged) return denied();
150
+ const count = memory.clearMemory({ scope, chatId });
151
+ return reply(
152
+ count
153
+ ? `🧹 اتمسحت *${count}* معلومة من ${
154
+ scope === "global" ? "الذاكرة العامة" : "ذاكرة الشات"
155
+ }.`
156
+ : "الذاكرة كانت فاضية أصلاً."
157
+ );
158
+ }
159
+
160
+ // --------------------------------------------------------------- file
161
+ if (["file", "export", "md", "ملف"].includes(action)) {
162
+ if (!privileged) return denied();
163
+ const filePath = memory.memoryFilePath(scope, chatId);
164
+ if (!fs.existsSync(filePath)) {
165
+ return reply("مفيش ملف ذاكرة لسه — ابدأ بـ `!memory add ...`.");
166
+ }
167
+ return sendBotMessage(
168
+ sock,
169
+ chatId,
170
+ {
171
+ document: fs.readFileSync(filePath),
172
+ mimetype: "text/markdown",
173
+ fileName: scope === "global" ? "global.md" : `${chatId}.md`,
174
+ caption: `🧠 ملف ${
175
+ scope === "global" ? "الذاكرة العامة" : "ذاكرة الشات"
176
+ }`,
177
+ },
178
+ { replyTo: msg }
179
+ );
180
+ }
181
+
182
+ // --------------------------------------------------------------- list
183
+ const stats = memory.memoryStats(chatId);
184
+ const target = scope === "global" ? "global" : "chat";
185
+ const entries = memory.listMemory({ scope: target, chatId });
186
+
187
+ return reply(
188
+ `🧠 *${target === "global" ? "الذاكرة العامة" : "ذاكرة المحادثة دي"}* ` +
189
+ `(${entries.length} معلومة)\n\n` +
190
+ `${formatEntries(entries)}\n\n` +
191
+ `_الشات: ${stats.chatCount} · العام: ${stats.globalCount}_\n` +
192
+ "`!memory add <معلومة>` · `!memory add global <معلومة>` · `!memory forget <رقم>` · `!memory file`"
193
+ );
194
+ } catch (error) {
195
+ logger.error({ err: error }, "[memory] command failed");
196
+ return reply(
197
+ `❌ *مشكلة في الذاكرة*\n\n*التفاصيل:* ${error.message || error}`
198
+ );
199
+ }
200
+ },
201
+ };