Skip to content

Commit 73ca1c7

Browse files
committed
feat(bot-services): implement advanced user management and moderation tools
- Add `MuteService` for muting and unmuting users in chats with duration-based and indefinite options. - Introduce `BlackListService` to manage group-specific blacklisted words: - `getAll` to fetch all blacklisted words for a group. - `add` to add a word to the blacklist. - `remove` to delete a specific or last word from the blacklist. - `clear` to reset the blacklist. - Create `BanService` for banning and unbanning users: - `ban` to remove users from groups and delete their database records. - `unBan` to lift chat bans. - Add `AdminService` to grant or revoke admin rights: - `grant` to promote users to admin with optional custom titles. - `revoke` to demote users from admin roles. - Enhance error handling and ensure proper validations using `AdminValidationService`. - Leverage `ServiceProvider` for seamless interaction with database services. This update provides robust moderation capabilities and improved group management workflows.
1 parent a89fc31 commit 73ca1c7

4 files changed

Lines changed: 247 additions & 0 deletions

File tree

src/bot/service/admin/Admin.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Context } from 'grammy';
2+
3+
export class AdminService {
4+
static async grant(ctx: Context): Promise<string> {
5+
const replyUser = ctx.message?.reply_to_message?.from!;
6+
if (!replyUser) {
7+
return 'Please reply to the user you want to promote to admin.';
8+
}
9+
const input = ctx.message?.text!.split(/\s+/).slice(1);
10+
const nickname = input!.join('').toLowerCase() || 'admin';
11+
await this.applyPromote(ctx, replyUser.id, true);
12+
if (nickname) {
13+
await this.setNickname(ctx, replyUser.id, nickname);
14+
return `${replyUser.first_name} has been promoted to admin with the title "${nickname}".`;
15+
}
16+
17+
return `${replyUser.first_name} has been promoted to admin.`;
18+
}
19+
static async revoke(ctx: Context) {
20+
const replyUser = ctx.message?.reply_to_message?.from!;
21+
if (!replyUser) {
22+
return 'Please reply to the user you want to promote to admin.';
23+
}
24+
await this.applyPromote(ctx, replyUser.id, false);
25+
return `${replyUser.first_name} has been demoted from admin.`;
26+
}
27+
private static async applyPromote(ctx: Context, userId: number, allow: boolean) {
28+
await ctx.api.promoteChatMember(ctx.chat!.id, userId, {
29+
can_delete_messages: allow,
30+
can_invite_users: allow,
31+
can_pin_messages: allow,
32+
});
33+
}
34+
private static async setNickname(ctx: Context, userId: number, nickname: string) {
35+
await ctx.api.setChatAdministratorCustomTitle(ctx.chat!.id, userId, nickname);
36+
}
37+
}

src/bot/service/admin/Ban.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Context } from 'grammy';
2+
import { AdminValidationService } from './validation';
3+
import { ServiceProvider } from '../../../service/database/ServiceProvider';
4+
5+
export class BanService {
6+
static async ban(ctx: Context): Promise<boolean> {
7+
const validationResult = await AdminValidationService.validateContext(ctx);
8+
if (!validationResult) {
9+
return false;
10+
}
11+
const { groupId, userId } = validationResult;
12+
const services = ServiceProvider.getInstance();
13+
const [groupService, userService] = await Promise.all([services.getGroupService(), services.getUserService()]);
14+
let group = await groupService.getByGroupId(groupId);
15+
let user = await userService.getByTelegramId(userId);
16+
const userData = { first_name: ctx!.message?.reply_to_message?.from?.first_name!, id: userId, username: ctx.message?.reply_to_message?.from?.username! };
17+
if (!user) {
18+
user = await userService.save(userData);
19+
}
20+
if (!group) {
21+
group = await groupService.save(ctx);
22+
}
23+
// If the user is part of the group, proceed with the removal
24+
if (group && user) {
25+
// Remove the user from the group's approved_users and members arrays
26+
const updatedGroup = {
27+
...group,
28+
approved_users: group.approved_users.filter((id) => Number(id) !== userId),
29+
members: group.members.filter((id) => Number(id) !== userId),
30+
};
31+
await groupService.update(updatedGroup);
32+
await userService.delete(userId);
33+
return true;
34+
} else {
35+
// If the group or user is not found, send an error message
36+
return false;
37+
}
38+
}
39+
static async unBan(ctx: Context): Promise<boolean> {
40+
const userIdToUnban = ctx.message?.reply_to_message?.from?.id!;
41+
if (!userIdToUnban) {
42+
return false;
43+
}
44+
await ctx.api.unbanChatMember(ctx.chat?.id!, ctx.message?.reply_to_message?.from!.id!);
45+
return true;
46+
}
47+
}

src/bot/service/admin/Blacklist.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { Context } from 'grammy';
2+
import { ServiceProvider } from '../../../service/database/ServiceProvider';
3+
4+
export class BlackListService {
5+
static async getAll(ctx: Context, groupId: number): Promise<string[]> {
6+
const service = ServiceProvider.getInstance();
7+
const groupService = await service.getGroupService();
8+
9+
// Fetch group by group ID
10+
let group = await groupService.getByGroupId(groupId);
11+
12+
// If group doesn't exist, initialize it
13+
if (!group) {
14+
group = await groupService.save(ctx);
15+
}
16+
17+
return group.black_list || [];
18+
}
19+
static async add(groupId: number, word: string): Promise<string[]> {
20+
const service = ServiceProvider.getInstance();
21+
const groupService = await service.getGroupService();
22+
23+
// Fetch group by group ID
24+
let group = await groupService.getByGroupId(groupId);
25+
26+
// If group doesn't exist, throw an error
27+
if (!group) {
28+
throw new Error(`Group with ID ${groupId} not found.`);
29+
}
30+
31+
// Ensure the blacklist is initialized
32+
group.black_list = group.black_list || [];
33+
34+
// Add the word if it's not already present
35+
if (!group.black_list.includes(word)) {
36+
group.black_list.push(word);
37+
await groupService.update({
38+
...group,
39+
black_list: group.black_list,
40+
});
41+
}
42+
43+
return group.black_list;
44+
}
45+
static async remove(groupId: number, word?: string): Promise<string[]> {
46+
const service = ServiceProvider.getInstance();
47+
const groupService = await service.getGroupService();
48+
49+
// Fetch group by group ID
50+
let group = await groupService.getByGroupId(groupId);
51+
52+
// If group doesn't exist, throw an error
53+
if (!group) {
54+
throw new Error(`Group with ID ${groupId} not found.`);
55+
}
56+
if (!word) {
57+
if (group.black_list && group.black_list.length > 0) {
58+
group.black_list.pop();
59+
}
60+
} else {
61+
// Remove the specified word
62+
group.black_list = group.black_list.filter((item) => item !== word);
63+
}
64+
await groupService.update({
65+
...group,
66+
black_list: group.black_list,
67+
});
68+
69+
return group.black_list || [];
70+
}
71+
static async clear(groupId: number): Promise<string[]> {
72+
const service = ServiceProvider.getInstance();
73+
const groupService = await service.getGroupService();
74+
75+
// Fetch group by group ID
76+
let group = await groupService.getByGroupId(groupId);
77+
78+
// If group doesn't exist, throw an error
79+
if (!group) {
80+
throw new Error(`Group with ID ${groupId} not found.`);
81+
}
82+
83+
// Clear the blacklist
84+
group.black_list = [];
85+
await groupService.update({
86+
...group,
87+
black_list: group.black_list,
88+
});
89+
90+
return group.black_list;
91+
}
92+
}

src/bot/service/admin/Mute.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Context } from 'grammy';
2+
import { parseDuration } from '../../../utils';
3+
import { tehranZone } from '../general/date';
4+
5+
export class MuteService {
6+
/**
7+
* Mutes a user in the chat for a specified duration or indefinitely.
8+
* @param ctx - The bot context.
9+
* @returns A message indicating the duration of the mute.
10+
*/
11+
static async muteUser(ctx: Context): Promise<string> {
12+
const replyMessage = ctx.message?.reply_to_message;
13+
if (!replyMessage) {
14+
throw new Error('Please reply to a user to mute them.');
15+
}
16+
17+
const userId = replyMessage.from?.id!;
18+
const durationStr = MuteService.extractDurationFromCommand(ctx.message?.text);
19+
const durationMs = durationStr ? parseDuration(durationStr) : null;
20+
21+
const expiration = durationMs ? new Date(tehranZone().getTime() + durationMs) : null;
22+
await MuteService.applyRestrictions(ctx, userId, false, expiration!);
23+
24+
return durationMs ? `User ${replyMessage.from?.first_name} has been muted for ${durationStr}.` : `User ${replyMessage.from?.first_name} has been muted indefinitely.`;
25+
}
26+
/**
27+
* Unmutes a user in the chat by restoring their permissions.
28+
* @param ctx - The bot context.
29+
* @returns A success message.
30+
*/
31+
static async unmuteUser(ctx: Context): Promise<string> {
32+
const replyMessage = ctx.message?.reply_to_message!;
33+
const userId = replyMessage.from?.id!;
34+
await MuteService.applyRestrictions(ctx, userId, true);
35+
36+
return `User ${replyMessage.from?.first_name} has been unmuted and their permissions have been restored.`;
37+
}
38+
/**
39+
* Extracts the duration from the bot command.
40+
* @param commandText - The text of the command.
41+
* @returns The duration string or null if not provided.
42+
*/
43+
private static extractDurationFromCommand(commandText?: string): string | null {
44+
if (!commandText) return null;
45+
const input = commandText.split(/\s+/).slice(1);
46+
return input.join('').toLowerCase() || null;
47+
}
48+
/**
49+
* Applies chat restrictions to a user.
50+
* @param ctx - The bot context.
51+
* @param userId - The ID of the user to restrict/unrestrict.
52+
* @param allow - Whether to allow or restrict permissions.
53+
* @param expiration - The expiration time of the restrictions (optional).
54+
*/
55+
private static async applyRestrictions(ctx: Context, userId: number, allow: boolean, expiration?: Date): Promise<void> {
56+
const untilDate = expiration ? Math.floor(expiration.getTime() / 1000) : undefined;
57+
58+
await ctx.restrictChatMember(
59+
userId,
60+
{
61+
can_send_messages: allow,
62+
can_send_polls: allow,
63+
can_send_other_messages: allow,
64+
can_add_web_page_previews: allow,
65+
can_send_photos: allow,
66+
can_send_audios: allow,
67+
},
68+
{ until_date: untilDate }
69+
);
70+
}
71+
}

0 commit comments

Comments
 (0)