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