import { CacheType, Client, GatewayIntentBits, Interaction, Message, OAuth2Scopes, PermissionFlagsBits, VoiceState } from 'discord.js'; import { Logger } from './utils/log'; import { config } from './utils/config'; import { CommandManager } from './commands'; type BotEventListeners = { messageCreate: (message: Message) => void; interactionCreate: (interaction: Interaction) => void; voiceStateUpdate: (oldState: VoiceState, newState: VoiceState) => void; }; type BotEventListener = BotEventListeners[K]; export class Bot { private client: Client | undefined; private cmdMgr: CommandManager; private readonly log: Logger; private eventListeners: { [K in keyof BotEventListeners]?: BotEventListener[]; } = {}; /* class methods */ private constructor() { this.log = new Logger('Bot'); this.cmdMgr = new CommandManager('./commands'); } public async init(): Promise { this.log.info('Bot init'); if (this.client) throw new Error('Client already exists, was init called twice?'); this.log.info('Loading commands'); await this.cmdMgr.init(); this.log.info('Instantiating client'); this.client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.MessageContent ] }); this.log.info('Registering callbacks'); this.client.on('clientReady', () => this.onReady()); this.client.on('error', (err) => this.onError(err, false)); this.client.on('shardError', (err) => this.onError(err, true)); this.client.on('messageCreate', (message: Message) => this.onMessage(message) ); this.client.on('interactionCreate', (interaction: Interaction) => this.onInteraction(interaction) ); this.client.on( 'voiceStateUpdate', (oldState: VoiceState, newState: VoiceState) => this.onVoiceStateUpdate(oldState, newState) ); this.log.info('Logging in...'); await this.client.login(config.token); } /* event listeners */ private onReady(): void { this.log.info('Logged in'); const inviteLink = this.client?.generateInvite({ scopes: [OAuth2Scopes.ApplicationsCommands, OAuth2Scopes.Bot], permissions: [ PermissionFlagsBits.AddReactions, PermissionFlagsBits.AttachFiles, PermissionFlagsBits.ChangeNickname, PermissionFlagsBits.Connect, PermissionFlagsBits.Speak, PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ReadMessageHistory, PermissionFlagsBits.SendMessages, PermissionFlagsBits.SendMessagesInThreads, PermissionFlagsBits.SendVoiceMessages, PermissionFlagsBits.EmbedLinks ] }); this.log.info('Invite link: %s', inviteLink); } private onError(error: Error, isShardError: boolean): void { if (isShardError) this.log.error( 'A shard error ocurred: %s - %s - %s', error.name, error.message, error.stack ); else this.log.error( 'An error ocurred: %s - %s - %s', error.name, error.message, error.stack ); } /* public event listeners */ private async onInteraction(interaction: Interaction): Promise { this.emit('interactionCreate', interaction); } private async onMessage(message: Message): Promise { this.emit('messageCreate', message); } private async onVoiceStateUpdate( oldState: VoiceState, newState: VoiceState ): Promise { this.emit('voiceStateUpdate', oldState, newState); } /* registerable event system */ public on( event: K, listener: BotEventListener ): void { if (!this.eventListeners[event]) { this.eventListeners[event] = []; } (this.eventListeners[event] as BotEventListener[]).push(listener); } public off( event: K, listener: BotEventListener ): boolean { const listeners = this.eventListeners[event]; if (!listeners) return false; const index = (listeners as BotEventListener[]).indexOf(listener); if (index > -1) { (listeners as BotEventListener[]).splice(index, 1); return true; } return false; } public once( event: K, listener: BotEventListener ): void { const onceWrapper = ((...args: Parameters>) => { this.off(event, onceWrapper as BotEventListener); (listener as (...args: unknown[]) => void)(...args); }) as BotEventListener; this.on(event, onceWrapper); } private emit( event: K, ...args: Parameters> ): void { const listeners = this.eventListeners[event]; if (listeners) { for (const listener of listeners as BotEventListener[]) { try { (listener as (...args: unknown[]) => void)(...args); } catch (error) { this.log.error( `Error in event listener for ${String(event)}:`, error ); } } } } /* singleton logic */ static #instance: Bot | null = null; public static get get(): Bot { if (!Bot.#instance) Bot.#instance = new Bot(); return Bot.#instance; } }