diff --git a/src/main/java/com/alttd/buttonManager/buttons/remindMeConfirm/ButtonRemindMeConfirm.java b/src/main/java/com/alttd/buttonManager/buttons/remindMeConfirm/ButtonRemindMeConfirm.java index 7076c00..9ab6d7c 100644 --- a/src/main/java/com/alttd/buttonManager/buttons/remindMeConfirm/ButtonRemindMeConfirm.java +++ b/src/main/java/com/alttd/buttonManager/buttons/remindMeConfirm/ButtonRemindMeConfirm.java @@ -3,7 +3,7 @@ package com.alttd.buttonManager.buttons.remindMeConfirm; import com.alttd.buttonManager.DiscordButton; import com.alttd.database.queries.QueriesReminders.QueriesReminders; import com.alttd.database.queries.QueriesReminders.Reminder; -import com.alttd.reminders.ReminderScheduler; +import com.alttd.schedulers.ReminderScheduler; import com.alttd.util.Util; import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.interactions.InteractionHook; diff --git a/src/main/java/com/alttd/commandManager/CommandManager.java b/src/main/java/com/alttd/commandManager/CommandManager.java index 4e791b2..4ca9f15 100644 --- a/src/main/java/com/alttd/commandManager/CommandManager.java +++ b/src/main/java/com/alttd/commandManager/CommandManager.java @@ -7,6 +7,7 @@ import com.alttd.contextMenuManager.ContextMenuManager; import com.alttd.database.Database; import com.alttd.listeners.ChatListener; import com.alttd.modalManager.ModalManager; +import com.alttd.selectMenuManager.SelectMenuManager; import com.alttd.util.Logger; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.JDA; @@ -31,7 +32,7 @@ public class CommandManager extends ListenerAdapter { private final List commands; private final HashMap> commandList = new HashMap<>(); - public CommandManager(JDA jda, ModalManager modalManager, ContextMenuManager contextMenuManager, ChatListener chatListener) { + public CommandManager(JDA jda, ModalManager modalManager, ContextMenuManager contextMenuManager, ChatListener chatListener, SelectMenuManager selectMenuManager) { commandList.put("manage", new ArrayList<>(List.of(new ScopeInfo(CommandScope.GLOBAL, 0)))); loadCommands(); Logger.info("Loading commands..."); @@ -51,7 +52,8 @@ public class CommandManager extends ListenerAdapter { commandSetToggleableRoles, new CommandToggleRole(commandSetToggleableRoles, jda, this), new CommandRemindMe(jda, this, modalManager), - new CommandSoftLock(jda, this, chatListener)); + new CommandSoftLock(jda, this, chatListener), + new CommandAuction(jda, this, selectMenuManager)); } @Override diff --git a/src/main/java/com/alttd/commandManager/commands/CommandAuction.java b/src/main/java/com/alttd/commandManager/commands/CommandAuction.java new file mode 100644 index 0000000..182add6 --- /dev/null +++ b/src/main/java/com/alttd/commandManager/commands/CommandAuction.java @@ -0,0 +1,254 @@ +package com.alttd.commandManager.commands; + +import com.alttd.AltitudeBot; +import com.alttd.commandManager.CommandManager; +import com.alttd.commandManager.DiscordCommand; +import com.alttd.database.queries.QueriesAuctions.Auction; +import com.alttd.database.queries.QueriesAuctions.QueriesAuction; +import com.alttd.database.queries.commandOutputChannels.CommandOutputChannels; +import com.alttd.database.queries.commandOutputChannels.OutputType; +import com.alttd.schedulers.AuctionScheduler; +import com.alttd.selectMenuManager.DiscordSelectMenu; +import com.alttd.selectMenuManager.SelectMenuManager; +import com.alttd.util.Logger; +import com.alttd.util.Util; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import net.dv8tion.jda.api.interactions.commands.build.Commands; +import net.dv8tion.jda.api.interactions.components.selections.SelectMenu; +import net.dv8tion.jda.api.interactions.components.selections.SelectOption; +import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction; +import net.dv8tion.jda.api.utils.AttachedFile; + +import java.awt.*; +import java.io.File; +import java.nio.file.Path; +import java.time.Instant; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +public class CommandAuction extends DiscordCommand { + + private final CommandData commandData; + private final SelectMenuManager selectMenuManager; + public CommandAuction(JDA jda, CommandManager commandManager, SelectMenuManager selectMenuManager) { + commandData = Commands.slash(getName(), "Create an auction") + .addOption(OptionType.STRING, "item", "The name (and type) of the item you're selling", true) + .addOption(OptionType.INTEGER, "amount", "How many of the item you're selling", true) + .addOption(OptionType.INTEGER, "starting-price", "How much the bids should start at (the minimum price you're willing to accept for the item", true) + .addOption(OptionType.INTEGER, "minimum-increase", "The minimum amount of money bids should increase by", true) + .addOption(OptionType.INTEGER, "insta-buy", "(optional) A price you're willing to sell the item for immediately (ends the auction)", false) + .addOption(OptionType.STRING, "description", "(optional) A further explanation of the item", false) + .addOption(OptionType.ATTACHMENT, "screenshot", "(optional) A screenshot of the item you're selling if needed", false) + .setDefaultPermissions(DefaultMemberPermissions.ENABLED); + + this.selectMenuManager = selectMenuManager; + Util.registerCommand(commandManager, jda, commandData, getName()); + } + + @Override + public String getName() { + return "auction"; + } + + @Override + public void execute(SlashCommandInteractionEvent event) { + Guild guild = event.getGuild(); + if (guild == null) { + handleError(event, "This command can only be executed within a guild"); + return; + } + + GuildChannel outputChannel = CommandOutputChannels.getOutputChannel(guild, OutputType.AUCTION); + if (outputChannel == null) { + handleError(event, "This guild does not have an Auction channel set"); + return; + } + + TextChannel textChannel = validTextChannel(event, outputChannel); + if (textChannel == null) { + handleError(event, "This guild has an invalid Auction channel"); + return; + } + + Integer minimumIncrease = event.getOption("minimum-increase", OptionMapping::getAsInt); + if (minimumIncrease == null) { + handleError(event, "Missing required minimum increase option"); + return; + } + + MessageEmbed messageEmbed = buildAuctionEmbed(event, minimumIncrease); + if (messageEmbed == null) + return; + ReplyCallbackAction replyCallbackAction = event.deferReply(true); + textChannel.sendMessageEmbeds(messageEmbed).queue(success -> { + Message.Attachment screenshot = event.getOption("screenshot", OptionMapping::getAsAttachment); + if (screenshot != null) { + String dataFolder = AltitudeBot.getInstance().getDataFolder(); + Path path = Path.of(dataFolder + File.separator + UUID.randomUUID() + "." + screenshot.getFileExtension()); + screenshot.getProxy().downloadToFile(path.toFile()).thenAccept(file -> + success.editMessageAttachments(AttachedFile.fromData(file)).queue(done -> file.delete(), failed -> { + Util.handleFailure(failed); + file.delete(); + })) + .exceptionally(e -> { + e.printStackTrace(); + return null; + }); + } + DiscordSelectMenu discordAuction = selectMenuManager.getDiscordSelectMenuFor("auction"); + if (discordAuction == null) { + replyCallbackAction.setEmbeds(Util.genericErrorEmbed("Error", "Unable to find select menu for your auction, removing message...")) + .queue(); + success.delete().queue(); + return; + } + SelectMenu selectMenu; + Integer instaBuy = event.getOption("insta-buy", OptionMapping::getAsInt); + int mediumIncrease = minimumIncrease * 5; + if (instaBuy != null) { + if (mediumIncrease == instaBuy) + mediumIncrease += 1; + selectMenu = discordAuction.getSelectMenu( + SelectOption.of("Increase bid by: " + minimumIncrease, "" + minimumIncrease), + SelectOption.of("Increase bid by: " + mediumIncrease, "" + mediumIncrease), + SelectOption.of("Insta Buy: " + instaBuy, "" + instaBuy) + ); + } else { + selectMenu = discordAuction.getSelectMenu( + SelectOption.of("Increase bid by: " + minimumIncrease, "" + minimumIncrease), + SelectOption.of("Increase bid by: " + mediumIncrease, "" + mediumIncrease) + ); + } + + if (selectMenu == null) { + replyCallbackAction.setEmbeds(Util.genericErrorEmbed("Error", "Unable to add select menu to your auction, removing message...")) + .queue(); + success.delete().queue(); + return; + } + success.editMessageComponents().setActionRow(selectMenu).queue(); + Integer startingPrice = event.getOption("starting-price", OptionMapping::getAsInt); + if (startingPrice == null) { + Logger.severe("Starting price magically became null"); + replyCallbackAction.setEmbeds(Util.genericSuccessEmbed("Error", "Failed to store auction")) + .queue(); + return; + } + AuctionScheduler auctionScheduler = AuctionScheduler.getInstance(); + if (auctionScheduler == null) { + replyCallbackAction.setEmbeds(Util.genericSuccessEmbed("Error", "Failed to store auction in scheduler")) + .queue(); + return; + } + auctionScheduler.addAuction(new Auction( + success, + success.getChannel().getIdLong(), + success.getGuild().getIdLong(), + startingPrice, + Instant.now().toEpochMilli() + TimeUnit.DAYS.toMillis(1))); + replyCallbackAction.setEmbeds(Util.genericSuccessEmbed("Success", "Your auction was created")) + .queue(); + }, error -> replyCallbackAction.setEmbeds(Util.genericErrorEmbed("Error", "Unable to send your auction to the auction channel")) + .queue()); + } + + private MessageEmbed buildAuctionEmbed(SlashCommandInteractionEvent event, int minimumIncrease) { + Member member = event.getMember(); + if (member == null) { + return handleBuildEmbedError(event, "You are not a member of this guild"); + } + EmbedBuilder embedBuilder = new EmbedBuilder(); + String item = event.getOption("item", OptionMapping::getAsString); + if (item == null) + return handleBuildEmbedError(event, "Missing required item option"); + if (item.length() > 128) + return handleBuildEmbedError(event, "Your item name is too long"); + embedBuilder.appendDescription("**Item**: " + item); + + Integer amount = event.getOption("amount", OptionMapping::getAsInt); + if (amount == null) + return handleBuildEmbedError(event, "Missing required amount option"); + embedBuilder.appendDescription("\n**Amount**: " + amount); + + Integer startingPrice = event.getOption("starting-price", OptionMapping::getAsInt); + if (startingPrice == null) + return handleBuildEmbedError(event, "Missing required starting price option"); + embedBuilder.appendDescription("\n**Starting Price**: $" + startingPrice); + + Integer instaBuy = event.getOption("insta-buy", OptionMapping::getAsInt); + if (instaBuy != null) { + if (instaBuy == minimumIncrease) + return handleBuildEmbedError(event, "Insta buy can't be the same as minimum increase"); + embedBuilder.appendDescription("\n**Insta Buy**: $" + instaBuy); + } + + + String description = event.getOption("description", OptionMapping::getAsString); + if (description != null) + embedBuilder.appendDescription("\n**Description**: " + description); + + embedBuilder + .setAuthor(member.getEffectiveName(), null, member.getAvatarUrl()) + .setTitle("Auction") + .setColor(Color.ORANGE) + .appendDescription("\n\nCloses "); + return embedBuilder.build(); + } + + private MessageEmbed handleBuildEmbedError(SlashCommandInteractionEvent event, String s) { + event.replyEmbeds(Util.genericErrorEmbed("Error", s)) + .setEphemeral(true) + .queue(); + return null; + } + + private void handleError(SlashCommandInteractionEvent event, String s) { + event.replyEmbeds(Util.genericErrorEmbed("Error", s)) + .setEphemeral(true) + .queue(); + } + + private TextChannel validTextChannel(SlashCommandInteractionEvent event, GuildChannel guildChannel) { + if (guildChannel == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "This server does not have a valid auction channel")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return null; + } + + if (!(guildChannel instanceof TextChannel channel)) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "A auction channel can't be of type: " + guildChannel.getType().name())) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return null; + } + return channel; + } + + @Override + public void suggest(CommandAutoCompleteInteractionEvent event) { + + } + + @Override + public String getHelpMessage() { + return null; + } + + @Override + public CommandData getCommandData() { + return commandData; + } +} diff --git a/src/main/java/com/alttd/database/DatabaseTables.java b/src/main/java/com/alttd/database/DatabaseTables.java index 24437ab..8a6ca85 100644 --- a/src/main/java/com/alttd/database/DatabaseTables.java +++ b/src/main/java/com/alttd/database/DatabaseTables.java @@ -135,6 +135,23 @@ public class DatabaseTables { } } + private void createAuctionTable() { + String sql = "CREATE TABLE IF NOT EXISTS auctions(" + + "message_id BIGINT NOT NULL, " + + "channel_id BIGINT NOT NULL, " + + "guild_id BIGINT NOT NULL, " + + "starting_price INT NOT NULL, " + + "expire_time BIGINT NOT NULL, " + + "PRIMARY KEY (message_id)" + + ")"; + try { + connection.prepareStatement(sql).executeUpdate(); + } catch (SQLException e) { + Logger.sql(e); + Logger.severe("Unable to create auction table, shutting down..."); + } + } + public static void createTables(Connection connection) { if (instance == null) instance = new DatabaseTables(connection); diff --git a/src/main/java/com/alttd/database/queries/QueriesAuctions/Auction.java b/src/main/java/com/alttd/database/queries/QueriesAuctions/Auction.java new file mode 100644 index 0000000..46cd639 --- /dev/null +++ b/src/main/java/com/alttd/database/queries/QueriesAuctions/Auction.java @@ -0,0 +1,91 @@ +package com.alttd.database.queries.QueriesAuctions; + +import com.alttd.AltitudeBot; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; +import java.time.Instant; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +public class Auction implements Comparable { + private final long messageId, channelId, guildId; + private final int startingPrice; + + private long expireTime; + + public Auction(long messageId, long channelId, long guildId, int startingPrice, long expireTime) { + this.messageId = messageId; + this.channelId = channelId; + this.guildId = guildId; + this.startingPrice = startingPrice; + this.expireTime = expireTime; + } + + public Auction(@NotNull Message message, long channelId, long guildId, int startingPrice, long expireTime) { + this.messageId = message.getIdLong(); + this.channelId = channelId; + this.guildId = guildId; + this.startingPrice = startingPrice; + this.expireTime = expireTime; + } + + public void updateMessage(Consumer success, @Nullable Consumer failure) { + Guild guild = AltitudeBot.getInstance().getJDA().getGuildById(guildId); + if (guild == null) { + if (failure != null) + failure.accept("Unable to retrieve auction message due to invalid guild"); + return; + } + + TextChannel textChannel = guild.getTextChannelById(channelId); + if (textChannel == null) { + if (failure != null) + failure.accept("Unable to retrieve auction message due to invalid text channel"); + return; + } + + textChannel.retrieveMessageById(messageId).queue(success, b -> { + if (failure != null) + failure.accept("Unable to retrieve the auction message"); + }); + } + + public long getExpireTime() { + return expireTime; + } + + public long getMessageId() { + return messageId; + } + + public long getChannelId() { + return channelId; + } + + public long getGuildId() { + return guildId; + } + + public int getStartingPrice() { + return startingPrice; + } + + public boolean updateExpiry() { + long future = Instant.now().toEpochMilli() + TimeUnit.MINUTES.toMillis(5); + if (expireTime < future) { + expireTime = future; + return true; + } + return false; + } + + @Override + public int compareTo(@NotNull Object o) { + Auction o1 = (Auction) o; + return Long.compare(expireTime, o1.getExpireTime()); + } +} diff --git a/src/main/java/com/alttd/database/queries/QueriesAuctions/QueriesAuction.java b/src/main/java/com/alttd/database/queries/QueriesAuctions/QueriesAuction.java new file mode 100644 index 0000000..b472664 --- /dev/null +++ b/src/main/java/com/alttd/database/queries/QueriesAuctions/QueriesAuction.java @@ -0,0 +1,67 @@ +package com.alttd.database.queries.QueriesAuctions; + +import com.alttd.database.Database; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; + +public class QueriesAuction { + + public static HashMap getAuctions() { + HashMap auctions = new HashMap<>(); + String sql = "SELECT * FROM auctions"; + try { + PreparedStatement statement = Database.getDatabase().getConnection().prepareStatement(sql); + + ResultSet resultSet = statement.executeQuery(); + while (resultSet.next()) { + long messageId = resultSet.getLong("message_id"); + long channelId = resultSet.getLong("channel_id"); + long guildId = resultSet.getLong("guild_id"); + int startingPrice = resultSet.getInt("starting_price"); + long expireTime = resultSet.getLong("expire_time"); + auctions.put(messageId, new Auction(messageId, channelId, guildId, startingPrice, expireTime)); + } + } catch (SQLException exception) { + exception.printStackTrace(); + return null; + } + return auctions; + } + + public static boolean saveAuction(Auction auction) { + String sql = "INSERT INTO auctions " + + "(message_id, channel_id, guild_id, starting_price, expire_time) " + + "VALUES (?, ?, ?, ?, ?) " + + "ON DUPLICATE KEY UPDATE expire_time = ?"; + try { + PreparedStatement statement = Database.getDatabase().getConnection().prepareStatement(sql); + statement.setLong(1, auction.getMessageId()); + statement.setLong(2, auction.getChannelId()); + statement.setLong(3, auction.getGuildId()); + statement.setInt(4, auction.getStartingPrice()); + statement.setLong(5, auction.getExpireTime()); + statement.setLong(6, auction.getExpireTime()); + + return statement.executeUpdate() == 1; + } catch (SQLException exception) { + exception.printStackTrace(); + } + return false; + } + + public static boolean removeAuction(Auction auction) { + String sql = "DELETE FROM auctions " + + "WHERE message_id = ?"; + try { + PreparedStatement statement = Database.getDatabase().getConnection().prepareStatement(sql); + statement.setLong(1, auction.getMessageId()); + return statement.executeUpdate() == 1; + } catch (SQLException exception) { + exception.printStackTrace(); + } + return false; + } +} diff --git a/src/main/java/com/alttd/database/queries/commandOutputChannels/OutputType.java b/src/main/java/com/alttd/database/queries/commandOutputChannels/OutputType.java index ce2b11b..0061a53 100644 --- a/src/main/java/com/alttd/database/queries/commandOutputChannels/OutputType.java +++ b/src/main/java/com/alttd/database/queries/commandOutputChannels/OutputType.java @@ -5,5 +5,7 @@ public enum OutputType { SUGGESTION_REVIEW, MOD_LOG, EVIDENCE, + AUCTION, + AUCTION_LOG, CRATE_TEAM } diff --git a/src/main/java/com/alttd/listeners/JDAListener.java b/src/main/java/com/alttd/listeners/JDAListener.java index 001ff6e..6388ad2 100644 --- a/src/main/java/com/alttd/listeners/JDAListener.java +++ b/src/main/java/com/alttd/listeners/JDAListener.java @@ -4,8 +4,10 @@ import com.alttd.buttonManager.ButtonManager; import com.alttd.commandManager.CommandManager; import com.alttd.contextMenuManager.ContextMenuManager; import com.alttd.modalManager.ModalManager; -import com.alttd.reminders.ReminderScheduler; +import com.alttd.schedulers.AuctionScheduler; +import com.alttd.schedulers.ReminderScheduler; import com.alttd.request.RequestManager; +import com.alttd.selectMenuManager.SelectMenuManager; import com.alttd.util.Logger; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.events.ReadyEvent; @@ -30,15 +32,23 @@ public class JDAListener extends ListenerAdapter { ButtonManager buttonManager = new ButtonManager(); ModalManager modalManager = new ModalManager(buttonManager); ContextMenuManager contextMenuManager = new ContextMenuManager(modalManager); - CommandManager commandManager = new CommandManager(jda, modalManager, contextMenuManager, chatListener); - jda.addEventListener(buttonManager, modalManager, commandManager, contextMenuManager, chatListener); - ReminderScheduler reminderScheduler = ReminderScheduler.getInstance(jda); - if (reminderScheduler == null) { - Logger.severe("Unable to start reminder scheduler!"); - } + SelectMenuManager selectMenuManager = new SelectMenuManager(); + CommandManager commandManager = new CommandManager(jda, modalManager, contextMenuManager, chatListener, selectMenuManager); + jda.addEventListener(buttonManager, modalManager, commandManager, contextMenuManager, chatListener, selectMenuManager); + startSchedulers(); // RequestManager.init(); } + private void startSchedulers() { + ReminderScheduler reminderScheduler = ReminderScheduler.getInstance(jda); + if (reminderScheduler == null) + Logger.severe("Unable to start reminder scheduler!"); + + AuctionScheduler auctionScheduler = AuctionScheduler.getInstance(); + if (auctionScheduler == null) + Logger.severe("Unable to start auction scheduler!"); + } + @Override public void onSelectMenuInteraction(@NotNull SelectMenuInteractionEvent event) { String s = event.getComponentId(); diff --git a/src/main/java/com/alttd/schedulers/AuctionScheduler.java b/src/main/java/com/alttd/schedulers/AuctionScheduler.java new file mode 100644 index 0000000..a725b8f --- /dev/null +++ b/src/main/java/com/alttd/schedulers/AuctionScheduler.java @@ -0,0 +1,135 @@ +package com.alttd.schedulers; + +import com.alttd.database.queries.QueriesAuctions.Auction; +import com.alttd.database.queries.QueriesAuctions.QueriesAuction; +import com.alttd.database.queries.commandOutputChannels.CommandOutputChannels; +import com.alttd.database.queries.commandOutputChannels.OutputType; +import com.alttd.util.Logger; +import com.alttd.util.Util; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; +import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; + +import javax.annotation.Nullable; +import java.awt.*; +import java.util.*; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class AuctionScheduler { + private static AuctionScheduler instance = null; + private final HashMap auctions; + private Auction nextAuction; + + private AuctionScheduler() { + instance = this; + auctions = QueriesAuction.getAuctions(); + if (auctions == null) { + Logger.severe("Unable to retrieve auctions"); + instance = null; + return; + } + setNextAuction(); + ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + scheduledExecutorService.scheduleWithFixedDelay(new AuctionScheduler.AuctionRun(), 0, 1, TimeUnit.SECONDS); + } + + private void setNextAuction() { + Optional first = auctions.values().stream().sorted().findFirst(); + if (first.isEmpty()) + nextAuction = null; + else + nextAuction = first.get(); + } + + public static AuctionScheduler getInstance() { + if (instance == null) + instance = new AuctionScheduler(); + return instance; + } + + public synchronized void addAuction(Auction auction) { + if (!QueriesAuction.saveAuction(auction)) + Logger.warning("Unable to save auction %", auction.getMessageId() + ""); + auctions.put(auction.getMessageId(), auction); + setNextAuction(); + } + + public synchronized void updateAuction(Auction auction) { + QueriesAuction.saveAuction(auction); + setNextAuction(); + } + + public synchronized void removeAuction(Auction auction) { + auctions.remove(auction.getMessageId()); + setNextAuction(); + if (!QueriesAuction.removeAuction(auction)) + Logger.warning("Unable to remove auction %", auction.getMessageId() + ""); + } + + public Auction getAuction(long messageId) { + return auctions.getOrDefault(messageId, null); + } + + public synchronized void finishAuction(Auction auction, @Nullable Member instaBuy) { + auction.updateMessage(success -> { + List embeds = success.getEmbeds(); + if (embeds.isEmpty()) { + Logger.warning("Received auction with no embed contents"); + return; + } + GuildChannel outputChannel = CommandOutputChannels.getOutputChannel(success.getGuild(), OutputType.AUCTION_LOG); + if (outputChannel != null) { + if (!(outputChannel instanceof GuildMessageChannel channel)) { + Logger.warning("Error" + outputChannel.getType().name() + " is not a valid crate auction log channel type"); + return; + } + if (!channel.canTalk()) { + Logger.warning("Error can't talk in auction log channel"); + return; + } + if (sendEmbed(embeds.get(0), channel, instaBuy)) + success.delete().queue(); + } else + success.delete().queue(); + }, Logger::warning); + removeAuction(auction); + } + + private boolean sendEmbed(MessageEmbed embed, GuildMessageChannel textChannel, Member instaBuy) { + EmbedBuilder embedBuilder = new EmbedBuilder(embed) + .clearFields(); + List fields = embed.getFields(); + if (instaBuy != null) + embedBuilder.addField("Winning Bid", "Insta bought by " + instaBuy.getAsMention(), false); + else if (!fields.isEmpty()) { + MessageEmbed.Field field = fields.get(0); + if (field.getName() != null && field.getName().equals("Current Bid") && field.getValue() != null) { + embedBuilder.addField("Winning Bid", field.getValue(), false); + } else if (fields.size() == 2) { + field = fields.get(1); + if (field.getName() != null && field.getName().equals("Current Bid") && field.getValue() != null) + embedBuilder.addField("Winning Bid", field.getValue(), false); + } + } + if (embedBuilder.getFields().size() != 0) + embedBuilder.setColor(Color.GREEN); + else + embedBuilder.setColor(Color.RED); + textChannel.sendMessageEmbeds(embedBuilder.build()).queue(Util::ignoreSuccess, failure -> Logger.warning("Failed to log auction result")); + return true; + } + private class AuctionRun implements Runnable { + @Override + public void run() { + long time = new Date().getTime(); + while (nextAuction != null && time > nextAuction.getExpireTime()) { + finishAuction(nextAuction, null); + } + } + } +} diff --git a/src/main/java/com/alttd/reminders/ReminderScheduler.java b/src/main/java/com/alttd/schedulers/ReminderScheduler.java similarity index 98% rename from src/main/java/com/alttd/reminders/ReminderScheduler.java rename to src/main/java/com/alttd/schedulers/ReminderScheduler.java index 89113b2..eb69d07 100644 --- a/src/main/java/com/alttd/reminders/ReminderScheduler.java +++ b/src/main/java/com/alttd/schedulers/ReminderScheduler.java @@ -1,4 +1,4 @@ -package com.alttd.reminders; +package com.alttd.schedulers; import com.alttd.database.queries.QueriesReminders.QueriesReminders; import com.alttd.database.queries.QueriesReminders.Reminder; @@ -10,7 +10,6 @@ import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; import net.dv8tion.jda.api.requests.RestAction; -import net.dv8tion.jda.api.utils.TimeUtil; import java.util.ArrayList; import java.util.Comparator; diff --git a/src/main/java/com/alttd/selectMenuManager/DiscordSelectMenu.java b/src/main/java/com/alttd/selectMenuManager/DiscordSelectMenu.java new file mode 100644 index 0000000..a206b6f --- /dev/null +++ b/src/main/java/com/alttd/selectMenuManager/DiscordSelectMenu.java @@ -0,0 +1,16 @@ +package com.alttd.selectMenuManager; + +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import net.dv8tion.jda.api.interactions.components.selections.SelectMenu; +import net.dv8tion.jda.api.interactions.components.selections.SelectOption; + +public abstract class DiscordSelectMenu { + + public abstract String getSelectMenuId(); + + public abstract void execute(SelectMenuInteractionEvent event); + + public abstract SelectMenu getSelectMenu(SelectOption ...selectOptions); +} diff --git a/src/main/java/com/alttd/selectMenuManager/SelectMenuManager.java b/src/main/java/com/alttd/selectMenuManager/SelectMenuManager.java new file mode 100644 index 0000000..c8c0a75 --- /dev/null +++ b/src/main/java/com/alttd/selectMenuManager/SelectMenuManager.java @@ -0,0 +1,52 @@ +package com.alttd.selectMenuManager; + +import com.alttd.selectMenuManager.selectMenus.SelectMenuAuction; +import com.alttd.util.Util; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.requests.RestAction; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.util.List; +import java.util.Optional; + +public class SelectMenuManager extends ListenerAdapter { + + private final List buttons; + + public SelectMenuManager() { + buttons = List.of(new SelectMenuAuction()); + } + + @Override + public void onSelectMenuInteraction(@NotNull SelectMenuInteractionEvent event) { + String selectMenuId = event.getSelectMenu().getId(); + Optional first = buttons.stream() + .filter(discordModal -> discordModal.getSelectMenuId().equalsIgnoreCase(selectMenuId)) + .findFirst(); + if (first.isEmpty()) { + event.replyEmbeds(new EmbedBuilder() + .setTitle("Invalid command") + .setDescription("Unable to process select menu with id: [" + selectMenuId + "], please report this issue to a Teri") + .setColor(Color.RED) + .build()) + .setEphemeral(true) + .queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + first.get().execute(event); + } + + public @Nullable DiscordSelectMenu getDiscordSelectMenuFor(String buttonId) { + Optional first = buttons.stream() + .filter(discordSelectMenu -> discordSelectMenu.getSelectMenuId().equalsIgnoreCase(buttonId)) + .findFirst(); + if (first.isEmpty()) + return null; + return first.get(); + } + +} diff --git a/src/main/java/com/alttd/selectMenuManager/selectMenus/SelectMenuAuction.java b/src/main/java/com/alttd/selectMenuManager/selectMenus/SelectMenuAuction.java new file mode 100644 index 0000000..6f0ca02 --- /dev/null +++ b/src/main/java/com/alttd/selectMenuManager/selectMenus/SelectMenuAuction.java @@ -0,0 +1,217 @@ +package com.alttd.selectMenuManager.selectMenus; + +import com.alttd.database.queries.QueriesAuctions.Auction; +import com.alttd.database.queries.QueriesAuctions.QueriesAuction; +import com.alttd.schedulers.AuctionScheduler; +import com.alttd.selectMenuManager.DiscordSelectMenu; +import com.alttd.util.Util; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Mentions; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent; +import net.dv8tion.jda.api.interactions.components.selections.SelectMenu; +import net.dv8tion.jda.api.interactions.components.selections.SelectOption; +import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction; + +import java.awt.*; +import java.time.Instant; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class SelectMenuAuction extends DiscordSelectMenu { + @Override + public String getSelectMenuId() { + return "auction"; + } + + @Override + public void execute(SelectMenuInteractionEvent event) { + Member member = event.getMember(); + if (member == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "This can only be used in a guild")) + .setEphemeral(true).queue(); + return; + } + AuctionScheduler auctionScheduler = AuctionScheduler.getInstance(); + if (auctionScheduler == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to get scheduler")) + .setEphemeral(true).queue(); + return; + } + Auction auction = auctionScheduler.getAuction(event.getMessage().getIdLong()); + if (auction == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to find this auction in the database, please try again")) + .setEphemeral(true).queue(); + return; + } + + List collect = event.getInteraction().getSelectedOptions().stream().filter(opt -> !opt.isDefault()).collect(Collectors.toList()); + if (collect.isEmpty()) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Received default input")) + .setEphemeral(true).queue(); + return; + } + if (collect.size() != 1) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Received invalid number of inputs, can only handle one")) + .setEphemeral(true).queue(); + return; + } + + SelectOption selectOption = collect.get(0); + String value = selectOption.getValue(); + int bid; + try { + bid = Integer.parseInt(value); + } catch (NumberFormatException e) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Received invalid input")) + .setEphemeral(true).queue(); + return; + } + + MessageEmbed messageEmbed = getMessageEmbed(event.getMessage().getEmbeds(), event); + if (messageEmbed == null) + return; + BidFieldInfo bidFieldInfo = getPreviousBid(messageEmbed, event); + if (bidFieldInfo == null) + return; + int prevBid; + if (bidFieldInfo.bid() == 0) + prevBid = auction.getStartingPrice(); + else + prevBid = bidFieldInfo.bid(); + + int currentBid = prevBid + bid; + if (selectOption.getLabel().startsWith("Insta Buy")) { + auctionScheduler.finishAuction(auction, member); + event.replyEmbeds(Util.genericSuccessEmbed("Success", "You successfully insta bought the item for " + bid + "!")) + .setEphemeral(true).queue(); + return; + } + + EmbedBuilder embedBuilder = new EmbedBuilder(messageEmbed) + .clearFields() + .setTimestamp(Instant.now()); + if (auction.updateExpiry()) { + auctionScheduler.updateAuction(auction); + String description = messageEmbed.getDescription(); + if (description != null) + embedBuilder.setDescription(description.substring(0, description.lastIndexOf("Closes "); + } + if (bidFieldInfo.member() != null) + embedBuilder.addField("Previous Bid", "$" + bidFieldInfo.bid() + " by <@" + bidFieldInfo.member() + ">", false); + embedBuilder.addField("Current Bid", "$" + currentBid + " by " + event.getMember().getAsMention(), false); + ReplyCallbackAction replyCallbackAction = event.deferReply(true); + + int finalPrevBid = prevBid; + event.getMessage().editMessageEmbeds(embedBuilder.build()).queue( + success -> { + if (auction.updateExpiry()) + auctionScheduler.updateAuction(auction); + replyCallbackAction.setEmbeds(Util.genericSuccessEmbed("Success", "You successfully increased the bid from " + finalPrevBid + " to " + currentBid + "!")) + .queue(); + }, + error -> replyCallbackAction.setEmbeds(Util.genericErrorEmbed("Error", "Unable to finish your bid")).queue()); + } + + /** + * Expecting to find: + * a: No fields + * b: One field containing the current bid + * c: Two fields, one containing the current bid and one for the previous bid + *

+ * option a: return BidFieldInfo with the bid set to 0 and member to null + * option b: check if field matches roughly this: + * Name: Current Bid + * Value: 160 by <@212303885988134914> + * if it does set the BidFieldInfo value to 160, and attempt ot get the user from the <@ string, + * set member to the member belonging to that user id or null if we can't find it + * option c: Find the field with the name Current Bid and proceed with that field as if it was option b + * @param messageEmbed Embed to find the fields for + * @param event Event that we need to respond to for errors + * @return BidFieldInfo or null if there was an error + */ + private BidFieldInfo getPreviousBid(MessageEmbed messageEmbed, SelectMenuInteractionEvent event) { + List fields = messageEmbed.getFields(); + if (fields.size() > 2) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "This auction embed has the wrong number of fields")) + .setEphemeral(true).queue(); + return null; + } + + if (fields.isEmpty()) + return new BidFieldInfo(0, null); + + MessageEmbed.Field field = fields.get(0); + if (field.getName() == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Found field with no name")) + .setEphemeral(true).queue(); + return null; + } + + if (!field.getName().equals("Current Bid")) { + if (fields.size() != 2) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Found one field and it's not the right one")) + .setEphemeral(true).queue(); + return null; + } + field = fields.get(1); + if (field.getName() == null || !field.getName().equals("Current Bid")) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to find current bid but it should be there")) + .setEphemeral(true).queue(); + return null; + } + } + String value1 = field.getValue(); + if (value1 == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Received default input")) + .setEphemeral(true).queue(); + return null; + } + String[] s = value1.split(" "); + if (s.length < 3) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Field has incorrect input")) + .setEphemeral(true).queue(); + return null; + } + + int bid; + try { + bid = Integer.parseInt(s[0].substring(1)); + } catch (NumberFormatException e) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Field had the wrong value")) + .setEphemeral(true).queue(); + return null; + } + + String substring = s[2].substring(2, s[2].length() - 1); + try { + long l = Long.parseLong(substring); + + return new BidFieldInfo(bid, l); + } catch (NumberFormatException e) { + return new BidFieldInfo(bid, null); + } + } + + private MessageEmbed getMessageEmbed(List embeds, SelectMenuInteractionEvent event) { + if (embeds.isEmpty()) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Input came from message with no embeds")) + .setEphemeral(true).queue(); + return null; + } + return embeds.get(0); + } + + @Override + public SelectMenu getSelectMenu(SelectOption... selectOptions) { + SelectMenu.Builder builder = SelectMenu.create(getSelectMenuId()); + if (selectOptions != null) + builder.addOptions(selectOptions); + return builder.build(); + } +} + +record BidFieldInfo(int bid, Long member) {}