Aethel Bot OSS repository! aethel.xyz
bot fun ai discord discord-bot aethel

refactor: Code cleanups, bug fixes and more

Changed files
+47 -272
src
+5 -5
src/commands/utilities/ai.ts
··· 421 421 return res.rows[0].count <= effectiveLimit; 422 422 } catch (err) { 423 423 await client.query('ROLLBACK'); 424 - console.error('Error in incrementAndCheckDailyLimit:', err); 424 + logger.error('Error in incrementAndCheckDailyLimit:', err); 425 425 throw err; 426 426 } finally { 427 427 client.release(); ··· 1232 1232 } 1233 1233 } 1234 1234 } catch (error) { 1235 - console.error('Error processing tool results:', error); 1235 + logger.error('Error processing tool results:', error); 1236 1236 try { 1237 1237 await interaction.followUp({ 1238 1238 content: 'An error occurred while processing the tool results.', 1239 1239 flags: MessageFlags.SuppressNotifications, 1240 1240 }); 1241 1241 } catch (followUpError) { 1242 - console.error('Failed to send error message:', followUpError); 1242 + logger.error('Failed to send error message:', followUpError); 1243 1243 } 1244 1244 } 1245 1245 } ··· 1378 1378 1379 1379 await processAIRequest(client, interaction); 1380 1380 } catch (error) { 1381 - console.error('Error in AI command:', error); 1381 + logger.error('Error in AI command:', error); 1382 1382 const errorMessage = `❌ An error occurred: ${error instanceof Error ? error.message : 'Unknown error'}`; 1383 1383 1384 1384 try { ··· 1391 1391 }); 1392 1392 } 1393 1393 } catch (replyError) { 1394 - console.error('Failed to send error message:', replyError); 1394 + logger.error('Failed to send error message:', replyError); 1395 1395 } 1396 1396 1397 1397 const userId = getInvokerId(interaction);
+19
src/events/messageCreate.ts
··· 104 104 import _fetch from '@/utils/dynamicFetch'; 105 105 import { executeMessageToolCall, type MessageToolCall } from '@/utils/messageToolExecutor'; 106 106 import type { ConversationMessage, AIResponse } from '@/commands/utilities/ai'; 107 + import pool from '@/utils/pgClient'; 107 108 108 109 type ApiConfiguration = ReturnType<typeof getApiConfiguration>; 109 110 import { createMemoryManager } from '@/utils/memoryManager'; ··· 399 400 serverLimit = 500; 400 401 } else if (memberCount >= 100) { 401 402 serverLimit = 150; 403 + } 404 + 405 + const voteBonus = await pool.query( 406 + `SELECT COUNT(DISTINCT user_id) as voter_count 407 + FROM votes 408 + WHERE vote_timestamp > NOW() - INTERVAL '24 hours' 409 + AND user_id IN ( 410 + SELECT user_id FROM votes WHERE server_id IS NULL 411 + )`, 412 + ); 413 + 414 + const voterCount = parseInt(voteBonus.rows[0]?.voter_count || '0'); 415 + if (voterCount > 0) { 416 + const bonus = Math.min(voterCount * 20, 100); 417 + serverLimit += bonus; 418 + logger.debug( 419 + `Server ${message.guildId} vote bonus: +${bonus} (${voterCount} voters)`, 420 + ); 402 421 } 403 422 404 423 const serverAllowed = await incrementAndCheckServerDailyLimit(
+4
src/events/ready.ts
··· 13 13 private async readyEvent(client: BotClient) { 14 14 try { 15 15 logger.info(`Logged in as ${client.user?.username}`); 16 + 16 17 await client.application?.commands.fetch({ withLocalizations: true }); 18 + 17 19 await loadActiveReminders(client); 18 20 19 21 const { sendDeploymentNotification } = await import('../utils/sendDeploymentNotification.js'); 20 22 await sendDeploymentNotification(this.startTime); 23 + 24 + logger.info('Bot fully initialized and ready'); 21 25 } catch (error) { 22 26 logger.error('Error during ready event:', error); 23 27 }
+1 -1
src/handlers/initialzeCommands.ts
··· 33 33 await import(commandUrl) 34 34 ).default) as SlashCommandProps | RemindCommandProps; 35 35 if (!command.data) { 36 - console.log('No command data in file', `${cat}/${file}.. Skipping`); 36 + logger.warn('No command data in file', `${cat}/${file}.. Skipping`); 37 37 continue; 38 38 } 39 39 command.category = cat;
-18
src/middlewares/auth.ts
··· 41 41 return res.status(500).json({ error: 'Token verification failed' }); 42 42 } 43 43 }; 44 - 45 - export const optionalAuth = (req: Request, res: Response, next: NextFunction) => { 46 - const authHeader = req.headers['authorization']; 47 - const token = authHeader && authHeader.split(' ')[1]; 48 - 49 - if (!token) { 50 - return next(); 51 - } 52 - 53 - try { 54 - const decoded = jwt.verify(token, JWT_SECRET) as unknown as JwtPayload; 55 - req.user = decoded; 56 - } catch (error) { 57 - logger.debug('Optional auth token verification failed:', error); 58 - } 59 - 60 - next(); 61 - };
-29
src/middlewares/verifyApiKey.ts
··· 1 1 import * as config from '@/config'; 2 2 import { RequestHandler } from 'express'; 3 - import { createHmac } from 'crypto'; 4 3 5 4 export const authenticateApiKey: RequestHandler = (req, res, next) => { 6 5 const apiKey = req.headers['x-api-key']; ··· 27 26 } 28 27 29 28 next(); 30 - }; 31 - 32 - export const authenticateTopGG: RequestHandler = (req, res, next) => { 33 - try { 34 - const authHeader = req.headers.authorization; 35 - const signature = req.headers['x-signature-sha256']; 36 - 37 - if (!authHeader || !signature || typeof signature !== 'string') { 38 - return res.status(401).json({ error: 'Missing authentication headers' }); 39 - } 40 - 41 - const hmac = createHmac('sha256', process.env.TOPGG_WEBHOOK_SECRET || ''); 42 - const digest = hmac.update(JSON.stringify(req.body)).digest('hex'); 43 - 44 - if (signature !== digest) { 45 - return res.status(401).json({ error: 'Invalid signature' }); 46 - } 47 - 48 - const [scheme, token] = authHeader.split(' '); 49 - if (scheme !== 'Bearer' || token !== process.env.TOPGG_WEBHOOK_AUTH) { 50 - return res.status(401).json({ error: 'Invalid authorization header' }); 51 - } 52 - 53 - next(); 54 - } catch (error) { 55 - console.error('Top.gg webhook auth error:', error); 56 - return res.status(500).json({ error: 'Authentication error' }); 57 - } 58 29 }; 59 30 60 31 export default authenticateApiKey;
+3 -3
src/services/Client.ts
··· 94 94 } 95 95 96 96 private async setupEvents() { 97 - console.log('Initializing events...'); 97 + logger.info('Initializing events...'); 98 98 const eventsDir = path.join(srcDir, 'events'); 99 99 for (const event of readdirSync(path.join(eventsDir))) { 100 100 const filepath = path.join(eventsDir, event); ··· 165 165 166 166 const shutdown = async (signal?: NodeJS.Signals) => { 167 167 try { 168 - console.log(`Received ${signal ?? 'shutdown'}: closing services and database pool...`); 168 + logger.info(`Received ${signal ?? 'shutdown'}: closing services and database pool...`); 169 169 await this.socialMediaManager?.cleanup(); 170 170 await pool.end(); 171 - console.log('Database pool closed. Exiting.'); 171 + logger.info('Database pool closed. Exiting.'); 172 172 } catch (e) { 173 173 console.error('Error during graceful shutdown:', e); 174 174 } finally {
-2
src/services/social/fetchers/index.ts
··· 1 - export { BlueskyFetcher, FediverseFetcher, UnifiedFetcher } from './UnifiedFetcher'; 2 - export type { SocialMediaFetcher } from '../../../types/social';
+1 -10
src/utils/encrypt.ts
··· 131 131 } 132 132 } 133 133 134 - function canDecrypt(encrypted: string): boolean { 135 - try { 136 - decrypt(encrypted); 137 - return true; 138 - } catch { 139 - return false; 140 - } 141 - } 142 - 143 134 function isValidEncryptedFormat(encrypted: string): boolean { 144 135 if (!encrypted || typeof encrypted !== 'string') { 145 136 return false; ··· 162 153 } 163 154 } 164 155 165 - export { encrypt, decrypt, canDecrypt, isValidEncryptedFormat, EncryptionError }; 156 + export { encrypt, decrypt, isValidEncryptedFormat, EncryptionError };
-82
src/utils/encryption.ts
··· 1 - import crypto from 'crypto'; 2 - import { API_KEY_ENCRYPTION_SECRET } from '../config'; 3 - 4 - const ENCRYPTION_KEY = API_KEY_ENCRYPTION_SECRET; 5 - const ALGORITHM = 'aes-256-gcm'; 6 - 7 - const getEncryptionKey = (): Buffer => { 8 - if (ENCRYPTION_KEY.length !== 32) { 9 - throw new Error('ENCRYPTION_KEY must be exactly 32 characters long'); 10 - } 11 - return Buffer.from(ENCRYPTION_KEY, 'utf8'); 12 - }; 13 - 14 - /** 15 - * Encrypts a string using AES-256-GCM 16 - * @param text The text to encrypt 17 - * @returns Base64 encoded encrypted data with IV and auth tag 18 - */ 19 - export const encryptApiKey = (text: string): string => { 20 - try { 21 - const key = getEncryptionKey(); 22 - const iv = crypto.randomBytes(16); 23 - 24 - const cipher = crypto.createCipheriv(ALGORITHM, key, iv); 25 - cipher.setAAD(Buffer.from('aethel-api-key', 'utf8')); 26 - 27 - let encrypted = cipher.update(text, 'utf8', 'hex'); 28 - encrypted += cipher.final('hex'); 29 - 30 - const authTag = cipher.getAuthTag(); 31 - 32 - const combined = Buffer.concat([iv, authTag, Buffer.from(encrypted, 'hex')]); 33 - 34 - return combined.toString('base64'); 35 - } catch { 36 - throw new Error('Failed to encrypt API key'); 37 - } 38 - }; 39 - 40 - /** 41 - * Decrypts a string that was encrypted with encryptApiKey 42 - * @param encryptedData Base64 encoded encrypted data 43 - * @returns The decrypted text 44 - */ 45 - export const decryptApiKey = (encryptedData: string): string => { 46 - try { 47 - const key = getEncryptionKey(); 48 - const combined = Buffer.from(encryptedData, 'base64'); 49 - 50 - const extractedIv = combined.subarray(0, 16); 51 - const authTag = combined.subarray(16, 32); 52 - const encrypted = combined.subarray(32); 53 - 54 - const decipher = crypto.createDecipheriv(ALGORITHM, key, extractedIv); 55 - decipher.setAAD(Buffer.from('aethel-api-key', 'utf8')); 56 - decipher.setAuthTag(authTag); 57 - 58 - let decrypted = decipher.update(encrypted, undefined, 'utf8'); 59 - decrypted += decipher.final('utf8'); 60 - 61 - return decrypted; 62 - } catch { 63 - throw new Error('Failed to decrypt API key'); 64 - } 65 - }; 66 - 67 - /** 68 - * Generates a secure random encryption key 69 - * @returns A 32-character random string suitable for use as ENCRYPTION_KEY 70 - */ 71 - export const generateEncryptionKey = (): string => { 72 - return crypto.randomBytes(32).toString('base64').substring(0, 32); 73 - }; 74 - 75 - /** 76 - * Validates that an encryption key is properly formatted 77 - * @param key The key to validate 78 - * @returns True if the key is valid 79 - */ 80 - export const validateEncryptionKey = (key: string): boolean => { 81 - return typeof key === 'string' && key.length === 32; 82 - };
+1 -1
src/utils/getGitCommitHash.ts
··· 40 40 } 41 41 42 42 initializeGitCommitHash().catch((error) => { 43 - console.warn('Failed to initialize git commit hash:', error.message); 43 + logger.warn('Failed to initialize git commit hash:', error.message); 44 44 }); 45 45 46 46 export default getGitCommitHash;
-9
src/utils/misc.ts
··· 5 5 return array[Math.floor(Math.random() * array.length)]; 6 6 } 7 7 8 - export function iso2ToFlagEmoji(iso2: string): string { 9 - if (!iso2 || iso2.length !== 2) return ''; 10 - const upper = iso2.toUpperCase(); 11 - if (!/^[A-Z]{2}$/.test(upper)) return ''; 12 - const codePoints = upper.split('').map((char) => 0x1f1e6 + char.charCodeAt(0) - 65); 13 - if (codePoints.some((cp) => cp < 0x1f1e6 || cp > 0x1f1ff)) return ''; 14 - return String.fromCodePoint(...codePoints); 15 - } 16 - 17 8 export function iso2ToDiscordFlag(iso2: string): string { 18 9 if (!iso2 || iso2.length !== 2) return ''; 19 10 return `:flag_${iso2.toLowerCase()}:`;
+1 -7
src/utils/topgg.ts
··· 1 1 const VOTE_COOLDOWN_HOURS = 12; 2 2 3 3 export async function checkVoteStatus( 4 - userId: string, 4 + _userId: string, 5 5 ): Promise<{ hasVoted: boolean; nextVote: Date; voteCount: number }> { 6 6 const now = new Date(); 7 7 const nextVote = new Date(now.getTime() + VOTE_COOLDOWN_HOURS * 60 * 60 * 1000); 8 - 9 - console.log(`Vote check for user ${userId}. Next vote available at ${nextVote.toISOString()}`); 10 8 11 9 return { 12 10 hasVoted: true, ··· 14 12 voteCount: 1, 15 13 }; 16 14 } 17 - 18 - export function getVoteLink(): string { 19 - return `https://top.gg/bot/${process.env.CLIENT_ID}/vote`; 20 - }
+1 -20
src/utils/userStrikes.ts
··· 16 16 } 17 17 } 18 18 19 - export async function getUserStrikeInfo(userId: string): Promise<StrikeInfo | null> { 19 + async function _getUserStrikeInfo(userId: string): Promise<StrikeInfo | null> { 20 20 if (!userId || typeof userId !== 'string') { 21 21 throw new StrikeError('Invalid user ID provided'); 22 22 } ··· 152 152 throw new StrikeError('Failed to reset old strikes'); 153 153 } 154 154 } 155 - 156 - export async function clearUserStrikes(userId: string): Promise<boolean> { 157 - if (!userId || typeof userId !== 'string') { 158 - throw new StrikeError('Invalid user ID provided'); 159 - } 160 - 161 - try { 162 - const res = await pgClient.query( 163 - 'UPDATE user_strikes SET strike_count = 0, banned_until = NULL WHERE user_id = $1', 164 - [userId], 165 - ); 166 - 167 - logger.info('Cleared user strikes', { userId }); 168 - return (res.rowCount ?? 0) > 0; 169 - } catch (error) { 170 - logger.error('Failed to clear user strikes', { userId, error }); 171 - throw new StrikeError('Failed to clear strikes', userId); 172 - } 173 - }
+11 -85
src/utils/voteManager.ts
··· 1 1 import pool from './pgClient'; 2 2 import { Client, GatewayIntentBits } from 'discord.js'; 3 3 import { checkVoteStatus } from './topgg'; 4 + import logger from './logger'; 4 5 5 6 const VOTE_CREDITS = 10; 6 7 const VOTE_COOLDOWN_HOURS = 12; ··· 16 17 lastReset: Date; 17 18 } 18 19 19 - export async function hasVotedToday( 20 + async function _hasVotedToday( 20 21 userId: string, 21 22 serverId?: string, 22 23 ): Promise<{ hasVoted: boolean; nextVote: Date }> { ··· 76 77 [userId, serverId || null, VOTE_CREDITS], 77 78 ); 78 79 79 - 80 80 await client.query('COMMIT'); 81 81 } catch (error) { 82 82 await client.query('ROLLBACK'); ··· 131 131 [userId, serverId || null, VOTE_CREDITS], 132 132 ); 133 133 134 - 135 134 const clientBot = new Client({ 136 135 intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers], 137 136 }); ··· 149 148 const member = await fullGuild.members.fetch(userId).catch(() => null); 150 149 151 150 if (member) { 152 - console.log( 151 + logger.debug( 153 152 `User ${userId} is member of server ${guild.id} - vote benefits apply`, 154 153 ); 155 154 } 156 155 } catch (error) { 157 - console.error(`Error processing guild ${guild.id}:`, error); 156 + logger.error(`Error processing guild ${guild.id}:`, error); 158 157 } 159 158 }), 160 159 ); 161 160 } 162 161 } catch (error) { 163 - console.error('Error in vote processing:', error); 162 + logger.error('Error in vote processing:', error); 164 163 } finally { 165 - clientBot.destroy().catch(console.error); 164 + clientBot.destroy().catch((err) => logger.error('Error destroying bot client:', err)); 166 165 } 167 166 168 - 169 - console.log(`User ${userId} voted - AI system will give +10 daily limit`); 167 + logger.info(`User ${userId} voted - AI system will give +10 daily limit`); 170 168 171 169 try { 172 170 const clientBot = new Client({ ··· 192 190 `\n` + 193 191 `Thank you for your support! ❤️`, 194 192 ) 195 - .catch(console.error); 193 + .catch((err) => logger.error('Failed to send vote DM:', err)); 196 194 } 197 195 198 - clientBot.destroy().catch(console.error); 196 + clientBot.destroy().catch((err) => logger.error('Error destroying bot client:', err)); 199 197 } catch (error) { 200 - console.error('Failed to send vote thank you DM:', error); 198 + logger.error('Failed to send vote thank you DM:', error); 201 199 } 202 200 203 201 await client.query('COMMIT'); ··· 209 207 }; 210 208 } catch (error) { 211 209 await client.query('ROLLBACK'); 212 - console.error('Error recording vote:', error); 210 + logger.error('Error recording vote:', error); 213 211 throw new Error('Failed to record your vote. Please try again later.'); 214 212 } finally { 215 213 client.release(); 216 214 } 217 215 } 218 - 219 - export async function getRemainingCredits(userId: string, serverId?: string): Promise<CreditsInfo> { 220 - try { 221 - const query = serverId 222 - ? 'SELECT credits_remaining as "creditsRemaining", last_reset as "lastReset" FROM message_credits WHERE user_id = $1 AND server_id = $2' 223 - : 'SELECT credits_remaining as "creditsRemaining", last_reset as "lastReset" FROM message_credits WHERE user_id = $1 AND server_id IS NULL'; 224 - 225 - const result = await pool.query(query, [userId, serverId].filter(Boolean)); 226 - 227 - if (result.rows.length === 0) { 228 - return { 229 - remaining: 0, 230 - lastReset: new Date(), 231 - }; 232 - } 233 - 234 - return { 235 - remaining: result.rows[0].creditsRemaining, 236 - lastReset: result.rows[0].lastReset, 237 - }; 238 - } catch (error) { 239 - console.error('Error getting remaining credits:', error); 240 - throw new Error('Failed to get remaining credits'); 241 - } 242 - } 243 - 244 - export async function canUseAIFeature( 245 - userId: string, 246 - _serverId?: string, 247 - ): Promise<{ canUse: boolean; remainingCredits: number }> { 248 - const client = await pool.connect(); 249 - try { 250 - await client.query('BEGIN'); 251 - 252 - const result = await client.query( 253 - `SELECT count FROM ai_usage 254 - WHERE user_id = $1 AND usage_date = CURRENT_DATE 255 - FOR UPDATE`, 256 - [userId], 257 - ); 258 - 259 - if (result.rows.length > 0 && result.rows[0].count > 0) { 260 - const updateResult = await client.query( 261 - `UPDATE ai_usage 262 - SET count = count - 1 263 - WHERE user_id = $1 AND usage_date = CURRENT_DATE 264 - RETURNING count`, 265 - [userId], 266 - ); 267 - 268 - await client.query('COMMIT'); 269 - 270 - return { 271 - canUse: true, 272 - remainingCredits: updateResult.rows[0]?.count || 0, 273 - }; 274 - } 275 - 276 - await client.query('COMMIT'); 277 - 278 - return { 279 - canUse: false, 280 - remainingCredits: 0, 281 - }; 282 - } catch (error) { 283 - await client.query('ROLLBACK'); 284 - console.error('Error checking AI feature usage:', error); 285 - throw new Error('Failed to check AI feature usage'); 286 - } finally { 287 - client.release(); 288 - } 289 - }