feat: implement web-driven voice connection with guild/channel selection and API integration

This commit is contained in:
MythEclipse
2026-05-13 18:23:20 +07:00
parent a5a794c590
commit 4dadcf3871
9 changed files with 937 additions and 72 deletions
+77 -1
View File
@@ -1,3 +1,4 @@
import type { Client } from "discord.js-selfbot-v13";
import express from "express";
import helmet from "helmet";
import http from "http";
@@ -5,9 +6,11 @@ import path from "path";
import pinoHttp from "pino-http";
import prism from "prism-media";
import { WebSocketServer } from "ws";
import { AppError } from "./errors";
import { createChildLogger, logger } from "./logger";
import { getMetrics, uptimeGauge } from "./metrics";
import { discordPlayer } from "./player";
import type { VoiceController } from "./voiceController";
const wsLogger = createChildLogger("webserver");
@@ -42,7 +45,11 @@ function rmsDb(pcm: Buffer): number {
return 20 * Math.log10(Math.max(rms, 1e-10));
}
export function startWebserver(port: number = 3000) {
export function startWebserver(
port: number = 3000,
_client: Client,
voiceController: VoiceController,
) {
const app = express();
const server = http.createServer(app);
@@ -55,6 +62,7 @@ export function startWebserver(port: number = 3000) {
// HTTP request logging
app.use(pinoHttp({ logger }));
app.use(express.json());
app.use(express.static(path.join(__dirname, "../public")));
@@ -76,6 +84,51 @@ export function startWebserver(port: number = 3000) {
res.send(await getMetrics());
});
app.get("/api/status", (_req, res) => {
res.json(voiceController.getStatus());
});
app.get("/api/guilds", (_req, res) => {
res.json(voiceController.listGuilds());
});
app.get("/api/guilds/:guildId/voice-channels", async (req, res, next) => {
try {
res.json(await voiceController.listVoiceChannels(req.params.guildId));
} catch (error) {
next(error);
}
});
app.post("/api/connect", async (req, res, next) => {
try {
const { guildId, channelId } = req.body as {
guildId?: string;
channelId?: string;
};
if (!guildId || !channelId) {
throw new AppError(
"guildId and channelId are required",
"MISSING_CONNECT_FIELDS",
400,
);
}
res.json(await voiceController.connect(guildId, channelId));
} catch (error) {
next(error);
}
});
app.post("/api/disconnect", async (_req, res, next) => {
try {
res.json(await voiceController.disconnect());
} catch (error) {
next(error);
}
});
// Inbound: Discord PCM → tagged chunks → browser
(global as any).broadcastPcmToWeb = (chunk: Buffer, userId: string) => {
let hash = 0;
@@ -233,6 +286,29 @@ export function startWebserver(port: number = 3000) {
});
});
app.use(
(
error: Error,
_req: express.Request,
res: express.Response,
_next: express.NextFunction,
) => {
if (error instanceof AppError) {
res.status(error.statusCode).json({
error: error.code,
message: error.message,
});
return;
}
wsLogger.error({ error }, "Unhandled webserver error");
res.status(500).json({
error: "INTERNAL_SERVER_ERROR",
message: "Internal server error",
});
},
);
server.listen(port, "0.0.0.0", () => {
wsLogger.info({ port }, "Web interface listening");
});