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

Contents of /branches/5.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: 9468 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 export default class ReactionRoleService extends Service implements HasEventListeners {
31 readonly reactionRoleEntries = new Map<string, ReactionRole | undefined>();
32 readonly usersInProgress = new Set<Snowflake>();
33
34 @GatewayEventListener("ready")
35 async onReady(client: Client<true>) {
36 log("Syncing reaction roles...");
37
38 const reactionRoles = await this.client.prisma.reactionRole.findMany({
39 where: {
40 guildId: {
41 in: [...client.guilds.cache.keys()]
42 }
43 }
44 });
45
46 for (const reactionRole of reactionRoles) {
47 this.reactionRoleEntries.set(
48 `${reactionRole.guildId}_${reactionRole.channelId}_${reactionRole.messageId}_${reactionRole.emoji}`,
49 reactionRole
50 );
51 }
52
53 log("Successfully synced reaction roles");
54 }
55
56 @GatewayEventListener("raw")
57 async onRaw(data: any) {
58 if (data.t !== "MESSAGE_REACTION_ADD" && data.t !== "MESSAGE_REACTION_REMOVE") {
59 return;
60 }
61
62 log(JSON.stringify(data, null, 2));
63
64 if (this.usersInProgress.has(data.d.user_id)) {
65 log("The user has hit a ratelimit.");
66 return;
67 }
68
69 this.usersInProgress.add(data.d.user_id);
70 const { aborted, member, reactionRole } = await this.processRequest(data, data.t === "MESSAGE_REACTION_ADD");
71
72 if (aborted) {
73 log("Request aborted");
74 setTimeout(() => this.usersInProgress.delete(data.d.user_id), 1500);
75 return;
76 }
77
78 try {
79 if (data.t === "MESSAGE_REACTION_ADD") {
80 await member?.roles.add(reactionRole!.roles, "Adding reaction roles");
81 } else {
82 await member?.roles.remove(reactionRole!.roles, "Removing reaction roles");
83 }
84 } catch (e) {
85 logError(e);
86 }
87
88 setTimeout(() => this.usersInProgress.delete(data.d.user_id), 1500);
89 }
90
91 async processRequest(
92 data: any,
93 permissionChecks = true
94 ): Promise<{
95 aborted: boolean;
96 reactionRole?: ReactionRole;
97 member?: GuildMember;
98 removedPreviousRoles?: boolean;
99 }> {
100 const emoji = data.d.emoji;
101 const userId = data.d.user_id;
102 const messageId = data.d.message_id;
103 const channelId = data.d.channel_id;
104 const guildId = data.d.guild_id;
105 const isReactionAddEvent = data.t === "MESSAGE_REACTION_ADD";
106
107 if (userId === this.client.user?.id) {
108 return { aborted: true };
109 }
110
111 if (!guildId) {
112 return { aborted: true };
113 }
114
115 const config = this.client.configManager.config[guildId]?.reaction_roles;
116 const usesLevels = this.client.configManager.config[guildId]?.permissions.mode === "levels";
117
118 if (!config?.enabled || (config.ignore_bots && data.d.member?.user?.bot)) {
119 return { aborted: true };
120 }
121
122 const entry = this.reactionRoleEntries.get(`${guildId}_${channelId}_${messageId!}_${emoji.id ?? emoji.name}`);
123
124 if (!entry) {
125 log("Reaction role entry not found, ignoring");
126 return { aborted: true };
127 }
128
129 if (entry.roles.length === 0) {
130 log("No role to add/remove");
131 return { aborted: true };
132 }
133
134 const guild = this.client.guilds.cache.get(guildId);
135
136 if (!guild) {
137 return { aborted: true };
138 }
139
140 const member = await safeMemberFetch(guild, userId);
141
142 if (!member) {
143 return { aborted: true };
144 }
145
146 if (config.ignore_bots && member.user.bot) {
147 return { aborted: true };
148 }
149
150 if (permissionChecks) {
151 if (!member.roles.cache.hasAll(...entry.requiredRoles)) {
152 log("Member does not have the required roles");
153 return await this.removeReactionAndAbort(data);
154 }
155
156 if (!member.permissions.has("Administrator") && entry.blacklistedUsers.includes(member.user.id)) {
157 log("User is blacklisted");
158 return await this.removeReactionAndAbort(data);
159 }
160
161 if (!member.permissions.has(entry.requiredPermissions as PermissionsString[], true)) {
162 log("Member does not have the required permissions");
163 return await this.removeReactionAndAbort(data);
164 }
165
166 if (usesLevels && entry.level) {
167 const level = this.client.permissionManager.getMemberPermissionLevel(member);
168
169 if (level < entry.level) {
170 log("Member does not have the required permission level");
171 return await this.removeReactionAndAbort(data);
172 }
173 }
174 }
175
176 let removedPreviousRoles = false;
177 const reactionsToRemove = [];
178
179 if (entry?.single && isReactionAddEvent) {
180 for (const value of this.reactionRoleEntries.values()) {
181 if (
182 value?.guildId === guildId &&
183 value?.channelId === channelId &&
184 value?.messageId === messageId &&
185 member.roles.cache.hasAny(...value!.roles)
186 ) {
187 await member.roles.remove(value!.roles, "Taking out the previous roles").catch(logError);
188 removedPreviousRoles = !removedPreviousRoles ? true : removedPreviousRoles;
189
190 if (reactionsToRemove.length <= 4) {
191 reactionsToRemove.push(`${value?.emoji}`);
192 }
193 }
194 }
195 }
196
197 if (removedPreviousRoles && reactionsToRemove.length > 0 && reactionsToRemove.length <= 4) {
198 log(reactionsToRemove);
199
200 for (const reaction of reactionsToRemove) {
201 const isBuiltIn = !/^\d+$/.test(reaction);
202 const emoji = !isBuiltIn
203 ? this.client.emojis.cache.find(e => e.id === reaction || e.identifier === reaction)
204 : null;
205
206 if (!isBuiltIn && !emoji) {
207 continue;
208 }
209
210 this.removeReactionAndAbort({
211 ...data,
212 d: {
213 ...data.d,
214 emoji: isBuiltIn
215 ? reaction
216 : {
217 id: emoji!.id,
218 name: emoji!.name
219 }
220 }
221 }).catch(logError);
222 }
223 }
224
225 return { aborted: false, member, reactionRole: entry, removedPreviousRoles };
226 }
227
228 async removeReactionAndAbort(data: any) {
229 await this.client.rest
230 .delete(
231 Routes.channelMessageUserReaction(
232 data.d.channel_id,
233 data.d.message_id,
234 data.d.emoji.id && data.d.emoji.name
235 ? `${data.d.emoji.name}:${data.d.emoji.id}`
236 : encodeURIComponent(data.d.emoji.name),
237 data.d.user_id
238 )
239 )
240 .catch(logError);
241
242 return { aborted: true };
243 }
244
245 async createReactionRole({
246 channelId,
247 messageId,
248 guildId,
249 emoji,
250 roles,
251 mode = "MULTIPLE"
252 }: {
253 channelId: Snowflake;
254 messageId: Snowflake;
255 guildId: Snowflake;
256 emoji: string;
257 roles: Snowflake[];
258 mode?: "SINGLE" | "MULTIPLE";
259 }) {
260 const reactionRole = await this.client.prisma.reactionRole.create({
261 data: {
262 channelId,
263 guildId,
264 messageId,
265 isBuiltInEmoji: !/^\d+$/.test(emoji),
266 emoji,
267 roles,
268 single: mode === "SINGLE"
269 }
270 });
271
272 this.reactionRoleEntries.set(
273 `${reactionRole.guildId}_${reactionRole.channelId}_${reactionRole.messageId}_${reactionRole.emoji}`,
274 reactionRole
275 );
276
277 return reactionRole;
278 }
279 }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26