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

Annotation of /branches/7.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: 16044 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 { 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