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.
- package/README.md +11 -7
- package/app.cjs +42 -45
- package/package.json +9 -1
- package/public/dashboard/assets/index-C1b7ijfu.js +391 -0
- package/public/dashboard/assets/index-pYaivHON.css +1 -0
- package/public/dashboard/index.html +19 -0
- package/public/dashboard.css +391 -66
- package/public/dashboard.js +593 -42
- package/scheduler.cjs +9 -14
- package/src/auth/use-database-auth-state.js +2 -4
- package/src/bootstrap/core.js +2 -5
- package/src/bootstrap/panel.js +10 -5
- package/src/cli.js +3 -5
- package/src/commands/autoschedule.cjs +1 -4
- package/src/commands/blacklist.cjs +22 -20
- package/src/commands/block.cjs +2 -4
- package/src/commands/calc.cjs +9 -16
- package/src/commands/digit.cjs +97 -0
- package/src/commands/gemini.cjs +147 -161
- package/src/commands/group/add.cjs +7 -16
- package/src/commands/group/antiSpam.cjs +5 -11
- package/src/commands/group/antilink.cjs +34 -28
- package/src/commands/group/approveall.cjs +3 -11
- package/src/commands/group/clearwarns.cjs +2 -5
- package/src/commands/group/demote.cjs +3 -8
- package/src/commands/group/kick.cjs +12 -15
- package/src/commands/group/media.cjs +17 -24
- package/src/commands/group/members.cjs +1 -2
- package/src/commands/group/promote.cjs +4 -12
- package/src/commands/group/removeall.cjs +2 -5
- package/src/commands/group/setpp.cjs +1 -1
- package/src/commands/group/setrules.cjs +2 -8
- package/src/commands/group/setwarn.cjs +4 -2
- package/src/commands/group/tagadmins.cjs +2 -5
- package/src/commands/group/warn.cjs +26 -24
- package/src/commands/group/warns.cjs +1 -2
- package/src/commands/group/welcome.cjs +13 -17
- package/src/commands/group.cjs +7 -10
- package/src/commands/help.cjs +6 -12
- package/src/commands/lang.cjs +104 -0
- package/src/commands/listschedules.cjs +2 -6
- package/src/commands/loop.cjs +6 -13
- package/src/commands/memory.cjs +19 -32
- package/src/commands/mod.cjs +97 -35
- package/src/commands/perm.cjs +18 -56
- package/src/commands/ping.cjs +1 -5
- package/src/commands/poll.cjs +1 -1
- package/src/commands/rand.cjs +2 -5
- package/src/commands/schedule.cjs +4 -8
- package/src/commands/score.cjs +8 -2
- package/src/commands/setprefix.cjs +2 -2
- package/src/commands/shortlink.cjs +7 -15
- package/src/commands/shutdown.cjs +1 -1
- package/src/commands/sticker.cjs +222 -0
- package/src/commands/stt.cjs +136 -68
- package/src/commands/todo.cjs +9 -15
- package/src/commands/tts.cjs +10 -16
- package/src/commands/unblock.cjs +2 -6
- package/src/commands/weather.cjs +5 -12
- package/src/config/ai-persona.md +5 -3
- package/src/config/available-models.json +1156 -0
- package/src/config/baileys.config.js +2 -1
- package/src/config/brand.esm.js +1 -10
- package/src/config/constants.js +1 -1
- package/src/config/defaults.cjs +66 -66
- package/src/config/lock.cjs +2 -2
- package/src/config/modelCapabilities.cjs +172 -0
- package/src/config/runtime-config.cjs +8 -13
- package/src/config/secrets.cjs +18 -10
- package/src/config/settings.cjs +94 -10
- package/src/config/vault.cjs +107 -0
- package/src/core/connection.js +13 -12
- package/src/core/events.js +41 -23
- package/src/core/proxy.js +7 -7
- package/src/core/session.js +91 -48
- package/src/core/socket.js +12 -15
- package/src/db/db.cjs +14 -6
- package/src/db/store.cjs +221 -87
- package/src/domain/caddy.js +1 -1
- package/src/domain/command.js +40 -19
- package/src/domain/detect.js +1 -1
- package/src/domain/system.js +2 -5
- package/src/handlers/command.handler.js +22 -43
- package/src/handlers/group.handler.js +13 -13
- package/src/handlers/message.handler.js +11 -21
- package/src/index.js +8 -4
- package/src/middleware/antispam.middleware.js +46 -23
- package/src/middleware/blacklist.middleware.js +14 -9
- package/src/middleware/forward-tracking.middleware.js +3 -2
- package/src/middleware/permissions.middleware.js +7 -10
- package/src/panel/login-throttle.cjs +43 -0
- package/src/panel/session-auth.cjs +17 -0
- package/src/routes/dashboard.api.esm.js +394 -72
- package/src/services/aiAgent.cjs +136 -49
- package/src/services/aiProviders.cjs +193 -47
- package/src/services/aiRouter.cjs +137 -0
- package/src/services/aiTools.cjs +305 -125
- package/src/services/llmDiscovery.cjs +481 -0
- package/src/services/modelRegistry.cjs +412 -0
- package/src/utils/datetime.cjs +2 -2
- package/src/utils/lid-helper.esm.js +7 -7
- package/src/utils/logger.cjs +1 -1
- package/src/utils/markdownParser.cjs +159 -0
- package/src/utils/memory.cjs +43 -13
- package/src/utils/messageContent.cjs +40 -0
- package/src/utils/normalizeJid.esm.js +7 -7
- package/src/utils/numerals.cjs +75 -0
- package/src/utils/permissions.esm.js +10 -15
- package/src/utils/providerUrl.cjs +281 -0
- package/src/utils/recurrence.cjs +7 -11
- package/src/utils/sendBotMessage.esm.js +4 -14
- package/src/utils/statusMessage.cjs +5 -13
- package/src/utils/storage.cjs +10 -0
- package/src/utils/storage.esm.js +11 -0
- package/src/utils/textDecode.cjs +4 -9
- package/src/utils/thumbnail.cjs +8 -14
- package/src/utils/typing.esm.js +12 -12
- package/views/dashboard.ejs +134 -106
package/src/services/aiTools.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
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 (
|
|
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) =>
|
|
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
|
|
292
|
+
let target = await assertPublicUrl(rawUrl);
|
|
236
293
|
let response;
|
|
237
294
|
|
|
238
295
|
for (let hop = 0; ; hop++) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
|
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 || "")
|
|
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) => `🔍
|
|
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) => `🌐
|
|
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
|
-
`🧠
|
|
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" && !
|
|
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) => `🗑️
|
|
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
|
-
`🔑
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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) => `🔑
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
777
|
+
const isEn = lang === "en";
|
|
778
|
+
const fallback = isEn ? `⚙️ Running: ${name}` : `⚙️ جاري تشغيل: ${name}`;
|
|
779
|
+
if (!tool) return fallback;
|
|
602
780
|
try {
|
|
603
|
-
|
|
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
|
|
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
|
};
|