levix-bot 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/SETUP.md +242 -0
- package/app.cjs +337 -0
- package/bin/levix.js +7 -0
- package/deploy/install.sh +182 -0
- package/deploy/levix.service +56 -0
- package/deploy/nginx/levix.leviro.net.conf +79 -0
- package/package.json +78 -0
- package/public/brand/apple-touch-icon.png +0 -0
- package/public/brand/banner.jpg +0 -0
- package/public/brand/banner.webp +0 -0
- package/public/brand/icon-16.png +0 -0
- package/public/brand/icon-192.png +0 -0
- package/public/brand/icon-32.png +0 -0
- package/public/brand/icon-512.png +0 -0
- package/public/brand/icon-512.webp +0 -0
- package/public/brand/mark.png +0 -0
- package/public/brand/mark.svg +16 -0
- package/public/brand/mark.webp +0 -0
- package/public/brand/wordmark.png +0 -0
- package/public/brand/wordmark.webp +0 -0
- package/public/dashboard.css +881 -0
- package/public/dashboard.js +1057 -0
- package/public/logo.svg +16 -0
- package/public/qrcode.min.js +1 -0
- package/public/socket.io.min.js +7 -0
- package/scheduler.cjs +197 -0
- package/src/auth/auth-storage.cjs +74 -0
- package/src/auth/use-database-auth-state.js +130 -0
- package/src/bootstrap/core.js +74 -0
- package/src/bootstrap/events.cjs +43 -0
- package/src/bootstrap/panel.js +97 -0
- package/src/cli.js +153 -0
- package/src/commands/autoschedule.cjs +84 -0
- package/src/commands/blacklist.cjs +116 -0
- package/src/commands/block.cjs +70 -0
- package/src/commands/calc.cjs +387 -0
- package/src/commands/debt.cjs +175 -0
- package/src/commands/deleteschedule.cjs +36 -0
- package/src/commands/gemini.cjs +727 -0
- package/src/commands/group/add.cjs +136 -0
- package/src/commands/group/all.cjs +43 -0
- package/src/commands/group/antiSpam.cjs +96 -0
- package/src/commands/group/antilink.cjs +167 -0
- package/src/commands/group/approveall.cjs +54 -0
- package/src/commands/group/clearwarns.cjs +41 -0
- package/src/commands/group/deletenote.cjs +33 -0
- package/src/commands/group/demote.cjs +62 -0
- package/src/commands/group/kick.cjs +85 -0
- package/src/commands/group/media.cjs +120 -0
- package/src/commands/group/members.cjs +59 -0
- package/src/commands/group/note.cjs +32 -0
- package/src/commands/group/promote.cjs +60 -0
- package/src/commands/group/removeall.cjs +42 -0
- package/src/commands/group/rules.cjs +31 -0
- package/src/commands/group/save.cjs +36 -0
- package/src/commands/group/setname.cjs +41 -0
- package/src/commands/group/setpp.cjs +51 -0
- package/src/commands/group/setrules.cjs +46 -0
- package/src/commands/group/setwarn.cjs +58 -0
- package/src/commands/group/tagadmins.cjs +50 -0
- package/src/commands/group/warn.cjs +105 -0
- package/src/commands/group/warns.cjs +66 -0
- package/src/commands/group/welcome.cjs +109 -0
- package/src/commands/group.cjs +160 -0
- package/src/commands/help.cjs +230 -0
- package/src/commands/listschedules.cjs +47 -0
- package/src/commands/loop.cjs +163 -0
- package/src/commands/memory.cjs +201 -0
- package/src/commands/mod.cjs +155 -0
- package/src/commands/notes.cjs +28 -0
- package/src/commands/perm.cjs +236 -0
- package/src/commands/ping.cjs +17 -0
- package/src/commands/poll.cjs +80 -0
- package/src/commands/prayer.cjs +64 -0
- package/src/commands/qr.cjs +39 -0
- package/src/commands/rand.cjs +130 -0
- package/src/commands/restart.cjs +25 -0
- package/src/commands/schedule.cjs +86 -0
- package/src/commands/score.cjs +130 -0
- package/src/commands/setprefix.cjs +40 -0
- package/src/commands/shortlink.cjs +74 -0
- package/src/commands/shutdown.cjs +45 -0
- package/src/commands/status.cjs +75 -0
- package/src/commands/stt.cjs +150 -0
- package/src/commands/todo.cjs +88 -0
- package/src/commands/tts.cjs +170 -0
- package/src/commands/unblock.cjs +57 -0
- package/src/commands/weather.cjs +79 -0
- package/src/config/ai-persona.md +17 -0
- package/src/config/baileys.config.js +51 -0
- package/src/config/brand.cjs +36 -0
- package/src/config/brand.esm.js +17 -0
- package/src/config/constants.js +16 -0
- package/src/config/defaults.cjs +91 -0
- package/src/config/lock.cjs +110 -0
- package/src/config/paths.cjs +90 -0
- package/src/config/runtime-config.cjs +219 -0
- package/src/config/secrets.cjs +130 -0
- package/src/config/settings.cjs +386 -0
- package/src/core/connection.js +199 -0
- package/src/core/events.js +61 -0
- package/src/core/socket.js +64 -0
- package/src/db/db.cjs +282 -0
- package/src/db/store.cjs +837 -0
- package/src/db/store.esm.js +17 -0
- package/src/domain/caddy.js +120 -0
- package/src/domain/command.js +380 -0
- package/src/domain/detect.js +173 -0
- package/src/domain/nginx.js +138 -0
- package/src/domain/system.js +148 -0
- package/src/handlers/command.handler.js +414 -0
- package/src/handlers/group.handler.js +73 -0
- package/src/handlers/message.handler.js +149 -0
- package/src/index.js +217 -0
- package/src/middleware/antispam.middleware.js +68 -0
- package/src/middleware/blacklist.middleware.js +29 -0
- package/src/middleware/forward-tracking.middleware.js +49 -0
- package/src/middleware/permissions.middleware.js +114 -0
- package/src/routes/dashboard.api.esm.js +779 -0
- package/src/services/aiAgent.cjs +317 -0
- package/src/services/aiTools.cjs +635 -0
- package/src/utils/datetime.cjs +61 -0
- package/src/utils/lid-helper.esm.js +143 -0
- package/src/utils/logger.cjs +72 -0
- package/src/utils/memory.cjs +385 -0
- package/src/utils/normalizeJid.cjs +65 -0
- package/src/utils/normalizeJid.esm.js +37 -0
- package/src/utils/openBrowser.cjs +129 -0
- package/src/utils/permissions.cjs +154 -0
- package/src/utils/permissions.esm.js +375 -0
- package/src/utils/recentMessageCache.esm.js +70 -0
- package/src/utils/requestOrigin.cjs +102 -0
- package/src/utils/sendBotMessage.cjs +43 -0
- package/src/utils/sendBotMessage.esm.js +139 -0
- package/src/utils/statusMessage.cjs +179 -0
- package/src/utils/storage-hub.cjs +49 -0
- package/src/utils/storage-hub.esm.js +24 -0
- package/src/utils/storage.cjs +80 -0
- package/src/utils/storage.esm.js +88 -0
- package/src/utils/textDecode.cjs +197 -0
- package/src/utils/thumbnail.cjs +591 -0
- package/src/utils/typing.esm.js +102 -0
- package/views/dashboard.ejs +329 -0
- package/views/login.ejs +105 -0
- package/views/qr.ejs +108 -0
- package/views/setup.ejs +122 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
// Gemini-powered chat command — now an agent, not a single API call.
|
|
2
|
+
//
|
|
3
|
+
// What this file still owns:
|
|
4
|
+
// * turning a WhatsApp message (text / image / video / audio / document /
|
|
5
|
+
// quoted message) into Gemini `parts`,
|
|
6
|
+
// * the multi-message context buffer (`!gemini add` ... `!gemini send`),
|
|
7
|
+
// * image generation (`!generate`),
|
|
8
|
+
// * memory management shortcuts (`!del`, `!delall`),
|
|
9
|
+
// * the fallbacks: sanitize-and-retry when an uploaded file URI expired, and
|
|
10
|
+
// Groq when Gemini is rate limited.
|
|
11
|
+
//
|
|
12
|
+
// What moved out:
|
|
13
|
+
// * the system prompt -> config/ai-persona.md (editable, hot-reloaded)
|
|
14
|
+
// * the model loop + tools -> services/aiAgent.cjs + services/aiTools.cjs
|
|
15
|
+
// * long-term memory -> utils/memory.cjs (Markdown files)
|
|
16
|
+
//
|
|
17
|
+
// One status message is posted at the start and edited all the way through
|
|
18
|
+
// ("🤖 بفكر..." -> "🔍 ببحث عن ..." -> the final answer), so a single answer
|
|
19
|
+
// never costs four messages.
|
|
20
|
+
|
|
21
|
+
const { GoogleGenerativeAI } = require("@google/generative-ai");
|
|
22
|
+
const { GoogleAIFileManager } = require("@google/generative-ai/server");
|
|
23
|
+
|
|
24
|
+
const logger = require("../utils/logger.cjs");
|
|
25
|
+
const brand = require("../config/brand.cjs");
|
|
26
|
+
const settings = require("../config/settings.cjs");
|
|
27
|
+
const {
|
|
28
|
+
getChatHistoryAsync,
|
|
29
|
+
saveChatHistoryAsync,
|
|
30
|
+
deleteChatHistoryAsync,
|
|
31
|
+
deleteAllChatHistoriesAsync,
|
|
32
|
+
} = require("../utils/storage-hub.cjs");
|
|
33
|
+
const { sendBotMessage, sendBotError } = require("../utils/sendBotMessage.cjs");
|
|
34
|
+
const { createStatus } = require("../utils/statusMessage.cjs");
|
|
35
|
+
const {
|
|
36
|
+
isOwnerJidSync,
|
|
37
|
+
isBotAdminUserSync,
|
|
38
|
+
isAdminInGroupSync,
|
|
39
|
+
} = require("../utils/permissions.cjs");
|
|
40
|
+
const {
|
|
41
|
+
runAgent,
|
|
42
|
+
isFileReferenceError,
|
|
43
|
+
sanitizeHistoryForFiles,
|
|
44
|
+
} = require("../services/aiAgent.cjs");
|
|
45
|
+
const { downloadContentFromMessage } = require("@whiskeysockets/baileys");
|
|
46
|
+
const fs = require("fs").promises;
|
|
47
|
+
const path = require("path");
|
|
48
|
+
|
|
49
|
+
// Per-(chat, sender) explicit context buffer for the multi-message workflow.
|
|
50
|
+
// Entries accumulate until `!gemini send` flushes them into a single call.
|
|
51
|
+
// Lives in memory only — short-lived by design.
|
|
52
|
+
const contextBuffers = new Map(); // key: "<chatJid>::<senderJid>" -> Entry[]
|
|
53
|
+
|
|
54
|
+
function bufferKey(msg) {
|
|
55
|
+
return `${msg.key.remoteJid}::${msg.key.participant || msg.key.remoteJid}`;
|
|
56
|
+
}
|
|
57
|
+
function getBuffer(msg) {
|
|
58
|
+
return contextBuffers.get(bufferKey(msg)) || [];
|
|
59
|
+
}
|
|
60
|
+
function setBuffer(msg, entries) {
|
|
61
|
+
if (!entries?.length) contextBuffers.delete(bufferKey(msg));
|
|
62
|
+
else contextBuffers.set(bufferKey(msg), entries);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Keys come from config/settings.cjs (what the dashboard saved, else the default)
|
|
66
|
+
// and are read per call: a key pasted into the dashboard has to work without a
|
|
67
|
+
// restart. The clients are cached per key so we don't rebuild them per message.
|
|
68
|
+
let geminiCache = { key: null, genAI: null, fileManager: null, imageModel: null };
|
|
69
|
+
|
|
70
|
+
function geminiClients() {
|
|
71
|
+
const key = settings.get("gemini_api_key");
|
|
72
|
+
if (!key) return { genAI: null, fileManager: null, imageModel: null };
|
|
73
|
+
if (geminiCache.key !== key) {
|
|
74
|
+
const genAI = new GoogleGenerativeAI(key);
|
|
75
|
+
geminiCache = {
|
|
76
|
+
key,
|
|
77
|
+
genAI,
|
|
78
|
+
fileManager: new GoogleAIFileManager(key),
|
|
79
|
+
imageModel: genAI.getGenerativeModel({ model: "gemini-2.5-flash" }),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return geminiCache;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isGeminiRateLimitError(error) {
|
|
86
|
+
const message = `${error?.message || ""} ${error?.details || ""}`.toLowerCase();
|
|
87
|
+
const status =
|
|
88
|
+
error?.status ||
|
|
89
|
+
error?.statusCode ||
|
|
90
|
+
error?.response?.status ||
|
|
91
|
+
error?.error?.code;
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
status === 429 ||
|
|
95
|
+
message.includes("429") ||
|
|
96
|
+
message.includes("rate limit") ||
|
|
97
|
+
message.includes("resource_exhausted") ||
|
|
98
|
+
message.includes("quota")
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function getGroqFallbackResponse(parts) {
|
|
103
|
+
const groqKey = settings.get("groq_api_key");
|
|
104
|
+
if (!groqKey) throw new Error("GROQ_API_KEY is not configured");
|
|
105
|
+
|
|
106
|
+
const mergedPrompt = parts
|
|
107
|
+
.map((part) => {
|
|
108
|
+
if (part?.text) return part.text;
|
|
109
|
+
if (part?.fileData) return "[تم إرفاق ملف/وسائط في الرسالة]";
|
|
110
|
+
return "";
|
|
111
|
+
})
|
|
112
|
+
.filter(Boolean)
|
|
113
|
+
.join("\n");
|
|
114
|
+
|
|
115
|
+
const response = await fetch("https://api.groq.com/openai/v1/chat/completions", {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: {
|
|
118
|
+
Authorization: `Bearer ${groqKey}`,
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify({
|
|
122
|
+
model: settings.get("groq_model"),
|
|
123
|
+
messages: [
|
|
124
|
+
{
|
|
125
|
+
role: "system",
|
|
126
|
+
// Same frozen identity the Gemini path gets — the fallback shouldn't
|
|
127
|
+
// answer "who made you?" differently from the main model.
|
|
128
|
+
content: `${brand.identityPrompt}\n\nبوت واتساب خفيف الظل. رد بالمصري العامي، قصير ومباشر.`,
|
|
129
|
+
},
|
|
130
|
+
{ role: "user", content: mergedPrompt },
|
|
131
|
+
],
|
|
132
|
+
temperature: 0.7,
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const payload = await response.json();
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
throw new Error(payload?.error?.message || "Groq API request failed");
|
|
139
|
+
}
|
|
140
|
+
return payload?.choices?.[0]?.message?.content?.trim();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function processIncomingMedia(parts, mediaMessage, mimeOverride = null) {
|
|
144
|
+
const { fileManager } = geminiClients();
|
|
145
|
+
if (!fileManager) throw new Error("Gemini fileManager unavailable");
|
|
146
|
+
const tempFilePath = path.join(__dirname, `temp_media_${Date.now()}`);
|
|
147
|
+
const stream = await downloadContentFromMessage(
|
|
148
|
+
mediaMessage,
|
|
149
|
+
mediaMessage.mimetype?.startsWith("image/")
|
|
150
|
+
? "image"
|
|
151
|
+
: mediaMessage.mimetype?.startsWith("video/")
|
|
152
|
+
? "video"
|
|
153
|
+
: mediaMessage.mimetype?.startsWith("audio/")
|
|
154
|
+
? "audio"
|
|
155
|
+
: "document"
|
|
156
|
+
);
|
|
157
|
+
let buffer = Buffer.from([]);
|
|
158
|
+
for await (const chunk of stream) buffer = Buffer.concat([buffer, chunk]);
|
|
159
|
+
if (!buffer.length) throw new Error("Empty media buffer");
|
|
160
|
+
|
|
161
|
+
await fs.writeFile(tempFilePath, buffer);
|
|
162
|
+
try {
|
|
163
|
+
const uploadResponse = await fileManager.uploadFile(tempFilePath, {
|
|
164
|
+
mimeType: mimeOverride || mediaMessage.mimetype,
|
|
165
|
+
displayName: `media-${Date.now()}`,
|
|
166
|
+
});
|
|
167
|
+
parts.push({
|
|
168
|
+
fileData: {
|
|
169
|
+
mimeType: mimeOverride || mediaMessage.mimetype,
|
|
170
|
+
fileUri: uploadResponse.file.uri,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
return uploadResponse.file.uri;
|
|
174
|
+
} finally {
|
|
175
|
+
try {
|
|
176
|
+
await fs.unlink(tempFilePath);
|
|
177
|
+
} catch {}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// === Multi-message context buffer helpers ===
|
|
182
|
+
// Each entry is { text, quotedText, mediaParts, quotedMediaParts }. Media is
|
|
183
|
+
// uploaded eagerly (on `add`) so the URIs are ready when `send` fires; Gemini
|
|
184
|
+
// file URIs are valid for ~48 h, plenty for any real workflow.
|
|
185
|
+
|
|
186
|
+
async function captureContextEntry(msg, args) {
|
|
187
|
+
const entry = {
|
|
188
|
+
text: "",
|
|
189
|
+
quotedText: "",
|
|
190
|
+
mediaParts: [],
|
|
191
|
+
quotedMediaParts: [],
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// args[0] is the sub-command name itself (`add` / `send`); the rest is the
|
|
195
|
+
// user's actual text.
|
|
196
|
+
const cleanedText = args.slice(1).join(" ").trim();
|
|
197
|
+
if (cleanedText) entry.text = cleanedText;
|
|
198
|
+
|
|
199
|
+
const m = msg.message || {};
|
|
200
|
+
|
|
201
|
+
const directMedia =
|
|
202
|
+
m.imageMessage || m.videoMessage || m.audioMessage || m.documentMessage;
|
|
203
|
+
if (directMedia) {
|
|
204
|
+
try {
|
|
205
|
+
await processIncomingMedia(entry.mediaParts, directMedia);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
logger.warn(
|
|
208
|
+
{ err: err?.message },
|
|
209
|
+
"[Gemini Context] direct media upload failed; entry will be text-only"
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const quoted = m.extendedTextMessage?.contextInfo?.quotedMessage;
|
|
215
|
+
if (quoted) {
|
|
216
|
+
entry.quotedText =
|
|
217
|
+
quoted.conversation ||
|
|
218
|
+
quoted.extendedTextMessage?.text ||
|
|
219
|
+
quoted.imageMessage?.caption ||
|
|
220
|
+
quoted.videoMessage?.caption ||
|
|
221
|
+
quoted.documentMessage?.caption ||
|
|
222
|
+
"";
|
|
223
|
+
const quotedMedia =
|
|
224
|
+
quoted.imageMessage ||
|
|
225
|
+
quoted.videoMessage ||
|
|
226
|
+
quoted.audioMessage ||
|
|
227
|
+
quoted.documentMessage;
|
|
228
|
+
if (quotedMedia) {
|
|
229
|
+
try {
|
|
230
|
+
await processIncomingMedia(entry.quotedMediaParts, quotedMedia);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
logger.warn(
|
|
233
|
+
{ err: err?.message },
|
|
234
|
+
"[Gemini Context] quoted media upload failed; entry will continue without it"
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return entry;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function entryHasContent(e) {
|
|
244
|
+
return Boolean(
|
|
245
|
+
e.text || e.quotedText || e.mediaParts?.length || e.quotedMediaParts?.length
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function buildPartsFromBuffer(entries) {
|
|
250
|
+
const parts = [];
|
|
251
|
+
for (const e of entries) {
|
|
252
|
+
if (e.quotedText) parts.push({ text: `(مرجع: "${e.quotedText}")` });
|
|
253
|
+
if (e.quotedMediaParts?.length) parts.push(...e.quotedMediaParts);
|
|
254
|
+
if (e.mediaParts?.length) parts.push(...e.mediaParts);
|
|
255
|
+
if (e.text) parts.push({ text: e.text });
|
|
256
|
+
}
|
|
257
|
+
return parts;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function summarizeEntry(e, idx) {
|
|
261
|
+
const bits = [];
|
|
262
|
+
if (e.text) {
|
|
263
|
+
const preview = e.text.length > 60 ? e.text.slice(0, 60) + "…" : e.text;
|
|
264
|
+
bits.push(`📝 ${preview}`);
|
|
265
|
+
}
|
|
266
|
+
if (e.quotedText) {
|
|
267
|
+
const preview =
|
|
268
|
+
e.quotedText.length > 40 ? e.quotedText.slice(0, 40) + "…" : e.quotedText;
|
|
269
|
+
bits.push(`↩️ "${preview}"`);
|
|
270
|
+
}
|
|
271
|
+
if (e.mediaParts?.length) bits.push(`🖼️ ميديا (${e.mediaParts.length})`);
|
|
272
|
+
if (e.quotedMediaParts?.length)
|
|
273
|
+
bits.push(`🖼️↩️ ميديا مقتبسة (${e.quotedMediaParts.length})`);
|
|
274
|
+
return `${idx + 1}. ${bits.join(" · ") || "(فارغة)"}`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = {
|
|
278
|
+
name: "gemini",
|
|
279
|
+
aliases: ["ask", "ai", "resetai", "del", "delall", "generate"],
|
|
280
|
+
description:
|
|
281
|
+
"مساعد ذكي بأدوات (بحث، فتح روابط، ذاكرة دائمة). sub-commands: add/send/clear/show للسياق، generate للصور، del/delall لمسح المحادثة.",
|
|
282
|
+
usage:
|
|
283
|
+
"gemini <النص>\ngemini add <النص>\ngemini send\ngemini show\ngemini clear\ngenerate <وصف الصورة>\ndel (مسح ذاكرة المحادثة)\ndelall (مسح كل المحادثات)",
|
|
284
|
+
chat: "all",
|
|
285
|
+
|
|
286
|
+
async execute(sock, msg, args, body, groupMetadata, ctx = {}) {
|
|
287
|
+
const chatId = msg.key.remoteJid;
|
|
288
|
+
const userName = msg.pushName;
|
|
289
|
+
const isGroup = chatId.endsWith("@g.us");
|
|
290
|
+
|
|
291
|
+
const senderId = msg.key.fromMe
|
|
292
|
+
? null
|
|
293
|
+
: isGroup
|
|
294
|
+
? msg.key.participant
|
|
295
|
+
: msg.key.remoteJid;
|
|
296
|
+
const isOwner = msg.key.fromMe || isOwnerJidSync(senderId);
|
|
297
|
+
// نفس تعريف "privileged" في أمر !memory: أدمن البوت أو أدمن الجروب —
|
|
298
|
+
// عشان أدوات الوكيل تطبّق نفس القاعدة اللي الأوامر بتطبّقها.
|
|
299
|
+
const isAdmin =
|
|
300
|
+
isOwner ||
|
|
301
|
+
isBotAdminUserSync(senderId) ||
|
|
302
|
+
(isGroup && isAdminInGroupSync(groupMetadata, senderId));
|
|
303
|
+
|
|
304
|
+
// Resolve sub-command. Aliases like !del arrive via ctx.invokedName, while
|
|
305
|
+
// `!gemini del ...` arrives as args[0].
|
|
306
|
+
const invoked = (ctx?.invokedName || "").toLowerCase();
|
|
307
|
+
const aliasSubCmds = new Set(["del", "resetai", "delall", "generate"]);
|
|
308
|
+
const inlineSubCmds = new Set([
|
|
309
|
+
"add",
|
|
310
|
+
"send",
|
|
311
|
+
"clear",
|
|
312
|
+
"show",
|
|
313
|
+
"del",
|
|
314
|
+
"resetai",
|
|
315
|
+
"delall",
|
|
316
|
+
"generate",
|
|
317
|
+
]);
|
|
318
|
+
const candidateInline = args[0]?.toLowerCase();
|
|
319
|
+
const subCommand = aliasSubCmds.has(invoked)
|
|
320
|
+
? invoked
|
|
321
|
+
: inlineSubCmds.has(candidateInline)
|
|
322
|
+
? candidateInline
|
|
323
|
+
: null;
|
|
324
|
+
|
|
325
|
+
// --- conversation history management (owner only) ---
|
|
326
|
+
if (subCommand === "del" || subCommand === "resetai") {
|
|
327
|
+
if (!isOwner)
|
|
328
|
+
return sendBotMessage(
|
|
329
|
+
sock,
|
|
330
|
+
chatId,
|
|
331
|
+
{ text: "🚫 هذا الأمر مخصص للمالك فقط." },
|
|
332
|
+
{ replyTo: msg }
|
|
333
|
+
);
|
|
334
|
+
await deleteChatHistoryAsync(chatId);
|
|
335
|
+
return sendBotMessage(
|
|
336
|
+
sock,
|
|
337
|
+
chatId,
|
|
338
|
+
{
|
|
339
|
+
text:
|
|
340
|
+
"✅ تم مسح سجل المحادثة.\n" +
|
|
341
|
+
"_(الذاكرة الدائمة مش بتتمسح كده — استخدم `!memory clear` لو ده اللي تقصده.)_",
|
|
342
|
+
},
|
|
343
|
+
{ replyTo: msg }
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
if (subCommand === "delall") {
|
|
347
|
+
if (!isOwner)
|
|
348
|
+
return sendBotMessage(
|
|
349
|
+
sock,
|
|
350
|
+
chatId,
|
|
351
|
+
{ text: "🚫 هذا الأمر مخصص للمالك فقط." },
|
|
352
|
+
{ replyTo: msg }
|
|
353
|
+
);
|
|
354
|
+
await deleteAllChatHistoriesAsync();
|
|
355
|
+
return sendBotMessage(
|
|
356
|
+
sock,
|
|
357
|
+
chatId,
|
|
358
|
+
{ text: "✅ تم مسح كل سجلات المحادثات بنجاح." },
|
|
359
|
+
{ replyTo: msg }
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// --- image generation ---
|
|
364
|
+
if (subCommand === "generate") {
|
|
365
|
+
const quotedMsg =
|
|
366
|
+
msg.message?.extendedTextMessage?.contextInfo?.quotedMessage?.conversation;
|
|
367
|
+
const imagePromptArgs = invoked === "generate" ? args : args.slice(1);
|
|
368
|
+
const imagePrompt = imagePromptArgs.join(" ");
|
|
369
|
+
if (!imagePrompt && !quotedMsg) {
|
|
370
|
+
return sendBotMessage(
|
|
371
|
+
sock,
|
|
372
|
+
chatId,
|
|
373
|
+
{
|
|
374
|
+
text: "اكتب وصف للصورة اللي عايزها بعد الأمر. مثال: !generate قطة ترتدي قبعة",
|
|
375
|
+
},
|
|
376
|
+
{ replyTo: msg }
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const status = await createStatus(
|
|
381
|
+
sock,
|
|
382
|
+
chatId,
|
|
383
|
+
"🎨 بجهّز الصورة...",
|
|
384
|
+
{ replyTo: msg }
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const fullPrompt =
|
|
389
|
+
imagePrompt && quotedMsg
|
|
390
|
+
? `Prompt: ${imagePrompt} ,using quote: ${quotedMsg}`
|
|
391
|
+
: imagePrompt
|
|
392
|
+
? `Prompt: ${imagePrompt}`
|
|
393
|
+
: `Prompt: ${quotedMsg}`;
|
|
394
|
+
const result = await geminiClients().imageModel.generateContent(
|
|
395
|
+
`Generate an image using this prompt: ${fullPrompt}`
|
|
396
|
+
);
|
|
397
|
+
const response = await result.response;
|
|
398
|
+
const image = response.candidates[0].content.parts[0];
|
|
399
|
+
const imageBuffer = Buffer.from(image.fileData.data, "base64");
|
|
400
|
+
|
|
401
|
+
await status.remove();
|
|
402
|
+
await sendBotMessage(
|
|
403
|
+
sock,
|
|
404
|
+
chatId,
|
|
405
|
+
{
|
|
406
|
+
image: imageBuffer,
|
|
407
|
+
caption: `🖼️ تفضل، صورة لـ: "${imagePrompt}"`,
|
|
408
|
+
},
|
|
409
|
+
{ replyTo: msg }
|
|
410
|
+
);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
logger.error({ err: error }, `Error in !generate command`);
|
|
413
|
+
await status.fail(error, "للأسف معرفتش أعمل الصورة");
|
|
414
|
+
}
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// --- multi-message context buffer ---
|
|
419
|
+
if (subCommand === "add") {
|
|
420
|
+
const entry = await captureContextEntry(msg, args);
|
|
421
|
+
if (!entryHasContent(entry)) {
|
|
422
|
+
return sendBotMessage(
|
|
423
|
+
sock,
|
|
424
|
+
chatId,
|
|
425
|
+
{
|
|
426
|
+
text:
|
|
427
|
+
"📭 مفيش حاجة أضيفها للـ context.\n\n" +
|
|
428
|
+
"اكتب نص بعد الأمر، أو رد على رسالة، أو ابعت ميديا مع كابشن `!gemini add`.",
|
|
429
|
+
},
|
|
430
|
+
{ replyTo: msg }
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
const buf = getBuffer(msg);
|
|
434
|
+
buf.push(entry);
|
|
435
|
+
setBuffer(msg, buf);
|
|
436
|
+
return sendBotMessage(
|
|
437
|
+
sock,
|
|
438
|
+
chatId,
|
|
439
|
+
{
|
|
440
|
+
text:
|
|
441
|
+
`✅ اتضافت للـ context. (إجمالي الآن: *${buf.length}*)\n\n` +
|
|
442
|
+
"كمل بـ `!gemini add ...` ، أو ابعت كله بـ `!gemini send`.",
|
|
443
|
+
},
|
|
444
|
+
{ replyTo: msg }
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (subCommand === "clear") {
|
|
449
|
+
const had = getBuffer(msg).length;
|
|
450
|
+
setBuffer(msg, []);
|
|
451
|
+
return sendBotMessage(
|
|
452
|
+
sock,
|
|
453
|
+
chatId,
|
|
454
|
+
{
|
|
455
|
+
text: had
|
|
456
|
+
? `🗑️ تم مسح *${had}* رسالة من الـ context.`
|
|
457
|
+
: "📭 الـ context كان فاضي أصلاً.",
|
|
458
|
+
},
|
|
459
|
+
{ replyTo: msg }
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (subCommand === "show") {
|
|
464
|
+
const buf = getBuffer(msg);
|
|
465
|
+
if (!buf.length) {
|
|
466
|
+
return sendBotMessage(
|
|
467
|
+
sock,
|
|
468
|
+
chatId,
|
|
469
|
+
{ text: "📭 الـ context فاضي. ابدأ بـ `!gemini add ...`." },
|
|
470
|
+
{ replyTo: msg }
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
return sendBotMessage(
|
|
474
|
+
sock,
|
|
475
|
+
chatId,
|
|
476
|
+
{
|
|
477
|
+
text:
|
|
478
|
+
`📋 *الـ context الحالي:* (${buf.length} رسالة)\n\n` +
|
|
479
|
+
buf.map((e, i) => summarizeEntry(e, i)).join("\n") +
|
|
480
|
+
"\n\nابعت كله بـ `!gemini send` أو امسحه بـ `!gemini clear`.",
|
|
481
|
+
},
|
|
482
|
+
{ replyTo: msg }
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// === build the parts for this turn ===
|
|
487
|
+
const prompt = args.join(" ");
|
|
488
|
+
let parts = [];
|
|
489
|
+
|
|
490
|
+
if (subCommand === "send") {
|
|
491
|
+
const buf = getBuffer(msg);
|
|
492
|
+
const finalEntry = await captureContextEntry(msg, args);
|
|
493
|
+
const allEntries = [...buf];
|
|
494
|
+
if (entryHasContent(finalEntry)) allEntries.push(finalEntry);
|
|
495
|
+
if (!allEntries.length) {
|
|
496
|
+
return sendBotMessage(
|
|
497
|
+
sock,
|
|
498
|
+
chatId,
|
|
499
|
+
{
|
|
500
|
+
text:
|
|
501
|
+
"📭 الـ context فاضي ومفيش نص في `!gemini send`.\n\n" +
|
|
502
|
+
"ابدأ بـ `!gemini add ...` الأول، أو اكتب نص مع `!gemini send ...`.",
|
|
503
|
+
},
|
|
504
|
+
{ replyTo: msg }
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
setBuffer(msg, []);
|
|
508
|
+
parts = buildPartsFromBuffer(allEntries);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Direct media. The text we attach is the CLEANED prompt (command name and
|
|
512
|
+
// prefix already stripped); the raw caption is only a fallback.
|
|
513
|
+
if (!parts.length && (msg.message?.imageMessage || msg.message?.videoMessage)) {
|
|
514
|
+
const mediaMessage = msg.message.imageMessage || msg.message.videoMessage;
|
|
515
|
+
try {
|
|
516
|
+
await processIncomingMedia(parts, mediaMessage);
|
|
517
|
+
parts.push({
|
|
518
|
+
text:
|
|
519
|
+
prompt || mediaMessage.caption || "ماذا يوجد في هذه الصورة/الفيديو؟",
|
|
520
|
+
});
|
|
521
|
+
} catch (error) {
|
|
522
|
+
logger.error({ err: error }, "Failed to upload media to Gemini.");
|
|
523
|
+
return sendBotError(
|
|
524
|
+
sock,
|
|
525
|
+
chatId,
|
|
526
|
+
error,
|
|
527
|
+
"حصلت مشكلة في رفع الصورة/الفيديو",
|
|
528
|
+
{ replyTo: msg }
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
} else if (!parts.length && msg.message?.audioMessage) {
|
|
532
|
+
try {
|
|
533
|
+
await processIncomingMedia(parts, msg.message.audioMessage);
|
|
534
|
+
parts.push({ text: prompt || "حلل لي هذا التسجيل الصوتي." });
|
|
535
|
+
} catch (error) {
|
|
536
|
+
logger.error({ err: error }, "Failed to upload audio to Gemini.");
|
|
537
|
+
return sendBotError(
|
|
538
|
+
sock,
|
|
539
|
+
chatId,
|
|
540
|
+
error,
|
|
541
|
+
"حصلت مشكلة في رفع التسجيل الصوتي",
|
|
542
|
+
{ replyTo: msg }
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
} else if (!parts.length && msg.message?.documentMessage) {
|
|
546
|
+
try {
|
|
547
|
+
await processIncomingMedia(parts, msg.message.documentMessage);
|
|
548
|
+
const fileName = msg.message.documentMessage.fileName || "document";
|
|
549
|
+
const text = prompt || msg.message.documentMessage.caption || "";
|
|
550
|
+
parts.push({
|
|
551
|
+
text: text
|
|
552
|
+
? `${text}\n(الملف: ${fileName})`
|
|
553
|
+
: `حلل/لخّص الملف: ${fileName}`,
|
|
554
|
+
});
|
|
555
|
+
} catch (error) {
|
|
556
|
+
logger.error({ err: error }, "Failed to upload document to Gemini.");
|
|
557
|
+
return sendBotError(sock, chatId, error, "حصلت مشكلة في رفع الملف", {
|
|
558
|
+
replyTo: msg,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
} else if (!parts.length && prompt) {
|
|
562
|
+
parts.push({ text: prompt });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (parts.length === 0) {
|
|
566
|
+
return sendBotMessage(
|
|
567
|
+
sock,
|
|
568
|
+
chatId,
|
|
569
|
+
{ text: "يرجى كتابة سؤال أو إرسال صورة/فيديو/تسجيل صوتي/ملف مع الأمر." },
|
|
570
|
+
{ replyTo: msg }
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Quoted message context. Skipped for `!gemini send` — those entries
|
|
575
|
+
// captured their own quotes at `add` time.
|
|
576
|
+
const quotedMsg =
|
|
577
|
+
subCommand === "send"
|
|
578
|
+
? null
|
|
579
|
+
: msg.message?.extendedTextMessage?.contextInfo?.quotedMessage;
|
|
580
|
+
|
|
581
|
+
if (quotedMsg) {
|
|
582
|
+
let quotedMediaPart = null;
|
|
583
|
+
const quotedText =
|
|
584
|
+
quotedMsg.conversation ||
|
|
585
|
+
quotedMsg.extendedTextMessage?.text ||
|
|
586
|
+
quotedMsg.documentMessage?.caption;
|
|
587
|
+
|
|
588
|
+
const mediaInQuote =
|
|
589
|
+
quotedMsg.imageMessage ||
|
|
590
|
+
quotedMsg.videoMessage ||
|
|
591
|
+
quotedMsg.audioMessage ||
|
|
592
|
+
quotedMsg.documentMessage;
|
|
593
|
+
|
|
594
|
+
if (mediaInQuote) {
|
|
595
|
+
try {
|
|
596
|
+
const probeParts = [];
|
|
597
|
+
await processIncomingMedia(probeParts, mediaInQuote);
|
|
598
|
+
quotedMediaPart = probeParts[0];
|
|
599
|
+
} catch (mediaError) {
|
|
600
|
+
logger.error({ err: mediaError }, "Failed to process quoted media");
|
|
601
|
+
await sendBotError(
|
|
602
|
+
sock,
|
|
603
|
+
chatId,
|
|
604
|
+
mediaError,
|
|
605
|
+
"حصلت مشكلة في الرسالة المقتبسة",
|
|
606
|
+
{ replyTo: msg }
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (quotedMediaPart) {
|
|
612
|
+
parts.unshift(quotedMediaPart);
|
|
613
|
+
if (quotedText) {
|
|
614
|
+
parts.unshift({
|
|
615
|
+
text: `بالاعتماد على هذه الرسالة كمرجع:\n"""\n${quotedText}\n"""\n\n`,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
} else if (quotedText) {
|
|
619
|
+
parts.unshift({
|
|
620
|
+
text: `بالاعتماد على هذه الرسالة كمرجع:\n"""\n${quotedText}\n"""\n\nأجب على التالي:`,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (!geminiClients().genAI) {
|
|
626
|
+
return sendBotMessage(
|
|
627
|
+
sock,
|
|
628
|
+
chatId,
|
|
629
|
+
{ text: "خطأ في الإعدادات: مفتاح Gemini API غير معرف." },
|
|
630
|
+
{ replyTo: msg }
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Everyone the agent needs to know about, plus the ids its role tools act
|
|
635
|
+
// on (a mention/reply is how "خلي فلان أدمن" gets resolved).
|
|
636
|
+
const contextInfo = msg.message?.extendedTextMessage?.contextInfo;
|
|
637
|
+
const agentContext = {
|
|
638
|
+
chatId,
|
|
639
|
+
senderId,
|
|
640
|
+
senderName: userName,
|
|
641
|
+
isOwner,
|
|
642
|
+
isAdmin,
|
|
643
|
+
isGroup,
|
|
644
|
+
chatName: groupMetadata?.subject || null,
|
|
645
|
+
mentionedJids: contextInfo?.mentionedJid || [],
|
|
646
|
+
quotedParticipant: contextInfo?.participant || null,
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
// Mentions/replies are ids the model can't invent — hand them over so
|
|
650
|
+
// "اعمل الراجل ده أدمن" has something concrete to act on.
|
|
651
|
+
const targets = [
|
|
652
|
+
...(agentContext.mentionedJids || []),
|
|
653
|
+
agentContext.quotedParticipant,
|
|
654
|
+
].filter(Boolean);
|
|
655
|
+
if (targets.length) {
|
|
656
|
+
parts.unshift({
|
|
657
|
+
text: `(معرفات مذكورة في الرسالة: ${[...new Set(targets)].join(", ")})`,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// The one message that will carry the whole run.
|
|
662
|
+
const status = await createStatus(sock, chatId, "🤖 بفكر...", {
|
|
663
|
+
replyTo: msg,
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
async function attempt(historyOverride) {
|
|
667
|
+
const history = historyOverride ?? ((await getChatHistoryAsync(chatId)) || []);
|
|
668
|
+
const result = await runAgent({
|
|
669
|
+
parts,
|
|
670
|
+
history,
|
|
671
|
+
status,
|
|
672
|
+
context: agentContext,
|
|
673
|
+
});
|
|
674
|
+
if (result.history?.length) {
|
|
675
|
+
await saveChatHistoryAsync(chatId, result.history);
|
|
676
|
+
}
|
|
677
|
+
return result;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
try {
|
|
681
|
+
let result;
|
|
682
|
+
try {
|
|
683
|
+
result = await attempt();
|
|
684
|
+
} catch (firstErr) {
|
|
685
|
+
// A dead fileData URI in the history poisons every later call — scrub
|
|
686
|
+
// it once and retry so the user never sees the expiry.
|
|
687
|
+
if (!isFileReferenceError(firstErr)) throw firstErr;
|
|
688
|
+
|
|
689
|
+
const oldHistory = (await getChatHistoryAsync(chatId)) || [];
|
|
690
|
+
const cleaned = sanitizeHistoryForFiles(oldHistory);
|
|
691
|
+
await saveChatHistoryAsync(chatId, cleaned);
|
|
692
|
+
logger.info(
|
|
693
|
+
`[Gemini] sanitized history for chat ${chatId}: ${oldHistory.length} -> ${cleaned.length}; retrying once`
|
|
694
|
+
);
|
|
695
|
+
result = await attempt(cleaned);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const text =
|
|
699
|
+
result.text?.trim() ||
|
|
700
|
+
"مفيش رد جه من الموديل. جرّب تصيغ السؤال بشكل تاني.";
|
|
701
|
+
await status.finish(text);
|
|
702
|
+
} catch (error) {
|
|
703
|
+
logger.error({ err: error }, "Error in !gemini command");
|
|
704
|
+
|
|
705
|
+
// Rate-limit fallback to Groq for text-only prompts.
|
|
706
|
+
if (isGeminiRateLimitError(error) && settings.get("groq_api_key")) {
|
|
707
|
+
if (parts.some((part) => part?.fileData)) {
|
|
708
|
+
await status.finish(
|
|
709
|
+
"جيميني عليه ضغط حالياً، ومش قادر أعالج الوسائط دلوقتي. حاول بعد شوية 🙏"
|
|
710
|
+
);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
try {
|
|
714
|
+
const groqResponse = await getGroqFallbackResponse(parts);
|
|
715
|
+
if (groqResponse) {
|
|
716
|
+
await status.finish(groqResponse);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
} catch (groqError) {
|
|
720
|
+
logger.error({ err: groqError }, "Groq fallback failed");
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
await status.fail(error, "حصلت مشكلة وأنا بكلم الذكاء الاصطناعي");
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
};
|