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