diff --git a/src/events/helper/DiscordCommandHandler.ts b/src/events/helper/DiscordCommandHandler.ts index 6707546..490c8f1 100644 --- a/src/events/helper/DiscordCommandHandler.ts +++ b/src/events/helper/DiscordCommandHandler.ts @@ -106,11 +106,15 @@ export class DiscordCommandHandler { name: `Discord Command: ${command.metadata.name}`, op: `discord.cmd.${command.metadata.name}`, parentSpan: null, - }, async () => { + }, async (span) => { try { await command.execute(interaction); } catch(ex) { + span.setStatus({ + code: 2, + message: "Command Execution Error" + }); const id = captureException(ex, {tags: {handled: "no"}}); await this.client.redis.set(`userSentryErrorID:${interaction.user.id}`, id, "EX", 1800); diff --git a/src/events/helper/DiscordConfirmBtn.ts b/src/events/helper/DiscordConfirmBtn.ts index ed131ab..05b5360 100644 --- a/src/events/helper/DiscordConfirmBtn.ts +++ b/src/events/helper/DiscordConfirmBtn.ts @@ -1,28 +1,40 @@ import { ButtonInteraction, GuildMember } from "discord.js"; import { DiscordClient } from "../../core/DiscordClient"; import { DiscordUser } from "../../utils/DiscordUser"; +import { startSpan } from "@sentry/node"; export async function VertificationHandler(client: DiscordClient, interaction: ButtonInteraction) { if(interaction.user.bot) return interaction.reply({ content: "PRIVILEGE INSUFFICIENT!", ephemeral: true }); - const user = new DiscordUser(client, interaction.user); - const member = interaction.member as GuildMember | null; - if(!member) return; + await startSpan({ + name: "Discord Verification Handler", + op: "discord.verify", + parentSpan: null + }, async () => { + const user = new DiscordUser(client, interaction.user); + const member = interaction.member as GuildMember | null; + if(!member) return; - // Check is already verified - const newUserRole = client.config.newUserRoleID; - if(member.roles.cache.has(newUserRole) && await user.isVerified()) - return interaction.reply({ content: "You are already verified!", ephemeral: true }); + // Check is already verified + const newUserRole = client.config.newUserRoleID; + if(member.roles.cache.has(newUserRole) && await user.isVerified()) + return await interaction.reply({ content: "You are already verified!", ephemeral: true }); - // Update the user - await member.roles.add(newUserRole, "Confirmation Role"); - await user.updateUserData({ rulesconfirmedon: new Date() }); + await startSpan({ + name: "Update Verification Status", + op: "event.helper.VertificationHandler", + }, async()=>{ + // Update the user + await member.roles.add(newUserRole, "Confirmation Role"); + await user.updateUserData({ rulesconfirmedon: new Date() }); - // Send the message - await interaction.reply({ content: "Thank you for confirming the rules.", ephemeral: true }); - await client.logger.sendLog({ - type: "Interaction", - message: `**${interaction.user.tag}** has confirmed the rules.` + // Send the message + await interaction.reply({ content: "Thank you for confirming the rules.", ephemeral: true }); + await client.logger.sendLog({ + type: "Interaction", + message: `**${interaction.user.tag}** has confirmed the rules.` + }); + }); }); } \ No newline at end of file diff --git a/src/events/helper/TwitchCommandHandler.ts b/src/events/helper/TwitchCommandHandler.ts index e83b5dd..7cc7cd2 100644 --- a/src/events/helper/TwitchCommandHandler.ts +++ b/src/events/helper/TwitchCommandHandler.ts @@ -40,7 +40,7 @@ export async function processCommand(eventData: eventType): Promise { + }, async (span) => { try { await command.execute({ channel: eventData.channel, @@ -51,6 +51,10 @@ export async function processCommand(eventData: eventType): Promise{ + if(!inviteOpt) inviteOpt = {}; + + // Use the vanity code if possible + if(this.guild.vanityURLCode) return inviteOpt.rawCode ? this.guild.vanityURLCode : this.baseUrl + this.guild.vanityURLCode; + + // Use the cached invite key if it exists + if(!inviteOpt.nocache) { + const cache = await this.client.redis.get(this.redisKey); + if(cache) return inviteOpt.rawCode ? cache : this.baseUrl + cache; + } - // Default value for the invite - inviteOpt.maxAge = inviteOpt.maxAge ?? 86400; - inviteOpt.reason = inviteOpt.reason ?? "Temporary Invite"; + // Default value for the invite + inviteOpt.maxAge = inviteOpt.maxAge ?? 86400; + inviteOpt.reason = inviteOpt.reason ?? "Temporary Invite"; - // Find a valid guild channel to create invite in - inviteOpt.channel = inviteOpt.channel ?? + // Find a valid guild channel to create invite in + inviteOpt.channel = inviteOpt.channel ?? this.guild.rulesChannel ?? this.guild.publicUpdatesChannel ?? this.guild.channels.cache.find(ch=>this.isInviteChannel(ch)) as TextChannel | VoiceChannel | NewsChannel | undefined ?? (await this.guild.channels.fetch()).find(ch=>this.isInviteChannel(ch)) as TextChannel | VoiceChannel | NewsChannel | undefined; - if(!inviteOpt.channel) throw new DiscordInviteError("No channel is associated with this server"); + if(!inviteOpt.channel) throw new DiscordInviteError("No channel is associated with this server"); - // Create invite - const inviteLink = await this.guild.invites.create(inviteOpt.channel, inviteOpt); - if(!inviteOpt.nocache) - return inviteOpt.rawCode ? inviteLink.code : inviteLink.url; + // Create invite + const inviteLink = await this.guild.invites.create(inviteOpt.channel, inviteOpt); + if(!inviteOpt.nocache) + return inviteOpt.rawCode ? inviteLink.code : inviteLink.url; - // Save to cache if allowed - await this.client.redis.set(this.redisKey, inviteLink.code, "EX", inviteOpt.maxAge); - return inviteOpt.rawCode ? inviteLink.code : inviteLink.url; + // Save to cache if allowed + await this.client.redis.set(this.redisKey, inviteLink.code, "EX", inviteOpt.maxAge); + return inviteOpt.rawCode ? inviteLink.code : inviteLink.url; + }); } - } diff --git a/src/utils/DiscordUser.ts b/src/utils/DiscordUser.ts index 3ce80bf..3476e62 100644 --- a/src/utils/DiscordUser.ts +++ b/src/utils/DiscordUser.ts @@ -1,6 +1,6 @@ import { APIEmbedField, ColorResolvable, DiscordAPIError, EmbedBuilder, User } from "discord.js"; import { APIErrors } from "./discordErrorCode"; -import { captureException } from "@sentry/node"; +import { captureException, startSpan } from "@sentry/node"; import { Prisma } from "@prisma/client"; import { createHash } from "crypto"; import { DiscordClient } from "../core/DiscordClient"; @@ -72,26 +72,32 @@ export class DiscordUser { * @returns {cacheData | undefined} returns undefined if the user does not exist in the database, otherwise returns cacheData */ public async getCacheData(): Promise { - // Check if the record already exist in redis - if(await this.cacheExists()) { + return await startSpan({ + name: "Get Discord Cache Data", + op: "discordUser.getCacheData", + onlyIfParent: true, + }, async()=>{ + // Check if the record already exist in redis + if(await this.cacheExists()) { // Pull it up and use it - const data = await this.client.redis.hgetall(this.cachekey); - return { - rulesconfirmedon: data.rulesconfirmedon ? new Date(data.rulesconfirmedon) : undefined, - points: data.points ? Number(data.points) : undefined, - lastgrantedpoint: data.lastgrantedpoint ? new Date(data.lastgrantedpoint) : undefined, + const data = await this.client.redis.hgetall(this.cachekey); + return { + rulesconfirmedon: data.rulesconfirmedon ? new Date(data.rulesconfirmedon) : undefined, + points: data.points ? Number(data.points) : undefined, + lastgrantedpoint: data.lastgrantedpoint ? new Date(data.lastgrantedpoint) : undefined, + }; + } + // Data doesn't exist in redis, Update the cache + const dbData = await this.getUserFromDB(); + if(!dbData) return; + const finalData: cacheData = { + rulesconfirmedon: dbData.rulesconfirmedon ?? undefined, + points: dbData.points, + lastgrantedpoint: dbData.lastgrantedpoint, }; - } - // Data doesn't exist in redis, Update the cache - const dbData = await this.getUserFromDB(); - if(!dbData) return; - const finalData: cacheData = { - rulesconfirmedon: dbData.rulesconfirmedon ?? undefined, - points: dbData.points, - lastgrantedpoint: dbData.lastgrantedpoint, - }; - await this.updateCacheData(finalData); - return finalData; + await this.updateCacheData(finalData); + return finalData; + }); } /** * Update the current cache with new data (use updateUserData instead) @@ -99,16 +105,21 @@ export class DiscordUser { * */ public async updateCacheData(newData: cacheData) { - // Clear out all the undefined and null objects - const filteredData: {[key: string]: string} = {}; - if(newData.rulesconfirmedon !== undefined) filteredData["rulesconfirmedon"] = newData.rulesconfirmedon.toString(); - if(newData.points !== undefined) filteredData["points"] = newData.points.toString(); - if(newData.lastgrantedpoint !== undefined) filteredData["lastgrantedpoint"] = newData.lastgrantedpoint.toString(); - // Update the cache - await this.client.redis.hset(this.cachekey, filteredData); - // set redis expire key in 5 hours - await this.client.redis.expire(this.cachekey, 18000); - return; + await startSpan({ + name: "Update Discord Cache Data", + op: "discordUser.updateCacheData", + onlyIfParent: true, + }, async()=>{ + // Clear out all the undefined and null objects + const filteredData: {[key: string]: string} = {}; + if(newData.rulesconfirmedon !== undefined) filteredData["rulesconfirmedon"] = newData.rulesconfirmedon.toString(); + if(newData.points !== undefined) filteredData["points"] = newData.points.toString(); + if(newData.lastgrantedpoint !== undefined) filteredData["lastgrantedpoint"] = newData.lastgrantedpoint.toString(); + // Update the cache + await this.client.redis.hset(this.cachekey, filteredData); + // set redis expire key in 5 hours + await this.client.redis.expire(this.cachekey, 18000); + }); } /** * Get the user data from the database directly, should only be used when the cache didn't have the data. @@ -142,32 +153,38 @@ export class DiscordUser { * @returns whether the operation was successful or not */ public async updateUserData(data?: updateUserData) { - try { - const newData = await this.client.prisma.members.update({ - data: { - username: data ? data.username : this.getUsername(), - displayname: data ? data.displayName : this.user.displayName, - rulesconfirmedon: data?.rulesconfirmedon, - }, - where: { - id: this.user.id - } - }); - await this.updateCacheData({ - rulesconfirmedon: newData.rulesconfirmedon ?? undefined, - points: newData.points, - lastgrantedpoint: newData.lastgrantedpoint - }); - return true; - } catch(ex) { - if(ex instanceof Prisma.PrismaClientKnownRequestError && ex.code === "P2025") { - // User not found, create one - await this.createNewUser(data?.rulesconfirmedon); + await startSpan({ + name: "Update Discord User Data", + op: "discordUser.updateUserData", + onlyIfParent: true, + }, async() => { + try { + const newData = await this.client.prisma.members.update({ + data: { + username: data ? data.username : this.getUsername(), + displayname: data ? data.displayName : this.user.displayName, + rulesconfirmedon: data?.rulesconfirmedon, + }, + where: { + id: this.user.id + } + }); + await this.updateCacheData({ + rulesconfirmedon: newData.rulesconfirmedon ?? undefined, + points: newData.points, + lastgrantedpoint: newData.lastgrantedpoint + }); return true; + } catch(ex) { + if(ex instanceof Prisma.PrismaClientKnownRequestError && ex.code === "P2025") { + // User not found, create one + await this.createNewUser(data?.rulesconfirmedon); + return true; + } + captureException(ex); + return false; } - captureException(ex); - return false; - } + }); } /** * create the user in the database directly @@ -227,30 +244,41 @@ export class DiscordUser { * @param metadata Additional data to be added for sendLog */ public async actionLog(opt: ActionLogOpt) { - try { - await this.client.prisma.modlog.create({ - data: { - targetid: opt.target?.user.id, - moderatorid: this.user.id, - action: opt.actionName, - reason: opt.reason, - metadata: opt.metadata - } - }); - } catch(ex) { - if(ex instanceof Prisma.PrismaClientKnownRequestError && ex.code === "P2003") - await this.client.logger.sendLog({ - type: "Warning", - message: "actionLog failed due to missing target in the database", - ...opt.metadata + await startSpan({ + name: "Action Logger", + op: "discordUser.actionLog", + onlyIfParent: true, + }, async(span)=>{ + try { + await this.client.prisma.modlog.create({ + data: { + targetid: opt.target?.user.id, + moderatorid: this.user.id, + action: opt.actionName, + reason: opt.reason, + metadata: opt.metadata + } }); - else captureException(ex); - } - - await this.client.logger.sendLog({ - type: "Interaction", - message: opt.message, - ...opt.metadata + } catch(ex) { + span.setStatus({ + code: 2, + message: "Prisma experience error when creating log" + }); + + if(ex instanceof Prisma.PrismaClientKnownRequestError && ex.code === "P2003") + await this.client.logger.sendLog({ + type: "Warning", + message: "actionLog failed due to missing target in the database", + ...opt.metadata + }); + else captureException(ex); + } + + await this.client.logger.sendLog({ + type: "Interaction", + message: opt.message, + ...opt.metadata + }); }); } } @@ -286,7 +314,6 @@ class UserEconomy { * Grant the user certain amount of points * @param options Customize the way points are granted * @param updateTimestampOnly Whether to only update the timestamp of the granted points or not (useful for auto-granting point system to deter spammers) - * @returns whether the points has been successfully granted or not */ public async grantPoints(points: number) { // User exist and condition passes, grant the user the points @@ -303,7 +330,6 @@ class UserEconomy { points: newData.points, lastgrantedpoint: newData.lastgrantedpoint }); - return true; } /** * Deduct certain amount of points from the user diff --git a/src/utils/TwitchUser.ts b/src/utils/TwitchUser.ts index 88ae9b3..3a1640a 100644 --- a/src/utils/TwitchUser.ts +++ b/src/utils/TwitchUser.ts @@ -1,5 +1,5 @@ import { Prisma } from "@prisma/client"; -import { captureException } from "@sentry/node"; +import { captureException, startSpan } from "@sentry/node"; import { DiscordUser } from "./DiscordUser"; import { createHash } from "crypto"; import { DiscordClient } from "../core/DiscordClient"; @@ -42,34 +42,40 @@ export class TwitchUser { * @returns Cache Data or undefined if the user does not exist */ public async getCacheData(): Promise { - // Check if the record already exist in redis - if(await this.cacheExists()) { + return await startSpan({ + name: "Twitch User Cache", + op: "twitch.getCacheData", + onlyIfParent: true, + }, async() => { + // Check if the record already exist in redis + if(await this.cacheExists()) { // Pull it up and use it - const data = await this.client.redis.hgetall(this.cachekey); - if(data.memberid === "-1") + const data = await this.client.redis.hgetall(this.cachekey); + if(data.memberid === "-1") + return; + return { + memberid: data.memberid, + username: data.username, + verified: (data.verified !== undefined) ? data.verified === "true" : undefined, + }; + } + // Data doesn't exist in redis, Update the cache + const dbData = await this.getUserFromDB(); + if(!dbData) { + const guestUserData = { + memberid: "-1", + }; + await this.updateDataCache(guestUserData); return; - return { - memberid: data.memberid, - username: data.username, - verified: (data.verified !== undefined) ? data.verified === "true" : undefined, - }; - } - // Data doesn't exist in redis, Update the cache - const dbData = await this.getUserFromDB(); - if(!dbData) { - const guestUserData = { - memberid: "-1", + } + const finalData: cacheData = { + memberid: dbData.memberid, + username: dbData.username, + verified: dbData.verified }; - await this.updateDataCache(guestUserData); - return; - } - const finalData: cacheData = { - memberid: dbData.memberid, - username: dbData.username, - verified: dbData.verified - }; - await this.updateDataCache(finalData); - return finalData; + await this.updateDataCache(finalData); + return finalData; + }); } /** * Checks if there is already a cache record for this user @@ -87,16 +93,21 @@ export class TwitchUser { username?: string, verified?: boolean, }): Promise { - // Clear out all the undefined and null objects - const filteredData: {[key: string]: string} = {}; - if(newData.memberid !== undefined) filteredData["memberid"] = newData.memberid; - if(newData.username !== undefined) filteredData["username"] = newData.username; - if(newData.verified !== undefined) filteredData["verified"] = newData.verified.toString(); - // Update the cache - await this.client.redis.hset(this.cachekey, filteredData); - // set redis expire key in 3 hours - await this.client.redis.expire(this.cachekey, 10800); - return; + await startSpan({ + name: "Twitch User Cache Update", + op: "TwitchUser.updateDataCache", + onlyIfParent: true, + }, async() => { + // Clear out all the undefined and null objects + const filteredData: {[key: string]: string} = {}; + if(newData.memberid !== undefined) filteredData["memberid"] = newData.memberid; + if(newData.username !== undefined) filteredData["username"] = newData.username; + if(newData.verified !== undefined) filteredData["verified"] = newData.verified.toString(); + // Update the cache + await this.client.redis.hset(this.cachekey, filteredData); + // set redis expire key in 3 hours + await this.client.redis.expire(this.cachekey, 10800); + }); } /** * Pull user data directly from PostgreSQL Database. Should only be used if the record does not already exist in cache. @@ -139,32 +150,38 @@ export class TwitchUser { * @returns {boolean} The result of the operation. False means unsuccessful, while true means successful */ public async updateUser(data: updateUser): Promise { - try { - await this.client.prisma.twitch.update({ - data: { + return await startSpan({ + name: "Twitch User Update", + op: "TwitchUser.updateUser", + onlyIfParent: true, + }, async() => { + try { + await this.client.prisma.twitch.update({ + data: { + memberid: data.memberid, + username: data.username, + verified: data.verified, + }, + where: { + id: this.userid + } + }); + await this.updateDataCache({ memberid: data.memberid, username: data.username, - verified: data.verified, - }, - where: { - id: this.userid - } - }); - await this.updateDataCache({ - memberid: data.memberid, - username: data.username, - verified: data.verified - }); - return true; - } catch(ex) { - // Record already existed (if add failure) or Record does not exist (if update failure) - if(ex instanceof Prisma.PrismaClientKnownRequestError) - if(["P2002", "P2001"].find(v => v === (ex as Prisma.PrismaClientKnownRequestError).code)) - return false; - // Some other errors, log it to sentry - captureException(ex); - return false; - } + verified: data.verified + }); + return true; + } catch(ex) { + // Record already existed (if add failure) or Record does not exist (if update failure) + if(ex instanceof Prisma.PrismaClientKnownRequestError) + if(["P2002", "P2001"].find(v => v === (ex as Prisma.PrismaClientKnownRequestError).code)) + return false; + // Some other errors, log it to sentry + captureException(ex); + return false; + } + }); } public async getDiscordUser(): Promise { const data = await this.getCacheData();