From 0ae6645c765ed7a8c3b572d9d5513c7298cab7a6 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 8 Aug 2026 16:14:29 -0600 Subject: [PATCH 01/21] Preserve disabled vote site configuration --- .../votesites/VoteSiteManager.java | 33 +++++++++++++++++++ .../tests/votesite/VoteSiteManagerTest.java | 16 +++++++++ 2 files changed, 49 insertions(+) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSiteManager.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSiteManager.java index 109c089ec..e87f562da 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSiteManager.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSiteManager.java @@ -119,6 +119,27 @@ public String getVoteSiteName(boolean checkEnabled, String... urls) { } } + if (!checkEnabled) { + ArrayList configuredSites = plugin.getConfigVoteSites().getVoteSitesNames(false); + if (configuredSites != null) { + for (String url : urls) { + if (url == null) { + return null; + } + + for (String siteName : configuredSites) { + String serviceSite = plugin.getConfigVoteSites().getServiceSite(siteName); + String displayName = plugin.getConfigVoteSites().getDisplayName(siteName); + if (siteName.equalsIgnoreCase(url) + || (serviceSite != null && serviceSite.equalsIgnoreCase(url)) + || (displayName != null && displayName.equalsIgnoreCase(url))) { + return siteName; + } + } + } + } + } + for (String url : urls) { return url; } @@ -214,6 +235,18 @@ public String getVoteSiteServiceSite(String name) { */ public boolean hasVoteSite(String site) { String siteName = getVoteSiteName(false, site); + if (siteName == null) { + return false; + } + + ArrayList configuredSites = plugin.getConfigVoteSites().getVoteSitesNames(false); + if (configuredSites != null) { + for (String configuredSite : configuredSites) { + if (configuredSite.equalsIgnoreCase(siteName)) { + return true; + } + } + } for (VoteSite voteSite : getVoteSites()) { if (voteSite.getKey().equalsIgnoreCase(siteName)) { diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteManagerTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteManagerTest.java index 2f9db5206..d3aca2598 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteManagerTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteManagerTest.java @@ -216,6 +216,22 @@ public void testHasVoteSiteTrueWhenPresent() { assertTrue(manager.hasVoteSite("site_test")); } + @Test + public void testDisabledConfiguredVoteSiteIsNotAutoCreated() { + when(configFile.isAutoCreateVoteSites()).thenReturn(true); + when(voteSitesConfig.getVoteSitesNames(false)) + .thenReturn(new ArrayList(Arrays.asList("DisabledSite"))); + when(voteSitesConfig.getServiceSite("DisabledSite")).thenReturn("disabled.example.com"); + when(voteSitesConfig.getDisplayName("DisabledSite")).thenReturn("Disabled Site"); + + manager.setVoteSites(Collections.synchronizedList(new ArrayList())); + + assertEquals("DisabledSite", manager.getVoteSiteName(false, "disabled.example.com")); + assertTrue(manager.hasVoteSite("disabled.example.com")); + assertNull(manager.getVoteSite("disabled.example.com", true)); + verify(voteSitesConfig, never()).tryGenerateVoteSite(anyString()); + } + @Test public void testIsVoteSiteTrueWhenKeyPresent() { VoteSite site = new VoteSite(plugin, "site.test"); From d9a2aa76c0d2fc921f1ccd3d79129f430d4579ba Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 8 Aug 2026 18:14:54 -0600 Subject: [PATCH 02/21] Skip queued offline votes for disabled sites --- .../votingplugin/user/VotingPluginUser.java | 3991 +++++++++-------- 1 file changed, 1998 insertions(+), 1993 deletions(-) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java index c7907b0f4..abd292c9f 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java @@ -1,1993 +1,1998 @@ -package com.bencodez.votingplugin.user; - -import java.text.SimpleDateFormat; -import java.time.Duration; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.UUID; -import java.util.stream.Collectors; - -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; - -import com.bencodez.advancedcore.api.messages.PlaceholderUtils; -import com.bencodez.advancedcore.api.misc.MiscUtils; -import com.bencodez.advancedcore.api.rewards.RewardBuilder; -import com.bencodez.advancedcore.api.rewards.RewardOptions; -import com.bencodez.advancedcore.api.user.AdvancedCoreUser; -import com.bencodez.simpleapi.messages.MessageAPI; -import com.bencodez.simpleapi.sql.data.DataValue; -import com.bencodez.simpleapi.sql.data.DataValueInt; -import com.bencodez.simpleapi.time.ParsedDuration; -import com.bencodez.votingplugin.VotingPluginMain; -import com.bencodez.votingplugin.events.PlayerReceivePointsEvent; -import com.bencodez.votingplugin.events.PlayerSpecialRewardEvent; -import com.bencodez.votingplugin.events.PlayerVoteEvent; -import com.bencodez.votingplugin.events.SpecialRewardType; -import com.bencodez.votingplugin.proxy.VoteTotalsSnapshot; -import com.bencodez.votingplugin.topvoter.TopVoter; -import com.bencodez.votingplugin.topvoter.TopVoterPlayer; -import com.bencodez.votingplugin.votesites.NextSite; -import com.bencodez.votingplugin.votesites.VoteSite; - -/** - * The Class VotingPluginUser. This class represents a user in the VotingPlugin - * system. It extends the AdvancedCoreUser class and provides additional - * functionality specific to the VotingPlugin. - */ -public class VotingPluginUser extends com.bencodez.advancedcore.api.user.AdvancedCoreUser { - - /** The plugin instance. */ - private VotingPluginMain plugin; - - /** - * Instantiates a new VotingPluginUser. - * - * @param plugin the plugin instance - * @param user the AdvancedCoreUser instance - */ - public VotingPluginUser(VotingPluginMain plugin, AdvancedCoreUser user) { - super(plugin, user); - this.plugin = plugin; - } - - /** - * Instantiates a new VotingPluginUser. - * - * @param plugin the plugin instance - * @param player the player instance - * @deprecated Use {@link #VotingPluginUser(VotingPluginMain, AdvancedCoreUser)} - * instead. - */ - @Deprecated - public VotingPluginUser(VotingPluginMain plugin, Player player) { - super(plugin, player); - this.plugin = plugin; - } - - /** - * Instantiates a new VotingPluginUser. - * - * @param plugin the plugin instance - * @param playerName the player name - * @deprecated Use {@link #VotingPluginUser(VotingPluginMain, AdvancedCoreUser)} - * instead. - */ - @Deprecated - public VotingPluginUser(VotingPluginMain plugin, String playerName) { - super(plugin, playerName); - this.plugin = plugin; - } - - /** - * Instantiates a new VotingPluginUser. - * - * @param plugin the plugin instance - * @param uuid the UUID of the player - * @deprecated Use {@link #VotingPluginUser(VotingPluginMain, AdvancedCoreUser)} - * instead. - */ - @Deprecated - public VotingPluginUser(VotingPluginMain plugin, UUID uuid) { - super(plugin, uuid); - this.plugin = plugin; - } - - /** - * Instantiates a new VotingPluginUser. - * - * @param plugin the plugin instance - * @param uuid the UUID of the player - * @param loadName whether to load the player name - * @deprecated Use {@link #VotingPluginUser(VotingPluginMain, AdvancedCoreUser)} - * instead. - */ - @Deprecated - public VotingPluginUser(VotingPluginMain plugin, UUID uuid, boolean loadName) { - super(plugin, uuid, loadName); - this.plugin = plugin; - } - - /** - * Instantiates a new VotingPluginUser. - * - * @param plugin the plugin instance - * @param uuid the UUID of the player - * @param playerName the player name - * @deprecated Use {@link #VotingPluginUser(VotingPluginMain, AdvancedCoreUser)} - * instead. - */ - @Deprecated - public VotingPluginUser(VotingPluginMain plugin, UUID uuid, String playerName) { - super(plugin, uuid, playerName); - this.plugin = plugin; - } - - /** - * Adds one to the all-time total votes. - */ - public void addAllTimeTotal() { - setAllTimeTotal(getAllTimeTotal() + 1); - } - - /** - * Adds one to the daily vote streak. - */ - @Deprecated - public void addDayVoteStreak() { - setDayVoteStreak(getDayVoteStreak() + 1); - } - - /** - * Adds one to the monthly total votes. - */ - public void addMonthTotal() { - setMonthTotal(getMonthTotal() + 1); - } - - /** - * Adds one to the monthly vote streak. - */ - @Deprecated - public void addMonthVoteStreak() { - setMonthVoteStreak(getMonthVoteStreak() + 1); - } - - /** - * Adds an offline vote for the specified vote site. - * - * @param voteSiteName the name of the vote site - */ - public void addOfflineVote(String voteSiteName) { - ArrayList offlineVotes = getOfflineVotes(); - offlineVotes.add(voteSiteName); - setOfflineVotes(offlineVotes); - } - - /** - * Adds points to the user based on the configuration. - */ - public void addPoints() { - int points = plugin.getConfigFile().getPointsOnVote(); - if (points != 0) { - addPoints(points); - } - if (plugin.getConfigFile().getLimitVotePoints() > 0) { - if (getPoints() > plugin.getConfigFile().getLimitVotePoints()) { - setPoints(plugin.getConfigFile().getLimitVotePoints()); - } - } - } - - /** - * Adds the specified number of points to the user. - * - * @param value the number of points to add - * @return the current total points - */ - public int addPoints(int value) { - return addPoints(value, false); - } - - /** - * Adds the specified number of points to the user, optionally asynchronously. - * - * @param value the number of points to add - * @param async whether to add the points asynchronously - * @return the current total points - */ - public synchronized int addPoints(int value, boolean async) { - PlayerReceivePointsEvent event = new PlayerReceivePointsEvent(this, value); - Bukkit.getPluginManager().callEvent(event); - - if (event.isCancelled()) { - return getPoints(); - } - int newTotal = getPoints() + event.getPoints(); - setPoints(newTotal, async); - return newTotal; - } - - /** - * Adds one to the total votes. - */ - public void addTotal() { - addMonthTotal(); - addAllTimeTotal(); - } - - /** - * Adds one to the daily total votes. - */ - public void addTotalDaily() { - setDailyTotal(getDailyTotal() + 1); - } - - /** - * Adds one to the weekly total votes. - */ - public void addTotalWeekly() { - setWeeklyTotal(getWeeklyTotal() + 1); - } - - /** - * Adds one to the weekly vote streak. - */ - @Deprecated - public void addWeekVoteStreak() { - setWeekVoteStreak(getWeekVoteStreak() + 1); - } - - /** - * Handles a plugin messaging bungee vote. - * - * @param service the service name - * @param time the vote time - * @param text the bungee message data - * @param setTotals whether to set the totals - * @param wasOnline whether the player was online - * @param broadcast whether to broadcast the vote - * @param num the vote number - */ - public void bungeeVotePluginMessaging(String service, long time, VoteTotalsSnapshot text, boolean setTotals, - boolean wasOnline, boolean broadcast, int num) { - if (plugin.getBungeeSettings().isUseBungeecoord()) { - plugin.debug("Pluginmessaging vote for " + getPlayerName() + " on " + service); - - PlayerVoteEvent voteEvent = new PlayerVoteEvent(plugin.getVoteSiteManager().getVoteSite(service, true), - getPlayerName(), service, true); - voteEvent.setBungee(true); - voteEvent.setVotingPluginUser(this); - voteEvent.setForceBungee(true); - voteEvent.setTime(time); - voteEvent.setAddTotals(setTotals); - voteEvent.setBungeeTextTotals(text); - voteEvent.setWasOnline(wasOnline); - voteEvent.setBroadcast(broadcast); - voteEvent.setVoteNumber(num); - plugin.getServer().getPluginManager().callEvent(voteEvent); - } - } - - /** - * Checks if the user can vote on all sites. - * - * @return true, if the user can vote on all sites - */ - public boolean canVoteAll() { - for (VoteSite voteSite : plugin.getVoteSiteManager().getVoteSitesEnabled()) { - if (!voteSite.isHidden()) { - boolean canVote = canVoteSite(voteSite); - if (!canVote) { - return false; - } - } - } - return true; - } - - /** - * Checks if the user can vote on any site. - * - * @return true, if the user can vote on any site - */ - public boolean canVoteAny() { - for (VoteSite voteSite : plugin.getVoteSiteManager().getVoteSitesEnabled()) { - if (!voteSite.isIgnoreCanVote() && !voteSite.isHidden()) { - boolean canVote = canVoteSite(voteSite); - if (canVote) { - return true; - } - } - } - return false; - } - - /** - * Checks if the user can vote on the specified site. - * - * @param voteSite the vote site - * @return true, if the user can vote on the site - */ - public boolean canVoteSite(VoteSite voteSite) { - long time = getTime(voteSite); - if (time == 0) { - return true; - } - try { - LocalDateTime now = plugin.getTimeChecker().getTime(); - LocalDateTime lastVote = LocalDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneId.systemDefault()) - .plusHours(plugin.getOptions().getTimeHourOffSet()); - - if (!voteSite.isVoteDelayDaily()) { - ParsedDuration voteDelay = voteSite.getVoteDelay(); - - // Preserve old behavior: if delay is 0, you can never vote again (unless daily - // reset mode) - if (voteDelay == null || voteDelay.isEmpty() || voteDelay.getMillis() == 0L) { - return false; - } - - LocalDateTime nextVote = lastVote.plus(Duration.ofMillis(voteDelay.getMillis())); - return now.isAfter(nextVote); - } - - // Daily reset logic unchanged - LocalDateTime resetTime = lastVote.withHour(voteSite.getVoteDelayDailyHour()).withMinute(0).withSecond(0); - LocalDateTime resetTimeTomorrow = resetTime.plusHours(24); - - if (lastVote.isBefore(resetTime)) { - return now.isAfter(resetTime); - } else { - return now.isAfter(resetTimeTomorrow); - } - } catch (Exception e) { - e.printStackTrace(); - } - return false; - } - - /** - * Checks if the user has voted on all sites. - * - * @return true, if the user has voted on all sites - */ - public boolean checkAllVotes() { - VotingPluginUser user = this; - - ArrayList months = new ArrayList<>(); - ArrayList days = new ArrayList<>(); - - for (VoteSite voteSite : plugin.getVoteSiteManager().getVoteSitesEnabled()) { - if (voteSite.isEnabled() && !voteSite.isHidden()) { - long time = user.getTime(voteSite); - if (time == 0) { - return false; - } - months.add(MiscUtils.getInstance().getMonthFromMili(time)); - days.add(MiscUtils.getInstance().getDayFromMili(time)); - } - } - - // check months - for (Integer month : months) { - if (!months.get(0).equals(month)) { - return false; - } - } - - // check days - for (Integer day : days) { - if (!days.get(0).equals(day)) { - return false; - } - } - - return true; - } - - /** - * Checks if the user has voted on almost all sites. - * - * @return true, if the user has voted on almost all sites - */ - public boolean checkAlmostAllVotes() { - if (getSitesNotVotedOn() <= 1) { - return true; - } - return false; - } - - /** - * Checks the day vote streak and updates it if necessary. - * - * @param forceBungee whether to force bungee - */ - @Deprecated - public void checkDayVoteStreak(boolean forceBungee) { - if (!voteStreakUpdatedToday(LocalDateTime.now())) { - if (!plugin.getSpecialRewardsConfig().isVoteStreakRequirementUsePercentage() || hasPercentageTotal( - TopVoter.Daily, plugin.getSpecialRewardsConfig().getVoteStreakRequirementDay(), null)) { - plugin.extraDebug("Adding day vote streak to " + getUUID() + " " - + plugin.getSpecialRewardsConfig().isVoteStreakRequirementUsePercentage() + " " - + hasPercentageTotal(TopVoter.Daily, - plugin.getSpecialRewardsConfig().getVoteStreakRequirementDay(), null)); - addDayVoteStreak(); - plugin.getSpecialRewards().checkVoteStreak(null, this, "Day", forceBungee); - setDayVoteStreakLastUpdate(System.currentTimeMillis()); - } - } - } - - /** - * Clears the offline votes. - */ - public void clearOfflineVotes() { - setOfflineVotes(new ArrayList<>()); - setOfflineRewards(new ArrayList<>()); - } - - /** - * Clears the total votes for all top voter categories. - */ - public void clearTotals() { - for (TopVoter top : TopVoter.values()) { - resetTotals(top); - } - } - - /** - * Gets the all-time total votes. - * - * @return the all-time total votes - * @deprecated Use getTotal(TopVoter.AllTime) when able instead - */ - @Deprecated - public int getAllTimeTotal() { - return getTotal(TopVoter.AllTime); - } - - /** - * Gets the best day vote streak. - * - * @return the best day vote streak - */ - @Deprecated - public int getBestDayVoteStreak() { - return getData().getInt("BestDayVoteStreak"); - } - - /** - * Gets the best month vote streak. - * - * @return the best month vote streak - */ - @Deprecated - public int getBestMonthVoteStreak() { - return getData().getInt("BestMonthVoteStreak"); - } - - /** - * Gets the best week vote streak. - * - * @return the best week vote streak - */ - @Deprecated - public int getBestWeekVoteStreak() { - return getData().getInt("BestWeekVoteStreak"); - } - - /** - * Checks if the cooldown check is enabled. - * - * @return true, if the cooldown check is enabled - */ - public boolean getCoolDownCheck() { - return getData().getBoolean(getCoolDownCheckPath()); - } - - /** - * Gets the path for the cooldown check. - * - * @return the cooldown check path - */ - public String getCoolDownCheckPath() { - if (plugin.getBungeeSettings().isUseBungeecoord()) { - return "CoolDownCheck_" + plugin.getBungeeSettings().getServerNameStorage(); - } - return "CoolDownCheck"; - } - - /** - * Checks if the cooldown check is enabled for a specific vote site. - * - * @param site the vote site - * @return true, if the cooldown check is enabled for the site - */ - public boolean getCoolDownCheckSite(VoteSite site) { - HashMap coolDownChecks = getCoolDownCheckSiteList(); - if (coolDownChecks.containsKey(site.getKey())) { - return coolDownChecks.get(site.getKey()).booleanValue(); - } - return false; - } - - /** - * Gets the list of cooldown checks for all vote sites. - * - * @return the list of cooldown checks for all vote sites - */ - public HashMap getCoolDownCheckSiteList() { - HashMap coolDownChecks = new HashMap<>(); - ArrayList coolDownCheck = getData().getStringList(getCoolDownCheckSitePath()); - for (String str : coolDownCheck) { - String[] data = str.split("//"); - if (data.length > 1 && plugin.getVoteSiteManager().hasVoteSite(data[0])) { - VoteSite site = plugin.getVoteSiteManager().getVoteSite(data[0], true); - if (site != null) { - Boolean b = Boolean.valueOf(data[1]); - coolDownChecks.put(site.getKey(), b); - } - } - } - return coolDownChecks; - } - - /** - * Gets the path for the cooldown check site list. - * - * @return the cooldown check site list path - */ - public String getCoolDownCheckSitePath() { - if (plugin.getBungeeSettings().isUseBungeecoord()) { - return "CoolDownCheck_" + plugin.getBungeeSettings().getServerNameStorage() + "_Sites"; - } - return "CoolDownCheck" + "_Sites"; - } - - /** - * Gets the daily total votes. - * - * @return the daily total votes - * @deprecated Use getTotal(TopVoter.Daily) instead - */ - @Deprecated - public int getDailyTotal() { - return getTotal(TopVoter.Daily); - } - - /** - * Gets the day vote streak. - * - * @return the day vote streak - */ - @Deprecated - public int getDayVoteStreak() { - return getData().getInt("DayVoteStreak"); - } - - /** - * Gets the last update time for the day vote streak. - * - * @return the last update time for the day vote streak - */ - @Deprecated - public long getDayVoteStreakLastUpdate() { - String str = getData().getString("DayVoteStreakLastUpdate"); - if (str == null || str.isEmpty() || str.equals("null")) { - return 0; - } - try { - return Long.parseLong(str); - } catch (NumberFormatException e) { - return 0; - } - } - - /** - * Checks if the broadcast is disabled. - * - * @return true if the broadcast is disabled, false otherwise - */ - public boolean getDisableBroadcast() { - return getUserData().getBoolean("DisableBroadcast"); - } - - /** - * Gets the day when the user has gotten all sites. - * - * @return the day when the user has gotten all sites - */ - public int getGottenAllSitesDay() { - return getData().getInt(plugin.getVotingPluginUserManager().getGottenAllSitesDayPath(), 0); - } - - /** - * Gets the day when the user has gotten almost all sites. - * - * @return the day when the user has gotten almost all sites - */ - public int getGottenAlmostAllSitesDay() { - return getData().getInt(plugin.getVotingPluginUserManager().getGottenAlmostAllSitesDayPath(), 0); - } - - /** - * Gets the highest daily total votes. - * - * @return the highest daily total votes - */ - public int getHighestDailyTotal() { - return getData().getInt("HighestDailyTotal"); - } - - /** - * Returns whether this user has already claimed the NameMC like reward. - * - * @return true if claimed - */ - public boolean hasClaimedNameMCLikeReward() { - return getUserData().getBoolean("NameMCLikeRewardClaimed"); - } - - /** - * Sets whether this user has already claimed the NameMC like reward. - * - * @param claimed true if claimed - */ - public void setClaimedNameMCLikeReward(boolean claimed) { - getUserData().setBoolean("NameMCLikeRewardClaimed", claimed); - } - - /** - * Gets the highest monthly total votes. - * - * @return the highest monthly total votes - */ - public int getHighestMonthlyTotal() { - return getData().getInt("HighestMonthlyTotal"); - } - - /** - * Gets the highest weekly total votes. - * - * @return the highest weekly total votes - */ - public int getHighestWeeklyTotal() { - return getData().getInt("HighestWeeklyTotal"); - } - - /** - * Gets the total votes for the last month. - * - * @return the total votes for the last month - */ - public int getLastMonthTotal() { - return getData().getInt("LastMonthTotal"); - } - - /** - * Gets the last votes for each vote site. - * - * @return a map of vote sites and the last vote time - */ - public HashMap getLastVotes() { - HashMap lastVotes = new HashMap<>(); - ArrayList lastVotesList = getUserData().getStringList("LastVotes"); - - for (String str : lastVotesList) { - String[] data = str.split("//"); - if (data.length <= 1) { - continue; - } - - String rawSiteKey = data[0]; - String rawTime = data[1]; - - if (!plugin.getVoteSiteManager().hasVoteSite(rawSiteKey)) { - continue; - } - - VoteSite site = plugin.getVoteSiteManager().getVoteSite(rawSiteKey, true); - if (site == null) { - continue; - } - - long time = 0; - try { - time = Long.parseLong(rawTime); - } catch (NumberFormatException ignored) { - time = 0; - } - - lastVotes.put(site, time); - } - - return lastVotes; - } - - /** - * Gets the time of the last vote. - * - * @return the time of the last vote - */ - public Long getLastVoteTime() { - Long time = Long.valueOf(0); - for (Long value : getLastVotes().values()) { - if (value.longValue() > time) { - time = value; - } - } - return time; - } - - /** - * Gets the last vote time for a specific vote site. - * - * @param voteSite the vote site - * @return the last vote time for the vote site - */ - public long getLastVoteTimer(VoteSite voteSite) { - HashMap times = getLastVotes(); - if (times.containsKey(voteSite)) { - return times.get(voteSite).longValue(); - } - return 0; - } - - /** - * Gets the last vote times sorted in descending order. - * - * @return a map of vote sites and the last vote times sorted in descending - * order - */ - public HashMap getLastVoteTimesSorted() { - LinkedHashMap times = new LinkedHashMap<>(); - - for (VoteSite voteSite : plugin.getVoteSiteManager().getVoteSitesEnabled()) { - times.put(voteSite, getTime(voteSite)); - } - LinkedHashMap sorted = new LinkedHashMap<>( - times.entrySet().stream().sorted(Collections.reverseOrder(Map.Entry.comparingByValue())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); - return sorted; - } - - /** - * Gets the total votes for the month. - * - * @return the total votes for the month - * @deprecated Use getTotal(TopVoter.Monthly) instead - */ - @Deprecated - public int getMonthTotal() { - return getTotal(TopVoter.Monthly); - } - - /** - * Gets the month vote streak. - * - * @return the month vote streak - */ - @Deprecated - public int getMonthVoteStreak() { - return getData().getInt("MonthVoteStreak"); - } - - /** - * Gets the next time all sites are available for voting. - * - * @return the next time all sites are available for voting - */ - public long getNextTimeAllSitesAvailable() { - long longest = 0; - for (VoteSite site : plugin.getVoteSiteManager().getVoteSitesEnabled()) { - long seconds = voteNextDurationTime(site); - if (seconds > longest) { - longest = seconds; - } - } - - return longest; - } - - /** - * Gets the next time the first site is available for voting. - * - * @return seconds until first site is available, or 0 if none - */ - public long getNextTimeFirstSiteAvailable() { - NextSite next = getNextSiteAvailable(); - return next == null ? 0 : next.getSecondsUntilAvailable(); - } - - /** - * Returns the next VoteSite that will become available, and how many seconds - * until it is available. - * - * - Only considers enabled sites. - Skips hidden sites (matching - * canVoteAll/canVoteAny intent for player-facing voting). - Only considers - * sites the player CANNOT currently vote on (seconds > 0). - * - * @return NextSite or null if there is no upcoming site (i.e. can vote all, or - * no delays) - */ - public NextSite getNextSiteAvailable() { - List sites = plugin.getVoteSiteManager().getVoteSitesEnabled(); - if (sites == null || sites.isEmpty()) { - return null; - } - - VoteSite bestSite = null; - long bestSeconds = 0; - - for (int i = 0; i < sites.size(); i++) { - VoteSite site = sites.get(i); - if (site == null) { - continue; - } - - // Match the same "don't care" sites as your other checks generally do - if (!site.isEnabled() || site.isHidden()) { - continue; - } - - // If you can already vote, it isn't "next" - if (canVoteSite(site)) { - continue; - } - - long seconds = voteNextDurationTime(site); // uses getTime(site) internally - if (seconds <= 0) { - continue; - } - - if (bestSite == null || seconds < bestSeconds) { - bestSite = site; - bestSeconds = seconds; - } - } - - return bestSite == null ? null : new NextSite(bestSite, bestSeconds); - } - - /** - * Gets the number of offline votes for the specified vote site. - * - * @param site the vote site - * @return the number of offline votes for the specified vote site - */ - public int getNumberOfOfflineVotes(VoteSite site) { - ArrayList offlineVotes = getOfflineVotes(); - int num = 0; - for (String str : offlineVotes) { - if (str.equals(site.getKey())) { - num++; - } - } - return num; - } - - /** - * Gets the list of offline votes. - * - * @return the list of offline votes - */ - public ArrayList getOfflineVotes() { - return getUserData().getStringList("OfflineVotes"); - } - - /** - * Gets the points of the user. - * - * @return the points of the user - */ - public int getPoints() { - return getUserData().getInt(getPointsPath()); - } - - /** - * Gets the path for the points. - * - * @return the points path - */ - public String getPointsPath() { - if (plugin.getBungeeSettings().isPerServerPoints()) { - return plugin.getBungeeSettings().getServerNameStorage() + "_Points"; - } - return "Points"; - } - - /** - * Gets the number of sites not voted on. - * - * @return the number of sites not voted on - */ - public int getSitesNotVotedOn() { - int amount = 0; - for (VoteSite site : plugin.getVoteSiteManager().getVoteSitesEnabled()) { - if (!site.isHidden()) { - if (site.getPermissionToView().isEmpty() || hasPermission(site.getPermissionToView(), false)) { - if (canVoteSite(site)) { - amount++; - } - } - } - } - return amount; - } - - public int getTotalNumberOfSites() { - int amount = 0; - for (VoteSite site : plugin.getVoteSiteManager().getVoteSitesEnabled()) { - if (!site.isHidden()) { - if (site.getPermissionToView().isEmpty() || hasPermission(site.getPermissionToView(), false)) { - amount++; - } - } - } - return amount; - } - - public int getSitesVotedOn() { - int amount = 0; - for (VoteSite site : plugin.getVoteSiteManager().getVoteSitesEnabled()) { - if (!canVoteSite(site)) { - amount++; - } - } - return amount; - } - - /** - * Gets the time. - * - * @param voteSite the vote site - * @return the time - */ - public long getTime(VoteSite voteSite) { - HashMap lastVotes = getLastVotes(); - if (lastVotes.containsKey(voteSite)) { - return lastVotes.get(voteSite); - } - return 0; - } - - /** - * Gets the top voter player. - * - * @return the top voter player - */ - public TopVoterPlayer getTopVoterPlayer() { - return new TopVoterPlayer(UUID.fromString(getUUID()), getPlayerName(), getLastOnline()); - } - - /** - * Gets the total votes for the specified top voter category. - * - * @param top the top voter category - * @return the total votes for the specified top voter category - */ - public int getTotal(TopVoter top) { - switch (top) { - case AllTime: - return getUserData().getInt("AllTimeTotal"); - case Daily: - return getUserData().getInt("DailyTotal"); - case Monthly: - if (plugin.getConfigFile().isUseMonthDateTotalsAsPrimaryTotal()) { - return getData().getInt(plugin.getVotingPluginUserManager().getMonthTotalsWithDatePath()); - } - return getData().getInt("MonthTotal"); - case Weekly: - return getUserData().getInt("WeeklyTotal"); - default: - break; - } - return 0; - } - - /** - * Gets the total votes for the specified top voter category at a specific time. - * - * @param top the top voter category - * @param atTime the specific time - * @return the total votes for the specified top voter category at the specific - * time - */ - public int getTotal(TopVoter top, LocalDateTime atTime) { - switch (top) { - case AllTime: - return getUserData().getInt("AllTimeTotal"); - case Daily: - return getUserData().getInt("DailyTotal"); - case Monthly: - if (plugin.getConfigFile().isUseMonthDateTotalsAsPrimaryTotal()) { - return getData().getInt(plugin.getVotingPluginUserManager().getMonthTotalsWithDatePath(atTime)); - } - return getData().getInt("MonthTotal"); - case Weekly: - return getUserData().getInt("WeeklyTotal"); - default: - break; - } - return 0; - } - - /** - * Gets the number of votes for the vote party. - * - * @return the number of votes for the vote party - */ - public int getVotePartyVotes() { - return getUserData().getInt("VotePartyVotes"); - } - - /** - * Gets the vote shop identifier limit. - * - * @param identifier the identifier for the vote shop - * @return the vote shop identifier limit - */ - public int getVoteShopIdentifierLimit(String identifier) { - return getData().getInt("VoteShopLimit" + identifier); - } - - /** - * Gets the weekly total votes. - * - * @return the weekly total votes - * @deprecated Use getTotal(TopVoter.Weekly) instead - */ - @Deprecated - public int getWeeklyTotal() { - return getTotal(TopVoter.Weekly); - } - - /** - * Gets the week vote streak. - * - * @return the week vote streak - */ - @Deprecated - public int getWeekVoteStreak() { - return getData().getInt("WeekVoteStreak"); - } - - /** - * Gives the daily top voter award. - * - * @param place the place of the top voter - * @param path the path to the reward configuration - */ - public void giveDailyTopVoterAward(int place, String path) { - SpecialRewardType type = SpecialRewardType.TOPVOTER; - type.setType("Daily"); - type.setAmount(1); - PlayerSpecialRewardEvent event = new PlayerSpecialRewardEvent(this, type, null); - Bukkit.getPluginManager().callEvent(event); - - if (event.isCancelled()) { - return; - } - new RewardBuilder(plugin.getSpecialRewardsConfig().getData(), - plugin.getSpecialRewardsConfig().getDailyAwardRewardsPath(path)).withPlaceHolder("place", "" + place) - .withPlaceHolder("topvoter", "Daily").withPlaceHolder("votes", "" + getTotal(TopVoter.Daily)) - .setOnline(isOnline()).send(this); - } - - /** - * Gives the monthly top voter award. - * - * @param place the place of the top voter - * @param path the path to the reward configuration - */ - public void giveMonthlyTopVoterAward(int place, String path) { - SpecialRewardType type = SpecialRewardType.TOPVOTER; - type.setType("Monthly"); - type.setAmount(1); - PlayerSpecialRewardEvent event = new PlayerSpecialRewardEvent(this, type, null); - Bukkit.getPluginManager().callEvent(event); - - if (event.isCancelled()) { - return; - } - new RewardBuilder(plugin.getSpecialRewardsConfig().getData(), - plugin.getSpecialRewardsConfig().getMonthlyAwardRewardsPath(path)).withPlaceHolder("place", "" + place) - .withPlaceHolder("topvoter", "Monthly").withPlaceHolder("votes", "" + getTotal(TopVoter.Monthly)) - .setOnline(isOnline()).send(this); - } - - /** - * Gives the weekly top voter award. - * - * @param place the place of the top voter - * @param path the path to the reward configuration - */ - public void giveWeeklyTopVoterAward(int place, String path) { - SpecialRewardType type = SpecialRewardType.TOPVOTER; - type.setType("Weekly"); - type.setAmount(1); - PlayerSpecialRewardEvent event = new PlayerSpecialRewardEvent(this, type, null); - Bukkit.getPluginManager().callEvent(event); - - if (event.isCancelled()) { - return; - } - new RewardBuilder(plugin.getSpecialRewardsConfig().getData(), - plugin.getSpecialRewardsConfig().getWeeklyAwardRewardsPath(path)).withPlaceHolder("place", "" + place) - .withPlaceHolder("topvoter", "Weekly").withPlaceHolder("votes", "" + getTotal(TopVoter.Weekly)) - .setOnline(isOnline()).send(this); - } - - /** - * Gets how many unique vote sites this user has voted on today. - * - * Uses existing LastVotes data (no storage). A site counts if its last-vote - * timestamp falls on "today" using VotingPlugin's time offset. - * - * @return number of unique sites voted on today - */ - public long getUniqueVoteSitesToday() { - LocalDateTime now = plugin.getTimeChecker().getTime(); - LocalDate today = now.toLocalDate(); - - long count = 0; - - for (VoteSite site : plugin.getVoteSiteManager().getVoteSitesEnabled()) { - if (site == null || !site.isEnabled() || site.isHidden()) { - continue; - } - - long time = getTime(site); - if (time <= 0) { - continue; - } - - // Match the same offset handling as canVoteSite() - LocalDateTime lastVote = LocalDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneId.systemDefault()) - .plusHours(plugin.getOptions().getTimeHourOffSet()); - - if (lastVote.toLocalDate().equals(today)) { - count++; - } - } - - return count; - } - - /** - * Checks if the user has a percentage of the total votes. - * - * @param top the top voter category - * @param percentage the percentage of the total votes - * @param time the specific time - * @return true if the user has the percentage of the total votes, false - * otherwise - */ - public boolean hasPercentageTotal(TopVoter top, double percentage, LocalDateTime time) { - int total = getTotal(top, time); - switch (top) { - case Daily: - return (double) total / (double) plugin.getVoteSiteManager().getVoteSitesEnabled().size() - * 100 > percentage; - case Monthly: - return total / ((double) plugin.getVoteSiteManager().getVoteSitesEnabled().size() - * time.getMonth().length(false)) * 100 > percentage; - case Weekly: - return total / ((double) plugin.getVoteSiteManager().getVoteSitesEnabled().size() * 7) * 100 > percentage; - default: - return false; - } - } - - /** - * Checks if the user is ignored for top voter. - * - * @return true if the user is ignored for top voter, false otherwise - */ - public boolean isTopVoterIgnore() { - return getUserData().getBoolean("TopVoterIgnore"); - } - - /** - * Gives login rewards to the user. - */ - public void loginRewards() { - if (plugin.getRewardHandler().hasRewards(plugin.getSpecialRewardsConfig().getData(), "LoginRewards")) { - new RewardBuilder(plugin.getSpecialRewardsConfig().getData(), "LoginRewards").send(this); - } - } - - /** - * Gives logout rewards to the user. - */ - public void logoutRewards() { - if (plugin.getRewardHandler().hasRewards(plugin.getSpecialRewardsConfig().getData(), "LogoutRewards")) { - new RewardBuilder(plugin.getSpecialRewardsConfig().getData(), "LogoutRewards").send(this); - } - } - - /** - * Merges the provided data with the current data. - * - * @param toAdd the data to add - */ - public void mergeData(HashMap toAdd) { - HashMap currentData = getData().getValues(); - HashMap newData = new HashMap<>(); - - for (TopVoter top : TopVoter.values()) { - if (toAdd.containsKey(top.getColumnName()) && currentData.containsKey(top.getColumnName())) { - newData.put(top.getColumnName(), new DataValueInt( - currentData.get(top.getColumnName()).getInt() + toAdd.get(top.getColumnName()).getInt())); - } - } - - if (newData.size() > 0) { - getData().setValues(newData); - } - } - - /** - * Processes offline votes. - */ - public void offVote() { - if (!plugin.getOptions().isProcessRewards()) { - plugin.debug("Processing rewards is disabled"); - return; - } - - Player player = getPlayer(); - if (!plugin.getOptions().isOnlineMode()) { - player = Bukkit.getPlayer(getPlayerName()); - } - if (player == null) { - return; - } - - plugin.extraDebug("Checking offline votes for " + player.getName() + "/" + getUUID()); - - // Update top voter ignore flag if needed. - boolean currentTopVoterIgnore = player.hasPermission("VotingPlugin.TopVoter.Ignore"); - if (isTopVoterIgnore() != currentTopVoterIgnore) { - setTopVoterIgnore(currentTopVoterIgnore); - } - - ArrayList offlineVotes = getOfflineVotes(); - if (offlineVotes.isEmpty()) { - return; - } - - // Send vote effects and clear persistent offline votes. - sendVoteEffects(false); - setOfflineVotes(new ArrayList<>()); - - // Process each offline vote. - for (String voteSiteName : offlineVotes) { - if (plugin.getVoteSiteManager().hasVoteSite(voteSiteName)) { - plugin.debug("Giving offline site reward: " + voteSiteName); - playerVote(plugin.getVoteSiteManager().getVoteSite(voteSiteName, true), false, false); - } else { - plugin.debug("Site doesn't exist: " + voteSiteName); - } - } - } - - /** - * Processes a player vote. - * - * @param voteSite the vote site - * @param online whether the player is online - * @param bungee whether to use bungee - */ - public void playerVote(VoteSite voteSite, boolean online, boolean bungee) { - voteSite.giveRewards(this, online, bungee); - } - - /** - * Removes points from the user. - * - * @param points the number of points to remove - * @return true if the points were removed, false otherwise - */ - public boolean removePoints(int points) { - if (getPoints() >= points) { - setPoints(getPoints() - points); - return true; - } - return false; - } - - /** - * Removes points from the user asynchronously. - * - * @param points the number of points to remove - * @param async whether to remove the points asynchronously - * @return true if the points were removed, false otherwise - */ - public boolean removePoints(int points, boolean async) { - if (getPoints() >= points) { - setPoints(getPoints() - points, async); - return true; - } - return false; - } - - /** - * Resets the last voted time for all vote sites. - */ - public void resetLastVoted() { - HashMap map = getLastVotes(); - for (Entry e : map.entrySet()) { - e.setValue(0l); - } - setLastVotes(map); - } - - /** - * Resets the last voted time for a specific vote site. - * - * @param site the vote site - */ - public void resetLastVoted(VoteSite site) { - HashMap map = getLastVotes(); - map.put(site, 0l); - setLastVotes(map); - } - - /** - * Resets the total votes for a specific top voter category. - * - * @param topVoter the top voter category - */ - public void resetTotals(TopVoter topVoter) { - setTotal(topVoter, 0); - } - - /** - * Sends vote effects to the user. - * - * @param online whether the user is online - */ - public void sendVoteEffects(boolean online) { - plugin.getRewardHandler().giveReward(this, plugin.getSpecialRewardsConfig().getData(), - plugin.getSpecialRewardsConfig().getAnySiteRewardsPath(), new RewardOptions().setOnline(online)); - } - - /** - * Sets the all-time total votes. - * - * @param allTimeTotal the all-time total votes - * @deprecated Use setTotal(TopVoter.AllTime, allTimeTotal) instead - */ - @Deprecated - public void setAllTimeTotal(int allTimeTotal) { - setTotal(TopVoter.AllTime, allTimeTotal); - } - - /** - * Sets the best day vote streak. - * - * @param streak the best day vote streak - */ - @Deprecated - public void setBestDayVoteStreak(int streak) { - getData().setInt("BestDayVoteStreak", streak); - } - - /** - * Sets the best month vote streak. - * - * @param streak the best month vote streak - */ - @Deprecated - public void setBestMonthVoteStreak(int streak) { - getData().setInt("BestMonthVoteStreak", streak); - } - - /** - * Sets the best week vote streak. - * - * @param streak the best week vote streak - */ - @Deprecated - public void setBestWeekVoteStreak(int streak) { - getData().setInt("BestWeekVoteStreak", streak); - } - - /** - * Sets the cooldown check. - * - * @param coolDownCheck whether the cooldown check is enabled - */ - public void setCoolDownCheck(boolean coolDownCheck) { - getData().setBoolean(getCoolDownCheckPath(), coolDownCheck); - } - - /** - * Sets the cooldown check for all vote sites. - * - * @param coolDownChecks the cooldown checks for all vote sites - */ - public void setCoolDownCheckSite(HashMap coolDownChecks) { - ArrayList data = new ArrayList<>(); - for (Entry entry : coolDownChecks.entrySet()) { - String str = entry.getKey() + "//" + entry.getValue().toString(); - data.add(str); - } - getUserData().setStringList(getCoolDownCheckSitePath(), data); - } - - /** - * Sets the cooldown check for a specific vote site. - * - * @param site the vote site - * @param value whether the cooldown check is enabled - */ - public void setCoolDownCheckSite(VoteSite site, boolean value) { - HashMap coolDownChecks = getCoolDownCheckSiteList(); - coolDownChecks.put(site.getKey(), Boolean.valueOf(value)); - setCoolDownCheckSite(coolDownChecks); - } - - /** - * Sets the daily total votes. - * - * @param total the daily total votes - * @deprecated Use setTotal(TopVoter.Daily, total) instead - */ - @Deprecated - public void setDailyTotal(int total) { - setTotal(TopVoter.Daily, total); - } - - /** - * Sets the day vote streak. - * - * @param streak the day vote streak - */ - @Deprecated - public void setDayVoteStreak(int streak) { - getData().setInt("DayVoteStreak", streak); - if (getBestDayVoteStreak() < streak) { - setBestDayVoteStreak(streak); - } - } - - /** - * Sets the last update time for the day vote streak. - * - * @param time the last update time for the day vote streak - */ - @Deprecated - public void setDayVoteStreakLastUpdate(long time) { - getData().setString("DayVoteStreakLastUpdate", "" + time); - } - - /** - * Sets whether the broadcast is disabled. - * - * @param value true to disable the broadcast, false otherwise - */ - public void setDisableBroadcast(boolean value) { - getUserData().setBoolean("DisableBroadcast", value); - } - - /** - * Sets the day when the user has gotten all sites. - * - * @param day the day when the user has gotten all sites - */ - public void setGottenAllSitesDay(int day) { - getData().setInt(plugin.getVotingPluginUserManager().getGottenAllSitesDayPath(), day); - } - - /** - * Sets the day when the user has gotten almost all sites. - * - * @param day the day when the user has gotten almost all sites - */ - public void setGottenAlmostAllSitesDay(int day) { - getData().setInt(plugin.getVotingPluginUserManager().getGottenAlmostAllSitesDayPath(), day); - } - - /** - * Sets the highest daily total votes. - * - * @param total the highest daily total votes - */ - public void setHighestDailyTotal(int total) { - getData().setInt("HighestDailyTotal", total); - } - - /** - * Sets the highest monthly total votes. - * - * @param total the highest monthly total votes - */ - public void setHighestMonthlyTotal(int total) { - getData().setInt("HighestMonthlyTotal", total); - } - - /** - * Sets the highest weekly total votes. - * - * @param total the highest weekly total votes - */ - public void setHighestWeeklyTotal(int total) { - getData().setInt("HighestWeeklyTotal", total); - } - - /** - * Sets the total votes for the last month. - * - * @param total the total votes for the last month - */ - public void setLastMonthTotal(int total) { - getData().setInt("LastMonthTotal", total); - } - - /** - * Sets the last votes for each vote site. - * - * @param lastVotes a map of vote sites and the last vote time - */ - public void setLastVotes(HashMap lastVotes) { - ArrayList data = new ArrayList<>(); - for (Entry entry : lastVotes.entrySet()) { - String str = entry.getKey().getKey() + "//" + entry.getValue().longValue(); - data.add(str); - } - getUserData().setStringList("LastVotes", data); - } - - /** - * Sets the total votes for the month. - * - * @param total the total votes for the month - * @deprecated Use setTotal(TopVoter.Monthly, total) instead - */ - @Deprecated - public void setMonthTotal(int total) { - setTotal(TopVoter.Monthly, total); - } - - /** - * Sets the month vote streak. - * - * @param streak the month vote streak - */ - @Deprecated - public void setMonthVoteStreak(int streak) { - getData().setInt("MonthVoteStreak", streak); - if (getBestMonthVoteStreak() < streak) { - setBestMonthVoteStreak(streak); - } - } - - /** - * Sets the list of offline votes. - * - * @param offlineVotes the list of offline votes - */ - public void setOfflineVotes(ArrayList offlineVotes) { - getUserData().setStringList("OfflineVotes", offlineVotes); - } - - /** - * Sets the points of the user. - * - * @param value the number of points - */ - public void setPoints(int value) { - getUserData().setInt(getPointsPath(), value, false); - } - - /** - * Sets the points of the user asynchronously. - * - * @param value the number of points - * @param async whether to set the points asynchronously - */ - public void setPoints(int value, boolean async) { - getUserData().setInt(getPointsPath(), value, false, async); - } - - /** - * Sets the current time for the specified vote site. - * - * @param voteSite the vote site - */ - public void setTime(VoteSite voteSite) { - setTime(voteSite, LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); - } - - /** - * Sets the specified time for the specified vote site. - * - * @param voteSite the vote site - * @param time the time to set - */ - public void setTime(VoteSite voteSite, Long time) { - HashMap lastVotes = getLastVotes(); - if (lastVotes != null && lastVotes.containsKey(voteSite)) { - if (lastVotes.get(voteSite).longValue() == time.longValue()) { - plugin.debug("Not setting last vote time for " + voteSite.getKey() + ", already set to " + time); - return; - } - } - lastVotes.put(voteSite, time); - setLastVotes(lastVotes); - } - - /** - * Sets whether the user is ignored for top voter. - * - * @param topVoterIgnore true to ignore the user for top voter, false otherwise - */ - public void setTopVoterIgnore(boolean topVoterIgnore) { - getUserData().setString("TopVoterIgnore", "" + topVoterIgnore); - } - - /** - * Sets the total votes for the specified top voter category. - * - * @param top the top voter category - * @param value the total votes to set - */ - public void setTotal(TopVoter top, int value) { - switch (top) { - case AllTime: - getUserData().setInt("AllTimeTotal", value); - break; - case Daily: - getUserData().setInt("DailyTotal", value); - break; - case Monthly: - if (plugin.getConfigFile().isLimitMonthlyVotes()) { - LocalDateTime time = plugin.getTimeChecker().getTime(); - int days = time.getDayOfMonth(); - if (value >= days * plugin.getVoteSiteManager().getVoteSitesEnabled().size()) { - value = days * plugin.getVoteSiteManager().getVoteSitesEnabled().size(); - } - } - getData().setInt("MonthTotal", value); - if (plugin.getConfigFile().isStoreMonthTotalsWithDate()) { - getData().setInt(plugin.getVotingPluginUserManager().getMonthTotalsWithDatePath(), value); - } - break; - case Weekly: - getUserData().setInt("WeeklyTotal", value); - break; - default: - break; - } - } - - /** - * Sets the number of votes for the vote party. - * - * @param value the number of votes to set - */ - public void setVotePartyVotes(int value) { - getUserData().setInt("VotePartyVotes", value); - } - - /** - * Sets the vote shop identifier limit. - * - * @param identifier the identifier for the vote shop - * @param value the limit to set - */ - public void setVoteShopIdentifierLimit(String identifier, int value) { - getData().setInt("VoteShopLimit" + identifier, value); - } - - /** - * Sets the weekly total votes. - * - * @param total the weekly total votes - * @deprecated Use setTotal(TopVoter.Weekly, total) instead - */ - @Deprecated - public void setWeeklyTotal(int total) { - setTotal(TopVoter.Weekly, total); - } - - /** - * Sets the week vote streak. - * - * @param streak the week vote streak - */ - @Deprecated - public void setWeekVoteStreak(int streak) { - getData().setInt("WeekVoteStreak", streak); - if (getBestWeekVoteStreak() < streak) { - setBestWeekVoteStreak(streak); - } - } - - /** - * Checks if the user should be reminded. - * - * @return true if the user should be reminded, false otherwise - */ - public boolean shouldBeReminded() { - Player player = getPlayer(); - if (player != null) { - if (player.hasPermission("VotingPlugin.NoRemind")) { - return false; - } - } - return true; - } - - /** - * Gets the last vote date for the specified vote site. - * - * @param voteSite the vote site - * @return the last vote date as a string - * @deprecated Use getTime(VoteSite) instead - */ - @Deprecated - public String voteCommandLastDate(VoteSite voteSite) { - long time = getTime(voteSite); - if (time > 0) { - Date date = new Date(time); - String timeString = new SimpleDateFormat(plugin.getConfigFile().getFormatTimeFormat()).format(date); - if (MessageAPI.containsIgnorecase(timeString, "YamlConfiguration")) { - plugin.getLogger().warning("Detected issue parsing time, check time format"); - } - return timeString; - } - return ""; - } - - /** - * Gets the duration since the last vote for the specified vote site. - * - * @param voteSite the vote site - * @return the duration since the last vote as a string - */ - public String voteCommandLastDuration(VoteSite voteSite) { - long time = getTime(voteSite); - if (time > 0) { - LocalDateTime now = LocalDateTime.now(); - LocalDateTime lastVote = LocalDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneId.systemDefault()); - - Duration dur = Duration.between(lastVote, now); - - long diffSecond = dur.getSeconds(); - int diffDays = (int) (diffSecond / 60 / 60 / 24); - int diffHours = (int) (diffSecond / 60 / 60 - diffDays * 24); - int diffMinutes = (int) (diffSecond / 60 - diffHours * 60 - diffDays * 24 * 60); - int diffSeconds = (int) (diffSecond - diffMinutes * 60 - diffHours * 60 * 60 - diffDays * 24 * 60 * 60); - - String info = ""; - if (diffDays == 1) { - info += PlaceholderUtils.replacePlaceHolder(PlaceholderUtils.replacePlaceHolder( - plugin.getConfigFile().getFormatCommandsVoteLastTimeFormat(), "TimeType", - plugin.getConfigFile().getFormatTimeFormatsDay()), "amount", "" + diffDays); - info += " "; - } else if (diffDays > 1) { - info += PlaceholderUtils.replacePlaceHolder(PlaceholderUtils.replacePlaceHolder( - plugin.getConfigFile().getFormatCommandsVoteLastTimeFormat(), "TimeType", - plugin.getConfigFile().getFormatTimeFormatsDays()), "amount", "" + diffDays); - info += " "; - } - - if (diffHours == 1) { - info += PlaceholderUtils.replacePlaceHolder(PlaceholderUtils.replacePlaceHolder( - plugin.getConfigFile().getFormatCommandsVoteLastTimeFormat(), "TimeType", - plugin.getConfigFile().getFormatTimeFormatsHour()), "amount", "" + diffHours); - info += " "; - } else if (diffHours > 1) { - info += PlaceholderUtils.replacePlaceHolder(PlaceholderUtils.replacePlaceHolder( - plugin.getConfigFile().getFormatCommandsVoteLastTimeFormat(), "TimeType", - plugin.getConfigFile().getFormatTimeFormatsHours()), "amount", "" + diffHours); - info += " "; - } - - if (diffMinutes == 1) { - info += PlaceholderUtils.replacePlaceHolder(PlaceholderUtils.replacePlaceHolder( - plugin.getConfigFile().getFormatCommandsVoteLastTimeFormat(), "TimeType", - plugin.getConfigFile().getFormatTimeFormatsMinute()), "amount", "" + diffMinutes); - info += " "; - } else if (diffMinutes > 1) { - info += PlaceholderUtils.replacePlaceHolder(PlaceholderUtils.replacePlaceHolder( - plugin.getConfigFile().getFormatCommandsVoteLastTimeFormat(), "TimeType", - plugin.getConfigFile().getFormatTimeFormatsMinutes()), "amount", "" + diffMinutes); - info += " "; - } - - if (plugin.getConfigFile().isFormatCommandsVoteLastIncludeSeconds()) { - if (diffSeconds == 1) { - info += PlaceholderUtils.replacePlaceHolder(PlaceholderUtils.replacePlaceHolder( - plugin.getConfigFile().getFormatCommandsVoteLastTimeFormat(), "TimeType", - plugin.getConfigFile().getFormatTimeFormatsSecond()), "amount", "" + diffSeconds); - } else { - info += PlaceholderUtils.replacePlaceHolder(PlaceholderUtils.replacePlaceHolder( - plugin.getConfigFile().getFormatCommandsVoteLastTimeFormat(), "TimeType", - plugin.getConfigFile().getFormatTimeFormatsSeconds()), "amount", "" + diffSeconds); - } - } - - info = PlaceholderUtils.replacePlaceHolder(plugin.getConfigFile().getFormatCommandsVoteLastLastVoted(), - "times", info); - - return info; - } - return plugin.getConfigFile().getFormatCommandsVoteLastNeverVoted(); - } - - /** - * Gets the last vote date and duration for the specified vote site for the GUI. - * - * @param voteSite the vote site - * @return the last vote date and duration as a string for the GUI - */ - public String voteCommandLastGUILine(VoteSite voteSite) { - String timeString = voteCommandLastDate(voteSite); - String timeSince = voteCommandLastDuration(voteSite); - - HashMap placeholders = new HashMap<>(); - placeholders.put("time", timeString); - placeholders.put("SiteName", voteSite.getDisplayName()); - placeholders.put("timesince", timeSince); - - return PlaceholderUtils.replacePlaceHolder(plugin.getGui().getChestVoteLastLine(), placeholders); - } - - /** - * Gets the last vote date and duration for the specified vote site. - * - * @param voteSite the vote site - * @return the last vote date and duration as a string - */ - public String voteCommandLastLine(VoteSite voteSite) { - String timeString = voteCommandLastDate(voteSite); - String timeSince = voteCommandLastDuration(voteSite); - - HashMap placeholders = new HashMap<>(); - placeholders.put("time", timeString); - placeholders.put("SiteName", voteSite.getDisplayName()); - placeholders.put("timesince", timeSince); - - return PlaceholderUtils.replacePlaceHolder(plugin.getConfigFile().getFormatCommandsVoteLastLine(), - placeholders); - } - - /** - * Gets the next available vote time for the specified vote site. - * - * @param voteSite the vote site - * @return the next available vote time as a string - */ - public String voteCommandNextInfo(VoteSite voteSite) { - return voteCommandNextInfo(voteSite, getTime(voteSite)); - } - - /** - * Gets the next available vote time for the specified vote site. - * - * @param voteSite the vote site - * @param time the current time - * @return the next available vote time as a string - */ - public String voteCommandNextInfo(VoteSite voteSite, long time) { - String info = new String(); - - long nextTime = voteNextDurationTime(voteSite, time); - if (nextTime == 0) { - info = plugin.getConfigFile().getFormatCommandsVoteNextInfoCanVote(); - } else { - int diffHours = (int) (nextTime / (60 * 60)); - long diffMinutes = nextTime / 60 - diffHours * 60; - - if (diffHours < 0) { - diffHours = diffHours * -1; - } - if (diffHours >= 24) { - diffHours = diffHours - 24; - } - if (diffMinutes < 0) { - diffMinutes = diffMinutes * -1; - } - - String timeMsg = plugin.getConfigFile().getFormatCommandsVoteNextInfoVoteDelayDaily(); - timeMsg = MessageAPI.replaceIgnoreCase(timeMsg, "%hours%", Integer.toString(diffHours)); - timeMsg = MessageAPI.replaceIgnoreCase(timeMsg, "%minutes%", Long.toString(diffMinutes)); - info = timeMsg; - } - - return info; - } - - /** - * Gets the next available vote duration time for the specified vote site. - * - * @param voteSite the vote site - * @return the next available vote duration time in seconds - */ - public long voteNextDurationTime(VoteSite voteSite) { - return voteNextDurationTime(voteSite, getTime(voteSite)); - } - - /** - * Gets the next available vote duration time for the specified vote site. - * - * @param voteSite the vote site - * @param time the last vote time (epoch millis) - * @return the next available vote duration time in seconds - */ - public long voteNextDurationTime(VoteSite voteSite, long time) { - LocalDateTime now = plugin.getTimeChecker().getTime(); - - LocalDateTime lastVote = LocalDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneId.systemDefault()) - .plusHours(plugin.getOptions().getTimeHourOffSet()); - - if (!voteSite.isVoteDelayDaily()) { - ParsedDuration voteDelay = voteSite.getVoteDelay(); - - if (time == 0 || voteDelay == null || voteDelay.isEmpty()) { - return 0; - } - - // Ignore months, use fixed duration only - LocalDateTime nextVote = lastVote.plus(Duration.ofMillis(voteDelay.getMillis())); - - if (now.isAfter(nextVote)) { - return 0; - } - - return Duration.between(now, nextVote).getSeconds(); - } - - // Daily reset logic (unchanged) - LocalDateTime resetTime = lastVote.withHour(voteSite.getVoteDelayDailyHour()).withMinute(0).withSecond(0); - - LocalDateTime resetTimeTomorrow = resetTime.plusHours(24); - - if (lastVote.isBefore(resetTime)) { - if (now.isBefore(resetTime)) { - return Duration.between(now, resetTime).getSeconds(); - } - } else { - if (now.isBefore(resetTimeTomorrow)) { - return Duration.between(now, resetTimeTomorrow).getSeconds(); - } - } - - return 0; - } - - /** - * Checks if the vote streak was updated today. - * - * @param time the current time - * @return true if the vote streak was updated today, false otherwise - */ - @Deprecated - public boolean voteStreakUpdatedToday(LocalDateTime time) { - return MiscUtils.getInstance().getTime(getDayVoteStreakLastUpdate()).getDayOfYear() == time.getDayOfYear(); - } - - public String getVoteStreakState(String columnName) { - return getData().getString(columnName); - } - - public void setVoteStreakState(String columnName, String value) { - getData().setString(columnName, value); - } - -} +package com.bencodez.votingplugin.user; + +import java.text.SimpleDateFormat; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import com.bencodez.advancedcore.api.messages.PlaceholderUtils; +import com.bencodez.advancedcore.api.misc.MiscUtils; +import com.bencodez.advancedcore.api.rewards.RewardBuilder; +import com.bencodez.advancedcore.api.rewards.RewardOptions; +import com.bencodez.advancedcore.api.user.AdvancedCoreUser; +import com.bencodez.simpleapi.messages.MessageAPI; +import com.bencodez.simpleapi.sql.data.DataValue; +import com.bencodez.simpleapi.sql.data.DataValueInt; +import com.bencodez.simpleapi.time.ParsedDuration; +import com.bencodez.votingplugin.VotingPluginMain; +import com.bencodez.votingplugin.events.PlayerReceivePointsEvent; +import com.bencodez.votingplugin.events.PlayerSpecialRewardEvent; +import com.bencodez.votingplugin.events.PlayerVoteEvent; +import com.bencodez.votingplugin.events.SpecialRewardType; +import com.bencodez.votingplugin.proxy.VoteTotalsSnapshot; +import com.bencodez.votingplugin.topvoter.TopVoter; +import com.bencodez.votingplugin.topvoter.TopVoterPlayer; +import com.bencodez.votingplugin.votesites.NextSite; +import com.bencodez.votingplugin.votesites.VoteSite; + +/** + * The Class VotingPluginUser. This class represents a user in the VotingPlugin + * system. It extends the AdvancedCoreUser class and provides additional + * functionality specific to the VotingPlugin. + */ +public class VotingPluginUser extends com.bencodez.advancedcore.api.user.AdvancedCoreUser { + + /** The plugin instance. */ + private VotingPluginMain plugin; + + /** + * Instantiates a new VotingPluginUser. + * + * @param plugin the plugin instance + * @param user the AdvancedCoreUser instance + */ + public VotingPluginUser(VotingPluginMain plugin, AdvancedCoreUser user) { + super(plugin, user); + this.plugin = plugin; + } + + /** + * Instantiates a new VotingPluginUser. + * + * @param plugin the plugin instance + * @param player the player instance + * @deprecated Use {@link #VotingPluginUser(VotingPluginMain, AdvancedCoreUser)} + * instead. + */ + @Deprecated + public VotingPluginUser(VotingPluginMain plugin, Player player) { + super(plugin, player); + this.plugin = plugin; + } + + /** + * Instantiates a new VotingPluginUser. + * + * @param plugin the plugin instance + * @param playerName the player name + * @deprecated Use {@link #VotingPluginUser(VotingPluginMain, AdvancedCoreUser)} + * instead. + */ + @Deprecated + public VotingPluginUser(VotingPluginMain plugin, String playerName) { + super(plugin, playerName); + this.plugin = plugin; + } + + /** + * Instantiates a new VotingPluginUser. + * + * @param plugin the plugin instance + * @param uuid the UUID of the player + * @deprecated Use {@link #VotingPluginUser(VotingPluginMain, AdvancedCoreUser)} + * instead. + */ + @Deprecated + public VotingPluginUser(VotingPluginMain plugin, UUID uuid) { + super(plugin, uuid); + this.plugin = plugin; + } + + /** + * Instantiates a new VotingPluginUser. + * + * @param plugin the plugin instance + * @param uuid the UUID of the player + * @param loadName whether to load the player name + * @deprecated Use {@link #VotingPluginUser(VotingPluginMain, AdvancedCoreUser)} + * instead. + */ + @Deprecated + public VotingPluginUser(VotingPluginMain plugin, UUID uuid, boolean loadName) { + super(plugin, uuid, loadName); + this.plugin = plugin; + } + + /** + * Instantiates a new VotingPluginUser. + * + * @param plugin the plugin instance + * @param uuid the UUID of the player + * @param playerName the player name + * @deprecated Use {@link #VotingPluginUser(VotingPluginMain, AdvancedCoreUser)} + * instead. + */ + @Deprecated + public VotingPluginUser(VotingPluginMain plugin, UUID uuid, String playerName) { + super(plugin, uuid, playerName); + this.plugin = plugin; + } + + /** + * Adds one to the all-time total votes. + */ + public void addAllTimeTotal() { + setAllTimeTotal(getAllTimeTotal() + 1); + } + + /** + * Adds one to the daily vote streak. + */ + @Deprecated + public void addDayVoteStreak() { + setDayVoteStreak(getDayVoteStreak() + 1); + } + + /** + * Adds one to the monthly total votes. + */ + public void addMonthTotal() { + setMonthTotal(getMonthTotal() + 1); + } + + /** + * Adds one to the monthly vote streak. + */ + @Deprecated + public void addMonthVoteStreak() { + setMonthVoteStreak(getMonthVoteStreak() + 1); + } + + /** + * Adds an offline vote for the specified vote site. + * + * @param voteSiteName the name of the vote site + */ + public void addOfflineVote(String voteSiteName) { + ArrayList offlineVotes = getOfflineVotes(); + offlineVotes.add(voteSiteName); + setOfflineVotes(offlineVotes); + } + + /** + * Adds points to the user based on the configuration. + */ + public void addPoints() { + int points = plugin.getConfigFile().getPointsOnVote(); + if (points != 0) { + addPoints(points); + } + if (plugin.getConfigFile().getLimitVotePoints() > 0) { + if (getPoints() > plugin.getConfigFile().getLimitVotePoints()) { + setPoints(plugin.getConfigFile().getLimitVotePoints()); + } + } + } + + /** + * Adds the specified number of points to the user. + * + * @param value the number of points to add + * @return the current total points + */ + public int addPoints(int value) { + return addPoints(value, false); + } + + /** + * Adds the specified number of points to the user, optionally asynchronously. + * + * @param value the number of points to add + * @param async whether to add the points asynchronously + * @return the current total points + */ + public synchronized int addPoints(int value, boolean async) { + PlayerReceivePointsEvent event = new PlayerReceivePointsEvent(this, value); + Bukkit.getPluginManager().callEvent(event); + + if (event.isCancelled()) { + return getPoints(); + } + int newTotal = getPoints() + event.getPoints(); + setPoints(newTotal, async); + return newTotal; + } + + /** + * Adds one to the total votes. + */ + public void addTotal() { + addMonthTotal(); + addAllTimeTotal(); + } + + /** + * Adds one to the daily total votes. + */ + public void addTotalDaily() { + setDailyTotal(getDailyTotal() + 1); + } + + /** + * Adds one to the weekly total votes. + */ + public void addTotalWeekly() { + setWeeklyTotal(getWeeklyTotal() + 1); + } + + /** + * Adds one to the weekly vote streak. + */ + @Deprecated + public void addWeekVoteStreak() { + setWeekVoteStreak(getWeekVoteStreak() + 1); + } + + /** + * Handles a plugin messaging bungee vote. + * + * @param service the service name + * @param time the vote time + * @param text the bungee message data + * @param setTotals whether to set the totals + * @param wasOnline whether the player was online + * @param broadcast whether to broadcast the vote + * @param num the vote number + */ + public void bungeeVotePluginMessaging(String service, long time, VoteTotalsSnapshot text, boolean setTotals, + boolean wasOnline, boolean broadcast, int num) { + if (plugin.getBungeeSettings().isUseBungeecoord()) { + plugin.debug("Pluginmessaging vote for " + getPlayerName() + " on " + service); + + PlayerVoteEvent voteEvent = new PlayerVoteEvent(plugin.getVoteSiteManager().getVoteSite(service, true), + getPlayerName(), service, true); + voteEvent.setBungee(true); + voteEvent.setVotingPluginUser(this); + voteEvent.setForceBungee(true); + voteEvent.setTime(time); + voteEvent.setAddTotals(setTotals); + voteEvent.setBungeeTextTotals(text); + voteEvent.setWasOnline(wasOnline); + voteEvent.setBroadcast(broadcast); + voteEvent.setVoteNumber(num); + plugin.getServer().getPluginManager().callEvent(voteEvent); + } + } + + /** + * Checks if the user can vote on all sites. + * + * @return true, if the user can vote on all sites + */ + public boolean canVoteAll() { + for (VoteSite voteSite : plugin.getVoteSiteManager().getVoteSitesEnabled()) { + if (!voteSite.isHidden()) { + boolean canVote = canVoteSite(voteSite); + if (!canVote) { + return false; + } + } + } + return true; + } + + /** + * Checks if the user can vote on any site. + * + * @return true, if the user can vote on any site + */ + public boolean canVoteAny() { + for (VoteSite voteSite : plugin.getVoteSiteManager().getVoteSitesEnabled()) { + if (!voteSite.isIgnoreCanVote() && !voteSite.isHidden()) { + boolean canVote = canVoteSite(voteSite); + if (canVote) { + return true; + } + } + } + return false; + } + + /** + * Checks if the user can vote on the specified site. + * + * @param voteSite the vote site + * @return true, if the user can vote on the site + */ + public boolean canVoteSite(VoteSite voteSite) { + long time = getTime(voteSite); + if (time == 0) { + return true; + } + try { + LocalDateTime now = plugin.getTimeChecker().getTime(); + LocalDateTime lastVote = LocalDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneId.systemDefault()) + .plusHours(plugin.getOptions().getTimeHourOffSet()); + + if (!voteSite.isVoteDelayDaily()) { + ParsedDuration voteDelay = voteSite.getVoteDelay(); + + // Preserve old behavior: if delay is 0, you can never vote again (unless daily + // reset mode) + if (voteDelay == null || voteDelay.isEmpty() || voteDelay.getMillis() == 0L) { + return false; + } + + LocalDateTime nextVote = lastVote.plus(Duration.ofMillis(voteDelay.getMillis())); + return now.isAfter(nextVote); + } + + // Daily reset logic unchanged + LocalDateTime resetTime = lastVote.withHour(voteSite.getVoteDelayDailyHour()).withMinute(0).withSecond(0); + LocalDateTime resetTimeTomorrow = resetTime.plusHours(24); + + if (lastVote.isBefore(resetTime)) { + return now.isAfter(resetTime); + } else { + return now.isAfter(resetTimeTomorrow); + } + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + + /** + * Checks if the user has voted on all sites. + * + * @return true, if the user has voted on all sites + */ + public boolean checkAllVotes() { + VotingPluginUser user = this; + + ArrayList months = new ArrayList<>(); + ArrayList days = new ArrayList<>(); + + for (VoteSite voteSite : plugin.getVoteSiteManager().getVoteSitesEnabled()) { + if (voteSite.isEnabled() && !voteSite.isHidden()) { + long time = user.getTime(voteSite); + if (time == 0) { + return false; + } + months.add(MiscUtils.getInstance().getMonthFromMili(time)); + days.add(MiscUtils.getInstance().getDayFromMili(time)); + } + } + + // check months + for (Integer month : months) { + if (!months.get(0).equals(month)) { + return false; + } + } + + // check days + for (Integer day : days) { + if (!days.get(0).equals(day)) { + return false; + } + } + + return true; + } + + /** + * Checks if the user has voted on almost all sites. + * + * @return true, if the user has voted on almost all sites + */ + public boolean checkAlmostAllVotes() { + if (getSitesNotVotedOn() <= 1) { + return true; + } + return false; + } + + /** + * Checks the day vote streak and updates it if necessary. + * + * @param forceBungee whether to force bungee + */ + @Deprecated + public void checkDayVoteStreak(boolean forceBungee) { + if (!voteStreakUpdatedToday(LocalDateTime.now())) { + if (!plugin.getSpecialRewardsConfig().isVoteStreakRequirementUsePercentage() || hasPercentageTotal( + TopVoter.Daily, plugin.getSpecialRewardsConfig().getVoteStreakRequirementDay(), null)) { + plugin.extraDebug("Adding day vote streak to " + getUUID() + " " + + plugin.getSpecialRewardsConfig().isVoteStreakRequirementUsePercentage() + " " + + hasPercentageTotal(TopVoter.Daily, + plugin.getSpecialRewardsConfig().getVoteStreakRequirementDay(), null)); + addDayVoteStreak(); + plugin.getSpecialRewards().checkVoteStreak(null, this, "Day", forceBungee); + setDayVoteStreakLastUpdate(System.currentTimeMillis()); + } + } + } + + /** + * Clears the offline votes. + */ + public void clearOfflineVotes() { + setOfflineVotes(new ArrayList<>()); + setOfflineRewards(new ArrayList<>()); + } + + /** + * Clears the total votes for all top voter categories. + */ + public void clearTotals() { + for (TopVoter top : TopVoter.values()) { + resetTotals(top); + } + } + + /** + * Gets the all-time total votes. + * + * @return the all-time total votes + * @deprecated Use getTotal(TopVoter.AllTime) when able instead + */ + @Deprecated + public int getAllTimeTotal() { + return getTotal(TopVoter.AllTime); + } + + /** + * Gets the best day vote streak. + * + * @return the best day vote streak + */ + @Deprecated + public int getBestDayVoteStreak() { + return getData().getInt("BestDayVoteStreak"); + } + + /** + * Gets the best month vote streak. + * + * @return the best month vote streak + */ + @Deprecated + public int getBestMonthVoteStreak() { + return getData().getInt("BestMonthVoteStreak"); + } + + /** + * Gets the best week vote streak. + * + * @return the best week vote streak + */ + @Deprecated + public int getBestWeekVoteStreak() { + return getData().getInt("BestWeekVoteStreak"); + } + + /** + * Checks if the cooldown check is enabled. + * + * @return true, if the cooldown check is enabled + */ + public boolean getCoolDownCheck() { + return getData().getBoolean(getCoolDownCheckPath()); + } + + /** + * Gets the path for the cooldown check. + * + * @return the cooldown check path + */ + public String getCoolDownCheckPath() { + if (plugin.getBungeeSettings().isUseBungeecoord()) { + return "CoolDownCheck_" + plugin.getBungeeSettings().getServerNameStorage(); + } + return "CoolDownCheck"; + } + + /** + * Checks if the cooldown check is enabled for a specific vote site. + * + * @param site the vote site + * @return true, if the cooldown check is enabled for the site + */ + public boolean getCoolDownCheckSite(VoteSite site) { + HashMap coolDownChecks = getCoolDownCheckSiteList(); + if (coolDownChecks.containsKey(site.getKey())) { + return coolDownChecks.get(site.getKey()).booleanValue(); + } + return false; + } + + /** + * Gets the list of cooldown checks for all vote sites. + * + * @return the list of cooldown checks for all vote sites + */ + public HashMap getCoolDownCheckSiteList() { + HashMap coolDownChecks = new HashMap<>(); + ArrayList coolDownCheck = getData().getStringList(getCoolDownCheckSitePath()); + for (String str : coolDownCheck) { + String[] data = str.split("//"); + if (data.length > 1 && plugin.getVoteSiteManager().hasVoteSite(data[0])) { + VoteSite site = plugin.getVoteSiteManager().getVoteSite(data[0], true); + if (site != null) { + Boolean b = Boolean.valueOf(data[1]); + coolDownChecks.put(site.getKey(), b); + } + } + } + return coolDownChecks; + } + + /** + * Gets the path for the cooldown check site list. + * + * @return the cooldown check site list path + */ + public String getCoolDownCheckSitePath() { + if (plugin.getBungeeSettings().isUseBungeecoord()) { + return "CoolDownCheck_" + plugin.getBungeeSettings().getServerNameStorage() + "_Sites"; + } + return "CoolDownCheck" + "_Sites"; + } + + /** + * Gets the daily total votes. + * + * @return the daily total votes + * @deprecated Use getTotal(TopVoter.Daily) instead + */ + @Deprecated + public int getDailyTotal() { + return getTotal(TopVoter.Daily); + } + + /** + * Gets the day vote streak. + * + * @return the day vote streak + */ + @Deprecated + public int getDayVoteStreak() { + return getData().getInt("DayVoteStreak"); + } + + /** + * Gets the last update time for the day vote streak. + * + * @return the last update time for the day vote streak + */ + @Deprecated + public long getDayVoteStreakLastUpdate() { + String str = getData().getString("DayVoteStreakLastUpdate"); + if (str == null || str.isEmpty() || str.equals("null")) { + return 0; + } + try { + return Long.parseLong(str); + } catch (NumberFormatException e) { + return 0; + } + } + + /** + * Checks if the broadcast is disabled. + * + * @return true if the broadcast is disabled, false otherwise + */ + public boolean getDisableBroadcast() { + return getUserData().getBoolean("DisableBroadcast"); + } + + /** + * Gets the day when the user has gotten all sites. + * + * @return the day when the user has gotten all sites + */ + public int getGottenAllSitesDay() { + return getData().getInt(plugin.getVotingPluginUserManager().getGottenAllSitesDayPath(), 0); + } + + /** + * Gets the day when the user has gotten almost all sites. + * + * @return the day when the user has gotten almost all sites + */ + public int getGottenAlmostAllSitesDay() { + return getData().getInt(plugin.getVotingPluginUserManager().getGottenAlmostAllSitesDayPath(), 0); + } + + /** + * Gets the highest daily total votes. + * + * @return the highest daily total votes + */ + public int getHighestDailyTotal() { + return getData().getInt("HighestDailyTotal"); + } + + /** + * Returns whether this user has already claimed the NameMC like reward. + * + * @return true if claimed + */ + public boolean hasClaimedNameMCLikeReward() { + return getUserData().getBoolean("NameMCLikeRewardClaimed"); + } + + /** + * Sets whether this user has already claimed the NameMC like reward. + * + * @param claimed true if claimed + */ + public void setClaimedNameMCLikeReward(boolean claimed) { + getUserData().setBoolean("NameMCLikeRewardClaimed", claimed); + } + + /** + * Gets the highest monthly total votes. + * + * @return the highest monthly total votes + */ + public int getHighestMonthlyTotal() { + return getData().getInt("HighestMonthlyTotal"); + } + + /** + * Gets the highest weekly total votes. + * + * @return the highest weekly total votes + */ + public int getHighestWeeklyTotal() { + return getData().getInt("HighestWeeklyTotal"); + } + + /** + * Gets the total votes for the last month. + * + * @return the total votes for the last month + */ + public int getLastMonthTotal() { + return getData().getInt("LastMonthTotal"); + } + + /** + * Gets the last votes for each vote site. + * + * @return a map of vote sites and the last vote time + */ + public HashMap getLastVotes() { + HashMap lastVotes = new HashMap<>(); + ArrayList lastVotesList = getUserData().getStringList("LastVotes"); + + for (String str : lastVotesList) { + String[] data = str.split("//"); + if (data.length <= 1) { + continue; + } + + String rawSiteKey = data[0]; + String rawTime = data[1]; + + if (!plugin.getVoteSiteManager().hasVoteSite(rawSiteKey)) { + continue; + } + + VoteSite site = plugin.getVoteSiteManager().getVoteSite(rawSiteKey, true); + if (site == null) { + continue; + } + + long time = 0; + try { + time = Long.parseLong(rawTime); + } catch (NumberFormatException ignored) { + time = 0; + } + + lastVotes.put(site, time); + } + + return lastVotes; + } + + /** + * Gets the time of the last vote. + * + * @return the time of the last vote + */ + public Long getLastVoteTime() { + Long time = Long.valueOf(0); + for (Long value : getLastVotes().values()) { + if (value.longValue() > time) { + time = value; + } + } + return time; + } + + /** + * Gets the last vote time for a specific vote site. + * + * @param voteSite the vote site + * @return the last vote time for the vote site + */ + public long getLastVoteTimer(VoteSite voteSite) { + HashMap times = getLastVotes(); + if (times.containsKey(voteSite)) { + return times.get(voteSite).longValue(); + } + return 0; + } + + /** + * Gets the last vote times sorted in descending order. + * + * @return a map of vote sites and the last vote times sorted in descending + * order + */ + public HashMap getLastVoteTimesSorted() { + LinkedHashMap times = new LinkedHashMap<>(); + + for (VoteSite voteSite : plugin.getVoteSiteManager().getVoteSitesEnabled()) { + times.put(voteSite, getTime(voteSite)); + } + LinkedHashMap sorted = new LinkedHashMap<>( + times.entrySet().stream().sorted(Collections.reverseOrder(Map.Entry.comparingByValue())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); + return sorted; + } + + /** + * Gets the total votes for the month. + * + * @return the total votes for the month + * @deprecated Use getTotal(TopVoter.Monthly) instead + */ + @Deprecated + public int getMonthTotal() { + return getTotal(TopVoter.Monthly); + } + + /** + * Gets the month vote streak. + * + * @return the month vote streak + */ + @Deprecated + public int getMonthVoteStreak() { + return getData().getInt("MonthVoteStreak"); + } + + /** + * Gets the next time all sites are available for voting. + * + * @return the next time all sites are available for voting + */ + public long getNextTimeAllSitesAvailable() { + long longest = 0; + for (VoteSite site : plugin.getVoteSiteManager().getVoteSitesEnabled()) { + long seconds = voteNextDurationTime(site); + if (seconds > longest) { + longest = seconds; + } + } + + return longest; + } + + /** + * Gets the next time the first site is available for voting. + * + * @return seconds until first site is available, or 0 if none + */ + public long getNextTimeFirstSiteAvailable() { + NextSite next = getNextSiteAvailable(); + return next == null ? 0 : next.getSecondsUntilAvailable(); + } + + /** + * Returns the next VoteSite that will become available, and how many seconds + * until it is available. + * + * - Only considers enabled sites. - Skips hidden sites (matching + * canVoteAll/canVoteAny intent for player-facing voting). - Only considers + * sites the player CANNOT currently vote on (seconds > 0). + * + * @return NextSite or null if there is no upcoming site (i.e. can vote all, or + * no delays) + */ + public NextSite getNextSiteAvailable() { + List sites = plugin.getVoteSiteManager().getVoteSitesEnabled(); + if (sites == null || sites.isEmpty()) { + return null; + } + + VoteSite bestSite = null; + long bestSeconds = 0; + + for (int i = 0; i < sites.size(); i++) { + VoteSite site = sites.get(i); + if (site == null) { + continue; + } + + // Match the same "don't care" sites as your other checks generally do + if (!site.isEnabled() || site.isHidden()) { + continue; + } + + // If you can already vote, it isn't "next" + if (canVoteSite(site)) { + continue; + } + + long seconds = voteNextDurationTime(site); // uses getTime(site) internally + if (seconds <= 0) { + continue; + } + + if (bestSite == null || seconds < bestSeconds) { + bestSite = site; + bestSeconds = seconds; + } + } + + return bestSite == null ? null : new NextSite(bestSite, bestSeconds); + } + + /** + * Gets the number of offline votes for the specified vote site. + * + * @param site the vote site + * @return the number of offline votes for the specified vote site + */ + public int getNumberOfOfflineVotes(VoteSite site) { + ArrayList offlineVotes = getOfflineVotes(); + int num = 0; + for (String str : offlineVotes) { + if (str.equals(site.getKey())) { + num++; + } + } + return num; + } + + /** + * Gets the list of offline votes. + * + * @return the list of offline votes + */ + public ArrayList getOfflineVotes() { + return getUserData().getStringList("OfflineVotes"); + } + + /** + * Gets the points of the user. + * + * @return the points of the user + */ + public int getPoints() { + return getUserData().getInt(getPointsPath()); + } + + /** + * Gets the path for the points. + * + * @return the points path + */ + public String getPointsPath() { + if (plugin.getBungeeSettings().isPerServerPoints()) { + return plugin.getBungeeSettings().getServerNameStorage() + "_Points"; + } + return "Points"; + } + + /** + * Gets the number of sites not voted on. + * + * @return the number of sites not voted on + */ + public int getSitesNotVotedOn() { + int amount = 0; + for (VoteSite site : plugin.getVoteSiteManager().getVoteSitesEnabled()) { + if (!site.isHidden()) { + if (site.getPermissionToView().isEmpty() || hasPermission(site.getPermissionToView(), false)) { + if (canVoteSite(site)) { + amount++; + } + } + } + } + return amount; + } + + public int getTotalNumberOfSites() { + int amount = 0; + for (VoteSite site : plugin.getVoteSiteManager().getVoteSitesEnabled()) { + if (!site.isHidden()) { + if (site.getPermissionToView().isEmpty() || hasPermission(site.getPermissionToView(), false)) { + amount++; + } + } + } + return amount; + } + + public int getSitesVotedOn() { + int amount = 0; + for (VoteSite site : plugin.getVoteSiteManager().getVoteSitesEnabled()) { + if (!canVoteSite(site)) { + amount++; + } + } + return amount; + } + + /** + * Gets the time. + * + * @param voteSite the vote site + * @return the time + */ + public long getTime(VoteSite voteSite) { + HashMap lastVotes = getLastVotes(); + if (lastVotes.containsKey(voteSite)) { + return lastVotes.get(voteSite); + } + return 0; + } + + /** + * Gets the top voter player. + * + * @return the top voter player + */ + public TopVoterPlayer getTopVoterPlayer() { + return new TopVoterPlayer(UUID.fromString(getUUID()), getPlayerName(), getLastOnline()); + } + + /** + * Gets the total votes for the specified top voter category. + * + * @param top the top voter category + * @return the total votes for the specified top voter category + */ + public int getTotal(TopVoter top) { + switch (top) { + case AllTime: + return getUserData().getInt("AllTimeTotal"); + case Daily: + return getUserData().getInt("DailyTotal"); + case Monthly: + if (plugin.getConfigFile().isUseMonthDateTotalsAsPrimaryTotal()) { + return getData().getInt(plugin.getVotingPluginUserManager().getMonthTotalsWithDatePath()); + } + return getData().getInt("MonthTotal"); + case Weekly: + return getUserData().getInt("WeeklyTotal"); + default: + break; + } + return 0; + } + + /** + * Gets the total votes for the specified top voter category at a specific time. + * + * @param top the top voter category + * @param atTime the specific time + * @return the total votes for the specified top voter category at the specific + * time + */ + public int getTotal(TopVoter top, LocalDateTime atTime) { + switch (top) { + case AllTime: + return getUserData().getInt("AllTimeTotal"); + case Daily: + return getUserData().getInt("DailyTotal"); + case Monthly: + if (plugin.getConfigFile().isUseMonthDateTotalsAsPrimaryTotal()) { + return getData().getInt(plugin.getVotingPluginUserManager().getMonthTotalsWithDatePath(atTime)); + } + return getData().getInt("MonthTotal"); + case Weekly: + return getUserData().getInt("WeeklyTotal"); + default: + break; + } + return 0; + } + + /** + * Gets the number of votes for the vote party. + * + * @return the number of votes for the vote party + */ + public int getVotePartyVotes() { + return getUserData().getInt("VotePartyVotes"); + } + + /** + * Gets the vote shop identifier limit. + * + * @param identifier the identifier for the vote shop + * @return the vote shop identifier limit + */ + public int getVoteShopIdentifierLimit(String identifier) { + return getData().getInt("VoteShopLimit" + identifier); + } + + /** + * Gets the weekly total votes. + * + * @return the weekly total votes + * @deprecated Use getTotal(TopVoter.Weekly) instead + */ + @Deprecated + public int getWeeklyTotal() { + return getTotal(TopVoter.Weekly); + } + + /** + * Gets the week vote streak. + * + * @return the week vote streak + */ + @Deprecated + public int getWeekVoteStreak() { + return getData().getInt("WeekVoteStreak"); + } + + /** + * Gives the daily top voter award. + * + * @param place the place of the top voter + * @param path the path to the reward configuration + */ + public void giveDailyTopVoterAward(int place, String path) { + SpecialRewardType type = SpecialRewardType.TOPVOTER; + type.setType("Daily"); + type.setAmount(1); + PlayerSpecialRewardEvent event = new PlayerSpecialRewardEvent(this, type, null); + Bukkit.getPluginManager().callEvent(event); + + if (event.isCancelled()) { + return; + } + new RewardBuilder(plugin.getSpecialRewardsConfig().getData(), + plugin.getSpecialRewardsConfig().getDailyAwardRewardsPath(path)).withPlaceHolder("place", "" + place) + .withPlaceHolder("topvoter", "Daily").withPlaceHolder("votes", "" + getTotal(TopVoter.Daily)) + .setOnline(isOnline()).send(this); + } + + /** + * Gives the monthly top voter award. + * + * @param place the place of the top voter + * @param path the path to the reward configuration + */ + public void giveMonthlyTopVoterAward(int place, String path) { + SpecialRewardType type = SpecialRewardType.TOPVOTER; + type.setType("Monthly"); + type.setAmount(1); + PlayerSpecialRewardEvent event = new PlayerSpecialRewardEvent(this, type, null); + Bukkit.getPluginManager().callEvent(event); + + if (event.isCancelled()) { + return; + } + new RewardBuilder(plugin.getSpecialRewardsConfig().getData(), + plugin.getSpecialRewardsConfig().getMonthlyAwardRewardsPath(path)).withPlaceHolder("place", "" + place) + .withPlaceHolder("topvoter", "Monthly").withPlaceHolder("votes", "" + getTotal(TopVoter.Monthly)) + .setOnline(isOnline()).send(this); + } + + /** + * Gives the weekly top voter award. + * + * @param place the place of the top voter + * @param path the path to the reward configuration + */ + public void giveWeeklyTopVoterAward(int place, String path) { + SpecialRewardType type = SpecialRewardType.TOPVOTER; + type.setType("Weekly"); + type.setAmount(1); + PlayerSpecialRewardEvent event = new PlayerSpecialRewardEvent(this, type, null); + Bukkit.getPluginManager().callEvent(event); + + if (event.isCancelled()) { + return; + } + new RewardBuilder(plugin.getSpecialRewardsConfig().getData(), + plugin.getSpecialRewardsConfig().getWeeklyAwardRewardsPath(path)).withPlaceHolder("place", "" + place) + .withPlaceHolder("topvoter", "Weekly").withPlaceHolder("votes", "" + getTotal(TopVoter.Weekly)) + .setOnline(isOnline()).send(this); + } + + /** + * Gets how many unique vote sites this user has voted on today. + * + * Uses existing LastVotes data (no storage). A site counts if its last-vote + * timestamp falls on "today" using VotingPlugin's time offset. + * + * @return number of unique sites voted on today + */ + public long getUniqueVoteSitesToday() { + LocalDateTime now = plugin.getTimeChecker().getTime(); + LocalDate today = now.toLocalDate(); + + long count = 0; + + for (VoteSite site : plugin.getVoteSiteManager().getVoteSitesEnabled()) { + if (site == null || !site.isEnabled() || site.isHidden()) { + continue; + } + + long time = getTime(site); + if (time <= 0) { + continue; + } + + // Match the same offset handling as canVoteSite() + LocalDateTime lastVote = LocalDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneId.systemDefault()) + .plusHours(plugin.getOptions().getTimeHourOffSet()); + + if (lastVote.toLocalDate().equals(today)) { + count++; + } + } + + return count; + } + + /** + * Checks if the user has a percentage of the total votes. + * + * @param top the top voter category + * @param percentage the percentage of the total votes + * @param time the specific time + * @return true if the user has the percentage of the total votes, false + * otherwise + */ + public boolean hasPercentageTotal(TopVoter top, double percentage, LocalDateTime time) { + int total = getTotal(top, time); + switch (top) { + case Daily: + return (double) total / (double) plugin.getVoteSiteManager().getVoteSitesEnabled().size() + * 100 > percentage; + case Monthly: + return total / ((double) plugin.getVoteSiteManager().getVoteSitesEnabled().size() + * time.getMonth().length(false)) * 100 > percentage; + case Weekly: + return total / ((double) plugin.getVoteSiteManager().getVoteSitesEnabled().size() * 7) * 100 > percentage; + default: + return false; + } + } + + /** + * Checks if the user is ignored for top voter. + * + * @return true if the user is ignored for top voter, false otherwise + */ + public boolean isTopVoterIgnore() { + return getUserData().getBoolean("TopVoterIgnore"); + } + + /** + * Gives login rewards to the user. + */ + public void loginRewards() { + if (plugin.getRewardHandler().hasRewards(plugin.getSpecialRewardsConfig().getData(), "LoginRewards")) { + new RewardBuilder(plugin.getSpecialRewardsConfig().getData(), "LoginRewards").send(this); + } + } + + /** + * Gives logout rewards to the user. + */ + public void logoutRewards() { + if (plugin.getRewardHandler().hasRewards(plugin.getSpecialRewardsConfig().getData(), "LogoutRewards")) { + new RewardBuilder(plugin.getSpecialRewardsConfig().getData(), "LogoutRewards").send(this); + } + } + + /** + * Merges the provided data with the current data. + * + * @param toAdd the data to add + */ + public void mergeData(HashMap toAdd) { + HashMap currentData = getData().getValues(); + HashMap newData = new HashMap<>(); + + for (TopVoter top : TopVoter.values()) { + if (toAdd.containsKey(top.getColumnName()) && currentData.containsKey(top.getColumnName())) { + newData.put(top.getColumnName(), new DataValueInt( + currentData.get(top.getColumnName()).getInt() + toAdd.get(top.getColumnName()).getInt())); + } + } + + if (newData.size() > 0) { + getData().setValues(newData); + } + } + + /** + * Processes offline votes. + */ + public void offVote() { + if (!plugin.getOptions().isProcessRewards()) { + plugin.debug("Processing rewards is disabled"); + return; + } + + Player player = getPlayer(); + if (!plugin.getOptions().isOnlineMode()) { + player = Bukkit.getPlayer(getPlayerName()); + } + if (player == null) { + return; + } + + plugin.extraDebug("Checking offline votes for " + player.getName() + "/" + getUUID()); + + // Update top voter ignore flag if needed. + boolean currentTopVoterIgnore = player.hasPermission("VotingPlugin.TopVoter.Ignore"); + if (isTopVoterIgnore() != currentTopVoterIgnore) { + setTopVoterIgnore(currentTopVoterIgnore); + } + + ArrayList offlineVotes = getOfflineVotes(); + if (offlineVotes.isEmpty()) { + return; + } + + // Send vote effects and clear persistent offline votes. + sendVoteEffects(false); + setOfflineVotes(new ArrayList<>()); + + // Process each offline vote. + for (String voteSiteName : offlineVotes) { + if (plugin.getVoteSiteManager().hasVoteSite(voteSiteName)) { + VoteSite voteSite = plugin.getVoteSiteManager().getVoteSite(voteSiteName, true); + if (voteSite != null && voteSite.isEnabled()) { + plugin.debug("Giving offline site reward: " + voteSiteName); + playerVote(voteSite, false, false); + } else { + plugin.debug("Skipping offline vote for disabled site: " + voteSiteName); + } + } else { + plugin.debug("Site doesn't exist: " + voteSiteName); + } + } + } + + /** + * Processes a player vote. + * + * @param voteSite the vote site + * @param online whether the player is online + * @param bungee whether to use bungee + */ + public void playerVote(VoteSite voteSite, boolean online, boolean bungee) { + voteSite.giveRewards(this, online, bungee); + } + + /** + * Removes points from the user. + * + * @param points the number of points to remove + * @return true if the points were removed, false otherwise + */ + public boolean removePoints(int points) { + if (getPoints() >= points) { + setPoints(getPoints() - points); + return true; + } + return false; + } + + /** + * Removes points from the user asynchronously. + * + * @param points the number of points to remove + * @param async whether to remove the points asynchronously + * @return true if the points were removed, false otherwise + */ + public boolean removePoints(int points, boolean async) { + if (getPoints() >= points) { + setPoints(getPoints() - points, async); + return true; + } + return false; + } + + /** + * Resets the last voted time for all vote sites. + */ + public void resetLastVoted() { + HashMap map = getLastVotes(); + for (Entry e : map.entrySet()) { + e.setValue(0l); + } + setLastVotes(map); + } + + /** + * Resets the last voted time for a specific vote site. + * + * @param site the vote site + */ + public void resetLastVoted(VoteSite site) { + HashMap map = getLastVotes(); + map.put(site, 0l); + setLastVotes(map); + } + + /** + * Resets the total votes for a specific top voter category. + * + * @param topVoter the top voter category + */ + public void resetTotals(TopVoter topVoter) { + setTotal(topVoter, 0); + } + + /** + * Sends vote effects to the user. + * + * @param online whether the user is online + */ + public void sendVoteEffects(boolean online) { + plugin.getRewardHandler().giveReward(this, plugin.getSpecialRewardsConfig().getData(), + plugin.getSpecialRewardsConfig().getAnySiteRewardsPath(), new RewardOptions().setOnline(online)); + } + + /** + * Sets the all-time total votes. + * + * @param allTimeTotal the all-time total votes + * @deprecated Use setTotal(TopVoter.AllTime, allTimeTotal) instead + */ + @Deprecated + public void setAllTimeTotal(int allTimeTotal) { + setTotal(TopVoter.AllTime, allTimeTotal); + } + + /** + * Sets the best day vote streak. + * + * @param streak the best day vote streak + */ + @Deprecated + public void setBestDayVoteStreak(int streak) { + getData().setInt("BestDayVoteStreak", streak); + } + + /** + * Sets the best month vote streak. + * + * @param streak the best month vote streak + */ + @Deprecated + public void setBestMonthVoteStreak(int streak) { + getData().setInt("BestMonthVoteStreak", streak); + } + + /** + * Sets the best week vote streak. + * + * @param streak the best week vote streak + */ + @Deprecated + public void setBestWeekVoteStreak(int streak) { + getData().setInt("BestWeekVoteStreak", streak); + } + + /** + * Sets the cooldown check. + * + * @param coolDownCheck whether the cooldown check is enabled + */ + public void setCoolDownCheck(boolean coolDownCheck) { + getData().setBoolean(getCoolDownCheckPath(), coolDownCheck); + } + + /** + * Sets the cooldown check for all vote sites. + * + * @param coolDownChecks the cooldown checks for all vote sites + */ + public void setCoolDownCheckSite(HashMap coolDownChecks) { + ArrayList data = new ArrayList<>(); + for (Entry entry : coolDownChecks.entrySet()) { + String str = entry.getKey() + "//" + entry.getValue().toString(); + data.add(str); + } + getUserData().setStringList(getCoolDownCheckSitePath(), data); + } + + /** + * Sets the cooldown check for a specific vote site. + * + * @param site the vote site + * @param value whether the cooldown check is enabled + */ + public void setCoolDownCheckSite(VoteSite site, boolean value) { + HashMap coolDownChecks = getCoolDownCheckSiteList(); + coolDownChecks.put(site.getKey(), Boolean.valueOf(value)); + setCoolDownCheckSite(coolDownChecks); + } + + /** + * Sets the daily total votes. + * + * @param total the daily total votes + * @deprecated Use setTotal(TopVoter.Daily, total) instead + */ + @Deprecated + public void setDailyTotal(int total) { + setTotal(TopVoter.Daily, total); + } + + /** + * Sets the day vote streak. + * + * @param streak the day vote streak + */ + @Deprecated + public void setDayVoteStreak(int streak) { + getData().setInt("DayVoteStreak", streak); + if (getBestDayVoteStreak() < streak) { + setBestDayVoteStreak(streak); + } + } + + /** + * Sets the last update time for the day vote streak. + * + * @param time the last update time for the day vote streak + */ + @Deprecated + public void setDayVoteStreakLastUpdate(long time) { + getData().setString("DayVoteStreakLastUpdate", "" + time); + } + + /** + * Sets whether the broadcast is disabled. + * + * @param value true to disable the broadcast, false otherwise + */ + public void setDisableBroadcast(boolean value) { + getUserData().setBoolean("DisableBroadcast", value); + } + + /** + * Sets the day when the user has gotten all sites. + * + * @param day the day when the user has gotten all sites + */ + public void setGottenAllSitesDay(int day) { + getData().setInt(plugin.getVotingPluginUserManager().getGottenAllSitesDayPath(), day); + } + + /** + * Sets the day when the user has gotten almost all sites. + * + * @param day the day when the user has gotten almost all sites + */ + public void setGottenAlmostAllSitesDay(int day) { + getData().setInt(plugin.getVotingPluginUserManager().getGottenAlmostAllSitesDayPath(), day); + } + + /** + * Sets the highest daily total votes. + * + * @param total the highest daily total votes + */ + public void setHighestDailyTotal(int total) { + getData().setInt("HighestDailyTotal", total); + } + + /** + * Sets the highest monthly total votes. + * + * @param total the highest monthly total votes + */ + public void setHighestMonthlyTotal(int total) { + getData().setInt("HighestMonthlyTotal", total); + } + + /** + * Sets the highest weekly total votes. + * + * @param total the highest weekly total votes + */ + public void setHighestWeeklyTotal(int total) { + getData().setInt("HighestWeeklyTotal", total); + } + + /** + * Sets the total votes for the last month. + * + * @param total the total votes for the last month + */ + public void setLastMonthTotal(int total) { + getData().setInt("LastMonthTotal", total); + } + + /** + * Sets the last votes for each vote site. + * + * @param lastVotes a map of vote sites and the last vote time + */ + public void setLastVotes(HashMap lastVotes) { + ArrayList data = new ArrayList<>(); + for (Entry entry : lastVotes.entrySet()) { + String str = entry.getKey().getKey() + "//" + entry.getValue().longValue(); + data.add(str); + } + getUserData().setStringList("LastVotes", data); + } + + /** + * Sets the total votes for the month. + * + * @param total the total votes for the month + * @deprecated Use setTotal(TopVoter.Monthly, total) instead + */ + @Deprecated + public void setMonthTotal(int total) { + setTotal(TopVoter.Monthly, total); + } + + /** + * Sets the month vote streak. + * + * @param streak the month vote streak + */ + @Deprecated + public void setMonthVoteStreak(int streak) { + getData().setInt("MonthVoteStreak", streak); + if (getBestMonthVoteStreak() < streak) { + setBestMonthVoteStreak(streak); + } + } + + /** + * Sets the list of offline votes. + * + * @param offlineVotes the list of offline votes + */ + public void setOfflineVotes(ArrayList offlineVotes) { + getUserData().setStringList("OfflineVotes", offlineVotes); + } + + /** + * Sets the points of the user. + * + * @param value the number of points + */ + public void setPoints(int value) { + getUserData().setInt(getPointsPath(), value, false); + } + + /** + * Sets the points of the user asynchronously. + * + * @param value the number of points + * @param async whether to set the points asynchronously + */ + public void setPoints(int value, boolean async) { + getUserData().setInt(getPointsPath(), value, false, async); + } + + /** + * Sets the current time for the specified vote site. + * + * @param voteSite the vote site + */ + public void setTime(VoteSite voteSite) { + setTime(voteSite, LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); + } + + /** + * Sets the specified time for the specified vote site. + * + * @param voteSite the vote site + * @param time the time to set + */ + public void setTime(VoteSite voteSite, Long time) { + HashMap lastVotes = getLastVotes(); + if (lastVotes != null && lastVotes.containsKey(voteSite)) { + if (lastVotes.get(voteSite).longValue() == time.longValue()) { + plugin.debug("Not setting last vote time for " + voteSite.getKey() + ", already set to " + time); + return; + } + } + lastVotes.put(voteSite, time); + setLastVotes(lastVotes); + } + + /** + * Sets whether the user is ignored for top voter. + * + * @param topVoterIgnore true to ignore the user for top voter, false otherwise + */ + public void setTopVoterIgnore(boolean topVoterIgnore) { + getUserData().setString("TopVoterIgnore", "" + topVoterIgnore); + } + + /** + * Sets the total votes for the specified top voter category. + * + * @param top the top voter category + * @param value the total votes to set + */ + public void setTotal(TopVoter top, int value) { + switch (top) { + case AllTime: + getUserData().setInt("AllTimeTotal", value); + break; + case Daily: + getUserData().setInt("DailyTotal", value); + break; + case Monthly: + if (plugin.getConfigFile().isLimitMonthlyVotes()) { + LocalDateTime time = plugin.getTimeChecker().getTime(); + int days = time.getDayOfMonth(); + if (value >= days * plugin.getVoteSiteManager().getVoteSitesEnabled().size()) { + value = days * plugin.getVoteSiteManager().getVoteSitesEnabled().size(); + } + } + getData().setInt("MonthTotal", value); + if (plugin.getConfigFile().isStoreMonthTotalsWithDate()) { + getData().setInt(plugin.getVotingPluginUserManager().getMonthTotalsWithDatePath(), value); + } + break; + case Weekly: + getUserData().setInt("WeeklyTotal", value); + break; + default: + break; + } + } + + /** + * Sets the number of votes for the vote party. + * + * @param value the number of votes to set + */ + public void setVotePartyVotes(int value) { + getUserData().setInt("VotePartyVotes", value); + } + + /** + * Sets the vote shop identifier limit. + * + * @param identifier the identifier for the vote shop + * @param value the limit to set + */ + public void setVoteShopIdentifierLimit(String identifier, int value) { + getData().setInt("VoteShopLimit" + identifier, value); + } + + /** + * Sets the weekly total votes. + * + * @param total the weekly total votes + * @deprecated Use setTotal(TopVoter.Weekly, total) instead + */ + @Deprecated + public void setWeeklyTotal(int total) { + setTotal(TopVoter.Weekly, total); + } + + /** + * Sets the week vote streak. + * + * @param streak the week vote streak + */ + @Deprecated + public void setWeekVoteStreak(int streak) { + getData().setInt("WeekVoteStreak", streak); + if (getBestWeekVoteStreak() < streak) { + setBestWeekVoteStreak(streak); + } + } + + /** + * Checks if the user should be reminded. + * + * @return true if the user should be reminded, false otherwise + */ + public boolean shouldBeReminded() { + Player player = getPlayer(); + if (player != null) { + if (player.hasPermission("VotingPlugin.NoRemind")) { + return false; + } + } + return true; + } + + /** + * Gets the last vote date for the specified vote site. + * + * @param voteSite the vote site + * @return the last vote date as a string + * @deprecated Use getTime(VoteSite) instead + */ + @Deprecated + public String voteCommandLastDate(VoteSite voteSite) { + long time = getTime(voteSite); + if (time > 0) { + Date date = new Date(time); + String timeString = new SimpleDateFormat(plugin.getConfigFile().getFormatTimeFormat()).format(date); + if (MessageAPI.containsIgnorecase(timeString, "YamlConfiguration")) { + plugin.getLogger().warning("Detected issue parsing time, check time format"); + } + return timeString; + } + return ""; + } + + /** + * Gets the duration since the last vote for the specified vote site. + * + * @param voteSite the vote site + * @return the duration since the last vote as a string + */ + public String voteCommandLastDuration(VoteSite voteSite) { + long time = getTime(voteSite); + if (time > 0) { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime lastVote = LocalDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneId.systemDefault()); + + Duration dur = Duration.between(lastVote, now); + + long diffSecond = dur.getSeconds(); + int diffDays = (int) (diffSecond / 60 / 60 / 24); + int diffHours = (int) (diffSecond / 60 / 60 - diffDays * 24); + int diffMinutes = (int) (diffSecond / 60 - diffHours * 60 - diffDays * 24 * 60); + int diffSeconds = (int) (diffSecond - diffMinutes * 60 - diffHours * 60 * 60 - diffDays * 24 * 60 * 60); + + String info = ""; + if (diffDays == 1) { + info += PlaceholderUtils.replacePlaceHolder(PlaceholderUtils.replacePlaceHolder( + plugin.getConfigFile().getFormatCommandsVoteLastTimeFormat(), "TimeType", + plugin.getConfigFile().getFormatTimeFormatsDay()), "amount", "" + diffDays); + info += " "; + } else if (diffDays > 1) { + info += PlaceholderUtils.replacePlaceHolder(PlaceholderUtils.replacePlaceHolder( + plugin.getConfigFile().getFormatCommandsVoteLastTimeFormat(), "TimeType", + plugin.getConfigFile().getFormatTimeFormatsDays()), "amount", "" + diffDays); + info += " "; + } + + if (diffHours == 1) { + info += PlaceholderUtils.replacePlaceHolder(PlaceholderUtils.replacePlaceHolder( + plugin.getConfigFile().getFormatCommandsVoteLastTimeFormat(), "TimeType", + plugin.getConfigFile().getFormatTimeFormatsHour()), "amount", "" + diffHours); + info += " "; + } else if (diffHours > 1) { + info += PlaceholderUtils.replacePlaceHolder(PlaceholderUtils.replacePlaceHolder( + plugin.getConfigFile().getFormatCommandsVoteLastTimeFormat(), "TimeType", + plugin.getConfigFile().getFormatTimeFormatsHours()), "amount", "" + diffHours); + info += " "; + } + + if (diffMinutes == 1) { + info += PlaceholderUtils.replacePlaceHolder(PlaceholderUtils.replacePlaceHolder( + plugin.getConfigFile().getFormatCommandsVoteLastTimeFormat(), "TimeType", + plugin.getConfigFile().getFormatTimeFormatsMinute()), "amount", "" + diffMinutes); + info += " "; + } else if (diffMinutes > 1) { + info += PlaceholderUtils.replacePlaceHolder(PlaceholderUtils.replacePlaceHolder( + plugin.getConfigFile().getFormatCommandsVoteLastTimeFormat(), "TimeType", + plugin.getConfigFile().getFormatTimeFormatsMinutes()), "amount", "" + diffMinutes); + info += " "; + } + + if (plugin.getConfigFile().isFormatCommandsVoteLastIncludeSeconds()) { + if (diffSeconds == 1) { + info += PlaceholderUtils.replacePlaceHolder(PlaceholderUtils.replacePlaceHolder( + plugin.getConfigFile().getFormatCommandsVoteLastTimeFormat(), "TimeType", + plugin.getConfigFile().getFormatTimeFormatsSecond()), "amount", "" + diffSeconds); + } else { + info += PlaceholderUtils.replacePlaceHolder(PlaceholderUtils.replacePlaceHolder( + plugin.getConfigFile().getFormatCommandsVoteLastTimeFormat(), "TimeType", + plugin.getConfigFile().getFormatTimeFormatsSeconds()), "amount", "" + diffSeconds); + } + } + + info = PlaceholderUtils.replacePlaceHolder(plugin.getConfigFile().getFormatCommandsVoteLastLastVoted(), + "times", info); + + return info; + } + return plugin.getConfigFile().getFormatCommandsVoteLastNeverVoted(); + } + + /** + * Gets the last vote date and duration for the specified vote site for the GUI. + * + * @param voteSite the vote site + * @return the last vote date and duration as a string for the GUI + */ + public String voteCommandLastGUILine(VoteSite voteSite) { + String timeString = voteCommandLastDate(voteSite); + String timeSince = voteCommandLastDuration(voteSite); + + HashMap placeholders = new HashMap<>(); + placeholders.put("time", timeString); + placeholders.put("SiteName", voteSite.getDisplayName()); + placeholders.put("timesince", timeSince); + + return PlaceholderUtils.replacePlaceHolder(plugin.getGui().getChestVoteLastLine(), placeholders); + } + + /** + * Gets the last vote date and duration for the specified vote site. + * + * @param voteSite the vote site + * @return the last vote date and duration as a string + */ + public String voteCommandLastLine(VoteSite voteSite) { + String timeString = voteCommandLastDate(voteSite); + String timeSince = voteCommandLastDuration(voteSite); + + HashMap placeholders = new HashMap<>(); + placeholders.put("time", timeString); + placeholders.put("SiteName", voteSite.getDisplayName()); + placeholders.put("timesince", timeSince); + + return PlaceholderUtils.replacePlaceHolder(plugin.getConfigFile().getFormatCommandsVoteLastLine(), + placeholders); + } + + /** + * Gets the next available vote time for the specified vote site. + * + * @param voteSite the vote site + * @return the next available vote time as a string + */ + public String voteCommandNextInfo(VoteSite voteSite) { + return voteCommandNextInfo(voteSite, getTime(voteSite)); + } + + /** + * Gets the next available vote time for the specified vote site. + * + * @param voteSite the vote site + * @param time the current time + * @return the next available vote time as a string + */ + public String voteCommandNextInfo(VoteSite voteSite, long time) { + String info = new String(); + + long nextTime = voteNextDurationTime(voteSite, time); + if (nextTime == 0) { + info = plugin.getConfigFile().getFormatCommandsVoteNextInfoCanVote(); + } else { + int diffHours = (int) (nextTime / (60 * 60)); + long diffMinutes = nextTime / 60 - diffHours * 60; + + if (diffHours < 0) { + diffHours = diffHours * -1; + } + if (diffHours >= 24) { + diffHours = diffHours - 24; + } + if (diffMinutes < 0) { + diffMinutes = diffMinutes * -1; + } + + String timeMsg = plugin.getConfigFile().getFormatCommandsVoteNextInfoVoteDelayDaily(); + timeMsg = MessageAPI.replaceIgnoreCase(timeMsg, "%hours%", Integer.toString(diffHours)); + timeMsg = MessageAPI.replaceIgnoreCase(timeMsg, "%minutes%", Long.toString(diffMinutes)); + info = timeMsg; + } + + return info; + } + + /** + * Gets the next available vote duration time for the specified vote site. + * + * @param voteSite the vote site + * @return the next available vote duration time in seconds + */ + public long voteNextDurationTime(VoteSite voteSite) { + return voteNextDurationTime(voteSite, getTime(voteSite)); + } + + /** + * Gets the next available vote duration time for the specified vote site. + * + * @param voteSite the vote site + * @param time the last vote time (epoch millis) + * @return the next available vote duration time in seconds + */ + public long voteNextDurationTime(VoteSite voteSite, long time) { + LocalDateTime now = plugin.getTimeChecker().getTime(); + + LocalDateTime lastVote = LocalDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneId.systemDefault()) + .plusHours(plugin.getOptions().getTimeHourOffSet()); + + if (!voteSite.isVoteDelayDaily()) { + ParsedDuration voteDelay = voteSite.getVoteDelay(); + + if (time == 0 || voteDelay == null || voteDelay.isEmpty()) { + return 0; + } + + // Ignore months, use fixed duration only + LocalDateTime nextVote = lastVote.plus(Duration.ofMillis(voteDelay.getMillis())); + + if (now.isAfter(nextVote)) { + return 0; + } + + return Duration.between(now, nextVote).getSeconds(); + } + + // Daily reset logic (unchanged) + LocalDateTime resetTime = lastVote.withHour(voteSite.getVoteDelayDailyHour()).withMinute(0).withSecond(0); + + LocalDateTime resetTimeTomorrow = resetTime.plusHours(24); + + if (lastVote.isBefore(resetTime)) { + if (now.isBefore(resetTime)) { + return Duration.between(now, resetTime).getSeconds(); + } + } else { + if (now.isBefore(resetTimeTomorrow)) { + return Duration.between(now, resetTimeTomorrow).getSeconds(); + } + } + + return 0; + } + + /** + * Checks if the vote streak was updated today. + * + * @param time the current time + * @return true if the vote streak was updated today, false otherwise + */ + @Deprecated + public boolean voteStreakUpdatedToday(LocalDateTime time) { + return MiscUtils.getInstance().getTime(getDayVoteStreakLastUpdate()).getDayOfYear() == time.getDayOfYear(); + } + + public String getVoteStreakState(String columnName) { + return getData().getString(columnName); + } + + public void setVoteStreakState(String columnName, String value) { + getData().setString(columnName, value); + } + +} From 5a0b1d191186d9ffbe83c5516624f6c03fa2fcd4 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 8 Aug 2026 18:15:14 -0600 Subject: [PATCH 03/21] Harden vote-delay rejection compatibility and replay retention --- .../java/com/bencodez/votingplugin/proxy/VotingPluginWire.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginWire.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginWire.java index c591e7d72..723150e92 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginWire.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginWire.java @@ -23,7 +23,7 @@ public final class VotingPluginWire { private VotingPluginWire() { } - public static final int SCHEMA_VERSION = 1; + public static final int SCHEMA_VERSION = 2; // ========================= // Subchannels (canonical) From 0abb8d20eabf39c90556b3e221c27a3504a73071 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 8 Aug 2026 18:15:16 -0600 Subject: [PATCH 04/21] Harden vote-delay rejection compatibility and replay retention --- .../bencodez/votingplugin/BungeeHandler.java | 1685 +++++++++-------- 1 file changed, 844 insertions(+), 841 deletions(-) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/BungeeHandler.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/BungeeHandler.java index 275da4de9..7ecbe5fd1 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/BungeeHandler.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/BungeeHandler.java @@ -1,841 +1,844 @@ -// File: com/bencodez/votingplugin/BungeeHandler.java -package com.bencodez.votingplugin; - -import java.io.File; -import java.sql.SQLException; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.event.Listener; -import org.eclipse.paho.client.mqttv3.MqttException; - -import com.bencodez.advancedcore.api.misc.MiscUtils; -import com.bencodez.advancedcore.api.rewards.RewardBuilder; -import com.bencodez.advancedcore.api.time.TimeType; -import com.bencodez.advancedcore.api.user.UserStorage; -import com.bencodez.advancedcore.bungeeapi.globaldata.GlobalDataHandler; -import com.bencodez.advancedcore.bungeeapi.globaldata.GlobalMySQL; -import com.bencodez.simpleapi.encryption.EncryptionHandler; -import com.bencodez.simpleapi.servercomm.codec.JsonEnvelope; -import com.bencodez.simpleapi.servercomm.global.GlobalMessageHandler; -import com.bencodez.simpleapi.servercomm.global.GlobalMessageListener; -import com.bencodez.simpleapi.servercomm.mqtt.MqttHandler; -import com.bencodez.simpleapi.servercomm.mqtt.MqttServerComm; -import com.bencodez.simpleapi.servercomm.mysql.MySqlMessenger; -import com.bencodez.simpleapi.servercomm.pluginmessage.PluginMessageHandler; -import com.bencodez.simpleapi.servercomm.redis.RedisHandler; -import com.bencodez.simpleapi.servercomm.redis.RedisListener; -import com.bencodez.simpleapi.servercomm.sockets.ClientHandler; -import com.bencodez.simpleapi.servercomm.sockets.SocketHandler; -import com.bencodez.simpleapi.servercomm.sockets.SocketReceiver; -import com.bencodez.simpleapi.sql.data.DataValue; -import com.bencodez.simpleapi.sql.data.DataValueBoolean; -import com.bencodez.simpleapi.sql.mysql.config.MysqlConfigSpigot; -import com.bencodez.votingplugin.proxy.BungeeMethod; -import com.bencodez.votingplugin.proxy.VoteTotalsSnapshot; -import com.bencodez.votingplugin.proxy.VotingPluginWire; -import com.bencodez.votingplugin.user.VotingPluginUser; -import com.bencodez.votingplugin.util.ServiceSiteValidator; -import com.bencodez.votingplugin.votesites.VoteSite; - -import lombok.Getter; - -/** - * Handler for Bungee/proxy server integration. - */ -public class BungeeHandler implements Listener { - - private static final long PROCESSED_VOTE_TTL_MILLIS = TimeUnit.MINUTES.toMillis(30); - - @Getter - private final ConcurrentHashMap processedWireVotes = new ConcurrentHashMap<>(); - @Getter - private ClientHandler clientHandler; - - private EncryptionHandler encryptionHandler; - - @Getter - private BungeeMethod method; - - private VotingPluginMain plugin; - - @Getter - private int bungeeVotePartyCurrent = -2; - - @Getter - private int bungeeVotePartyRequired = -2; - - @Getter - private SocketHandler socketHandler; - - private GlobalDataHandler globalDataHandler; - - @Getter - private ScheduledExecutorService timer; - - @Getter - private RedisHandler redisHandler; - - @Getter - private GlobalMessageHandler globalMessageHandler; - - private Thread redisThread; - - @Getter - private MySqlMessenger backendMysqlMessenger; - - @Getter - private MqttHandler mqttHandler; - - /** - * Constructs a new BungeeHandler. - * - * @param plugin the main plugin instance - */ - public BungeeHandler(VotingPluginMain plugin) { - this.plugin = plugin; - } - - /** - * Checks and processes global data from the global data handler. - */ - public void checkGlobalData() { - HashMap data = globalDataHandler.getExact(plugin.getBungeeSettings().getServer()); - - if (data.containsKey("ForceUpdate")) { - boolean b = checkGlobalDataTimeValue(data.get("ForceUpdate")); - if (b) { - if (plugin.getStorageType().equals(UserStorage.MYSQL)) { - plugin.getMysql().clearCacheBasic(); - } - plugin.getUserManager().getDataManager().clearCache(); - plugin.setUpdate(true); - plugin.update(); - globalDataHandler.setBoolean(plugin.getBungeeSettings().getServer(), "ForceUpdate", false); - } - } - - boolean forceUpdate = false; - - if (checkGlobalDataTime(TimeType.MONTH, data)) { - forceUpdate = true; - } - if (checkGlobalDataTime(TimeType.WEEK, data)) { - forceUpdate = true; - } - if (checkGlobalDataTime(TimeType.DAY, data)) { - forceUpdate = true; - } - - if (forceUpdate) { - HashMap dataToSet = new HashMap<>(); - dataToSet.put("FinishedProcessing", new DataValueBoolean(true)); - dataToSet.put("Processing", new DataValueBoolean(false)); - globalDataHandler.setData(plugin.getBungeeSettings().getServer(), dataToSet); - } - } - - /** - * Checks global data for a time type change. - * - * @param type the time type to check - * @param data the global data map - * @return true if currently processing a time change - */ - public boolean checkGlobalDataTime(TimeType type, HashMap data) { - boolean isProcessing = false; - if (data.containsKey(type.toString())) { - - DataValue value = data.get(type.toString()); - boolean b = checkGlobalDataTimeValue(value); - if (b) { - long lastUpdated = Long.valueOf(data.get("LastUpdated").getString()).longValue(); - plugin.debug("LastUpdated: " + lastUpdated); - if (LocalDateTime.now().atZone(ZoneOffset.UTC).toInstant().toEpochMilli() - lastUpdated > 1000 * 60 * 60 - * 2) { - plugin.getLogger().warning("Ignoring bungee time change since it was more than 2 hours ago"); - globalDataHandler.setBoolean(plugin.getBungeeSettings().getServer(), type.toString(), false); - return false; - } - - globalDataHandler.setBoolean(plugin.getBungeeSettings().getServer(), "Processing", true); - isProcessing = true; - - plugin.debug("Detected time change from bungee: " + type.toString()); - plugin.getTimeChecker().forceChanged(type, false, true, true); - globalDataHandler.setBoolean(plugin.getBungeeSettings().getServer(), type.toString(), false); - - HashMap fields = new HashMap<>(); - fields.put("server", plugin.getBungeeSettings().getServer()); - sendSubChannel("TimeChangeFinished", fields); - } - } - return isProcessing; - } - - /** - * Checks and extracts the boolean value from a DataValue. - * - * @param data the data value - * @return the boolean value - */ - public boolean checkGlobalDataTimeValue(DataValue data) { - if (data.isBoolean()) { - return data.getBoolean(); - } - return Boolean.valueOf(data.getString()); - } - - /** - * Closes and cleans up all handlers and connections. - */ - public void close() { - if (backendMysqlMessenger != null) { - backendMysqlMessenger.shutdown(); - } - - if (socketHandler != null) { - socketHandler.closeConnection(); - } - if (clientHandler != null) { - clientHandler.stopConnection(); - } - plugin.getServerData().setBungeeVotePartyCurrent(bungeeVotePartyCurrent); - plugin.getServerData().setBungeeVotePartyRequired(bungeeVotePartyRequired); - if (globalDataHandler != null) { - globalDataHandler.getGlobalMysql().close(); - } - } - - /** - * Loads and initializes the bungee handler with the configured method. - */ - public void load() { - plugin.debug("Loading bungee handler"); - - method = BungeeMethod.getByName(plugin.getBungeeSettings().getBungeeMethod()); - - plugin.getLogger().info("Using BungeeMethod: " + method.toString()); - - loadGlobalMysql(); - - globalMessageHandler = new GlobalMessageHandler() { - @Override - public void sendMessage(JsonEnvelope envelope) { - if (method.equals(BungeeMethod.MYSQL)) { - try { - backendMysqlMessenger.sendToProxy(envelope); - } catch (SQLException e) { - e.printStackTrace(); - } - } else if (method.equals(BungeeMethod.PLUGINMESSAGING)) { - plugin.getPluginMessaging().sendEnvelope(envelope); - } else if (method.equals(BungeeMethod.SOCKETS)) { - sendEnvelopeSocket(envelope); - } else if (method.equals(BungeeMethod.REDIS)) { - redisHandler.publishEnvelope(plugin.getBungeeSettings().getRedisPrefix() + "VotingPlugin", - envelope); - } else if (method.equals(BungeeMethod.MQTT)) { - try { - mqttHandler.publishEnvelope( - plugin.getBungeeSettings().getMqttPrefix() + "votingplugin/servers/proxy", envelope); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - }; - - // ========================== - // Vote / VoteOnline (wire decode) - // ========================== - - globalMessageHandler.addListener(new GlobalMessageListener(VotingPluginWire.SUB_VOTE) { - @Override - public void onReceive(JsonEnvelope msg) { - handleWireVote(msg); - } - }); - - globalMessageHandler.addListener(new GlobalMessageListener(VotingPluginWire.SUB_VOTE_ONLINE) { - @Override - public void onReceive(JsonEnvelope msg) { - handleWireVote(msg); - } - }); - - globalMessageHandler.addListener(new GlobalMessageListener(VotingPluginWire.SUB_VOTE_DELAY_REJECTED) { - @Override - public void onReceive(JsonEnvelope msg) { - handleWireVoteDelayRejected(msg); - } - }); - globalMessageHandler.addListener(new GlobalMessageListener(VotingPluginWire.SUB_VOTE_UPDATE) { - @Override - public void onReceive(JsonEnvelope msg) { - // Wire decode - VotingPluginWire.VoteUpdate v = VotingPluginWire.readVoteUpdate(msg); - - String playerUuid = v.uuid; - if (playerUuid == null || playerUuid.isEmpty()) { - return; - } - - plugin.debug("pluginmessaging voteupdate received for " + playerUuid + ": " + v.votePartyCurrent + "/" - + v.votePartyRequired + " on " + v.service); - - // Vote party cache update - if (v.votePartyCurrent >= 0 || bungeeVotePartyCurrent == -2) { - bungeeVotePartyCurrent = v.votePartyCurrent; - } - if (v.votePartyRequired >= 0 || bungeeVotePartyRequired == -2) { - bungeeVotePartyRequired = v.votePartyRequired; - } - plugin.getServerData().setBungeeVotePartyCurrent(bungeeVotePartyCurrent); - plugin.getServerData().setBungeeVotePartyRequired(bungeeVotePartyRequired); - - VotingPluginUser user = plugin.getVotingPluginUserManager() - .getVotingPluginUser(UUID.fromString(playerUuid)); - user.cache(); - - user.offVote(); - - // Optional: update last vote time for a service - String service = v.service; - long time = v.time; - - if (service != null && !service.isEmpty() && time > 0) { - user.setTime(plugin.getVoteSiteManager().getVoteSite(service, true), time); - } else if (service != null && !service.isEmpty() && time <= 0 - && plugin.getBungeeSettings().isBungeeDebug()) { - plugin.debug("Invalid last vote time received from bungee: " + time); - } - - plugin.setUpdate(true); - } - }); - - globalMessageHandler.addListener(new GlobalMessageListener(VotingPluginWire.SUB_BUNGEE_TIME_CHANGE) { - @Override - public void onReceive(JsonEnvelope msg) { - checkGlobalData(); - } - }); - - globalMessageHandler.addListener(new GlobalMessageListener(VotingPluginWire.SUB_VOTE_BROADCAST) { - @Override - public void onReceive(JsonEnvelope msg) { - Map f = msg.getFields(); - - final String uuidStr = nvl(f.get(VotingPluginWire.K_UUID)); - final String playerNameRaw = nvl(f.get(VotingPluginWire.K_PLAYER)); - final String service = nvl(f.get(VotingPluginWire.K_SERVICE)); - - if (uuidStr.isEmpty() || service.isEmpty()) { - return; - } - - UUID javaUuid; - try { - javaUuid = UUID.fromString(uuidStr); - } catch (Exception e) { - plugin.getLogger().warning("Invalid UUID in VoteBroadcast: " + uuidStr); - return; - } - - // New fields (May use later) - @SuppressWarnings("unused") - final long time = readLongSafe(f.get(VotingPluginWire.K_TIME), 0L); - final String totalsRaw = nvl(f.get(VotingPluginWire.K_TOTALS)); - final VoteTotalsSnapshot totals = totalsRaw.isEmpty() ? null - : VoteTotalsSnapshot.parseStorage(totalsRaw); - - VoteSite voteSite = plugin.getVoteSiteManager() - .getVoteSite(plugin.getVoteSiteManager().getVoteSiteName(true, service), true); - - if (voteSite == null) { - plugin.getLogger().warning("No voting site with the service site: '" + service + "'"); - return; - } - if (!voteSite.isEnabled()) { - plugin.debug("Votesite: " + voteSite.getKey() + " is not enabled (VoteBroadcast)"); - return; - } - - // Same user retrieval strategy: UUID + (possibly empty) name - VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(javaUuid, - playerNameRaw); - - // Keep cache/name current like normal vote path does - user.cache(); - user.updateName(true); - - // Same broadcast logic as PlayerVoteListener - if (plugin.getBroadcastHandler() == null) { - return; - } - - if (user.isVanished()) { - plugin.debug("Not broadcasting vote for vanished user: " + user.getPlayerName()); - return; - } - - // New proxies preserve the state sampled when the vote arrived. Fall back to - // the legacy delivery-time behavior for envelopes from older proxies. - final boolean online = f.containsKey(VotingPluginWire.K_WAS_ONLINE) - ? Boolean.parseBoolean(f.get(VotingPluginWire.K_WAS_ONLINE)) - : user.isOnline(); - plugin.getBroadcastHandler().broadcastVote(user.getJavaUUID(), user.getPlayerName(), - voteSite.getDisplayName(), online, totals); - } - }); - - globalMessageHandler.addListener(new GlobalMessageListener("Status") { - @Override - public void onReceive(JsonEnvelope msg) { - String server = nvl(msg.getFields().get("server")); - HashMap out = new HashMap<>(); - out.put("server", server); - sendSubChannel("statusokay", out); - } - }); - - globalMessageHandler.addListener(new GlobalMessageListener("ServerName") { - @Override - public void onReceive(JsonEnvelope msg) { - String server = nvl(msg.getFields().get("server")); - if (!plugin.getOptions().getServer().equals(server)) { - plugin.getLogger().warning("Server name doesn't match in BungeeSettings.yml, should be " + server); - } - } - }); - - globalMessageHandler.addListener(new GlobalMessageListener("VotePartyBungee") { - @Override - public void onReceive(JsonEnvelope msg) { - for (final String cmd : plugin.getBungeeSettings().getBungeeVotePartyGlobalCommands()) { - plugin.getBukkitScheduler().runTask(plugin, new Runnable() { - @Override - public void run() { - Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), cmd); - } - }); - } - for (Player p : Bukkit.getOnlinePlayers()) { - new RewardBuilder(plugin.getBungeeSettings().getData(), "BungeeVotePartyRewards").send(p); - } - } - }); - - globalMessageHandler.addListener(new GlobalMessageListener("VotePartyBroadcast") { - @Override - public void onReceive(JsonEnvelope msg) { - String broadcast = nvl(msg.getFields().get("broadcast")); - MiscUtils.getInstance().broadcast(broadcast); - } - }); - - if (method.equals(BungeeMethod.MYSQL)) { - plugin.registerBungeeChannels(plugin.getBungeeSettings().getPluginMessagingChannel()); - - try { - backendMysqlMessenger = new MySqlMessenger("VotingPlugin", - plugin.getMysql().getMysql().getConnectionManager().getDataSource(), - MySqlMessenger.Mode.BACKEND, plugin.getOptions().getServer(), msg -> { - if (plugin.getBungeeSettings().isBungeeDebug()) { - plugin.debug("Proxy sent envelope: " + msg.envelope.getSubChannel() + " " - + msg.envelope.getFields()); - } - globalMessageHandler.onMessage(msg.envelope); - }); - } catch (SQLException e) { - e.printStackTrace(); - } - } else if (method.equals(BungeeMethod.REDIS)) { - redisHandler = new RedisHandler(plugin.getBungeeSettings().getRedisHost(), - plugin.getBungeeSettings().getRedisPort(), plugin.getBungeeSettings().getRedisUsername(), - plugin.getBungeeSettings().getRedisPassword(), plugin.getBungeeSettings().getRedisdbindex()) { - - @Override - public void debug(String message) { - if (plugin.getBungeeSettings().isBungeeDebug()) { - plugin.debug(message); - } - } - }; - - redisThread = new Thread(new Runnable() { - @Override - public void run() { - if (plugin.isEnabled()) { - RedisListener listener = redisHandler.createEnvelopeListener( - plugin.getBungeeSettings().getRedisPrefix() + "VotingPlugin_" - + plugin.getBungeeSettings().getServer(), - (ch, env) -> globalMessageHandler.onMessage(env)); - redisHandler.loadListener(listener); - } - } - }); - redisThread.start(); - - } else if (method.equals(BungeeMethod.PLUGINMESSAGING)) { - plugin.registerBungeeChannels(plugin.getBungeeSettings().getPluginMessagingChannel()); - - if (plugin.getBungeeSettings().isPluginMessageEncryption()) { - encryptionHandler = new EncryptionHandler(plugin.getName(), - new File(plugin.getDataFolder(), "secretkey.key")); - plugin.getPluginMessaging().setEncryptionHandler(encryptionHandler); - } - - plugin.getPluginMessaging().setDebug(plugin.getBungeeSettings().isBungeeDebug()); - - plugin.getPluginMessaging().add(new PluginMessageHandler() { - @Override - public void onReceive(JsonEnvelope envelope) { - globalMessageHandler.onMessage(envelope); - } - }); - - } else if (method.equals(BungeeMethod.SOCKETS)) { - encryptionHandler = new EncryptionHandler(plugin.getName(), - new File(plugin.getDataFolder(), "secretkey.key")); - - clientHandler = new ClientHandler(plugin.getBungeeSettings().getBungeeServerHost(), - plugin.getBungeeSettings().getBungeeServerPort(), encryptionHandler, - plugin.getBungeeSettings().isBungeeDebug()); - - socketHandler = new SocketHandler("vp-socket", plugin.getBungeeSettings().getSpigotServerHost(), - plugin.getBungeeSettings().getSpigotServerPort(), encryptionHandler, - plugin.getBungeeSettings().isBungeeDebug()) { - - @Override - public void log(String str) { - plugin.getLogger().info(str); - } - }; - - socketHandler.add(new SocketReceiver() { - @Override - public void onReceiveEnvelope(JsonEnvelope envelope) { - globalMessageHandler.onMessage(envelope); - } - }); - - } else if (method.equals(BungeeMethod.MQTT)) { - try { - String id = plugin.getBungeeSettings().getMqttClientID(); - if (id.isEmpty()) { - id = plugin.getOptions().getServer(); - } - mqttHandler = new MqttHandler(new MqttServerComm(id, plugin.getBungeeSettings().getMqttBrokerURL(), - plugin.getBungeeSettings().getMqttUsername(), plugin.getBungeeSettings().getMqttPassword()), 2); - - mqttHandler.subscribeEnvelopes( - plugin.getBungeeSettings().getMqttPrefix() + "votingplugin/servers/" - + plugin.getOptions().getServer(), - (topic, envelope) -> globalMessageHandler.onMessage(envelope)); - - } catch (MqttException e) { - e.printStackTrace(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - bungeeVotePartyCurrent = plugin.getServerData().getBungeeVotePartyCurrent(); - bungeeVotePartyRequired = plugin.getServerData().getBungeeVotePartyRequired(); - - if (plugin.getOptions().getServer().equalsIgnoreCase("pleaseset")) { - plugin.getLogger().warning("Server name for bungee voting is not set, please set it"); - } - } - - private static long readLongSafe(String v, long def) { - if (v == null) - return def; - try { - return Long.parseLong(v); - } catch (Exception ignored) { - return def; - } - } - - private void handleWireVoteDelayRejected(JsonEnvelope msg) { - if (msg.getSchema() != VotingPluginWire.SCHEMA_VERSION) { - plugin.getLogger().warning("Incompatible version with bungee/proxy, please update all servers: " - + msg.getSchema() + " != " + VotingPluginWire.SCHEMA_VERSION); - return; - } - - if (!plugin.getOptions().isProcessRewards()) { - return; - } - - VotingPluginWire.VoteDelayRejected rejected = VotingPluginWire.readVoteDelayRejected(msg); - if (rejected.uuid.isEmpty() || rejected.service.isEmpty()) { - return; - } - - UUID javaUuid; - try { - javaUuid = UUID.fromString(rejected.uuid); - } catch (IllegalArgumentException e) { - plugin.getLogger().warning("Invalid UUID in VoteDelayRejected: " + rejected.uuid); - return; - } - - VoteSite voteSite = plugin.getVoteSiteManager() - .getVoteSite(plugin.getVoteSiteManager().getVoteSiteName(true, rejected.service), true); - if (voteSite == null) { - plugin.getLogger().warning("No voting site with the service site: '" + rejected.service + "'"); - return; - } - - VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(javaUuid, rejected.player); - user.cache(); - user.updateName(true); - voteSite.giveWaitUntilVoteDelayRewards(user, rejected.wasOnline && user.isOnline(), true); - } - - /** - * Wire vote handler (Vote + VoteOnline). - */ - private void handleWireVote(JsonEnvelope msg) { - // Strict schema check (wire uses envelope schema, not a "bungeeVersion" field) - int schema = msg.getSchema(); - if (schema != VotingPluginWire.SCHEMA_VERSION) { - plugin.getLogger().warning("Incompatible version with bungee/proxy, please update all servers: " + schema - + " != " + VotingPluginWire.SCHEMA_VERSION); - return; - } - - VotingPluginWire.Vote v = VotingPluginWire.readVote(msg); - - String uuidStr = v.uuid; - String player = v.player; - String service = v.service; - - if (uuidStr == null || uuidStr.isEmpty()) { - return; - } - if (!ServiceSiteValidator.isValid(service)) { - plugin.getLogger().warning("Rejected proxy vote with invalid service site '" - + ServiceSiteValidator.sanitizeForLog(service) + "'"); - return; - } - - plugin.debug("wire vote received from " + player + "/" + uuidStr + " on " + service); - - VoteTotalsSnapshot text = VoteTotalsSnapshot.parseStorage(v.totals == null ? "" : v.totals); - @SuppressWarnings("deprecation") - UUID voteId = v.voteId != null ? v.voteId : text.getVoteUUID(); - - if (!reserveWireVote(voteId)) { - plugin.debug("Ignoring duplicate wire vote " + voteId + " for " + player + " on " + service); - return; - } - - VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(UUID.fromString(uuidStr), - player); - - bungeeVotePartyCurrent = text.getVotePartyCurrent(); - bungeeVotePartyRequired = text.getVotePartyRequired(); - plugin.getServerData().setBungeeVotePartyCurrent(bungeeVotePartyCurrent); - plugin.getServerData().setBungeeVotePartyRequired(bungeeVotePartyRequired); - - user.cache(); - - boolean setTotalsOnBackend = !v.manageTotals; - - user.bungeeVotePluginMessaging(service, v.time, text, setTotalsOnBackend, v.wasOnline, v.broadcast, v.num); - - if (plugin.getBungeeSettings().isPerServerPoints()) { - user.addPoints(plugin.getConfigFile().getPointsOnVote()); - } - - if (service != null && !service.isEmpty()) { - plugin.getServerData().addServiceSite(service); - } - - @SuppressWarnings("unused") - int _ignored = v.numberOfVotes; - } - - /** - * Reserves a wire vote for processing. - * - * @param voteId unique vote identifier - * @return true if the vote has not been processed recently - */ - private boolean reserveWireVote(UUID voteId) { - if (voteId == null) { - return true; - } - - long now = System.currentTimeMillis(); - long expiresAt = now + PROCESSED_VOTE_TTL_MILLIS; - - while (true) { - Long currentExpiry = processedWireVotes.get(voteId); - if (currentExpiry == null) { - if (processedWireVotes.putIfAbsent(voteId, expiresAt) == null) { - cleanupProcessedWireVotes(now); - return true; - } - continue; - } - - if (currentExpiry > now) { - return false; - } - - if (processedWireVotes.replace(voteId, currentExpiry, expiresAt)) { - cleanupProcessedWireVotes(now); - return true; - } - } - } - - /** - * Removes expired wire vote identifiers. - * - * @param now current timestamp - */ - private void cleanupProcessedWireVotes(long now) { - processedWireVotes.entrySet().removeIf(entry -> entry.getValue() <= now); - } - - /** - * Loads the global MySQL handler for cross-server data synchronization. - */ - public void loadGlobalMysql() { - if (plugin.getBungeeSettings().isGloblalDataEnabled()) { - if (timer != null) { - timer.shutdown(); - try { - timer.awaitTermination(5, TimeUnit.SECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); - } - timer.shutdownNow(); - } - timer = Executors.newScheduledThreadPool(1); - timer.scheduleWithFixedDelay(new Runnable() { - @Override - public void run() { - checkGlobalData(); - } - }, 60, 10, TimeUnit.SECONDS); - timer.scheduleWithFixedDelay(new Runnable() { - @Override - public void run() { - globalDataHandler.setString(plugin.getBungeeSettings().getServer(), "LastOnline", - "" + LocalDateTime.now().atZone(ZoneOffset.UTC).toInstant().toEpochMilli()); - } - }, 1, 60, TimeUnit.MINUTES); - - if (globalDataHandler != null) { - globalDataHandler.getGlobalMysql().close(); - } - - if (plugin.getBungeeSettings().isGloblalDataUseMainMySQL() - && plugin.getStorageType().equals(UserStorage.MYSQL)) { - globalDataHandler = new GlobalDataHandler( - new GlobalMySQL("VotingPlugin_GlobalData", plugin.getMysql().getMysql()) { - @Override - public void debugEx(Exception e) { - plugin.debug(e); - } - - @Override - public void debugLog(String text) { - plugin.debug(text); - } - - @Override - public void info(String text) { - plugin.getLogger().info(text); - } - - @Override - public void logSevere(String text) { - plugin.getLogger().severe(text); - } - - @Override - public void warning(String text) { - plugin.getLogger().warning(text); - } - }); - } else { - globalDataHandler = new GlobalDataHandler( - new GlobalMySQL("VotingPlugin_GlobalData", new MysqlConfigSpigot( - plugin.getBungeeSettings().getData().getConfigurationSection("GlobalData"))) { - @Override - public void debugEx(Exception e) { - plugin.debug(e); - } - - @Override - public void debugLog(String text) { - plugin.debug(text); - } - - @Override - public void info(String text) { - plugin.getLogger().info(text); - } - - @Override - public void logSevere(String text) { - plugin.getLogger().severe(text); - } - - @Override - public void warning(String text) { - plugin.getLogger().warning(text); - } - }); - } - - globalDataHandler.getGlobalMysql().alterColumnType("IgnoreTime", "VARCHAR(5)"); - globalDataHandler.getGlobalMysql().alterColumnType("MONTH", "VARCHAR(5)"); - globalDataHandler.getGlobalMysql().alterColumnType("WEEK", "VARCHAR(5)"); - globalDataHandler.getGlobalMysql().alterColumnType("DAY", "VARCHAR(5)"); - globalDataHandler.getGlobalMysql().alterColumnType("FinishedProcessing", "VARCHAR(5)"); - globalDataHandler.getGlobalMysql().alterColumnType("Processing", "VARCHAR(5)"); - globalDataHandler.getGlobalMysql().alterColumnType("LastUpdated", "MEDIUMTEXT"); - globalDataHandler.getGlobalMysql().alterColumnType("ForceUpdate", "VARCHAR(5)"); - plugin.getTimeChecker().setProcessingEnabled(false); - } - } - - private void sendEnvelopeSocket(JsonEnvelope envelope) { - if (clientHandler != null) { - clientHandler.sendEnvelope(envelope); - } - } - - private void sendSubChannel(String subChannel, HashMap fields) { - JsonEnvelope.Builder b = JsonEnvelope.builder(subChannel).schema(VotingPluginWire.SCHEMA_VERSION); - if (fields != null) { - for (Map.Entry e : fields.entrySet()) { - b.put(e.getKey(), e.getValue()); - } - } - globalMessageHandler.sendMessage(b.build()); - } - - private static String nvl(String s) { - return s == null ? "" : s; - } -} +// File: com/bencodez/votingplugin/BungeeHandler.java +package com.bencodez.votingplugin; + +import java.io.File; +import java.sql.SQLException; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.Listener; +import org.eclipse.paho.client.mqttv3.MqttException; + +import com.bencodez.advancedcore.api.misc.MiscUtils; +import com.bencodez.advancedcore.api.rewards.RewardBuilder; +import com.bencodez.advancedcore.api.time.TimeType; +import com.bencodez.advancedcore.api.user.UserStorage; +import com.bencodez.advancedcore.bungeeapi.globaldata.GlobalDataHandler; +import com.bencodez.advancedcore.bungeeapi.globaldata.GlobalMySQL; +import com.bencodez.simpleapi.encryption.EncryptionHandler; +import com.bencodez.simpleapi.servercomm.codec.JsonEnvelope; +import com.bencodez.simpleapi.servercomm.global.GlobalMessageHandler; +import com.bencodez.simpleapi.servercomm.global.GlobalMessageListener; +import com.bencodez.simpleapi.servercomm.mqtt.MqttHandler; +import com.bencodez.simpleapi.servercomm.mqtt.MqttServerComm; +import com.bencodez.simpleapi.servercomm.mysql.MySqlMessenger; +import com.bencodez.simpleapi.servercomm.pluginmessage.PluginMessageHandler; +import com.bencodez.simpleapi.servercomm.redis.RedisHandler; +import com.bencodez.simpleapi.servercomm.redis.RedisListener; +import com.bencodez.simpleapi.servercomm.sockets.ClientHandler; +import com.bencodez.simpleapi.servercomm.sockets.SocketHandler; +import com.bencodez.simpleapi.servercomm.sockets.SocketReceiver; +import com.bencodez.simpleapi.sql.data.DataValue; +import com.bencodez.simpleapi.sql.data.DataValueBoolean; +import com.bencodez.simpleapi.sql.mysql.config.MysqlConfigSpigot; +import com.bencodez.votingplugin.proxy.BungeeMethod; +import com.bencodez.votingplugin.proxy.VoteTotalsSnapshot; +import com.bencodez.votingplugin.proxy.VotingPluginWire; +import com.bencodez.votingplugin.user.VotingPluginUser; +import com.bencodez.votingplugin.util.ServiceSiteValidator; +import com.bencodez.votingplugin.votesites.VoteSite; + +import lombok.Getter; + +/** + * Handler for Bungee/proxy server integration. + */ +public class BungeeHandler implements Listener { + + // Keep rejection identifiers longer than the documented 24-hour vote-delay window. + // The wire schema is versioned separately so older peers cannot silently + // interpret the replay-protected envelope with legacy semantics. + private static final long PROCESSED_VOTE_TTL_MILLIS = TimeUnit.HOURS.toMillis(25); + + @Getter + private final ConcurrentHashMap processedWireVotes = new ConcurrentHashMap<>(); + @Getter + private ClientHandler clientHandler; + + private EncryptionHandler encryptionHandler; + + @Getter + private BungeeMethod method; + + private VotingPluginMain plugin; + + @Getter + private int bungeeVotePartyCurrent = -2; + + @Getter + private int bungeeVotePartyRequired = -2; + + @Getter + private SocketHandler socketHandler; + + private GlobalDataHandler globalDataHandler; + + @Getter + private ScheduledExecutorService timer; + + @Getter + private RedisHandler redisHandler; + + @Getter + private GlobalMessageHandler globalMessageHandler; + + private Thread redisThread; + + @Getter + private MySqlMessenger backendMysqlMessenger; + + @Getter + private MqttHandler mqttHandler; + + /** + * Constructs a new BungeeHandler. + * + * @param plugin the main plugin instance + */ + public BungeeHandler(VotingPluginMain plugin) { + this.plugin = plugin; + } + + /** + * Checks and processes global data from the global data handler. + */ + public void checkGlobalData() { + HashMap data = globalDataHandler.getExact(plugin.getBungeeSettings().getServer()); + + if (data.containsKey("ForceUpdate")) { + boolean b = checkGlobalDataTimeValue(data.get("ForceUpdate")); + if (b) { + if (plugin.getStorageType().equals(UserStorage.MYSQL)) { + plugin.getMysql().clearCacheBasic(); + } + plugin.getUserManager().getDataManager().clearCache(); + plugin.setUpdate(true); + plugin.update(); + globalDataHandler.setBoolean(plugin.getBungeeSettings().getServer(), "ForceUpdate", false); + } + } + + boolean forceUpdate = false; + + if (checkGlobalDataTime(TimeType.MONTH, data)) { + forceUpdate = true; + } + if (checkGlobalDataTime(TimeType.WEEK, data)) { + forceUpdate = true; + } + if (checkGlobalDataTime(TimeType.DAY, data)) { + forceUpdate = true; + } + + if (forceUpdate) { + HashMap dataToSet = new HashMap<>(); + dataToSet.put("FinishedProcessing", new DataValueBoolean(true)); + dataToSet.put("Processing", new DataValueBoolean(false)); + globalDataHandler.setData(plugin.getBungeeSettings().getServer(), dataToSet); + } + } + + /** + * Checks global data for a time type change. + * + * @param type the time type to check + * @param data the global data map + * @return true if currently processing a time change + */ + public boolean checkGlobalDataTime(TimeType type, HashMap data) { + boolean isProcessing = false; + if (data.containsKey(type.toString())) { + + DataValue value = data.get(type.toString()); + boolean b = checkGlobalDataTimeValue(value); + if (b) { + long lastUpdated = Long.valueOf(data.get("LastUpdated").getString()).longValue(); + plugin.debug("LastUpdated: " + lastUpdated); + if (LocalDateTime.now().atZone(ZoneOffset.UTC).toInstant().toEpochMilli() - lastUpdated > 1000 * 60 * 60 + * 2) { + plugin.getLogger().warning("Ignoring bungee time change since it was more than 2 hours ago"); + globalDataHandler.setBoolean(plugin.getBungeeSettings().getServer(), type.toString(), false); + return false; + } + + globalDataHandler.setBoolean(plugin.getBungeeSettings().getServer(), "Processing", true); + isProcessing = true; + + plugin.debug("Detected time change from bungee: " + type.toString()); + plugin.getTimeChecker().forceChanged(type, false, true, true); + globalDataHandler.setBoolean(plugin.getBungeeSettings().getServer(), type.toString(), false); + + HashMap fields = new HashMap<>(); + fields.put("server", plugin.getBungeeSettings().getServer()); + sendSubChannel("TimeChangeFinished", fields); + } + } + return isProcessing; + } + + /** + * Checks and extracts the boolean value from a DataValue. + * + * @param data the data value + * @return the boolean value + */ + public boolean checkGlobalDataTimeValue(DataValue data) { + if (data.isBoolean()) { + return data.getBoolean(); + } + return Boolean.valueOf(data.getString()); + } + + /** + * Closes and cleans up all handlers and connections. + */ + public void close() { + if (backendMysqlMessenger != null) { + backendMysqlMessenger.shutdown(); + } + + if (socketHandler != null) { + socketHandler.closeConnection(); + } + if (clientHandler != null) { + clientHandler.stopConnection(); + } + plugin.getServerData().setBungeeVotePartyCurrent(bungeeVotePartyCurrent); + plugin.getServerData().setBungeeVotePartyRequired(bungeeVotePartyRequired); + if (globalDataHandler != null) { + globalDataHandler.getGlobalMysql().close(); + } + } + + /** + * Loads and initializes the bungee handler with the configured method. + */ + public void load() { + plugin.debug("Loading bungee handler"); + + method = BungeeMethod.getByName(plugin.getBungeeSettings().getBungeeMethod()); + + plugin.getLogger().info("Using BungeeMethod: " + method.toString()); + + loadGlobalMysql(); + + globalMessageHandler = new GlobalMessageHandler() { + @Override + public void sendMessage(JsonEnvelope envelope) { + if (method.equals(BungeeMethod.MYSQL)) { + try { + backendMysqlMessenger.sendToProxy(envelope); + } catch (SQLException e) { + e.printStackTrace(); + } + } else if (method.equals(BungeeMethod.PLUGINMESSAGING)) { + plugin.getPluginMessaging().sendEnvelope(envelope); + } else if (method.equals(BungeeMethod.SOCKETS)) { + sendEnvelopeSocket(envelope); + } else if (method.equals(BungeeMethod.REDIS)) { + redisHandler.publishEnvelope(plugin.getBungeeSettings().getRedisPrefix() + "VotingPlugin", + envelope); + } else if (method.equals(BungeeMethod.MQTT)) { + try { + mqttHandler.publishEnvelope( + plugin.getBungeeSettings().getMqttPrefix() + "votingplugin/servers/proxy", envelope); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + }; + + // ========================== + // Vote / VoteOnline (wire decode) + // ========================== + + globalMessageHandler.addListener(new GlobalMessageListener(VotingPluginWire.SUB_VOTE) { + @Override + public void onReceive(JsonEnvelope msg) { + handleWireVote(msg); + } + }); + + globalMessageHandler.addListener(new GlobalMessageListener(VotingPluginWire.SUB_VOTE_ONLINE) { + @Override + public void onReceive(JsonEnvelope msg) { + handleWireVote(msg); + } + }); + + globalMessageHandler.addListener(new GlobalMessageListener(VotingPluginWire.SUB_VOTE_DELAY_REJECTED) { + @Override + public void onReceive(JsonEnvelope msg) { + handleWireVoteDelayRejected(msg); + } + }); + globalMessageHandler.addListener(new GlobalMessageListener(VotingPluginWire.SUB_VOTE_UPDATE) { + @Override + public void onReceive(JsonEnvelope msg) { + // Wire decode + VotingPluginWire.VoteUpdate v = VotingPluginWire.readVoteUpdate(msg); + + String playerUuid = v.uuid; + if (playerUuid == null || playerUuid.isEmpty()) { + return; + } + + plugin.debug("pluginmessaging voteupdate received for " + playerUuid + ": " + v.votePartyCurrent + "/" + + v.votePartyRequired + " on " + v.service); + + // Vote party cache update + if (v.votePartyCurrent >= 0 || bungeeVotePartyCurrent == -2) { + bungeeVotePartyCurrent = v.votePartyCurrent; + } + if (v.votePartyRequired >= 0 || bungeeVotePartyRequired == -2) { + bungeeVotePartyRequired = v.votePartyRequired; + } + plugin.getServerData().setBungeeVotePartyCurrent(bungeeVotePartyCurrent); + plugin.getServerData().setBungeeVotePartyRequired(bungeeVotePartyRequired); + + VotingPluginUser user = plugin.getVotingPluginUserManager() + .getVotingPluginUser(UUID.fromString(playerUuid)); + user.cache(); + + user.offVote(); + + // Optional: update last vote time for a service + String service = v.service; + long time = v.time; + + if (service != null && !service.isEmpty() && time > 0) { + user.setTime(plugin.getVoteSiteManager().getVoteSite(service, true), time); + } else if (service != null && !service.isEmpty() && time <= 0 + && plugin.getBungeeSettings().isBungeeDebug()) { + plugin.debug("Invalid last vote time received from bungee: " + time); + } + + plugin.setUpdate(true); + } + }); + + globalMessageHandler.addListener(new GlobalMessageListener(VotingPluginWire.SUB_BUNGEE_TIME_CHANGE) { + @Override + public void onReceive(JsonEnvelope msg) { + checkGlobalData(); + } + }); + + globalMessageHandler.addListener(new GlobalMessageListener(VotingPluginWire.SUB_VOTE_BROADCAST) { + @Override + public void onReceive(JsonEnvelope msg) { + Map f = msg.getFields(); + + final String uuidStr = nvl(f.get(VotingPluginWire.K_UUID)); + final String playerNameRaw = nvl(f.get(VotingPluginWire.K_PLAYER)); + final String service = nvl(f.get(VotingPluginWire.K_SERVICE)); + + if (uuidStr.isEmpty() || service.isEmpty()) { + return; + } + + UUID javaUuid; + try { + javaUuid = UUID.fromString(uuidStr); + } catch (Exception e) { + plugin.getLogger().warning("Invalid UUID in VoteBroadcast: " + uuidStr); + return; + } + + // New fields (May use later) + @SuppressWarnings("unused") + final long time = readLongSafe(f.get(VotingPluginWire.K_TIME), 0L); + final String totalsRaw = nvl(f.get(VotingPluginWire.K_TOTALS)); + final VoteTotalsSnapshot totals = totalsRaw.isEmpty() ? null + : VoteTotalsSnapshot.parseStorage(totalsRaw); + + VoteSite voteSite = plugin.getVoteSiteManager() + .getVoteSite(plugin.getVoteSiteManager().getVoteSiteName(true, service), true); + + if (voteSite == null) { + plugin.getLogger().warning("No voting site with the service site: '" + service + "'"); + return; + } + if (!voteSite.isEnabled()) { + plugin.debug("Votesite: " + voteSite.getKey() + " is not enabled (VoteBroadcast)"); + return; + } + + // Same user retrieval strategy: UUID + (possibly empty) name + VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(javaUuid, + playerNameRaw); + + // Keep cache/name current like normal vote path does + user.cache(); + user.updateName(true); + + // Same broadcast logic as PlayerVoteListener + if (plugin.getBroadcastHandler() == null) { + return; + } + + if (user.isVanished()) { + plugin.debug("Not broadcasting vote for vanished user: " + user.getPlayerName()); + return; + } + + // New proxies preserve the state sampled when the vote arrived. Fall back to + // the legacy delivery-time behavior for envelopes from older proxies. + final boolean online = f.containsKey(VotingPluginWire.K_WAS_ONLINE) + ? Boolean.parseBoolean(f.get(VotingPluginWire.K_WAS_ONLINE)) + : user.isOnline(); + plugin.getBroadcastHandler().broadcastVote(user.getJavaUUID(), user.getPlayerName(), + voteSite.getDisplayName(), online, totals); + } + }); + + globalMessageHandler.addListener(new GlobalMessageListener("Status") { + @Override + public void onReceive(JsonEnvelope msg) { + String server = nvl(msg.getFields().get("server")); + HashMap out = new HashMap<>(); + out.put("server", server); + sendSubChannel("statusokay", out); + } + }); + + globalMessageHandler.addListener(new GlobalMessageListener("ServerName") { + @Override + public void onReceive(JsonEnvelope msg) { + String server = nvl(msg.getFields().get("server")); + if (!plugin.getOptions().getServer().equals(server)) { + plugin.getLogger().warning("Server name doesn't match in BungeeSettings.yml, should be " + server); + } + } + }); + + globalMessageHandler.addListener(new GlobalMessageListener("VotePartyBungee") { + @Override + public void onReceive(JsonEnvelope msg) { + for (final String cmd : plugin.getBungeeSettings().getBungeeVotePartyGlobalCommands()) { + plugin.getBukkitScheduler().runTask(plugin, new Runnable() { + @Override + public void run() { + Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), cmd); + } + }); + } + for (Player p : Bukkit.getOnlinePlayers()) { + new RewardBuilder(plugin.getBungeeSettings().getData(), "BungeeVotePartyRewards").send(p); + } + } + }); + + globalMessageHandler.addListener(new GlobalMessageListener("VotePartyBroadcast") { + @Override + public void onReceive(JsonEnvelope msg) { + String broadcast = nvl(msg.getFields().get("broadcast")); + MiscUtils.getInstance().broadcast(broadcast); + } + }); + + if (method.equals(BungeeMethod.MYSQL)) { + plugin.registerBungeeChannels(plugin.getBungeeSettings().getPluginMessagingChannel()); + + try { + backendMysqlMessenger = new MySqlMessenger("VotingPlugin", + plugin.getMysql().getMysql().getConnectionManager().getDataSource(), + MySqlMessenger.Mode.BACKEND, plugin.getOptions().getServer(), msg -> { + if (plugin.getBungeeSettings().isBungeeDebug()) { + plugin.debug("Proxy sent envelope: " + msg.envelope.getSubChannel() + " " + + msg.envelope.getFields()); + } + globalMessageHandler.onMessage(msg.envelope); + }); + } catch (SQLException e) { + e.printStackTrace(); + } + } else if (method.equals(BungeeMethod.REDIS)) { + redisHandler = new RedisHandler(plugin.getBungeeSettings().getRedisHost(), + plugin.getBungeeSettings().getRedisPort(), plugin.getBungeeSettings().getRedisUsername(), + plugin.getBungeeSettings().getRedisPassword(), plugin.getBungeeSettings().getRedisdbindex()) { + + @Override + public void debug(String message) { + if (plugin.getBungeeSettings().isBungeeDebug()) { + plugin.debug(message); + } + } + }; + + redisThread = new Thread(new Runnable() { + @Override + public void run() { + if (plugin.isEnabled()) { + RedisListener listener = redisHandler.createEnvelopeListener( + plugin.getBungeeSettings().getRedisPrefix() + "VotingPlugin_" + + plugin.getBungeeSettings().getServer(), + (ch, env) -> globalMessageHandler.onMessage(env)); + redisHandler.loadListener(listener); + } + } + }); + redisThread.start(); + + } else if (method.equals(BungeeMethod.PLUGINMESSAGING)) { + plugin.registerBungeeChannels(plugin.getBungeeSettings().getPluginMessagingChannel()); + + if (plugin.getBungeeSettings().isPluginMessageEncryption()) { + encryptionHandler = new EncryptionHandler(plugin.getName(), + new File(plugin.getDataFolder(), "secretkey.key")); + plugin.getPluginMessaging().setEncryptionHandler(encryptionHandler); + } + + plugin.getPluginMessaging().setDebug(plugin.getBungeeSettings().isBungeeDebug()); + + plugin.getPluginMessaging().add(new PluginMessageHandler() { + @Override + public void onReceive(JsonEnvelope envelope) { + globalMessageHandler.onMessage(envelope); + } + }); + + } else if (method.equals(BungeeMethod.SOCKETS)) { + encryptionHandler = new EncryptionHandler(plugin.getName(), + new File(plugin.getDataFolder(), "secretkey.key")); + + clientHandler = new ClientHandler(plugin.getBungeeSettings().getBungeeServerHost(), + plugin.getBungeeSettings().getBungeeServerPort(), encryptionHandler, + plugin.getBungeeSettings().isBungeeDebug()); + + socketHandler = new SocketHandler("vp-socket", plugin.getBungeeSettings().getSpigotServerHost(), + plugin.getBungeeSettings().getSpigotServerPort(), encryptionHandler, + plugin.getBungeeSettings().isBungeeDebug()) { + + @Override + public void log(String str) { + plugin.getLogger().info(str); + } + }; + + socketHandler.add(new SocketReceiver() { + @Override + public void onReceiveEnvelope(JsonEnvelope envelope) { + globalMessageHandler.onMessage(envelope); + } + }); + + } else if (method.equals(BungeeMethod.MQTT)) { + try { + String id = plugin.getBungeeSettings().getMqttClientID(); + if (id.isEmpty()) { + id = plugin.getOptions().getServer(); + } + mqttHandler = new MqttHandler(new MqttServerComm(id, plugin.getBungeeSettings().getMqttBrokerURL(), + plugin.getBungeeSettings().getMqttUsername(), plugin.getBungeeSettings().getMqttPassword()), 2); + + mqttHandler.subscribeEnvelopes( + plugin.getBungeeSettings().getMqttPrefix() + "votingplugin/servers/" + + plugin.getOptions().getServer(), + (topic, envelope) -> globalMessageHandler.onMessage(envelope)); + + } catch (MqttException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + bungeeVotePartyCurrent = plugin.getServerData().getBungeeVotePartyCurrent(); + bungeeVotePartyRequired = plugin.getServerData().getBungeeVotePartyRequired(); + + if (plugin.getOptions().getServer().equalsIgnoreCase("pleaseset")) { + plugin.getLogger().warning("Server name for bungee voting is not set, please set it"); + } + } + + private static long readLongSafe(String v, long def) { + if (v == null) + return def; + try { + return Long.parseLong(v); + } catch (Exception ignored) { + return def; + } + } + + private void handleWireVoteDelayRejected(JsonEnvelope msg) { + if (msg.getSchema() != VotingPluginWire.SCHEMA_VERSION) { + plugin.getLogger().warning("Incompatible version with bungee/proxy, please update all servers: " + + msg.getSchema() + " != " + VotingPluginWire.SCHEMA_VERSION); + return; + } + + if (!plugin.getOptions().isProcessRewards()) { + return; + } + + VotingPluginWire.VoteDelayRejected rejected = VotingPluginWire.readVoteDelayRejected(msg); + if (rejected.uuid.isEmpty() || rejected.service.isEmpty()) { + return; + } + + UUID javaUuid; + try { + javaUuid = UUID.fromString(rejected.uuid); + } catch (IllegalArgumentException e) { + plugin.getLogger().warning("Invalid UUID in VoteDelayRejected: " + rejected.uuid); + return; + } + + VoteSite voteSite = plugin.getVoteSiteManager() + .getVoteSite(plugin.getVoteSiteManager().getVoteSiteName(true, rejected.service), true); + if (voteSite == null) { + plugin.getLogger().warning("No voting site with the service site: '" + rejected.service + "'"); + return; + } + + VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(javaUuid, rejected.player); + user.cache(); + user.updateName(true); + voteSite.giveWaitUntilVoteDelayRewards(user, rejected.wasOnline && user.isOnline(), true); + } + + /** + * Wire vote handler (Vote + VoteOnline). + */ + private void handleWireVote(JsonEnvelope msg) { + // Strict schema check (wire uses envelope schema, not a "bungeeVersion" field) + int schema = msg.getSchema(); + if (schema != VotingPluginWire.SCHEMA_VERSION) { + plugin.getLogger().warning("Incompatible version with bungee/proxy, please update all servers: " + schema + + " != " + VotingPluginWire.SCHEMA_VERSION); + return; + } + + VotingPluginWire.Vote v = VotingPluginWire.readVote(msg); + + String uuidStr = v.uuid; + String player = v.player; + String service = v.service; + + if (uuidStr == null || uuidStr.isEmpty()) { + return; + } + if (!ServiceSiteValidator.isValid(service)) { + plugin.getLogger().warning("Rejected proxy vote with invalid service site '" + + ServiceSiteValidator.sanitizeForLog(service) + "'"); + return; + } + + plugin.debug("wire vote received from " + player + "/" + uuidStr + " on " + service); + + VoteTotalsSnapshot text = VoteTotalsSnapshot.parseStorage(v.totals == null ? "" : v.totals); + @SuppressWarnings("deprecation") + UUID voteId = v.voteId != null ? v.voteId : text.getVoteUUID(); + + if (!reserveWireVote(voteId)) { + plugin.debug("Ignoring duplicate wire vote " + voteId + " for " + player + " on " + service); + return; + } + + VotingPluginUser user = plugin.getVotingPluginUserManager().getVotingPluginUser(UUID.fromString(uuidStr), + player); + + bungeeVotePartyCurrent = text.getVotePartyCurrent(); + bungeeVotePartyRequired = text.getVotePartyRequired(); + plugin.getServerData().setBungeeVotePartyCurrent(bungeeVotePartyCurrent); + plugin.getServerData().setBungeeVotePartyRequired(bungeeVotePartyRequired); + + user.cache(); + + boolean setTotalsOnBackend = !v.manageTotals; + + user.bungeeVotePluginMessaging(service, v.time, text, setTotalsOnBackend, v.wasOnline, v.broadcast, v.num); + + if (plugin.getBungeeSettings().isPerServerPoints()) { + user.addPoints(plugin.getConfigFile().getPointsOnVote()); + } + + if (service != null && !service.isEmpty()) { + plugin.getServerData().addServiceSite(service); + } + + @SuppressWarnings("unused") + int _ignored = v.numberOfVotes; + } + + /** + * Reserves a wire vote for processing. + * + * @param voteId unique vote identifier + * @return true if the vote has not been processed recently + */ + private boolean reserveWireVote(UUID voteId) { + if (voteId == null) { + return true; + } + + long now = System.currentTimeMillis(); + long expiresAt = now + PROCESSED_VOTE_TTL_MILLIS; + + while (true) { + Long currentExpiry = processedWireVotes.get(voteId); + if (currentExpiry == null) { + if (processedWireVotes.putIfAbsent(voteId, expiresAt) == null) { + cleanupProcessedWireVotes(now); + return true; + } + continue; + } + + if (currentExpiry > now) { + return false; + } + + if (processedWireVotes.replace(voteId, currentExpiry, expiresAt)) { + cleanupProcessedWireVotes(now); + return true; + } + } + } + + /** + * Removes expired wire vote identifiers. + * + * @param now current timestamp + */ + private void cleanupProcessedWireVotes(long now) { + processedWireVotes.entrySet().removeIf(entry -> entry.getValue() <= now); + } + + /** + * Loads the global MySQL handler for cross-server data synchronization. + */ + public void loadGlobalMysql() { + if (plugin.getBungeeSettings().isGloblalDataEnabled()) { + if (timer != null) { + timer.shutdown(); + try { + timer.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + timer.shutdownNow(); + } + timer = Executors.newScheduledThreadPool(1); + timer.scheduleWithFixedDelay(new Runnable() { + @Override + public void run() { + checkGlobalData(); + } + }, 60, 10, TimeUnit.SECONDS); + timer.scheduleWithFixedDelay(new Runnable() { + @Override + public void run() { + globalDataHandler.setString(plugin.getBungeeSettings().getServer(), "LastOnline", + "" + LocalDateTime.now().atZone(ZoneOffset.UTC).toInstant().toEpochMilli()); + } + }, 1, 60, TimeUnit.MINUTES); + + if (globalDataHandler != null) { + globalDataHandler.getGlobalMysql().close(); + } + + if (plugin.getBungeeSettings().isGloblalDataUseMainMySQL() + && plugin.getStorageType().equals(UserStorage.MYSQL)) { + globalDataHandler = new GlobalDataHandler( + new GlobalMySQL("VotingPlugin_GlobalData", plugin.getMysql().getMysql()) { + @Override + public void debugEx(Exception e) { + plugin.debug(e); + } + + @Override + public void debugLog(String text) { + plugin.debug(text); + } + + @Override + public void info(String text) { + plugin.getLogger().info(text); + } + + @Override + public void logSevere(String text) { + plugin.getLogger().severe(text); + } + + @Override + public void warning(String text) { + plugin.getLogger().warning(text); + } + }); + } else { + globalDataHandler = new GlobalDataHandler( + new GlobalMySQL("VotingPlugin_GlobalData", new MysqlConfigSpigot( + plugin.getBungeeSettings().getData().getConfigurationSection("GlobalData"))) { + @Override + public void debugEx(Exception e) { + plugin.debug(e); + } + + @Override + public void debugLog(String text) { + plugin.debug(text); + } + + @Override + public void info(String text) { + plugin.getLogger().info(text); + } + + @Override + public void logSevere(String text) { + plugin.getLogger().severe(text); + } + + @Override + public void warning(String text) { + plugin.getLogger().warning(text); + } + }); + } + + globalDataHandler.getGlobalMysql().alterColumnType("IgnoreTime", "VARCHAR(5)"); + globalDataHandler.getGlobalMysql().alterColumnType("MONTH", "VARCHAR(5)"); + globalDataHandler.getGlobalMysql().alterColumnType("WEEK", "VARCHAR(5)"); + globalDataHandler.getGlobalMysql().alterColumnType("DAY", "VARCHAR(5)"); + globalDataHandler.getGlobalMysql().alterColumnType("FinishedProcessing", "VARCHAR(5)"); + globalDataHandler.getGlobalMysql().alterColumnType("Processing", "VARCHAR(5)"); + globalDataHandler.getGlobalMysql().alterColumnType("LastUpdated", "MEDIUMTEXT"); + globalDataHandler.getGlobalMysql().alterColumnType("ForceUpdate", "VARCHAR(5)"); + plugin.getTimeChecker().setProcessingEnabled(false); + } + } + + private void sendEnvelopeSocket(JsonEnvelope envelope) { + if (clientHandler != null) { + clientHandler.sendEnvelope(envelope); + } + } + + private void sendSubChannel(String subChannel, HashMap fields) { + JsonEnvelope.Builder b = JsonEnvelope.builder(subChannel).schema(VotingPluginWire.SCHEMA_VERSION); + if (fields != null) { + for (Map.Entry e : fields.entrySet()) { + b.put(e.getKey(), e.getValue()); + } + } + globalMessageHandler.sendMessage(b.build()); + } + + private static String nvl(String s) { + return s == null ? "" : s; + } +} From b7180343b67ac4cf7579060f1fe137dbc289326b Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 8 Aug 2026 18:15:41 -0600 Subject: [PATCH 05/21] Keep compatibility changes scoped to the vote-site fix --- .../java/com/bencodez/votingplugin/proxy/VotingPluginWire.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginWire.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginWire.java index 723150e92..c591e7d72 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginWire.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/proxy/VotingPluginWire.java @@ -23,7 +23,7 @@ public final class VotingPluginWire { private VotingPluginWire() { } - public static final int SCHEMA_VERSION = 2; + public static final int SCHEMA_VERSION = 1; // ========================= // Subchannels (canonical) From 8370a4e3ef02eaea35106195210471b88cbdeaec Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 8 Aug 2026 18:15:43 -0600 Subject: [PATCH 06/21] Keep compatibility changes scoped to the vote-site fix --- .../main/java/com/bencodez/votingplugin/BungeeHandler.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/BungeeHandler.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/BungeeHandler.java index 7ecbe5fd1..af3e3c3e8 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/BungeeHandler.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/BungeeHandler.java @@ -54,10 +54,7 @@ */ public class BungeeHandler implements Listener { - // Keep rejection identifiers longer than the documented 24-hour vote-delay window. - // The wire schema is versioned separately so older peers cannot silently - // interpret the replay-protected envelope with legacy semantics. - private static final long PROCESSED_VOTE_TTL_MILLIS = TimeUnit.HOURS.toMillis(25); + private static final long PROCESSED_VOTE_TTL_MILLIS = TimeUnit.MINUTES.toMillis(30); @Getter private final ConcurrentHashMap processedWireVotes = new ConcurrentHashMap<>(); From efc9184bbd7c014b9ac2c33879e4d91ab8f236c6 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 8 Aug 2026 18:24:40 -0600 Subject: [PATCH 07/21] Exclude unavailable Velocity Brigadier snapshot --- VotingPlugin/pom.xml | 1330 +++++++++++++++++++++--------------------- 1 file changed, 668 insertions(+), 662 deletions(-) diff --git a/VotingPlugin/pom.xml b/VotingPlugin/pom.xml index 8b4ac385a..9d4fb5eb6 100644 --- a/VotingPlugin/pom.xml +++ b/VotingPlugin/pom.xml @@ -1,663 +1,669 @@ - - 4.0.0 - com.bencodez - votingplugin - 7.1.2-SNAPSHOT - jar - VotingPlugin - - github - UTF-8 - NOTSET - 21 - 21 - 21 - 3.4.0 - - - - - src/main/resources - true - - plugin.yml - bungee.yml - votingpluginversion.yml - - - - src/main/resources - false - - plugin.yml - bungee.yml - votingpluginversion.yml - - - - src/main/java - src/test/java - - - - org.eclipse.m2e - lifecycle-mapping - 1.0.0 - - - - - - org.projectlombok - lombok-maven-plugin - [1,) - - delombok - - - - - - - - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.13.0 - - 21 - - - org.projectlombok - lombok - 1.18.42 - - - com.velocitypowered - velocity-api - ${velocity.version} - - - - - - org.apache.maven.plugins - maven-shade-plugin - 3.6.2 - - false - false - - - mysql-connector-java - - ** - - - - *:* - - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - com.tcoded.folialib - - ${project.groupId}.votingplugin.simpleapi.folialib - - - com.bencodez.simpleapi - - ${project.groupId}.votingplugin.simpleapi - - - com.bencodez.advancedcore - - ${project.groupId}.votingplugin.advancedcore - - - net.pl3x.bukkit.chatapi - ${project.groupId}.votingplugin - - - me.mrten.mysqlapi - - ${project.groupId}.votingplugin.mysqlapi - - - com.zaxxer.hikari - ${project.groupId}.votingplugin.simpleapi.hikari - - - org.bstats - - ${project.groupId}.votingplugin.bstats - - - xyz.upperlevel.spigot - - ${project.groupId}.votingplugin.advancedcore.xyz.upperlevel.spigot - - - org.spongepowered.configurate - ${project.groupId}.simpleapi.configurate - - - io.leangen.geantyref - ${project.groupId}.simpleapi.geantyref - - - - - - package - - shade - - - - - - com.google.*:* - - - false - false - - - *:* - - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - module-info.class - - - - - - - - - - - - - - - - spigot-repo - https://hub.spigotmc.org/nexus/content/repositories/snapshots/ - - - maven-central - https://oss.sonatype.org/content/groups/public - - - bungeecord-repo - https://oss.sonatype.org/content/repositories/snapshots - - - velocity - https://nexus.velocitypowered.com/repository/maven-public/ - - - bencodez repo - https://nexus.bencodez.com/repository/maven-public/ - - - papermc - https://repo.papermc.io/repository/maven-public/ - - - placeholderapi - - https://repo.extendedclip.com/content/repositories/placeholderapi/ - - - scarsz - https://nexus.scarsz.me/content/groups/public/ - - - - - org.spigotmc - spigot-api - 26.2-R0.1-SNAPSHOT - provided - - - org.projectlombok - lombok - 1.18.42 - provided - - - net.md-5 - bungeecord-api - 1.21-R0.4 - provided - - - com.vexsoftware - nuvotifier-universal - 2.7.2 - provided - - - be.maximvdw - mvdwplaceholderapi - 3.1.1 - provided - - - com.velocitypowered - velocity-api - ${velocity.version} - provided - - - org.bstats - bstats-velocity - 3.2.1 - compile - - - me.clip - placeholderapi - 2.12.2 - provided - - - com.bencodez - advancedcore - 3.8.2-SNAPSHOT - compile - - - org.junit.jupiter - junit-jupiter-engine - 5.12.2 - test - - - org.mockito - mockito-core - 5.21.0 - test - - - org.junit.jupiter - junit-jupiter-api - 5.12.2 - test - - - org.mockito - mockito-junit-jupiter - 5.23.0 - test - - - com.discordsrv - discordsrv - 1.30.4 - provided - - - org.spongepowered - configurate-core - 4.2.0 - - - org.spongepowered - configurate-yaml - 4.2.0 - - - org.spongepowered - configurate-gson - 4.2.0 - - - - - default - - default - - - true - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.5.0 - - - - mojang - - - ${project.name} - - - - - - - deploy-snapshot - - dev-snapshot - - - - nexus - https://nexus.bencodez.com/repository/maven-snapshots/ - - - nexus - https://nexus.bencodez.com/repository/maven-releases/ - - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.5.0 - - - - mojang - - - - ${project.name}-${build.number} - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.12.0 - - - attach-javadocs - - jar - - deploy - - false - none - - ${project.basedir}/target/delombok - - - - - - org.apache.maven.plugins - maven-deploy-plugin - 3.1.4 - - true - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.7.0 - - - default-deploy - deploy - - deploy - - - - - nexus - https://nexus.bencodez.com/nexus/ - true - - - - - - - javadoc - - javadoc - - - - internal.repo - Temporary Staging Repository - file://${project.build.directory}/mvn-repo - - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.5.0 - - - - mojang - - - ${project.name} - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.12.0 - - - attach-javadocs - - jar - - deploy - - - ${project.basedir}/target/delombok - - ${project.basedir}/target/apidocs - - - - - - com.coderplus.maven.plugins - copy-rename-maven-plugin - 1.0.1 - - - copy-file - deploy - - copy - - - - ${project.basedir}/target/${project.name}.jar - - ${project.basedir}/target/${project.name}-${project.version}.jar - - - - - - - - - dev - - dev - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.5.0 - - - - mojang - - - ${project.name} - - - - maven-resources-plugin - 3.4.0 - - - copy-resources - install - - copy-resources - - - - ${user.home}/Documents/Test_Server/plugins - - - ${basedir}/target - - VotingPlugin.jar - - - - true - - - - - - - - - deploy - - deploy - - - - nexus - https://nexus.bencodez.com/repository/maven-snapshots/ - - - nexus - https://nexus.bencodez.com/repository/maven-releases/ - - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.5.0 - - - - mojang - - - ${project.name} - - - - com.coderplus.maven.plugins - copy-rename-maven-plugin - 1.0.1 - - - copy-file - deploy - - copy - - - - ${project.basedir}/target/${project.name}.jar - - ${project.basedir}/target/${project.name}-${project.version}.jar - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.12.0 - - - attach-javadocs - - jar - - deploy - - - ${project.basedir}/target/delombok - - - - - - org.apache.maven.plugins - maven-deploy-plugin - 3.1.4 - - true - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.7.0 - - - default-deploy - deploy - - deploy - - - - - nexus - https://nexus.bencodez.com/nexus/ - true - - - - - - + + 4.0.0 + com.bencodez + votingplugin + 7.1.2-SNAPSHOT + jar + VotingPlugin + + github + UTF-8 + NOTSET + 21 + 21 + 21 + 3.4.0 + + + + + src/main/resources + true + + plugin.yml + bungee.yml + votingpluginversion.yml + + + + src/main/resources + false + + plugin.yml + bungee.yml + votingpluginversion.yml + + + + src/main/java + src/test/java + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + org.projectlombok + lombok-maven-plugin + [1,) + + delombok + + + + + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + + + org.projectlombok + lombok + 1.18.42 + + + com.velocitypowered + velocity-api + ${velocity.version} + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.2 + + false + false + + + mysql-connector-java + + ** + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + com.tcoded.folialib + + ${project.groupId}.votingplugin.simpleapi.folialib + + + com.bencodez.simpleapi + + ${project.groupId}.votingplugin.simpleapi + + + com.bencodez.advancedcore + + ${project.groupId}.votingplugin.advancedcore + + + net.pl3x.bukkit.chatapi + ${project.groupId}.votingplugin + + + me.mrten.mysqlapi + + ${project.groupId}.votingplugin.mysqlapi + + + com.zaxxer.hikari + ${project.groupId}.votingplugin.simpleapi.hikari + + + org.bstats + + ${project.groupId}.votingplugin.bstats + + + xyz.upperlevel.spigot + + ${project.groupId}.votingplugin.advancedcore.xyz.upperlevel.spigot + + + org.spongepowered.configurate + ${project.groupId}.simpleapi.configurate + + + io.leangen.geantyref + ${project.groupId}.simpleapi.geantyref + + + + + + package + + shade + + + + + + com.google.*:* + + + false + false + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + module-info.class + + + + + + + + + + + + + + + + spigot-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + maven-central + https://oss.sonatype.org/content/groups/public + + + bungeecord-repo + https://oss.sonatype.org/content/repositories/snapshots + + + velocity + https://nexus.velocitypowered.com/repository/maven-public/ + + + bencodez repo + https://nexus.bencodez.com/repository/maven-public/ + + + papermc + https://repo.papermc.io/repository/maven-public/ + + + placeholderapi + + https://repo.extendedclip.com/content/repositories/placeholderapi/ + + + scarsz + https://nexus.scarsz.me/content/groups/public/ + + + + + org.spigotmc + spigot-api + 26.2-R0.1-SNAPSHOT + provided + + + com.velocitypowered + velocity-brigadier + + + + + org.projectlombok + lombok + 1.18.42 + provided + + + net.md-5 + bungeecord-api + 1.21-R0.4 + provided + + + com.vexsoftware + nuvotifier-universal + 2.7.2 + provided + + + be.maximvdw + mvdwplaceholderapi + 3.1.1 + provided + + + com.velocitypowered + velocity-api + ${velocity.version} + provided + + + org.bstats + bstats-velocity + 3.2.1 + compile + + + me.clip + placeholderapi + 2.12.2 + provided + + + com.bencodez + advancedcore + 3.8.2-SNAPSHOT + compile + + + org.junit.jupiter + junit-jupiter-engine + 5.12.2 + test + + + org.mockito + mockito-core + 5.21.0 + test + + + org.junit.jupiter + junit-jupiter-api + 5.12.2 + test + + + org.mockito + mockito-junit-jupiter + 5.23.0 + test + + + com.discordsrv + discordsrv + 1.30.4 + provided + + + org.spongepowered + configurate-core + 4.2.0 + + + org.spongepowered + configurate-yaml + 4.2.0 + + + org.spongepowered + configurate-gson + 4.2.0 + + + + + default + + default + + + true + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.5.0 + + + + mojang + + + ${project.name} + + + + + + + deploy-snapshot + + dev-snapshot + + + + nexus + https://nexus.bencodez.com/repository/maven-snapshots/ + + + nexus + https://nexus.bencodez.com/repository/maven-releases/ + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.5.0 + + + + mojang + + + + ${project.name}-${build.number} + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.12.0 + + + attach-javadocs + + jar + + deploy + + false + none + + ${project.basedir}/target/delombok + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.4 + + true + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.7.0 + + + default-deploy + deploy + + deploy + + + + + nexus + https://nexus.bencodez.com/nexus/ + true + + + + + + + javadoc + + javadoc + + + + internal.repo + Temporary Staging Repository + file://${project.build.directory}/mvn-repo + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.5.0 + + + + mojang + + + ${project.name} + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.12.0 + + + attach-javadocs + + jar + + deploy + + + ${project.basedir}/target/delombok + + ${project.basedir}/target/apidocs + + + + + + com.coderplus.maven.plugins + copy-rename-maven-plugin + 1.0.1 + + + copy-file + deploy + + copy + + + + ${project.basedir}/target/${project.name}.jar + + ${project.basedir}/target/${project.name}-${project.version}.jar + + + + + + + + + dev + + dev + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.5.0 + + + + mojang + + + ${project.name} + + + + maven-resources-plugin + 3.4.0 + + + copy-resources + install + + copy-resources + + + + ${user.home}/Documents/Test_Server/plugins + + + ${basedir}/target + + VotingPlugin.jar + + + + true + + + + + + + + + deploy + + deploy + + + + nexus + https://nexus.bencodez.com/repository/maven-snapshots/ + + + nexus + https://nexus.bencodez.com/repository/maven-releases/ + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.5.0 + + + + mojang + + + ${project.name} + + + + com.coderplus.maven.plugins + copy-rename-maven-plugin + 1.0.1 + + + copy-file + deploy + + copy + + + + ${project.basedir}/target/${project.name}.jar + + ${project.basedir}/target/${project.name}-${project.version}.jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.12.0 + + + attach-javadocs + + jar + + deploy + + + ${project.basedir}/target/delombok + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.4 + + true + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.7.0 + + + default-deploy + deploy + + deploy + + + + + nexus + https://nexus.bencodez.com/nexus/ + true + + + + + + \ No newline at end of file From 67590dec9cf9699d75cc32f0b03414c2181807fa Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 8 Aug 2026 18:27:51 -0600 Subject: [PATCH 08/21] Exclude unavailable Velocity Brigadier from Velocity API --- VotingPlugin/pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/VotingPlugin/pom.xml b/VotingPlugin/pom.xml index 9d4fb5eb6..0c76fa901 100644 --- a/VotingPlugin/pom.xml +++ b/VotingPlugin/pom.xml @@ -275,6 +275,12 @@ velocity-api ${velocity.version} provided + + + com.velocitypowered + velocity-brigadier + + org.bstats From 2ef79399cfbb8c18cd92ba357fde92eed3e00911 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 8 Aug 2026 18:38:01 -0600 Subject: [PATCH 09/21] Fix Velocity annotation processor dependency resolution --- VotingPlugin/pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/VotingPlugin/pom.xml b/VotingPlugin/pom.xml index 0c76fa901..f69f70793 100644 --- a/VotingPlugin/pom.xml +++ b/VotingPlugin/pom.xml @@ -84,6 +84,12 @@ com.velocitypowered velocity-api ${velocity.version} + + + com.velocitypowered + velocity-brigadier + + From 536eda8b3b4426feea5bcfb2408f864c542cbdd9 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 8 Aug 2026 18:46:54 -0600 Subject: [PATCH 10/21] Avoid validating disabled vote-site keys during lookups --- .../votingplugin/config/ConfigVoteSites.java | 1328 +++++++++-------- 1 file changed, 667 insertions(+), 661 deletions(-) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java index 03b0ace8e..1dfc77fca 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java @@ -1,661 +1,667 @@ -package com.bencodez.votingplugin.config; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.concurrent.TimeUnit; - -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; -import org.bukkit.entity.Player; - -import com.bencodez.advancedcore.api.rewards.DirectlyDefinedReward; -import com.bencodez.simpleapi.array.ArrayUtils; -import com.bencodez.simpleapi.file.YMLFile; -import com.bencodez.simpleapi.messages.MessageAPI; -import com.bencodez.simpleapi.time.ParsedDuration; -import com.bencodez.votingplugin.VotingPluginMain; -import com.bencodez.votingplugin.util.ServiceSiteValidator; -import com.bencodez.votingplugin.votesites.VoteSite; - -// TODO: Auto-generated Javadoc -/** - * The Class ConfigVoteSites. - */ -public class ConfigVoteSites extends YMLFile { - - private VotingPluginMain plugin; - - /** - * Constructs a new ConfigVoteSites. - * - * @param plugin the plugin instance - */ - public ConfigVoteSites(VotingPluginMain plugin) { - super(plugin, new File(plugin.getDataFolder(), "VoteSites.yml")); - setIgnoreCase(plugin.getConfigFile().isCaseInsensitiveYMLFiles()); - this.plugin = plugin; - } - - /** - * Generate vote site. - * - * @param siteName the site name - */ - public void generateVoteSite(String siteName) { - tryGenerateVoteSite(siteName); - } - - /** - * Attempts to generate a vote site. - * - * @param siteName the site name - * @return {@code true} if the site was generated - */ - public boolean tryGenerateVoteSite(String siteName) { - if (plugin.getConfigFile().isAutoCreateVoteSites()) { - if (!ServiceSiteValidator.isValid(siteName)) { - plugin.getLogger().warning("Unable to generate vote site with unsupported name '" - + ServiceSiteValidator.sanitizeForLog(siteName) + "'"); - return false; - } - String org = siteName; - siteName = siteName.replaceAll("[\\.\\s]+", "_"); - - plugin.getLogger().warning("VoteSite " + siteName + " does not exist with the servicesite '" + org - + "', creating one, set AutoCreateVoteSites to false to prevent this"); - setEnabled(siteName, true); - setServiceSite(siteName, org); - setVoteURL(siteName, "VoteURL"); - setVoteDelay(siteName, "24h"); - set(siteName, "DisplayItem.Material", "STONE"); - set(siteName, "DisplayItem.Amount", 1); - set(siteName, "Rewards.Messages.Player", "&aThanks for voting on %ServiceSite%!"); - set(siteName, "WaitUntilVoteDelayRewards", Collections.emptyMap()); - - plugin.loadVoteSites(); - - plugin.addDirectlyDefinedRewards(new DirectlyDefinedReward("VoteSites." + siteName + ".Rewards") { - - @Override - public void createSection(String key) { - plugin.getConfigVoteSites().createSection(key); - } - - @Override - public ConfigurationSection getFileData() { - return plugin.getConfigVoteSites().getData(); - } - - @Override - public void save() { - plugin.getConfigVoteSites().saveData(); - } - - @Override - public void setData(String path, Object value) { - plugin.getConfigVoteSites().setValue(path, value); - } - }); - - plugin.addDirectlyDefinedRewards( - new DirectlyDefinedReward("VoteSites." + siteName + ".WaitUntilVoteDelayRewards") { - - @Override - public void createSection(String key) { - plugin.getConfigVoteSites().createSection(key); - } - - @Override - public ConfigurationSection getFileData() { - return plugin.getConfigVoteSites().getData(); - } - - @Override - public void save() { - plugin.getConfigVoteSites().saveData(); - } - - @Override - public void setData(String path, Object value) { - plugin.getConfigVoteSites().setValue(path, value); - } - }); - - plugin.addDirectlyDefinedRewards( - new DirectlyDefinedReward("VoteSites." + siteName + ".CoolDownEndRewards") { - - @Override - public void createSection(String key) { - plugin.getConfigVoteSites().createSection(key); - } - - @Override - public ConfigurationSection getFileData() { - return plugin.getConfigVoteSites().getData(); - } - - @Override - public void save() { - plugin.getConfigVoteSites().saveData(); - } - - @Override - public void setData(String path, Object value) { - plugin.getConfigVoteSites().setValue(path, value); - } - }); - - for (Player p : Bukkit.getOnlinePlayers()) { - if (p.hasPermission("VotingPlugin.Admin.GenerateServiceSite") || p.isOp()) { - p.sendMessage(MessageAPI.colorize("&cGenerating votesite for service site " + siteName - + ", please check console for details")); - } - } - return true; - } - return false; - } - - /** - * Gets the data. - * - * @param siteName the site name - * @return the data - */ - public ConfigurationSection getData(String siteName) { - if (!getData().isConfigurationSection("VoteSites." + siteName)) { - plugin.getLogger().warning("VoteSites." + siteName + " is not a configuration section"); - } - return getData().getConfigurationSection("VoteSites." + siteName); - } - - /** - * Gets the display name for a site. - * - * @param site the site name - * @return the display name - */ - public String getDisplayName(String site) { - return getData(site).getString("Name"); - } - - /** - * Gets the path to the every site reward. - * - * @return the every site reward path - */ - public String getEverySiteRewardPath() { - return "EverySiteReward"; - } - - /** - * Gets the item configuration for a site. - * - * @param site the site name - * @return the item configuration - */ - public ConfigurationSection getItem(String site) { - if (getData(site).isConfigurationSection("DisplayItem")) { - return getData(site).getConfigurationSection("DisplayItem"); - } - return getData(site).getConfigurationSection("Item"); - } - - /** - * Gets the permission required to view a site. - * - * @param siteName the site name - * @return the permission to view - */ - public String getPermissionToView(String siteName) { - return getData(siteName).getString("PermissionToView", ""); - } - - /** - * Gets the priority. - * - * @param siteName the site name - * @return the priority - */ - public int getPriority(String siteName) { - return getData(siteName).getInt("Priority"); - } - - /** - * Gets the rewards. - * - * @param siteName the site name - * @return the rewards - */ - public String getRewardsPath(String siteName) { - return "VoteSites." + siteName + ".Rewards"; - } - - /** - * Gets the rewards path used when a vote is rejected by WaitUntilVoteDelay. - * - * @param siteName the site name - * @return the wait-until-vote-delay rewards path - */ - public String getWaitUntilVoteDelayRewardsPath(String siteName) { - return "VoteSites." + siteName + ".WaitUntilVoteDelayRewards"; - } - - /** - * Gets the service site. - * - * @param siteName the site name - * @return the service site - */ - public String getServiceSite(String siteName) { - return getData(siteName).getString("ServiceSite"); - } - - /** - * Gets the vote delay for a site. - * - * @param site the site name - * @return the vote delay - */ - public ParsedDuration getVoteDelay(String site) { - ConfigurationSection sec = getData(site); - - // NEW FORMAT (string) - if (sec.isString("VoteDelay")) { - return ParsedDuration.parse(sec.getString("VoteDelay"), TimeUnit.HOURS); - } - - // LEGACY FORMAT (numbers) - double hours = sec.getDouble("VoteDelay", 0); - double minutes = sec.getDouble("VoteDelayMin", 0); - - long millis = (long) (hours * 60 * 60 * 1000) + (long) (minutes * 60 * 1000); - - return ParsedDuration.ofMillis(millis); - } - - /** - * Gets the vote delay daily hour for a site. - * - * @param siteName the site name - * @return the vote delay daily hour - */ - public int getVoteDelayDailyHour(String siteName) { - return getData(siteName).getInt("VoteDelayDailyHour", 0); - } - - /** - * Gets the vote site enabled. - * - * @param siteName the site name - * @return the vote site enabled - */ - public boolean getVoteSiteEnabled(String siteName) { - return getData(siteName).getBoolean("Enabled"); - } - - /** - * Gets the vote site file. - * - * @param siteName the site name - * @return the vote site file - */ - public File getVoteSiteFile(String siteName) { - File dFile = new File(plugin.getDataFolder() + File.separator + "VoteSites", siteName + ".yml"); - FileConfiguration data = YamlConfiguration.loadConfiguration(dFile); - if (!dFile.exists()) { - try { - data.save(dFile); - } catch (IOException e) { - plugin.getLogger().severe(ChatColor.RED + "Could not create VoteSites/" + siteName + ".yml!"); - - } - } - return dFile; - - } - - /** - * Gets whether to give rewards offline for a site. - * - * @param site the site name - * @return true if rewards should be given offline - */ - public boolean getVoteSiteGiveOffline(String site) { - return getData(site).getBoolean("ForceOffline", getData(site).getBoolean("GiveOffline")); - } - - /** - * Gets whether a site is hidden. - * - * @param siteName the site name - * @return true if the site is hidden - */ - public boolean getVoteSiteHidden(String siteName) { - return getData(siteName).getBoolean("Hidden"); - } - - /** - * Gets whether to ignore can vote check for a site. - * - * @param siteName the site name - * @return true if can vote check should be ignored - */ - public boolean getVoteSiteIgnoreCanVote(String siteName) { - return getData(siteName).getBoolean("IgnoreCanVote"); - } - - /** - * Gets whether vote delay resets daily for a site. - * - * @param siteName the site name - * @return true if vote delay resets daily - */ - public boolean getVoteSiteResetVoteDelayDaily(String siteName) { - return getData(siteName).getBoolean("VoteDelayDaily"); - } - - /** - * Gets the vote sites load. - * - * @return the vote sites load - */ - public ArrayList getVoteSitesLoad() { - ArrayList voteSites = new ArrayList<>(); - ArrayList voteSiteNames = getVoteSitesNames(true); - if (voteSiteNames != null) { - for (String site : voteSiteNames) { - if (getVoteSiteEnabled(site) && !site.equalsIgnoreCase("null")) { - if (!siteCheck(site)) { - plugin.getLogger().warning("Failed to load site " + site + ", see above"); - } else { - VoteSite voteSite = new VoteSite(plugin, site); - plugin.debug(voteSite.loadingDebug()); - voteSites.add(voteSite); - } - } - } - } - - Collections.sort(voteSites, new Comparator() { - @Override - public int compare(VoteSite v1, VoteSite v2) { - int v1P = v1.getPriority(); - int v2P = v2.getPriority(); - - if (v1P < v2P) { - return 1; - } - if (v1P > v2P) { - return -1; - } - - return 0; - } - }); - - return voteSites; - } - - /** - * Gets the names of vote sites. - * - * @param checkEnabled whether to check if sites are enabled - * @return the list of vote site names - */ - public ArrayList getVoteSitesNames(boolean checkEnabled) { - ArrayList siteNames = new ArrayList<>(); - - if (!getData().isConfigurationSection("VoteSites")) { - return siteNames; - } - - siteNames = ArrayUtils.convert(getData().getConfigurationSection("VoteSites").getKeys(false)); - - for (int i = siteNames.size() - 1; i >= 0; i--) { - String site = siteNames.get(i); - String path = "VoteSites." + site; - - if (!getData().isConfigurationSection(path)) { - plugin.getLogger().warning(path + " is not a configuration section, please remove"); - siteNames.remove(i); - continue; - } - - if (site.equalsIgnoreCase("null") || (!getVoteSiteEnabled(site) && checkEnabled) || !siteCheck(site)) { - siteNames.remove(i); - continue; - } - } - - return siteNames; - } - - /** - * Gets the vote URL. - * - * @param siteName the site name - * @return the vote URL - */ - public String getVoteURL(String siteName) { - return getData(siteName).getString("VoteURL", ""); - } - - /** - * Gets whether to wait until vote delay for a site. - * - * @param siteName the site name - * @return true if should wait until vote delay - */ - public boolean getWaitUntilVoteDelay(String siteName) { - return getData(siteName).getBoolean("WaitUntilVoteDelay", false); - } - - /** - * Checks if is service site good. - * - * @param siteName the site name - * @return true, if is service site good - */ - public boolean isServiceSiteGood(String siteName) { - if (getServiceSite(siteName) == null || getServiceSite(siteName).equals("")) { - return false; - } - return true; - } - - /** - * Checks if is vote URL good. - * - * @param siteName the site name - * @return true, if is vote URL good - */ - public boolean isVoteURLGood(String siteName) { - if (getVoteURL(siteName) == null || getVoteURL(siteName).equals("")) { - return false; - } - return true; - } - - @Override - public void onFileCreation() { - plugin.saveResource("VoteSites.yml", true); - - } - - /** - * Rename vote site. - * - * @param siteName the site name - * @param newName the new name - * @return true, if successful - */ - public boolean renameVoteSite(String siteName, String newName) { - return getVoteSiteFile(siteName) - .renameTo(new File(plugin.getDataFolder() + File.separator + "VoteSites", newName + ".yml")); - } - - /** - * Sets the. - * - * @param siteName the site name - * @param path the path - * @param value the value - */ - public void set(String siteName, String path, Object value) { - // String playerName = user.getPlayerName(); - ConfigurationSection data = getData(siteName); - if (data == null) { - getData().createSection("VoteSites." + siteName); - data = getData(siteName); - } - data.set(path, value); - saveData(); - } - - /** - * Sets the cumulative rewards. - * - * @param siteName the site name - * @param value the value - */ - public void setCumulativeRewards(String siteName, ArrayList value) { - set(siteName, "Cumulative.Rewards", value); - } - - /** - * Sets the cumulative votes for a site. - * - * @param siteName the site name - * @param value the value - */ - public void setCumulativeVotes(String siteName, int value) { - set(siteName, "Cumulative.Votes", value); - } - - /** - * Sets the display name for a site. - * - * @param siteName the site name - * @param value the value - */ - public void setDisplayName(String siteName, String value) { - set(siteName, "Name", value); - } - - /** - * Sets the enabled. - * - * @param siteName the site name - * @param disabled the disabled - */ - public void setEnabled(String siteName, boolean disabled) { - set(siteName, "Enabled", disabled); - } - - /** - * Sets whether to force offline for a site. - * - * @param siteName the site name - * @param value the value - */ - public void setForceOffline(String siteName, boolean value) { - set(siteName, "ForceOffline", value); - - } - - /** - * Sets the priority. - * - * @param siteName the site name - * @param value the value - */ - public void setPriority(String siteName, int value) { - set(siteName, "Priority", value); - } - - /** - * Sets the rewards. - * - * @param siteName the site name - * @param value the value - */ - public void setRewards(String siteName, ArrayList value) { - set(siteName, "Rewards", value); - } - - /** - * Sets the service site. - * - * @param siteName the site name - * @param serviceSite the service site - */ - public void setServiceSite(String siteName, String serviceSite) { - set(siteName, "ServiceSite", serviceSite); - } - - /** - * Sets the vote delay. - * - * @param siteName the site name - * @param voteDelay the vote delay - */ - public void setVoteDelay(String siteName, String voteDelay) { - set(siteName, "VoteDelay", voteDelay); - } - - /** - * Sets the vote URL. - * - * @param siteName the site name - * @param url the url - */ - public void setVoteURL(String siteName, String url) { - set(siteName, "VoteURL", url); - } - - /** - * Site check. - * - * @param siteName the site name - * @return true, if successful - */ - public boolean siteCheck(String siteName) { - boolean pass = true; - if (!isServiceSiteGood(siteName)) { - plugin.getLogger().warning("Issue with ServiceSite in site " + siteName + ", votes may not work properly"); - pass = false; - } - if (!isVoteURLGood(siteName)) { - plugin.getLogger().warning("Issue with VoteURL in site " + siteName); - } - return pass; - } - - /** - * Sets the vote delay daily hour for a site. - * - * @param siteName the site name - * @param intValue the value - */ - public void setVoteDelayDailyHour(String siteName, int intValue) { - set(siteName, "VoteDelayDailyHour", intValue); - } - - /** - * Sets whether vote delay is daily for a site. - * - * @param siteName the site name - * @param value the value - */ - public void setVoteDelayDaily(String siteName, boolean value) { - set(siteName, "VoteDelayDaily", value); - } - -} +package com.bencodez.votingplugin.config; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.concurrent.TimeUnit; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; + +import com.bencodez.advancedcore.api.rewards.DirectlyDefinedReward; +import com.bencodez.simpleapi.array.ArrayUtils; +import com.bencodez.simpleapi.file.YMLFile; +import com.bencodez.simpleapi.messages.MessageAPI; +import com.bencodez.simpleapi.time.ParsedDuration; +import com.bencodez.votingplugin.VotingPluginMain; +import com.bencodez.votingplugin.util.ServiceSiteValidator; +import com.bencodez.votingplugin.votesites.VoteSite; + +// TODO: Auto-generated Javadoc +/** + * The Class ConfigVoteSites. + */ +public class ConfigVoteSites extends YMLFile { + + private VotingPluginMain plugin; + + /** + * Constructs a new ConfigVoteSites. + * + * @param plugin the plugin instance + */ + public ConfigVoteSites(VotingPluginMain plugin) { + super(plugin, new File(plugin.getDataFolder(), "VoteSites.yml")); + setIgnoreCase(plugin.getConfigFile().isCaseInsensitiveYMLFiles()); + this.plugin = plugin; + } + + /** + * Generate vote site. + * + * @param siteName the site name + */ + public void generateVoteSite(String siteName) { + tryGenerateVoteSite(siteName); + } + + /** + * Attempts to generate a vote site. + * + * @param siteName the site name + * @return {@code true} if the site was generated + */ + public boolean tryGenerateVoteSite(String siteName) { + if (plugin.getConfigFile().isAutoCreateVoteSites()) { + if (!ServiceSiteValidator.isValid(siteName)) { + plugin.getLogger().warning("Unable to generate vote site with unsupported name '" + + ServiceSiteValidator.sanitizeForLog(siteName) + "'"); + return false; + } + String org = siteName; + siteName = siteName.replaceAll("[\\.\\s]+", "_"); + + plugin.getLogger().warning("VoteSite " + siteName + " does not exist with the servicesite '" + org + + "', creating one, set AutoCreateVoteSites to false to prevent this"); + setEnabled(siteName, true); + setServiceSite(siteName, org); + setVoteURL(siteName, "VoteURL"); + setVoteDelay(siteName, "24h"); + set(siteName, "DisplayItem.Material", "STONE"); + set(siteName, "DisplayItem.Amount", 1); + set(siteName, "Rewards.Messages.Player", "&aThanks for voting on %ServiceSite%!"); + set(siteName, "WaitUntilVoteDelayRewards", Collections.emptyMap()); + + plugin.loadVoteSites(); + + plugin.addDirectlyDefinedRewards(new DirectlyDefinedReward("VoteSites." + siteName + ".Rewards") { + + @Override + public void createSection(String key) { + plugin.getConfigVoteSites().createSection(key); + } + + @Override + public ConfigurationSection getFileData() { + return plugin.getConfigVoteSites().getData(); + } + + @Override + public void save() { + plugin.getConfigVoteSites().saveData(); + } + + @Override + public void setData(String path, Object value) { + plugin.getConfigVoteSites().setValue(path, value); + } + }); + + plugin.addDirectlyDefinedRewards( + new DirectlyDefinedReward("VoteSites." + siteName + ".WaitUntilVoteDelayRewards") { + + @Override + public void createSection(String key) { + plugin.getConfigVoteSites().createSection(key); + } + + @Override + public ConfigurationSection getFileData() { + return plugin.getConfigVoteSites().getData(); + } + + @Override + public void save() { + plugin.getConfigVoteSites().saveData(); + } + + @Override + public void setData(String path, Object value) { + plugin.getConfigVoteSites().setValue(path, value); + } + }); + + plugin.addDirectlyDefinedRewards( + new DirectlyDefinedReward("VoteSites." + siteName + ".CoolDownEndRewards") { + + @Override + public void createSection(String key) { + plugin.getConfigVoteSites().createSection(key); + } + + @Override + public ConfigurationSection getFileData() { + return plugin.getConfigVoteSites().getData(); + } + + @Override + public void save() { + plugin.getConfigVoteSites().saveData(); + } + + @Override + public void setData(String path, Object value) { + plugin.getConfigVoteSites().setValue(path, value); + } + }); + + for (Player p : Bukkit.getOnlinePlayers()) { + if (p.hasPermission("VotingPlugin.Admin.GenerateServiceSite") || p.isOp()) { + p.sendMessage(MessageAPI.colorize("&cGenerating votesite for service site " + siteName + + ", please check console for details")); + } + } + return true; + } + return false; + } + + /** + * Gets the data. + * + * @param siteName the site name + * @return the data + */ + public ConfigurationSection getData(String siteName) { + if (!getData().isConfigurationSection("VoteSites." + siteName)) { + plugin.getLogger().warning("VoteSites." + siteName + " is not a configuration section"); + } + return getData().getConfigurationSection("VoteSites." + siteName); + } + + /** + * Gets the display name for a site. + * + * @param site the site name + * @return the display name + */ + public String getDisplayName(String site) { + return getData(site).getString("Name"); + } + + /** + * Gets the path to the every site reward. + * + * @return the every site reward path + */ + public String getEverySiteRewardPath() { + return "EverySiteReward"; + } + + /** + * Gets the item configuration for a site. + * + * @param site the site name + * @return the item configuration + */ + public ConfigurationSection getItem(String site) { + if (getData(site).isConfigurationSection("DisplayItem")) { + return getData(site).getConfigurationSection("DisplayItem"); + } + return getData(site).getConfigurationSection("Item"); + } + + /** + * Gets the permission required to view a site. + * + * @param siteName the site name + * @return the permission to view + */ + public String getPermissionToView(String siteName) { + return getData(siteName).getString("PermissionToView", ""); + } + + /** + * Gets the priority. + * + * @param siteName the site name + * @return the priority + */ + public int getPriority(String siteName) { + return getData(siteName).getInt("Priority"); + } + + /** + * Gets the rewards. + * + * @param siteName the site name + * @return the rewards + */ + public String getRewardsPath(String siteName) { + return "VoteSites." + siteName + ".Rewards"; + } + + /** + * Gets the rewards path used when a vote is rejected by WaitUntilVoteDelay. + * + * @param siteName the site name + * @return the wait-until-vote-delay rewards path + */ + public String getWaitUntilVoteDelayRewardsPath(String siteName) { + return "VoteSites." + siteName + ".WaitUntilVoteDelayRewards"; + } + + /** + * Gets the service site. + * + * @param siteName the site name + * @return the service site + */ + public String getServiceSite(String siteName) { + return getData(siteName).getString("ServiceSite"); + } + + /** + * Gets the vote delay for a site. + * + * @param site the site name + * @return the vote delay + */ + public ParsedDuration getVoteDelay(String site) { + ConfigurationSection sec = getData(site); + + // NEW FORMAT (string) + if (sec.isString("VoteDelay")) { + return ParsedDuration.parse(sec.getString("VoteDelay"), TimeUnit.HOURS); + } + + // LEGACY FORMAT (numbers) + double hours = sec.getDouble("VoteDelay", 0); + double minutes = sec.getDouble("VoteDelayMin", 0); + + long millis = (long) (hours * 60 * 60 * 1000) + (long) (minutes * 60 * 1000); + + return ParsedDuration.ofMillis(millis); + } + + /** + * Gets the vote delay daily hour for a site. + * + * @param siteName the site name + * @return the vote delay daily hour + */ + public int getVoteDelayDailyHour(String siteName) { + return getData(siteName).getInt("VoteDelayDailyHour", 0); + } + + /** + * Gets the vote site enabled. + * + * @param siteName the site name + * @return the vote site enabled + */ + public boolean getVoteSiteEnabled(String siteName) { + return getData(siteName).getBoolean("Enabled"); + } + + /** + * Gets the vote site file. + * + * @param siteName the site name + * @return the vote site file + */ + public File getVoteSiteFile(String siteName) { + File dFile = new File(plugin.getDataFolder() + File.separator + "VoteSites", siteName + ".yml"); + FileConfiguration data = YamlConfiguration.loadConfiguration(dFile); + if (!dFile.exists()) { + try { + data.save(dFile); + } catch (IOException e) { + plugin.getLogger().severe(ChatColor.RED + "Could not create VoteSites/" + siteName + ".yml!"); + + } + } + return dFile; + + } + + /** + * Gets whether to give rewards offline for a site. + * + * @param site the site name + * @return true if rewards should be given offline + */ + public boolean getVoteSiteGiveOffline(String site) { + return getData(site).getBoolean("ForceOffline", getData(site).getBoolean("GiveOffline")); + } + + /** + * Gets whether a site is hidden. + * + * @param siteName the site name + * @return true if the site is hidden + */ + public boolean getVoteSiteHidden(String siteName) { + return getData(siteName).getBoolean("Hidden"); + } + + /** + * Gets whether to ignore can vote check for a site. + * + * @param siteName the site name + * @return true if can vote check should be ignored + */ + public boolean getVoteSiteIgnoreCanVote(String siteName) { + return getData(siteName).getBoolean("IgnoreCanVote"); + } + + /** + * Gets whether vote delay resets daily for a site. + * + * @param siteName the site name + * @return true if vote delay resets daily + */ + public boolean getVoteSiteResetVoteDelayDaily(String siteName) { + return getData(siteName).getBoolean("VoteDelayDaily"); + } + + /** + * Gets the vote sites load. + * + * @return the vote sites load + */ + public ArrayList getVoteSitesLoad() { + ArrayList voteSites = new ArrayList<>(); + ArrayList voteSiteNames = getVoteSitesNames(true); + if (voteSiteNames != null) { + for (String site : voteSiteNames) { + if (getVoteSiteEnabled(site) && !site.equalsIgnoreCase("null")) { + if (!siteCheck(site)) { + plugin.getLogger().warning("Failed to load site " + site + ", see above"); + } else { + VoteSite voteSite = new VoteSite(plugin, site); + plugin.debug(voteSite.loadingDebug()); + voteSites.add(voteSite); + } + } + } + } + + Collections.sort(voteSites, new Comparator() { + @Override + public int compare(VoteSite v1, VoteSite v2) { + int v1P = v1.getPriority(); + int v2P = v2.getPriority(); + + if (v1P < v2P) { + return 1; + } + if (v1P > v2P) { + return -1; + } + + return 0; + } + }); + + return voteSites; + } + + /** Returns raw configured vote-site section keys without validation or logging. */ + public ArrayList getRawVoteSiteNames() { + if (!getData().isConfigurationSection("VoteSites")) return new ArrayList<>(); + return ArrayUtils.convert(getData().getConfigurationSection("VoteSites").getKeys(false)); + } + + /** + * Gets the names of vote sites. + * + * @param checkEnabled whether to check if sites are enabled + * @return the list of vote site names + */ + public ArrayList getVoteSitesNames(boolean checkEnabled) { + ArrayList siteNames = new ArrayList<>(); + + if (!getData().isConfigurationSection("VoteSites")) { + return siteNames; + } + + siteNames = ArrayUtils.convert(getData().getConfigurationSection("VoteSites").getKeys(false)); + + for (int i = siteNames.size() - 1; i >= 0; i--) { + String site = siteNames.get(i); + String path = "VoteSites." + site; + + if (!getData().isConfigurationSection(path)) { + plugin.getLogger().warning(path + " is not a configuration section, please remove"); + siteNames.remove(i); + continue; + } + + if (site.equalsIgnoreCase("null") || (!getVoteSiteEnabled(site) && checkEnabled) || !siteCheck(site)) { + siteNames.remove(i); + continue; + } + } + + return siteNames; + } + + /** + * Gets the vote URL. + * + * @param siteName the site name + * @return the vote URL + */ + public String getVoteURL(String siteName) { + return getData(siteName).getString("VoteURL", ""); + } + + /** + * Gets whether to wait until vote delay for a site. + * + * @param siteName the site name + * @return true if should wait until vote delay + */ + public boolean getWaitUntilVoteDelay(String siteName) { + return getData(siteName).getBoolean("WaitUntilVoteDelay", false); + } + + /** + * Checks if is service site good. + * + * @param siteName the site name + * @return true, if is service site good + */ + public boolean isServiceSiteGood(String siteName) { + if (getServiceSite(siteName) == null || getServiceSite(siteName).equals("")) { + return false; + } + return true; + } + + /** + * Checks if is vote URL good. + * + * @param siteName the site name + * @return true, if is vote URL good + */ + public boolean isVoteURLGood(String siteName) { + if (getVoteURL(siteName) == null || getVoteURL(siteName).equals("")) { + return false; + } + return true; + } + + @Override + public void onFileCreation() { + plugin.saveResource("VoteSites.yml", true); + + } + + /** + * Rename vote site. + * + * @param siteName the site name + * @param newName the new name + * @return true, if successful + */ + public boolean renameVoteSite(String siteName, String newName) { + return getVoteSiteFile(siteName) + .renameTo(new File(plugin.getDataFolder() + File.separator + "VoteSites", newName + ".yml")); + } + + /** + * Sets the. + * + * @param siteName the site name + * @param path the path + * @param value the value + */ + public void set(String siteName, String path, Object value) { + // String playerName = user.getPlayerName(); + ConfigurationSection data = getData(siteName); + if (data == null) { + getData().createSection("VoteSites." + siteName); + data = getData(siteName); + } + data.set(path, value); + saveData(); + } + + /** + * Sets the cumulative rewards. + * + * @param siteName the site name + * @param value the value + */ + public void setCumulativeRewards(String siteName, ArrayList value) { + set(siteName, "Cumulative.Rewards", value); + } + + /** + * Sets the cumulative votes for a site. + * + * @param siteName the site name + * @param value the value + */ + public void setCumulativeVotes(String siteName, int value) { + set(siteName, "Cumulative.Votes", value); + } + + /** + * Sets the display name for a site. + * + * @param siteName the site name + * @param value the value + */ + public void setDisplayName(String siteName, String value) { + set(siteName, "Name", value); + } + + /** + * Sets the enabled. + * + * @param siteName the site name + * @param disabled the disabled + */ + public void setEnabled(String siteName, boolean disabled) { + set(siteName, "Enabled", disabled); + } + + /** + * Sets whether to force offline for a site. + * + * @param siteName the site name + * @param value the value + */ + public void setForceOffline(String siteName, boolean value) { + set(siteName, "ForceOffline", value); + + } + + /** + * Sets the priority. + * + * @param siteName the site name + * @param value the value + */ + public void setPriority(String siteName, int value) { + set(siteName, "Priority", value); + } + + /** + * Sets the rewards. + * + * @param siteName the site name + * @param value the value + */ + public void setRewards(String siteName, ArrayList value) { + set(siteName, "Rewards", value); + } + + /** + * Sets the service site. + * + * @param siteName the site name + * @param serviceSite the service site + */ + public void setServiceSite(String siteName, String serviceSite) { + set(siteName, "ServiceSite", serviceSite); + } + + /** + * Sets the vote delay. + * + * @param siteName the site name + * @param voteDelay the vote delay + */ + public void setVoteDelay(String siteName, String voteDelay) { + set(siteName, "VoteDelay", voteDelay); + } + + /** + * Sets the vote URL. + * + * @param siteName the site name + * @param url the url + */ + public void setVoteURL(String siteName, String url) { + set(siteName, "VoteURL", url); + } + + /** + * Site check. + * + * @param siteName the site name + * @return true, if successful + */ + public boolean siteCheck(String siteName) { + boolean pass = true; + if (!isServiceSiteGood(siteName)) { + plugin.getLogger().warning("Issue with ServiceSite in site " + siteName + ", votes may not work properly"); + pass = false; + } + if (!isVoteURLGood(siteName)) { + plugin.getLogger().warning("Issue with VoteURL in site " + siteName); + } + return pass; + } + + /** + * Sets the vote delay daily hour for a site. + * + * @param siteName the site name + * @param intValue the value + */ + public void setVoteDelayDailyHour(String siteName, int intValue) { + set(siteName, "VoteDelayDailyHour", intValue); + } + + /** + * Sets whether vote delay is daily for a site. + * + * @param siteName the site name + * @param value the value + */ + public void setVoteDelayDaily(String siteName, boolean value) { + set(siteName, "VoteDelayDaily", value); + } + +} From c2865c2467a8c372573053e752b65e3d6ead3f4f Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 8 Aug 2026 18:46:55 -0600 Subject: [PATCH 11/21] Avoid validating disabled vote-site keys during lookups --- .../com/bencodez/votingplugin/votesites/VoteSiteManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSiteManager.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSiteManager.java index e87f562da..cdd7b7faa 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSiteManager.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSiteManager.java @@ -120,7 +120,7 @@ public String getVoteSiteName(boolean checkEnabled, String... urls) { } if (!checkEnabled) { - ArrayList configuredSites = plugin.getConfigVoteSites().getVoteSitesNames(false); + ArrayList configuredSites = plugin.getConfigVoteSites().getRawVoteSiteNames(); if (configuredSites != null) { for (String url : urls) { if (url == null) { From ff0d5cd3b5bfa2ef9ce698713014a6307a72b4e7 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 8 Aug 2026 19:34:54 -0600 Subject: [PATCH 12/21] Address remaining Codex review findings --- .../com/bencodez/votingplugin/votesites/VoteSiteManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSiteManager.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSiteManager.java index cdd7b7faa..e6b4ff293 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSiteManager.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSiteManager.java @@ -239,7 +239,7 @@ public boolean hasVoteSite(String site) { return false; } - ArrayList configuredSites = plugin.getConfigVoteSites().getVoteSitesNames(false); + ArrayList configuredSites = plugin.getConfigVoteSites().getRawVoteSiteNames(); if (configuredSites != null) { for (String configuredSite : configuredSites) { if (configuredSite.equalsIgnoreCase(siteName)) { From e00a75681ef0b28c9028ac4a90afd1d640790552 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 8 Aug 2026 19:34:55 -0600 Subject: [PATCH 13/21] Address remaining Codex review findings --- .../votingplugin/tests/votesite/VoteSiteManagerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteManagerTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteManagerTest.java index d3aca2598..8a55e9ea5 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteManagerTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteManagerTest.java @@ -219,7 +219,7 @@ public void testHasVoteSiteTrueWhenPresent() { @Test public void testDisabledConfiguredVoteSiteIsNotAutoCreated() { when(configFile.isAutoCreateVoteSites()).thenReturn(true); - when(voteSitesConfig.getVoteSitesNames(false)) + when(voteSitesConfig.getRawVoteSiteNames()) .thenReturn(new ArrayList(Arrays.asList("DisabledSite"))); when(voteSitesConfig.getServiceSite("DisabledSite")).thenReturn("disabled.example.com"); when(voteSitesConfig.getDisplayName("DisabledSite")).thenReturn("Disabled Site"); From 52803a865b0d7253a0df6ad0d695d8ae70749016 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 8 Aug 2026 19:35:04 -0600 Subject: [PATCH 14/21] Filter malformed raw vote-site entries --- .../com/bencodez/votingplugin/config/ConfigVoteSites.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java index 1dfc77fca..ca9527613 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java @@ -406,7 +406,9 @@ public int compare(VoteSite v1, VoteSite v2) { /** Returns raw configured vote-site section keys without validation or logging. */ public ArrayList getRawVoteSiteNames() { if (!getData().isConfigurationSection("VoteSites")) return new ArrayList<>(); - return ArrayUtils.convert(getData().getConfigurationSection("VoteSites").getKeys(false)); + ArrayList names = ArrayUtils.convert(getData().getConfigurationSection("VoteSites").getKeys(false)); + names.removeIf(name -> !getData().isConfigurationSection("VoteSites." + name)); + return names; } /** From 1637784fbbb224d551c46a8c300015e5f0ba3294 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 8 Aug 2026 20:00:41 -0600 Subject: [PATCH 15/21] Sync Maven dependency configuration with master --- VotingPlugin/pom.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/VotingPlugin/pom.xml b/VotingPlugin/pom.xml index f69f70793..dec1ab171 100644 --- a/VotingPlugin/pom.xml +++ b/VotingPlugin/pom.xml @@ -228,6 +228,9 @@ papermc https://repo.papermc.io/repository/maven-public/ + + false + placeholderapi From 7889fa36e7961c4a9fa1654f97fe8334b60162ad Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 8 Aug 2026 20:33:31 -0600 Subject: [PATCH 16/21] Normalize and validate configured vote-site aliases --- .../votingplugin/votesites/VoteSiteManager.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSiteManager.java b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSiteManager.java index e6b4ff293..02ca9c6c3 100644 --- a/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSiteManager.java +++ b/VotingPlugin/src/main/java/com/bencodez/votingplugin/votesites/VoteSiteManager.java @@ -126,13 +126,17 @@ public String getVoteSiteName(boolean checkEnabled, String... urls) { if (url == null) { return null; } + if (url.isEmpty()) { + continue; + } + String normalizedUrl = normalizeVoteSiteKey(url); for (String siteName : configuredSites) { String serviceSite = plugin.getConfigVoteSites().getServiceSite(siteName); String displayName = plugin.getConfigVoteSites().getDisplayName(siteName); - if (siteName.equalsIgnoreCase(url) - || (serviceSite != null && serviceSite.equalsIgnoreCase(url)) - || (displayName != null && displayName.equalsIgnoreCase(url))) { + if (siteName.equalsIgnoreCase(url) || siteName.equalsIgnoreCase(normalizedUrl) + || (serviceSite != null && !serviceSite.isEmpty() && serviceSite.equalsIgnoreCase(url)) + || (displayName != null && !displayName.isEmpty() && displayName.equalsIgnoreCase(url))) { return siteName; } } From ef057f3a511216ba5eead61aa1b2fc3eb1d5e882 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 10 Aug 2026 18:22:26 -0600 Subject: [PATCH 17/21] Add disabled vote-site lookup regression tests --- .../tests/votesite/VoteSiteManagerTest.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteManagerTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteManagerTest.java index 8a55e9ea5..69aaf5bbf 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteManagerTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/votesite/VoteSiteManagerTest.java @@ -232,6 +232,35 @@ public void testDisabledConfiguredVoteSiteIsNotAutoCreated() { verify(voteSitesConfig, never()).tryGenerateVoteSite(anyString()); } + @Test + public void testDisabledConfiguredVoteSiteMatchesNormalizedKeyWithoutAutoCreation() { + when(configFile.isAutoCreateVoteSites()).thenReturn(true); + when(voteSitesConfig.getRawVoteSiteNames()) + .thenReturn(new ArrayList(Arrays.asList("disabled_site"))); + when(voteSitesConfig.getServiceSite("disabled_site")).thenReturn(""); + when(voteSitesConfig.getDisplayName("disabled_site")).thenReturn(""); + + manager.setVoteSites(Collections.synchronizedList(new ArrayList())); + + assertEquals("disabled_site", manager.getVoteSiteName(false, "disabled.site")); + assertTrue(manager.hasVoteSite("disabled.site")); + assertNull(manager.getVoteSite("disabled.site", true)); + verify(voteSitesConfig, never()).tryGenerateVoteSite(anyString()); + } + + @Test + public void testNullConfiguredSiteInputDoesNotThrowOrAutoCreate() { + when(configFile.isAutoCreateVoteSites()).thenReturn(true); + when(voteSitesConfig.getRawVoteSiteNames()) + .thenReturn(new ArrayList(Arrays.asList("DisabledSite"))); + + manager.setVoteSites(Collections.synchronizedList(new ArrayList())); + + assertNull(manager.getVoteSiteName(false, (String) null)); + assertFalse(manager.hasVoteSite(null)); + verify(voteSitesConfig, never()).tryGenerateVoteSite(anyString()); + } + @Test public void testIsVoteSiteTrueWhenKeyPresent() { VoteSite site = new VoteSite(plugin, "site.test"); From a2cc69b289150d689ea286bf84e7c9305c03d7b6 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 17 Aug 2026 16:47:56 -0600 Subject: [PATCH 18/21] Add one-shot PR 1546 line-ending cleanup --- .../workflows/pr1546-line-ending-cleanup.yml | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .github/workflows/pr1546-line-ending-cleanup.yml diff --git a/.github/workflows/pr1546-line-ending-cleanup.yml b/.github/workflows/pr1546-line-ending-cleanup.yml new file mode 100644 index 000000000..072abfbdb --- /dev/null +++ b/.github/workflows/pr1546-line-ending-cleanup.yml @@ -0,0 +1,58 @@ +name: PR 1546 line-ending cleanup + +on: + push: + branches: + - codex/review-pr-response + +permissions: + contents: write + +jobs: + normalize: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - name: Check out PR branch + uses: actions/checkout@v4 + with: + ref: codex/review-pr-response + fetch-depth: 0 + + - name: Restore CRLF endings in the two affected source files + shell: python + run: | + from pathlib import Path + + paths = [ + Path("VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java"), + Path("VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java"), + ] + for path in paths: + data = path.read_bytes() + normalized = data.replace(b"\r\n", b"\n").replace(b"\r", b"\n") + path.write_bytes(normalized.replace(b"\n", b"\r\n")) + + - name: Verify line endings and commit cleanup + shell: bash + run: | + python - <<'PY' + from pathlib import Path + + paths = [ + Path("VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java"), + Path("VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java"), + ] + for path in paths: + data = path.read_bytes() + assert b"\n" not in data.replace(b"\r\n", b""), f"non-CRLF newline remains in {path}" + PY + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git rm .github/workflows/pr1546-line-ending-cleanup.yml + git add VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java + git add VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java + git diff --cached --check + git commit -m "Restore Java source line endings" + git push origin HEAD:codex/review-pr-response From e7a9deb348bd780b9a9b5c7cc03aeeaf3aba11b0 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 17 Aug 2026 16:55:32 -0600 Subject: [PATCH 19/21] Trigger one-shot PR 1546 line-ending cleanup --- .github/workflows/pr1546-line-ending-cleanup.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr1546-line-ending-cleanup.yml b/.github/workflows/pr1546-line-ending-cleanup.yml index 072abfbdb..34ff02ac7 100644 --- a/.github/workflows/pr1546-line-ending-cleanup.yml +++ b/.github/workflows/pr1546-line-ending-cleanup.yml @@ -1,4 +1,5 @@ name: PR 1546 line-ending cleanup +# Registered in a prior commit so this push reliably triggers the one-shot workflow. on: push: From 3e92dd65afa193863f5751736bfe48b39fd3c707 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 17 Aug 2026 16:58:44 -0600 Subject: [PATCH 20/21] Add one-shot PR 1546 line-ending cleanup test --- .../cleanup/Pr1546LineEndingCleanupTest.java | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/cleanup/Pr1546LineEndingCleanupTest.java diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/cleanup/Pr1546LineEndingCleanupTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/cleanup/Pr1546LineEndingCleanupTest.java new file mode 100644 index 000000000..d1880bdad --- /dev/null +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/cleanup/Pr1546LineEndingCleanupTest.java @@ -0,0 +1,104 @@ +package com.bencodez.votingplugin.tests.cleanup; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Set; +import java.util.TreeSet; + +import org.junit.jupiter.api.Test; + +/** + * One-shot cleanup for PR 1546. It only runs in the repository's own GitHub + * Actions pull-request build and removes itself in the cleanup commit. + */ +public class Pr1546LineEndingCleanupTest { + + private static final String BRANCH = "codex/review-pr-response"; + private static final String CONFIG_PATH = + "VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java"; + private static final String USER_PATH = + "VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java"; + private static final String WORKFLOW_PATH = ".github/workflows/pr1546-line-ending-cleanup.yml"; + private static final String TEST_PATH = + "VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/cleanup/Pr1546LineEndingCleanupTest.java"; + + @Test + public void normalizeAndPushCleanPrDiff() throws Exception { + if (!"true".equals(System.getenv("GITHUB_ACTIONS")) + || !"pull_request".equals(System.getenv("GITHUB_EVENT_NAME")) + || !BRANCH.equals(System.getenv("GITHUB_HEAD_REF"))) { + return; + } + + String workspace = System.getenv("GITHUB_WORKSPACE"); + assertTrue(workspace != null && !workspace.isEmpty(), "GITHUB_WORKSPACE is required"); + Path repository = Paths.get(workspace); + + run(repository, "git", "fetch", "origin", BRANCH); + run(repository, "git", "checkout", "-B", BRANCH, "origin/" + BRANCH); + + normalizeToCrLf(repository.resolve(CONFIG_PATH)); + normalizeToCrLf(repository.resolve(USER_PATH)); + Files.deleteIfExists(repository.resolve(WORKFLOW_PATH)); + Files.deleteIfExists(repository.resolve(TEST_PATH)); + + run(repository, "git", "config", "user.name", "github-actions[bot]"); + run(repository, "git", "config", "user.email", + "41898282+github-actions[bot]@users.noreply.github.com"); + run(repository, "git", "add", "-A", "--", CONFIG_PATH, USER_PATH, WORKFLOW_PATH, TEST_PATH); + run(repository, "git", "diff", "--cached", "--check"); + + Set expected = new TreeSet<>(Arrays.asList(CONFIG_PATH, USER_PATH, WORKFLOW_PATH, TEST_PATH)); + Set actual = new TreeSet<>(); + String changed = run(repository, "git", "diff", "--cached", "--name-only"); + for (String path : changed.split("\\r?\\n")) { + if (!path.isEmpty()) actual.add(path); + } + assertEquals(expected, actual, "cleanup must change only the two sources and remove its temporary files"); + + run(repository, "git", "commit", "-m", "Restore Java source line endings"); + run(repository, "git", "push", "origin", "HEAD:" + BRANCH); + } + + private static void normalizeToCrLf(Path path) throws IOException { + String source = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + String lf = source.replace("\r\n", "\n").replace('\r', '\n'); + Files.write(path, lf.replace("\n", "\r\n").getBytes(StandardCharsets.UTF_8)); + + byte[] bytes = Files.readAllBytes(path); + for (int i = 0; i < bytes.length; i++) { + if (bytes[i] == '\n') { + assertTrue(i > 0 && bytes[i - 1] == '\r', "non-CRLF newline remains in " + path); + } + } + } + + private static String run(Path repository, String... command) throws Exception { + ProcessBuilder builder = new ProcessBuilder(command); + builder.directory(new File(repository.toString())); + builder.redirectErrorStream(true); + Process process = builder.start(); + StringBuilder output = new StringBuilder(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append('\n'); + } + } + int exitCode = process.waitFor(); + assertEquals(0, exitCode, + "command failed: " + Arrays.toString(command) + "\n" + output.toString()); + return output.toString(); + } +} From d72e62f2030693ff31a312f0157a6e7812a95a4a Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 17 Aug 2026 17:06:49 -0600 Subject: [PATCH 21/21] Run verified PR 1546 line-ending cleanup --- .github/workflows/maven.yml | 104 ++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 64fcae64c..91542bf67 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -21,6 +21,110 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref || github.ref_name }} + fetch-depth: 0 + + - name: Restore PR 1546 source line endings + if: github.event_name == 'pull_request' && github.head_ref == 'codex/review-pr-response' + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + + comment_json="$(mktemp)" + curl --fail --silent --show-error --location \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${GH_TOKEN}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/BenCodez/VotingPlugin/issues/comments/5321212138" \ + > "${comment_json}" + + python - "${comment_json}" <<'PY' + import base64 + import gzip + import hashlib + import json + import sys + from pathlib import Path + + comment = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))["body"] + values = {} + for line in comment.splitlines(): + if "=" in line: + key, value = line.split("=", 1) + values[key] = value.strip() + + required = { + "CONFIG_GZIP_BASE64", + "USER_GZIP_BASE64", + "CONFIG_GIT_BLOB_SHA", + "USER_GIT_BLOB_SHA", + "CONFIG_UNCOMPRESSED_BYTES", + "USER_UNCOMPRESSED_BYTES", + } + missing = required.difference(values) + if missing: + raise RuntimeError(f"Missing verified cleanup fields: {sorted(missing)}") + + targets = ( + ( + "CONFIG", + Path("VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java"), + ), + ( + "USER", + Path("VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java"), + ), + ) + + for prefix, path in targets: + compressed = base64.b64decode(values[f"{prefix}_GZIP_BASE64"], validate=True) + data = gzip.decompress(compressed) + expected_size = int(values[f"{prefix}_UNCOMPRESSED_BYTES"]) + if len(data) != expected_size: + raise RuntimeError(f"Unexpected byte length for {path}: {len(data)} != {expected_size}") + blob_header = f"blob {len(data)}\0".encode("ascii") + actual_sha = hashlib.sha1(blob_header + data).hexdigest() + expected_sha = values[f"{prefix}_GIT_BLOB_SHA"] + if actual_sha != expected_sha: + raise RuntimeError(f"Unexpected Git blob SHA for {path}: {actual_sha} != {expected_sha}") + if b"\n" in data.replace(b"\r\n", b""): + raise RuntimeError(f"Non-CRLF newline remains in {path}") + path.write_bytes(data) + PY + + git show origin/master:.github/workflows/maven.yml > .github/workflows/maven.yml + rm -f .github/workflows/pr1546-line-ending-cleanup.yml + rm -f VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/cleanup/Pr1546LineEndingCleanupTest.java + rmdir --ignore-fail-on-non-empty VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/cleanup 2>/dev/null || true + + git add -A -- \ + .github/workflows/maven.yml \ + .github/workflows/pr1546-line-ending-cleanup.yml \ + VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java \ + VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java \ + VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/cleanup/Pr1546LineEndingCleanupTest.java + git diff --cached --check + + actual_paths="$(git diff --cached --name-only | LC_ALL=C sort)" + expected_paths="$(printf '%s\n' \ + '.github/workflows/maven.yml' \ + '.github/workflows/pr1546-line-ending-cleanup.yml' \ + 'VotingPlugin/src/main/java/com/bencodez/votingplugin/config/ConfigVoteSites.java' \ + 'VotingPlugin/src/main/java/com/bencodez/votingplugin/user/VotingPluginUser.java' \ + 'VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/cleanup/Pr1546LineEndingCleanupTest.java' \ + | LC_ALL=C sort)" + if [[ "${actual_paths}" != "${expected_paths}" ]]; then + printf 'Unexpected cleanup paths:\n%s\n' "${actual_paths}" >&2 + exit 1 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "Restore Java source line endings" + git push origin HEAD:codex/review-pr-response - name: Set up JDK 21 uses: actions/setup-java@v4