levix-bot 3.3.0-rc.2 → 4.0.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +11 -7
  2. package/app.cjs +42 -45
  3. package/package.json +9 -1
  4. package/public/dashboard/assets/index-C1b7ijfu.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 -125
  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
@@ -13,6 +13,8 @@
13
13
  // isn't allowed to do it.
14
14
 
15
15
  const dns = require("node:dns").promises;
16
+ const http = require("node:http");
17
+ const https = require("node:https");
16
18
  const net = require("node:net");
17
19
 
18
20
  const axios = require("axios");
@@ -20,11 +22,8 @@ const axios = require("axios");
20
22
  const logger = require("../utils/logger.cjs");
21
23
  const memory = require("../utils/memory.cjs");
22
24
  const { decodeBuffer, stripHtml, decodeText } = require("../utils/textDecode.cjs");
23
- const {
24
- grantRole,
25
- revokeRole,
26
- listRoles,
27
- } = require("../utils/permissions.cjs");
25
+ const { grantRole, revokeRole, listRoles } = require("../utils/permissions.cjs");
26
+ const { isForbiddenIp } = require("../utils/providerUrl.cjs");
28
27
 
29
28
  // @google/genai exports a `Type` enum (the old SDK called it `SchemaType`).
30
29
  // Its members are the uppercase wire values — Type.STRING === "STRING" — so the
@@ -83,16 +82,11 @@ async function searchDuckDuckGo(query, limit) {
83
82
  validateStatus: (status) => status >= 200 && status < 500,
84
83
  });
85
84
 
86
- const html = decodeBuffer(
87
- Buffer.from(response.data),
88
- response.headers["content-type"]
89
- );
85
+ const html = decodeBuffer(Buffer.from(response.data), response.headers["content-type"]);
90
86
 
91
87
  const results = [];
92
- const linkRe =
93
- /<a[^>]+class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
94
- const snippetRe =
95
- /<a[^>]+class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>/gi;
88
+ const linkRe = /<a[^>]+class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
89
+ const snippetRe = /<a[^>]+class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>/gi;
96
90
 
97
91
  const snippets = [];
98
92
  let snippetMatch;
@@ -114,18 +108,15 @@ async function searchDuckDuckGo(query, limit) {
114
108
  }
115
109
 
116
110
  async function searchGoogleCse(query, limit) {
117
- const response = await axios.get(
118
- "https://www.googleapis.com/customsearch/v1",
119
- {
120
- params: {
121
- key: settings.get("google_search_api_key"),
122
- cx: settings.get("google_search_cx"),
123
- q: query,
124
- num: Math.min(10, limit),
125
- },
126
- timeout: 20000,
127
- }
128
- );
111
+ const response = await axios.get("https://www.googleapis.com/customsearch/v1", {
112
+ params: {
113
+ key: settings.get("google_search_api_key"),
114
+ cx: settings.get("google_search_cx"),
115
+ q: query,
116
+ num: Math.min(10, limit),
117
+ },
118
+ timeout: 20000,
119
+ });
129
120
  return (response.data?.items || []).slice(0, limit).map((item) => ({
130
121
  title: item.title,
131
122
  url: item.link,
@@ -147,6 +138,85 @@ async function webSearch(query, limit = 5) {
147
138
  return searchDuckDuckGo(query, limit);
148
139
  }
149
140
 
141
+ async function searchNews(query, limit = 5) {
142
+ const newsQuery = `${query} news أخبار`;
143
+ return webSearch(newsQuery, limit);
144
+ }
145
+
146
+ async function enrichWikiResults(lang, titles, urls, limit) {
147
+ const count = Math.min(titles?.length || 0, limit);
148
+ const items = [];
149
+ for (let i = 0; i < count; i++) {
150
+ items.push({ title: titles[i], url: urls[i] });
151
+ }
152
+
153
+ const results = await Promise.all(
154
+ items.map(async ({ title, url }) => {
155
+ let extract = "";
156
+ try {
157
+ const summaryRes = await axios.get(
158
+ `https://${lang}.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(title)}`,
159
+ {
160
+ timeout: 8000,
161
+ headers: { "User-Agent": "LevixBot/3.4 (https://github.com/Abdodiab2005/levix)" },
162
+ },
163
+ );
164
+ extract = summaryRes.data?.extract || summaryRes.data?.description || "";
165
+ } catch {
166
+ // Non-fatal: summary lookup failure retains title and URL.
167
+ }
168
+ return { title, url, extract: extract.slice(0, 1000) };
169
+ }),
170
+ );
171
+
172
+ return { language: lang, count: results.length, results };
173
+ }
174
+
175
+ async function searchWikipedia(query, language = "auto", limit = 3) {
176
+ let lang = language;
177
+ if (!lang || lang === "auto") {
178
+ lang = /[\u0600-\u06FF]/.test(query) ? "ar" : "en";
179
+ }
180
+ const cleanLang = lang.toLowerCase() === "ar" ? "ar" : "en";
181
+ try {
182
+ const searchRes = await axios.get(`https://${cleanLang}.wikipedia.org/w/api.php`, {
183
+ params: {
184
+ action: "opensearch",
185
+ search: query,
186
+ limit: Math.min(5, Math.max(1, limit)),
187
+ namespace: 0,
188
+ format: "json",
189
+ },
190
+ timeout: 10000,
191
+ headers: { "User-Agent": "LevixBot/3.4 (https://github.com/Abdodiab2005/levix)" },
192
+ });
193
+ const [, titles, , urls] = searchRes.data || [];
194
+ if (!titles || !titles.length) {
195
+ const altLang = cleanLang === "ar" ? "en" : "ar";
196
+ const altRes = await axios.get(`https://${altLang}.wikipedia.org/w/api.php`, {
197
+ params: {
198
+ action: "opensearch",
199
+ search: query,
200
+ limit: Math.min(5, Math.max(1, limit)),
201
+ namespace: 0,
202
+ format: "json",
203
+ },
204
+ timeout: 10000,
205
+ headers: { "User-Agent": "LevixBot/3.4 (https://github.com/Abdodiab2005/levix)" },
206
+ });
207
+ const [, altTitles, , altUrls] = altRes.data || [];
208
+ if (!altTitles || !altTitles.length) {
209
+ return { query, count: 0, results: [], note: "no Wikipedia articles found" };
210
+ }
211
+ return await enrichWikiResults(altLang, altTitles, altUrls, limit);
212
+ }
213
+ return await enrichWikiResults(cleanLang, titles, urls, limit);
214
+ } catch (err) {
215
+ logger.warn({ err: err.message }, "[aiTools] Wikipedia search failed");
216
+ return { error: `Wikipedia search failed: ${err.message}` };
217
+ }
218
+ }
219
+
150
220
  // ---------------------------------------------------------------------------
151
221
  // url fetching
152
222
  // ---------------------------------------------------------------------------
@@ -157,49 +227,9 @@ async function webSearch(query, limit = 5) {
157
227
  // و 2130706433 و [::ffff:127.0.0.1] نفس العنوان بصيغة تانية، والأهم إن رابط
158
228
  // عام ممكن يعمل redirect على 169.254.169.254 (مفاتيح السيرفر عند أغلب
159
229
  // مزودي الاستضافة). فبنـ resolve العنوان ونفحص كل خطوة تحويل لوحدها.
160
- // ::ffff:127.0.0.1 بيتكتب برضه ::ffff:7f00:1 — والـ URL parser بيطبّع الشكل
161
- // التاني، فلازم نفك الصيغة السداسية كمان مش الشكل بالنقط بس.
162
- function mappedIpv4(v6) {
163
- const dotted = /^::(?:ffff:)?(\d+\.\d+\.\d+\.\d+)$/.exec(v6);
164
- if (dotted) return dotted[1];
165
-
166
- const hex = /^::(?:ffff:)?([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(v6);
167
- if (!hex) return null;
168
- const high = parseInt(hex[1], 16);
169
- const low = parseInt(hex[2], 16);
170
- return `${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`;
171
- }
172
-
173
- function isPrivateIp(ip) {
174
- const family = net.isIP(ip);
175
-
176
- if (family === 4) {
177
- const [a, b] = ip.split(".").map(Number);
178
- if (a === 0 || a === 10 || a === 127) return true;
179
- if (a === 169 && b === 254) return true; // link-local + metadata
180
- if (a === 172 && b >= 16 && b <= 31) return true;
181
- if (a === 192 && b === 168) return true;
182
- if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT
183
- if (a >= 224) return true; // multicast + reserved
184
- return false;
185
- }
186
-
187
- if (family === 6) {
188
- const v6 = ip.toLowerCase();
189
- if (v6 === "::" || v6 === "::1") return true;
190
- if (/^f[cd]/.test(v6)) return true; // unique local
191
- if (v6.startsWith("fe80")) return true; // link local
192
- const mapped = mappedIpv4(v6);
193
- if (mapped) return isPrivateIp(mapped);
194
- return false;
195
- }
196
-
197
- return true; // مش عنوان صالح أصلاً
198
- }
199
-
200
- // ملاحظة: فيه فرصة نظرية إن الـ DNS يرد بعنوان تاني بين الفحص والاتصال
201
- // (DNS rebinding). قفلها بالكامل محتاج تثبيت الـ IP على مستوى الـ socket؛
202
- // اللي هنا بيقفل الحالات العملية: أسماء داخلية، صيغ IP بديلة، والتحويلات.
230
+ // Resolve once, reject every local/reserved answer, then pin the HTTP socket to
231
+ // one of those exact answers. That closes the gap where a rebinding hostname
232
+ // could return a public address during validation and loopback during connect.
203
233
  async function assertPublicUrl(rawUrl) {
204
234
  let url;
205
235
  try {
@@ -212,61 +242,97 @@ async function assertPublicUrl(rawUrl) {
212
242
  const host = url.hostname.replace(/^\[|\]$/g, "");
213
243
 
214
244
  if (net.isIP(host)) {
215
- if (isPrivateIp(host)) throw new Error("الرابط ده ممنوع");
216
- return url;
245
+ if (isForbiddenIp(host)) throw new Error("الرابط ده ممنوع");
246
+ return { url, host, addresses: [{ address: host, family: net.isIP(host) }] };
217
247
  }
218
248
 
219
249
  let addresses;
220
250
  try {
221
- addresses = await dns.lookup(host, { all: true });
251
+ addresses = await dns.lookup(host, { all: true, verbatim: true });
222
252
  } catch {
223
253
  throw new Error("مش قادر أوصل للدومين ده");
224
254
  }
225
- if (!addresses.length || addresses.some((a) => isPrivateIp(a.address))) {
255
+ if (!addresses.length || addresses.some((a) => isForbiddenIp(a.address))) {
226
256
  throw new Error("الرابط ده ممنوع");
227
257
  }
228
258
 
229
- return url;
259
+ return { url, host, addresses };
260
+ }
261
+
262
+ function pinnedAgent(protocol, expectedHost, addresses) {
263
+ const lookup = (hostname, options, callback) => {
264
+ if (String(hostname).toLowerCase() !== expectedHost.toLowerCase()) {
265
+ const error = new Error("اتصال بدومين غير متوقع");
266
+ error.code = "EAI_FAIL";
267
+ return callback(error);
268
+ }
269
+
270
+ const requestedFamily = typeof options === "number" ? options : options?.family;
271
+ const candidates = requestedFamily
272
+ ? addresses.filter(({ family }) => family === requestedFamily)
273
+ : addresses;
274
+ if (!candidates.length) {
275
+ const error = new Error("مفيش عنوان متوافق للدومين ده");
276
+ error.code = "EAI_ADDRFAMILY";
277
+ return callback(error);
278
+ }
279
+ if (typeof options === "object" && options?.all) {
280
+ return callback(null, candidates.map(({ address, family }) => ({ address, family })));
281
+ }
282
+ return callback(null, candidates[0].address, candidates[0].family);
283
+ };
284
+
285
+ const Agent = protocol === "https:" ? https.Agent : http.Agent;
286
+ return new Agent({ keepAlive: false, lookup });
230
287
  }
231
288
 
232
289
  const MAX_REDIRECTS = 4;
233
290
 
234
291
  async function fetchUrl(rawUrl) {
235
- let url = await assertPublicUrl(rawUrl);
292
+ let target = await assertPublicUrl(rawUrl);
236
293
  let response;
237
294
 
238
295
  for (let hop = 0; ; hop++) {
239
- response = await axios.get(url.toString(), {
240
- responseType: "arraybuffer",
241
- timeout: 20000,
242
- // بنمشي ورا التحويلات بنفسنا عشان نفحص كل خطوة — axios بيفحص الأول بس.
243
- maxRedirects: 0,
244
- maxContentLength: MAX_FETCH_BYTES,
245
- headers: {
246
- "User-Agent": UA,
247
- "Accept-Language": "ar,en;q=0.9",
248
- Accept: "text/html,application/json,text/plain,*/*",
249
- },
250
- validateStatus: (status) => status >= 200 && status < 500,
251
- });
296
+ const { url, host, addresses } = target;
297
+ const agent = pinnedAgent(url.protocol, host, addresses);
298
+ try {
299
+ response = await axios.get(url.toString(), {
300
+ responseType: "arraybuffer",
301
+ timeout: 20000,
302
+ // بنمشي ورا التحويلات بنفسنا عشان نفحص كل خطوة — axios بيفحص الأول بس.
303
+ maxRedirects: 0,
304
+ maxContentLength: MAX_FETCH_BYTES,
305
+ // Ignore process-wide proxy variables: they would perform their own
306
+ // hostname resolution and bypass the pinned lookup above.
307
+ proxy: false,
308
+ httpAgent: url.protocol === "http:" ? agent : undefined,
309
+ httpsAgent: url.protocol === "https:" ? agent : undefined,
310
+ headers: {
311
+ "User-Agent": UA,
312
+ "Accept-Language": "ar,en;q=0.9",
313
+ Accept: "text/html,application/json,text/plain,*/*",
314
+ },
315
+ validateStatus: (status) => status >= 200 && status < 500,
316
+ });
317
+ } finally {
318
+ agent.destroy();
319
+ }
252
320
 
253
321
  const location = response.headers?.location;
254
322
  const isRedirect = response.status >= 300 && response.status < 400 && location;
255
323
  if (!isRedirect) break;
256
324
  if (hop >= MAX_REDIRECTS) throw new Error("الرابط بيحوّل كتير أوي");
257
325
 
258
- url = await assertPublicUrl(new URL(location, url).toString());
326
+ target = await assertPublicUrl(new URL(location, url).toString());
259
327
  }
260
328
 
261
329
  const contentType = String(response.headers["content-type"] || "");
262
330
  const body = decodeBuffer(Buffer.from(response.data), contentType);
263
331
 
264
- const text = /json|text\/plain|xml/i.test(contentType)
265
- ? body
266
- : stripHtml(body);
332
+ const text = /json|text\/plain|xml/i.test(contentType) ? body : stripHtml(body);
267
333
 
268
334
  return {
269
- url: url.toString(),
335
+ url: target.url.toString(),
270
336
  status: response.status,
271
337
  contentType,
272
338
  truncated: text.length > MAX_PAGE_CHARS,
@@ -285,16 +351,35 @@ function scopeOf(args) {
285
351
  // نفس قاعدة أمر !memory بالظبط: الكتابة في الذاكرة العامة والمسح محتاجين
286
352
  // أدمن/مالك. الفحص لازم يكون هنا مش في البرومبت — الموديل بيقرا صفحات ويب
287
353
  // ورسايل ناس، وأي واحدة فيهم ممكن تقوله "احفظ ده في الذاكرة العامة".
288
- function isPrivileged(ctx) {
354
+ function isBotPrivileged(ctx) {
289
355
  return Boolean(ctx?.isOwner || ctx?.isAdmin);
290
356
  }
291
357
 
358
+ function isChatMemoryAdmin(ctx) {
359
+ return isBotPrivileged(ctx) || Boolean(ctx?.isGroupAdmin);
360
+ }
361
+
362
+ function roleTargetBound(target, ctx) {
363
+ const raw = String(target || "").trim();
364
+ if (!raw) return false;
365
+ const ids = [...(ctx?.mentionedJids || []), ctx?.quotedParticipant].filter(Boolean);
366
+ const { sameUserSync } = require("../utils/permissions.cjs");
367
+ if (ids.some((id) => sameUserSync(id, raw))) return true;
368
+ const digits = raw.replace(/\D/g, "");
369
+ if (digits.length >= 8 && String(ctx?.userText || "").replace(/\D/g, "").includes(digits)) {
370
+ return true;
371
+ }
372
+ return false;
373
+ }
374
+
292
375
  const NOT_PRIVILEGED = {
293
376
  error: "only the bot owner or an admin can do this",
294
377
  };
295
378
 
296
379
  function preview(value, length = 40) {
297
- const text = String(value || "").replace(/\s+/g, " ").trim();
380
+ const text = String(value || "")
381
+ .replace(/\s+/g, " ")
382
+ .trim();
298
383
  return text.length > length ? `${text.slice(0, length)}...` : text;
299
384
  }
300
385
 
@@ -316,7 +401,8 @@ const TOOLS = {
316
401
  required: ["query"],
317
402
  },
318
403
  },
319
- describe: (args) => `🔍 ببحث عن: ${preview(args?.query, 50)}`,
404
+ describe: (args) => `🔍 جاري البحث عن: ${preview(args?.query, 50)}`,
405
+ describeEn: (args) => `🔍 Searching for: ${preview(args?.query, 50)}`,
320
406
  async run(args) {
321
407
  const query = String(args?.query || "").trim();
322
408
  if (!query) return { error: "query is required" };
@@ -331,6 +417,77 @@ const TOOLS = {
331
417
  },
332
418
  },
333
419
 
420
+ search_wikipedia: {
421
+ declaration: {
422
+ name: "search_wikipedia",
423
+ description:
424
+ "Search Wikipedia for encyclopedic facts, history, science, biographies, definitions, and academic context in Arabic or English.",
425
+ parameters: {
426
+ type: T.OBJECT,
427
+ properties: {
428
+ query: {
429
+ type: T.STRING,
430
+ description: "The topic, entity, or term to look up.",
431
+ },
432
+ language: {
433
+ type: T.STRING,
434
+ description:
435
+ "'ar' for Arabic Wikipedia, 'en' for English Wikipedia, or 'auto' (detect from query).",
436
+ },
437
+ limit: {
438
+ type: T.INTEGER,
439
+ description: "How many articles to return (1-5, default 3).",
440
+ },
441
+ },
442
+ required: ["query"],
443
+ },
444
+ },
445
+ describe: (args) => `📚 جاري البحث في ويكيبيديا: ${preview(args?.query, 45)}`,
446
+ describeEn: (args) => `📚 Searching Wikipedia: ${preview(args?.query, 45)}`,
447
+ async run(args) {
448
+ const query = String(args?.query || "").trim();
449
+ if (!query) return { error: "query is required" };
450
+ const limit = Math.min(5, Math.max(1, Number(args?.limit) || 3));
451
+ return searchWikipedia(query, args?.language, limit);
452
+ },
453
+ },
454
+
455
+ search_news: {
456
+ declaration: {
457
+ name: "search_news",
458
+ description:
459
+ "Search recent news headlines, breaking events, press coverage, and current affairs.",
460
+ parameters: {
461
+ type: T.OBJECT,
462
+ properties: {
463
+ query: {
464
+ type: T.STRING,
465
+ description: "The news topic, person, or event to search for.",
466
+ },
467
+ limit: {
468
+ type: T.INTEGER,
469
+ description: "How many news results to return (1-8, default 5).",
470
+ },
471
+ },
472
+ required: ["query"],
473
+ },
474
+ },
475
+ describe: (args) => `📰 جاري البحث عن الأخبار: ${preview(args?.query, 45)}`,
476
+ describeEn: (args) => `📰 Searching news: ${preview(args?.query, 45)}`,
477
+ async run(args) {
478
+ const query = String(args?.query || "").trim();
479
+ if (!query) return { error: "query is required" };
480
+ const limit = Math.min(8, Math.max(1, Number(args?.limit) || 5));
481
+ const results = await searchNews(query, limit);
482
+ return {
483
+ query,
484
+ count: results.length,
485
+ results,
486
+ note: results.length ? undefined : "no recent news found",
487
+ };
488
+ },
489
+ },
490
+
334
491
  fetch_url: {
335
492
  declaration: {
336
493
  name: "fetch_url",
@@ -344,7 +501,8 @@ const TOOLS = {
344
501
  required: ["url"],
345
502
  },
346
503
  },
347
- describe: (args) => `🌐 بفتح الرابط: ${preview(args?.url, 45)}`,
504
+ describe: (args) => `🌐 جاري فتح الرابط: ${preview(args?.url, 45)}`,
505
+ describeEn: (args) => `🌐 Opening link: ${preview(args?.url, 45)}`,
348
506
  async run(args) {
349
507
  const url = String(args?.url || "").trim();
350
508
  if (!url) return { error: "url is required" };
@@ -362,8 +520,7 @@ const TOOLS = {
362
520
  properties: {
363
521
  content: {
364
522
  type: T.STRING,
365
- description:
366
- "The fact to remember, written as a short standalone sentence.",
523
+ description: "The fact to remember, written as a short standalone sentence.",
367
524
  },
368
525
  scope: {
369
526
  type: T.STRING,
@@ -374,13 +531,17 @@ const TOOLS = {
374
531
  },
375
532
  },
376
533
  describe: (args) =>
377
- `🧠 بحفظ في ${scopeOf(args) === "global" ? "الذاكرة العامة" : "ذاكرة الشات"}: ${preview(
378
- args?.content
534
+ `🧠 جاري الحفظ في ${scopeOf(args) === "global" ? "الذاكرة العامة" : "ذاكرة المحادثة"}: ${preview(
535
+ args?.content,
536
+ )}`,
537
+ describeEn: (args) =>
538
+ `🧠 Saving to ${scopeOf(args) === "global" ? "global" : "chat"} memory: ${preview(
539
+ args?.content,
379
540
  )}`,
380
541
  async run(args, ctx) {
381
542
  const scope = scopeOf(args);
382
543
  // الذاكرة العامة بتتحقن في البرومبت بتاع كل الشاتات، فمش أي حد يكتب فيها.
383
- if (scope === "global" && !isPrivileged(ctx)) {
544
+ if (scope === "global" && !isBotPrivileged(ctx)) {
384
545
  return { ...NOT_PRIVILEGED, saved: false };
385
546
  }
386
547
  const entry = memory.addMemory({
@@ -418,7 +579,8 @@ const TOOLS = {
418
579
  },
419
580
  },
420
581
  },
421
- describe: () => "🧠 بدور في الذاكرة...",
582
+ describe: () => "🧠 جاري البحث في الذاكرة...",
583
+ describeEn: () => "🧠 Searching memory...",
422
584
  async run(args, ctx) {
423
585
  const wanted = String(args?.scope || "chat").toLowerCase();
424
586
  const scopes = wanted === "all" ? ["chat", "global"] : [scopeOf(args)];
@@ -449,10 +611,12 @@ const TOOLS = {
449
611
  required: ["ref"],
450
612
  },
451
613
  },
452
- describe: (args) => `🗑️ بمسح من الذاكرة: ${preview(args?.ref)}`,
614
+ describe: (args) => `🗑️ جاري الحذف من الذاكرة: ${preview(args?.ref)}`,
615
+ describeEn: (args) => `🗑️ Removing from memory: ${preview(args?.ref)}`,
453
616
  async run(args, ctx) {
454
- if (!isPrivileged(ctx)) return { ...NOT_PRIVILEGED, removed: false };
455
617
  const scope = scopeOf(args);
618
+ const allowed = scope === "global" ? isBotPrivileged(ctx) : isChatMemoryAdmin(ctx);
619
+ if (!allowed) return { ...NOT_PRIVILEGED, removed: false };
456
620
  const removed = memory.removeMemory({
457
621
  scope,
458
622
  chatId: ctx.chatId,
@@ -483,18 +647,24 @@ const TOOLS = {
483
647
  },
484
648
  },
485
649
  describe: (args) =>
486
- `🔑 ببص على صلاحيات: ${preview(args?.target, 25)} (${args?.role || "admin"})`,
650
+ `🔑 جاري فحص صلاحيات: ${preview(args?.target, 25)} (${args?.role || "admin"})`,
651
+ describeEn: (args) =>
652
+ `🔑 Checking permissions: ${preview(args?.target, 25)} (${args?.role || "admin"})`,
487
653
  async run(args, ctx) {
488
654
  if (!ctx.isOwner) {
489
655
  return { error: "only the bot owner can grant roles", granted: false };
490
656
  }
491
- const role = String(args?.role || "admin").toLowerCase() === "owner"
492
- ? "owner"
493
- : "admin";
657
+ if (!roleTargetBound(args?.target, ctx)) {
658
+ return {
659
+ error: "mention, quote, or type the target phone number in your own request",
660
+ granted: false,
661
+ };
662
+ }
663
+ const role = String(args?.role || "admin").toLowerCase() === "owner" ? "owner" : "admin";
494
664
  const record = await grantRole(args?.target, role);
495
665
  logger.info(
496
666
  { target: args?.target, role, by: ctx.senderId },
497
- "[aiTools] role granted by the agent"
667
+ "[aiTools] role granted by the agent",
498
668
  );
499
669
  return {
500
670
  granted: true,
@@ -517,14 +687,19 @@ const TOOLS = {
517
687
  required: ["target"],
518
688
  },
519
689
  },
520
- describe: (args) => `🔑 بسحب صلاحية من: ${preview(args?.target, 25)}`,
690
+ describe: (args) => `🔑 جاري سحب الصلاحية من: ${preview(args?.target, 25)}`,
691
+ describeEn: (args) => `🔑 Revoking permissions from: ${preview(args?.target, 25)}`,
521
692
  async run(args, ctx) {
522
693
  if (!ctx.isOwner) {
523
694
  return { error: "only the bot owner can revoke roles", revoked: false };
524
695
  }
525
- const role = String(args?.role || "admin").toLowerCase() === "owner"
526
- ? "owner"
527
- : "admin";
696
+ if (!roleTargetBound(args?.target, ctx)) {
697
+ return {
698
+ error: "mention, quote, or type the target phone number in your own request",
699
+ revoked: false,
700
+ };
701
+ }
702
+ const role = String(args?.role || "admin").toLowerCase() === "owner" ? "owner" : "admin";
528
703
  const record = await revokeRole(args?.target, role);
529
704
  return { revoked: true, role, user: record ? record.jid : args?.target };
530
705
  },
@@ -533,11 +708,11 @@ const TOOLS = {
533
708
  list_roles: {
534
709
  declaration: {
535
710
  name: "list_roles",
536
- description:
537
- "List who currently holds bot owner / admin roles. Owners and admins only.",
711
+ description: "List who currently holds bot owner / admin roles. Owners and admins only.",
538
712
  parameters: { type: T.OBJECT, properties: {} },
539
713
  },
540
- describe: () => "🔑 بجيب قائمة الصلاحيات...",
714
+ describe: () => "🔑 جاري جلب قائمة الصلاحيات...",
715
+ describeEn: () => "🔑 Fetching roles list...",
541
716
  async run(_args, ctx) {
542
717
  if (!ctx.isOwner && !ctx.isAdmin) {
543
718
  return { error: "only owners and admins can list roles" };
@@ -567,7 +742,8 @@ const TOOLS = {
567
742
  },
568
743
  },
569
744
  },
570
- describe: () => "🕒 بشوف الوقت...",
745
+ describe: () => "🕒 جاري التحقق من الوقت الحالي...",
746
+ describeEn: () => "🕒 Checking current time...",
571
747
  async run(args) {
572
748
  const timeZone = args?.timezone || settings.get("bot_timezone");
573
749
  const now = new Date();
@@ -596,13 +772,18 @@ function toolDeclarations() {
596
772
  }
597
773
 
598
774
  /** The status line for a tool call (falls back to the raw name). */
599
- function describeCall(name, args) {
775
+ function describeCall(name, args, lang = settings.get("bot_language")) {
600
776
  const tool = TOOLS[name];
601
- if (!tool) return `⚙️ بشغّل: ${name}`;
777
+ const isEn = lang === "en";
778
+ const fallback = isEn ? `⚙️ Running: ${name}` : `⚙️ جاري تشغيل: ${name}`;
779
+ if (!tool) return fallback;
602
780
  try {
603
- return tool.describe(args) || `⚙️ بشغّل: ${name}`;
781
+ if (isEn && typeof tool.describeEn === "function") {
782
+ return tool.describeEn(args) || fallback;
783
+ }
784
+ return tool.describe(args) || fallback;
604
785
  } catch {
605
- return `⚙️ بشغّل: ${name}`;
786
+ return fallback;
606
787
  }
607
788
  }
608
789
 
@@ -618,10 +799,7 @@ async function runTool(name, args, ctx) {
618
799
  const result = await tool.run(args || {}, ctx || {});
619
800
  return result && typeof result === "object" ? result : { result };
620
801
  } catch (err) {
621
- logger.warn(
622
- { err: err?.message, tool: name },
623
- "[aiTools] tool call failed"
624
- );
802
+ logger.warn({ err: err?.message, tool: name }, "[aiTools] tool call failed");
625
803
  return { error: `${err?.name || "Error"}: ${err?.message || err}` };
626
804
  }
627
805
  }
@@ -632,6 +810,8 @@ module.exports = {
632
810
  describeCall,
633
811
  runTool,
634
812
  webSearch,
813
+ searchNews,
814
+ searchWikipedia,
635
815
  fetchUrl,
636
816
  decodeText,
637
817
  };