diff --git a/commands.config b/commands.config index 5b3532f..5833e23 100644 --- a/commands.config +++ b/commands.config @@ -25,6 +25,7 @@ indicator=1 info, about=1 invite=1 java, jdoodle=1 +leaderboard=1 map=1 perish, thenperish=1 ping=1 diff --git a/locCommands.config b/locCommands.config index b1e2cc7..830017c 100644 --- a/locCommands.config +++ b/locCommands.config @@ -21,6 +21,7 @@ rpend= java, jdoodle= rafflejoin= guildroleallowed= +leaderboard= buildHelp= dbflush= shutdown= diff --git a/locStrings.config b/locStrings.config index 7d69929..0147462 100644 --- a/locStrings.config +++ b/locStrings.config @@ -17,6 +17,10 @@ BetHistoryInfo=Sees the amount of beans Kitty has earned through your bets! [CommandCatch] CatchInfo=Play catch with yourself or with a friend with @! +[CommandLeaderboard] +LeaderboardInfo=Shows you the 10 users with the most beans on the server +LeaderboardTitle=Server Leaderboard + [CommandGuildRoleList] GuildRoleListOutput=The allowed roles are %s! GuildRoleListEmpty=There are no roles allowed! @@ -141,7 +145,7 @@ InfoInfo=Provides author info and a link to Kitty's website GuildRoleRemoveNotAllowed=You're not allowed to add %s GuildRoleRemoveFailure=Couldn't remove %s from %s GuildRoleRemoveSuccess=Removed %s from %s! -GuildRoleRemoveInfo= +GuildRoleRemoveInfo=Removes the role specified! [CommandWolfram] WolframError=Something went wrong! @@ -262,6 +266,6 @@ RaffleJoinInfo= RaffleJoinSuccess=You joined the raffle! [CommandDBFlush] -DBFlushInfo= +DBFlushInfo=Takes all objects queued for the database and writes them to the database, then says how many were written. diff --git a/src/commands/CommandDBFlush.java b/src/commands/CommandDBFlush.java index d0f37b8..506d3c2 100644 --- a/src/commands/CommandDBFlush.java +++ b/src/commands/CommandDBFlush.java @@ -1,8 +1,7 @@ package commands; -import java.awt.Color; - import core.Command; +import core.Config; import core.DatabaseManager; import core.LocStrings; import dataStructures.KittyChannel; @@ -29,7 +28,7 @@ public class CommandDBFlush extends Command KittyEmbed embed = new KittyEmbed(); embed.title = "Database queue flushed"; embed.descriptionText = "**Dirty:** " + numUpdated; - embed.color = new Color(7*16, 8*16, 9*16); + embed.color = Config.ColorDefault; res.CallEmbed(embed); } diff --git a/src/commands/CommandDBStats.java b/src/commands/CommandDBStats.java index 033bf34..ea0641d 100644 --- a/src/commands/CommandDBStats.java +++ b/src/commands/CommandDBStats.java @@ -1,10 +1,10 @@ package commands; -import java.awt.Color; import java.text.DateFormat; import java.text.SimpleDateFormat; import core.Command; +import core.Config; import core.DatabaseManager; import core.LocStrings; import dataStructures.KittyChannel; @@ -36,7 +36,7 @@ public class CommandDBStats extends Command embed.descriptionText = "**Tracked Items:** " + DatabaseManager.instance.getTrackedObjectsSize(); embed.descriptionText += "\n"; embed.descriptionText += "**Last Upkeep:** " + dateFormat.format(DatabaseManager.instance.getLastUpkeep()) + " UTC-7"; - embed.color = new Color(7*16, 8*16, 9*16); + embed.color = Config.ColorDefault; res.CallEmbed(embed); } diff --git a/src/commands/CommandLeaderboard.java b/src/commands/CommandLeaderboard.java new file mode 100644 index 0000000..cfc06af --- /dev/null +++ b/src/commands/CommandLeaderboard.java @@ -0,0 +1,99 @@ +package commands; + +import java.util.ArrayList; +import java.util.List; + +import core.Command; +import core.Config; +import core.DatabaseManager; +import core.LocStrings; +import core.ObjectBuilderFactory; +import dataStructures.KittyChannel; +import dataStructures.KittyEmbed; +import dataStructures.KittyGuild; +import dataStructures.KittyRating; +import dataStructures.KittyRole; +import dataStructures.KittyUser; +import dataStructures.Pair; +import dataStructures.Response; +import dataStructures.UserInput; +import utils.GlobalLog; +import utils.LogFilter; + +public class CommandLeaderboard extends Command +{ + public CommandLeaderboard(KittyRole roleLevel, KittyRating contentRating) { super(roleLevel, contentRating); } + + @Override + public String HelpText() { return LocStrings.Stub("LeaderboardInfo"); } + + // Called when the command is run! + @Override + public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res) + { + // Start by force-flushing. We need to be up-to-date. + GlobalLog.Log("Flushed database for " + DatabaseManager.instance.upkeep() + " items."); + + // Get all users associated with a guild and get their bean count + String guildID = guild.uniqueID; + List> users = new ArrayList>(); + List out = DatabaseManager.instance.scrapeGlobalForString(guildID); + + for(String item : out) + { + String val = item.replace("" + guildID, ""); + if(!val.contains("-") && val.length() > 0) + { + String userID = val; + + if(userID.length() < 1) + continue; + + // Look up in the database the user beans + String dbUserData = DatabaseManager.instance.globalGetRemoteValue(guildID + userID); + + // Leverage the fact that the user is automatically read and parsed when constructed. + Long parsedBeans = KittyUser.parseBeans(KittyUser.prepareFromString(dbUserData)); + + // If we were only using cached users, we would use this. However, we're not. + // KittyUser cachedUser = ObjectBuilderFactory.getCachedUser(guildID, userID); + users.add(new Pair(parsedBeans, userID)); + } + } + + // Sort users by beans + users.sort((pair1, pair2) -> { + long difference = pair2.First - pair1.First; + + if(difference < Integer.MIN_VALUE) + return Integer.MIN_VALUE; + + if(difference > Integer.MAX_VALUE) + return Integer.MAX_VALUE; + + return (int)difference; + }); + + GlobalLog.Log(LogFilter.Command, "Sorted through " + users.size() + " KittyUsers for leaderboard purposes."); + + // Configure output + final int listSize = 10; + KittyEmbed embed = new KittyEmbed(); + embed.color = Config.ColorDefault; + embed.title = LocStrings.Stub("LeaderboardTitle"); + embed.descriptionText = ""; + + for(int i = 0; i < listSize && i < users.size(); ++i) + { + // Only now that we've sorted do we do the full construction and caching of users + Pair userPair = users.get(i); + KittyUser cachedUser = ObjectBuilderFactory.getKittyUser(guildID, userPair.Second); + + embed.descriptionText += "**" + (i + 1) + ":** " + userPair.First + " - " + cachedUser.name; + embed.descriptionText += "\n"; + } + + // Write out embed result + res.CallEmbed(embed); + } +} diff --git a/src/core/Config.java b/src/core/Config.java new file mode 100644 index 0000000..b202650 --- /dev/null +++ b/src/core/Config.java @@ -0,0 +1,8 @@ +package core; + +import java.awt.Color; + +public final class Config +{ + public static final Color ColorDefault = new Color(7*16, 8*16, 9*16); // A slate-grey +} diff --git a/src/core/DatabaseDriverKeyValue.java b/src/core/DatabaseDriverKeyValue.java index df8032e..01e63c1 100644 --- a/src/core/DatabaseDriverKeyValue.java +++ b/src/core/DatabaseDriverKeyValue.java @@ -2,6 +2,8 @@ package core; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; import network.JDBCDriver; import network.JDBCDriverSQLite; @@ -72,7 +74,7 @@ public class DatabaseDriverKeyValue // Creates a key. The key will be created if it doesn't exist, and the default value returned. public String CreateGetKey(String key) { - GlobalLog.Log(LogFilter.Database, "CreateGeyKey: key-" + key); + GlobalLog.Log(LogFilter.Database, "CreateGetKey: " + key); if(HasKey(key)) { @@ -119,6 +121,30 @@ public class DatabaseDriverKeyValue GlobalLog.Log(LogFilter.Database, "CreateKey status: " + status); } + // Get all keys containing a substring + public List GetKeysWith(String keySubstring) + { + String command = "SELECT * FROM " + tableName + " WHERE " + keyColumnName + " like ?"; + ResultSet result = driver.ExecuteReturningStatement(JDBCStatementType.Select, command, new String[] { "%" + keySubstring + "%" }); + + List keys = new ArrayList(); + + try + { + while(result.next()) + { + String key = (String) result.getObject(1); + keys.add(key); + } + } + catch (SQLException e) + { + e.printStackTrace(); + } + + return keys; + } + // Transforms a result into a string if possible. private String ResultAsString(ResultSet rs, String key) { diff --git a/src/core/DatabaseManager.java b/src/core/DatabaseManager.java index c4a12c1..4649e7f 100644 --- a/src/core/DatabaseManager.java +++ b/src/core/DatabaseManager.java @@ -1,6 +1,7 @@ package core; import java.util.Date; +import java.util.List; import java.util.Vector; import utils.GlobalLog; import utils.LogFilter; @@ -128,6 +129,10 @@ public class DatabaseManager } } + public List scrapeGlobalForString(String substring) + { + return globalDataDriver.GetKeysWith(substring); + } ///////////////// // Global Data // @@ -141,7 +146,8 @@ public class DatabaseManager } } - private String globalGetRemoteValue(String key) + // You can get values, but not modify them. + public String globalGetRemoteValue(String key) { synchronized(globalDataDriver) { @@ -170,7 +176,8 @@ public class DatabaseManager } } - private String characterGetRemoteValue(String key) + // You can get values, but not modify them. + public String characterGetRemoteValue(String key) { synchronized(characterDataDriver) { diff --git a/src/core/ObjectBuilderFactory.java b/src/core/ObjectBuilderFactory.java index ec55748..f26367b 100644 --- a/src/core/ObjectBuilderFactory.java +++ b/src/core/ObjectBuilderFactory.java @@ -4,12 +4,23 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.concurrent.Semaphore; + +import javax.security.auth.login.LoginException; + import commands.*; import core.lua.PluginManager; import dataStructures.*; +import main.Main; +import net.dv8tion.jda.core.AccountType; +import net.dv8tion.jda.core.JDA; +import net.dv8tion.jda.core.JDABuilder; import net.dv8tion.jda.core.entities.Emote; +import net.dv8tion.jda.core.entities.Game; +import net.dv8tion.jda.core.entities.Guild; import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.User; import net.dv8tion.jda.core.events.message.guild.GuildMessageReceivedEvent; +import offline.Ref; import utils.AdminControl; import utils.GlobalLog; import utils.LogFilter; @@ -53,6 +64,7 @@ public class ObjectBuilderFactory // TODO: Investigate using 'synchronized' instead potentially private static boolean hasInitialized; private static Semaphore initMutex = new Semaphore(1); + private static JDA kitty; // This is it, this is how the lazy init starts! private static void LazyInit() @@ -264,6 +276,7 @@ public class ObjectBuilderFactory return user; } + // TODO: Cleanup public static KittyUser ExtractUserByJDAUser(String guildID, String name, String userID, String avatarID, String discordID) { LazyInit(); @@ -293,7 +306,9 @@ public class ObjectBuilderFactory return user; } - public static KittyUser getCachedUser(String guildID, String userID) + // There is some redundant lookup occurring. If the user isn't cached yet, then we construct them and cache them. + // The assumption is made that the user does, in fact, exist. + public static KittyUser getKittyUser(String guildID, String userID) { String uid = guildID + userID; KittyUser user = null; @@ -301,6 +316,17 @@ public class ObjectBuilderFactory { user = userCache.get(uid); } + + if(user == null) + { + Guild jdaGuild = kitty.getGuildById(guildID); + Member jdaMember = jdaGuild.getMemberById(userID); + User jdaUser = jdaMember.getUser(); + + user = ExtractUserByJDAUser(guildID, jdaMember.getNickname(), jdaUser.getId(), jdaUser.getAvatarUrl(), jdaUser.getId()); + updateUser(user, jdaMember); + } + return user; } @@ -318,6 +344,17 @@ public class ObjectBuilderFactory // Construction Methods // ////////////////////////// + public static KittyCore ConstructKittyCore() throws LoginException, InterruptedException + { + LazyInit(); + + kitty = new JDABuilder(AccountType.BOT).setToken(Ref.TestToken).buildBlocking(); + kitty.getPresence().setGame(Game.playing("with digital yarn")); + kitty.addEventListener(new Main()); + + return new KittyCore(kitty); + } + // Default construction of the command manager. In order to remotely resolve command enabling // and disabling, what we do is construct the commands with a localized pair that is checked against // the CommandEnabler object passed in. In theory, we could have multiple CommandManagers, tho we can @@ -386,6 +423,7 @@ public class ObjectBuilderFactory manager.Register(LocCommands.Stub("crouton"), new CommandCrouton(KittyRole.General, KittyRating.Safe)); manager.Register(LocCommands.Stub("benchmark, bench"), new CommandBenchmark(KittyRole.General, KittyRating.Safe)); manager.Register(LocCommands.Stub("rafflejoin"), new CommandRaffleJoin(KittyRole.General, KittyRating.Safe)); + manager.Register(LocCommands.Stub("leaderboard"), new CommandLeaderboard(KittyRole.General, KittyRating.Safe)); return manager; } diff --git a/src/core/RPManager.java b/src/core/RPManager.java index e3d64e2..f56be78 100644 --- a/src/core/RPManager.java +++ b/src/core/RPManager.java @@ -6,9 +6,7 @@ import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.HashMap; import java.util.Map.Entry; - import dataStructures.*; -import net.dv8tion.jda.core.JDA; import utils.GlobalLog; import utils.LogFilter; @@ -57,7 +55,7 @@ public class RPManager return log; } - public static void Upkeep(JDA kitty) + public static void Upkeep(KittyCore kitty) { Response res = new Response(null, kitty); String reminder = ""; diff --git a/src/dataStructures/KittyCore.java b/src/dataStructures/KittyCore.java new file mode 100644 index 0000000..ff9e13d --- /dev/null +++ b/src/dataStructures/KittyCore.java @@ -0,0 +1,13 @@ +package dataStructures; + +import net.dv8tion.jda.core.JDA; + +public class KittyCore +{ + public final JDA jda; + + public KittyCore(JDA kitty) + { + this.jda = kitty; + } +} diff --git a/src/dataStructures/KittyUser.java b/src/dataStructures/KittyUser.java index 74e0179..538c818 100644 --- a/src/dataStructures/KittyUser.java +++ b/src/dataStructures/KittyUser.java @@ -64,29 +64,46 @@ public class KittyUser extends DatabaseTrackedObject return beans + "," + role.getValue(); } + public static String[] prepareFromString(String string) + { + return string.split(","); + } + + public static long parseBeans(String[] prepared) + { + return Integer.parseInt(prepared[0]); + } + + public static KittyRole parseRole(String[] prepared) + { + return KittyRole.valueOf(Integer.parseInt(prepared[1])).get(); + } + @Override public void DeSerialzie(String string) { try { - String[] strings = string.split(","); - beans = Integer.parseInt(strings[0]); - if(strings.length > 1) - { - role = KittyRole.valueOf(Integer.parseInt(strings[1])).get(); - } - else + String[] strings = prepareFromString(string); + if(strings.length < 2) { GlobalLog.Log(LogFilter.Database, "Upgrading user " + name + " to include 'role' in DB"); + // Mark ourselves dirty to re-write the role information stored in the user. // Just uses defaults from earlier again. MarkDirty(); } + else + { + beans = parseBeans(strings); + role = parseRole(strings); + } } catch (NumberFormatException e) { GlobalLog.Warn(LogFilter.Database, "Invalid user data for user " + name + "! " + "Starting over at 0 beans with a general role!"); + // We don't need to specify the role at this point because it is set at this point. // We use what the user was created with whatever defaults were in the factory. // Beans are maintained too, just in case there's some in cache. diff --git a/src/dataStructures/Response.java b/src/dataStructures/Response.java index 8c070cb..012130b 100644 --- a/src/dataStructures/Response.java +++ b/src/dataStructures/Response.java @@ -2,6 +2,7 @@ package dataStructures; import java.io.File; import java.io.InputStream; + import net.dv8tion.jda.core.JDA; import net.dv8tion.jda.core.entities.TextChannel; import net.dv8tion.jda.core.events.message.guild.*; @@ -16,15 +17,15 @@ public class Response { // Variables private GuildMessageReceivedEvent event; - private JDA kitty; private final int discordMessageMax = 2000; private final int kittyMessageMax = 1950; + private JDA kitty; // Constructor - public Response(GuildMessageReceivedEvent event, JDA kitty) + public Response(GuildMessageReceivedEvent event, KittyCore kitty) { this.event = event; - this.kitty = kitty; + this.kitty = kitty.jda; } // Builds a nicely formatted embedded message based on information provided diff --git a/src/dataStructures/UserInput.java b/src/dataStructures/UserInput.java index 00e950c..063c6a6 100644 --- a/src/dataStructures/UserInput.java +++ b/src/dataStructures/UserInput.java @@ -93,7 +93,7 @@ public class UserInput KittyUser [] KittyMentions = new KittyUser[JDAMentions.size()]; for(int i = 0; i < KittyMentions.length; i ++) { - KittyMentions [i] = ObjectBuilderFactory.getCachedUser(event.getGuild().getId(), JDAMentions.get(i).getUser().getId()); + KittyMentions [i] = ObjectBuilderFactory.getKittyUser(event.getGuild().getId(), JDAMentions.get(i).getUser().getId()); } return KittyMentions; } diff --git a/src/main/Main.java b/src/main/Main.java index aa7608d..8969e81 100644 --- a/src/main/Main.java +++ b/src/main/Main.java @@ -6,6 +6,7 @@ import core.benchmark.BenchmarkManager; import core.lua.PluginManager; import core.lua.PluginUser; import dataStructures.KittyChannel; +import dataStructures.KittyCore; import dataStructures.KittyGuild; import dataStructures.KittyRole; import dataStructures.KittyUser; @@ -27,7 +28,7 @@ import java.util.*; public class Main extends ListenerAdapter { // Variables and bot specific objects - private static JDA kitty; // TODO: Wrap + private static KittyCore kittyCore; private static DatabaseManager databaseManager; private static CommandEnabler commandEnabler; private static CommandManager commandManager; @@ -48,9 +49,7 @@ public class Main extends ListenerAdapter pluginManager = ObjectBuilderFactory.ConstructPluginManager(); // Bot startup - kitty = new JDABuilder(AccountType.BOT).setToken(Ref.TestToken).buildBlocking(); - kitty.getPresence().setGame(Game.playing("with digital yarn")); - kitty.addEventListener(new Main()); + kittyCore = ObjectBuilderFactory.ConstructKittyCore(); } // When a message is sent in a server that kitty is in, this is what's called. @@ -67,7 +66,7 @@ public class Main extends ListenerAdapter KittyChannel channel = ObjectBuilderFactory.ExtractChannel(event); // Specialized uncached objects - Response response = new Response(event, kitty); + Response response = new Response(event, kittyCore); UserInput input = new UserInput(event, guild); // Tweak object construction as necessary @@ -106,6 +105,6 @@ public class Main extends ListenerAdapter } // Run any upkeep in post we need to - Superintendent.PerCommandUpkeepPost(kitty, databaseManager); + Superintendent.PerCommandUpkeepPost(kittyCore, databaseManager); } } \ No newline at end of file diff --git a/src/main/Superintendent.java b/src/main/Superintendent.java index b7b19f4..f678058 100644 --- a/src/main/Superintendent.java +++ b/src/main/Superintendent.java @@ -8,12 +8,12 @@ import core.LocStrings; import core.RPManager; import core.Stats; import dataStructures.KittyChannel; +import dataStructures.KittyCore; import dataStructures.KittyGuild; import dataStructures.KittyRole; import dataStructures.KittyUser; import dataStructures.Response; import dataStructures.UserInput; -import net.dv8tion.jda.core.JDA; import net.dv8tion.jda.core.events.message.guild.GuildMessageReceivedEvent; import offline.Ref; @@ -87,7 +87,7 @@ public class Superintendent // This is for stuff that we need to do on a regular basis, but don't // necessarily want running at all points in time. // Happens just after the command / plugin runs. - public static boolean PerCommandUpkeepPost(JDA bot, DatabaseManager databaseManager) + public static boolean PerCommandUpkeepPost(KittyCore kittyCore, DatabaseManager databaseManager) { // Upkeep database lazily on occasion if(delayTimerCurrent.decrementAndGet() < 0) @@ -97,7 +97,7 @@ public class Superintendent } // Update command-specific - RPManager.Upkeep(bot); + RPManager.Upkeep(kittyCore); return true; }