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

Annotation of /branches/8.x/src/automod/VerificationService.ts

Parent Directory Parent Directory | Revision Log Revision Log


Revision 577 - (hide annotations)
Mon Jul 29 18:52:37 2024 UTC (8 months ago) by rakinar2
File MIME type: application/typescript
File size: 16097 byte(s)
chore: add old version archive branches (2.x to 9.x-dev)
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 { 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 { logError } from "../utils/Logger";
39     import { userInfo } from "../utils/embed";
40     import { safeChannelFetch, safeMemberFetch } from "../utils/fetch";
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) {
125     return;
126     }
127    
128     if ((config.max_attempts === 0 || attempts < config.max_attempts) && remainingTime > 0) {
129     return;
130     }
131    
132     switch (config.action_on_fail?.type) {
133     case "ban":
134     if (member.bannable) {
135     await this.client.infractionManager.createUserBan(member.user, {
136     guild: member.guild,
137     moderator: this.client.user!,
138     autoRemoveQueue: true,
139     notifyUser: true,
140     sendLog: true,
141     reason: "Failed verification"
142     });
143     }
144    
145     break;
146    
147     case "kick":
148     if (member.kickable) {
149     await this.client.infractionManager.createMemberKick(member, {
150     guild: member.guild,
151     moderator: this.client.user!,
152     notifyUser: true,
153     sendLog: true,
154     reason: "Failed verification"
155     });
156     }
157    
158     break;
159    
160     case "mute":
161     if (member.manageable || member.moderatable) {
162     await this.client.infractionManager.createMemberMute(member, {
163     guild: member.guild,
164     moderator: this.client.user!,
165     notifyUser: true,
166     sendLog: true,
167     reason: "Failed verification",
168     autoRemoveQueue: true
169     });
170     }
171    
172     break;
173    
174     case "role":
175     if (member.manageable) {
176     const methodName = config.action_on_fail!.mode === "give" ? "add" : "remove";
177     await member.roles[methodName](config.action_on_fail!.roles).catch(logError);
178     }
179    
180     break;
181     }
182     }
183    
184     requiresVerification(member: GuildMember) {
185     const config = this.client.configManager.config[member.guild.id]?.verification;
186    
187     return (
188     config?.parameters?.always ||
189     (typeof config?.parameters?.age_less_than === "number" &&
190     Date.now() - member.user.createdAt.getTime() < config?.parameters?.age_less_than) ||
191     (config?.parameters?.no_avatar && member.user.avatar === null)
192     );
193     }
194    
195     async createDatabaseEntry(member: GuildMember) {
196     const config = this.client.configManager.config[member.guild.id]?.verification;
197    
198     const seed = await bcrypt.hash((Math.random() * 100000000).toString(), await bcrypt.genSalt());
199     const token = jwt.sign(
200     {
201     seed,
202     userId: member.user.id
203     },
204     process.env.JWT_SECRET!,
205     {
206     expiresIn: config?.max_time === 0 ? undefined : config?.max_time,
207     issuer: "SudoBot",
208     subject: "Verification Token"
209     }
210     );
211    
212     return this.client.prisma.verificationEntry.create({
213     data: {
214     userId: member.user.id,
215     token,
216     guildId: member.guild.id
217     }
218     });
219     }
220    
221     async sendLog(guild: Guild, embed: APIEmbed) {
222     const config = this.client.configManager.config[guild.id]?.verification;
223    
224     if (!config?.logging.enabled) {
225     return;
226     }
227    
228     const channelId = config.logging.channel ?? this.client.configManager.config[guild.id]?.logging?.primary_channel;
229    
230     if (!channelId) {
231     return;
232     }
233    
234     const channel = await safeChannelFetch(guild, channelId);
235    
236     if (!channel || !channel.isTextBased()) {
237     return;
238     }
239    
240     return channel
241     .send({
242     embeds: [embed]
243     })
244     .catch(logError);
245     }
246    
247     sendVerificationDMToMember(member: GuildMember, token: string) {
248     const url = `${process.env.FRONTEND_URL}/challenge/verify?t=${encodeURIComponent(token)}&u=${member.id}&g=${
249     member.guild.id
250     }&n=${encodeURIComponent(member.guild.name)}`;
251    
252     return member.send({
253     embeds: [
254     {
255     author: {
256     icon_url: member.guild.iconURL() ?? undefined,
257     name: "Verification Required"
258     },
259     color: Colors.Gold,
260     description: `
261     Hello **${escapeMarkdown(member.user.username)}**,\n
262     [${member.guild.name}](https://discord.com/channels/${
263     member.guild.id
264     }) 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
265     ${url}\n
266     You might be asked to solve a captcha.\n
267     Sincerely,
268     **The Staff of ${member.guild.name}**
269     `.replace(/(\r\n|\n)\t+/, "\n"),
270     footer: {
271     text: `You have ${formatDistanceToNowStrict(
272     Date.now() - (this.client.configManager.config[member.guild.id]?.verification?.max_time ?? 0)
273     )} to verify`
274     },
275     timestamp: new Date().toISOString()
276     }
277     ],
278     components: [
279     new ActionRowBuilder<ButtonBuilder>().addComponents(
280     new ButtonBuilder().setStyle(ButtonStyle.Link).setURL(url).setLabel("Verify")
281     )
282     ]
283     });
284     }
285    
286     async sendVerificationSuccessDMToMember(member: GuildMember) {
287     return member.send({
288     embeds: [
289     {
290     author: {
291     icon_url: member.guild.iconURL() ?? undefined,
292     name: "Verification Completed"
293     },
294     color: Colors.Green,
295     description: `
296     Hello **${escapeMarkdown(member.user.username)}**,
297     You have successfully verified yourself. You've been granted access to the server now.\n
298     Cheers,
299     **The Staff of ${member.guild.name}**
300     `.replace(/(\r\n|\n)\t+/, "\n"),
301     footer: {
302     text: "Completed"
303     },
304     timestamp: new Date().toISOString()
305     }
306     ]
307     });
308     }
309    
310     async attemptToVerifyUserByToken(userId: string, token: string, method: string) {
311     const entry = await this.client.prisma.verificationEntry.findFirst({
312     where: {
313     userId,
314     token
315     }
316     });
317    
318     if (!entry) {
319     return null;
320     }
321    
322     const config = this.client.configManager.config[entry.guildId]?.verification;
323     const guild = this.client.guilds.cache.get(entry.guildId);
324    
325     if (!guild || !config) {
326     return null;
327     }
328    
329     const member = await safeMemberFetch(guild, entry.userId);
330    
331     if (!member) {
332     return null;
333     }
334    
335     let userIdFromPayload: string | undefined;
336    
337     try {
338     let { payload } = jwt.verify(entry.token, process.env.JWT_SECRET!, {
339     complete: true,
340     issuer: "SudoBot",
341     subject: "Verification Token"
342     });
343    
344     if (typeof payload === "string") {
345     payload = JSON.parse(payload);
346     }
347    
348     userIdFromPayload = (payload as { [key: string]: string }).userId;
349     } catch (error) {
350     logError(error);
351     }
352    
353     const maxAttemptsExcceded =
354     typeof config?.max_attempts === "number" && config?.max_attempts > 0 && entry.attempts > config?.max_attempts;
355    
356     if (entry.token !== token || userIdFromPayload !== userId || maxAttemptsExcceded) {
357     const remainingTime =
358     config.max_time === 0
359     ? Number.POSITIVE_INFINITY
360     : Math.max(entry.createdAt.getTime() + config.max_time - Date.now(), 0);
361    
362     await this.sendLog(guild, {
363     author: {
364     name: member?.user.username ?? "Unknown",
365     icon_url: member?.user.displayAvatarURL()
366     },
367     title: "Failed Verification Attempt",
368     color: Colors.Red,
369     fields: [
370     {
371     name: "User",
372     value: member ? userInfo(member.user) : entry.userId
373     },
374     {
375     name: "Attempts",
376     value: `${maxAttemptsExcceded ? "More than " : ""}${entry.attempts} times ${
377     typeof config?.max_attempts === "number" && config?.max_attempts > 0
378     ? `(${config?.max_attempts} max)`
379     : ""
380     }`
381     },
382     {
383     name: "Verification Initiated At",
384     value: `${time(entry.createdAt, "R")} (${
385     remainingTime === 0
386     ? "Session expired"
387     : Number.isFinite(remainingTime)
388     ? `${formatDistanceToNowStrict(new Date(Date.now() - remainingTime))} remaining`
389     : "Session never expires"
390     })`
391     },
392     {
393     name: "Method",
394     value: method
395     }
396     ],
397     footer: {
398     text: "Failed"
399     },
400     timestamp: new Date().toISOString()
401     });
402    
403     if (!maxAttemptsExcceded) {
404     await this.client.prisma.verificationEntry.update({
405     where: {
406     id: entry.id
407     },
408     data: {
409     attempts: {
410     increment: 1
411     }
412     }
413     });
414     }
415    
416     await this.onMemberVerificationFail(member, entry, remainingTime);
417     return null;
418     }
419    
420     if (member) {
421     await this.sendVerificationSuccessDMToMember(member).catch(logError);
422     }
423    
424     await this.sendLog(guild, {
425     author: {
426     name: member?.user.username ?? "Unknown",
427     icon_url: member?.user.displayAvatarURL()
428     },
429     title: "Successfully Verified Member",
430     color: Colors.Green,
431     fields: [
432     {
433     name: "User",
434     value: member ? userInfo(member.user) : entry.userId
435     },
436     {
437     name: "Method",
438     value: method
439     }
440     ],
441     footer: {
442     text: "Verified"
443     },
444     timestamp: new Date().toISOString()
445     });
446    
447     await this.onMemberVerify(member).catch(logError);
448    
449     await this.client.prisma.verificationEntry.delete({
450     where: {
451     id: entry.id
452     }
453     });
454    
455     return {
456     id: entry.userId,
457     token
458     };
459     }
460    
461     async onMemberVerify(member: GuildMember) {
462     const config = this.client.configManager.config[member.guild.id]?.verification;
463    
464     if (config?.unverified_roles?.length) {
465     await member.roles.remove(config?.unverified_roles);
466     }
467    
468     if (config?.verified_roles?.length) {
469     await member.roles.add(config?.verified_roles);
470     }
471     }
472     }

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26