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

Contents of /branches/6.x/src/services/ReactionRoleService.ts

Parent Directory Parent Directory | Revision Log Revision Log


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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26