Compare commits

...

9 Commits

Author SHA1 Message Date
neru e3ad17d25e feat: add empty vc listener 2026-01-10 12:22:18 -03:00
neru ace276b2a8 feat: add join and leave commands 2026-01-10 12:22:12 -03:00
neru c9c88baa11 chore: install davey 2026-01-10 12:19:26 -03:00
neru 3e0654047d fix: ignore /dist for eslint 2026-01-10 12:17:51 -03:00
neru ae6296e0ae chore: add voice support packages 2026-01-10 12:08:19 -03:00
neru 20540f7b53 fix: make requiresAdmin optional 2026-01-10 12:07:04 -03:00
neru 207b16e1cd style: run format:apply 2026-01-10 12:05:24 -03:00
neru f69a62a314 feat: add isModule 2026-01-10 12:05:20 -03:00
neru 6ad0ba5340 fix: wrong file handling for builds 2026-01-10 11:13:02 -03:00
9 changed files with 1077 additions and 34 deletions
+1 -1
View File
@@ -5,5 +5,5 @@ import { defineConfig } from "eslint/config";
export default defineConfig([ export default defineConfig([
{ files: ["src/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } }, { files: ["src/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.browser } },
tseslint.configs.recommended, tseslint.configs.recommended, { ignores: ["dist/**", "**/dist/**"] }
]); ]);
+917 -7
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -34,6 +34,9 @@
"typescript-eslint": "^8.52.0" "typescript-eslint": "^8.52.0"
}, },
"dependencies": { "dependencies": {
"@discordjs/opus": "^0.10.0",
"@discordjs/voice": "^0.19.0",
"@snazzah/davey": "^0.1.9",
"colorts": "^0.1.63", "colorts": "^0.1.63",
"discord.js": "^14.25.1", "discord.js": "^14.25.1",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
+32 -26
View File
@@ -18,10 +18,11 @@ import {
import { Logger } from './utils/log'; import { Logger } from './utils/log';
import { config } from './utils/config'; import { config } from './utils/config';
import { Bot } from './bot'; import { Bot } from './bot';
import { isModule } from './utils/misc';
export interface Command { export interface Command {
name?: string; name?: string;
requiresAdmin: boolean; requiresAdmin?: boolean;
ownerOnly?: boolean; ownerOnly?: boolean;
execute?: (interaction: ChatInputCommandInteraction) => Promise<void>; execute?: (interaction: ChatInputCommandInteraction) => Promise<void>;
autocomplete?: (interaction: AutocompleteInteraction) => Promise<void>; autocomplete?: (interaction: AutocompleteInteraction) => Promise<void>;
@@ -33,7 +34,7 @@ export interface Command {
builder?: SlashCommandOptionsOnlyBuilder; builder?: SlashCommandOptionsOnlyBuilder;
} }
interface CommandCategoryInfo { export interface CommandCategoryInfo {
name: string; name: string;
description: string; description: string;
} }
@@ -51,8 +52,8 @@ export class CommandManager {
private categories: Array<CommandCategory> = []; private categories: Array<CommandCategory> = [];
/* /*
public public
*/ */
public constructor(cmdFolder: string) { public constructor(cmdFolder: string) {
this.folderPath = path.resolve(__dirname, cmdFolder); this.folderPath = path.resolve(__dirname, cmdFolder);
this.log = new Logger('Command manager'); this.log = new Logger('Command manager');
@@ -95,8 +96,8 @@ export class CommandManager {
} }
/* /*
internal internal
*/ */
private async populateCommands() { private async populateCommands() {
if (!fs.existsSync(this.folderPath)) if (!fs.existsSync(this.folderPath))
throw new Error(`Command directory not found: ${this.folderPath}`); throw new Error(`Command directory not found: ${this.folderPath}`);
@@ -144,12 +145,21 @@ export class CommandManager {
catPath: string catPath: string
): Promise<CommandCategoryInfo | undefined> { ): Promise<CommandCategoryInfo | undefined> {
try { try {
const descriptorPath = path.join(catPath, 'category.json'); const extensions = ['js', 'mjs', 'cjs', 'ts'];
if (!fs.existsSync(descriptorPath))
throw new Error('Missing categoryinfo.json');
const content = await fs.promises.readFile(descriptorPath, 'utf-8'); for (const ext of extensions) {
return JSON.parse(content) as CommandCategoryInfo; const descriptorPath = path.join(catPath, `_category.${ext}`);
if (!fs.existsSync(descriptorPath)) continue;
const module = await import(`file://${descriptorPath}`);
return (
module.default?.default ||
module.default ||
(module as CommandCategoryInfo)
);
}
throw new Error('Missing categoryinfo.json');
} catch (err) { } catch (err) {
this.log.error('Error loading category info at %s: %s', catPath, err); this.log.error('Error loading category info at %s: %s', catPath, err);
return undefined; return undefined;
@@ -159,7 +169,7 @@ export class CommandManager {
private async loadCatCommands(catPath: string): Promise<Array<Command>> { private async loadCatCommands(catPath: string): Promise<Array<Command>> {
const promises = fs const promises = fs
.readdirSync(catPath) .readdirSync(catPath)
.filter((file) => this.isValidCommandFile(file)) .filter((file) => isModule(file))
.map( .map(
async (file) => await this.attemptLoadCommand(path.join(catPath, file)) async (file) => await this.attemptLoadCommand(path.join(catPath, file))
); );
@@ -167,12 +177,8 @@ export class CommandManager {
} }
/* /*
cmd parsing cmd parsing
*/ */
private isValidCommandFile(file: string): boolean {
return file.endsWith('.js') || file.endsWith('.ts');
}
private async attemptLoadCommand(filePath: string): Promise<Command | null> { private async attemptLoadCommand(filePath: string): Promise<Command | null> {
try { try {
const module = await import(`file://${filePath}`); const module = await import(`file://${filePath}`);
@@ -188,8 +194,8 @@ export class CommandManager {
} }
/* /*
misc functions misc functions
*/ */
private async registerSlashCommands(): Promise<void> { private async registerSlashCommands(): Promise<void> {
this.log.info('Registering slash commands...'); this.log.info('Registering slash commands...');
@@ -214,8 +220,8 @@ export class CommandManager {
} }
/* /*
interaction handling interaction handling
*/ */
private async executeCommandInteraction( private async executeCommandInteraction(
interaction: ChatInputCommandInteraction interaction: ChatInputCommandInteraction
): Promise<void> { ): Promise<void> {
@@ -290,12 +296,12 @@ export class CommandManager {
} }
/* /*
event listeners event listeners
*/ */
private async onInteraction(interaction: BaseInteraction): Promise<void> { private async onInteraction(interaction: BaseInteraction): Promise<void> {
/* /*
cmd execution cmd execution
*/ */
if (interaction.isChatInputCommand()) if (interaction.isChatInputCommand())
return this.executeCommandInteraction(interaction); return this.executeCommandInteraction(interaction);
+8
View File
@@ -0,0 +1,8 @@
import { CommandCategoryInfo } from '../../commands';
const info: CommandCategoryInfo = {
name: 'Voice',
description: 'Voice chat related commands'
};
export default info;
+47
View File
@@ -0,0 +1,47 @@
import { ChatInputCommandInteraction, GuildMember, SlashCommandBuilder } from "discord.js";
import { Command } from "../../commands";
import { CreateVoiceConnectionOptions, getVoiceConnection, joinVoiceChannel, JoinVoiceChannelOptions } from "@discordjs/voice";
const builder = new SlashCommandBuilder()
.setName("join")
.setDescription("Makes the bot join your current voice channel");
const cmd: Command = {
name: builder.name,
builder: builder,
execute: async (interaction: ChatInputCommandInteraction): Promise<void> => {
const member = interaction.member as GuildMember;
if (!member || !interaction.guild) {
interaction.reply("This command only works on guilds");
return;
}
if (!member.voice.channelId) {
interaction.reply("You are not currently on a voice channel");
return;
}
const me = interaction.guild.members.me as GuildMember;
if (getVoiceConnection(interaction.guild.id) && me.voice.channelId === member.voice.channelId) {
interaction.reply("Already connected");
return;
}
const voiceOptions: JoinVoiceChannelOptions & CreateVoiceConnectionOptions = {
channelId: member.voice.channelId,
guildId: interaction.guild.id,
adapterCreator: interaction.guild.voiceAdapterCreator
};
const connection = await joinVoiceChannel(voiceOptions);
if (!connection) {
interaction.reply("Unable to join");
return
}
interaction.reply("Joined");
}
}
export default cmd;
+32
View File
@@ -0,0 +1,32 @@
import { ChatInputCommandInteraction, GuildMember, SlashCommandBuilder } from "discord.js";
import { Command } from "../../commands";
import { getVoiceConnection } from "@discordjs/voice";
const builder = new SlashCommandBuilder()
.setName("leave")
.setDescription("Makes the bot leave its current voice channel");
const cmd: Command = {
name: builder.name,
builder: builder,
execute: async (interaction: ChatInputCommandInteraction): Promise<void> => {
const member = interaction.member as GuildMember;
if (!member || interaction.guild === null) {
interaction.reply("This command only works on guilds");
return;
}
const connection = getVoiceConnection(interaction.guildId as string);
if (!connection) {
interaction.reply('currently not connected to a voice channel')
return;
}
connection.disconnect();
connection.destroy();
interaction.reply("Disconnected");
}
}
export default cmd;
+28
View File
@@ -0,0 +1,28 @@
import { VoiceState } from "discord.js";
import { Command } from "../../commands";
import { getVoiceConnection } from "@discordjs/voice";
const cmd: Command = {
voiceStateListener: async function (oldState: VoiceState): Promise<void> {
const guild = oldState.guild;
if (!guild) return;
const voiceConnection = getVoiceConnection(guild.id);
if (!voiceConnection) return;
const me = guild.members.me;
if (!me)
return;
if (!me.voice.channel)
return;
if (me.voice.channel.members.size > 1)
return;
voiceConnection.disconnect();
voiceConnection.destroy();
}
}
export default cmd;
+9
View File
@@ -0,0 +1,9 @@
const MOD_EXCLUDE: Array<string> = ['.d.ts'];
const MOD_INCLUDE: Array<string> = ['.ts', '.js', '.mjs', '.cjs'];
export function isModule(path: string): boolean {
for (const ext of MOD_EXCLUDE) if (path.endsWith(ext)) return false;
for (const ext of MOD_INCLUDE) if (path.endsWith(ext)) return true;
return false;
}