+5
-5
src/commands/utilities/ai.ts
+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
+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
+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
+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
-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
-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
+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 {
+1
-10
src/utils/encrypt.ts
+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
-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
+1
-1
src/utils/getGitCommitHash.ts
-9
src/utils/misc.ts
-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
-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
+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
+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
-
}