Aethel Bot OSS repository! aethel.xyz
bot fun ai discord discord-bot aethel
at dev 6.5 kB view raw
1import * as config from '@/config'; 2import initialzeCommands from '@/handlers/initialzeCommands'; 3import { SlashCommandProps } from '@/types/command'; 4import { Client, Collection, GatewayIntentBits, Partials } from 'discord.js'; 5import { Pool } from 'pg'; 6import { initializeSocialMediaManager, SocialMediaManager } from './social/SocialMediaManager'; 7import { promises, readdirSync } from 'fs'; 8import path from 'path'; 9import { fileURLToPath } from 'url'; 10import { dirname } from 'path'; 11import logger from '@/utils/logger'; 12 13const __filename = fileURLToPath(import.meta.url); 14const __dirname = dirname(__filename); 15 16export const srcDir = path.join(__dirname, '..'); 17 18export default class BotClient extends Client { 19 private static instance: BotClient | null = null; 20 public commands = new Collection<string, SlashCommandProps>(); 21 // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 public t = new Collection<string, any>(); 23 public socialMediaManager?: SocialMediaManager; 24 25 constructor() { 26 super({ 27 intents: [ 28 GatewayIntentBits.Guilds, 29 GatewayIntentBits.GuildMessages, 30 GatewayIntentBits.MessageContent, 31 GatewayIntentBits.DirectMessages, 32 ], 33 partials: [Partials.Channel, Partials.Message], 34 presence: { 35 status: 'online', 36 activities: [ 37 { 38 name: '/weather | /ai', 39 }, 40 ], 41 }, 42 }); 43 BotClient.instance = this; 44 } 45 46 public static getInstance(): BotClient | null { 47 return BotClient.instance; 48 } 49 50 public async init() { 51 await this.setupLocalization(); 52 await initialzeCommands(this); 53 await this.setupEvents(); 54 await this.setupDatabase(); 55 this.login(config.TOKEN); 56 } 57 58 private async setupEvents() { 59 console.log('Initializing events...'); 60 const eventsDir = path.join(srcDir, 'events'); 61 for (const event of readdirSync(path.join(eventsDir))) { 62 const filepath = path.join(eventsDir, event); 63 const fileUrl = `file://${filepath.replace(/\\/g, '/')}`; 64 const EventModule = await (await import(fileUrl)).default; 65 66 if (typeof EventModule === 'function') { 67 new EventModule(this); 68 } else if (EventModule && typeof EventModule.execute === 'function') { 69 this.on(EventModule.name, (...args) => EventModule.execute(...args, this)); 70 } 71 } 72 } 73 74 private async setupLocalization() { 75 logger.info('Loading localization files...'); 76 const localesDir = path.join(srcDir, '..', 'locales'); 77 78 try { 79 const localeFiles = (await promises.readdir(localesDir)).filter((f) => f.endsWith('.json')); 80 81 const localePromises = localeFiles.map(async (locale) => { 82 const localeFile = path.join(localesDir, locale); 83 try { 84 const data = await promises.readFile(localeFile, { encoding: 'utf8' }); 85 const localeKey = locale.split('.')[0]; 86 this.t.set(localeKey, JSON.parse(data)); 87 logger.debug(`Loaded locale: ${localeKey}`); 88 } catch (error) { 89 logger.error(`Failed to load locale file ${locale}:`, error); 90 } 91 }); 92 93 await Promise.all(localePromises); 94 logger.info(`Loaded ${this.t.size} locale(s)`); 95 } catch (error) { 96 logger.error('Failed to read locales directory:', error); 97 throw new Error('Failed to initialize localization'); 98 } 99 } 100 private async setupDatabase() { 101 try { 102 const sslMode = (process.env.PGSSLMODE || process.env.DATABASE_SSL || '').toLowerCase(); 103 let ssl: false | { rejectUnauthorized?: boolean; ca?: string } = false; 104 const rootCertPath = process.env.PGSSLROOTCERT || process.env.DATABASE_SSL_CA; 105 106 if (sslMode === 'require') { 107 ssl = { rejectUnauthorized: true }; 108 } 109 110 if (rootCertPath) { 111 try { 112 const ca = await promises.readFile(rootCertPath, 'utf8'); 113 ssl = { ca, rejectUnauthorized: true }; 114 } catch (e) { 115 console.warn('Failed to read CA certificate: unable to access the specified path.', e); 116 } 117 } 118 119 const pool = new Pool({ 120 connectionString: process.env.DATABASE_URL, 121 ssl, 122 }); 123 124 pool.on('error', (err) => { 125 console.error('Unexpected error on idle PostgreSQL client:', err); 126 }); 127 128 const shutdown = async (signal?: NodeJS.Signals) => { 129 try { 130 console.log(`Received ${signal ?? 'shutdown'}: closing services and database pool...`); 131 await this.socialMediaManager?.cleanup(); 132 await pool.end(); 133 console.log('Database pool closed. Exiting.'); 134 } catch (e) { 135 console.error('Error during graceful shutdown:', e); 136 } finally { 137 process.exit(0); 138 } 139 }; 140 141 process.on('SIGINT', () => shutdown('SIGINT')); 142 process.on('SIGTERM', () => shutdown('SIGTERM')); 143 144 this.socialMediaManager = initializeSocialMediaManager(this, pool); 145 await this.socialMediaManager.initialize(); 146 } catch (error) { 147 console.error('Failed to initialize database and services:', error); 148 throw error; 149 } 150 } 151 152 public async getLocaleText(key: string, locale: string, replaces = {}): Promise<string> { 153 const fallbackLocale = 'en-US'; 154 155 if (!locale) { 156 locale = fallbackLocale; 157 } 158 159 let langMap = this.t.get(locale); 160 if (!langMap) { 161 const langOnly = locale.split('-')[0]; 162 langMap = this.t.get(langOnly); 163 if (!langMap) { 164 const fuzzyLocale = Array.from(this.t.keys()).find((k) => k.startsWith(langOnly + '-')); 165 if (fuzzyLocale) { 166 langMap = this.t.get(fuzzyLocale); 167 } else { 168 langMap = this.t.get(fallbackLocale); 169 } 170 } 171 } 172 173 // eslint-disable-next-line @typescript-eslint/no-explicit-any 174 const getValueFromMap = (map: any, keyPath: string): any => { 175 return keyPath 176 .split('.') 177 .reduce((prev, cur) => (prev && prev[cur] !== undefined ? prev[cur] : undefined), map); 178 }; 179 180 let text = getValueFromMap(langMap, key); 181 182 if (text === undefined && locale !== fallbackLocale) { 183 langMap = this.t.get(fallbackLocale); 184 text = getValueFromMap(langMap, key); 185 } 186 187 if (text === undefined) { 188 text = `Missing translation for key: ${key}`; 189 } 190 191 for (const [varName, value] of Object.entries(replaces)) { 192 const regex = new RegExp(`{${varName}}`, 'g'); 193 text = text.replace(regex, value); 194 } 195 196 return text; 197 } 198}