From 64d9c8e7375e5ea39664708914c3bd030d8f3d96 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Thu, 22 Feb 2024 18:38:20 +0000 Subject: [PATCH 1/4] Add trade command --- src/buttonEvents/Trade.ts | 174 ++++++++++++++++++++++++++++++++++ src/commands/trade.ts | 127 +++++++++++++++++++++++++ src/constants/EmbedColours.ts | 1 + src/registry.ts | 6 +- 4 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 src/buttonEvents/Trade.ts create mode 100644 src/commands/trade.ts diff --git a/src/buttonEvents/Trade.ts b/src/buttonEvents/Trade.ts new file mode 100644 index 0000000..beb6669 --- /dev/null +++ b/src/buttonEvents/Trade.ts @@ -0,0 +1,174 @@ +import { ButtonInteraction, EmbedBuilder } from "discord.js"; +import { ButtonEvent } from "../type/buttonEvent"; +import { CoreClient } from "../client/client"; +import Inventory from "../database/entities/app/Inventory"; +import EmbedColours from "../constants/EmbedColours"; + +export default class Trade extends ButtonEvent { + public override async execute(interaction: ButtonInteraction) { + const action = interaction.customId.split(" ")[1]; + + switch (action) { + case "accept": + await this.AcceptTrade(interaction); + break; + case "decline": + await this.DeclineTrade(interaction); + break; + } + } + + private async AcceptTrade(interaction: ButtonInteraction) { + const giveUserId = interaction.customId.split(" ")[2]; + const receiveUserId = interaction.customId.split(" ")[3]; + const giveCardNumber = interaction.customId.split(" ")[4]; + const receiveCardNumber = interaction.customId.split(" ")[5]; + const expiry = interaction.customId.split(" ")[6]; + const timeoutId = interaction.customId.split(" ")[7]; + + const expiryDate = new Date(expiry); + + if (expiryDate < new Date()) { + await interaction.reply("Trade has expired"); + return; + } + + if (interaction.user.id !== receiveUserId) { + await interaction.reply("You are not the user who the trade is intended for"); + return; + } + + const giveItem = CoreClient.Cards + .flatMap(x => x.cards) + .find(x => x.id === giveCardNumber); + + const receiveItem = CoreClient.Cards + .flatMap(x => x.cards) + .find(x => x.id === receiveCardNumber); + + if (!giveItem || !receiveItem) { + await interaction.reply("One or more of the items you are trying to trade does not exist."); + return; + } + + const giveUser = interaction.client.users.cache.get(giveUserId) || await interaction.client.users.fetch(giveUserId); + const receiveUser = interaction.client.users.cache.get(receiveUserId) || await interaction.client.users.fetch(receiveUserId); + + const giveUserInventory1 = await Inventory.FetchOneByCardNumberAndUserId(giveUserId, giveCardNumber); + const receiveUserInventory1 = await Inventory.FetchOneByCardNumberAndUserId(receiveUserId, receiveCardNumber); + + if (!giveUserInventory1 || !receiveUserInventory1) { + await interaction.reply("One or more of the items you are trying to trade does not exist."); + return; + } + + if (giveUserInventory1.Quantity < 1 || receiveUserInventory1.Quantity < 1) { + await interaction.reply("One or more of the items you are trying to trade does not exist."); + return; + } + + giveUserInventory1.SetQuantity(giveUserInventory1.Quantity - 1); + receiveUserInventory1.SetQuantity(receiveUserInventory1.Quantity - 1); + + await giveUserInventory1.Save(Inventory, giveUserInventory1); + await receiveUserInventory1.Save(Inventory, receiveUserInventory1); + + let giveUserInventory2 = await Inventory.FetchOneByCardNumberAndUserId(receiveUserId, giveCardNumber); + let receiveUserInventory2 = await Inventory.FetchOneByCardNumberAndUserId(giveUserId, receiveCardNumber); + + if (!giveUserInventory2) { + giveUserInventory2 = new Inventory(receiveUserId, giveCardNumber, 1); + } else { + giveUserInventory2.SetQuantity(giveUserInventory2.Quantity + 1); + } + + if (!receiveUserInventory2) { + receiveUserInventory2 = new Inventory(giveUserId, receiveCardNumber, 1); + } else { + receiveUserInventory2.SetQuantity(receiveUserInventory2.Quantity + 1); + } + + await giveUserInventory2.Save(Inventory, giveUserInventory2); + await receiveUserInventory2.Save(Inventory, receiveUserInventory2); + + clearTimeout(timeoutId); + + const tradeEmbed = new EmbedBuilder() + .setTitle("Trade Accepted") + .setDescription(`Trade initiated between ${receiveUser.username} and ${giveUser.username}`) + .setColor(EmbedColours.Success) + .addFields([ + { + name: `${receiveUser.username} is giving`, + value: `${giveItem.id}: ${giveItem.name}`, + inline: true, + }, + { + name: `${giveUser.username} is giving`, + value: `${receiveItem.id}: ${receiveItem.name}`, + inline: true, + }, + { + name: "Complete", + value: new Date().toLocaleString(), + } + ]); + + await interaction.update({ embeds: [ tradeEmbed ]}); + } + + private async DeclineTrade(interaction: ButtonInteraction) { + const giveUserId = interaction.customId.split(" ")[2]; + const receiveUserId = interaction.customId.split(" ")[3]; + const giveCardNumber = interaction.customId.split(" ")[4]; + const receiveCardNumber = interaction.customId.split(" ")[5]; + // No need to get expiry date + const timeoutId = interaction.customId.split(" ")[7]; + + if (interaction.user.id !== receiveUserId || interaction.user.id !== giveUserId) { + await interaction.reply("You are not the user who the trade is intended for"); + return; + } + + const giveUser = interaction.client.users.cache.get(giveUserId) || await interaction.client.users.fetch(giveUserId); + const receiveUser = interaction.client.users.cache.get(receiveUserId) || await interaction.client.users.fetch(receiveUserId); + + const giveItem = CoreClient.Cards + .flatMap(x => x.cards) + .find(x => x.id === giveCardNumber); + + const receiveItem = CoreClient.Cards + .flatMap(x => x.cards) + .find(x => x.id === receiveCardNumber); + + if (!giveItem || !receiveItem) { + await interaction.reply("One or more of the items you are trying to trade does not exist."); + return; + } + + clearTimeout(timeoutId); + + const tradeEmbed = new EmbedBuilder() + .setTitle("Trade Declined") + .setDescription(`Trade initiated between ${receiveUser.username} and ${giveUser.username}`) + .setColor(EmbedColours.Error) + .addFields([ + { + name: `${receiveUser.username} is giving`, + value: `${giveItem.id}: ${giveItem.name}`, + inline: true, + }, + { + name: `${giveUser.username} is giving`, + value: `${receiveItem.id}: ${receiveItem.name}`, + inline: true, + }, + { + name: "Declined", + value: new Date().toLocaleString(), + } + ]); + + await interaction.update({ embeds: [ tradeEmbed ]}); + } +} \ No newline at end of file diff --git a/src/commands/trade.ts b/src/commands/trade.ts new file mode 100644 index 0000000..21e57c4 --- /dev/null +++ b/src/commands/trade.ts @@ -0,0 +1,127 @@ +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction, EmbedBuilder, SlashCommandBuilder } from "discord.js"; +import { Command } from "../type/command"; +import Inventory from "../database/entities/app/Inventory"; +import { CoreClient } from "../client/client"; +import EmbedColours from "../constants/EmbedColours"; + +export default class Trade extends Command { + constructor() { + super(); + + this.CommandBuilder = new SlashCommandBuilder() + .setName("trade") + .setDescription("Initiate a trade with another user.") + .addUserOption(x => + x + .setName("user") + .setDescription("User to trade with") + .setRequired(true)) + .addStringOption(x => + x + .setName("give") + .setDescription("Item to give") + .setRequired(true)) + .addStringOption(x => + x + .setName("receive") + .setDescription("Item to receive") + .setRequired(true)); + } + + public override async execute(interaction: CommandInteraction) { + const user = interaction.options.getUser("user")!; + const give = interaction.options.get("give")!; + const receive = interaction.options.get("receive")!; + + const giveItemEntity = await Inventory.FetchOneByCardNumberAndUserId(interaction.user.id, give.value!.toString()); + const receiveItemEntity = await Inventory.FetchOneByCardNumberAndUserId(user.id, receive.value!.toString()); + + if (!giveItemEntity) { + await interaction.reply("You do not have the item you are trying to trade."); + return; + } + + if (!receiveItemEntity) { + await interaction.reply("The user you are trying to trade with does not have the item you are trying to trade for."); + return; + } + + const giveItem = CoreClient.Cards + .flatMap(x => x.cards) + .find(x => x.id === give.value!.toString()); + + const receiveItem = CoreClient.Cards + .flatMap(x => x.cards) + .find(x => x.id === receive.value!.toString()); + + if (!giveItem || !receiveItem) { + await interaction.reply("One or more of the items you are trying to trade does not exist."); + return; + } + + const now = new Date(); + const expiry = now.setMinutes(now.getMinutes() + 15); + + const tradeEmbed = new EmbedBuilder() + .setTitle("Trade Offer") + .setDescription(`Trade initiated between ${interaction.user.username} and ${user.username}`) + .setColor(EmbedColours.Grey) + .addFields([ + { + name: `${interaction.user.username} is giving`, + value: `${giveItem.id}: ${giveItem.name}`, + inline: true, + }, + { + name: `${user.username} is giving`, + value: `${receiveItem.id}: ${receiveItem.name}`, + inline: true, + }, + { + name: "Expires", + value: new Date(expiry).toLocaleString(), + } + ]); + + const timeoutId = setTimeout(async () => this.autoDecline(interaction, interaction.user.username, user.username, giveItem.id, receiveItem.id, giveItem.name, receiveItem.name), 1000 * 60 * 15); // 15 minutes + + const row = new ActionRowBuilder() + .addComponents([ + new ButtonBuilder() + .setCustomId(`trade accept ${interaction.user.id} ${user.id} ${giveItem.id} ${receiveItem.id} ${expiry} ${timeoutId}`) + .setLabel("Accept") + .setStyle(ButtonStyle.Success), + new ButtonBuilder() + .setCustomId(`trade decline ${interaction.user.id} ${user.id} ${giveItem.id} ${receiveItem.id} ${expiry} ${timeoutId}`) + .setLabel("Decline") + .setStyle(ButtonStyle.Danger), + ]); + + await interaction.reply({ content: `${user}`, embeds: [ tradeEmbed ], components: [ row ] }); + } + + private async autoDecline(interaction: CommandInteraction, giveUsername: string, receiveUsername: string, giveCardNumber: string, receiveCardNumber: string, giveCardName: string, receiveCardName: string) { + const tradeEmbed = new EmbedBuilder() + .setTitle("Trade Expired") + .setDescription(`Trade initiated between ${receiveUsername} and ${giveUsername}`) + .setColor(EmbedColours.Error) + .addFields([ + { + name: `${receiveUsername} is giving`, + value: `${giveCardNumber}: ${giveCardName}`, + inline: true, + }, + { + name: `${giveUsername} is giving`, + value: `${receiveCardNumber}: ${receiveCardName}`, + inline: true, + }, + { + name: "Expired", + value: new Date().toLocaleString(), + } + ]); + + await interaction.editReply({ embeds: [ tradeEmbed ]}); + } +} \ No newline at end of file diff --git a/src/constants/EmbedColours.ts b/src/constants/EmbedColours.ts index a54d56f..36777e3 100644 --- a/src/constants/EmbedColours.ts +++ b/src/constants/EmbedColours.ts @@ -1,5 +1,6 @@ export default class EmbedColours { public static readonly Ok = 0x3050ba; + public static readonly Success = 0x50c878; public static readonly Error = 0xff0000; public static readonly Grey = 0xd3d3d3; public static readonly BronzeCard = 0xcd7f32; diff --git a/src/registry.ts b/src/registry.ts index 2fc657d..1087cda 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -8,6 +8,7 @@ import Gdrivesync from "./commands/gdrivesync"; import Give from "./commands/give"; import Inventory from "./commands/inventory"; import Resync from "./commands/resync"; +import Trade from "./commands/trade"; import View from "./commands/view"; // Test Command Imports @@ -18,6 +19,7 @@ import Droprarity from "./commands/stage/droprarity"; import Claim from "./buttonEvents/Claim"; import InventoryButtonEvent from "./buttonEvents/Inventory"; import Reroll from "./buttonEvents/Reroll"; +import TradeButtonEvent from "./buttonEvents/Trade"; export default class Registry { public static RegisterCommands() { @@ -28,6 +30,7 @@ export default class Registry { CoreClient.RegisterCommand("give", new Give()); CoreClient.RegisterCommand("inventory", new Inventory()); CoreClient.RegisterCommand("resync", new Resync()); + CoreClient.RegisterCommand("trade", new Trade()); CoreClient.RegisterCommand("view", new View()); // Test Commands @@ -41,7 +44,8 @@ export default class Registry { public static RegisterButtonEvents() { CoreClient.RegisterButtonEvent("claim", new Claim()); - CoreClient.RegisterButtonEvent("inventory", new InventoryButtonEvent); + CoreClient.RegisterButtonEvent("inventory", new InventoryButtonEvent()); CoreClient.RegisterButtonEvent("reroll", new Reroll()); + CoreClient.RegisterButtonEvent("trade", new TradeButtonEvent()); } } \ No newline at end of file -- 2.43.4 From ef795efd28b19f77d5a4f99f6516fd776a0cfcfa Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Thu, 22 Feb 2024 18:40:21 +0000 Subject: [PATCH 2/4] Fix linting issues --- src/buttonEvents/Trade.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/buttonEvents/Trade.ts b/src/buttonEvents/Trade.ts index beb6669..b768412 100644 --- a/src/buttonEvents/Trade.ts +++ b/src/buttonEvents/Trade.ts @@ -9,12 +9,12 @@ export default class Trade extends ButtonEvent { const action = interaction.customId.split(" ")[1]; switch (action) { - case "accept": - await this.AcceptTrade(interaction); - break; - case "decline": - await this.DeclineTrade(interaction); - break; + case "accept": + await this.AcceptTrade(interaction); + break; + case "decline": + await this.DeclineTrade(interaction); + break; } } -- 2.43.4 From 339ec575c88243096ceb388fd0ffc8c7ed4c94e8 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Thu, 22 Feb 2024 18:49:53 +0000 Subject: [PATCH 3/4] Disable buttons on press --- src/buttonEvents/Trade.ts | 34 +++++++++++++++++++++++++++++++--- src/commands/trade.ts | 16 +++++++++++++++- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/buttonEvents/Trade.ts b/src/buttonEvents/Trade.ts index b768412..5ff0dee 100644 --- a/src/buttonEvents/Trade.ts +++ b/src/buttonEvents/Trade.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, EmbedBuilder } from "discord.js"; +import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, EmbedBuilder } from "discord.js"; import { ButtonEvent } from "../type/buttonEvent"; import { CoreClient } from "../client/client"; import Inventory from "../database/entities/app/Inventory"; @@ -114,7 +114,21 @@ export default class Trade extends ButtonEvent { } ]); - await interaction.update({ embeds: [ tradeEmbed ]}); + const row = new ActionRowBuilder() + .addComponents([ + new ButtonBuilder() + .setCustomId("trade expired accept") + .setLabel("Accept") + .setStyle(ButtonStyle.Success) + .setDisabled(true), + new ButtonBuilder() + .setCustomId("trade expired decline") + .setLabel("Decline") + .setStyle(ButtonStyle.Danger) + .setDisabled(true), + ]); + + await interaction.update({ embeds: [ tradeEmbed ], components: [ row ]}); } private async DeclineTrade(interaction: ButtonInteraction) { @@ -169,6 +183,20 @@ export default class Trade extends ButtonEvent { } ]); - await interaction.update({ embeds: [ tradeEmbed ]}); + const row = new ActionRowBuilder() + .addComponents([ + new ButtonBuilder() + .setCustomId("trade expired accept") + .setLabel("Accept") + .setStyle(ButtonStyle.Success) + .setDisabled(true), + new ButtonBuilder() + .setCustomId("trade expired decline") + .setLabel("Decline") + .setStyle(ButtonStyle.Danger) + .setDisabled(true), + ]); + + await interaction.update({ embeds: [ tradeEmbed ], components: [ row ]}); } } \ No newline at end of file diff --git a/src/commands/trade.ts b/src/commands/trade.ts index 21e57c4..f6c20d5 100644 --- a/src/commands/trade.ts +++ b/src/commands/trade.ts @@ -122,6 +122,20 @@ export default class Trade extends Command { } ]); - await interaction.editReply({ embeds: [ tradeEmbed ]}); + const row = new ActionRowBuilder() + .addComponents([ + new ButtonBuilder() + .setCustomId("trade expired accept") + .setLabel("Accept") + .setStyle(ButtonStyle.Success) + .setDisabled(true), + new ButtonBuilder() + .setCustomId("trade expired declined") + .setLabel("Decline") + .setStyle(ButtonStyle.Danger) + .setDisabled(true), + ]); + + await interaction.editReply({ embeds: [ tradeEmbed ], components: [ row ]}); } } \ No newline at end of file -- 2.43.4 From e0a0b309ca400dfa3784bfb04dbbc8b6a87fd300 Mon Sep 17 00:00:00 2001 From: Ethan Lane Date: Thu, 22 Feb 2024 21:50:22 +0000 Subject: [PATCH 4/4] Add trade offer gif --- src/buttonEvents/Trade.ts | 2 ++ src/commands/trade.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/buttonEvents/Trade.ts b/src/buttonEvents/Trade.ts index 5ff0dee..28b08ee 100644 --- a/src/buttonEvents/Trade.ts +++ b/src/buttonEvents/Trade.ts @@ -97,6 +97,7 @@ export default class Trade extends ButtonEvent { .setTitle("Trade Accepted") .setDescription(`Trade initiated between ${receiveUser.username} and ${giveUser.username}`) .setColor(EmbedColours.Success) + .setImage("https://i.imgur.com/9w5f1ls.gif") .addFields([ { name: `${receiveUser.username} is giving`, @@ -166,6 +167,7 @@ export default class Trade extends ButtonEvent { .setTitle("Trade Declined") .setDescription(`Trade initiated between ${receiveUser.username} and ${giveUser.username}`) .setColor(EmbedColours.Error) + .setImage("https://i.imgur.com/9w5f1ls.gif") .addFields([ { name: `${receiveUser.username} is giving`, diff --git a/src/commands/trade.ts b/src/commands/trade.ts index f6c20d5..60e78d8 100644 --- a/src/commands/trade.ts +++ b/src/commands/trade.ts @@ -66,6 +66,7 @@ export default class Trade extends Command { .setTitle("Trade Offer") .setDescription(`Trade initiated between ${interaction.user.username} and ${user.username}`) .setColor(EmbedColours.Grey) + .setImage("https://media1.tenor.com/m/KkZwKl2AQ2QAAAAd/trade-offer.gif") .addFields([ { name: `${interaction.user.username} is giving`, @@ -105,6 +106,7 @@ export default class Trade extends Command { .setTitle("Trade Expired") .setDescription(`Trade initiated between ${receiveUsername} and ${giveUsername}`) .setColor(EmbedColours.Error) + .setImage("https://media1.tenor.com/m/KkZwKl2AQ2QAAAAd/trade-offer.gif") .addFields([ { name: `${receiveUsername} is giving`, -- 2.43.4