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