Skip to content

Commit 8ecde40

Browse files
feat: quick mute service
1 parent 1c96850 commit 8ecde40

File tree

10 files changed

+187
-15
lines changed

10 files changed

+187
-15
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@
8888
"Unban",
8989
"Unbans",
9090
"undici",
91-
"Unmutes"
91+
"Unmutes",
92+
"xnor"
9293
],
9394
"material-icon-theme.folders.associations": {
9495
"automod": "Server",

src/framework/typescript/services/Service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* along with SudoBot. If not, see <https://www.gnu.org/licenses/>.
1818
*/
1919

20+
import type { Logger } from "@framework/log/Logger";
2021
import type { Awaitable } from "discord.js";
2122
import type Application from "../app/Application";
2223
import type BaseClient from "../client/BaseClient";
@@ -25,11 +26,13 @@ import { HasApplication } from "../types/HasApplication";
2526
abstract class Service extends HasApplication {
2627
protected static override name: string;
2728
protected readonly client: BaseClient;
29+
protected readonly logger: Logger;
2830
public boot(): Awaitable<void> {}
2931

3032
public constructor(application: Application) {
3133
super(application);
3234
this.client = application.getClient();
35+
this.logger = this.application.logger;
3336
}
3437

3538
public static getName() {

src/framework/typescript/types/ClientEvents.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ enum Events {
103103
Warn = "warn",
104104
WebhooksUpdate = "webhooksUpdate",
105105
WebhookUpdate = "webhookUpdate",
106+
Raw = "raw",
106107

107108
NormalMessageCreate = "normalMessageCreate",
108109
NormalMessageUpdate = "normalMessageUpdate",

src/framework/typescript/utils/logic.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,12 @@
66
* @returns The logical XOR of the two boolean values.
77
*/
88
export const xor = (a: boolean, b: boolean) => a !== b;
9+
10+
/**
11+
* Returns the logical XNOR of two boolean values.
12+
*
13+
* @param a - The first boolean value.
14+
* @param b - The second boolean value.
15+
* @returns The logical XNOR of the two boolean values.
16+
*/
17+
export const xnor = (a: boolean, b: boolean) => a === b;

src/main/typescript/core/DiscordKernel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class DiscordKernel extends Kernel {
8080
"@automod/RuleModerationService",
8181
"@automod/RaidProtectionService",
8282
"@automod/VerificationService",
83+
"@services/QuickMuteService",
8384
"@services/ChannelLockManager",
8485
"@services/ReactionRoleService",
8586
"@services/AFKService",
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Inject } from "@framework/container/Inject";
2+
import EventListener from "@framework/events/EventListener";
3+
import { Events } from "@framework/types/ClientEvents";
4+
import type ReactionRoleService from "@main/services/ReactionRoleService";
5+
import type { RawMessageReactionData } from "@main/services/ReactionRoleService";
6+
7+
class RawEventListener extends EventListener<Events.Raw> {
8+
public override readonly name = Events.Raw;
9+
10+
@Inject("reactionRoleService")
11+
protected readonly reactionRoleService!: ReactionRoleService;
12+
13+
public override async execute(data: { t: string; d: unknown }): Promise<void> {
14+
await this.reactionRoleService.onRaw(data as RawMessageReactionData);
15+
}
16+
}
17+
18+
export default RawEventListener;

src/main/typescript/schemas/GuildConfigSchema.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -220,22 +220,23 @@ export const GuildConfigSchema = z.object({
220220
.int()
221221
.optional()
222222
})
223-
.optional()
224-
/*
225-
quickmute: z
223+
.optional(),
224+
quick_mute: z
226225
.object({
227226
enabled: z.boolean().optional().default(false),
228-
clear_emoji: z.string().optional(),
229-
noclear_emoji: z.string().optional(),
230-
duration: z
227+
mute_clear_emoji: z.string().optional(),
228+
mute_emoji: z.string().optional(),
229+
default_duration: z
231230
.number()
232231
.int()
233232
.min(0)
234233
.default(1000 * 60 * 60 * 2)
235234
.optional(),
236235
reason: z.string().optional()
237236
})
238-
.optional(),
237+
.optional()
238+
/*
239+
239240
logging: z
240241
.object({
241242
enabled: z.boolean().default(false),

src/main/typescript/services/InfractionManager.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,14 @@ import { formatDistanceStrict, formatDistanceToNowStrict } from "date-fns";
3636
import {
3737
APIEmbed,
3838
Awaitable,
39-
bold,
4039
CategoryChannel,
4140
ChannelType,
4241
Collection,
4342
Colors,
4443
DiscordAPIError,
4544
Guild,
4645
GuildMember,
47-
italic,
46+
GuildTextBasedChannel,
4847
Message,
4948
MessageCreateOptions,
5049
MessagePayload,
@@ -55,8 +54,9 @@ import {
5554
RoleResolvable,
5655
Snowflake,
5756
TextBasedChannel,
58-
TextChannel,
5957
User,
58+
bold,
59+
italic,
6060
userMention
6161
} from "discord.js";
6262
import InfractionChannelDeleteQueue from "../queues/InfractionChannelDeleteQueue";
@@ -2782,7 +2782,7 @@ type CreateModeratorMessage<E extends boolean> = CommonOptions<E> & {
27822782

27832783
type CreateClearMessagesPayload<E extends boolean> = Omit<CommonOptions<E>, "notify"> & {
27842784
user?: User;
2785-
channel: TextChannel;
2785+
channel: GuildTextBasedChannel;
27862786
count?: number;
27872787
filters?: Array<MessageFilter>;
27882788
respond?: boolean;
@@ -2795,7 +2795,7 @@ type CreateMutePayload<E extends boolean> = CommonOptions<E> & {
27952795
duration?: Duration;
27962796
mode?: "role" | "timeout";
27972797
clearMessagesCount?: number;
2798-
channel?: TextChannel;
2798+
channel?: GuildTextBasedChannel;
27992799
roleTakeout?: boolean;
28002800
};
28012801

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { Inject } from "@framework/container/Inject";
2+
import Duration from "@framework/datetime/Duration";
3+
import { GatewayEventListener } from "@framework/events/GatewayEventListener";
4+
import { Name } from "@framework/services/Name";
5+
import { Service } from "@framework/services/Service";
6+
import { Events } from "@framework/types/ClientEvents";
7+
import { HasEventListeners } from "@framework/types/HasEventListeners";
8+
import { fetchMember } from "@framework/utils/entities";
9+
import { xnor } from "@framework/utils/logic";
10+
import type ConfigurationManager from "@main/services/ConfigurationManager";
11+
import type InfractionManager from "@main/services/InfractionManager";
12+
import type PermissionManagerService from "@main/services/PermissionManagerService";
13+
import {
14+
ChannelType,
15+
User,
16+
type GuildTextBasedChannel,
17+
type MessageReaction,
18+
type PartialMessageReaction,
19+
type PartialUser
20+
} from "discord.js";
21+
22+
@Name("quickMuteService")
23+
class QuickMuteService extends Service implements HasEventListeners {
24+
@Inject("configManager")
25+
private readonly configManager!: ConfigurationManager;
26+
27+
@Inject("infractionManager")
28+
private readonly infractionManager!: InfractionManager;
29+
30+
@Inject("permissionManager")
31+
private readonly permissionManager!: PermissionManagerService;
32+
33+
@GatewayEventListener(Events.MessageReactionAdd)
34+
public async onMessageReactionAdd(
35+
reaction: MessageReaction | PartialMessageReaction,
36+
user: User | PartialUser
37+
): Promise<boolean> {
38+
const guildId = reaction.message.guildId;
39+
40+
if (!guildId || !reaction.message.author) {
41+
this.logger.debug(
42+
"[QuickMuteService] Received MESSAGE_REACTION_ADD event without guildId or message author"
43+
);
44+
return false;
45+
}
46+
47+
const config = this.configManager.get(guildId)?.quick_mute;
48+
49+
if (!config?.enabled) {
50+
this.logger.debug(
51+
"[QuickMuteService] Received MESSAGE_REACTION_ADD event but quick mute is not enabled"
52+
);
53+
return false;
54+
}
55+
56+
const isMute =
57+
(reaction.emoji.id && !!config.mute_emoji && reaction.emoji.id === config.mute_emoji) ||
58+
(!reaction.emoji.id &&
59+
!!config.mute_emoji &&
60+
reaction.emoji.identifier === config.mute_emoji);
61+
const isClearMute =
62+
(reaction.emoji.id &&
63+
!!config.mute_clear_emoji &&
64+
reaction.emoji.id === config.mute_clear_emoji) ||
65+
(!reaction.emoji.id &&
66+
!!config.mute_clear_emoji &&
67+
reaction.emoji.identifier === config.mute_clear_emoji);
68+
69+
if (xnor(isMute, isClearMute)) {
70+
this.logger.debug(
71+
"[QuickMuteService] Received MESSAGE_REACTION_ADD event but the reaction is not a mute or clear mute reaction"
72+
);
73+
return false;
74+
}
75+
76+
const guild = this.client.guilds.cache.get(guildId);
77+
78+
if (!guild) {
79+
this.logger.debug(
80+
"[QuickMuteService] Received MESSAGE_REACTION_ADD event but the guild was not found"
81+
);
82+
return false;
83+
}
84+
85+
let member = reaction.message.member;
86+
87+
if (!member) {
88+
member = await fetchMember(guild, reaction.message.author.id);
89+
}
90+
91+
if (!member) {
92+
this.logger.debug(
93+
"[QuickMuteService] Received MESSAGE_REACTION_ADD event but the member was not found"
94+
);
95+
return false;
96+
}
97+
98+
const moderator = await fetchMember(guild, user.id);
99+
100+
if (!moderator) {
101+
this.logger.debug(
102+
"[QuickMuteService] Received MESSAGE_REACTION_ADD event but the moderator was not found"
103+
);
104+
return false;
105+
}
106+
107+
if (!(await this.permissionManager.canModerate(member, moderator))) {
108+
await reaction.remove().catch(this.logger.error);
109+
this.logger.debug(
110+
"[QuickMuteService] Received MESSAGE_REACTION_ADD event but the moderator does not have permission to mute"
111+
);
112+
return false;
113+
}
114+
115+
const clearCondition =
116+
isClearMute &&
117+
reaction.message.channel.type !== ChannelType.DM &&
118+
reaction.message.channel.isTextBased();
119+
120+
await this.infractionManager.createMute({
121+
guildId,
122+
member,
123+
moderator: user as User,
124+
reason: config.reason ?? "You have violated the server rules.",
125+
duration: Duration.fromMilliseconds(config.default_duration ?? 1000 * 60 * 60 * 2),
126+
notify: true,
127+
generateOverviewEmbed: false,
128+
channel: clearCondition
129+
? (reaction.message.channel as GuildTextBasedChannel)
130+
: undefined,
131+
clearMessagesCount: clearCondition ? 50 : undefined
132+
});
133+
134+
this.logger.debug("[QuickMuteService] Received MESSAGE_REACTION_ADD event");
135+
return true;
136+
}
137+
}
138+
139+
export default QuickMuteService;

src/main/typescript/services/ReactionRoleService.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ class ReactionRoleService extends Service implements HasEventListeners {
4444
this.application.logger.info("Successfully synced reaction roles");
4545
}
4646

47-
@GatewayEventListener("raw")
4847
public async onRaw(data: RawMessageReactionData) {
4948
if (data.t !== "MESSAGE_REACTION_ADD" && data.t !== "MESSAGE_REACTION_REMOVE") {
5049
return;
@@ -334,7 +333,7 @@ interface UserRequestInfo {
334333
timeout?: Timer;
335334
}
336335

337-
type RawMessageReactionData = {
336+
export type RawMessageReactionData = {
338337
t: "MESSAGE_REACTION_ADD" | "MESSAGE_REACTION_REMOVE";
339338
d: {
340339
user_id: string;

0 commit comments

Comments
 (0)