1
0
Fork 0
forked from Elijah/AudioBot
This commit is contained in:
Elijah R 2024-01-25 22:03:22 -05:00
commit a062579d36
13 changed files with 782 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
node_modules/
bin/
build/
config.json

1
.npmrc Normal file
View file

@ -0,0 +1 @@
package-lock=false

22
README.MD Normal file
View file

@ -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

125
audio.c Normal file
View file

@ -0,0 +1,125 @@
#include <stdio.h>
#include <rfb/rfbclient.h>
// 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;
}

10
config.example.json Normal file
View file

@ -0,0 +1,10 @@
{
"DiscordToken": "",
"DiscordClientID": "",
"VMs": [
{
"name": "vm1",
"vncaddr": "127.0.0.1:5900"
}
]
}

312
index_old.js Normal file
View file

@ -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);
});

24
name.c Normal file
View file

@ -0,0 +1,24 @@
#include <stdio.h>
#include <rfb/rfbclient.h>
#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;
}

25
package.json Normal file
View file

@ -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"
}
}

21
src/commands.ts Normal file
View file

@ -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;

8
src/config.ts Normal file
View file

@ -0,0 +1,8 @@
export default interface IConfig {
DiscordToken: string;
DiscordClientID: string;
VMs : {
name : string;
vncaddr : string;
}[];
}

118
src/index.ts Normal file
View file

@ -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<void>((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
}));
}

3
src/log.ts Normal file
View file

@ -0,0 +1,3 @@
export default function log(loglevel : string, message : string) {
console.log(`[${new Date().toLocaleString()}] [${loglevel}] ${message}`);
}

109
tsconfig.json Normal file
View file

@ -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 '<reference>'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. */
}
}