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