diff --git a/src/commands/utilities/ai.ts b/src/commands/utilities/ai.ts index 124d49d..3372689 100644 --- a/src/commands/utilities/ai.ts +++ b/src/commands/utilities/ai.ts @@ -421,7 +421,7 @@ async function incrementAndCheckDailyLimit(userId: string, limit = 20): Promise< return res.rows[0].count <= effectiveLimit; } catch (err) { await client.query('ROLLBACK'); - console.error('Error in incrementAndCheckDailyLimit:', err); + logger.error('Error in incrementAndCheckDailyLimit:', err); throw err; } finally { client.release(); @@ -1232,14 +1232,14 @@ async function sendAIResponse( } } } catch (error) { - console.error('Error processing tool results:', error); + logger.error('Error processing tool results:', error); try { await interaction.followUp({ content: 'An error occurred while processing the tool results.', flags: MessageFlags.SuppressNotifications, }); } catch (followUpError) { - console.error('Failed to send error message:', followUpError); + logger.error('Failed to send error message:', followUpError); } } } @@ -1378,7 +1378,7 @@ const aiCommand: AICommand = { await processAIRequest(client, interaction); } catch (error) { - console.error('Error in AI command:', error); + logger.error('Error in AI command:', error); const errorMessage = `❌ An error occurred: ${error instanceof Error ? error.message : 'Unknown error'}`; try { @@ -1391,7 +1391,7 @@ const aiCommand: AICommand = { }); } } catch (replyError) { - console.error('Failed to send error message:', replyError); + logger.error('Failed to send error message:', replyError); } const userId = getInvokerId(interaction); diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 2f4a90a..6ec24e5 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -104,6 +104,7 @@ import { extractToolCalls as extractSlashToolCalls } from '@/utils/commandExecut import _fetch from '@/utils/dynamicFetch'; import { executeMessageToolCall, type MessageToolCall } from '@/utils/messageToolExecutor'; import type { ConversationMessage, AIResponse } from '@/commands/utilities/ai'; +import pool from '@/utils/pgClient'; type ApiConfiguration = ReturnType; import { createMemoryManager } from '@/utils/memoryManager'; @@ -401,6 +402,24 @@ export default class MessageCreateEvent { serverLimit = 150; } + const voteBonus = await pool.query( + `SELECT COUNT(DISTINCT user_id) as voter_count + FROM votes + WHERE vote_timestamp > NOW() - INTERVAL '24 hours' + AND user_id IN ( + SELECT user_id FROM votes WHERE server_id IS NULL + )`, + ); + + const voterCount = parseInt(voteBonus.rows[0]?.voter_count || '0'); + if (voterCount > 0) { + const bonus = Math.min(voterCount * 20, 100); + serverLimit += bonus; + logger.debug( + `Server ${message.guildId} vote bonus: +${bonus} (${voterCount} voters)`, + ); + } + const serverAllowed = await incrementAndCheckServerDailyLimit( message.guildId!, serverLimit, diff --git a/src/events/ready.ts b/src/events/ready.ts index 7263dcf..b880e14 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -13,11 +13,15 @@ export default class ReadyEvent { private async readyEvent(client: BotClient) { try { logger.info(`Logged in as ${client.user?.username}`); + await client.application?.commands.fetch({ withLocalizations: true }); + await loadActiveReminders(client); const { sendDeploymentNotification } = await import('../utils/sendDeploymentNotification.js'); await sendDeploymentNotification(this.startTime); + + logger.info('Bot fully initialized and ready'); } catch (error) { logger.error('Error during ready event:', error); } diff --git a/src/handlers/initialzeCommands.ts b/src/handlers/initialzeCommands.ts index 1c09950..a9a7d70 100644 --- a/src/handlers/initialzeCommands.ts +++ b/src/handlers/initialzeCommands.ts @@ -33,7 +33,7 @@ export default async (c: BotClient) => { await import(commandUrl) ).default) as SlashCommandProps | RemindCommandProps; if (!command.data) { - console.log('No command data in file', `${cat}/${file}.. Skipping`); + logger.warn('No command data in file', `${cat}/${file}.. Skipping`); continue; } command.category = cat; diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts index fece629..8121791 100644 --- a/src/middlewares/auth.ts +++ b/src/middlewares/auth.ts @@ -41,21 +41,3 @@ export const authenticateToken = (req: Request, res: Response, next: NextFunctio return res.status(500).json({ error: 'Token verification failed' }); } }; - -export const optionalAuth = (req: Request, res: Response, next: NextFunction) => { - const authHeader = req.headers['authorization']; - const token = authHeader && authHeader.split(' ')[1]; - - if (!token) { - return next(); - } - - try { - const decoded = jwt.verify(token, JWT_SECRET) as unknown as JwtPayload; - req.user = decoded; - } catch (error) { - logger.debug('Optional auth token verification failed:', error); - } - - next(); -}; diff --git a/src/middlewares/verifyApiKey.ts b/src/middlewares/verifyApiKey.ts index 16f4761..852b3b2 100644 --- a/src/middlewares/verifyApiKey.ts +++ b/src/middlewares/verifyApiKey.ts @@ -1,6 +1,5 @@ import * as config from '@/config'; import { RequestHandler } from 'express'; -import { createHmac } from 'crypto'; export const authenticateApiKey: RequestHandler = (req, res, next) => { const apiKey = req.headers['x-api-key']; @@ -29,32 +28,4 @@ export const authenticateApiKey: RequestHandler = (req, res, next) => { next(); }; -export const authenticateTopGG: RequestHandler = (req, res, next) => { - try { - const authHeader = req.headers.authorization; - const signature = req.headers['x-signature-sha256']; - - if (!authHeader || !signature || typeof signature !== 'string') { - return res.status(401).json({ error: 'Missing authentication headers' }); - } - - const hmac = createHmac('sha256', process.env.TOPGG_WEBHOOK_SECRET || ''); - const digest = hmac.update(JSON.stringify(req.body)).digest('hex'); - - if (signature !== digest) { - return res.status(401).json({ error: 'Invalid signature' }); - } - - const [scheme, token] = authHeader.split(' '); - if (scheme !== 'Bearer' || token !== process.env.TOPGG_WEBHOOK_AUTH) { - return res.status(401).json({ error: 'Invalid authorization header' }); - } - - next(); - } catch (error) { - console.error('Top.gg webhook auth error:', error); - return res.status(500).json({ error: 'Authentication error' }); - } -}; - export default authenticateApiKey; diff --git a/src/services/Client.ts b/src/services/Client.ts index 66040c0..7c249f8 100644 --- a/src/services/Client.ts +++ b/src/services/Client.ts @@ -94,7 +94,7 @@ export default class BotClient extends Client { } private async setupEvents() { - console.log('Initializing events...'); + logger.info('Initializing events...'); const eventsDir = path.join(srcDir, 'events'); for (const event of readdirSync(path.join(eventsDir))) { const filepath = path.join(eventsDir, event); @@ -165,10 +165,10 @@ export default class BotClient extends Client { const shutdown = async (signal?: NodeJS.Signals) => { try { - console.log(`Received ${signal ?? 'shutdown'}: closing services and database pool...`); + logger.info(`Received ${signal ?? 'shutdown'}: closing services and database pool...`); await this.socialMediaManager?.cleanup(); await pool.end(); - console.log('Database pool closed. Exiting.'); + logger.info('Database pool closed. Exiting.'); } catch (e) { console.error('Error during graceful shutdown:', e); } finally { diff --git a/src/services/social/fetchers/index.ts b/src/services/social/fetchers/index.ts deleted file mode 100644 index 92e87a6..0000000 --- a/src/services/social/fetchers/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { BlueskyFetcher, FediverseFetcher, UnifiedFetcher } from './UnifiedFetcher'; -export type { SocialMediaFetcher } from '../../../types/social'; diff --git a/src/utils/encrypt.ts b/src/utils/encrypt.ts index 20e9ff1..71fc7ba 100644 --- a/src/utils/encrypt.ts +++ b/src/utils/encrypt.ts @@ -131,15 +131,6 @@ function decrypt(encrypted: string): string { } } -function canDecrypt(encrypted: string): boolean { - try { - decrypt(encrypted); - return true; - } catch { - return false; - } -} - function isValidEncryptedFormat(encrypted: string): boolean { if (!encrypted || typeof encrypted !== 'string') { return false; @@ -162,4 +153,4 @@ function isValidEncryptedFormat(encrypted: string): boolean { } } -export { encrypt, decrypt, canDecrypt, isValidEncryptedFormat, EncryptionError }; +export { encrypt, decrypt, isValidEncryptedFormat, EncryptionError }; diff --git a/src/utils/encryption.ts b/src/utils/encryption.ts deleted file mode 100644 index afcf868..0000000 --- a/src/utils/encryption.ts +++ /dev/null @@ -1,82 +0,0 @@ -import crypto from 'crypto'; -import { API_KEY_ENCRYPTION_SECRET } from '../config'; - -const ENCRYPTION_KEY = API_KEY_ENCRYPTION_SECRET; -const ALGORITHM = 'aes-256-gcm'; - -const getEncryptionKey = (): Buffer => { - if (ENCRYPTION_KEY.length !== 32) { - throw new Error('ENCRYPTION_KEY must be exactly 32 characters long'); - } - return Buffer.from(ENCRYPTION_KEY, 'utf8'); -}; - -/** - * Encrypts a string using AES-256-GCM - * @param text The text to encrypt - * @returns Base64 encoded encrypted data with IV and auth tag - */ -export const encryptApiKey = (text: string): string => { - try { - const key = getEncryptionKey(); - const iv = crypto.randomBytes(16); - - const cipher = crypto.createCipheriv(ALGORITHM, key, iv); - cipher.setAAD(Buffer.from('aethel-api-key', 'utf8')); - - let encrypted = cipher.update(text, 'utf8', 'hex'); - encrypted += cipher.final('hex'); - - const authTag = cipher.getAuthTag(); - - const combined = Buffer.concat([iv, authTag, Buffer.from(encrypted, 'hex')]); - - return combined.toString('base64'); - } catch { - throw new Error('Failed to encrypt API key'); - } -}; - -/** - * Decrypts a string that was encrypted with encryptApiKey - * @param encryptedData Base64 encoded encrypted data - * @returns The decrypted text - */ -export const decryptApiKey = (encryptedData: string): string => { - try { - const key = getEncryptionKey(); - const combined = Buffer.from(encryptedData, 'base64'); - - const extractedIv = combined.subarray(0, 16); - const authTag = combined.subarray(16, 32); - const encrypted = combined.subarray(32); - - const decipher = crypto.createDecipheriv(ALGORITHM, key, extractedIv); - decipher.setAAD(Buffer.from('aethel-api-key', 'utf8')); - decipher.setAuthTag(authTag); - - let decrypted = decipher.update(encrypted, undefined, 'utf8'); - decrypted += decipher.final('utf8'); - - return decrypted; - } catch { - throw new Error('Failed to decrypt API key'); - } -}; - -/** - * Generates a secure random encryption key - * @returns A 32-character random string suitable for use as ENCRYPTION_KEY - */ -export const generateEncryptionKey = (): string => { - return crypto.randomBytes(32).toString('base64').substring(0, 32); -}; - -/** - * Validates that an encryption key is properly formatted - * @param key The key to validate - * @returns True if the key is valid - */ -export const validateEncryptionKey = (key: string): boolean => { - return typeof key === 'string' && key.length === 32; -}; diff --git a/src/utils/getGitCommitHash.ts b/src/utils/getGitCommitHash.ts index 8bc3b3f..010d781 100644 --- a/src/utils/getGitCommitHash.ts +++ b/src/utils/getGitCommitHash.ts @@ -40,7 +40,7 @@ function getGitCommitHash(): string { } initializeGitCommitHash().catch((error) => { - console.warn('Failed to initialize git commit hash:', error.message); + logger.warn('Failed to initialize git commit hash:', error.message); }); export default getGitCommitHash; diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 0230842..a412a06 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -5,15 +5,6 @@ export function random(array: T[]): T | undefined { return array[Math.floor(Math.random() * array.length)]; } -export function iso2ToFlagEmoji(iso2: string): string { - if (!iso2 || iso2.length !== 2) return ''; - const upper = iso2.toUpperCase(); - if (!/^[A-Z]{2}$/.test(upper)) return ''; - const codePoints = upper.split('').map((char) => 0x1f1e6 + char.charCodeAt(0) - 65); - if (codePoints.some((cp) => cp < 0x1f1e6 || cp > 0x1f1ff)) return ''; - return String.fromCodePoint(...codePoints); -} - export function iso2ToDiscordFlag(iso2: string): string { if (!iso2 || iso2.length !== 2) return ''; return `:flag_${iso2.toLowerCase()}:`; diff --git a/src/utils/topgg.ts b/src/utils/topgg.ts index 01fac0c..590f42f 100644 --- a/src/utils/topgg.ts +++ b/src/utils/topgg.ts @@ -1,20 +1,14 @@ const VOTE_COOLDOWN_HOURS = 12; export async function checkVoteStatus( - userId: string, + _userId: string, ): Promise<{ hasVoted: boolean; nextVote: Date; voteCount: number }> { const now = new Date(); const nextVote = new Date(now.getTime() + VOTE_COOLDOWN_HOURS * 60 * 60 * 1000); - console.log(`Vote check for user ${userId}. Next vote available at ${nextVote.toISOString()}`); - return { hasVoted: true, nextVote, voteCount: 1, }; } - -export function getVoteLink(): string { - return `https://top.gg/bot/${process.env.CLIENT_ID}/vote`; -} diff --git a/src/utils/userStrikes.ts b/src/utils/userStrikes.ts index d3ebe66..ce7dd62 100644 --- a/src/utils/userStrikes.ts +++ b/src/utils/userStrikes.ts @@ -16,7 +16,7 @@ export class StrikeError extends Error { } } -export async function getUserStrikeInfo(userId: string): Promise { +async function _getUserStrikeInfo(userId: string): Promise { if (!userId || typeof userId !== 'string') { throw new StrikeError('Invalid user ID provided'); } @@ -152,22 +152,3 @@ export async function resetOldStrikes(): Promise { throw new StrikeError('Failed to reset old strikes'); } } - -export async function clearUserStrikes(userId: string): Promise { - if (!userId || typeof userId !== 'string') { - throw new StrikeError('Invalid user ID provided'); - } - - try { - const res = await pgClient.query( - 'UPDATE user_strikes SET strike_count = 0, banned_until = NULL WHERE user_id = $1', - [userId], - ); - - logger.info('Cleared user strikes', { userId }); - return (res.rowCount ?? 0) > 0; - } catch (error) { - logger.error('Failed to clear user strikes', { userId, error }); - throw new StrikeError('Failed to clear strikes', userId); - } -} diff --git a/src/utils/voteManager.ts b/src/utils/voteManager.ts index c50a10f..f31d6f4 100644 --- a/src/utils/voteManager.ts +++ b/src/utils/voteManager.ts @@ -1,6 +1,7 @@ import pool from './pgClient'; import { Client, GatewayIntentBits } from 'discord.js'; import { checkVoteStatus } from './topgg'; +import logger from './logger'; const VOTE_CREDITS = 10; const VOTE_COOLDOWN_HOURS = 12; @@ -16,7 +17,7 @@ export interface CreditsInfo { lastReset: Date; } -export async function hasVotedToday( +async function _hasVotedToday( userId: string, serverId?: string, ): Promise<{ hasVoted: boolean; nextVote: Date }> { @@ -76,7 +77,6 @@ async function recordVoteInDatabase(userId: string, serverId?: string): Promise< [userId, serverId || null, VOTE_CREDITS], ); - await client.query('COMMIT'); } catch (error) { await client.query('ROLLBACK'); @@ -131,7 +131,6 @@ export async function recordVote( [userId, serverId || null, VOTE_CREDITS], ); - const clientBot = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers], }); @@ -149,24 +148,23 @@ export async function recordVote( const member = await fullGuild.members.fetch(userId).catch(() => null); if (member) { - console.log( + logger.debug( `User ${userId} is member of server ${guild.id} - vote benefits apply`, ); } } catch (error) { - console.error(`Error processing guild ${guild.id}:`, error); + logger.error(`Error processing guild ${guild.id}:`, error); } }), ); } } catch (error) { - console.error('Error in vote processing:', error); + logger.error('Error in vote processing:', error); } finally { - clientBot.destroy().catch(console.error); + clientBot.destroy().catch((err) => logger.error('Error destroying bot client:', err)); } - - console.log(`User ${userId} voted - AI system will give +10 daily limit`); + logger.info(`User ${userId} voted - AI system will give +10 daily limit`); try { const clientBot = new Client({ @@ -192,12 +190,12 @@ export async function recordVote( `\n` + `Thank you for your support! ❤️`, ) - .catch(console.error); + .catch((err) => logger.error('Failed to send vote DM:', err)); } - clientBot.destroy().catch(console.error); + clientBot.destroy().catch((err) => logger.error('Error destroying bot client:', err)); } catch (error) { - console.error('Failed to send vote thank you DM:', error); + logger.error('Failed to send vote thank you DM:', error); } await client.query('COMMIT'); @@ -209,81 +207,9 @@ export async function recordVote( }; } catch (error) { await client.query('ROLLBACK'); - console.error('Error recording vote:', error); + logger.error('Error recording vote:', error); throw new Error('Failed to record your vote. Please try again later.'); } finally { client.release(); } } - -export async function getRemainingCredits(userId: string, serverId?: string): Promise { - try { - const query = serverId - ? 'SELECT credits_remaining as "creditsRemaining", last_reset as "lastReset" FROM message_credits WHERE user_id = $1 AND server_id = $2' - : 'SELECT credits_remaining as "creditsRemaining", last_reset as "lastReset" FROM message_credits WHERE user_id = $1 AND server_id IS NULL'; - - const result = await pool.query(query, [userId, serverId].filter(Boolean)); - - if (result.rows.length === 0) { - return { - remaining: 0, - lastReset: new Date(), - }; - } - - return { - remaining: result.rows[0].creditsRemaining, - lastReset: result.rows[0].lastReset, - }; - } catch (error) { - console.error('Error getting remaining credits:', error); - throw new Error('Failed to get remaining credits'); - } -} - -export async function canUseAIFeature( - userId: string, - _serverId?: string, -): Promise<{ canUse: boolean; remainingCredits: number }> { - const client = await pool.connect(); - try { - await client.query('BEGIN'); - - const result = await client.query( - `SELECT count FROM ai_usage - WHERE user_id = $1 AND usage_date = CURRENT_DATE - FOR UPDATE`, - [userId], - ); - - if (result.rows.length > 0 && result.rows[0].count > 0) { - const updateResult = await client.query( - `UPDATE ai_usage - SET count = count - 1 - WHERE user_id = $1 AND usage_date = CURRENT_DATE - RETURNING count`, - [userId], - ); - - await client.query('COMMIT'); - - return { - canUse: true, - remainingCredits: updateResult.rows[0]?.count || 0, - }; - } - - await client.query('COMMIT'); - - return { - canUse: false, - remainingCredits: 0, - }; - } catch (error) { - await client.query('ROLLBACK'); - console.error('Error checking AI feature usage:', error); - throw new Error('Failed to check AI feature usage'); - } finally { - client.release(); - } -}