Skip to content

Commit 0bc84c1

Browse files
committed
refactor(bot): The bot/index file and handleMessage method bug were fixed.
In this commit, all the codes related to handling user messages were transferred to the decorator, and various error handlings were made in different codes, such as createDecorator, and no code was placed in the handleMessage method, and all data processing is in the decorators. (I hope it works :))
1 parent 2ba819d commit 0bc84c1

10 files changed

Lines changed: 128 additions & 107 deletions

File tree

src/app.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ async function main() {
1818

1919
// Graceful shutdown
2020
process.on('SIGTERM', async () => {
21-
logger.info('SIGTERM signal received. Shutting down...');
21+
logger.warn('SIGTERM signal received. Shutting down...');
2222
await shutdown(botInstance);
2323
});
2424

2525
process.on('SIGINT', async () => {
26-
logger.info('SIGINT signal received. Shutting down...');
26+
logger.warn('SIGINT signal received. Shutting down...');
2727
await shutdown(botInstance);
2828
});
2929

src/bot/index.ts

Lines changed: 8 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,11 @@ import { Bot, webhookCallback } from 'grammy';
22
import type { Context } from 'grammy';
33
import Config from '../config';
44
import { GenerateCommand } from './handlers/GenerateCommands';
5-
import { GeneralCommands } from './commands/genearl/GeneralCommands';
6-
import { UserCommands } from './commands/user/UserCommands';
7-
import { AdminCommands } from './commands/admin/AdminCommands';
85
import { SaveUserData } from '../decorators/Database';
96
import { MessageValidator } from '../decorators/Context';
7+
import * as express from 'express';
108
import { BotReply } from '../utils/chat/BotReply';
119
import logger from '../utils/logger';
12-
import * as express from 'express';
1310
export class CopBot {
1411
private static instance: CopBot;
1512
private _bot: Bot<Context>;
@@ -53,25 +50,13 @@ export class CopBot {
5350
app.use(express.json());
5451
app.post(webhookPath, async (req, res) => {
5552
res.sendStatus(200);
56-
const MAX_RETRIES = 3;
57-
let retryCount = 0;
58-
59-
const processWebhook = async () => {
53+
setImmediate(async () => {
6054
try {
61-
// Process the webhook
6255
await webhookCallback(this._bot, 'express')(req, res);
63-
} catch (err) {
64-
if (retryCount < MAX_RETRIES) {
65-
retryCount++;
66-
const delay = Math.min(1000 * 2 ** retryCount, 30000);
67-
await new Promise((resolve) => setTimeout(resolve, delay));
68-
await processWebhook();
69-
} else {
70-
console.error('Max retries reached. Webhook processing failed.');
71-
}
56+
} catch (error: any) {
57+
logger.error(`Error processing update:${error}`);
7258
}
73-
};
74-
await processWebhook();
59+
});
7560
});
7661

7762
const port = process.env.PORT || 3000;
@@ -85,7 +70,7 @@ export class CopBot {
8570
const trySetWebhook = async () => {
8671
try {
8772
if (!webhookInfo.url) {
88-
logger.info('Webhook is not set. Trying to set the webhook...');
73+
logger.warn('Webhook is not set. Trying to set the webhook...');
8974
await this._bot.api.setWebhook(webhookURL);
9075
webhookInfo = await this._bot.api.getWebhookInfo();
9176
logger.info(`Webhook set: ${webhookInfo.url}`);
@@ -97,7 +82,7 @@ export class CopBot {
9782
logger.error(`Error setting webhook (Attempt ${retries}): ${error.message}`);
9883
if (retries < MAX_RETRIES) {
9984
const delay = Math.min(1000 * 2 ** retries, 30000); // Exponential backoff
100-
logger.info(`Retrying in ${delay / 1000} seconds...`);
85+
logger.warn(`Retrying in ${delay / 1000} seconds...`);
10186
await new Promise((resolve) => setTimeout(resolve, delay));
10287
await trySetWebhook();
10388
} else {
@@ -130,48 +115,11 @@ export class CopBot {
130115
this._bot.catch((err) => {
131116
console.error('Middleware error:', err);
132117
});
133-
134118
await this.start();
135119
logger.info('Bot is running');
136120
}
137-
@SaveUserData()
138121
@MessageValidator()
139-
async handleMessage(ctx: Context) {
140-
try {
141-
if (!ctx.message?.text) {
142-
logger.warn('Message text is undefined');
143-
return;
144-
}
145-
const messageText = ctx.message?.text?.toLowerCase().trim();
146-
const reply = new BotReply(ctx);
147-
const user = ctx.message?.reply_to_message?.from;
148-
149-
if (messageText === 'ask' && user) {
150-
const name = user.username ? `@${user.username}` : user.first_name;
151-
const responseMessage = `Dear ${name}, ask your question correctly.\nIf you want to know how to do this, read the article below:\ndontasktoask.ir`;
152-
await reply.textReply(responseMessage);
153-
}
154-
155-
if (ctx.message?.entities) {
156-
const commandEntity = ctx.message.entities.find((entity) => entity.type === 'bot_command');
157-
if (commandEntity) {
158-
let command = messageText?.split(' ')[0]?.replace('/', '')!;
159-
logger.debug(`Processing command: ${command} `);
160-
command = command.includes('@') ? command.split('@')[0] : command;
161-
const handler = (GeneralCommands as any)[command] || (UserCommands as any)[command] || (AdminCommands as any)[command];
162-
if (typeof handler === 'function') {
163-
await handler(ctx);
164-
return;
165-
}
166-
}
167-
}
168-
} catch (err: any) {
169-
logger.error('Error in handleMessage:', err.message);
170-
const reply = new BotReply(ctx);
171-
await reply.textReply('An unexpected error occurred.');
172-
}
173-
}
174-
122+
async handleMessage(ctx: Context) {}
175123
@SaveUserData()
176124
async handleJoinNewChat(ctx: Context) {
177125
if (!ctx.message?.text) {

src/decorators/Bot.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export function ReplyToBot() {
2222
const randomMessage = jokeMessage();
2323
await reply.textReply(randomMessage);
2424
close();
25+
return;
2526
}
2627
return next();
2728
});

src/decorators/Catch.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
1-
import { GrammyError } from 'grammy';
21
import { ErrorResponse } from '../types/ResponseTypes';
32
import logger from '../utils/logger';
43
import { createDecorator } from './index';
4+
import { isGrammyError } from '../errors/decoratorErrorHandler';
55
export function Catch(customResponse?: ErrorResponse) {
66
return createDecorator(async (ctx, next) => {
77
try {
88
await next();
99
} catch (error: any) {
10-
if (error instanceof GrammyError && error.error_code === 400 && error.description === 'Bad Request: message to be replied not found') {
11-
console.warn(`Message not found to reply to. Skipping...`);
12-
return;
10+
if (isGrammyError(error)) {
11+
if (error.error_code === 400 && error.description === 'Bad Request: message to be replied not found') {
12+
console.warn(`Message not found to reply to. Skipping...`);
13+
return;
14+
}
15+
if (error.error_code === 403 && error.description.includes('bot was kicked')) {
16+
logger.warn(`Bot was kicked from a group (chat_id: ${error.payload?.chat_id || 'unknown'}). Skipping.`);
17+
return;
18+
}
1319
}
14-
if (error.error_code === 403 && error.description.includes('bot was kicked')) {
15-
logger.warn(`[Warning] Bot was kicked from a group (chat_id: ${error.payload.chat_id}). Skipping.`);
16-
return;
20+
if (!isGrammyError(error)) {
21+
logger.error(`Unhandled error: ${error.message || 'No error message provided'}`);
22+
if (error.stack) {
23+
logger.debug(`Stack trace: ${error.stack}`);
24+
}
1725
}
1826
// Custom response or default error message
1927
const errorResponse: ErrorResponse = customResponse || {
20-
message: error.message,
28+
message: error.message || 'An unknown error occurred.',
2129
statusCode: error.statusCode || 500,
2230
category: 'General',
2331
};

src/decorators/Context.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { BotReply } from '../utils/chat/BotReply';
22
import { MessagesService } from '../service/messages';
3-
import { createDecorator } from '.';
43
import { RateLimitConfig } from '../types/CommandTypes';
54
import { RateLimiter } from '../utils/RateLimiter';
5+
import { createDecorator } from './index';
66
/**
77
* A decorator to restrict commands to group chats.
88
* Provides user feedback for invalid chat types.
@@ -36,8 +36,15 @@ export function RestrictToGroupChats() {
3636
export function MessageValidator() {
3737
return createDecorator(async (ctx, next) => {
3838
const messageService = new MessagesService(ctx);
39-
await Promise.all([messageService.isNewUser(), messageService.isCode(), messageService.checkAndHandleBlacklistedWords(), messageService.userIsLeftGroup()]);
40-
next();
39+
await Promise.all([
40+
messageService.isNewUser(),
41+
messageService.isCode(),
42+
messageService.checkAndHandleBlacklistedWords(),
43+
messageService.userIsLeftGroup(),
44+
messageService.askCommand(),
45+
messageService.executeCommand(),
46+
]);
47+
await next();
4148
});
4249
}
4350
/**

src/decorators/index.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,26 @@ import { handleDecoratorError } from '../errors/decoratorErrorHandler';
33
export function createDecorator(middleware: (ctx: Context, next: () => Promise<void>, close: () => void) => Promise<void>) {
44
return function (_target: any, _propertyKey: string | symbol, descriptor: PropertyDescriptor) {
55
const originalMethod = descriptor.value;
6-
76
descriptor.value = async function (...args: any[]) {
87
try {
98
const ctx: Context = (this as any)?.ctx || args[0];
109
let shouldContinue = true;
1110
const close = () => {
1211
shouldContinue = false;
1312
};
14-
await middleware(
13+
return await middleware(
1514
ctx,
1615
async () => {
1716
if (shouldContinue) {
18-
return await originalMethod.apply(this, [...args]);
17+
try {
18+
return await originalMethod.apply(this, [...args]);
19+
} catch (error: any) {
20+
handleDecoratorError(error);
21+
}
1922
}
2023
},
2124
close
2225
);
23-
24-
return;
2526
} catch (error: any) {
2627
handleDecoratorError(error);
2728
}

src/errors/decoratorErrorHandler.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { GrammyError } from 'grammy';
22
import logger from '../utils/logger';
3-
3+
export function isGrammyError(error: any): error is GrammyError {
4+
return error && error.error_code !== undefined && error.description !== undefined;
5+
}
46
export function handleDecoratorError(error: any) {
57
if (error instanceof GrammyError) {
68
handleGrammyError(error);

src/service/messages/index.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,25 @@ import { Context } from 'grammy';
22
import { ServiceProvider } from '../database/ServiceProvider';
33
import { BotReply } from '../../utils/chat/BotReply';
44
import { ChatInfo } from '../../utils/chat/ChatInfo';
5+
import logger from '../../utils/logger';
6+
import { MessageEntity } from 'grammy/types';
57
export class MessagesService {
68
private _reply: BotReply;
79
private _chatInfo: ChatInfo;
810
constructor(private _ctx: Context) {
911
this._reply = new BotReply(_ctx);
1012
this._chatInfo = new ChatInfo(_ctx);
1113
}
14+
private isText(): boolean {
15+
if (!this._ctx.message?.text) {
16+
logger.warn('Message text is undefined');
17+
return false;
18+
}
19+
return true;
20+
}
21+
1222
async isCode() {
23+
if (!this.isText()) return;
1324
const entities = this._ctx.message!.entities;
1425
entities?.forEach((e) => {
1526
if (e.type === 'pre' && e.language) {
@@ -18,6 +29,7 @@ export class MessagesService {
1829
});
1930
}
2031
async isNewUser() {
32+
if (!this.isText()) return;
2133
if (this._ctx.message?.new_chat_members?.length! > 0) {
2234
const users = this._ctx.message!.new_chat_members!;
2335
const chat = this._ctx.chat;
@@ -39,8 +51,8 @@ export class MessagesService {
3951
}
4052
}
4153
}
42-
Spam() {}
4354
async userIsLeftGroup() {
55+
if (!this.isText()) return;
4456
if (this._ctx.message?.left_chat_member) {
4557
const user = this._ctx.message.left_chat_member!;
4658

@@ -53,6 +65,7 @@ export class MessagesService {
5365
}
5466

5567
async checkAndHandleBlacklistedWords() {
68+
if (!this.isText()) return;
5669
const groupId = this._ctx.chat?.id;
5770
if (!groupId) {
5871
return;
@@ -154,4 +167,69 @@ We appreciate your understanding and cooperation in keeping the community welcom
154167
}
155168
return warningMessage;
156169
}
170+
async askCommand() {
171+
if (!this.isText()) return;
172+
const messageText = this._ctx.message?.text?.toLowerCase().trim();
173+
if (messageText !== 'ask' || !this._ctx.message?.reply_to_message?.from) {
174+
return;
175+
}
176+
const user = this._ctx.message?.reply_to_message?.from!;
177+
const name = user.username ? `@${user.username}` : user.first_name;
178+
const responseMessage = `Dear ${name}, ask your question correctly.\nIf you want to know how to do this, read the article below:\ndontasktoask.ir`;
179+
logger.debug(`Sending ask command response to ${name}`);
180+
await this._reply.textReply(responseMessage);
181+
}
182+
async executeCommand() {
183+
if (!this.isText()) return;
184+
const isCommand = this.checkIfCommand();
185+
if (!isCommand) {
186+
logger.debug('Non-command message received. Ignoring.');
187+
return;
188+
}
189+
const messageText = this._ctx.message?.text?.toLowerCase().trim()!;
190+
const command = this.parseCommand(messageText, this._ctx.message?.entities)!;
191+
logger.debug(`Attempting to execute the command: ${command}`);
192+
const [GeneralCommands, UserCommands, AdminCommands] = await Promise.all([
193+
import('../../bot/commands/genearl/GeneralCommands').then((module) => module.GeneralCommands),
194+
import('../../bot/commands/user/UserCommands').then((module) => module.UserCommands),
195+
import('../../bot/commands/admin/AdminCommands').then((module) => module.AdminCommands),
196+
]);
197+
const handler = (GeneralCommands as any)[command] || (UserCommands as any)[command] || (AdminCommands as any)[command];
198+
199+
if (typeof handler === 'function') {
200+
try {
201+
await handler(this._ctx);
202+
} catch (err: any) {
203+
logger.error(`Handler for command "${command}" threw an error: ${err.message}`);
204+
await this._reply.textReply('An error occurred while processing your command.');
205+
}
206+
} else {
207+
logger.warn(`No handler found for command: ${command}`);
208+
await this._reply.textReply(`Unknown command: ${command}`);
209+
}
210+
}
211+
private checkIfCommand(): boolean {
212+
const isCommand = this._ctx.message?.entities?.some((entity) => entity.type === 'bot_command');
213+
if (!isCommand) {
214+
logger.debug('Message does not contain a command entity.');
215+
return false;
216+
}
217+
return true;
218+
}
219+
private parseCommand(messageText: string, entities?: MessageEntity[]): string | null {
220+
if (entities) {
221+
const commandEntity = entities.find((entity) => entity.type === 'bot_command');
222+
223+
if (commandEntity) {
224+
let command = messageText.split(' ')[0].replace('/', '');
225+
command = command.includes('@') ? command.split('@')[0] : command;
226+
227+
logger.debug(`Parsed command: ${command}`);
228+
return command;
229+
}
230+
}
231+
232+
logger.warn('No command entity found in the message.');
233+
return null;
234+
}
157235
}

src/utils/chat/BotInfo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Context } from 'grammy';
2-
32
export class BotInfo {
43
constructor(private _ctx: Context) {
54
this._ctx = _ctx;
@@ -17,3 +16,4 @@ export class BotInfo {
1716
return ['administrator', 'creator'].includes(chatMember.status);
1817
}
1918
}
19+

0 commit comments

Comments
 (0)