/[sudobot]/branches/7.x/src/automod/VerificationService.ts
ViewVC logotype

Contents of /branches/7.x/src/automod/VerificationService.ts

Parent Directory Parent Directory | Revision Log Revision Log


Revision 577 - (show annotations)
Mon Jul 29 18:52:37 2024 UTC (8 months ago) by rakinar2
File MIME type: application/typescript
File size: 16044 byte(s)
chore: add old version archive branches (2.x to 9.x-dev)
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 { VerificationEntry } from "@prisma/client";
21 import bcrypt from "bcrypt";
22 import { formatDistanceToNowStrict } from "date-fns";
23 import {
24 APIEmbed,
25 ActionRowBuilder,
26 ButtonBuilder,
27 ButtonStyle,
28 Colors,
29 Guild,
30 GuildMember,
31 PartialGuildMember,
32 escapeMarkdown,
33 time
34 } from "discord.js";
35 import jwt from "jsonwebtoken";
36 import Service from "../core/Service";
37 import { HasEventListeners } from "../types/HasEventListeners";
38 import { userInfo } from "../utils/embed";
39 import { safeChannelFetch, safeMemberFetch } from "../utils/fetch";
40 import { logError } from "../utils/logger";
41
42 export const name = "verification";
43
44 export default class VerificationService extends Service implements HasEventListeners {
45 // FIXME: do not create doubled entries if the user leaves and rejoins
46 async onGuildMemberAdd(member: GuildMember) {
47 if (member.user.bot) {
48 return;
49 }
50
51 const config = this.client.configManager.config[member.guild.id]?.verification;
52
53 if (!config?.enabled || !this.requiresVerification(member)) {
54 return;
55 }
56
57 if (config.unverified_roles.length > 0) {
58 await member.roles.add(config.unverified_roles);
59 }
60
61 const { token } = await this.createDatabaseEntry(member);
62
63 try {
64 await this.sendVerificationDMToMember(member, token);
65 } catch (error) {
66 logError(error);
67
68 await this.sendLog(member.guild, {
69 author: {
70 name: member.user.username,
71 icon_url: member.user.displayAvatarURL()
72 },
73 title: "Failed to send verification DM",
74 color: Colors.Red,
75 description:
76 "This user will not be able to verify and will remain unverified unless an admin manually verifies them.",
77 fields: [
78 {
79 name: "User",
80 value: userInfo(member.user)
81 }
82 ],
83 footer: {
84 text: "Failed"
85 },
86 timestamp: new Date().toISOString()
87 });
88
89 return;
90 }
91
92 await this.sendLog(member.guild, {
93 author: {
94 name: member.user.username,
95 icon_url: member.user.displayAvatarURL()
96 },
97 title: "Verification Initiated",
98 color: Colors.Gold,
99 fields: [
100 {
101 name: "User",
102 value: userInfo(member.user)
103 }
104 ],
105 footer: {
106 text: "Initiated"
107 },
108 timestamp: new Date().toISOString()
109 });
110 }
111
112 async onGuildMemberRemove(member: GuildMember | PartialGuildMember) {
113 await this.client.prisma.verificationEntry.deleteMany({
114 where: {
115 userId: member.user.id,
116 guildId: member.guild.id
117 }
118 });
119 }
120
121 async onMemberVerificationFail(member: GuildMember, { attempts, guildId }: VerificationEntry, remainingTime: number) {
122 const config = this.client.configManager.config[guildId]?.verification!;
123
124 if ((config.max_attempts === 0 || attempts < config.max_attempts) && remainingTime > 0) {
125 return;
126 }
127
128 switch (config.action_on_fail?.type) {
129 case "ban":
130 if (member.bannable) {
131 await this.client.infractionManager.createUserBan(member.user, {
132 guild: member.guild,
133 moderator: this.client.user!,
134 autoRemoveQueue: true,
135 notifyUser: true,
136 sendLog: true,
137 reason: "Failed verification"
138 });
139 }
140
141 break;
142
143 case "kick":
144 if (member.kickable) {
145 await this.client.infractionManager.createMemberKick(member, {
146 guild: member.guild,
147 moderator: this.client.user!,
148 notifyUser: true,
149 sendLog: true,
150 reason: "Failed verification"
151 });
152 }
153
154 break;
155
156 case "mute":
157 if (member.manageable || member.moderatable) {
158 await this.client.infractionManager.createMemberMute(member, {
159 guild: member.guild,
160 moderator: this.client.user!,
161 notifyUser: true,
162 sendLog: true,
163 reason: "Failed verification",
164 autoRemoveQueue: true
165 });
166 }
167
168 break;
169
170 case "role":
171 if (member.manageable) {
172 const methodName = config.action_on_fail!.mode === "give" ? "add" : "remove";
173 await member.roles[methodName](config.action_on_fail!.roles).catch(logError);
174 }
175
176 break;
177 }
178 }
179
180 requiresVerification(member: GuildMember) {
181 const config = this.client.configManager.config[member.guild.id]?.verification;
182
183 return (
184 config?.parameters?.always ||
185 (typeof config?.parameters?.age_less_than === "number" &&
186 Date.now() - member.user.createdAt.getTime() < config?.parameters?.age_less_than) ||
187 (config?.parameters?.no_avatar && member.user.avatar === null)
188 );
189 }
190
191 async createDatabaseEntry(member: GuildMember) {
192 const config = this.client.configManager.config[member.guild.id]?.verification;
193
194 const seed = await bcrypt.hash((Math.random() * 100000000).toString(), await bcrypt.genSalt());
195 const token = jwt.sign(
196 {
197 seed,
198 userId: member.user.id
199 },
200 process.env.JWT_SECRET!,
201 {
202 expiresIn: config?.max_time === 0 ? undefined : config?.max_time,
203 issuer: `SudoBot`,
204 subject: "Verification Token"
205 }
206 );
207
208 return this.client.prisma.verificationEntry.create({
209 data: {
210 userId: member.user.id,
211 token,
212 guildId: member.guild.id
213 }
214 });
215 }
216
217 async sendLog(guild: Guild, embed: APIEmbed) {
218 const config = this.client.configManager.config[guild.id]?.verification;
219
220 if (!config?.logging.enabled) {
221 return;
222 }
223
224 const channelId = config.logging.channel ?? this.client.configManager.config[guild.id]?.logging?.primary_channel;
225
226 if (!channelId) {
227 return;
228 }
229
230 const channel = await safeChannelFetch(guild, channelId);
231
232 if (!channel || !channel.isTextBased()) {
233 return;
234 }
235
236 return channel
237 .send({
238 embeds: [embed]
239 })
240 .catch(logError);
241 }
242
243 sendVerificationDMToMember(member: GuildMember, token: string) {
244 const url = `${process.env.FRONTEND_URL}/challenge/verify?t=${encodeURIComponent(token)}&u=${member.id}&g=${
245 member.guild.id
246 }&n=${encodeURIComponent(member.guild.name)}`;
247
248 return member.send({
249 embeds: [
250 {
251 author: {
252 icon_url: member.guild.iconURL() ?? undefined,
253 name: "Verification Required"
254 },
255 color: Colors.Gold,
256 description: `
257 Hello **${escapeMarkdown(member.user.username)}**,\n
258 [${member.guild.name}](https://discord.com/channels/${
259 member.guild.id
260 }) requires you to verify to continue. Click on the button below to complete verification. Alternatively, you can copy-paste this link into your browser:\n
261 ${url}\n
262 You might be asked to solve a captcha.\n
263 Sincerely,
264 **The Staff of ${member.guild.name}**
265 `.replace(/(\r\n|\n)\t+/, "\n"),
266 footer: {
267 text: `You have ${formatDistanceToNowStrict(
268 Date.now() - (this.client.configManager.config[member.guild.id]?.verification?.max_time ?? 0)
269 )} to verify`
270 },
271 timestamp: new Date().toISOString()
272 }
273 ],
274 components: [
275 new ActionRowBuilder<ButtonBuilder>().addComponents(
276 new ButtonBuilder().setStyle(ButtonStyle.Link).setURL(url).setLabel("Verify")
277 )
278 ]
279 });
280 }
281
282 async sendVerificationSuccessDMToMember(member: GuildMember) {
283 return member.send({
284 embeds: [
285 {
286 author: {
287 icon_url: member.guild.iconURL() ?? undefined,
288 name: "Verification Completed"
289 },
290 color: Colors.Green,
291 description: `
292 Hello **${escapeMarkdown(member.user.username)}**,
293 You have successfully verified yourself. You've been granted access to the server now.\n
294 Cheers,
295 **The Staff of ${member.guild.name}**
296 `.replace(/(\r\n|\n)\t+/, "\n"),
297 footer: {
298 text: `Completed`
299 },
300 timestamp: new Date().toISOString()
301 }
302 ]
303 });
304 }
305
306 async attemptToVerifyUserByToken(userId: string, token: string, method: string) {
307 const entry = await this.client.prisma.verificationEntry.findFirst({
308 where: {
309 userId,
310 token
311 }
312 });
313
314 if (!entry) {
315 return null;
316 }
317
318 const config = this.client.configManager.config[entry.guildId]?.verification;
319 const guild = this.client.guilds.cache.get(entry.guildId);
320
321 if (!guild || !config) {
322 return null;
323 }
324
325 const member = await safeMemberFetch(guild, entry.userId);
326
327 if (!member) {
328 return null;
329 }
330
331 let userIdFromPayload: string | undefined;
332
333 try {
334 let { payload } = jwt.verify(entry.token, process.env.JWT_SECRET!, {
335 complete: true,
336 issuer: "SudoBot",
337 subject: "Verification Token"
338 });
339
340 if (typeof payload === "string") {
341 payload = JSON.parse(payload);
342 }
343
344 userIdFromPayload = (payload as { [key: string]: string }).userId;
345 } catch (error) {
346 logError(error);
347 }
348
349 const maxAttemptsExcceded =
350 typeof config?.max_attempts === "number" && config?.max_attempts > 0 && entry.attempts > config?.max_attempts;
351
352 if (entry.token !== token || userIdFromPayload !== userId || maxAttemptsExcceded) {
353 const remainingTime =
354 config.max_time === 0
355 ? Number.POSITIVE_INFINITY
356 : Math.max(entry.createdAt.getTime() + config.max_time - Date.now(), 0);
357
358 await this.sendLog(guild, {
359 author: {
360 name: member?.user.username ?? "Unknown",
361 icon_url: member?.user.displayAvatarURL()
362 },
363 title: "Failed Verification Attempt",
364 color: Colors.Red,
365 fields: [
366 {
367 name: "User",
368 value: member ? userInfo(member.user) : entry.userId
369 },
370 {
371 name: "Attempts",
372 value: `${maxAttemptsExcceded ? "More than " : ""}${entry.attempts} times ${
373 typeof config?.max_attempts === "number" && config?.max_attempts > 0
374 ? `(${config?.max_attempts} max)`
375 : ""
376 }`
377 },
378 {
379 name: "Verification Initiated At",
380 value: `${time(entry.createdAt, "R")} (${
381 remainingTime === 0
382 ? "Session expired"
383 : Number.isFinite(remainingTime)
384 ? `${formatDistanceToNowStrict(new Date(Date.now() - remainingTime))} remaining`
385 : `Session never expires`
386 })`
387 },
388 {
389 name: "Method",
390 value: method
391 }
392 ],
393 footer: {
394 text: "Failed"
395 },
396 timestamp: new Date().toISOString()
397 });
398
399 if (!maxAttemptsExcceded) {
400 await this.client.prisma.verificationEntry.update({
401 where: {
402 id: entry.id
403 },
404 data: {
405 attempts: {
406 increment: 1
407 }
408 }
409 });
410 }
411
412 await this.onMemberVerificationFail(member, entry, remainingTime);
413 return null;
414 }
415
416 if (member) {
417 await this.sendVerificationSuccessDMToMember(member).catch(logError);
418 }
419
420 await this.sendLog(guild, {
421 author: {
422 name: member?.user.username ?? "Unknown",
423 icon_url: member?.user.displayAvatarURL()
424 },
425 title: "Successfully Verified Member",
426 color: Colors.Green,
427 fields: [
428 {
429 name: "User",
430 value: member ? userInfo(member.user) : entry.userId
431 },
432 {
433 name: "Method",
434 value: method
435 }
436 ],
437 footer: {
438 text: "Verified"
439 },
440 timestamp: new Date().toISOString()
441 });
442
443 await this.onMemberVerify(member).catch(logError);
444
445 await this.client.prisma.verificationEntry.delete({
446 where: {
447 id: entry.id
448 }
449 });
450
451 return {
452 id: entry.userId,
453 token
454 };
455 }
456
457 async onMemberVerify(member: GuildMember) {
458 const config = this.client.configManager.config[member.guild.id]?.verification;
459
460 if (config?.unverified_roles?.length) {
461 await member.roles.remove(config?.unverified_roles);
462 }
463
464 if (config?.verified_roles?.length) {
465 await member.roles.add(config?.verified_roles);
466 }
467 }
468 }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26