/[sudobot]/branches/8.x/src/services/ReactionRoleService.ts
ViewVC logotype

Annotation of /branches/8.x/src/services/ReactionRoleService.ts

Parent Directory Parent Directory | Revision Log Revision Log


Revision 577 - (hide annotations)
Mon Jul 29 18:52:37 2024 UTC (8 months ago) by rakinar2
File MIME type: application/typescript
File size: 11395 byte(s)
chore: add old version archive branches (2.x to 9.x-dev)
1 rakinar2 577 /**
2     * This file is part of SudoBot.
3     *
4     * Copyright (C) 2021-2023 OSN Developers.
5     *
6     * SudoBot is free software; you can redistribute it and/or modify it
7     * under the terms of the GNU Affero General Public License as published by
8     * the Free Software Foundation, either version 3 of the License, or
9     * (at your option) any later version.
10     *
11     * SudoBot is distributed in the hope that it will be useful, but
12     * WITHOUT ANY WARRANTY; without even the implied warranty of
13     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14     * GNU Affero General Public License for more details.
15     *
16     * You should have received a copy of the GNU Affero General Public License
17     * along with SudoBot. If not, see <https://www.gnu.org/licenses/>.
18     */
19    
20     import { ReactionRole } from "@prisma/client";
21     import { Client, GuildMember, PermissionsString, Routes, Snowflake } from "discord.js";
22     import Service from "../core/Service";
23     import { GatewayEventListener } from "../decorators/GatewayEventListener";
24     import { HasEventListeners } from "../types/HasEventListeners";
25     import { log, logError } from "../utils/Logger";
26     import { safeMemberFetch } from "../utils/fetch";
27    
28     export const name = "reactionRoleService";
29    
30     interface UserRequestInfo {
31     timestamps: number[];
32     timeout?: Timer;
33     }
34    
35     type RawMessageReactionData = {
36     t: "MESSAGE_REACTION_ADD" | "MESSAGE_REACTION_REMOVE";
37     d: {
38     user_id: string;
39     message_id: string;
40     channel_id: string;
41     guild_id: string;
42     emoji: {
43     name: string;
44     id?: string;
45     };
46     member?: {
47     user: {
48     bot: boolean;
49     };
50     };
51     };
52     };
53    
54     export default class ReactionRoleService extends Service implements HasEventListeners {
55     readonly reactionRoleEntries = new Map<string, ReactionRole | undefined>();
56     readonly users: Record<string, UserRequestInfo> = {};
57     readonly rateLimited = new Map<string, number>();
58    
59     @GatewayEventListener("ready")
60     async onReady(client: Client<true>) {
61     log("Syncing reaction roles...");
62    
63     const reactionRoles = await this.client.prisma.reactionRole.findMany({
64     where: {
65     guildId: {
66     in: [...client.guilds.cache.keys()]
67     }
68     }
69     });
70    
71     for (const reactionRole of reactionRoles) {
72     this.reactionRoleEntries.set(
73     `${reactionRole.guildId}_${reactionRole.channelId}_${reactionRole.messageId}_${reactionRole.emoji}`,
74     reactionRole
75     );
76     }
77    
78     log("Successfully synced reaction roles");
79     }
80    
81     @GatewayEventListener("raw")
82     async onRaw(data: RawMessageReactionData) {
83     if (data.t !== "MESSAGE_REACTION_ADD" && data.t !== "MESSAGE_REACTION_REMOVE") {
84     return;
85     }
86    
87     if (!this.client.configManager.config[data.d.guild_id]?.reaction_roles?.enabled) {
88     return;
89     }
90    
91     log(JSON.stringify(data, null, 2));
92    
93     const config = this.client.configManager.config[data.d.guild_id]?.reaction_roles?.ratelimiting;
94    
95     if (config?.enabled) {
96     const rateLimitedAt = this.rateLimited.get(data.d.user_id) ?? 0;
97    
98     if (Date.now() - rateLimitedAt < config.block_duration) {
99     log("The user has hit a ratelimit.");
100     return;
101     }
102    
103     this.rateLimited.delete(data.d.user_id);
104    
105     if (!this.users[data.d.user_id]) {
106     this.users[data.d.user_id] = {
107     timestamps: []
108     };
109     }
110    
111     const info = this.users[data.d.user_id];
112    
113     info.timestamps.push(Date.now());
114     }
115    
116     const { aborted, member, reactionRole } = await this.processRequest(data, data.t === "MESSAGE_REACTION_ADD");
117    
118     if (aborted) {
119     log("Request aborted");
120     this.setTimeout(data.d.user_id, data.d.guild_id);
121     return;
122     }
123    
124     try {
125     if (data.t === "MESSAGE_REACTION_ADD") {
126     await member?.roles.add(reactionRole!.roles, "Adding reaction roles");
127     } else {
128     await member?.roles.remove(reactionRole!.roles, "Removing reaction roles");
129     }
130     } catch (e) {
131     logError(e);
132     }
133    
134     this.setTimeout(data.d.user_id, data.d.guild_id);
135     }
136    
137     setTimeout(userId: string, guildId: string) {
138     if (!this.users[userId]) {
139     return;
140     }
141    
142     this.users[userId].timeout ??= setTimeout(() => {
143     const config = this.client.configManager.config[guildId]?.reaction_roles?.ratelimiting;
144    
145     if (config?.enabled) {
146     const delayedInfo = this.users[userId];
147     const timestamps = delayedInfo.timestamps.filter(timestamp => config.timeframe + timestamp >= Date.now());
148    
149     if (timestamps.length >= config.max_attempts) {
150     this.rateLimited.set(userId, Date.now());
151     }
152     }
153    
154     delete this.users[userId];
155     }, 9000);
156     }
157    
158     async processRequest(
159     data: RawMessageReactionData,
160     permissionChecks = true
161     ): Promise<{
162     aborted: boolean;
163     reactionRole?: ReactionRole;
164     member?: GuildMember;
165     removedPreviousRoles?: boolean;
166     }> {
167     const emoji = data.d.emoji;
168     const userId = data.d.user_id;
169     const messageId = data.d.message_id;
170     const channelId = data.d.channel_id;
171     const guildId = data.d.guild_id;
172     const isReactionAddEvent = data.t === "MESSAGE_REACTION_ADD";
173    
174     if (userId === this.client.user?.id) {
175     return { aborted: true };
176     }
177    
178     if (!guildId) {
179     return { aborted: true };
180     }
181    
182     const config = this.client.configManager.config[guildId]?.reaction_roles;
183    
184     if (!config?.enabled || (config.ignore_bots && data.d.member?.user?.bot)) {
185     return { aborted: true };
186     }
187    
188     const entry = this.reactionRoleEntries.get(`${guildId}_${channelId}_${messageId!}_${emoji.id ?? emoji.name}`);
189    
190     if (!entry) {
191     log("Reaction role entry not found, ignoring");
192     return { aborted: true };
193     }
194    
195     if (entry.roles.length === 0) {
196     log("No role to add/remove");
197     return { aborted: true };
198     }
199    
200     const guild = this.client.guilds.cache.get(guildId);
201    
202     if (!guild) {
203     return { aborted: true };
204     }
205    
206     const member = await safeMemberFetch(guild, userId);
207    
208     if (!member) {
209     return { aborted: true };
210     }
211    
212     if (config.ignore_bots && member.user.bot) {
213     return { aborted: true };
214     }
215    
216     if (permissionChecks) {
217     if (!member.roles.cache.hasAll(...entry.requiredRoles)) {
218     log("Member does not have the required roles");
219     return await this.removeReactionAndAbort(data);
220     }
221    
222     if (!member.permissions.has("Administrator") && entry.blacklistedUsers.includes(member.user.id)) {
223     log("User is blacklisted");
224     return await this.removeReactionAndAbort(data);
225     }
226    
227     if (!member.permissions.has(entry.requiredPermissions as PermissionsString[], true)) {
228     log("Member does not have the required permissions");
229     return await this.removeReactionAndAbort(data);
230     }
231    
232     if (this.client.permissionManager.usesLevelBasedMode(member.guild.id) && entry.level) {
233     const level = (await this.client.permissionManager.getManager(member.guild.id)).getPermissionLevel(member);
234    
235     if (level < entry.level) {
236     log("Member does not have the required permission level");
237     return await this.removeReactionAndAbort(data);
238     }
239     }
240     }
241    
242     let removedPreviousRoles = false;
243     const reactionsToRemove = [];
244    
245     if (entry?.single && isReactionAddEvent) {
246     for (const value of this.reactionRoleEntries.values()) {
247     if (
248     value?.guildId === guildId &&
249     value?.channelId === channelId &&
250     value?.messageId === messageId &&
251     member.roles.cache.hasAny(...value!.roles)
252     ) {
253     await member.roles.remove(value!.roles, "Taking out the previous roles").catch(logError);
254     removedPreviousRoles = !removedPreviousRoles ? true : removedPreviousRoles;
255    
256     if (reactionsToRemove.length <= 4) {
257     reactionsToRemove.push(`${value?.emoji}`);
258     }
259     }
260     }
261     }
262    
263     if (removedPreviousRoles && reactionsToRemove.length > 0 && reactionsToRemove.length <= 4) {
264     log(reactionsToRemove);
265    
266     for (const reaction of reactionsToRemove) {
267     const isBuiltIn = !/^\d+$/.test(reaction);
268     const emoji = !isBuiltIn
269     ? this.client.emojis.cache.find(e => e.id === reaction || e.identifier === reaction)
270     : null;
271    
272     if (!isBuiltIn && !emoji) {
273     continue;
274     }
275    
276     this.removeReactionAndAbort({
277     ...data,
278     d: {
279     ...data.d,
280     emoji: (isBuiltIn
281     ? reaction
282     : {
283     id: emoji!.id,
284     name: emoji!.name
285     }) as RawMessageReactionData["d"]["emoji"]
286     }
287     }).catch(logError);
288     }
289     }
290    
291     return { aborted: false, member, reactionRole: entry, removedPreviousRoles };
292     }
293    
294     async removeReactionAndAbort(data: RawMessageReactionData) {
295     await this.client.rest
296     .delete(
297     Routes.channelMessageUserReaction(
298     data.d.channel_id,
299     data.d.message_id,
300     data.d.emoji.id && data.d.emoji.name
301     ? `${data.d.emoji.name}:${data.d.emoji.id}`
302     : encodeURIComponent(data.d.emoji.name),
303     data.d.user_id
304     )
305     )
306     .catch(logError);
307    
308     return { aborted: true };
309     }
310    
311     async createReactionRole({
312     channelId,
313     messageId,
314     guildId,
315     emoji,
316     roles,
317     mode = "MULTIPLE"
318     }: {
319     channelId: Snowflake;
320     messageId: Snowflake;
321     guildId: Snowflake;
322     emoji: string;
323     roles: Snowflake[];
324     mode?: "SINGLE" | "MULTIPLE";
325     }) {
326     const reactionRole = await this.client.prisma.reactionRole.create({
327     data: {
328     channelId,
329     guildId,
330     messageId,
331     isBuiltInEmoji: !/^\d+$/.test(emoji),
332     emoji,
333     roles,
334     single: mode === "SINGLE"
335     }
336     });
337    
338     this.reactionRoleEntries.set(
339     `${reactionRole.guildId}_${reactionRole.channelId}_${reactionRole.messageId}_${reactionRole.emoji}`,
340     reactionRole
341     );
342    
343     return reactionRole;
344     }
345     }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26