import { Client, GatewayIntentBits, Guild, Collection, GuildMember, Message, Partials, Role, TextChannel } from 'discord.js' import * as dotenv from 'dotenv' import { DyNTS_SingletonService } from '../../../_services/base/singleton.service'; import { DyFM_Async, DyFM_Error, DyFM_Log } from '@futdevpro/fsm-dynamo'; import { DyNTS_global_settings } from '../../../_collections/global-settings.const'; import { DyNTS_DiBo_Routines_ControlService } from './dibo-routines.control-service'; import { DyNTS_DiBo_IO_ControlService } from './dibo-io.control-service'; import { DyNTS_DiBo_global_settings } from '../_collections/dibo-global-settings.conts'; import { DyNTS_GlobalService } from '../../../_services/core/global.service'; import { DyNTS_DiBo_Operations_Util } from '../_collections/dibo-operations.util'; import { DyNTS_DiBo_Commands_ControlService } from './dibo-commands.control-service'; /** * This is the main control service for the Discord Bot. * * * Setup: * 1. creating the Bot * - go to: https://discord.com/developers/applications * - click: "New Application" * - give a name → Create * * 2. adding the Bot to the app * - on the left: "Bot" → "Add Bot" → Yes, do it * * 3. getting the token * - on the same page: "Reset Token" → Copy * - put the token in the .env file: DISCORD_BOT_TOKEN=... * * 4. Bot ID (Client ID) and Invite link * - on the left: "OAuth2" > "URL Generator" * - Scopes: tick the bot * - Bot Permissions: pl. Send Messages, Read Message History * - at the bottom: generate Invite URL → open it → add it to a server * - the Client ID is on the same page, the "Application ID" on the "OAuth2 > General" or "Application" tab. * - it only works if you are admin on that server or have "Manage Server" permissions. * * 5. Permissions * - on the left: "Bot" tab * - enable these: * - MESSAGE CONTENT INTENT * - PRESENCE INTENT (if you use it) * - SERVER MEMBERS INTENT (if you need guild user info) * - Save * */ export abstract class DyNTS_DiBo_Main_ControlService extends DyNTS_SingletonService { /* static getInstance(): DyNTS_DiBo_Main_ControlService { return DyNTS_DiBo_Main_ControlService.getSingletonInstance(); } */ readonly client: Client; private _discordServer: Guild; get discordServer(): Guild { return this._discordServer; } get botClientId(): string { return this.client?.user?.id ?? DyNTS_global_settings?.env_settings?.discord?.clientId; } get botDisplayName(): string { return this.client?.user?.displayName; } protected botIO_CS: DyNTS_DiBo_IO_ControlService; protected abstract getBotIOControlService(): DyNTS_DiBo_IO_ControlService; protected routines_CS: DyNTS_DiBo_Routines_ControlService; protected abstract getRoutinesControlService(): DyNTS_DiBo_Routines_ControlService; protected commands_CS: DyNTS_DiBo_Commands_ControlService; protected abstract getCommandsControlService(): DyNTS_DiBo_Commands_ControlService; private _channelsByNames: { [key: string]: TextChannel; } = {}; private _rolesByNames: { [key: string]: Role; } = {}; protected debugLog = false; protected constructor( setupAutomatically?: boolean, ) { super(); DyNTS_global_settings.bot_settings ??= DyNTS_DiBo_global_settings; if (this.client) { throw new DyFM_Error({ ...this.getDefaultErrorSettings( 'start', new Error('Discord client already initialized'), 'system-init' ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DiBo-MCS-C000`, }) } this.client = new Client({ intents: DyNTS_global_settings.bot_settings.intents, partials: DyNTS_global_settings.bot_settings.partials, }) DyFM_Log.success('Discord client initialized'); if (setupAutomatically) { this.setup('system-init'); } } async setup(issuer: string): Promise { try { await DyFM_Async.delay(100); if (!DyNTS_global_settings.env_settings.discord.guildId) { throw new Error( `No discord server id found!` + `\nPlease set the DISCORD_GUILD_ID in the .env file, ` + `or directly in the DyNTS_global_settings.env_settings.discord.guildId` + `\nYou also will need to set the DISCORD_BOT_TOKEN, DISCORD_BOT_CLIENT_ID and DISCORD_BOT_OAUTH2_URL.` ); } if (!DyNTS_global_settings.env_settings.discord.token) { throw new Error( `No discord token found!` + `\nPlease set the DISCORD_BOT_TOKEN in the .env file, ` + `or directly in the DyNTS_global_settings.env_settings.discord.token` + `\nYou also will need to set the DISCORD_GUILD_ID, DISCORD_BOT_CLIENT_ID and DISCORD_BOT_OAUTH2_URL.` ); } if (!DyNTS_global_settings.env_settings.discord.clientId) { throw new Error( `No discord client id found!` + `\nPlease set the DISCORD_BOT_CLIENT_ID in the .env file, ` + `or directly in the DyNTS_global_settings.env_settings.discord.clientId` + `\nYou also will need to set the DISCORD_GUILD_ID, DISCORD_BOT_TOKEN and DISCORD_BOT_OAUTH2_URL.` ); } if (!DyNTS_global_settings.env_settings.discord.oauth2Url) { throw new Error( `No discord oauth2 url found!` + `\nPlease set the DISCORD_BOT_OAUTH2_URL in the .env file, ` + `or directly in the DyNTS_global_settings.env_settings.discord.oauth2Url` + `\nYou also will need to set the DISCORD_GUILD_ID, DISCORD_BOT_TOKEN and DISCORD_BOT_CLIENT_ID.` ); } DyNTS_DiBo_global_settings.channelSettings.defaultChannels.forEach( channelName => { if (!DyNTS_DiBo_global_settings.channelSettings.allowedChannels.includes(channelName)) { DyNTS_DiBo_global_settings.channelSettings.allowedChannels.push(channelName); } } ); if (!DyNTS_DiBo_global_settings.channelSettings.allowedChannels.includes( DyNTS_DiBo_global_settings.channelSettings.reportChannelName )) { DyNTS_DiBo_global_settings.channelSettings.allowedChannels.push( DyNTS_DiBo_global_settings.channelSettings.reportChannelName ); } this.botIO_CS = this.getBotIOControlService(); this.routines_CS = this.getRoutinesControlService(); this.commands_CS = this.getCommandsControlService(); if (!this.botIO_CS) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('setup', new Error('Bot IO Control Service not found'), issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DiBo-MCS-S01`, }) } if (!this.routines_CS) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('setup', new Error('Routines Control Service not found'), issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DiBo-MCS-S02`, }) } if (!this.commands_CS) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('setup', new Error('Commands Control Service not found'), issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DiBo-MCS-S03`, }) } await this.botIO_CS.setup(issuer); await this.routines_CS.setup(issuer); await this.commands_CS.setup(issuer); return new Promise((resolve, reject) => { this.client.on('error', (error) => { if (DyNTS_DiBo_global_settings.debugLevel >= 1) { /* DyFM_Log.error('Discord client error:', error); */ DyFM_Error.logSimple('Discord client error:', error); } DyNTS_GlobalService.globalErrorHandler(error); reject(error); }); if (this.debugLog) { DyFM_Log.success('Error event registered\n\n', new Error().stack); } else { DyFM_Log.success('Error event registered'); } this.client.on('ready', async () => { if (DyNTS_DiBo_global_settings.debugLevel >= 1) { DyFM_Log.success(`Logged in as ${this.client.user?.tag}`) } // szerver azonosító this._discordServer = this.client.guilds.cache.get( DyNTS_global_settings.env_settings.discord.guildId ) if (!this._discordServer) { throw new Error(`No discord server found`); } await this.start(issuer); //await Operations.reportIn(this._discordServer, this.client) DyFM_Log.success('Discord Bot setup complete') resolve(); }); if (this.debugLog) { DyFM_Log.success('Ready event registered\n\n', new Error().stack); } else { DyFM_Log.success('Ready event registered'); } this.client.on('messageCreate', (message: Message) => { try { // Discord-specific implementation doesn't use generic wrapper pattern // It has its own handleNewMessage signature this.botIO_CS.handleNewMessage(message, message?.author?.id ?? 'unknown') } catch (error) { DyFM_Error.logSimple('Error handling new message:', error); } }); if (this.debugLog) { DyFM_Log.success('Message create event registered\n\n', new Error().stack); } else { DyFM_Log.success('Message create event registered'); } process.on('exit', () => { try { DyFM_Log.warn('Bot is shutting down...') DyNTS_DiBo_Operations_Util.sendMessageToChannelByName( this._discordServer, DyNTS_DiBo_global_settings.channelSettings.reportChannelName, `Bot is shutting down...` ) } catch (error) { DyFM_Error.logSimple('Error sending message to report channel:', error); } }); if (this.debugLog) { DyFM_Log.success('Exit event registered\n\n', new Error().stack); } else { DyFM_Log.success('Exit event registered'); } this.client.login(DyNTS_global_settings.env_settings.discord.token).catch((error) => { reject(error); }); }); } catch (error) { if (this.discordServer) { try { DyNTS_DiBo_Operations_Util.sendMessageToChannelByName( this.discordServer, DyNTS_DiBo_global_settings.channelSettings.reportChannelName, `#Error during bot startup:\n` + ((error as DyFM_Error)._message ?? error.message ?? error) ) } catch {} } DyFM_Error.logSimple('Error during bot startup:', error); throw new DyFM_Error({ ...this.getDefaultErrorSettings('start', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DiBo-MCS-SE0`, }) } } async start(issuer: string): Promise { try { if (!this.discordServer) { throw new DyFM_Error({ ...this.getDefaultErrorSettings( 'start', new Error( 'Discord server not yet initialized, ' + 'should only be called after DiscordBot_ControlService.start' ), issuer, ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DiBo-MCS-ST01`, }) } await this.routines_CS.start(issuer); await this.botIO_CS.start(issuer); await this.commands_CS.start(issuer); DyFM_Log.success('Discord Bot started') } catch (error) { DyFM_Error.logSimple('Error during bot start:', error); throw new DyFM_Error({ ...this.getDefaultErrorSettings('start', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DiBo-MCS-ST0`, }) } } async getRolePingByName(roleName: string): Promise { const role: Role = this.getRoleByName(roleName); return role ? `<@&${role.id}>` : ''; } async getRolePingsByName(roleNames: string[]): Promise { const pings: string[] = await Promise.all( roleNames.map(roleName => this.getRolePingByName(roleName)) ); return pings.join(' '); } getRoleByName(roleName: string): Role { this._rolesByNames[roleName] ??= this.discordServer.roles.cache.find(r => r.name === roleName); return this._rolesByNames[roleName]; } getChannelByName(channelName: string): TextChannel { this._channelsByNames[channelName] ??= DyNTS_DiBo_Operations_Util.findTextChannelByName( this.discordServer.channels, channelName ); return this._channelsByNames[channelName]; } async sendMessageToChannelByName( channelName: string, message: string, issuer: string, ): Promise { try { const channel = await this.getChannelByName(channelName); if (!channel) { throw new DyFM_Error({ ...this.getDefaultErrorSettings( 'sendMessageToChannelByName', new Error('Channel not found'), issuer ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DiBo-MCS-SMCN1`, }) } if (!channel.isTextBased()) { throw new DyFM_Error({ ...this.getDefaultErrorSettings( 'sendMessageToChannelByName', new Error('Channel is not a text channel'), issuer ), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DiBo-MCS-SMCN2`, }) } if (message.length > 2_000) { // 2000 characters per message let firstMessage: Message; while (message.length) { if (!firstMessage) { firstMessage = await channel.send(message.substring(0, 2_000)); } else { await channel.send(message.substring(0, 2_000)); } message = message.substring(2_000); } } else { return await channel.send(message); } } catch (error) { DyFM_Error.logSimple('Error sending message to channel by name:', error); throw new DyFM_Error({ ...this.getDefaultErrorSettings('sendMessageToChannelByName', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DiBo-MCS-SMCN0`, }) } } async clearChannel( channel: TextChannel, issuer: string, ): Promise { try { DyFM_Log.testInfo('Clearing channel... ', channel.name); let messages: Message[] = (await DyNTS_DiBo_Operations_Util.fetchAllMessagesWithPaging(channel)).map( (message) => message, ); while (messages.length) { DyFM_Log.testInfo('Deleting next 1000 messages... ', messages.length); await Promise.all(messages.map(message => { return new Promise(async (resolve, reject) => { await DyNTS_DiBo_Operations_Util.deleteMessage(message).catch((error) => { DyFM_Error.logSimple('Failed to delete message', error); }); messages = messages.filter(m => m.id !== message.id); DyFM_Log.success('Deleted message', message.content); resolve(true); }); })); if (messages.length === 0) { messages = (await DyNTS_DiBo_Operations_Util.fetchAllMessagesWithPaging(channel)).map( (message) => message, ); } } DyFM_Log.success('Deleted messages', messages.length); channel.send('[SYSTEM|OPERATION|RESULT] Csatorna üzeneteinek törlése sikeresen befejeződött.'); } catch (error) { DyFM_Error.logSimple('Error clearing channel:', error); throw new DyFM_Error({ ...this.getDefaultErrorSettings('clearChannel', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-DiBo-MCS-CC0`, }) } } } /** * A leggyakoribb client.on(...) események a következők (TypeScript + discord.js 14+): * * 🔁 Alap események * client.on('ready', () => {}) // Bot elindult * client.on('messageCreate', (msg) => {}) // Üzenet érkezett * client.on('interactionCreate', (i) => {}) // Slash command vagy gomb stb. * client.on('guildCreate', (guild) => {}) // Új szerverre került * client.on('guildDelete', (guild) => {}) // Szerverről eltávolították * client.on('error', (err) => {}) // Hiba történt * * 👥 Felhasználókkal kapcsolatos események * client.on('guildMemberAdd', (member) => {}) // Új user csatlakozott * client.on('guildMemberRemove', (member) => {}) // User kilépett * client.on('presenceUpdate', (oldP, newP) => {})// Status változott (pl. online → idle) * ✏️ Üzenet események * client.on('messageUpdate', (oldMsg, newMsg) => {}) // Üzenet módosult * client.on('messageDelete', (msg) => {}) // Üzenetet töröltek * * 🔧 Moderáció/jogosultság/stb. * client.on('roleCreate', (role) => {}) * client.on('roleDelete', (role) => {}) * client.on('channelCreate', (ch) => {}) * client.on('channelDelete', (ch) => {}) */