Skip to content

Commit b1d5275

Browse files
committed
feat: Added rate limiting and database connection retry logic
1 parent 8f91cb2 commit b1d5275

4 files changed

Lines changed: 78 additions & 23 deletions

File tree

src/bot/index.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export class CopBot {
1717
private webhookPath = `/bot/${Config.token}`;
1818
private webhookURL = `${Config.web_hook}${this.webhookPath}`;
1919
private mode = this.isProduction ? 'webhook' : 'long-polling';
20+
private static latestContext: Context | null = null;
2021
private constructor() {
2122
this._bot = new Bot<Context>(Config.token);
2223
}
@@ -27,6 +28,19 @@ export class CopBot {
2728
}
2829
return CopBot.instance;
2930
}
31+
public static setContext(ctx: Context): void {
32+
logger.info(`Setting new context: at ${new Date().toISOString()}`);
33+
this.latestContext = ctx;
34+
}
35+
36+
public static getContext(): Context | null {
37+
if (this.latestContext) {
38+
logger.info(`Retrieved latest context: at ${new Date().toISOString()}`);
39+
} else {
40+
logger.warn('Attempted to retrieve context, but no context is set.');
41+
}
42+
return this.latestContext;
43+
}
3044
// Stop the bot
3145
async stop(): Promise<void> {
3246
if (this.healthCheckInterval) {
@@ -82,6 +96,7 @@ export class CopBot {
8296
app.listen(port, async () => {
8397
logger.info(`Webhook server running on port ${port}`);
8498
await this.setupWebhook(this.webhookURL);
99+
logger.info(`Bot started in ${this.mode} mode!`);
85100
});
86101
} catch (err: any) {
87102
console.error('Error setting up webhook:', err);
@@ -100,11 +115,26 @@ export class CopBot {
100115
new GenerateCommand(this._bot).generate();
101116
this._bot.use(
102117
limit({
103-
onLimitExceeded: (ctx) => ctx.reply('Too many requests! Please slow down.'),
118+
onLimitExceeded: async (ctx: Context, next: () => Promise<void>) => {
119+
const waitTime = '1000';
120+
const message = `⚠️ Rate limit exceeded. Please wait for ${waitTime} ms before trying again.`;
121+
122+
try {
123+
await ctx.reply(message);
124+
} catch (error: any) {
125+
logger.error(`Failed to send rate limit message: ${error.message}`);
126+
}
127+
if (next) {
128+
await next();
129+
}
130+
},
104131
})
105132
);
106133
this._bot.on('my_chat_member', (ctx) => this.handleJoinNewChat(ctx));
107-
this._bot.on('message', (ctx) => this.handleMessage(ctx));
134+
this._bot.on('message', (ctx) => {
135+
CopBot.setContext(ctx);
136+
this.handleMessage(ctx);
137+
});
108138
this._bot.catch(async (error: BotError<Context>) => {
109139
if (error.message.includes('timeout')) {
110140
await error.ctx.reply('The request took too long to process. Please try again later.');

src/database/ConnectionPool.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,37 @@ export class ConnectionPool {
1010
this._pool = this.initializePool(connectionString);
1111
}
1212
async connect(): Promise<boolean> {
13+
let client;
1314
try {
14-
await this._pool.connect();
15+
client = await this._pool.connect();
1516
logger.info('Database connection successful');
1617
return true;
1718
} catch (error: any) {
1819
console.error('Database connection error:', error.message);
20+
21+
// Handle missing database error (PostgreSQL code: 3D000)
1922
if (error.code === '3D000') {
2023
console.log(`Database does not exist. Creating database ${Config.database.databaseName}...`);
21-
await this.createDatabase();
22-
await this.reinitializePool();
24+
2325
try {
24-
const client = await this._pool.connect();
26+
await this.createDatabase();
27+
await this.reinitializePool();
28+
client = await this._pool.connect(); // Retry connection
2529
logger.info('Database connection successful after reinitialization');
2630
return true;
2731
} catch (reconnectError: any) {
28-
console.error('Reconnection failed:', reconnectError.message);
32+
console.error('Reconnection failed after reinitialization:', reconnectError.message);
2933
return false;
3034
}
3135
} else {
3236
console.error('Unexpected error connecting to the database:', error);
3337
return false;
3438
}
39+
} finally {
40+
// Release client only if it was successfully acquired
41+
if (client) {
42+
client.release();
43+
}
3544
}
3645
}
3746
private async createDatabase(): Promise<void> {
@@ -68,7 +77,9 @@ export class ConnectionPool {
6877
});
6978
}
7079
async reinitializePool() {
71-
await this._pool.end(); // Close old connections
80+
if (this._pool) {
81+
await this._pool.end();
82+
}
7283
const newConnectionString = this.getConnectionString();
7384
this._pool = this.initializePool(newConnectionString);
7485
console.warn('Connection pool reinitialized.');

src/database/service/Database.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,16 @@ export class DatabaseService {
88
}
99
this._client = client;
1010
}
11-
12-
/**
13-
* Runs a query with parameters and returns the result.
14-
*/
1511
async query<T extends QueryResultRow>(sql: string, params: any[] = []): Promise<QueryResult<T>> {
1612
try {
1713
return await this._client.query<T>(sql, params);
1814
} catch (error: any) {
1915
console.error(`Error executing query: ${sql}`, error);
2016
throw new Error(`Database query failed: ${error.message}`);
17+
} finally {
18+
this._client.release();
2119
}
2220
}
23-
/**
24-
* Inserts a new record and returns the inserted row.
25-
*/
2621
async insert<T extends QueryResultRow>(tableName: string, data: Record<string, any>, returning: string[] = ['*']): Promise<T> {
2722
const columns = Object.keys(data).join(', ');
2823
const values = Object.values(data);
@@ -32,10 +27,6 @@ export class DatabaseService {
3227
const result = await this.query<T>(sql, values);
3328
return result.rows[0];
3429
}
35-
36-
/**
37-
* Updates a record and returns the updated row.
38-
*/
3930
async update<T extends QueryResultRow>(tableName: string, data: Record<string, any>, condition: Record<string, any>, returning: string[] = ['*']): Promise<T> {
4031
const setClauses = Object.keys(data)
4132
.map((key, i) => `"${key}" = $${i + 1}`)
@@ -49,10 +40,6 @@ export class DatabaseService {
4940
const result = await this.query<T>(sql, values);
5041
return result.rows[0];
5142
}
52-
53-
/**
54-
* Deletes records and optionally returns the deleted rows.
55-
*/
5643
async delete<T extends QueryResultRow>(tableName: string, condition: Record<string, any>, returning: string[] = ['*']): Promise<T[]> {
5744
const whereClauses = Object.keys(condition)
5845
.map((key, i) => `"${key}" = $${i + 1}`)

src/service/database/ServiceProvider.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ import { UserService } from '../../database/models/User';
66
import { GroupRuleService } from '../../database/models/GroupRule';
77
import { WarningDatabaseService } from '../../database/models/Warning';
88
import logger from '../../utils/logger';
9+
import { CopBot } from '../../bot';
910

1011
export class ServiceProvider {
1112
private static instance: ServiceProvider;
1213
private _clientInstance: Client;
1314
private _connectionPool!: ConnectionPool;
15+
16+
private lastRequestTime: number | null = null; // Track the last request time
17+
private readonly requestInterval: number = 5000;
1418
private constructor() {
1519
this._clientInstance = new Client();
1620
}
@@ -27,6 +31,28 @@ export class ServiceProvider {
2731
}
2832
return ServiceProvider.instance;
2933
}
34+
private async enforceRateLimit(): Promise<void> {
35+
const ctx = CopBot.getContext();
36+
const now = Date.now();
37+
if (this.lastRequestTime) {
38+
const elapsed = now - this.lastRequestTime;
39+
if (elapsed < this.requestInterval) {
40+
const waitTime = this.requestInterval - elapsed;
41+
42+
if (ctx) {
43+
try {
44+
await ctx.reply(`⚠️ Rate limit exceeded. Please wait for ${waitTime} ms before making another request.`);
45+
} catch (error: any) {
46+
logger.error(`Failed to notify user about rate limit: ${error.message}`);
47+
}
48+
} else {
49+
logger.warn('No active context to send a rate limit message.');
50+
}
51+
await new Promise((resolve) => setTimeout(resolve, waitTime));
52+
}
53+
}
54+
this.lastRequestTime = Date.now();
55+
}
3056
static getInstance() {
3157
return ServiceProvider.instance;
3258
}
@@ -41,6 +67,7 @@ export class ServiceProvider {
4167
await this._connectionPool.close();
4268
}
4369
async getPoolClint(): Promise<PoolClient> {
70+
await this.enforceRateLimit();
4471
try {
4572
const client = await this._connectionPool.getClient();
4673
client.on('error', (err: any) => {

0 commit comments

Comments
 (0)