From 0438074195ded76703a0874dd0866640e7a7e83e Mon Sep 17 00:00:00 2001 From: Edgar Hernandez Date: Thu, 27 Mar 2025 20:22:15 +0000 Subject: [PATCH 1/8] Fix wrong discord random command image path --- libs/discord/community/src/lib/miscellaneous/fun/fun.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/discord/community/src/lib/miscellaneous/fun/fun.module.ts b/libs/discord/community/src/lib/miscellaneous/fun/fun.module.ts index 8e96c30c..19e8ce6c 100644 --- a/libs/discord/community/src/lib/miscellaneous/fun/fun.module.ts +++ b/libs/discord/community/src/lib/miscellaneous/fun/fun.module.ts @@ -46,7 +46,7 @@ export class FunDiscordModule implements SlashCommands, OnInteractionCreate { break; case 'wrongDiscord': interaction.reply({ - files: ['https://cdn.discordapp.com/attachments/262744296875229185/1092881251775545474/SupremeARKmod.png'], + files: ['http://static.supremegaming.gg/images/misc/SupremeARKmod.png'], }); break; default: From e38834ba34783845781a6b693cda2ad563fc98c4 Mon Sep 17 00:00:00 2001 From: Edgar Hernandez Date: Thu, 13 Mar 2025 02:37:11 +0000 Subject: [PATCH 2/8] Add base patreon reporter discord module Include patreon-api.ts lib to hopefully make the integration a little bit easier --- .../src/main.ts | 2 + libs/discord/community/src/index.ts | 1 + .../patreon/patreon-reporter.module.ts | 81 +++++++++++++++++++ package-lock.json | 18 +++++ package.json | 1 + 5 files changed, 103 insertions(+) create mode 100644 libs/discord/community/src/lib/support/patreon/patreon-reporter.module.ts diff --git a/apps/supreme-discord-community-bot-node/src/main.ts b/apps/supreme-discord-community-bot-node/src/main.ts index ad34b2c4..4f80a207 100644 --- a/apps/supreme-discord-community-bot-node/src/main.ts +++ b/apps/supreme-discord-community-bot-node/src/main.ts @@ -7,6 +7,7 @@ import { FunDiscordModule, GeneralHelpDiscordModule, NewMemberDiscordModule, + PatreonReportDiscordModule, RoleAssignmentDiscordModule, } from '@supremegaming/discord/community'; import { GatewayIntentBits, Partials } from 'discord.js'; @@ -54,6 +55,7 @@ new DiscordClientBootstrapper({ GeneralHelpDiscordModule, NewMemberDiscordModule, RoleAssignmentDiscordModule, + PatreonReportDiscordModule, ], options: { clientToken: process.env.DISCORD_API_TOKEN, diff --git a/libs/discord/community/src/index.ts b/libs/discord/community/src/index.ts index ccb4a3a9..3ede1146 100644 --- a/libs/discord/community/src/index.ts +++ b/libs/discord/community/src/index.ts @@ -8,3 +8,4 @@ export * from './lib/support/donate/donate-info.module'; export * from './lib/support/help/general-help.module'; export * from './lib/support/membership/new-member.module'; export * from './lib/support/roles/role-assignment.module'; +export * from './lib/support/patreon/patreon-reporter.module'; diff --git a/libs/discord/community/src/lib/support/patreon/patreon-reporter.module.ts b/libs/discord/community/src/lib/support/patreon/patreon-reporter.module.ts new file mode 100644 index 00000000..6547aacb --- /dev/null +++ b/libs/discord/community/src/lib/support/patreon/patreon-reporter.module.ts @@ -0,0 +1,81 @@ +import { CacheType, CommandInteractionOptionResolver, Interaction, PermissionFlagsBits } from 'discord.js'; +import { SlashCommandBuilder } from '@discordjs/builders'; + +import { PatreonCreatorClient, PatreonStore } from 'patreon-api.ts'; + +import { OnInteractionCreate, SlashCommands, SlashCommandTypes } from '@supremegaming/discord/bootstrap'; + +export class PatreonReportDiscordModule implements SlashCommands, OnInteractionCreate { + public patreonClient = new PatreonCreatorClient({ + name: 'Supreme Gaming V2', + oauth: { + clientId: process.env.PATREON_CLIENT_ID, + clientSecret: process.env.PATREON_CLIENT_SECRET, + token: { + access_token: process.env.PATREON_ACCESS_TOKEN, + refresh_token: process.env.PATREON_REFRESH_TOKEN, + }, + }, + }).initialize(); + + public commands(): SlashCommandTypes { + return [ + new SlashCommandBuilder() + .setName('patreon') + .setDescription('Patreon community integration - Admins only') + .addSubcommandGroup((group) => + group + .setName('list') + .setDescription('List patrons based on criteria') + .addSubcommand((subcommand) => + subcommand + .setName('active') + .setDescription('List active patrons') + .addBooleanOption((option) => + option.setName('ephemeral').setDescription('Send the message as ephemeral').setRequired(false) + ) + ) + ) + .addSubcommandGroup((group) => + group + .setName('audit') + .setDescription('Audit Patron by ID') + .addSubcommand((subcommand) => + subcommand + .setName('patron') + .setDescription('Get information about a patron by their ID') + .addStringOption((subcommand) => + subcommand + .setName('patron_id') + .setDescription('Get information about a patron by their ID') + .setRequired(true) + ) + .addBooleanOption((option) => + option.setName('ephemeral').setDescription('Send the message as ephemeral').setRequired(false) + ) + ) + ) + .setDefaultMemberPermissions(PermissionFlagsBits.BanMembers), + ]; + } + + public async clientOnInteractionCreate(interaction: Interaction): Promise { + if (interaction.isCommand() && interaction.commandName === 'patreon') { + const subCommandGroup = (interaction.options as CommandInteractionOptionResolver).getSubcommandGroup(); + const subCommand = (interaction.options as CommandInteractionOptionResolver).getSubcommand(); + const ephemeral = (interaction.options as CommandInteractionOptionResolver).getBoolean('ephemeral') || true; + + await interaction.deferReply({ ephemeral }); + + if (subCommandGroup === 'list') { + await interaction.editReply({ + content: 'List of active patrons', + }); + } else if (subCommandGroup === 'audit') { + await interaction.editReply({ + content: 'Audit patron by ID feature is not yet implemented.', + }); + } + } + } +} diff --git a/package-lock.json b/package-lock.json index 9aa2d701..618f38b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "passport": "^0.4.1", "passport-jwt": "^4.0.0", "passport-steam": "^1.0.15", + "patreon-api.ts": "^0.11.0", "public-ip": "^4.0.4", "reflect-metadata": "^0.1.13", "rxjs": "~7.5.0", @@ -18437,6 +18438,18 @@ "node": ">=8" } }, + "node_modules/patreon-api.ts": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/patreon-api.ts/-/patreon-api.ts-0.11.0.tgz", + "integrity": "sha512-JI+SxBWp4J8L8HfciUWFItmJ1DJBZiaOoMzYVPur+qpwxGp+we5Si1CRr8e3Ouc+MJ0EUhhCsWk9ccmz8R2vUQ==", + "license": "MIT", + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://paypal.me/05ghostrider" + } + }, "node_modules/pause": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", @@ -36401,6 +36414,11 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" }, + "patreon-api.ts": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/patreon-api.ts/-/patreon-api.ts-0.11.0.tgz", + "integrity": "sha512-JI+SxBWp4J8L8HfciUWFItmJ1DJBZiaOoMzYVPur+qpwxGp+we5Si1CRr8e3Ouc+MJ0EUhhCsWk9ccmz8R2vUQ==" + }, "pause": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", diff --git a/package.json b/package.json index c13ab71f..cf3c54dc 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "passport": "^0.4.1", "passport-jwt": "^4.0.0", "passport-steam": "^1.0.15", + "patreon-api.ts": "^0.11.0", "public-ip": "^4.0.4", "reflect-metadata": "^0.1.13", "rxjs": "~7.5.0", From 9ae9698e608bf262f7d5a768edf1c41d4e601413 Mon Sep 17 00:00:00 2001 From: Edgar Hernandez Date: Thu, 13 Mar 2025 02:37:44 +0000 Subject: [PATCH 3/8] Fix race condition with slash commands registering before all modules are loaded --- .../src/lib/bootstrapper/discord-bootstrap.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/libs/discord/bootstrap/src/lib/bootstrapper/discord-bootstrap.ts b/libs/discord/bootstrap/src/lib/bootstrapper/discord-bootstrap.ts index 9eb0889a..f57f730c 100644 --- a/libs/discord/bootstrap/src/lib/bootstrapper/discord-bootstrap.ts +++ b/libs/discord/bootstrap/src/lib/bootstrapper/discord-bootstrap.ts @@ -25,7 +25,8 @@ export class DiscordClientBootstrapper { const slashCommands: SlashCommandTypes = []; if (options.modules.length > 0) { - options.modules.forEach((dm, index, arr) => { + for (let i = 0; i < options.modules.length; i++) { + const dm = options.modules[i]; const moduleInstance: DiscordFeatureModule = new dm(); if (moduleInstance.commands) { @@ -64,8 +65,10 @@ export class DiscordClientBootstrapper { this._onMessageDelete(moduleInstance.clientOnMessageDelete, moduleInstance); } + console.log(`Loaded ${moduleInstance.constructor.name}.`); + // Register slash commands after all module listeners have been initialized. - if (slashCommands.length > 0 && index === arr.length - 1) { + if (slashCommands.length > 0 && i === options.modules.length - 1) { if (process.env.DISCORD_REGISTER_SLASH_COMMANDS === 'false') { console.warn('Modules have configured slash commands but slash command registration is disabled.'); } else if ( @@ -89,11 +92,7 @@ export class DiscordClientBootstrapper { } } } - - console.log(`Loaded ${moduleInstance.constructor.name}.`); - - return this; - }); + } } // Call the initial onReady handler to cache guild members @@ -221,7 +220,7 @@ export class DiscordClientBootstrapper { } console.log('Successfully refreshed application (/) commands.'); - } catch (err: any) { + } catch (err) { console.error(err.message); } } From 71c0ae7e5eb1745cd4b324f15f8eaf898640edec Mon Sep 17 00:00:00 2001 From: Edgar Hernandez Date: Thu, 13 Mar 2025 05:09:02 +0000 Subject: [PATCH 4/8] Working active list --- .../patreon/patreon-reporter.module.ts | 113 ++++++++++++++++-- 1 file changed, 104 insertions(+), 9 deletions(-) diff --git a/libs/discord/community/src/lib/support/patreon/patreon-reporter.module.ts b/libs/discord/community/src/lib/support/patreon/patreon-reporter.module.ts index 6547aacb..a1aae5cf 100644 --- a/libs/discord/community/src/lib/support/patreon/patreon-reporter.module.ts +++ b/libs/discord/community/src/lib/support/patreon/patreon-reporter.module.ts @@ -1,13 +1,12 @@ -import { CacheType, CommandInteractionOptionResolver, Interaction, PermissionFlagsBits } from 'discord.js'; +import { CacheType, CommandInteractionOptionResolver, EmbedBuilder, Interaction, PermissionFlagsBits } from 'discord.js'; import { SlashCommandBuilder } from '@discordjs/builders'; -import { PatreonCreatorClient, PatreonStore } from 'patreon-api.ts'; +import { PatreonCreatorClient, QueryBuilder } from 'patreon-api.ts'; import { OnInteractionCreate, SlashCommands, SlashCommandTypes } from '@supremegaming/discord/bootstrap'; export class PatreonReportDiscordModule implements SlashCommands, OnInteractionCreate { public patreonClient = new PatreonCreatorClient({ - name: 'Supreme Gaming V2', oauth: { clientId: process.env.PATREON_CLIENT_ID, clientSecret: process.env.PATREON_CLIENT_SECRET, @@ -16,7 +15,7 @@ export class PatreonReportDiscordModule implements SlashCommands, OnInteractionC refresh_token: process.env.PATREON_REFRESH_TOKEN, }, }, - }).initialize(); + }); public commands(): SlashCommandTypes { return [ @@ -63,14 +62,110 @@ export class PatreonReportDiscordModule implements SlashCommands, OnInteractionC if (interaction.isCommand() && interaction.commandName === 'patreon') { const subCommandGroup = (interaction.options as CommandInteractionOptionResolver).getSubcommandGroup(); const subCommand = (interaction.options as CommandInteractionOptionResolver).getSubcommand(); - const ephemeral = (interaction.options as CommandInteractionOptionResolver).getBoolean('ephemeral') || true; - await interaction.deferReply({ ephemeral }); + const shouldBePersistent = + interaction.options.get('ephemeral')?.value !== undefined && + (interaction.options as CommandInteractionOptionResolver).getBoolean('ephemeral') === false; + + await interaction.deferReply({ ephemeral: !shouldBePersistent }); if (subCommandGroup === 'list') { - await interaction.editReply({ - content: 'List of active patrons', - }); + const memberQuery = QueryBuilder.campaignMembers + .addRelationships(['currently_entitled_tiers', 'user']) + .setAttributes({ + member: [ + 'full_name', + 'last_charge_date', + 'last_charge_status', + 'campaign_lifetime_support_cents', + 'currently_entitled_amount_cents', + 'patron_status', + ], + tier: ['amount_cents', 'description', 'discord_role_ids', 'title'], + user: ['created', 'social_connections'], + }) + .setRequestOptions({ + count: 1000, + }); + + try { + const members = await this.patreonClient.fetchCampaignMembers(process.env.PATREON_CAMPAIGN_ID, memberQuery); + + const mappedTiers = members.included + .filter((tier) => tier.type === 'tier') + .reduce((acc, curr) => { + acc[curr.id] = curr.attributes; + return acc; + }, {}); + + const mappedUserSocialConnections = members.included + .filter((user) => user.type === 'user') + .reduce((acc, curr) => { + acc[curr.id] = curr.attributes['social_connections']; + return acc; + }, {}); + + const activeMembers = members.data + .filter((member) => member.attributes.patron_status === 'active_patron') + .map((member) => { + const tier = mappedTiers[member.relationships.currently_entitled_tiers.data[0].id]; + const userSocialConnections = mappedUserSocialConnections[member.relationships.user.data.id]; + + return { + user: member.attributes.full_name, + tier: tier.title, + amount: tier.amount_cents / 100, + lifetimeSupport: member.attributes.campaign_lifetime_support_cents / 100, + lastChargeDate: member.attributes.last_charge_date, + lastChargeStatus: member.attributes.last_charge_status, + discordId: userSocialConnections.discord ? userSocialConnections.discord.user_id : null, + }; + }); + + // Group active members into buckets of 24 + const activeMembersChunks = activeMembers.reduce((acc, curr, index) => { + const chunkIndex = Math.floor(index / 24); + if (!acc[chunkIndex]) { + acc[chunkIndex] = []; + } + acc[chunkIndex].push(curr); + return acc; + }, []); + + // One embed per chunk + const embeds = activeMembersChunks.map((chunk, index) => { + const embed = new EmbedBuilder({ + title: 'Active Patrons', + description: `Total Active Patrons: ${activeMembers.length}`, + color: 0x00ae86, + footer: { + text: `Page ${index + 1} of ${activeMembersChunks.length}`, + }, + fields: chunk.map((member) => ({ + name: member.user, + value: `Tier: ${member.tier}\nAmount: $${member.amount.toFixed( + 2 + )}\nLifetime Support: $${member.lifetimeSupport.toFixed(2)}\nLast Charge Date: \nLast Charge Status: ${member.lastChargeStatus}\nDiscord ID: ${ + member.discordId !== null ? `<@${member.discordId}>` : 'Not linked' + }`, + })), + }); + + return embed; + }); + + await interaction.editReply({ + embeds, + }); + } catch (err) { + console.error(err); + + await interaction.editReply({ + content: 'An error occurred while fetching the list of active patrons.', + }); + } } else if (subCommandGroup === 'audit') { await interaction.editReply({ content: 'Audit patron by ID feature is not yet implemented.', From af43adcd0b2f507827fbdb98f1d364f5c708fdc8 Mon Sep 17 00:00:00 2001 From: Edgar Hernandez Date: Thu, 13 Mar 2025 05:13:47 +0000 Subject: [PATCH 5/8] Add missing .env.sample entries --- apps/supreme-discord-community-bot-node/.env.sample | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/supreme-discord-community-bot-node/.env.sample b/apps/supreme-discord-community-bot-node/.env.sample index 99234960..6bcb5e21 100644 --- a/apps/supreme-discord-community-bot-node/.env.sample +++ b/apps/supreme-discord-community-bot-node/.env.sample @@ -14,4 +14,10 @@ DISCORD_WELCOME_CHANNEL_NAME= # # https://discordjs.guide/creating-your-bot/command-deployment.html#command-registration # -DISCORD_REGISTER_SLASH_COMMANDS= \ No newline at end of file +DISCORD_REGISTER_SLASH_COMMANDS= + +PATREON_CAMPAIGN_ID= +PATREON_CLIENT_ID= +PATREON_CLIENT_SECRET= +PATREON_ACCESS_TOKEN= +PATREON_REFRESH_TOKEN= \ No newline at end of file From 6244be7920714a0926c1af9486659925621aec92 Mon Sep 17 00:00:00 2001 From: Edgar Hernandez Date: Thu, 13 Mar 2025 05:35:42 +0000 Subject: [PATCH 6/8] Include hidden files in build artifact upload --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cfc38023..380bb6ce 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -86,3 +86,4 @@ jobs: with: name: ${{ github.sha }} path: dist/ + include-hidden-files: true From 5917d31634056c3edd96a2a7e6ddb9dd8b448c49 Mon Sep 17 00:00:00 2001 From: Edgar Hernandez Date: Thu, 27 Mar 2025 22:31:21 +0000 Subject: [PATCH 7/8] Fix discord timestamp Discord timestamp uses seconds instead of the milliseconds returned by the Date.parse function --- .../src/lib/support/patreon/patreon-reporter.module.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/discord/community/src/lib/support/patreon/patreon-reporter.module.ts b/libs/discord/community/src/lib/support/patreon/patreon-reporter.module.ts index a1aae5cf..746fb6a7 100644 --- a/libs/discord/community/src/lib/support/patreon/patreon-reporter.module.ts +++ b/libs/discord/community/src/lib/support/patreon/patreon-reporter.module.ts @@ -145,9 +145,9 @@ export class PatreonReportDiscordModule implements SlashCommands, OnInteractionC name: member.user, value: `Tier: ${member.tier}\nAmount: $${member.amount.toFixed( 2 - )}\nLifetime Support: $${member.lifetimeSupport.toFixed(2)}\nLast Charge Date: \nLast Charge Status: ${member.lastChargeStatus}\nDiscord ID: ${ + )}\nLifetime Support: $${member.lifetimeSupport.toFixed(2)}\nLast Charge Date: \nLast Charge Status: ${member.lastChargeStatus}\nDiscord ID: ${ member.discordId !== null ? `<@${member.discordId}>` : 'Not linked' }`, })), From 22a370b91793d1fe92df49b0ed151e1ad7b0f721 Mon Sep 17 00:00:00 2001 From: Edgar Hernandez Date: Fri, 28 Mar 2025 02:19:02 +0000 Subject: [PATCH 8/8] Calculate and report earned roles --- .../.env.sample | 5 +- .../patreon/patreon-reporter.module.ts | 53 ++++++++++++++++--- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/apps/supreme-discord-community-bot-node/.env.sample b/apps/supreme-discord-community-bot-node/.env.sample index 6bcb5e21..afbad156 100644 --- a/apps/supreme-discord-community-bot-node/.env.sample +++ b/apps/supreme-discord-community-bot-node/.env.sample @@ -20,4 +20,7 @@ PATREON_CAMPAIGN_ID= PATREON_CLIENT_ID= PATREON_CLIENT_SECRET= PATREON_ACCESS_TOKEN= -PATREON_REFRESH_TOKEN= \ No newline at end of file +PATREON_REFRESH_TOKEN= + +# Example: {"Supporter":[],"Backer":[]}. Where the key is the tier name on Patreon and the value is an array of Discord role IDs. +PATREON_TIERS_DISCORD_ROLES= \ No newline at end of file diff --git a/libs/discord/community/src/lib/support/patreon/patreon-reporter.module.ts b/libs/discord/community/src/lib/support/patreon/patreon-reporter.module.ts index 746fb6a7..bfe04d8d 100644 --- a/libs/discord/community/src/lib/support/patreon/patreon-reporter.module.ts +++ b/libs/discord/community/src/lib/support/patreon/patreon-reporter.module.ts @@ -1,7 +1,7 @@ import { CacheType, CommandInteractionOptionResolver, EmbedBuilder, Interaction, PermissionFlagsBits } from 'discord.js'; import { SlashCommandBuilder } from '@discordjs/builders'; -import { PatreonCreatorClient, QueryBuilder } from 'patreon-api.ts'; +import { PatreonCreatorClient, QueryBuilder, Tier } from 'patreon-api.ts'; import { OnInteractionCreate, SlashCommands, SlashCommandTypes } from '@supremegaming/discord/bootstrap'; @@ -70,6 +70,26 @@ export class PatreonReportDiscordModule implements SlashCommands, OnInteractionC await interaction.deferReply({ ephemeral: !shouldBePersistent }); if (subCommandGroup === 'list') { + let definedTiers = process.env.PATREON_TIERS_DISCORD_ROLES; + + if (!definedTiers) { + await interaction.editReply({ + content: 'No defined tiers found. Please define application discord tiers and roles.', + }); + + return; + } else { + try { + definedTiers = JSON.parse(definedTiers); + } catch (err) { + await interaction.editReply({ + content: 'An error occurred while parsing the defined tiers. Please check the configuration.', + }); + + return; + } + } + const memberQuery = QueryBuilder.campaignMembers .addRelationships(['currently_entitled_tiers', 'user']) .setAttributes({ @@ -91,7 +111,10 @@ export class PatreonReportDiscordModule implements SlashCommands, OnInteractionC try { const members = await this.patreonClient.fetchCampaignMembers(process.env.PATREON_CAMPAIGN_ID, memberQuery); - const mappedTiers = members.included + const mappedTiers: Record< + string, + Pick + > = members.included .filter((tier) => tier.type === 'tier') .reduce((acc, curr) => { acc[curr.id] = curr.attributes; @@ -109,11 +132,29 @@ export class PatreonReportDiscordModule implements SlashCommands, OnInteractionC .filter((member) => member.attributes.patron_status === 'active_patron') .map((member) => { const tier = mappedTiers[member.relationships.currently_entitled_tiers.data[0].id]; + + // Find the mapped tier with the highest + const earnedTier = Object.values(mappedTiers).reduce((acc, currentMax) => { + if (acc === null) { + return currentMax; + } + + if ( + currentMax.amount_cents >= acc.amount_cents && + member.attributes.campaign_lifetime_support_cents >= currentMax.amount_cents + ) { + return currentMax; + } + + return acc; + }, null); + const userSocialConnections = mappedUserSocialConnections[member.relationships.user.data.id]; return { user: member.attributes.full_name, tier: tier.title, + earnedTier: earnedTier ? earnedTier?.title : null, amount: tier.amount_cents / 100, lifetimeSupport: member.attributes.campaign_lifetime_support_cents / 100, lastChargeDate: member.attributes.last_charge_date, @@ -143,13 +184,11 @@ export class PatreonReportDiscordModule implements SlashCommands, OnInteractionC }, fields: chunk.map((member) => ({ name: member.user, - value: `Tier: ${member.tier}\nAmount: $${member.amount.toFixed( + value: `Sub: ${member.tier}\nEarned: ${member.earnedTier}\nAmount: $${member.amount.toFixed( 2 - )}\nLifetime Support: $${member.lifetimeSupport.toFixed(2)}\nLast Charge Date: \nLast Charge Status: ${member.lastChargeStatus}\nDiscord ID: ${ - member.discordId !== null ? `<@${member.discordId}>` : 'Not linked' - }`, + }:f>\nDiscord ID: ${member.discordId !== null ? `<@${member.discordId}>` : 'N/A'}`, })), });