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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +11 -7
  2. package/app.cjs +42 -45
  3. package/package.json +9 -1
  4. package/public/dashboard/assets/index-C1b7ijfu.js +391 -0
  5. package/public/dashboard/assets/index-pYaivHON.css +1 -0
  6. package/public/dashboard/index.html +19 -0
  7. package/public/dashboard.css +391 -66
  8. package/public/dashboard.js +593 -42
  9. package/scheduler.cjs +9 -14
  10. package/src/auth/use-database-auth-state.js +2 -4
  11. package/src/bootstrap/core.js +2 -5
  12. package/src/bootstrap/panel.js +10 -5
  13. package/src/cli.js +3 -5
  14. package/src/commands/autoschedule.cjs +1 -4
  15. package/src/commands/blacklist.cjs +22 -20
  16. package/src/commands/block.cjs +2 -4
  17. package/src/commands/calc.cjs +9 -16
  18. package/src/commands/digit.cjs +97 -0
  19. package/src/commands/gemini.cjs +147 -161
  20. package/src/commands/group/add.cjs +7 -16
  21. package/src/commands/group/antiSpam.cjs +5 -11
  22. package/src/commands/group/antilink.cjs +34 -28
  23. package/src/commands/group/approveall.cjs +3 -11
  24. package/src/commands/group/clearwarns.cjs +2 -5
  25. package/src/commands/group/demote.cjs +3 -8
  26. package/src/commands/group/kick.cjs +12 -15
  27. package/src/commands/group/media.cjs +17 -24
  28. package/src/commands/group/members.cjs +1 -2
  29. package/src/commands/group/promote.cjs +4 -12
  30. package/src/commands/group/removeall.cjs +2 -5
  31. package/src/commands/group/setpp.cjs +1 -1
  32. package/src/commands/group/setrules.cjs +2 -8
  33. package/src/commands/group/setwarn.cjs +4 -2
  34. package/src/commands/group/tagadmins.cjs +2 -5
  35. package/src/commands/group/warn.cjs +26 -24
  36. package/src/commands/group/warns.cjs +1 -2
  37. package/src/commands/group/welcome.cjs +13 -17
  38. package/src/commands/group.cjs +7 -10
  39. package/src/commands/help.cjs +6 -12
  40. package/src/commands/lang.cjs +104 -0
  41. package/src/commands/listschedules.cjs +2 -6
  42. package/src/commands/loop.cjs +6 -13
  43. package/src/commands/memory.cjs +19 -32
  44. package/src/commands/mod.cjs +97 -35
  45. package/src/commands/perm.cjs +18 -56
  46. package/src/commands/ping.cjs +1 -5
  47. package/src/commands/poll.cjs +1 -1
  48. package/src/commands/rand.cjs +2 -5
  49. package/src/commands/schedule.cjs +4 -8
  50. package/src/commands/score.cjs +8 -2
  51. package/src/commands/setprefix.cjs +2 -2
  52. package/src/commands/shortlink.cjs +7 -15
  53. package/src/commands/shutdown.cjs +1 -1
  54. package/src/commands/sticker.cjs +222 -0
  55. package/src/commands/stt.cjs +136 -68
  56. package/src/commands/todo.cjs +9 -15
  57. package/src/commands/tts.cjs +10 -16
  58. package/src/commands/unblock.cjs +2 -6
  59. package/src/commands/weather.cjs +5 -12
  60. package/src/config/ai-persona.md +5 -3
  61. package/src/config/available-models.json +1156 -0
  62. package/src/config/baileys.config.js +2 -1
  63. package/src/config/brand.esm.js +1 -10
  64. package/src/config/constants.js +1 -1
  65. package/src/config/defaults.cjs +66 -66
  66. package/src/config/lock.cjs +2 -2
  67. package/src/config/modelCapabilities.cjs +172 -0
  68. package/src/config/runtime-config.cjs +8 -13
  69. package/src/config/secrets.cjs +18 -10
  70. package/src/config/settings.cjs +94 -10
  71. package/src/config/vault.cjs +107 -0
  72. package/src/core/connection.js +13 -12
  73. package/src/core/events.js +41 -23
  74. package/src/core/proxy.js +7 -7
  75. package/src/core/session.js +91 -48
  76. package/src/core/socket.js +12 -15
  77. package/src/db/db.cjs +14 -6
  78. package/src/db/store.cjs +221 -87
  79. package/src/domain/caddy.js +1 -1
  80. package/src/domain/command.js +40 -19
  81. package/src/domain/detect.js +1 -1
  82. package/src/domain/system.js +2 -5
  83. package/src/handlers/command.handler.js +22 -43
  84. package/src/handlers/group.handler.js +13 -13
  85. package/src/handlers/message.handler.js +11 -21
  86. package/src/index.js +8 -4
  87. package/src/middleware/antispam.middleware.js +46 -23
  88. package/src/middleware/blacklist.middleware.js +14 -9
  89. package/src/middleware/forward-tracking.middleware.js +3 -2
  90. package/src/middleware/permissions.middleware.js +7 -10
  91. package/src/panel/login-throttle.cjs +43 -0
  92. package/src/panel/session-auth.cjs +17 -0
  93. package/src/routes/dashboard.api.esm.js +394 -72
  94. package/src/services/aiAgent.cjs +136 -49
  95. package/src/services/aiProviders.cjs +193 -47
  96. package/src/services/aiRouter.cjs +137 -0
  97. package/src/services/aiTools.cjs +305 -125
  98. package/src/services/llmDiscovery.cjs +481 -0
  99. package/src/services/modelRegistry.cjs +412 -0
  100. package/src/utils/datetime.cjs +2 -2
  101. package/src/utils/lid-helper.esm.js +7 -7
  102. package/src/utils/logger.cjs +1 -1
  103. package/src/utils/markdownParser.cjs +159 -0
  104. package/src/utils/memory.cjs +43 -13
  105. package/src/utils/messageContent.cjs +40 -0
  106. package/src/utils/normalizeJid.esm.js +7 -7
  107. package/src/utils/numerals.cjs +75 -0
  108. package/src/utils/permissions.esm.js +10 -15
  109. package/src/utils/providerUrl.cjs +281 -0
  110. package/src/utils/recurrence.cjs +7 -11
  111. package/src/utils/sendBotMessage.esm.js +4 -14
  112. package/src/utils/statusMessage.cjs +5 -13
  113. package/src/utils/storage.cjs +10 -0
  114. package/src/utils/storage.esm.js +11 -0
  115. package/src/utils/textDecode.cjs +4 -9
  116. package/src/utils/thumbnail.cjs +8 -14
  117. package/src/utils/typing.esm.js +12 -12
  118. package/views/dashboard.ejs +134 -106
@@ -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
+ };
@@ -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 = String(settings.get("gemini_base_url")).replace(/\/+$/, "");
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 (FREE - uses Gemini API)",
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أرسل رسالة صوتية أو رد على رسالة صوتية بالأمر !stt\n\n✨ مجاني تماماً - يستخدم Gemini API\n🌍 يدعم العربية والإنجليزية وجميع اللغات",
122
+ text: "📢 الاستخدام:\nأرسل رسالة صوتية أو قم بالرد على رسالة صوتية بالأمر !stt\n\n✨ يدعم تفريغ الصوت عبر Gemini و Groq/OpenAI Whisper\n🌍 يدعم العربية والإنجليزية وكافة اللغات",
58
123
  });
59
124
  }
60
125
 
61
- const gemini = geminiStt();
62
- if (!gemini) {
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 مش متظبط. ضيفه من الداشبورد (Settings).",
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
- sock,
73
- chatId,
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
- audioMessage,
82
- "audio"
83
- );
84
-
85
- // Save audio to temporary file
86
- tempAudioPath = path.join(__dirname, `stt_audio_${Date.now()}.ogg`);
87
- await fs.writeFile(tempAudioPath, audioBuffer);
88
-
89
- logger.info(`[STT] Saved audio to temporary file: ${tempAudioPath}`);
90
-
91
- // Upload audio to Gemini. ai.files.upload returns the File directly,
92
- // where the old fileManager wrapped it in { file }.
93
- const uploaded = await gemini.genAI.files.upload({
94
- file: tempAudioPath,
95
- config: {
96
- mimeType: audioMessage.mimetype || "audio/ogg; codecs=opus",
97
- displayName: `audio-${Date.now()}`,
98
- },
99
- });
100
-
101
- logger.info(`[STT] Uploaded audio to Gemini: ${uploaded.uri}`);
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
- // `text` is a getter on the new response, and undefined rather than a
125
- // throw when the model returned nothing usable.
126
- const transcription = (response.text ?? "").trim();
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
  }
@@ -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
  });
@@ -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
- ?.conversation ||
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
 
@@ -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
  });