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 +22 -1
- package/package.json +11 -7
- package/src/commands/autoschedule.cjs +2 -3
- package/src/commands/tts.cjs +55 -35
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 (
|
|
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.
|
|
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
|
-
"@
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
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:
|
|
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.
|
package/src/commands/tts.cjs
CHANGED
|
@@ -1,36 +1,30 @@
|
|
|
1
|
-
// Text-to-Speech Command using
|
|
1
|
+
// Text-to-Speech Command using Google TTS and ffmpeg-static.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
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 (!
|
|
59
|
+
if (!ffmpegStatic) return null;
|
|
60
|
+
|
|
56
61
|
const oggPath = tmpPath("ogg");
|
|
57
|
-
await
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|