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 { 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 |
} |