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 { PermissionOverwrite } from "@prisma/client"; |
21 |
import { log } from "console"; |
22 |
import { GuildMember, PermissionFlagsBits, PermissionResolvable, PermissionsBitField, Role, Snowflake } from "discord.js"; |
23 |
import Service from "../core/Service"; |
24 |
import AbstractPermissionManager from "../utils/AbstractPermissionManager"; |
25 |
import DiscordBasedPermissionManager from "../utils/DiscordBasedPermissionManager"; |
26 |
import LayerBasedPermissionManager from "../utils/LayerBasedPermissionManager"; |
27 |
import LevelBasedPermissionManager from "../utils/LevelBasedPermissionManager"; |
28 |
import { logInfo } from "../utils/logger"; |
29 |
import { GuildConfig } from "./ConfigManager"; |
30 |
|
31 |
export const name = "permissionManager"; |
32 |
|
33 |
export type GetMemberPermissionInGuildResult = { |
34 |
permissions: PermissionsBitField; |
35 |
} & ( |
36 |
| { |
37 |
type: "levels"; |
38 |
level: number; |
39 |
} |
40 |
| { |
41 |
type: "discord"; |
42 |
} |
43 |
| { |
44 |
type: "layered"; |
45 |
highestOverwrite?: PermissionOverwrite; |
46 |
highestRoleHavingOverwrite?: Role; |
47 |
overwriteIds: number[]; |
48 |
} |
49 |
); |
50 |
|
51 |
export default class PermissionManager<M extends AbstractPermissionManager = AbstractPermissionManager> extends Service { |
52 |
protected readonly cache: Record<`${Snowflake}_${Snowflake}`, object> = {}; |
53 |
protected readonly managers: Record<NonNullable<GuildConfig["permissions"]["mode"]>, AbstractPermissionManager | undefined> = |
54 |
{ |
55 |
layered: new LayerBasedPermissionManager(this.client), |
56 |
discord: new DiscordBasedPermissionManager(this.client), |
57 |
levels: new LevelBasedPermissionManager(this.client) |
58 |
}; |
59 |
|
60 |
boot() { |
61 |
if (!this.client.configManager.systemConfig.sync_permission_managers_on_boot) { |
62 |
return; |
63 |
} |
64 |
|
65 |
for (const managerName in this.managers) { |
66 |
this.managers[managerName as keyof typeof this.managers]?.sync?.(); |
67 |
logInfo(`[${this.constructor.name}] Synchronizing ${managerName} permission manager`); |
68 |
} |
69 |
} |
70 |
|
71 |
async isImmuneToAutoMod(member: GuildMember, permission?: PermissionResolvable[] | PermissionResolvable) { |
72 |
if (this.client.configManager.systemConfig.system_admins.includes(member.user.id)) return true; |
73 |
|
74 |
const config = this.client.configManager.config[member.guild.id]; |
75 |
|
76 |
if (!config) return true; |
77 |
|
78 |
const { admin_role, mod_role, staff_role, check_discord_permissions, invincible_roles } = config.permissions ?? {}; |
79 |
|
80 |
if (member.roles.cache.hasAny(admin_role ?? "_", mod_role ?? "_", staff_role ?? "_")) { |
81 |
log("Member has roles that are immune to automod"); |
82 |
return true; |
83 |
} |
84 |
|
85 |
for (const roleId of invincible_roles) { |
86 |
if (member.roles.cache.has(roleId)) { |
87 |
log("Member has roles that are immune to this action"); |
88 |
return false; |
89 |
} |
90 |
} |
91 |
|
92 |
if (check_discord_permissions === "automod" || check_discord_permissions === "both") { |
93 |
const hasDiscordPerms = |
94 |
member.permissions.has(PermissionFlagsBits.ManageGuild, true) || |
95 |
(permission && member.permissions.has(permission, true)); |
96 |
|
97 |
if (hasDiscordPerms) { |
98 |
log("Member has discord permissions that are immune to automod"); |
99 |
return true; |
100 |
} |
101 |
} |
102 |
|
103 |
const manager = await this.getManager(member.guild.id); |
104 |
const { permissions } = manager.getMemberPermissions(member); |
105 |
const hasPermissions = permissions.has("Administrator") || permissions.has("ManageGuild"); |
106 |
|
107 |
if (hasPermissions) { |
108 |
log("Member has permissions that are immune to automod"); |
109 |
return true; |
110 |
} |
111 |
|
112 |
return manager.isImmuneToAutoMod(member, permission); |
113 |
} |
114 |
|
115 |
// FIXME: Move these permission checks to DiscordBasedPermissionManager |
116 |
async shouldModerate(member: GuildMember, moderator: GuildMember) { |
117 |
if (this.client.configManager.systemConfig.system_admins.includes(moderator.user.id)) return true; |
118 |
|
119 |
const config = this.client.configManager.config[member.guild.id]; |
120 |
|
121 |
if (!config) return false; |
122 |
|
123 |
if (member.guild.ownerId === member.user.id) return false; |
124 |
if (member.guild.ownerId === moderator.user.id) return true; |
125 |
|
126 |
const { admin_role, mod_role, staff_role, invincible_roles, check_discord_permissions } = config.permissions ?? {}; |
127 |
|
128 |
if (member.roles.cache.hasAny(admin_role ?? "_", mod_role ?? "_", staff_role ?? "_")) { |
129 |
log("Member has roles that are immune to this action"); |
130 |
return false; |
131 |
} |
132 |
|
133 |
for (const roleId of invincible_roles) { |
134 |
if (member.roles.cache.has(roleId)) { |
135 |
log("Member has roles that are immune to this action"); |
136 |
return false; |
137 |
} |
138 |
} |
139 |
|
140 |
if ( |
141 |
(check_discord_permissions === "manual_actions" || check_discord_permissions === "both") && |
142 |
member.roles.highest.position >= moderator.roles.highest.position |
143 |
) { |
144 |
log("Member has higher/equal roles than moderator"); |
145 |
return false; |
146 |
} |
147 |
|
148 |
const manager = await this.getManager(member.guild.id); |
149 |
return await manager.shouldModerate(member, moderator); |
150 |
} |
151 |
|
152 |
protected getMode(guildId: string) { |
153 |
return this.client.configManager.config[guildId]?.permissions.mode ?? "discord"; |
154 |
} |
155 |
|
156 |
async getManager(guildId: string): Promise<M> { |
157 |
const manager = this.managers[this.getMode(guildId)]; |
158 |
|
159 |
if (!manager) { |
160 |
throw new Error("Unknown/Unsupported permission mode: " + this.getMode(guildId)); |
161 |
} |
162 |
|
163 |
await manager.triggerSyncIfNeeded(); |
164 |
return manager as M; |
165 |
} |
166 |
|
167 |
async getMemberPermissions(member: GuildMember, mergeWithDiscordPermissions?: boolean) { |
168 |
console.log("before Getting member permissions"); |
169 |
const manager = await this.getManager(member.guild.id); |
170 |
console.log("Getting member permissions"); |
171 |
return manager.getMemberPermissions(member, mergeWithDiscordPermissions); |
172 |
} |
173 |
|
174 |
usesLayeredMode(guildId: string) { |
175 |
return this.getMode(guildId) === "layered"; |
176 |
} |
177 |
|
178 |
usesLevelBasedMode(guildId: string): this is PermissionManager<LevelBasedPermissionManager> { |
179 |
return this.getMode(guildId) === "levels"; |
180 |
} |
181 |
|
182 |
usesDiscordMode(guildId: string) { |
183 |
return this.getMode(guildId) === "discord"; |
184 |
} |
185 |
} |