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
@@ -34,6 +34,7 @@ const {
34
34
  } = require("../utils/storage-hub.cjs");
35
35
  const { sendBotMessage, sendBotError } = require("../utils/sendBotMessage.cjs");
36
36
  const { createStatus } = require("../utils/statusMessage.cjs");
37
+ const { formatMarkdownForWhatsApp } = require("../utils/markdownParser.cjs");
37
38
  const {
38
39
  isOwnerJidSync,
39
40
  isBotAdminUserSync,
@@ -46,24 +47,68 @@ const {
46
47
  sanitizeHistoryForFiles,
47
48
  activeProviderKeySetting,
48
49
  } = require("../services/aiAgent.cjs");
50
+ const { getProvider } = require("../services/aiRouter.cjs");
49
51
  const { downloadContentFromMessage } = require("@whiskeysockets/baileys");
50
52
  const fs = require("fs").promises;
51
53
  const path = require("path");
54
+ const {
55
+ assertProviderBaseUrl,
56
+ assertProviderRequestUrl,
57
+ } = require("../utils/providerUrl.cjs");
52
58
 
53
59
  // Per-(chat, sender) explicit context buffer for the multi-message workflow.
54
60
  // Entries accumulate until `!gemini send` flushes them into a single call.
55
61
  // Lives in memory only — short-lived by design.
56
- const contextBuffers = new Map(); // key: "<chatJid>::<senderJid>" -> Entry[]
62
+ const contextBuffers = new Map(); // key: "<chatJid>::<senderJid>" -> { entries, at }
63
+ const MAX_BUFFER_ENTRIES = 20;
64
+ const MAX_BUFFER_CHARS = 32_000;
65
+ const MAX_BUFFER_KEYS = 64;
66
+ const BUFFER_TTL_MS = 30 * 60 * 1000;
57
67
 
58
68
  function bufferKey(msg) {
59
69
  return `${msg.key.remoteJid}::${msg.key.participant || msg.key.remoteJid}`;
60
70
  }
71
+ function pruneBuffers(now = Date.now()) {
72
+ for (const [key, slot] of contextBuffers) {
73
+ if (!slot?.entries?.length || now - (slot.at || 0) > BUFFER_TTL_MS) {
74
+ contextBuffers.delete(key);
75
+ }
76
+ }
77
+ }
78
+ function entryChars(entry) {
79
+ let size = (entry?.text?.length || 0) + (entry?.quotedText?.length || 0);
80
+ for (const part of [...(entry?.mediaParts || []), ...(entry?.quotedMediaParts || [])]) {
81
+ size += JSON.stringify(part || {}).length;
82
+ }
83
+ return size;
84
+ }
61
85
  function getBuffer(msg) {
62
- return contextBuffers.get(bufferKey(msg)) || [];
86
+ pruneBuffers();
87
+ return contextBuffers.get(bufferKey(msg))?.entries || [];
63
88
  }
64
89
  function setBuffer(msg, entries) {
65
- if (!entries?.length) contextBuffers.delete(bufferKey(msg));
66
- else contextBuffers.set(bufferKey(msg), entries);
90
+ pruneBuffers();
91
+ const key = bufferKey(msg);
92
+ if (!entries?.length) {
93
+ contextBuffers.delete(key);
94
+ return true;
95
+ }
96
+ const capped = entries.slice(-MAX_BUFFER_ENTRIES);
97
+ let used = 0;
98
+ const kept = [];
99
+ for (let i = capped.length - 1; i >= 0; i--) {
100
+ const nextSize = entryChars(capped[i]);
101
+ if (nextSize > MAX_BUFFER_CHARS || used + nextSize > MAX_BUFFER_CHARS) break;
102
+ used += nextSize;
103
+ kept.unshift(capped[i]);
104
+ }
105
+ if (!kept.length) return false;
106
+ if (contextBuffers.size >= MAX_BUFFER_KEYS && !contextBuffers.has(key)) return false;
107
+ contextBuffers.set(key, { entries: kept, at: Date.now() });
108
+ return true;
109
+ }
110
+ function clearAllContextBuffers() {
111
+ contextBuffers.clear();
67
112
  }
68
113
 
69
114
  // Keys come from config/settings.cjs (what the dashboard saved, else the default)
@@ -78,7 +123,9 @@ let geminiCache = { key: null, baseUrl: null, genAI: null };
78
123
  function geminiClients() {
79
124
  const key = settings.get("gemini_api_key");
80
125
  if (!key) return { genAI: null };
81
- const baseUrl = String(settings.get("gemini_base_url")).replace(/\/+$/, "");
126
+ const baseUrl = assertProviderBaseUrl(settings.get("gemini_base_url"), {
127
+ allowLoopback: true,
128
+ });
82
129
  if (geminiCache.key !== key || geminiCache.baseUrl !== baseUrl) {
83
130
  geminiCache = {
84
131
  key,
@@ -90,58 +137,16 @@ function geminiClients() {
90
137
  }
91
138
 
92
139
  async function processIncomingMedia(parts, mediaMessage, mimeOverride = null) {
93
- // Reading media is a Gemini capability: the Files API it uploads to does not
94
- // exist on the openai/anthropic paths. There the turn carries a note and the
95
- // caption/question text still reaches the model.
96
- if (settings.get("ai_provider") !== "gemini") {
97
- const mime = mimeOverride || mediaMessage.mimetype || "ملف";
98
- parts.push({
99
- text: `[تم إرفاق وسائط (${mime}) — المزود الحالي لا يستطيع قراءة الوسائط]`,
100
- });
101
- return null;
140
+ const provider = getProvider();
141
+ if (provider.id === "gemini") {
142
+ await assertProviderRequestUrl(settings.get("gemini_base_url"), { allowLoopback: true });
102
143
  }
103
-
104
144
  const { genAI } = geminiClients();
105
- if (!genAI) throw new Error("Gemini client unavailable");
106
- const tempFilePath = path.join(__dirname, `temp_media_${Date.now()}`);
107
- const stream = await downloadContentFromMessage(
108
- mediaMessage,
109
- mediaMessage.mimetype?.startsWith("image/")
110
- ? "image"
111
- : mediaMessage.mimetype?.startsWith("video/")
112
- ? "video"
113
- : mediaMessage.mimetype?.startsWith("audio/")
114
- ? "audio"
115
- : "document"
116
- );
117
- let buffer = Buffer.from([]);
118
- for await (const chunk of stream) buffer = Buffer.concat([buffer, chunk]);
119
- if (!buffer.length) throw new Error("Empty media buffer");
120
-
121
- await fs.writeFile(tempFilePath, buffer);
122
- try {
123
- // ai.files.upload returns the File itself, where the old fileManager
124
- // wrapped it in { file }. The fileData part shape is unchanged, so history
125
- // written by an older Levix still loads.
126
- const uploaded = await genAI.files.upload({
127
- file: tempFilePath,
128
- config: {
129
- mimeType: mimeOverride || mediaMessage.mimetype,
130
- displayName: `media-${Date.now()}`,
131
- },
132
- });
133
- parts.push({
134
- fileData: {
135
- mimeType: mimeOverride || mediaMessage.mimetype,
136
- fileUri: uploaded.uri,
137
- },
138
- });
139
- return uploaded.uri;
140
- } finally {
141
- try {
142
- await fs.unlink(tempFilePath);
143
- } catch {}
144
- }
145
+ return provider.prepareMedia(parts, mediaMessage, mimeOverride, {
146
+ downloadContentFromMessage,
147
+ genAI,
148
+ tempDir: __dirname,
149
+ });
145
150
  }
146
151
 
147
152
  // === Multi-message context buffer helpers ===
@@ -164,15 +169,14 @@ async function captureContextEntry(msg, args) {
164
169
 
165
170
  const m = msg.message || {};
166
171
 
167
- const directMedia =
168
- m.imageMessage || m.videoMessage || m.audioMessage || m.documentMessage;
172
+ const directMedia = m.imageMessage || m.videoMessage || m.audioMessage || m.documentMessage;
169
173
  if (directMedia) {
170
174
  try {
171
175
  await processIncomingMedia(entry.mediaParts, directMedia);
172
176
  } catch (err) {
173
177
  logger.warn(
174
178
  { err: err?.message },
175
- "[Gemini Context] direct media upload failed; entry will be text-only"
179
+ "[Gemini Context] direct media upload failed; entry will be text-only",
176
180
  );
177
181
  }
178
182
  }
@@ -187,17 +191,14 @@ async function captureContextEntry(msg, args) {
187
191
  quoted.documentMessage?.caption ||
188
192
  "";
189
193
  const quotedMedia =
190
- quoted.imageMessage ||
191
- quoted.videoMessage ||
192
- quoted.audioMessage ||
193
- quoted.documentMessage;
194
+ quoted.imageMessage || quoted.videoMessage || quoted.audioMessage || quoted.documentMessage;
194
195
  if (quotedMedia) {
195
196
  try {
196
197
  await processIncomingMedia(entry.quotedMediaParts, quotedMedia);
197
198
  } catch (err) {
198
199
  logger.warn(
199
200
  { err: err?.message },
200
- "[Gemini Context] quoted media upload failed; entry will continue without it"
201
+ "[Gemini Context] quoted media upload failed; entry will continue without it",
201
202
  );
202
203
  }
203
204
  }
@@ -207,9 +208,7 @@ async function captureContextEntry(msg, args) {
207
208
  }
208
209
 
209
210
  function entryHasContent(e) {
210
- return Boolean(
211
- e.text || e.quotedText || e.mediaParts?.length || e.quotedMediaParts?.length
212
- );
211
+ return Boolean(e.text || e.quotedText || e.mediaParts?.length || e.quotedMediaParts?.length);
213
212
  }
214
213
 
215
214
  function buildPartsFromBuffer(entries) {
@@ -230,13 +229,11 @@ function summarizeEntry(e, idx) {
230
229
  bits.push(`📝 ${preview}`);
231
230
  }
232
231
  if (e.quotedText) {
233
- const preview =
234
- e.quotedText.length > 40 ? e.quotedText.slice(0, 40) + "…" : e.quotedText;
232
+ const preview = e.quotedText.length > 40 ? e.quotedText.slice(0, 40) + "…" : e.quotedText;
235
233
  bits.push(`↩️ "${preview}"`);
236
234
  }
237
235
  if (e.mediaParts?.length) bits.push(`🖼️ ميديا (${e.mediaParts.length})`);
238
- if (e.quotedMediaParts?.length)
239
- bits.push(`🖼️↩️ ميديا مقتبسة (${e.quotedMediaParts.length})`);
236
+ if (e.quotedMediaParts?.length) bits.push(`🖼️↩️ ميديا مقتبسة (${e.quotedMediaParts.length})`);
240
237
  return `${idx + 1}. ${bits.join(" · ") || "(فارغة)"}`;
241
238
  }
242
239
 
@@ -254,18 +251,10 @@ module.exports = {
254
251
  const userName = msg.pushName;
255
252
  const isGroup = chatId.endsWith("@g.us");
256
253
 
257
- const senderId = msg.key.fromMe
258
- ? null
259
- : isGroup
260
- ? msg.key.participant
261
- : msg.key.remoteJid;
254
+ const senderId = msg.key.fromMe ? null : isGroup ? msg.key.participant : msg.key.remoteJid;
262
255
  const isOwner = msg.key.fromMe || isOwnerJidSync(senderId);
263
- // نفس تعريف "privileged" في أمر !memory: أدمن البوت أو أدمن الجروب —
264
- // عشان أدوات الوكيل تطبّق نفس القاعدة اللي الأوامر بتطبّقها.
265
- const isAdmin =
266
- isOwner ||
267
- isBotAdminUserSync(senderId) ||
268
- (isGroup && isAdminInGroupSync(groupMetadata, senderId));
256
+ const isBotAdmin = isOwner || isBotAdminUserSync(senderId);
257
+ const isGroupAdmin = Boolean(isGroup && isAdminInGroupSync(groupMetadata, senderId));
269
258
 
270
259
  // Resolve sub-command. Aliases like !del arrive via ctx.invokedName, while
271
260
  // `!gemini del ...` arrives as args[0].
@@ -285,8 +274,8 @@ module.exports = {
285
274
  const subCommand = aliasSubCmds.has(invoked)
286
275
  ? invoked
287
276
  : inlineSubCmds.has(candidateInline)
288
- ? candidateInline
289
- : null;
277
+ ? candidateInline
278
+ : null;
290
279
 
291
280
  // --- conversation history management (owner only) ---
292
281
  if (subCommand === "del" || subCommand === "resetai") {
@@ -295,7 +284,7 @@ module.exports = {
295
284
  sock,
296
285
  chatId,
297
286
  { text: "🚫 هذا الأمر مخصص للمالك فقط." },
298
- { replyTo: msg }
287
+ { replyTo: msg },
299
288
  );
300
289
  await deleteChatHistoryAsync(chatId);
301
290
  return sendBotMessage(
@@ -306,7 +295,7 @@ module.exports = {
306
295
  "✅ تم مسح سجل المحادثة.\n" +
307
296
  "_(الذاكرة الدائمة مش بتتمسح كده — استخدم `!memory clear` لو ده اللي تقصده.)_",
308
297
  },
309
- { replyTo: msg }
298
+ { replyTo: msg },
310
299
  );
311
300
  }
312
301
  if (subCommand === "delall") {
@@ -315,21 +304,20 @@ module.exports = {
315
304
  sock,
316
305
  chatId,
317
306
  { text: "🚫 هذا الأمر مخصص للمالك فقط." },
318
- { replyTo: msg }
307
+ { replyTo: msg },
319
308
  );
320
309
  await deleteAllChatHistoriesAsync();
321
310
  return sendBotMessage(
322
311
  sock,
323
312
  chatId,
324
313
  { text: "✅ تم مسح كل سجلات المحادثات بنجاح." },
325
- { replyTo: msg }
314
+ { replyTo: msg },
326
315
  );
327
316
  }
328
317
 
329
318
  // --- image generation ---
330
319
  if (subCommand === "generate") {
331
- const quotedMsg =
332
- msg.message?.extendedTextMessage?.contextInfo?.quotedMessage?.conversation;
320
+ const quotedMsg = msg.message?.extendedTextMessage?.contextInfo?.quotedMessage?.conversation;
333
321
  const imagePromptArgs = invoked === "generate" ? args : args.slice(1);
334
322
  const imagePrompt = imagePromptArgs.join(" ");
335
323
  if (!imagePrompt && !quotedMsg) {
@@ -339,32 +327,28 @@ module.exports = {
339
327
  {
340
328
  text: "اكتب وصف للصورة اللي عايزها بعد الأمر. مثال: !generate قطة ترتدي قبعة",
341
329
  },
342
- { replyTo: msg }
330
+ { replyTo: msg },
343
331
  );
344
332
  }
345
333
 
346
- const status = await createStatus(
347
- sock,
348
- chatId,
349
- "🎨 بجهّز الصورة...",
350
- { replyTo: msg }
351
- );
334
+ const status = await createStatus(sock, chatId, "🎨 بجهّز الصورة...", { replyTo: msg });
352
335
 
353
336
  try {
354
337
  // Image generation needs a model that returns image bytes, which only
355
338
  // the Gemini image models do — it stays on Gemini whatever the chat
356
339
  // provider is.
357
340
  if (!geminiClients().genAI) {
358
- throw new Error(
359
- "مفتاح Gemini API غير معرف — توليد الصور يعمل على Gemini فقط"
360
- );
341
+ throw new Error("مفتاح Gemini API غير معرف — توليد الصور يعمل على Gemini فقط");
361
342
  }
343
+ await assertProviderRequestUrl(settings.get("gemini_base_url"), {
344
+ allowLoopback: true,
345
+ });
362
346
  const fullPrompt =
363
347
  imagePrompt && quotedMsg
364
348
  ? `Prompt: ${imagePrompt} ,using quote: ${quotedMsg}`
365
349
  : imagePrompt
366
- ? `Prompt: ${imagePrompt}`
367
- : `Prompt: ${quotedMsg}`;
350
+ ? `Prompt: ${imagePrompt}`
351
+ : `Prompt: ${quotedMsg}`;
368
352
  const response = await geminiClients().genAI.models.generateContent({
369
353
  model: settings.get("gemini_image_model"),
370
354
  contents: `Generate an image using this prompt: ${fullPrompt}`,
@@ -374,7 +358,7 @@ module.exports = {
374
358
  // `fileData.data`, which is not a field that exists on either SDK —
375
359
  // this path could never have produced an image.)
376
360
  const image = (response.candidates?.[0]?.content?.parts || []).find(
377
- (part) => part?.inlineData?.data
361
+ (part) => part?.inlineData?.data,
378
362
  );
379
363
  if (!image) throw new Error("الموديل رجّع رد من غير صورة");
380
364
  const imageBuffer = Buffer.from(image.inlineData.data, "base64");
@@ -387,7 +371,7 @@ module.exports = {
387
371
  image: imageBuffer,
388
372
  caption: `🖼️ تفضل، صورة لـ: "${imagePrompt}"`,
389
373
  },
390
- { replyTo: msg }
374
+ { replyTo: msg },
391
375
  );
392
376
  } catch (error) {
393
377
  logger.error({ err: error }, `Error in !generate command`);
@@ -408,21 +392,41 @@ module.exports = {
408
392
  "📭 مفيش حاجة أضيفها للـ context.\n\n" +
409
393
  "اكتب نص بعد الأمر، أو رد على رسالة، أو ابعت ميديا مع كابشن `!gemini add`.",
410
394
  },
411
- { replyTo: msg }
395
+ { replyTo: msg },
412
396
  );
413
397
  }
414
398
  const buf = getBuffer(msg);
399
+ if (buf.length >= MAX_BUFFER_ENTRIES) {
400
+ return sendBotMessage(
401
+ sock,
402
+ chatId,
403
+ { text: `الـ context ممتلئ (حد أقصى ${MAX_BUFFER_ENTRIES} رسائل). ابعت \`!gemini send\` أو \`!gemini clear\`.` },
404
+ { replyTo: msg },
405
+ );
406
+ }
415
407
  buf.push(entry);
416
- setBuffer(msg, buf);
408
+ if (!setBuffer(msg, buf)) {
409
+ return sendBotMessage(
410
+ sock,
411
+ chatId,
412
+ {
413
+ text:
414
+ "الـ context كبير جدًا أو عدد الـ contexts المفتوحة وصل للحد. " +
415
+ "ابعت `!gemini send` أو `!gemini clear` وحاول تاني.",
416
+ },
417
+ { replyTo: msg },
418
+ );
419
+ }
420
+ const stored = getBuffer(msg);
417
421
  return sendBotMessage(
418
422
  sock,
419
423
  chatId,
420
424
  {
421
425
  text:
422
- `✅ اتضافت للـ context. (إجمالي الآن: *${buf.length}*)\n\n` +
426
+ `✅ اتضافت للـ context. (إجمالي الآن: *${stored.length}*)\n\n` +
423
427
  "كمل بـ `!gemini add ...` ، أو ابعت كله بـ `!gemini send`.",
424
428
  },
425
- { replyTo: msg }
429
+ { replyTo: msg },
426
430
  );
427
431
  }
428
432
 
@@ -433,11 +437,9 @@ module.exports = {
433
437
  sock,
434
438
  chatId,
435
439
  {
436
- text: had
437
- ? `🗑️ تم مسح *${had}* رسالة من الـ context.`
438
- : "📭 الـ context كان فاضي أصلاً.",
440
+ text: had ? `🗑️ تم مسح *${had}* رسالة من الـ context.` : "📭 الـ context كان فاضي أصلاً.",
439
441
  },
440
- { replyTo: msg }
442
+ { replyTo: msg },
441
443
  );
442
444
  }
443
445
 
@@ -448,7 +450,7 @@ module.exports = {
448
450
  sock,
449
451
  chatId,
450
452
  { text: "📭 الـ context فاضي. ابدأ بـ `!gemini add ...`." },
451
- { replyTo: msg }
453
+ { replyTo: msg },
452
454
  );
453
455
  }
454
456
  return sendBotMessage(
@@ -460,7 +462,7 @@ module.exports = {
460
462
  buf.map((e, i) => summarizeEntry(e, i)).join("\n") +
461
463
  "\n\nابعت كله بـ `!gemini send` أو امسحه بـ `!gemini clear`.",
462
464
  },
463
- { replyTo: msg }
465
+ { replyTo: msg },
464
466
  );
465
467
  }
466
468
 
@@ -482,7 +484,7 @@ module.exports = {
482
484
  "📭 الـ context فاضي ومفيش نص في `!gemini send`.\n\n" +
483
485
  "ابدأ بـ `!gemini add ...` الأول، أو اكتب نص مع `!gemini send ...`.",
484
486
  },
485
- { replyTo: msg }
487
+ { replyTo: msg },
486
488
  );
487
489
  }
488
490
  setBuffer(msg, []);
@@ -496,18 +498,13 @@ module.exports = {
496
498
  try {
497
499
  await processIncomingMedia(parts, mediaMessage);
498
500
  parts.push({
499
- text:
500
- prompt || mediaMessage.caption || "ماذا يوجد في هذه الصورة/الفيديو؟",
501
+ text: prompt || mediaMessage.caption || "ماذا يوجد في هذه الصورة/الفيديو؟",
501
502
  });
502
503
  } catch (error) {
503
504
  logger.error({ err: error }, "Failed to upload media to Gemini.");
504
- return sendBotError(
505
- sock,
506
- chatId,
507
- error,
508
- "حصلت مشكلة في رفع الصورة/الفيديو",
509
- { replyTo: msg }
510
- );
505
+ return sendBotError(sock, chatId, error, "حصلت مشكلة في رفع الصورة/الفيديو", {
506
+ replyTo: msg,
507
+ });
511
508
  }
512
509
  } else if (!parts.length && msg.message?.audioMessage) {
513
510
  try {
@@ -515,13 +512,9 @@ module.exports = {
515
512
  parts.push({ text: prompt || "حلل لي هذا التسجيل الصوتي." });
516
513
  } catch (error) {
517
514
  logger.error({ err: error }, "Failed to upload audio to Gemini.");
518
- return sendBotError(
519
- sock,
520
- chatId,
521
- error,
522
- "حصلت مشكلة في رفع التسجيل الصوتي",
523
- { replyTo: msg }
524
- );
515
+ return sendBotError(sock, chatId, error, "حصلت مشكلة في رفع التسجيل الصوتي", {
516
+ replyTo: msg,
517
+ });
525
518
  }
526
519
  } else if (!parts.length && msg.message?.documentMessage) {
527
520
  try {
@@ -529,9 +522,7 @@ module.exports = {
529
522
  const fileName = msg.message.documentMessage.fileName || "document";
530
523
  const text = prompt || msg.message.documentMessage.caption || "";
531
524
  parts.push({
532
- text: text
533
- ? `${text}\n(الملف: ${fileName})`
534
- : `حلل/لخّص الملف: ${fileName}`,
525
+ text: text ? `${text}\n(الملف: ${fileName})` : `حلل/لخّص الملف: ${fileName}`,
535
526
  });
536
527
  } catch (error) {
537
528
  logger.error({ err: error }, "Failed to upload document to Gemini.");
@@ -548,16 +539,14 @@ module.exports = {
548
539
  sock,
549
540
  chatId,
550
541
  { text: "يرجى كتابة سؤال أو إرسال صورة/فيديو/تسجيل صوتي/ملف مع الأمر." },
551
- { replyTo: msg }
542
+ { replyTo: msg },
552
543
  );
553
544
  }
554
545
 
555
546
  // Quoted message context. Skipped for `!gemini send` — those entries
556
547
  // captured their own quotes at `add` time.
557
548
  const quotedMsg =
558
- subCommand === "send"
559
- ? null
560
- : msg.message?.extendedTextMessage?.contextInfo?.quotedMessage;
549
+ subCommand === "send" ? null : msg.message?.extendedTextMessage?.contextInfo?.quotedMessage;
561
550
 
562
551
  if (quotedMsg) {
563
552
  let quotedMediaPart = null;
@@ -579,13 +568,9 @@ module.exports = {
579
568
  quotedMediaPart = probeParts[0];
580
569
  } catch (mediaError) {
581
570
  logger.error({ err: mediaError }, "Failed to process quoted media");
582
- await sendBotError(
583
- sock,
584
- chatId,
585
- mediaError,
586
- "حصلت مشكلة في الرسالة المقتبسة",
587
- { replyTo: msg }
588
- );
571
+ await sendBotError(sock, chatId, mediaError, "حصلت مشكلة في الرسالة المقتبسة", {
572
+ replyTo: msg,
573
+ });
589
574
  }
590
575
  }
591
576
 
@@ -603,17 +588,16 @@ module.exports = {
603
588
  }
604
589
  }
605
590
 
606
- // Provider-aware, on purpose: when the panel picked openai or anthropic,
607
- // the bot answers with that key and a missing Gemini key is irrelevant.
608
- if (!settings.get(activeProviderKeySetting())) {
609
- const provider = settings.get("ai_provider");
591
+ // Provider-aware: delegates configuration check to the active provider adapter.
592
+ const activeProvider = getProvider();
593
+ if (!activeProvider.isConfigured()) {
610
594
  return sendBotMessage(
611
595
  sock,
612
596
  chatId,
613
597
  {
614
- text: `خطأ في الإعدادات: مفتاح مزود الذكاء الاصطناعي الحالي (${provider}) غير معرف — عدّله من لوحة التحكم.`,
598
+ text: `خطأ في الإعدادات: مفتاح مزود الذكاء الاصطناعي الحالي (${activeProvider.label || activeProvider.id}) غير معرف — عدّله من لوحة التحكم.`,
615
599
  },
616
- { replyTo: msg }
600
+ { replyTo: msg },
617
601
  );
618
602
  }
619
603
 
@@ -625,19 +609,20 @@ module.exports = {
625
609
  senderId,
626
610
  senderName: userName,
627
611
  isOwner,
628
- isAdmin,
612
+ isAdmin: isBotAdmin,
613
+ isGroupAdmin,
629
614
  isGroup,
630
615
  chatName: groupMetadata?.subject || null,
631
616
  mentionedJids: contextInfo?.mentionedJid || [],
632
617
  quotedParticipant: contextInfo?.participant || null,
618
+ userText: body || "",
633
619
  };
634
620
 
635
621
  // Mentions/replies are ids the model can't invent — hand them over so
636
622
  // "اعمل الراجل ده أدمن" has something concrete to act on.
637
- const targets = [
638
- ...(agentContext.mentionedJids || []),
639
- agentContext.quotedParticipant,
640
- ].filter(Boolean);
623
+ const targets = [...(agentContext.mentionedJids || []), agentContext.quotedParticipant].filter(
624
+ Boolean,
625
+ );
641
626
  if (targets.length) {
642
627
  parts.unshift({
643
628
  text: `(معرفات مذكورة في الرسالة: ${[...new Set(targets)].join(", ")})`,
@@ -645,7 +630,7 @@ module.exports = {
645
630
  }
646
631
 
647
632
  // The one message that will carry the whole run.
648
- const status = await createStatus(sock, chatId, "🤖 بفكر...", {
633
+ const status = await createStatus(sock, chatId, "🤖 جاري التفكير...", {
649
634
  replyTo: msg,
650
635
  });
651
636
 
@@ -676,21 +661,22 @@ module.exports = {
676
661
  const cleaned = sanitizeHistoryForFiles(oldHistory);
677
662
  await saveChatHistoryAsync(chatId, cleaned);
678
663
  logger.info(
679
- `[Gemini] sanitized history for chat ${chatId}: ${oldHistory.length} -> ${cleaned.length}; retrying once`
664
+ `[Gemini] sanitized history for chat ${chatId}: ${oldHistory.length} -> ${cleaned.length}; retrying once`,
680
665
  );
681
666
  result = await attempt(cleaned);
682
667
  }
683
668
 
684
- const text =
685
- result.text?.trim() ||
686
- "مفيش رد جه من الموديل. جرّب تصيغ السؤال بشكل تاني.";
669
+ const text = result.text?.trim() || "مفيش رد جه من الموديل. جرّب تصيغ السؤال بشكل تاني.";
687
670
  // Appended only when Gemini really grounded the answer on a search —
688
671
  // formatSources() returns "" for an answer the model gave from its own
689
672
  // knowledge, so there is never a Sources block with nothing behind it.
690
- await status.finish(`${text}${formatSources(result.sources)}`);
673
+ const responseText = formatMarkdownForWhatsApp(`${text}${formatSources(result.sources)}`);
674
+ await status.finish(responseText);
691
675
  } catch (error) {
692
676
  logger.error({ err: error }, "Error in !gemini command");
693
677
  await status.fail(error, "حصلت مشكلة وأنا بكلم الذكاء الاصطناعي");
694
678
  }
695
679
  },
696
680
  };
681
+
682
+ module.exports.clearAllContextBuffers = clearAllContextBuffers;
@@ -4,8 +4,7 @@ const normalizeJid = require("../../utils/normalizeJid.esm.js").default;
4
4
 
5
5
  module.exports = {
6
6
  name: "add",
7
- description:
8
- "Adds a member to the group or sends an invite if privacy settings block it.",
7
+ description: "Adds a member to the group or sends an invite if privacy settings block it.",
9
8
  usage: "add [admin] <رقم الموبايل>",
10
9
  chat: "group",
11
10
  userAdminRequired: true,
@@ -22,25 +21,21 @@ module.exports = {
22
21
  try {
23
22
  // --- ✅ THIS IS THE MISSING LOGIC TO DEFINE targetJid ---
24
23
  // 1. Identify the Target User
25
- const mentionedJid =
26
- msg.message?.extendedTextMessage?.contextInfo?.mentionedJid?.[0];
24
+ const mentionedJid = msg.message?.extendedTextMessage?.contextInfo?.mentionedJid?.[0];
27
25
  let contactJid;
28
26
 
29
27
  // Check for contact card in a quoted message
30
28
  const quotedVcard =
31
- msg.message?.extendedTextMessage?.contextInfo?.quotedMessage
32
- ?.contactMessage?.vcard;
29
+ msg.message?.extendedTextMessage?.contextInfo?.quotedMessage?.contactMessage?.vcard;
33
30
  if (quotedVcard) {
34
- contactJid =
35
- quotedVcard.match(/waid=([0-9]+)/)?.[1] + "@s.whatsapp.net";
31
+ contactJid = quotedVcard.match(/waid=([0-9]+)/)?.[1] + "@s.whatsapp.net";
36
32
  }
37
33
 
38
34
  // If not found, check for contact card in the main message
39
35
  if (!contactJid || !contactJid.includes("@")) {
40
36
  const mainVcard = msg.message?.contactMessage?.vcard;
41
37
  if (mainVcard) {
42
- contactJid =
43
- mainVcard.match(/waid=([0-9]+)/)?.[1] + "@s.whatsapp.net";
38
+ contactJid = mainVcard.match(/waid=([0-9]+)/)?.[1] + "@s.whatsapp.net";
44
39
  }
45
40
  }
46
41
 
@@ -70,7 +65,7 @@ module.exports = {
70
65
 
71
66
  // Pre-check if the user is already in the group
72
67
  const isAlreadyMember = groupMetadata.participants.some(
73
- (p) => normalizeJid(p.id) === normalizeJid(targetJid)
68
+ (p) => normalizeJid(p.id) === normalizeJid(targetJid),
74
69
  );
75
70
  if (isAlreadyMember) {
76
71
  return await sock.sendMessage(groupId, {
@@ -83,11 +78,7 @@ module.exports = {
83
78
  text: `جاري محاولة إضافة @${targetJid.split("@")[0]}...`,
84
79
  mentions: [targetJid],
85
80
  });
86
- const response = await sock.groupParticipantsUpdate(
87
- groupId,
88
- [targetJid],
89
- "add"
90
- );
81
+ const response = await sock.groupParticipantsUpdate(groupId, [targetJid], "add");
91
82
  const status = response[0].status;
92
83
 
93
84
  if (status === "200") {