From a062579d36f653f2232bd8011686f4471a7be1bd Mon Sep 17 00:00:00 2001 From: Elijah R Date: Thu, 25 Jan 2024 22:03:22 -0500 Subject: [PATCH] init --- .gitignore | 4 + .npmrc | 1 + README.MD | 22 ++++ audio.c | 125 ++++++++++++++++++ config.example.json | 10 ++ index_old.js | 312 ++++++++++++++++++++++++++++++++++++++++++++ name.c | 24 ++++ package.json | 25 ++++ src/commands.ts | 21 +++ src/config.ts | 8 ++ src/index.ts | 118 +++++++++++++++++ src/log.ts | 3 + tsconfig.json | 109 ++++++++++++++++ 13 files changed, 782 insertions(+) create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 README.MD create mode 100644 audio.c create mode 100644 config.example.json create mode 100644 index_old.js create mode 100644 name.c create mode 100644 package.json create mode 100644 src/commands.ts create mode 100644 src/config.ts create mode 100644 src/index.ts create mode 100644 src/log.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b21b136 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +bin/ +build/ +config.json \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..9cf9495 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..ed9ceaf --- /dev/null +++ b/README.MD @@ -0,0 +1,22 @@ +# AudioBot +A simple bot that relays audio from a QEMU VM to a Discord call. Developed for use with CollabVM. + +## External dependencies + +- libvncclient + +## Running + +1. Install depencencies: `npm i` +2. Build the typescript: `npm run build` +3. Build the C binaries: (TODO: Add build system) + - `mkdir bin` + - `gcc audio.c -lvncclient -o bin/audio` + - `gcc name.c -lvncclient -o bin/name` +4. Copy config.example.json to config.json, and fill out your discord token and client ID, as well as a list of available VMs and their VNC servers +5. **Run it:** `node build/index.js` + +## Credits + +- QEMU Audio/C portion: DarkOK +- Discord bot/TypeScript portion: Elijah R \ No newline at end of file diff --git a/audio.c b/audio.c new file mode 100644 index 0000000..fd46ba8 --- /dev/null +++ b/audio.c @@ -0,0 +1,125 @@ +#include +#include + +// The world if libvncclient let you easily connect to a server without using their own retarded argv shit +// (audio and name wouldn't even have to be 2 different binaries and password could easily be specified!!!) + +#define VNC_PASSWORD "D0gg0!!!" + +#define VNC_ENCODING_AUDIO 0XFFFFFEFD /* -259 */ + +#define VNC_MSG_CLIENT_QEMU 255 +#define VNC_MSG_CLIENT_QEMU_AUDIO 1 + +#define VNC_MSG_CLIENT_QEMU_AUDIO_ENABLE 0 +#define VNC_MSG_CLIENT_QEMU_AUDIO_DISABLE 1 +#define VNC_MSG_CLIENT_QEMU_AUDIO_SET_FORMAT 2 + +#define VNC_MSG_SERVER_QEMU_AUDIO_END 0 +#define VNC_MSG_SERVER_QEMU_AUDIO_BEGIN 1 +#define VNC_MSG_SERVER_QEMU_AUDIO_DATA 2 + +#define AUDIO_FORMAT_U8 0 +#define AUDIO_FORMAT_S8 1 +#define AUDIO_FORMAT_U16 2 +#define AUDIO_FORMAT_S16 3 +#define AUDIO_FORMAT_U32 4 +#define AUDIO_FORMAT_S32 5 + +#define VNC_QEMU_AUDIO_RATE 48000 +#define VNC_QEMU_AUDIO_CHANNELS 2 +#define VNC_QEMU_AUDIO_BPS 16 + +char * getpassword(rfbClient *client) { + return strdup(VNC_PASSWORD); +}; + +static rfbBool vnc_qemu_audio_encoding(rfbClient* client, rfbFramebufferUpdateRectHeader* rect) { + struct { + uint8_t type; + uint8_t msg_id; + uint16_t audio_id; + struct { + uint8_t format; + uint8_t channels; + char frequency[sizeof(uint32_t)]; + } set_format; + } audio_format_msg = { + VNC_MSG_CLIENT_QEMU, + VNC_MSG_CLIENT_QEMU_AUDIO, + rfbClientSwap16IfLE(VNC_MSG_CLIENT_QEMU_AUDIO_SET_FORMAT), + AUDIO_FORMAT_S16, + VNC_QEMU_AUDIO_CHANNELS + }; + + *(uint32_t*) audio_format_msg.set_format.frequency = rfbClientSwap32IfLE(VNC_QEMU_AUDIO_RATE); + + if (!WriteToRFBServer(client, (char*)& audio_format_msg, sizeof(audio_format_msg))) return FALSE; + + struct { + uint8_t type; + uint8_t msg_id; + uint16_t audio_id; + } audio_enable_msg = { + VNC_MSG_CLIENT_QEMU, + VNC_MSG_CLIENT_QEMU_AUDIO, + rfbClientSwap16IfLE(VNC_MSG_CLIENT_QEMU_AUDIO_ENABLE) + }; + + if (!WriteToRFBServer(client, (char*)& audio_enable_msg, sizeof(audio_enable_msg))) return FALSE; + + return TRUE; +} + +static rfbBool vnc_qemu_audio_msg(rfbClient* client, rfbServerToClientMsg* message) { + if (message->type != VNC_MSG_CLIENT_QEMU) return FALSE; + + struct { + uint8_t msg_id; + char audio_id[sizeof(uint16_t)]; + } msg; + + if (!ReadFromRFBServer(client, (char*)&msg, sizeof(msg))) return TRUE; + if (msg.msg_id != VNC_MSG_CLIENT_QEMU_AUDIO) return TRUE; + + switch (rfbClientSwap16IfLE(*(uint16_t*)msg.audio_id)) { + case VNC_MSG_SERVER_QEMU_AUDIO_BEGIN: + break; + case VNC_MSG_SERVER_QEMU_AUDIO_DATA: { + uint32_t size; + if (!ReadFromRFBServer(client, (char*) &size, sizeof(uint32_t))) return TRUE; + size = rfbClientSwap32IfLE(size); + char* data = malloc(size); + if (ReadFromRFBServer(client, data, size)) fwrite(data, sizeof(char), size, stdout); + free(data); + break; + }; + case VNC_MSG_SERVER_QEMU_AUDIO_END: + break; + }; + + return TRUE; +} + +static int QEMU_AUDIO_ENCODING[] = {VNC_ENCODING_AUDIO, 0}; +static rfbClientProtocolExtension qemu_audio_extension = { + QEMU_AUDIO_ENCODING, + vnc_qemu_audio_encoding, + vnc_qemu_audio_msg, + NULL, + NULL, + NULL +}; + +int main(int argc, char **argv) { + rfbClient* client = rfbGetClient(8,3,4); + client->GetPassword = getpassword; + rfbClientRegisterExtension(&qemu_audio_extension); + if (!rfbInitClient(client,&argc,argv)) return 1; + while (1) { + if (WaitForMessage(client,50) < 0) break; + if (!HandleRFBServerMessage(client)) break; + }; + rfbClientCleanup(client); + return 0; +} diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..88d7097 --- /dev/null +++ b/config.example.json @@ -0,0 +1,10 @@ +{ + "DiscordToken": "", + "DiscordClientID": "", + "VMs": [ + { + "name": "vm1", + "vncaddr": "127.0.0.1:5900" + } + ] +} \ No newline at end of file diff --git a/index_old.js b/index_old.js new file mode 100644 index 0000000..e73ba8b --- /dev/null +++ b/index_old.js @@ -0,0 +1,312 @@ +// This is the old version of the bot, written by DarkOK, and is kept here for archival purposes. +// It no longer works due to discord api changes, hence the rewrite. + + +const Discord = require('discord.js-v11'); // update this you lazy prick and rewrite the bot +const client = new Discord.Client(); +const {execSync, exec, spawnSync, spawn} = require('child_process'); + +var token = ""; // token used for authentication to discord + +var ownerIds = []; // user ids of people that can eval, change servers to non-whitelisted ones, etc + +var pf = "cvm!"; // command prefix +var vncServers = [ + "localhost:5900" +]; // servers that appears in the list, and ones that people don't need admin to switch to (first item in array is default server) +// (if this is public, keep this as the one (Hm i should really make all VMs use one broadcast for all servers to fix this)) + +// dont modify +var vncServer = vncServers[0]; +var vncName = ""; + +// especially dont modify these +var arecord; +var broadcast; +var bp={txt:"",type:"",status:""}; + +function changeStatus(txt, type, status) { + bp.txt = txt; + bp.type = type ? type : "LISTENING"; + bp.status = status ? status : "online"; + + if (bp.txt != txt || bp.type != type || bp.status != status) { + client.user.setPresence({ + activity: { + name: pf + "help" + " | " + bp.txt, + type: bp.type + }, + status: bp.status + }); + } +}; + +function startStream() { + console.log("Connecting to " + vncServer); + + changeStatus("Connecting"); + + exec("./name " + vncServer, (error, stdout, stderr) => { + if (error) { + //vncName = "Failed to get VM name"; + console.log("Error getting VM name: " + error.message); + } else { + vncName = stdout; + console.log("VNC name: " + vncName); + changeStatus(vncName); + }; + }); + + arecord = spawn('./audio', [vncServer]); + + arecord.stderr.setEncoding("utf8"); + arecord.stderr.on("data", data => { + console.log(data); + }); + + arecord.on("close", code => { + console.log("Audio process exited with " + code); + setTimeout(()=>{ + startStream(); + for (var a of client.voice.connections.values()) { + a.play(broadcast); + } + }, 2000); + }); + + broadcast = client.voice.createBroadcast(); + broadcast.play( + arecord.stdout.on("data", data => { + return data; + }), + { + type: "converted", + volume: false, + highWaterMark: 1 + } + ); +}; + +client.login(token); + +client.on("ready", ()=>{ + console.log(`Logged in as: ${client.user.tag} (ID: ${client.user.id})`); + console.log(`Prefix is: ${pf}`); + console.log(`Currently in ${Array.from(client.guilds.cache.values()).length} servers\n`); + + startStream(); +}); + +client.on("error", async err => { + console.log("An error occurred: " + err.message); +}); + +// HURR NO GOOD COMMAND HANDLER HURR DURR +client.on("message", async message => { + if (!message.guild) return; + if (!message.member) return; + if (message.member.id == client.user.id) return; + // i could put it in the same one if i want to BUT I CANT BE BOTHERED + + var args = message.content.split(' '); + var cmd = args.shift(); + + //console.log(`[Message] <${message.author.tag} in ${message.guild.name}> ${message.content}`); + + if (cmd.startsWith(pf)) { + cmd = cmd.slice(pf.length).toLowerCase(); + console.log(`[Command] <${message.author.tag} in #${message.channel.name}@${message.guild.name}> ${message.content}`); + switch (cmd) { + + case "play": + case "join": + case "connect": + if (message.member.voice.channel) { + message.member.voice.channel.join().then(connection => { + connection.play(broadcast); + message.reply("joined"); + }).catch(err =>{ + message.reply("an error occurred while trying to join the voice channel: `" + err.message + '`'); + console.log(err.message); + }); + } else { + message.reply("join a voice channel"); + }; + break; + + case "stop": + case "leave": + case "disconnect": + if (message.member.voice.channel) { + message.member.voice.channel.leave(); + message.reply("left"); + } else { + message.reply("join a voice channel so I know where to leave"); + // i can probably make it not do it this way but Cannot Be Bothered + }; + break; + + case "eval": + if (ownerIds.includes(message.member.id)) { + try { + //console.log("Evaluating " + args.join(' ')); + var evalOutput = eval(args.join(' ')); + //var evalOutputChunks = evalOutput.match(/.{1,1950}/g); + message.reply("```\n" + evalOutput + "\n```"); + //for (var i = 0; i < evalOutputChunks.length; i++) { + // message.channel.send(`Part ${i + 1} of ${evalOutputChunks.length}` + "```\n" + evalOutputChunks[i] + "\n```"); + //} + console.log(evalOutput); + } catch (err) { + message.reply("fuck!\n```\n" + err.stack + "\n```"); + } + } else { + message.reply("you need to be in the owner list"); + }; + break; + + case "rs": + case "restartstream": + if (ownerIds.includes(message.member.id)) { + arecord.kill("SIGTERM"); + console.log("Terminated the audio process"); + message.reply("terminated the audio process"); + } else { + message.reply("you need to be in the owner list"); + }; + break; + + case "ss": + case "setserver": + // to do: maybe I could make it so each server can choose which VM in the list it plays audio from, instead of using the same broadcast + // (although maybe still use broadcasts for multiple connections to the same server, for optimisation, and less connections to the same server) + if (vncServers.includes(args[0]) || ownerIds.includes(message.member.id)) { + if (args[0] != undefined) { + vncServer = args[0]; + arecord.kill("SIGTERM"); + console.log("Set the VNC server to " + args[0] + " and terminated the audio process"); + message.reply("set the VNC server to `" + args[0] + "` and terminated the audio process"); + } else { + message.reply("specify a server you dumb fuck"); + }; + } else { + message.reply("you need to be in the owner list to connect to VNC servers not in the VNC server list (check `" + pf + "list`)"); + // having the ability for anybody to connect to literally any host + tcp port maybe isn't a good idea + }; + break; + + case "cs": + case "currentserver": + if (ownerIds.includes(message.member.id)) { + message.reply(`\nIP: \`${vncServer}\`\nName: \`${vncName}\``); + } else { + message.reply("you need to be in the owner list"); + }; + break; + + case "list": + var vmlist = "VM List:\n"; + vncServers.forEach(server => { + var vmname = spawnSync("./name", [server], {timeout: 2000}); + if (!vmname.status && vmname.stdout) { + vmlist += `\`${server}\` - **${vmname.stdout}**\n`; + } else { + vmlist += `\`${server}\` - Failed to get name\n`; + }; + }); + if (vmlist.length < 2000) message.reply(vmlist); + else message.reply("VM list is too big to fit in a message (oops!)"); + break; + + case "guilds": + if (ownerIds.includes(message.member.id)) { + let guildsOnEachPage = 10; + let guildsTmp = Array.from(client.guilds.cache.values()); + let pageCount = Math.ceil(guildsTmp.length / guildsOnEachPage); + + if (args[0] == "all") { + for (var i = 1; i <= pageCount; ++i) { + let guildList = `Guild list - __**Page ${i}/${pageCount}**__:\n`; + + guildsTmp.slice((i * guildsOnEachPage) - guildsOnEachPage, (i * guildsOnEachPage)).forEach(guild => { + guildList += `**\`${guild.name}\`** - owned by ${guild.ownerID}, with ${guild.memberCount} users ${client.voice.connections.has(guild.id) ? `__**[Voice connected, ${Array.from(client.voice.connections.get(guild.id).channel.members).length} users]**__` : ""}\n`; // obese as shit holy FUCK + }); + + if (guildList.length < 2000) message.channel.send(guildList); + else message.reply("Guild list is too big to fit in a message (oops!)"); + }; + } else { + let page = isNaN(parseInt(args[0])) ? 1 : parseInt(args[0]); + let guildList = `Guild list - __**Page ${page}/${pageCount}**__:\n`; + + guildsTmp.slice((page * guildsOnEachPage) - guildsOnEachPage, (page * guildsOnEachPage)).forEach(guild => { + guildList += `**\`${guild.name}\`** - owned by ${guild.ownerID}, with ${guild.memberCount} users ${client.voice.connections.has(guild.id) ? `__**[Voice connected, ${Array.from(client.voice.connections.get(guild.id).channel.members).length} users]**__` : ""}\n`; // obese as shit holy FUCK + }); + + if (guildList.length < 2000) message.channel.send(guildList); + else message.reply("Guild list is too big to fit in a message (oops!)"); + }; + } else { + message.reply("you need to be in the owner list"); + }; + break; + + case "stats": + let uptimeSeconds = (client.uptime / 1000); + let botStats = "Bot stats:\n"; + botStats += "**Server count**: " + Array.from(client.guilds.cache).length + '\n'; + botStats += "**Voice connection count**: " + Array.from(client.voice.connections).length + '\n'; + + botStats += "**Bot uptime**: "; + botStats += Math.floor(uptimeSeconds / 86400) + " days, "; + uptimeSeconds %= 86400; + botStats += Math.floor(uptimeSeconds / 3600) + " hrs, "; + uptimeSeconds %= 3600; + botStats += Math.floor(uptimeSeconds / 60) + " mins, "; + botStats += Math.floor(uptimeSeconds % 60) + " secs\n"; + + botStats += "**Current VM name**: `" + vncName + "`\n"; + + if (botStats.length < 2000) message.reply(botStats); + else message.reply("stats are too big to fit in a message (oops!)"); + // i should probably calculate how big the mention part is too, but this somewhat works for now + break; + + case "about": + message.reply("this uses DarkOK's own QEMU_VNC_Audio->Discord bot"); + break; + + case "invite": + message.reply(`my invite URL is: https://discord.com/oauth2/authorize?client_id=${client.user.id}&permissions=36703232&scope=bot`); + break; + + case "help": + message.reply( + "Command list:\n" + + `**${pf}join** - Join a voice channel\n` + + `**${pf}leave** - Leave a voice channel\n` + + `**${pf}invite** - Sends the invite URL for the bot\n` + + `**${pf}stats** - Display some statistics about the bot\n` + + '\n' + + "and that's about it\n" + + "sometimes audio just stops working, try making the bot leave and rejoin the voice channel to see if it solves the issue\n" + + "this bot is relatively unstable as of now, expect crashes/things not working\n\n" + + "contact? dark@darkok.xyz" + ); + // really shitty but it's the best I can do since there's no proper command handler + break; + }; + }; +}); + +client.on("guildCreate", async guild => { + console.log(`[Server Joined] ${guild.name} - ${guild.memberCount} members, ID: ${guild.id}`); +}); + +client.on("guildDelete", async guild => { + console.log(`[Server Left] ${guild.name} - ${guild.memberCount} members, ID: ${guild.id}`); +}); + +process.on("unhandledRejection", async err => { + console.error("Unhandled promise rejection:", err); +}); diff --git a/name.c b/name.c new file mode 100644 index 0000000..7a3d99d --- /dev/null +++ b/name.c @@ -0,0 +1,24 @@ +#include +#include + +#define VNC_PASSWORD "D0gg0!!!" + +char * getpassword(rfbClient *client) { + return strdup(VNC_PASSWORD); +}; + +int main(int argc, char **argv) { + rfbClient* client = rfbGetClient(8,3,4); + rfbEnableClientLogging = FALSE; + client->GetPassword = getpassword; + if (!rfbInitClient(client,&argc,argv) || WaitForMessage(client,50) < 0 || !HandleRFBServerMessage(client)) return 1; + + if (*client->desktopName) { + fputs(client->desktopName, stdout); + } else { + printf("a virtual machine"); + }; + + rfbClientCleanup(client); + return 0; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b479e39 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "collabvm-audiobot", + "version": "1.0.0", + "description": "QEMU VNC Audio to Discord bridge", + "main": "build/index.js", + "scripts": { + "build": "tsc" + }, + "author": "DarkOK, Elijah R", + "license": "ISC", + "type": "module", + "devDependencies": { + "@types/memorystream": "^0.3.4", + "@types/node": "^20.11.5", + "typescript": "^5.3.3" + }, + "dependencies": { + "@discordjs/opus": "^0.9.0", + "@discordjs/voice": "^0.16.1", + "discord.js": "^14.14.1", + "libsodium-wrappers": "^0.7.13", + "memorystream": "^0.3.1", + "sodium": "^3.0.2" + } +} diff --git a/src/commands.ts b/src/commands.ts new file mode 100644 index 0000000..b14cd73 --- /dev/null +++ b/src/commands.ts @@ -0,0 +1,21 @@ +import { SlashCommandBuilder, APIApplicationCommandOptionChoice, SlashCommandStringOption } from "discord.js"; +import IConfig from './config.js'; +import { readFileSync } from "fs"; +const config : IConfig = JSON.parse(readFileSync('./config.json', 'utf8')); + +const Commands = [ + new SlashCommandBuilder() + .setName('connect') + .setDescription('Connect the bot to the specified VM') + .addStringOption(option => { + option.setName('vm'); + option.setDescription('The QEMU instance to connect to'); + option.setRequired(true); + config.VMs.forEach(vm => option.addChoices({name: vm.name, value: vm.name})); + return option; + }).toJSON(), + new SlashCommandBuilder() + .setName('disconnect') + .setDescription('Leave the voice channel'), + ]; +export default Commands; \ No newline at end of file diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..a1b7c95 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,8 @@ +export default interface IConfig { + DiscordToken: string; + DiscordClientID: string; + VMs : { + name : string; + vncaddr : string; + }[]; +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..e7a7d01 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,118 @@ +import {REST, Routes, Client, GatewayIntentBits, ChatInputCommandInteraction, Guild, GuildMember} from 'discord.js'; +import {AudioPlayer, AudioPlayerStatus, StreamType, VoiceConnectionStatus, createAudioPlayer, createAudioResource, getVoiceConnection, joinVoiceChannel} from '@discordjs/voice'; +import Commands from './commands.js'; +import IConfig from './config.js'; +import log from './log.js'; +import {readFileSync} from 'fs'; +import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process'; +import { Readable } from 'stream'; +import MemoryStream from 'memorystream'; + +log("INFO", "QEMU Discord Audio bot starting..."); +const config : IConfig = JSON.parse(readFileSync('./config.json', 'utf8')); +const rest = new REST({version: '10'}).setToken(config.DiscordToken); +const client = new Client({intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates]}); + +var player : AudioPlayer = createAudioPlayer(); +player.on('error', (error) => { + log("ERROR", `Audio player: ${error.message}`); +}); +player.on(AudioPlayerStatus.Playing, () => log("INFO", "Audio playing")); +player.on(AudioPlayerStatus.Idle, () => { + log("INFO", "Audio idle"); + stream.destroy(); + if (audioprc) audioStream(); +}) +var vncname : string = ""; +var audioprc : ChildProcessWithoutNullStreams | null = null; +var stream : MemoryStream; + +client.on('ready', () => { + log("INFO", `Logged into discord as ${client.user?.tag}`); +}); +client.on('interactionCreate', async i => { + if (!i.isChatInputCommand) return; + var cmd = i as ChatInputCommandInteraction; + switch (cmd.commandName) { + case "connect": { + await cmd.deferReply(); + // kill the old process if it exists + if (audioprc != null) audioprc.kill("SIGTERM"); + // get vnc address + var vm = config.VMs.find(vm => vm.name == cmd.options.getString('vm')); + if (!vm) { + await cmd.editReply("Invalid VM"); + return; + } + var vncaddr = vm.vncaddr; + // get vnc name + var error = false; + await new Promise((res, rej) => { + exec(`./bin/name ${vncaddr}`, (err, stdout, stderr) => { + if (err) { + log("ERROR", `Failed to get name of VM: ${err}`); + cmd.editReply("Failed to connect to VNC"); + error = true; + } else vncname = stdout; + res(); + }); + }) + if (error) return; + log("INFO", `Connecting to VNC ${vncname} at ${vncaddr}`); + // start the audio process + audioprc = spawn("./bin/audio", [vncaddr]); + audioprc.stderr.setEncoding('utf8'); + audioprc.stderr.on('data', console.log); + audioprc.on('exit', (code) => { + audioprc = null; + log("INFO", `Audio process exited with code ${code}`); + }); + audioStream(); + // connect to voice channel if not already connected + var guild = cmd.guild!; + if (getVoiceConnection(guild.id)) { + await cmd.editReply(`Connected to ${vncname}`); + return; + } + var channel = (cmd.member! as GuildMember).voice.channel; + if (!channel) { + await cmd.editReply(`Connected to ${vncname}`); + return; + } + var con = joinVoiceChannel({ + guildId: guild.id, + channelId: channel.id, + adapterCreator: guild.voiceAdapterCreator, + }); + con.on(VoiceConnectionStatus.Ready, () => log("INFO", `Connected to voice channel ${channel!.name}`)); + con.subscribe(player); + await cmd.editReply(`Connected to ${vncname}`); + break; + } + case "disconnect": { + var conn = getVoiceConnection(cmd.guild!.id); + if (conn == undefined) { + await cmd.reply("Not connected to a voice channel"); + return; + } + conn.disconnect(); + await cmd.reply("Disconnected"); + } + } +}); +async function main() { + log("INFO", "Publishing slash commands..."); + //await rest.put(Routes.applicationCommands(config.DiscordClientID), {body: Commands}); + client.login(config.DiscordToken); +} +main(); + +function audioStream() { + if (!audioprc) return; + stream = new MemoryStream(); + audioprc.stdout.pipe(stream); + player.play(createAudioResource(stream, { + inputType: StreamType.Raw, + inlineVolume: false + })); +} \ No newline at end of file diff --git a/src/log.ts b/src/log.ts new file mode 100644 index 0000000..206c907 --- /dev/null +++ b/src/log.ts @@ -0,0 +1,3 @@ +export default function log(loglevel : string, message : string) { + console.log(`[${new Date().toLocaleString()}] [${loglevel}] ${message}`); +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4de4143 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,109 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "es2022", /* Specify what module code is generated. */ + "rootDir": "./src", /* Specify the root folder within your source files. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./build", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +}