diff --git a/commands/debug.js b/commands/debug.js index 3d3691e..2c1dc53 100644 --- a/commands/debug.js +++ b/commands/debug.js @@ -2,6 +2,7 @@ const { SlashCommandBuilder, ComponentType, ActionRowBuilder, ButtonBuilder, But const { customAlphabet } = require("nanoid"); const { Card, User } = require("../models"); const { UserUtils, CardUtils, GeneralUtils } = require("../util"); +const stores = require("../stores"); require('dotenv').config(); module.exports = { @@ -24,6 +25,7 @@ module.exports = { { name: 'add_primary', value: 'add_primary' }, { name: 'add_secondary', value: 'add_secondary' }, { name: 'toggle_maintenance', value: 'toggle_maintenance' }, + { name: 'store', value: 'store' }, ) ) .addStringOption((option) => @@ -144,6 +146,12 @@ module.exports = { ephemeral: false }); break; + case "store": + interaction.editReply({ + content: `${JSON.stringify(stores)}`, + ephemeral: false + }); + break; default: interaction.editReply({ content: `Your permission level is ${await UserUtils.getPermissionLevel(interaction.member)}`, diff --git a/commands/trade.js b/commands/trade.js new file mode 100644 index 0000000..bf1a4cc --- /dev/null +++ b/commands/trade.js @@ -0,0 +1,193 @@ +const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require("discord.js"); +const { Card, User } = require("../models"); +const { UserUtils, CardUtils } = require("../util"); +const { TradeStore } = require("../stores"); + +const tradeTimeout = 90000; +module.exports = { + data: new SlashCommandBuilder() + .setName("trade") + .setDescription("Start trading with a User") + .addSubcommand((subcommand) => + subcommand + .setName("start") + .setDescription("Start a trade with a User") + .addUserOption((option) => + option + .setName("user") + .setDescription("User to trade with") + .setRequired(true) + )) + .addSubcommand((subcommand) => + subcommand + .setName("view") + .setDescription("View active trade") + ) + .addSubcommand((subcommand) => + subcommand + .setName("add") + .setDescription("Add a card to the trade") + .addStringOption((option) => + option + .setName("card") + .setDescription("Card to add") + .setRequired(true) + .setAutocomplete(true) + ) + ), + permissionLevel: 0, + async execute(interaction) { + let user1 = await UserUtils.getUserByDiscordId(interaction.member.id); + let trade = await TradeStore.getTradeByUser(user1.id); + + switch (interaction.options.getSubcommand()) { + case "start": + await interaction.deferReply(); + let user2 = await UserUtils.getUserByDiscordId(interaction.options.getUser("user").id); + //Attach usernames for convenience + user2.name = interaction.options.getUser("user").username; + user1.name = interaction.member.user.username; + this.startTrade(interaction, user1, user2); + break; + case "view": + await interaction.deferReply(); + this.viewTrade(interaction, trade); + break; + case "add": + if (!trade) { + await interaction.reply({ content: "You don't have an active trade", ephemeral: true }); + return; + } + await interaction.deferReply(); + let card = await Card.findOne({ + where: + { identifier: interaction.options.getString("card") , userId: user1.id } + }, + include: [ + { model: Character, include: [{ model: Band }] }, + { model: User} + ]); + this.addCardToTrade(interaction, trade, card); + break; + } + }, + + async startTrade(interaction, user1, user2) { + if (!user2) { + await interaction.editReply({ content: "This User is not registered yet!" }); + return; + } + if (user2.discordId === interaction.member.id) { + await interaction.editReply({ content: "You can't trade with yourself!" }); + return; + } + if (await TradeStore.getTradeByUser(user1.id)) { + await interaction.editReply({ content: "You are already in a Trade!" }); + return; + } + if (await TradeStore.getTradeByUser(user2.id)) { + await interaction.editReply({ content: "This User is already in a Trade!" }); + return; + } + let trade = new TradeStore.Trade(CardUtils.generateIdentifier(), user1, user2); + await TradeStore.addTrade(trade); + this.viewTrade(interaction, trade); + + + }, + + async addCardToTrade(interaction, trade, card) { + //find card by identifier if owned by user + + if (!card) { + await interaction.reply({ content: "You don't own this card", ephemeral: true }); + return; + } + if (card.userId !== trade.user1.id) { + await interaction.editReply({ content: "You don't own this Card!" }); + return; + } + + if (trade.user1.id === interaction.member.id) { + trade.user1Cards.push(card); + } else { + trade.user2Cards.push(card); + } + this.viewTrade(interaction, trade); + }, + + async viewTrade(interaction, trade) { + if (!trade) { + await interaction.editReply({ content: "This Trade does not exist!" }); + return; + } + //delete existing trade message + if (trade.embed) { + await trade.embed.delete(); + } + + let user1Cards = "No cards" + let user2Cards = "No cards" + // for each of user1's cards in the trade + for (card of trade.user1Cards) { + //get the card object + let card = await Card.findOne({ + where: { + identifier: cardIdentifier + }, + include: [ + { model: Character, include: [{ model: Band }] }, + { model: User} + ] + }); + //add it to the list + user1Cards += `${card.identifier}\n`; + } + // for each of user2's cards in the trade + for (card of trade.user2Cards) { + //get the card object + let card = await Card.findOne({ + const embed = new EmbedBuilder() + .setTitle(`Trade [${trade.id}] ${trade.user1.name} with ${trade.user2.name}`) + .setDescription("DUMMY DESCRIPTION") + .addFields( + { name: `${trade.user1.name}'s cards`, value: user1Cards }, + { name: `${trade.user2.name}'s cards`, value: `DATA2` } + ) + .setColor(0x00ff00) + .setFooter({ text: `TRADE`, iconURL: 'https://cdn.discordapp.com/attachments/856904078754971658/1017431187234508820/fp.png' }); + + const row = new ActionRowBuilder(); + row.addComponents( + new ButtonBuilder() + .setCustomId(`cancel-trade-${trade.id}`) + .setLabel('Cancel') + .setStyle(ButtonStyle.Danger) + ); + let reply = await interaction.editReply({ embeds: [embed], components: [row] }); + trade.embed = reply; + + //button collector + const filter = (button) => button.user.id === trade.user1.discordId || button.user.id === trade.user2.discordId; + const collector = reply.createMessageComponentCollector({ filter, time: tradeTimeout }); + + collector.on('collect', async (button) => { + if (button.customId === `cancel-trade-${trade.id}`) { + await TradeStore.removeTrade(trade); + collector.stop("cancel"); + } + }); + + collector.on('end', async (collected, reason) => { + console.log(`Collected ${collected.size} items, reason: ${reason}`); + if (reason === "time") { + await TradeStore.removeTrade(trade); + await trade.embed.edit({ content: `Trade ${trade.id} expired`, embeds: [], components: [] }); + } + if (reason === "cancel") { + await trade.embed.edit({ content: `Trade ${trade.id} cancelled`, embeds: [], components: [] }); + } + }); + + } +} \ No newline at end of file diff --git a/events/autocompleteRequest.js b/events/autocompleteRequest.js index 2a33294..6a4cf6a 100644 --- a/events/autocompleteRequest.js +++ b/events/autocompleteRequest.js @@ -3,7 +3,7 @@ const { UserUtils } = require('../util'); const { Card, Character, User } = require('../models'); const Sequelize = require('sequelize'); const { QUALITY_NAMES } = require('../config/constants'); - +const { TestStore } = require('../stores'); module.exports = { name: "interactionCreate", async execute (interaction) { @@ -20,6 +20,12 @@ module.exports = { choices = await this.fetchCards(focusedOption, { user: user, ownedOnly: true }); } + if (interaction.commandName === "trade") { + if (focusedOption.name === "card") { + choices = await this.fetchCards(focusedOption, { user: user, ownedOnly: true }); + } + } + if (interaction.commandName === 'view') { const viewType = interaction.options.getString('type'); diff --git a/stores/index.js b/stores/index.js new file mode 100644 index 0000000..3cc5802 --- /dev/null +++ b/stores/index.js @@ -0,0 +1,25 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const basename = path.basename(__filename); +const utils = {}; + +fs + .readdirSync(__dirname) + .filter(file => { + return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); + }) + .forEach(file => { + const util = require(path.join(__dirname, file)); + utils[util.name] = util; + console.log(`Registered Store: ${util.name}`); + }); + +Object.keys(utils).forEach(modelName => { + if (utils[modelName].associate) { + utils[modelName].associate(utils); + } +}); + +module.exports = utils; \ No newline at end of file diff --git a/stores/tradeStore.js b/stores/tradeStore.js new file mode 100644 index 0000000..d0f9931 --- /dev/null +++ b/stores/tradeStore.js @@ -0,0 +1,33 @@ +module.exports = { + name: "TradeStore", + activeTrades: [], + + async addTrade(trade) { + this.activeTrades.push(trade); + }, + + async removeTrade(trade) { + this.activeTrades.splice(this.activeTrades.indexOf(trade), 1); + }, + + async getTradeById(tradeId) { + return this.activeTrades.find(trade => trade.id === tradeId); + }, + + async getTradeByUser(userId) { + return this.activeTrades.find(trade => trade.user1.id === userId || trade.user2.id === userId); + }, + + Trade: class Trade { + constructor(id, user1, user2) { + this.id = id; + this.user1 = user1; + this.user2 = user2; + this.embed = null; + this.user1Cards = []; + this.user2Cards = []; + this.user1Accept = false; + this.user2Accept = false; + } + } +} \ No newline at end of file