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 { PermissionRole } from "@prisma/client"; |
21 |
|
|
import { GuildMember, PermissionFlagsBits, PermissionResolvable, PermissionsString, Snowflake } from "discord.js"; |
22 |
|
|
import Service from "../core/Service"; |
23 |
|
|
import { GatewayEventListener } from "../decorators/GatewayEventListener"; |
24 |
|
|
import { HasEventListeners } from "../types/HasEventListeners"; |
25 |
|
|
import { PermissionLevelsRecord, getPermissionLevels } from "../utils/PermissionLevels"; |
26 |
|
|
import { log, logWarn } from "../utils/logger"; |
27 |
|
|
|
28 |
|
|
export const name = "permissionManager"; |
29 |
|
|
|
30 |
|
|
export default class PermissionManager extends Service implements HasEventListeners { |
31 |
|
|
public readonly users: Record<string, Map<string, PermissionRole> | undefined> = {}; |
32 |
|
|
public readonly roles: Record<string, Map<string, PermissionRole> | undefined> = {}; |
33 |
|
|
public readonly levels: PermissionLevelsRecord = getPermissionLevels(); |
34 |
|
|
public readonly guildPermissionLevels: Record<string, number | undefined> = {}; |
35 |
|
|
|
36 |
|
|
@GatewayEventListener("ready") |
37 |
|
|
async onReady() { |
38 |
|
|
await this.sync(); |
39 |
|
|
} |
40 |
|
|
|
41 |
|
|
shouldModerate(member: GuildMember, moderator: GuildMember) { |
42 |
|
|
if (this.client.configManager.systemConfig.system_admins.includes(moderator.user.id)) return true; |
43 |
|
|
|
44 |
|
|
const config = this.client.configManager.config[member.guild.id]; |
45 |
|
|
|
46 |
|
|
if (!config) return false; |
47 |
|
|
|
48 |
|
|
if (member.guild.ownerId === member.user.id) return false; |
49 |
|
|
if (member.guild.ownerId === moderator.user.id) return true; |
50 |
|
|
|
51 |
|
|
const { admin_role, mod_role, staff_role } = config.permissions ?? {}; |
52 |
|
|
|
53 |
|
|
if (member.roles.cache.hasAny(admin_role ?? "_", mod_role ?? "_", staff_role ?? "_")) { |
54 |
|
|
log("Member has roles that are immune to this action"); |
55 |
|
|
return false; |
56 |
|
|
} |
57 |
|
|
|
58 |
|
|
if (member.roles.highest.position >= moderator.roles.highest.position) { |
59 |
|
|
log("Member has higher roles than moderator"); |
60 |
|
|
return false; |
61 |
|
|
} |
62 |
|
|
|
63 |
|
|
return true; |
64 |
|
|
} |
65 |
|
|
|
66 |
|
|
isImmuneToAutoMod(member: GuildMember, permission?: PermissionResolvable[] | PermissionResolvable) { |
67 |
|
|
if (this.client.configManager.systemConfig.system_admins.includes(member.user.id)) return true; |
68 |
|
|
|
69 |
|
|
const config = this.client.configManager.config[member.guild.id]; |
70 |
|
|
|
71 |
|
|
if (!config) return true; |
72 |
|
|
|
73 |
|
|
const { admin_role, mod_role, staff_role } = config.permissions ?? {}; |
74 |
|
|
|
75 |
|
|
if (member.roles.cache.hasAny(admin_role ?? "_", mod_role ?? "_", staff_role ?? "_")) { |
76 |
|
|
log("Member has roles that are immune to automod"); |
77 |
|
|
return true; |
78 |
|
|
} |
79 |
|
|
|
80 |
|
|
const hasDiscordPerms = |
81 |
|
|
member.permissions.has(PermissionFlagsBits.ManageGuild, true) || (permission && member.permissions.has(permission, true)); |
82 |
|
|
|
83 |
|
|
if (hasDiscordPerms) { |
84 |
|
|
log("Member has discord permissions that are immune to automod"); |
85 |
|
|
return true; |
86 |
|
|
} |
87 |
|
|
|
88 |
|
|
const permissions = this.getMemberPermissions(member, false); |
89 |
|
|
const hasPermissions = permissions.has("Administrator") || permissions.has("ManageGuild"); |
90 |
|
|
|
91 |
|
|
if (hasPermissions) log("Member has permissions that are immune to automod"); |
92 |
|
|
|
93 |
|
|
return hasPermissions; |
94 |
|
|
} |
95 |
|
|
|
96 |
|
|
getMemberPermissionFromLevel(member: GuildMember, mergeWithDiscordPermissions?: boolean) { |
97 |
|
|
const level = this.getMemberPermissionLevel(member); |
98 |
|
|
|
99 |
|
|
return new Set<PermissionsString>([...(mergeWithDiscordPermissions ? member.permissions.toArray() : []), ...this.levels[level]]); |
100 |
|
|
} |
101 |
|
|
|
102 |
|
|
getMemberPermissionLevel(member: GuildMember) { |
103 |
|
|
let level = this.guildPermissionLevels[`${member.guild.id}_u_${member.user.id}`] ?? 0; |
104 |
|
|
|
105 |
|
|
for (const [id] of member.roles.cache) { |
106 |
|
|
const roleLevel = this.guildPermissionLevels[`${member.guild.id}_r_${id}`] ?? 0; |
107 |
|
|
|
108 |
|
|
if (roleLevel > level) { |
109 |
|
|
level = roleLevel; |
110 |
|
|
} |
111 |
|
|
} |
112 |
|
|
|
113 |
|
|
return level < 0 ? 0 : level > 100 ? 100 : level; |
114 |
|
|
} |
115 |
|
|
|
116 |
|
|
getPermissionsFromPermissionRole(permissionRole: PermissionRole, guildId: string) { |
117 |
|
|
const guildUsesPermissionLevels = this.client.configManager.config[guildId]?.permissions.mode === "levels"; |
118 |
|
|
|
119 |
|
|
if (guildUsesPermissionLevels && permissionRole.level !== undefined && permissionRole.level !== null) { |
120 |
|
|
return this.levels[permissionRole.level]; |
121 |
|
|
} |
122 |
|
|
|
123 |
|
|
return permissionRole.grantedPermissions as readonly PermissionsString[]; |
124 |
|
|
} |
125 |
|
|
|
126 |
|
|
getPermissionsFromPermissionRoles(permissionRoles: PermissionRole[], guildId: string) { |
127 |
|
|
const guildUsesPermissionLevels = this.client.configManager.config[guildId]?.permissions.mode === "levels"; |
128 |
|
|
const permissionStrings = new Set<PermissionsString>(); |
129 |
|
|
|
130 |
|
|
for (const permissionRole of permissionRoles) { |
131 |
|
|
if (guildUsesPermissionLevels && permissionRole.level !== undefined && permissionRole.level !== null) { |
132 |
|
|
for (const permission of this.levels[permissionRole.level]) { |
133 |
|
|
permissionStrings.add(permission); |
134 |
|
|
} |
135 |
|
|
} else { |
136 |
|
|
for (const permission of permissionRole.grantedPermissions as readonly PermissionsString[]) { |
137 |
|
|
permissionStrings.add(permission); |
138 |
|
|
} |
139 |
|
|
} |
140 |
|
|
} |
141 |
|
|
|
142 |
|
|
return permissionStrings; |
143 |
|
|
} |
144 |
|
|
|
145 |
|
|
getPermissionsFromPermissionRoleSet(permissionRole: PermissionRole, guildId: string) { |
146 |
|
|
return new Set(this.getPermissionsFromPermissionRole(permissionRole, guildId)); |
147 |
|
|
} |
148 |
|
|
|
149 |
|
|
getMemberPermissions(member: GuildMember, mergeWithDiscordPermissions?: boolean) { |
150 |
|
|
const guildUsesPermissionLevels = this.client.configManager.config[member.guild.id]?.permissions.mode === "levels"; |
151 |
|
|
|
152 |
|
|
if (guildUsesPermissionLevels) { |
153 |
|
|
return this.getMemberPermissionFromLevel(member, mergeWithDiscordPermissions); |
154 |
|
|
} |
155 |
|
|
|
156 |
|
|
const allPermissions = new Set<PermissionsString>(mergeWithDiscordPermissions ? member.permissions.toArray() : []); |
157 |
|
|
|
158 |
|
|
if (this.users[member.guild.id]) { |
159 |
|
|
const permissions = this.users[`${member.guild.id}_${member.user.id}`]; |
160 |
|
|
|
161 |
|
|
if (permissions) { |
162 |
|
|
for (const permission of permissions.values()) { |
163 |
|
|
for (const key of permission.grantedPermissions) { |
164 |
|
|
allPermissions.add(key as PermissionsString); |
165 |
|
|
} |
166 |
|
|
} |
167 |
|
|
} |
168 |
|
|
} |
169 |
|
|
|
170 |
|
|
for (const [id] of member.roles.cache) { |
171 |
|
|
const permissions = this.roles[`${member.guild.id}_${id}`]; |
172 |
|
|
|
173 |
|
|
if (!permissions) { |
174 |
|
|
continue; |
175 |
|
|
} |
176 |
|
|
|
177 |
|
|
for (const permission of permissions.values()) { |
178 |
|
|
for (const key of permission.grantedPermissions) { |
179 |
|
|
allPermissions.add(key as PermissionsString); |
180 |
|
|
} |
181 |
|
|
} |
182 |
|
|
} |
183 |
|
|
|
184 |
|
|
return allPermissions; |
185 |
|
|
} |
186 |
|
|
|
187 |
|
|
async sync(guildId?: string) { |
188 |
|
|
log("Started syncing permission levels", guildId ? `for guild ${guildId}` : ""); |
189 |
|
|
|
190 |
|
|
const permissionRoles = await this.client.prisma.permissionRole.findMany({ |
191 |
|
|
where: { |
192 |
|
|
guild_id: guildId |
193 |
|
|
} |
194 |
|
|
}); |
195 |
|
|
|
196 |
|
|
for (const permissionRole of permissionRoles) { |
197 |
|
|
const guildUsesPermissionLevels = this.client.configManager.config[permissionRole.guild_id]?.permissions.mode === "levels"; |
198 |
|
|
|
199 |
|
|
for (const roleId of permissionRole.roles) { |
200 |
|
|
if (guildUsesPermissionLevels && typeof permissionRole.level === "number") { |
201 |
|
|
this.guildPermissionLevels[`${permissionRole.guild_id}_r_${roleId}`] = permissionRole.level; |
202 |
|
|
continue; |
203 |
|
|
} |
204 |
|
|
|
205 |
|
|
this.roles[`${permissionRole.guild_id}_${roleId}`] ??= new Map(); |
206 |
|
|
this.roles[`${permissionRole.guild_id}_${roleId}`]!.set(permissionRole.name, permissionRole); |
207 |
|
|
} |
208 |
|
|
|
209 |
|
|
for (const userId of permissionRole.users) { |
210 |
|
|
if (guildUsesPermissionLevels && typeof permissionRole.level === "number") { |
211 |
|
|
this.guildPermissionLevels[`${permissionRole.guild_id}_u_${userId}`] = permissionRole.level; |
212 |
|
|
continue; |
213 |
|
|
} |
214 |
|
|
|
215 |
|
|
this.users[`${permissionRole.guild_id}_${userId}`] ??= new Map(); |
216 |
|
|
this.users[`${permissionRole.guild_id}_${userId}`]!.set(permissionRole.name, permissionRole); |
217 |
|
|
} |
218 |
|
|
} |
219 |
|
|
|
220 |
|
|
log("Completed syncing permission levels", guildId ? `for guild ${guildId}` : ""); |
221 |
|
|
} |
222 |
|
|
|
223 |
|
|
async createPermissionLevel({ guildId, name, permissions, roles, users }: CreatePermissionLevelPayload) { |
224 |
|
|
const existingPermissionLevel = await this.client.prisma.permissionRole.findFirst({ |
225 |
|
|
where: { |
226 |
|
|
name, |
227 |
|
|
guild_id: guildId |
228 |
|
|
} |
229 |
|
|
}); |
230 |
|
|
|
231 |
|
|
if (existingPermissionLevel) { |
232 |
|
|
return null; |
233 |
|
|
} |
234 |
|
|
|
235 |
|
|
const permissionLevel = await this.client.prisma.permissionRole.create({ |
236 |
|
|
data: { |
237 |
|
|
name, |
238 |
|
|
guild_id: guildId, |
239 |
|
|
grantedPermissions: permissions, |
240 |
|
|
roles, |
241 |
|
|
users |
242 |
|
|
} |
243 |
|
|
}); |
244 |
|
|
|
245 |
|
|
for (const roleId of permissionLevel.roles) { |
246 |
|
|
this.roles[`${permissionLevel.guild_id}_${roleId}`] ??= new Map(); |
247 |
|
|
this.roles[`${permissionLevel.guild_id}_${roleId}`]!.set(permissionLevel.name, permissionLevel); |
248 |
|
|
} |
249 |
|
|
|
250 |
|
|
for (const userId of permissionLevel.users) { |
251 |
|
|
this.users[`${permissionLevel.guild_id}_${userId}`] ??= new Map(); |
252 |
|
|
this.users[`${permissionLevel.guild_id}_${userId}`]!.set(permissionLevel.name, permissionLevel); |
253 |
|
|
} |
254 |
|
|
|
255 |
|
|
return permissionLevel; |
256 |
|
|
} |
257 |
|
|
|
258 |
|
|
async deletePermissionLevel({ guildId, name }: Pick<CreatePermissionLevelPayload, "guildId" | "name">) { |
259 |
|
|
const permissionLevel = await this.client.prisma.permissionRole.findFirst({ |
260 |
|
|
where: { |
261 |
|
|
name, |
262 |
|
|
guild_id: guildId |
263 |
|
|
} |
264 |
|
|
}); |
265 |
|
|
|
266 |
|
|
if (!permissionLevel) return null; |
267 |
|
|
|
268 |
|
|
await this.client.prisma.permissionRole.delete({ |
269 |
|
|
where: { |
270 |
|
|
id: permissionLevel.id |
271 |
|
|
} |
272 |
|
|
}); |
273 |
|
|
|
274 |
|
|
for (const roleId of permissionLevel.roles) { |
275 |
|
|
this.roles[`${permissionLevel.guild_id}_${roleId}`] ??= new Map(); |
276 |
|
|
|
277 |
|
|
if (this.roles[`${permissionLevel.guild_id}_${roleId}`]?.has(permissionLevel.name)) { |
278 |
|
|
this.roles[`${permissionLevel.guild_id}_${roleId}`]!.delete(permissionLevel.name); |
279 |
|
|
} |
280 |
|
|
} |
281 |
|
|
|
282 |
|
|
for (const userId of permissionLevel.users) { |
283 |
|
|
this.users[`${permissionLevel.guild_id}_${userId}`] ??= new Map(); |
284 |
|
|
|
285 |
|
|
if (this.users[`${permissionLevel.guild_id}_${userId}`]?.has(permissionLevel.name)) { |
286 |
|
|
this.users[`${permissionLevel.guild_id}_${userId}`]!.delete(permissionLevel.name); |
287 |
|
|
} |
288 |
|
|
} |
289 |
|
|
|
290 |
|
|
return permissionLevel; |
291 |
|
|
} |
292 |
|
|
|
293 |
|
|
async updatePermissionLevel({ guildId, name, permissions, newName, roles, users }: CreatePermissionLevelPayload & { newName?: string }) { |
294 |
|
|
const existingPermissionLevel = await this.client.prisma.permissionRole.findFirst({ |
295 |
|
|
where: { |
296 |
|
|
name, |
297 |
|
|
guild_id: guildId |
298 |
|
|
} |
299 |
|
|
}); |
300 |
|
|
|
301 |
|
|
if (!existingPermissionLevel) { |
302 |
|
|
return null; |
303 |
|
|
} |
304 |
|
|
|
305 |
|
|
if (newName !== name || roles || users || permissions) { |
306 |
|
|
const permissionLevel = await this.client.prisma.permissionRole.update({ |
307 |
|
|
where: { |
308 |
|
|
id: existingPermissionLevel.id |
309 |
|
|
}, |
310 |
|
|
data: { |
311 |
|
|
name: newName, |
312 |
|
|
roles, |
313 |
|
|
users, |
314 |
|
|
grantedPermissions: permissions |
315 |
|
|
} |
316 |
|
|
}); |
317 |
|
|
|
318 |
|
|
for (const roleId of permissionLevel.roles) { |
319 |
|
|
this.roles[`${permissionLevel.guild_id}_${roleId}`] ??= new Map(); |
320 |
|
|
|
321 |
|
|
if (this.roles[`${permissionLevel.guild_id}_${roleId}`]!.has(name)) { |
322 |
|
|
this.roles[`${permissionLevel.guild_id}_${roleId}`]!.delete(name); |
323 |
|
|
} |
324 |
|
|
|
325 |
|
|
this.roles[`${permissionLevel.guild_id}_${roleId}`]!.set(permissionLevel.name, permissionLevel); |
326 |
|
|
} |
327 |
|
|
|
328 |
|
|
for (const userId of permissionLevel.users) { |
329 |
|
|
this.users[`${permissionLevel.guild_id}_${userId}`] ??= new Map(); |
330 |
|
|
|
331 |
|
|
if (this.users[`${permissionLevel.guild_id}_${userId}`]!.has(name)) { |
332 |
|
|
this.users[`${permissionLevel.guild_id}_${userId}`]!.delete(name); |
333 |
|
|
} |
334 |
|
|
|
335 |
|
|
this.users[`${permissionLevel.guild_id}_${userId}`]!.set(permissionLevel.name, permissionLevel); |
336 |
|
|
} |
337 |
|
|
|
338 |
|
|
return permissionLevel; |
339 |
|
|
} |
340 |
|
|
|
341 |
|
|
logWarn("Nothing updated"); |
342 |
|
|
return false; |
343 |
|
|
} |
344 |
|
|
} |
345 |
|
|
|
346 |
|
|
interface CreatePermissionLevelPayload { |
347 |
|
|
guildId: Snowflake; |
348 |
|
|
name: string; |
349 |
|
|
permissions: PermissionsString[]; |
350 |
|
|
roles?: Snowflake[]; |
351 |
|
|
users?: Snowflake[]; |
352 |
|
|
} |