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 { |
21 |
APIEmbedField, |
22 |
Channel, |
23 |
ChannelType, |
24 |
ColorResolvable, |
25 |
ComponentEmojiResolvable, |
26 |
EmbedBuilder, |
27 |
GuildEmoji, |
28 |
GuildMember, |
29 |
NewsChannel, |
30 |
PermissionOverwrites, |
31 |
PermissionResolvable, |
32 |
PermissionsBitField, |
33 |
TextBasedChannel, |
34 |
TextChannel, |
35 |
ThreadChannel, |
36 |
User, |
37 |
escapeMarkdown |
38 |
} from "discord.js"; |
39 |
import { mkdirSync } from "fs"; |
40 |
import path from "path"; |
41 |
import Client from "../core/Client"; |
42 |
import { ActionDoneName } from "../services/InfractionManager"; |
43 |
import { userInfo } from "./embed"; |
44 |
|
45 |
export function isSnowflake(input: string) { |
46 |
return /^\d{16,22}$/.test(input); |
47 |
} |
48 |
|
49 |
export function pick<T, K extends Array<keyof T>>(object: T, keys: K): Pick<T, K extends Array<infer E> ? E : never> { |
50 |
if (typeof object === "object" && object !== null) { |
51 |
const picked: Partial<T> = {}; |
52 |
|
53 |
for (const key of keys) { |
54 |
picked[key] = object[key]; |
55 |
} |
56 |
|
57 |
return picked as Pick<T, K extends Array<infer E> ? E : never>; |
58 |
} |
59 |
|
60 |
return {} as Pick<T, K extends Array<infer E> ? E : never>; |
61 |
} |
62 |
|
63 |
export interface CreateModerationEmbedOptions { |
64 |
user: User; |
65 |
moderator: User; |
66 |
actionDoneName?: ActionDoneName; |
67 |
reason?: string; |
68 |
description?: string; |
69 |
fields?: |
70 |
| APIEmbedField[] |
71 |
| ((fields: APIEmbedField[], id: string, reason?: string) => Promise<APIEmbedField[]> | APIEmbedField[]); |
72 |
id: string | number; |
73 |
color?: ColorResolvable; |
74 |
} |
75 |
|
76 |
export async function createModerationEmbed({ |
77 |
user, |
78 |
actionDoneName, |
79 |
reason, |
80 |
description, |
81 |
fields, |
82 |
id, |
83 |
color = 0xf14a60, |
84 |
moderator |
85 |
}: CreateModerationEmbedOptions) { |
86 |
return new EmbedBuilder({ |
87 |
author: { |
88 |
name: user.tag, |
89 |
icon_url: user.displayAvatarURL() |
90 |
}, |
91 |
description: description ?? `**${escapeMarkdown(user.tag)}** has been ${actionDoneName}.`, |
92 |
fields: |
93 |
typeof fields === "function" |
94 |
? await fields( |
95 |
[ |
96 |
{ |
97 |
name: "Reason", |
98 |
value: reason ?? "*No reason provided*" |
99 |
}, |
100 |
{ |
101 |
name: "User", |
102 |
value: userInfo(user) |
103 |
}, |
104 |
{ |
105 |
name: "Moderator", |
106 |
value: userInfo(moderator) |
107 |
}, |
108 |
{ |
109 |
name: "Infraction ID", |
110 |
value: `${id}` |
111 |
} |
112 |
], |
113 |
`${id}`, |
114 |
reason |
115 |
) |
116 |
: [ |
117 |
{ |
118 |
name: "Reason", |
119 |
value: reason ?? "*No reason provided*" |
120 |
}, |
121 |
{ |
122 |
name: "User", |
123 |
value: userInfo(user) |
124 |
}, |
125 |
{ |
126 |
name: "Moderator", |
127 |
value: userInfo(moderator) |
128 |
}, |
129 |
...(fields ?? []), |
130 |
{ |
131 |
name: "Infraction ID", |
132 |
value: `${id}` |
133 |
} |
134 |
], |
135 |
footer: actionDoneName |
136 |
? { |
137 |
text: actionDoneName[0].toUpperCase() + actionDoneName.substring(1) |
138 |
} |
139 |
: undefined |
140 |
}) |
141 |
.setTimestamp() |
142 |
.setColor(color); |
143 |
} |
144 |
|
145 |
export function getEmoji<B extends boolean = false>( |
146 |
client: Client, |
147 |
name: string, |
148 |
returnNull: B = false as B |
149 |
): B extends false ? string : string | null { |
150 |
return ( |
151 |
client.guilds.cache |
152 |
.get(process.env.HOME_GUILD_ID!) |
153 |
?.emojis.cache.find(e => e.name === name) |
154 |
?.toString() ?? |
155 |
client.configManager.systemConfig.emojis?.[name] ?? |
156 |
client.emojiMap.get(name)?.toString() ?? |
157 |
(returnNull ? null : "") |
158 |
); |
159 |
} |
160 |
|
161 |
export function getEmojiObject(client: Client, name: string): GuildEmoji | null { |
162 |
return ( |
163 |
client.guilds.cache.get(process.env.HOME_GUILD_ID!)?.emojis.cache.find(e => e.name === name) ?? |
164 |
client.emojiMap.get(name) ?? |
165 |
null |
166 |
); |
167 |
} |
168 |
|
169 |
export function getComponentEmojiResolvable(client: Client, name: string): ComponentEmojiResolvable | null { |
170 |
const emoji = getEmojiObject(client, name); |
171 |
|
172 |
return emoji |
173 |
? { |
174 |
id: emoji?.id, |
175 |
name: emoji?.name ?? emoji?.identifier, |
176 |
animated: emoji?.animated ?? false |
177 |
} |
178 |
: null; |
179 |
} |
180 |
|
181 |
export function isTextableChannel( |
182 |
channel: Channel | ThreadChannel, |
183 |
DMs = false |
184 |
): channel is TextChannel | NewsChannel | ThreadChannel { |
185 |
return [ |
186 |
...(DMs ? [ChannelType.DM, ChannelType.GroupDM] : []), |
187 |
ChannelType.GuildAnnouncement, |
188 |
ChannelType.GuildText, |
189 |
ChannelType.PrivateThread, |
190 |
ChannelType.PublicThread, |
191 |
ChannelType.GuildVoice |
192 |
].includes(channel.type); |
193 |
} |
194 |
|
195 |
export function developmentMode() { |
196 |
return ( |
197 |
["dev", "development"].includes(process.env.NODE_ENV?.toLowerCase()!) || |
198 |
["dev", "development"].includes(process.env.SUDO_ENV?.toLowerCase()!) |
199 |
); |
200 |
} |
201 |
|
202 |
export function isImmuneToAutoMod( |
203 |
client: Client, |
204 |
member: GuildMember, |
205 |
permission?: PermissionResolvable[] | PermissionResolvable |
206 |
) { |
207 |
return client.permissionManager.isImmuneToAutoMod(member, permission); |
208 |
} |
209 |
|
210 |
export function wait(time: number) { |
211 |
return new Promise(resolve => setTimeout(resolve, time)); |
212 |
} |
213 |
|
214 |
export function sudoPrefix(pathLike: string, createDirIfNotExists = false) { |
215 |
const directoryOrFile = path.resolve(process.env.SUDO_PREFIX ?? __dirname, process.env.SUDO_PREFIX ? "" : "../..", pathLike); |
216 |
|
217 |
if (createDirIfNotExists) mkdirSync(directoryOrFile, { recursive: true }); |
218 |
|
219 |
return directoryOrFile; |
220 |
} |
221 |
|
222 |
export function getPermissionNames(permissionsBit: bigint) { |
223 |
const result = []; |
224 |
const permissions = new PermissionsBitField(permissionsBit); |
225 |
|
226 |
for (const permission of Object.keys(PermissionsBitField.Flags) as (keyof typeof PermissionsBitField.Flags)[]) { |
227 |
if (permissions.has(PermissionsBitField.Flags[permission])) { |
228 |
result.push(permission); |
229 |
} |
230 |
} |
231 |
|
232 |
return result; |
233 |
} |
234 |
|
235 |
export function forceGetPermissionNames(permissions: PermissionResolvable[]) { |
236 |
const strings: string[] = []; |
237 |
|
238 |
for (const permission of permissions) { |
239 |
if (typeof permission === "bigint") { |
240 |
strings.push(...getPermissionNames(permission)); |
241 |
} else if (typeof permission === "string") { |
242 |
strings.push(permission); |
243 |
} else throw new Error("Unknown permission type"); |
244 |
} |
245 |
|
246 |
return strings; |
247 |
} |
248 |
|
249 |
export function getChannelPermissionOverride(permission: PermissionResolvable, permissionOverwrites: PermissionOverwrites) { |
250 |
return permissionOverwrites.allow.has(permission, true) |
251 |
? true |
252 |
: permissionOverwrites.deny.has(permission, true) |
253 |
? true |
254 |
: null; |
255 |
} |
256 |
|
257 |
export function chunkedString(str: string, chunkSize = 4000) { |
258 |
let output = []; |
259 |
let chunk = ""; |
260 |
|
261 |
for (let i = 0; i < str.length; i++) { |
262 |
if (i !== 0 && i % chunkSize === 0) { |
263 |
output.push(chunk); |
264 |
chunk = str[i]; |
265 |
continue; |
266 |
} |
267 |
|
268 |
chunk += str[i]; |
269 |
} |
270 |
|
271 |
if (chunk.length !== 0) { |
272 |
output.push(chunk); |
273 |
} |
274 |
|
275 |
return output; |
276 |
} |
277 |
|
278 |
export function escapeRegex(string: string) { |
279 |
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, "\\$&"); |
280 |
} |
281 |
|
282 |
export function safeMessageContent(content: string, member: GuildMember, channel?: TextBasedChannel) { |
283 |
return member.permissions.has("MentionEveryone") && |
284 |
(!channel || member.permissionsIn(channel as TextChannel).has("MentionEveryone")) |
285 |
? content |
286 |
: content |
287 |
.replaceAll(/@everyone/gi, "`@everyone`") |
288 |
.replaceAll(/@here/gi, "`@here`") |
289 |
.replaceAll(`<@&${member.guild.id}>`, "`@everyone`") |
290 |
.replace(/<@&(\d+)>/gim, (_, id) => `@${member.guild.roles.cache.get(id)?.name ?? id}`); |
291 |
} |
292 |
|
293 |
export function isSystemAdmin(client: Client, userId: string) { |
294 |
return client.configManager.systemConfig.system_admins.includes(userId); |
295 |
} |