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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// file: src/commands/sticker.cjs
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const os = require("node:os");
|
|
5
|
+
const { spawn } = require("node:child_process");
|
|
6
|
+
const { downloadContentFromMessage } = require("@whiskeysockets/baileys");
|
|
7
|
+
const logger = require("../utils/logger.cjs");
|
|
8
|
+
const { ffmpegPath } = require("../utils/thumbnail.cjs");
|
|
9
|
+
|
|
10
|
+
function runFfmpeg(args) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const bin = ffmpegPath();
|
|
13
|
+
const child = spawn(bin, ["-hide_banner", "-nostdin", ...args], {
|
|
14
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
let stderr = "";
|
|
18
|
+
child.stderr.on("data", (chunk) => {
|
|
19
|
+
stderr += chunk.toString();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const timeout = setTimeout(() => {
|
|
23
|
+
child.kill("SIGKILL");
|
|
24
|
+
reject(new Error("FFmpeg conversion timed out"));
|
|
25
|
+
}, 30000);
|
|
26
|
+
|
|
27
|
+
child.on("error", (err) => {
|
|
28
|
+
clearTimeout(timeout);
|
|
29
|
+
reject(err);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
child.on("close", (code) => {
|
|
33
|
+
clearTimeout(timeout);
|
|
34
|
+
if (code === 0) resolve({ ok: true, stderr });
|
|
35
|
+
else reject(new Error(`FFmpeg exited with code ${code}: ${stderr.slice(-300)}`));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function streamToBuffer(stream) {
|
|
41
|
+
let buffer = Buffer.from([]);
|
|
42
|
+
for await (const chunk of stream) {
|
|
43
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
44
|
+
}
|
|
45
|
+
return buffer;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
name: "sticker",
|
|
50
|
+
aliases: ["s", "ملصق", "toimg", "tosticker"],
|
|
51
|
+
description: "Convert images/videos to stickers, or convert stickers to images/videos",
|
|
52
|
+
usage: "sticker (reply to image/video/sticker) or toimg (reply to sticker)",
|
|
53
|
+
chat: "all",
|
|
54
|
+
|
|
55
|
+
async execute(sock, msg, args, body, groupMetadata, { invokedName } = {}) {
|
|
56
|
+
const chatId = msg.key.remoteJid;
|
|
57
|
+
const isToImg = invokedName === "toimg";
|
|
58
|
+
|
|
59
|
+
const m = msg.message || {};
|
|
60
|
+
const quoted = m.extendedTextMessage?.contextInfo?.quotedMessage;
|
|
61
|
+
|
|
62
|
+
// Resolve media source (either attached directly or quoted)
|
|
63
|
+
const targetMsg = quoted || m;
|
|
64
|
+
|
|
65
|
+
const isSticker = Boolean(targetMsg.stickerMessage);
|
|
66
|
+
const isImage = Boolean(targetMsg.imageMessage);
|
|
67
|
+
const isVideo = Boolean(targetMsg.videoMessage);
|
|
68
|
+
const isDocument = Boolean(
|
|
69
|
+
targetMsg.documentMessage &&
|
|
70
|
+
(targetMsg.documentMessage.mimetype?.startsWith("image/") ||
|
|
71
|
+
targetMsg.documentMessage.mimetype?.startsWith("video/")),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (!isSticker && !isImage && !isVideo && !isDocument) {
|
|
75
|
+
await sock.sendMessage(
|
|
76
|
+
chatId,
|
|
77
|
+
{
|
|
78
|
+
text: `🎨 *محول الملصقات والوسائط / Sticker Converter*\n\n• *صورة/فيديو ➔ ملصق:* أرسل أو رد على صورة أو فيديو بـ \`!sticker\` أو \`!s\`\n• *ملصق ➔ صورة/فيديو:* رد على أي ملصق بـ \`!toimg\` أو \`!sticker\``,
|
|
79
|
+
},
|
|
80
|
+
{ quoted: msg },
|
|
81
|
+
);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const tempDir = os.tmpdir();
|
|
86
|
+
const stamp = `${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
87
|
+
const tempFiles = [];
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
if (isSticker || isToImg) {
|
|
91
|
+
// --- Convert Sticker to Image / Video ---
|
|
92
|
+
const stickerMsg = targetMsg.stickerMessage;
|
|
93
|
+
if (!stickerMsg) {
|
|
94
|
+
await sock.sendMessage(
|
|
95
|
+
chatId,
|
|
96
|
+
{ text: "يرجى الرد على ملصق لتحويله إلى صورة أو فيديو." },
|
|
97
|
+
{ quoted: msg },
|
|
98
|
+
);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const isAnimated = Boolean(stickerMsg.isAnimated);
|
|
103
|
+
const stream = await downloadContentFromMessage(stickerMsg, "sticker");
|
|
104
|
+
const stickerBuffer = await streamToBuffer(stream);
|
|
105
|
+
|
|
106
|
+
const inWebp = path.join(tempDir, `stk_in_${stamp}.webp`);
|
|
107
|
+
await fs.promises.writeFile(inWebp, stickerBuffer);
|
|
108
|
+
tempFiles.push(inWebp);
|
|
109
|
+
|
|
110
|
+
if (isAnimated) {
|
|
111
|
+
// Convert animated sticker to MP4 video
|
|
112
|
+
const outMp4 = path.join(tempDir, `stk_out_${stamp}.mp4`);
|
|
113
|
+
tempFiles.push(outMp4);
|
|
114
|
+
|
|
115
|
+
await runFfmpeg([
|
|
116
|
+
"-i",
|
|
117
|
+
inWebp,
|
|
118
|
+
"-pix_fmt",
|
|
119
|
+
"yuv420p",
|
|
120
|
+
"-c:v",
|
|
121
|
+
"libx264",
|
|
122
|
+
"-movflags",
|
|
123
|
+
"+faststart",
|
|
124
|
+
"-y",
|
|
125
|
+
outMp4,
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
const videoBuffer = await fs.promises.readFile(outMp4);
|
|
129
|
+
await sock.sendMessage(
|
|
130
|
+
chatId,
|
|
131
|
+
{ video: videoBuffer, gifPlayback: true, caption: "✅ تم تحويل الملصق إلى فيديو/GIF" },
|
|
132
|
+
{ quoted: msg },
|
|
133
|
+
);
|
|
134
|
+
} else {
|
|
135
|
+
// Convert static sticker to JPEG image
|
|
136
|
+
const outJpg = path.join(tempDir, `stk_out_${stamp}.jpg`);
|
|
137
|
+
tempFiles.push(outJpg);
|
|
138
|
+
|
|
139
|
+
await runFfmpeg(["-i", inWebp, "-vframes", "1", "-q:v", "2", "-y", outJpg]);
|
|
140
|
+
|
|
141
|
+
const imageBuffer = await fs.promises.readFile(outJpg);
|
|
142
|
+
await sock.sendMessage(
|
|
143
|
+
chatId,
|
|
144
|
+
{ image: imageBuffer, caption: "✅ تم تحويل الملصق إلى صورة" },
|
|
145
|
+
{ quoted: msg },
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
// --- Convert Image / Video to Sticker ---
|
|
150
|
+
const mediaSource =
|
|
151
|
+
targetMsg.imageMessage || targetMsg.videoMessage || targetMsg.documentMessage;
|
|
152
|
+
const mediaType = targetMsg.imageMessage
|
|
153
|
+
? "image"
|
|
154
|
+
: targetMsg.videoMessage
|
|
155
|
+
? "video"
|
|
156
|
+
: "document";
|
|
157
|
+
|
|
158
|
+
const stream = await downloadContentFromMessage(mediaSource, mediaType);
|
|
159
|
+
const mediaBuffer = await streamToBuffer(stream);
|
|
160
|
+
|
|
161
|
+
const inExt = mediaType === "video" || isVideo ? ".mp4" : ".png";
|
|
162
|
+
const inFile = path.join(tempDir, `media_in_${stamp}${inExt}`);
|
|
163
|
+
const outWebp = path.join(tempDir, `media_out_${stamp}.webp`);
|
|
164
|
+
await fs.promises.writeFile(inFile, mediaBuffer);
|
|
165
|
+
tempFiles.push(inFile, outWebp);
|
|
166
|
+
|
|
167
|
+
if (isVideo || mediaType === "video") {
|
|
168
|
+
// Convert video/gif to animated WebP sticker
|
|
169
|
+
await runFfmpeg([
|
|
170
|
+
"-i",
|
|
171
|
+
inFile,
|
|
172
|
+
"-vcodec",
|
|
173
|
+
"libwebp",
|
|
174
|
+
"-vf",
|
|
175
|
+
"scale=512:512:force_original_aspect_ratio=decrease,fps=12,pad=512:512:-1:-1:color=white@0.0",
|
|
176
|
+
"-loop",
|
|
177
|
+
"0",
|
|
178
|
+
"-ss",
|
|
179
|
+
"00:00:00",
|
|
180
|
+
"-t",
|
|
181
|
+
"00:00:06",
|
|
182
|
+
"-an",
|
|
183
|
+
"-vsync",
|
|
184
|
+
"0",
|
|
185
|
+
"-y",
|
|
186
|
+
outWebp,
|
|
187
|
+
]);
|
|
188
|
+
} else {
|
|
189
|
+
// Convert static image to WebP sticker
|
|
190
|
+
await runFfmpeg([
|
|
191
|
+
"-i",
|
|
192
|
+
inFile,
|
|
193
|
+
"-vcodec",
|
|
194
|
+
"libwebp",
|
|
195
|
+
"-vf",
|
|
196
|
+
"scale=512:512:force_original_aspect_ratio=decrease,pad=512:512:-1:-1:color=white@0.0",
|
|
197
|
+
"-y",
|
|
198
|
+
outWebp,
|
|
199
|
+
]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const stickerBuffer = await fs.promises.readFile(outWebp);
|
|
203
|
+
await sock.sendMessage(chatId, { sticker: stickerBuffer }, { quoted: msg });
|
|
204
|
+
}
|
|
205
|
+
} catch (err) {
|
|
206
|
+
logger.error({ err: err?.message }, "[Sticker] Conversion failed");
|
|
207
|
+
await sock.sendMessage(
|
|
208
|
+
chatId,
|
|
209
|
+
{ text: `❌ فشل تحويل الوسائط: ${err?.message || "خطأ غير متوقع"}` },
|
|
210
|
+
{ quoted: msg },
|
|
211
|
+
);
|
|
212
|
+
} finally {
|
|
213
|
+
for (const file of tempFiles) {
|
|
214
|
+
try {
|
|
215
|
+
if (fs.existsSync(file)) await fs.promises.unlink(file);
|
|
216
|
+
} catch {
|
|
217
|
+
// ignore cleanup errors
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
};
|
package/src/commands/stt.cjs
CHANGED
|
@@ -7,6 +7,11 @@ const logger = require("../utils/logger.cjs");
|
|
|
7
7
|
const { createStatus } = require("../utils/statusMessage.cjs");
|
|
8
8
|
|
|
9
9
|
const settings = require("../config/settings.cjs");
|
|
10
|
+
const {
|
|
11
|
+
assertProviderBaseUrl,
|
|
12
|
+
assertProviderRequestUrl,
|
|
13
|
+
safeProviderFetch,
|
|
14
|
+
} = require("../utils/providerUrl.cjs");
|
|
10
15
|
|
|
11
16
|
// Built on first use from whatever key is in force, and
|
|
12
17
|
// rebuilt if that key changes — the operator can paste one without a restart.
|
|
@@ -18,7 +23,9 @@ let cache = { key: null, baseUrl: null, genAI: null };
|
|
|
18
23
|
function geminiStt() {
|
|
19
24
|
const key = settings.get("gemini_api_key");
|
|
20
25
|
if (!key) return null;
|
|
21
|
-
const baseUrl =
|
|
26
|
+
const baseUrl = assertProviderBaseUrl(settings.get("gemini_base_url"), {
|
|
27
|
+
allowLoopback: true,
|
|
28
|
+
});
|
|
22
29
|
if (cache.key !== key || cache.baseUrl !== baseUrl) {
|
|
23
30
|
cache = {
|
|
24
31
|
key,
|
|
@@ -36,11 +43,70 @@ function sttModel() {
|
|
|
36
43
|
return settings.get("gemini_stt_model");
|
|
37
44
|
}
|
|
38
45
|
|
|
46
|
+
function resolveSttProvider() {
|
|
47
|
+
const chosen = settings.get("ai_stt_provider");
|
|
48
|
+
if (chosen === "openai") return "openai";
|
|
49
|
+
if (chosen === "gemini") return "gemini";
|
|
50
|
+
const active = settings.get("ai_provider");
|
|
51
|
+
if (active === "openai" && settings.get("openai_api_key")) {
|
|
52
|
+
return "openai";
|
|
53
|
+
}
|
|
54
|
+
return settings.get("gemini_api_key")
|
|
55
|
+
? "gemini"
|
|
56
|
+
: settings.get("openai_api_key")
|
|
57
|
+
? "openai"
|
|
58
|
+
: "gemini";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function transcribeWithOpenAi(audioBuffer, mimetype) {
|
|
62
|
+
const apiKey = settings.get("openai_api_key");
|
|
63
|
+
if (!apiKey) throw new Error("مفتاح OpenAI / Groq غير مضبوط في الإعدادات");
|
|
64
|
+
const baseUrl = assertProviderBaseUrl(
|
|
65
|
+
settings.get("openai_base_url") || "https://api.openai.com/v1",
|
|
66
|
+
{ allowLoopback: true },
|
|
67
|
+
);
|
|
68
|
+
const model = settings.get("openai_stt_model") || "whisper-large-v3";
|
|
69
|
+
|
|
70
|
+
const formData = new FormData();
|
|
71
|
+
const mime = mimetype || "audio/ogg";
|
|
72
|
+
const ext = mime.includes("mp4")
|
|
73
|
+
? "m4a"
|
|
74
|
+
: mime.includes("mp3")
|
|
75
|
+
? "mp3"
|
|
76
|
+
: mime.includes("wav")
|
|
77
|
+
? "wav"
|
|
78
|
+
: "ogg";
|
|
79
|
+
const blob = new Blob([audioBuffer], { type: mime });
|
|
80
|
+
formData.append("file", blob, `audio.${ext}`);
|
|
81
|
+
formData.append("model", model);
|
|
82
|
+
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const timer = setTimeout(() => controller.abort(), 60000);
|
|
85
|
+
try {
|
|
86
|
+
const res = await safeProviderFetch(`${baseUrl}/audio/transcriptions`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: {
|
|
89
|
+
Authorization: `Bearer ${apiKey}`,
|
|
90
|
+
},
|
|
91
|
+
body: formData,
|
|
92
|
+
signal: controller.signal,
|
|
93
|
+
}, { allowLoopback: true });
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
const err = await res.text().catch(() => "");
|
|
96
|
+
throw new Error(`STT API (${res.status}): ${err}`);
|
|
97
|
+
}
|
|
98
|
+
const data = await res.json();
|
|
99
|
+
return String(data?.text || "").trim();
|
|
100
|
+
} finally {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
39
105
|
module.exports = {
|
|
40
106
|
name: "stt",
|
|
41
107
|
aliases: ["totext", "transcribe"],
|
|
42
|
-
description: "Convert speech/audio to text (
|
|
43
|
-
usage: "stt (
|
|
108
|
+
description: "Convert speech/audio to text (supports Gemini & Groq/OpenAI Whisper)",
|
|
109
|
+
usage: "stt (قم بالرد على رسالة صوتية أو إرسالها مع الأمر)",
|
|
44
110
|
chat: "all",
|
|
45
111
|
|
|
46
112
|
async execute(sock, msg, args, body, groupMetadata) {
|
|
@@ -49,81 +115,86 @@ module.exports = {
|
|
|
49
115
|
// Check if message has audio or if it's a reply to an audio message
|
|
50
116
|
const audioMessage =
|
|
51
117
|
msg.message?.audioMessage ||
|
|
52
|
-
msg.message?.extendedTextMessage?.contextInfo?.quotedMessage
|
|
53
|
-
?.audioMessage;
|
|
118
|
+
msg.message?.extendedTextMessage?.contextInfo?.quotedMessage?.audioMessage;
|
|
54
119
|
|
|
55
120
|
if (!audioMessage) {
|
|
56
121
|
return await sock.sendMessage(chatId, {
|
|
57
|
-
text: "📢 الاستخدام:\nأرسل رسالة صوتية أو
|
|
122
|
+
text: "📢 الاستخدام:\nأرسل رسالة صوتية أو قم بالرد على رسالة صوتية بالأمر !stt\n\n✨ يدعم تفريغ الصوت عبر Gemini و Groq/OpenAI Whisper\n🌍 يدعم العربية والإنجليزية وكافة اللغات",
|
|
58
123
|
});
|
|
59
124
|
}
|
|
60
125
|
|
|
61
|
-
const
|
|
62
|
-
|
|
126
|
+
const provider = resolveSttProvider();
|
|
127
|
+
const hasGemini = !!geminiStt();
|
|
128
|
+
const hasOpenAi = !!settings.get("openai_api_key");
|
|
129
|
+
|
|
130
|
+
if (!hasGemini && !hasOpenAi) {
|
|
63
131
|
return await sock.sendMessage(chatId, {
|
|
64
|
-
text: "⚠️ مفتاح Gemini
|
|
132
|
+
text: "⚠️ مفتاح الذكاء الاصطناعي (Gemini أو OpenAI/Groq) غير مضبوط. يرجى إضافته من لوحة التحكم (Settings).",
|
|
65
133
|
});
|
|
66
134
|
}
|
|
67
135
|
|
|
68
136
|
let tempAudioPath = null;
|
|
69
137
|
|
|
70
138
|
// One message, edited from "transcribing" into the transcript itself.
|
|
71
|
-
const status = await createStatus(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"🎧 بحوّل الصوت لنص...",
|
|
75
|
-
{ replyTo: msg },
|
|
76
|
-
);
|
|
139
|
+
const status = await createStatus(sock, chatId, "🎧 جاري تحويل الصوت إلى نص...", {
|
|
140
|
+
replyTo: msg,
|
|
141
|
+
});
|
|
77
142
|
|
|
78
143
|
try {
|
|
79
144
|
// Download audio content
|
|
80
|
-
const audioBuffer = await downloadContentFromMessage(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
file: tempAudioPath
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// Generate transcription using Gemini
|
|
104
|
-
const response = await gemini.genAI.models.generateContent({
|
|
105
|
-
model: sttModel(),
|
|
106
|
-
contents: [
|
|
107
|
-
{
|
|
108
|
-
role: "user",
|
|
109
|
-
parts: [
|
|
110
|
-
{
|
|
111
|
-
fileData: {
|
|
112
|
-
mimeType: uploaded.mimeType,
|
|
113
|
-
fileUri: uploaded.uri,
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
text: "Please transcribe this audio message accurately. Return ONLY the transcription text without any additional commentary, explanations, or formatting. Just the raw transcribed text.",
|
|
118
|
-
},
|
|
119
|
-
],
|
|
145
|
+
const audioBuffer = await downloadContentFromMessage(audioMessage, "audio");
|
|
146
|
+
|
|
147
|
+
let transcription = "";
|
|
148
|
+
if (provider === "openai" && hasOpenAi) {
|
|
149
|
+
logger.info("[STT] Transcribing via OpenAI/Groq Whisper API");
|
|
150
|
+
transcription = await transcribeWithOpenAi(audioBuffer, audioMessage.mimetype);
|
|
151
|
+
} else if (hasGemini) {
|
|
152
|
+
await assertProviderRequestUrl(settings.get("gemini_base_url"), {
|
|
153
|
+
allowLoopback: true,
|
|
154
|
+
});
|
|
155
|
+
// Save audio to temporary file
|
|
156
|
+
tempAudioPath = path.join(__dirname, `stt_audio_${Date.now()}.ogg`);
|
|
157
|
+
await fs.writeFile(tempAudioPath, audioBuffer);
|
|
158
|
+
|
|
159
|
+
logger.info(`[STT] Saved audio to temporary file: ${tempAudioPath}`);
|
|
160
|
+
|
|
161
|
+
// Upload audio to Gemini. ai.files.upload returns the File directly
|
|
162
|
+
const uploaded = await gemini.genAI.files.upload({
|
|
163
|
+
file: tempAudioPath,
|
|
164
|
+
config: {
|
|
165
|
+
mimeType: audioMessage.mimetype || "audio/ogg; codecs=opus",
|
|
166
|
+
displayName: `audio-${Date.now()}`,
|
|
120
167
|
},
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
logger.info(`[STT] Uploaded audio to Gemini: ${uploaded.uri}`);
|
|
171
|
+
|
|
172
|
+
// Generate transcription using Gemini
|
|
173
|
+
const response = await gemini.genAI.models.generateContent({
|
|
174
|
+
model: sttModel(),
|
|
175
|
+
contents: [
|
|
176
|
+
{
|
|
177
|
+
role: "user",
|
|
178
|
+
parts: [
|
|
179
|
+
{
|
|
180
|
+
fileData: {
|
|
181
|
+
mimeType: uploaded.mimeType,
|
|
182
|
+
fileUri: uploaded.uri,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
text: "Please transcribe this audio message accurately. Return ONLY the transcription text without any additional commentary, explanations, or formatting. Just the raw transcribed text.",
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
transcription = (response.text ?? "").trim();
|
|
194
|
+
} else if (hasOpenAi) {
|
|
195
|
+
logger.info("[STT] Fallback transcribing via OpenAI/Groq Whisper API");
|
|
196
|
+
transcription = await transcribeWithOpenAi(audioBuffer, audioMessage.mimetype);
|
|
197
|
+
}
|
|
127
198
|
|
|
128
199
|
if (!transcription || transcription === "") {
|
|
129
200
|
await status.finish(
|
|
@@ -143,14 +214,11 @@ module.exports = {
|
|
|
143
214
|
|
|
144
215
|
// Provide more specific error messages
|
|
145
216
|
if (error.message?.includes("quota")) {
|
|
146
|
-
errorMessage +=
|
|
147
|
-
"\n\nتم تجاوز الحد المجاني. حاول مرة أخرى لاحقاً.";
|
|
217
|
+
errorMessage += "\n\nتم تجاوز الحد المجاني. حاول مرة أخرى لاحقاً.";
|
|
148
218
|
} else if (error.message?.includes("API key")) {
|
|
149
|
-
errorMessage +=
|
|
150
|
-
"\n\nخطأ في مفتاح API. تواصل مع المطور.";
|
|
219
|
+
errorMessage += "\n\nخطأ في مفتاح API. تواصل مع المطور.";
|
|
151
220
|
} else if (error.message?.includes("upload")) {
|
|
152
|
-
errorMessage +=
|
|
153
|
-
"\n\nفشل رفع الملف الصوتي. حاول مرة أخرى.";
|
|
221
|
+
errorMessage += "\n\nفشل رفع الملف الصوتي. حاول مرة أخرى.";
|
|
154
222
|
}
|
|
155
223
|
|
|
156
224
|
await status.finish(errorMessage);
|
|
@@ -163,7 +231,7 @@ module.exports = {
|
|
|
163
231
|
} catch (cleanupErr) {
|
|
164
232
|
logger.warn(
|
|
165
233
|
{ err: cleanupErr },
|
|
166
|
-
`[STT] Failed to delete temporary file: ${tempAudioPath}
|
|
234
|
+
`[STT] Failed to delete temporary file: ${tempAudioPath}`,
|
|
167
235
|
);
|
|
168
236
|
}
|
|
169
237
|
}
|
package/src/commands/todo.cjs
CHANGED
|
@@ -18,7 +18,7 @@ module.exports = {
|
|
|
18
18
|
const userTasks = getUserTodos(senderId);
|
|
19
19
|
|
|
20
20
|
switch (subCommand) {
|
|
21
|
-
case "add":
|
|
21
|
+
case "add": {
|
|
22
22
|
const taskToAdd = args.slice(1).join(" ");
|
|
23
23
|
if (!taskToAdd) {
|
|
24
24
|
return await sock.sendMessage(remoteJid, {
|
|
@@ -32,15 +32,12 @@ module.exports = {
|
|
|
32
32
|
mentions: [senderId],
|
|
33
33
|
});
|
|
34
34
|
break;
|
|
35
|
+
}
|
|
35
36
|
|
|
36
37
|
case "remove":
|
|
37
|
-
case "del":
|
|
38
|
+
case "del": {
|
|
38
39
|
const taskNumber = parseInt(args[1], 10);
|
|
39
|
-
if (
|
|
40
|
-
isNaN(taskNumber) ||
|
|
41
|
-
taskNumber <= 0 ||
|
|
42
|
-
taskNumber > userTasks.length
|
|
43
|
-
) {
|
|
40
|
+
if (isNaN(taskNumber) || taskNumber <= 0 || taskNumber > userTasks.length) {
|
|
44
41
|
return await sock.sendMessage(remoteJid, {
|
|
45
42
|
text: "رقم المهمة غير صالح.",
|
|
46
43
|
});
|
|
@@ -48,15 +45,14 @@ module.exports = {
|
|
|
48
45
|
const removedTask = userTasks.splice(taskNumber - 1, 1);
|
|
49
46
|
saveUserTodos(senderId, userTasks);
|
|
50
47
|
await sock.sendMessage(remoteJid, {
|
|
51
|
-
text: `☑️ يا @${senderId.split("@")[0]}، تم حذف المهمة: *${
|
|
52
|
-
removedTask[0]
|
|
53
|
-
}*`,
|
|
48
|
+
text: `☑️ يا @${senderId.split("@")[0]}، تم حذف المهمة: *${removedTask[0]}*`,
|
|
54
49
|
mentions: [senderId],
|
|
55
50
|
});
|
|
56
51
|
break;
|
|
52
|
+
}
|
|
57
53
|
|
|
58
54
|
case "list":
|
|
59
|
-
default:
|
|
55
|
+
default: {
|
|
60
56
|
if (userTasks.length === 0) {
|
|
61
57
|
return await sock.sendMessage(remoteJid, {
|
|
62
58
|
text: `قائمة مهامك فارغة يا @${senderId.split("@")[0]}.`,
|
|
@@ -74,12 +70,10 @@ module.exports = {
|
|
|
74
70
|
mentions: [senderId],
|
|
75
71
|
});
|
|
76
72
|
break;
|
|
73
|
+
}
|
|
77
74
|
}
|
|
78
75
|
} catch (error) {
|
|
79
|
-
logger.error(
|
|
80
|
-
{ err: error, command: "todo" },
|
|
81
|
-
"An error occurred in the todo command"
|
|
82
|
-
);
|
|
76
|
+
logger.error({ err: error, command: "todo" }, "An error occurred in the todo command");
|
|
83
77
|
await sock.sendMessage(msg.key.remoteJid, {
|
|
84
78
|
text: "حدث خطأ أثناء معالجة قائمة المهام.",
|
|
85
79
|
});
|
package/src/commands/tts.cjs
CHANGED
|
@@ -27,7 +27,7 @@ const { ffmpegPath } = require("../utils/thumbnail.cjs");
|
|
|
27
27
|
function tmpPath(ext) {
|
|
28
28
|
return path.join(
|
|
29
29
|
os.tmpdir(),
|
|
30
|
-
`wa-bot-tts-${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}
|
|
30
|
+
`wa-bot-tts-${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`,
|
|
31
31
|
);
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -72,7 +72,7 @@ async function transcodeWithEncoder(bin, encoder, mp3Path, oggPath) {
|
|
|
72
72
|
"ogg",
|
|
73
73
|
oggPath,
|
|
74
74
|
],
|
|
75
|
-
{ windowsHide: true, timeout: 30_000 }
|
|
75
|
+
{ windowsHide: true, timeout: 30_000 },
|
|
76
76
|
);
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -100,7 +100,7 @@ async function transcodeToOpus(mp3Path) {
|
|
|
100
100
|
|
|
101
101
|
logger.warn(
|
|
102
102
|
{ err: lastError?.message, bin },
|
|
103
|
-
"[TTS] ffmpeg transcode failed — will fall back to raw MP3"
|
|
103
|
+
"[TTS] ffmpeg transcode failed — will fall back to raw MP3",
|
|
104
104
|
);
|
|
105
105
|
return null;
|
|
106
106
|
}
|
|
@@ -117,10 +117,8 @@ module.exports = {
|
|
|
117
117
|
|
|
118
118
|
let textToConvert = args.join(" ");
|
|
119
119
|
const quotedMsg =
|
|
120
|
-
msg.message?.extendedTextMessage?.contextInfo?.quotedMessage
|
|
121
|
-
|
|
122
|
-
msg.message?.extendedTextMessage?.contextInfo?.quotedMessage
|
|
123
|
-
?.extendedTextMessage?.text;
|
|
120
|
+
msg.message?.extendedTextMessage?.contextInfo?.quotedMessage?.conversation ||
|
|
121
|
+
msg.message?.extendedTextMessage?.contextInfo?.quotedMessage?.extendedTextMessage?.text;
|
|
124
122
|
|
|
125
123
|
if (!textToConvert && quotedMsg) textToConvert = quotedMsg;
|
|
126
124
|
|
|
@@ -129,10 +127,9 @@ module.exports = {
|
|
|
129
127
|
sock,
|
|
130
128
|
chatId,
|
|
131
129
|
{
|
|
132
|
-
text:
|
|
133
|
-
"📢 الاستخدام:\n!tts <النص>\n\nأو رد على رسالة نصية بالأمر !tts\n\n✨ مجاني تماماً - بدون تكاليف!",
|
|
130
|
+
text: "📢 الاستخدام:\n!tts <النص>\n\nأو رد على رسالة نصية بالأمر !tts\n\n✨ مجاني تماماً - بدون تكاليف!",
|
|
134
131
|
},
|
|
135
|
-
{ replyTo: msg }
|
|
132
|
+
{ replyTo: msg },
|
|
136
133
|
);
|
|
137
134
|
}
|
|
138
135
|
|
|
@@ -152,10 +149,7 @@ module.exports = {
|
|
|
152
149
|
try {
|
|
153
150
|
oggPath = await transcodeToOpus(mp3Path);
|
|
154
151
|
} catch (err) {
|
|
155
|
-
logger.warn(
|
|
156
|
-
{ err: err?.message },
|
|
157
|
-
"[TTS] Opus transcode failed, will fall back to MP3"
|
|
158
|
-
);
|
|
152
|
+
logger.warn({ err: err?.message }, "[TTS] Opus transcode failed, will fall back to MP3");
|
|
159
153
|
oggPath = null;
|
|
160
154
|
}
|
|
161
155
|
|
|
@@ -169,7 +163,7 @@ module.exports = {
|
|
|
169
163
|
mimetype: "audio/ogg; codecs=opus",
|
|
170
164
|
ptt: true,
|
|
171
165
|
},
|
|
172
|
-
{ replyTo: msg, typing: false }
|
|
166
|
+
{ replyTo: msg, typing: false },
|
|
173
167
|
);
|
|
174
168
|
} else {
|
|
175
169
|
// Fallback: send the raw MP3 with the correct mimetype. Cannot be
|
|
@@ -183,7 +177,7 @@ module.exports = {
|
|
|
183
177
|
mimetype: "audio/mpeg",
|
|
184
178
|
ptt: false,
|
|
185
179
|
},
|
|
186
|
-
{ replyTo: msg, typing: false }
|
|
180
|
+
{ replyTo: msg, typing: false },
|
|
187
181
|
);
|
|
188
182
|
}
|
|
189
183
|
|
package/src/commands/unblock.cjs
CHANGED
|
@@ -16,8 +16,7 @@ module.exports = {
|
|
|
16
16
|
if (!isGroup && args.length === 0) {
|
|
17
17
|
targetJid = remoteJid;
|
|
18
18
|
} else {
|
|
19
|
-
const mentionedJid =
|
|
20
|
-
msg.message?.extendedTextMessage?.contextInfo?.mentionedJid?.[0];
|
|
19
|
+
const mentionedJid = msg.message?.extendedTextMessage?.contextInfo?.mentionedJid?.[0];
|
|
21
20
|
const numberArg = args[0];
|
|
22
21
|
|
|
23
22
|
if (mentionedJid) {
|
|
@@ -45,10 +44,7 @@ module.exports = {
|
|
|
45
44
|
// mentions: [normalizedTargetJid],
|
|
46
45
|
// });
|
|
47
46
|
} catch (error) {
|
|
48
|
-
logger.error(
|
|
49
|
-
{ err: error, command: "unblock" },
|
|
50
|
-
"Error in !unblock command"
|
|
51
|
-
);
|
|
47
|
+
logger.error({ err: error, command: "unblock" }, "Error in !unblock command");
|
|
52
48
|
await sock.sendMessage(remoteJid, {
|
|
53
49
|
text: "حدث خطأ أثناء محاولة فك حظر المستخدم.",
|
|
54
50
|
});
|