levix-bot 2.0.0 → 2.0.1

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/app.cjs CHANGED
@@ -40,6 +40,27 @@ function isAllowedOrigin(origin, host) {
40
40
  return origin === `http://${host}` || origin === `https://${host}`;
41
41
  }
42
42
 
43
+ // Browsers may legitimately serialize the Origin header as the literal string
44
+ // "null" for a same-origin navigation in an opaque-origin context. Treating
45
+ // that as a foreign origin breaks first-run/login forms in real browsers even
46
+ // though Sec-Fetch-Site still proves the navigation is same-origin. Fetch
47
+ // metadata headers are forbidden to page JavaScript, so they are a stronger
48
+ // browser signal than trying to special-case the string "null" globally.
49
+ function isAllowedMutationRequest(req) {
50
+ const origin = req.get("origin");
51
+ const host = req.get("host");
52
+
53
+ // Preserve the explicit dashboard_origin escape hatch before consulting
54
+ // fetch metadata: a deliberately separate dashboard can be cross-site.
55
+ if (origin && origin !== "null" && isAllowedOrigin(origin, host)) return true;
56
+
57
+ const fetchSite = req.get("sec-fetch-site");
58
+ if (fetchSite === "same-origin") return true;
59
+ if (fetchSite === "cross-site") return false;
60
+
61
+ return isAllowedOrigin(origin, host);
62
+ }
63
+
43
64
  // serveClient reads the browser bundle out of node_modules on every request,
44
65
  // which a packaged build has no way to do. We vendor it in public/ instead —
45
66
  // same reason qrcode.min.js is there rather than on a CDN.
@@ -91,7 +112,7 @@ app.use((req, res, next) => {
91
112
  const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
92
113
  app.use((req, res, next) => {
93
114
  if (SAFE_METHODS.has(req.method)) return next();
94
- if (isAllowedOrigin(req.get("origin"), req.get("host"))) return next();
115
+ if (isAllowedMutationRequest(req)) return next();
95
116
  return res.status(403).json({ error: "Cross-origin request rejected" });
96
117
  });
97
118
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "levix-bot",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "A self-hosted personal WhatsApp bot with an AI agent, group moderation and a web control panel.",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -27,22 +27,26 @@
27
27
  "type": "module",
28
28
  "dependencies": {
29
29
  "@google/generative-ai": "^0.24.1",
30
- "@whiskeysockets/baileys": "7.0.0-rc13",
30
+ "@sefinek/google-tts-api": "^2.1.15",
31
+ "@whiskeysockets/baileys": "7.0.0-rc14",
31
32
  "axios": "^1.9.0",
32
33
  "ejs": "^3.1.10",
33
34
  "express": "^5.1.0",
34
35
  "express-session": "^1.18.1",
35
- "ffmpeg-static": "^5.2.0",
36
- "fluent-ffmpeg": "^2.1.3",
36
+ "ffmpeg-static": "^5.3.0",
37
37
  "node-cache": "^5.1.2",
38
38
  "node-cron": "^4.1.0",
39
- "node-gtts": "^2.0.2",
40
39
  "pino": "^9.7.0",
41
40
  "pino-pretty": "^13.0.0",
42
41
  "qrcode": "^1.5.4",
43
42
  "qrcode-terminal": "^0.12.0",
44
- "socket.io": "^4.8.1",
45
- "uuid": "^11.1.0"
43
+ "socket.io": "^4.8.1"
44
+ },
45
+ "allowScripts": {
46
+ "@whiskeysockets/baileys": true,
47
+ "esbuild": true,
48
+ "ffmpeg-static": true,
49
+ "protobufjs": true
46
50
  },
47
51
  "repository": {
48
52
  "type": "git",
@@ -2,7 +2,7 @@ const {
2
2
  scheduleNewJob,
3
3
  saveScheduledJob,
4
4
  } = require("../../scheduler.cjs");
5
- const { v4: uuidv4 } = require("uuid"); // Use UUID for unique IDs
5
+ const { randomUUID } = require("node:crypto");
6
6
  const { defaultTimezone } = require("../utils/datetime.cjs");
7
7
 
8
8
  module.exports = {
@@ -58,7 +58,7 @@ module.exports = {
58
58
  }
59
59
 
60
60
  const newJob = {
61
- id: uuidv4(),
61
+ id: randomUUID(),
62
62
  type: "recurring",
63
63
  cronString: cronString,
64
64
  message: message,
@@ -81,4 +81,3 @@ module.exports = {
81
81
  });
82
82
  },
83
83
  };
84
- // Note: You need to run `npm install uuid` for this command.
@@ -1,36 +1,30 @@
1
- // Text-to-Speech Command using FREE node-gtts library.
1
+ // Text-to-Speech Command using Google TTS and ffmpeg-static.
2
2
  //
3
- // Audit fix: previously we saved the file as `.mp3` but sent it with
4
- // `mimetype: "audio/mp4"` and `ptt: true`. Some WhatsApp clients (Telegram-
5
- // linked viewers, web clients on certain Androids) refuse to play that
6
- // because the container declared in the mimetype doesn't match the actual
7
- // MP3 data, hence the "This audio is not available because something is
8
- // wrong with the audio file" pop-up.
3
+ // Google TTS produces MP3. WhatsApp voice notes are OGG/Opus, so the command:
4
+ // 1. Generates one or more MP3 chunks with @sefinek/google-tts-api.
5
+ // 2. Concatenates the MP3 frames into one temporary file.
6
+ // 3. Invokes the ffmpeg-static binary directly to create OGG/Opus.
9
7
  //
10
- // Proper voice messages on WhatsApp are OGG/Opus. We now:
11
- // 1. Generate the speech as MP3 via node-gtts (its only supported format).
12
- // 2. Re-encode it to OGG/Opus with ffmpeg-static.
13
- // 3. Send with `mimetype: "audio/ogg; codecs=opus"` + `ptt: true`.
14
- //
15
- // If ffmpeg isn't available we fall back to sending the raw MP3 with the
16
- // CORRECT mimetype (`audio/mpeg`) and `ptt: false`, so the user at least
17
- // gets a playable audio attachment.
8
+ // Calling ffmpeg directly keeps the dependency surface small and avoids the
9
+ // deprecated fluent-ffmpeg wrapper. If ffmpeg is unavailable or transcoding
10
+ // fails, the raw MP3 is still sent as a normal audio attachment.
18
11
 
19
- const gtts = require("node-gtts")("ar");
12
+ const googleTTS = require("@sefinek/google-tts-api");
20
13
  const fs = require("fs");
21
14
  const fsp = require("fs").promises;
22
15
  const path = require("path");
23
16
  const os = require("os");
17
+ const { execFile } = require("node:child_process");
18
+ const { promisify } = require("node:util");
24
19
  const logger = require("../utils/logger.cjs");
25
20
  const { createStatus } = require("../utils/statusMessage.cjs");
26
- const { sendBotMessage, sendBotError } = require("../utils/sendBotMessage.cjs");
21
+ const { sendBotMessage } = require("../utils/sendBotMessage.cjs");
22
+
23
+ const execFileAsync = promisify(execFile);
27
24
 
28
- let ffmpeg = null;
29
25
  let ffmpegStatic = null;
30
26
  try {
31
- ffmpeg = require("fluent-ffmpeg");
32
27
  ffmpegStatic = require("ffmpeg-static");
33
- if (ffmpegStatic) ffmpeg.setFfmpegPath(ffmpegStatic);
34
28
  } catch (err) {
35
29
  logger.warn(
36
30
  { err: err?.message },
@@ -46,26 +40,52 @@ function tmpPath(ext) {
46
40
  }
47
41
 
48
42
  async function synthesizeMp3(text, mp3Path) {
49
- return new Promise((resolve, reject) => {
50
- gtts.save(mp3Path, text, (err) => (err ? reject(err) : resolve()));
43
+ const parts = await googleTTS.getAllAudioBase64(text, {
44
+ lang: "ar",
45
+ slow: false,
46
+ timeout: 15_000,
47
+ splitPunct: "،,.!?؟؛;:\n",
51
48
  });
49
+
50
+ if (!Array.isArray(parts) || parts.length === 0) {
51
+ throw new Error("Google TTS returned no audio");
52
+ }
53
+
54
+ const buffers = parts.map((part) => Buffer.from(part.base64, "base64"));
55
+ await fsp.writeFile(mp3Path, Buffer.concat(buffers));
52
56
  }
53
57
 
54
58
  async function transcodeToOpus(mp3Path) {
55
- if (!ffmpeg) return null;
59
+ if (!ffmpegStatic) return null;
60
+
56
61
  const oggPath = tmpPath("ogg");
57
- await new Promise((resolve, reject) => {
58
- ffmpeg(mp3Path)
59
- .audioCodec("libopus")
60
- .audioChannels(1)
61
- .audioFrequency(48000)
62
- .audioBitrate("48k")
63
- .format("ogg")
64
- .outputOptions(["-application voip"])
65
- .on("error", reject)
66
- .on("end", resolve)
67
- .save(oggPath);
68
- });
62
+ await execFileAsync(
63
+ ffmpegStatic,
64
+ [
65
+ "-hide_banner",
66
+ "-loglevel",
67
+ "error",
68
+ "-y",
69
+ "-i",
70
+ mp3Path,
71
+ "-vn",
72
+ "-c:a",
73
+ "libopus",
74
+ "-ac",
75
+ "1",
76
+ "-ar",
77
+ "48000",
78
+ "-b:a",
79
+ "48k",
80
+ "-application",
81
+ "voip",
82
+ "-f",
83
+ "ogg",
84
+ oggPath,
85
+ ],
86
+ { windowsHide: true, timeout: 30_000 },
87
+ );
88
+
69
89
  return oggPath;
70
90
  }
71
91