diff --git a/build.gradle b/build.gradle index 536d095..b9aa486 100644 --- a/build.gradle +++ b/build.gradle @@ -12,12 +12,23 @@ jar { } group 'com.sidpatchy' -version '3.3.3' +version '3.4.0-SNAPSHOT' + +processResources { + filesMatching('**/build.properties') { + expand( + version: project.version, + buildDate: new Date().format("yyyy-MM-dd HH:mm:ss") + ) + } +} -sourceCompatibility = 17 -targetCompatibility = 17 +kotlin { + jvmToolchain(17) +} repositories { + mavenLocal() mavenCentral() maven { url 'https://m2.dv8tion.net/releases' } maven { url 'https://jitpack.io' } @@ -27,7 +38,7 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.1' - implementation 'com.github.Sidpatchy:Robin:2.0.0' + implementation 'com.github.Sidpatchy:Robin:2.2.4' implementation 'org.javacord:javacord:3.8.0' @@ -40,7 +51,8 @@ dependencies { implementation 'org.yaml:snakeyaml:2.0' implementation 'org.json:json:20231013' - implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.16.1' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.18.0' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.18.0' } diff --git a/code_of_conduct.md b/code_of_conduct.md new file mode 100644 index 0000000..ae45e3f --- /dev/null +++ b/code_of_conduct.md @@ -0,0 +1,134 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +`conduct@sidpatchy.com` or `@sidpatchy` on the Discord. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations + diff --git a/src/main/java/com/sidpatchy/clairebot/API/APIUser.java b/src/main/java/com/sidpatchy/clairebot/API/APIUser.java index d86d23c..5331c7c 100644 --- a/src/main/java/com/sidpatchy/clairebot/API/APIUser.java +++ b/src/main/java/com/sidpatchy/clairebot/API/APIUser.java @@ -15,8 +15,8 @@ import java.net.URLConnection; import java.util.ArrayList; import java.util.Base64; +import java.util.List; import java.util.Map; -import java.util.stream.Collectors; public class APIUser { private final String userID; @@ -72,72 +72,67 @@ public String getLanguage() { * * @return the value of pointsGuildID */ - public ArrayList getPointsGuildID() { - return (ArrayList) user.getList("pointsGuildID") - .stream() - .map(Object::toString) - .collect(Collectors.toList()); + public List getPointsGuildID() { + return user.getList("pointsGuildID", String.class); } /** * * @return */ - public ArrayList getPointsMessages() { - return (ArrayList) user.getList("pointsMessages") - .stream() - .filter(Integer.class::isInstance) - .map(Integer.class::cast) - .collect(Collectors.toList()); + public List getPointsMessages() { + return user.getList("pointsMessages", Integer.class); } - public ArrayList getPointsVoiceChat() { - return (ArrayList) user.getList("pointsVoiceChat") - .stream() - .filter(Integer.class::isInstance) - .map(Integer.class::cast) - .collect(Collectors.toList()); + public List getPointsVoiceChat() { + return user.getList("pointsVoiceChat", Integer.class); } public void createUser(String accentColour, String language, - ArrayList pointsGuildID, - ArrayList pointsMessages, - ArrayList pointsVoiceChat) throws IOException { + List pointsGuildID, + List pointsMessages, + List pointsVoiceChat) throws IOException { POST post = new POST(); post.postToURL(Main.getApiPath() + "api/v1/user/", userConstructor(accentColour, language, pointsGuildID, pointsMessages, pointsVoiceChat)); } public void createUserWithDefaults() { - Map defaults = Main.getUserDefaults(); + RobinConfiguration.RobinSection defaults = new RobinConfiguration.RobinSection(Main.getUserDefaults()); try { createUser( - (String) defaults.get("accentColour"), - (String) defaults.get("language"), - (ArrayList) defaults.get("pointsGuildID"), - (ArrayList) defaults.get("pointsMessages"), - (ArrayList) defaults.get("pointsVoiceChat") + defaults.getString("accentColour"), + defaults.getString("language"), + defaults.getList("pointsGuildID", String.class), + defaults.getList("pointsMessages", Integer.class), + defaults.getList("pointsVoiceChat", Integer.class) ); } - // top 10 bad ideas #1 - catch (Exception ignored) { - ignored.printStackTrace(); - Main.getLogger().error("Unable to create user with defaults."); + catch (Exception e) { + Main.getLogger().error("Unable to create user with defaults.", e); } createNewWithDefaults = false; // prevent recursion if ClaireData goes down. } public void updateUser(String accentColour, String language, - ArrayList pointsGuildID, - ArrayList pointsMessages, - ArrayList pointsVoiceChat) throws IOException { + List pointsGuildID, + List pointsMessages, + List pointsVoiceChat) throws IOException { + // Add null check and fallback for language + if (language == null) { + new Exception("Language null origin trace").printStackTrace(); + } + PUT put = new PUT(); - put.putToURL(Main.getApiPath() + "api/v1/user/" + userID, userConstructor(accentColour, language, pointsGuildID, pointsMessages, pointsVoiceChat)); + put.putToURL(Main.getApiPath() + "api/v1/user/" + userID, + userConstructor(accentColour, language, pointsGuildID, pointsMessages, pointsVoiceChat)); } public void updateUserColour(String accentColour) throws IOException { + // Ensure that the values for the getters below are populated before querying. + getUser(); updateUser(accentColour, getLanguage(), getPointsGuildID(), @@ -147,6 +142,8 @@ public void updateUserColour(String accentColour) throws IOException { } public void updateUserLanguage(String languageString) throws IOException { + // Ensure that the values for the getters below are populated before querying. + getUser(); updateUser(getAccentColour(), languageString, getPointsGuildID(), @@ -155,14 +152,16 @@ public void updateUserLanguage(String languageString) throws IOException { } public void updateUserPointsGuildID(String guildID, Integer newPoints) throws IOException { - updateUserPointsGuildID((ArrayList) LevelingTools.updateUserPoints(userID, guildID, newPoints)); + updateUserPointsGuildID(LevelingTools.updateUserPoints(userID, guildID, newPoints)); } public void updateUserPointsGuildID(Map guildPointsToUpdate) throws IOException { - updateUserPointsGuildID((ArrayList) LevelingTools.updateUserPoints(userID, guildPointsToUpdate)); + updateUserPointsGuildID(LevelingTools.updateUserPoints(userID, guildPointsToUpdate)); } - public void updateUserPointsGuildID(ArrayList pointsGuildID) throws IOException { + public void updateUserPointsGuildID(List pointsGuildID) throws IOException { + // Ensure that the values for the getters below are populated before querying. + getUser(); updateUser(getAccentColour(), getLanguage(), pointsGuildID, @@ -187,18 +186,18 @@ public void deleteUser() throws IOException { */ public String userConstructor(String accentColour, String language, - ArrayList pointsGuildID, - ArrayList pointsMessages, - ArrayList pointsVoiceChat) { + List pointsGuildID, + List pointsMessages, + List pointsVoiceChat) { ObjectMapper objectMapper = new ObjectMapper(); ObjectNode userNode = objectMapper.createObjectNode(); userNode.put("userID", userID); userNode.put("accentColour", accentColour); userNode.put("language", language); - userNode.put("pointsGuildID", objectMapper.valueToTree(pointsGuildID)); - userNode.put("pointsMessages", objectMapper.valueToTree(pointsMessages)); - userNode.put("pointsVoiceChat", objectMapper.valueToTree(pointsVoiceChat)); + userNode.set("pointsGuildID", objectMapper.valueToTree(pointsGuildID)); + userNode.set("pointsMessages", objectMapper.valueToTree(pointsMessages)); + userNode.set("pointsVoiceChat", objectMapper.valueToTree(pointsVoiceChat)); return userNode.toString(); } @@ -235,4 +234,4 @@ private void fixUserPointsGuildID() throws IOException { updateUserPointsGuildID((ArrayList) defaults.get("pointsGuildID")); } } -} +} \ No newline at end of file diff --git a/src/main/java/com/sidpatchy/clairebot/Clockwork.java b/src/main/java/com/sidpatchy/clairebot/Clockwork.java index d9970b6..d980c3f 100644 --- a/src/main/java/com/sidpatchy/clairebot/Clockwork.java +++ b/src/main/java/com/sidpatchy/clairebot/Clockwork.java @@ -40,7 +40,7 @@ class Helper extends TimerTask { public void run() { try { config.loadFromURL("https://raw.githubusercontent.com/nikolaischunk/discord-phishing-links/main/domain-list.json"); - Clockwork.setPhishingDomains(config.getList("domains") + Clockwork.setPhishingDomains(config.getList("domains", String.class) .stream() .map(Object::toString) .collect(Collectors.toList())); diff --git a/src/main/java/com/sidpatchy/clairebot/Commands.java b/src/main/java/com/sidpatchy/clairebot/Commands.java new file mode 100644 index 0000000..81c629c --- /dev/null +++ b/src/main/java/com/sidpatchy/clairebot/Commands.java @@ -0,0 +1,181 @@ +package com.sidpatchy.clairebot; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.sidpatchy.Robin.Discord.Command; + +import java.util.List; +import java.util.Objects; + +/** + * Represents ALL commands within ClaireBot. Initialized using the CommandFactory from Robin >= 2.1.0. + */ +public class Commands { + private Command avatar; + private Command config; + private Command eightball; + private Command help; + private Command info; + private Command leaderboard; + private Command level; + private Command poll; + private Command quote; + private Command request; + private Command santa; + private Command server; + private Command user; + @JsonProperty("config-revision") + private String configRevision; + + /** + * Retrieves a list of all available commands. + * + * @return a list containing all Command objects. + */ + public List getAllCommands() { + return List.of( + avatar, config, eightball, help, info, leaderboard, + level, poll, quote, request, santa, server, user + ); + } + + public Command getAvatar() { + validateCommand(avatar); + return avatar; + } + + public Command getConfig() { + validateCommand(config); + return config; + } + + public Command getEightball() { + validateCommand(eightball); + return eightball; + } + + public Command getHelp() { + validateCommand(help); + return help; + } + + public Command getInfo() { + validateCommand(info); + return info; + } + + public Command getLeaderboard() { + validateCommand(leaderboard); + return leaderboard; + } + + public Command getLevel() { + validateCommand(level); + return level; + } + + public Command getPoll() { + validateCommand(poll); + return poll; + } + + public Command getQuote() { + validateCommand(quote); + return quote; + } + + public Command getRequest() { + validateCommand(request); + return request; + } + + public Command getSanta() { + validateCommand(santa); + return santa; + } + + public Command getServer() { + validateCommand(server); + return server; + } + + public Command getUser() { + validateCommand(user); + return user; + } + + public String getConfigRevision() { + return configRevision.isEmpty() ? null : configRevision; + } + + protected void validateCommand(Command command) { + Objects.requireNonNull(command, "Command cannot be null"); + Objects.requireNonNull(command.getName(), "Command name cannot be null"); + Objects.requireNonNull(command.getUsage(), "Command usage cannot be null"); + Objects.requireNonNull(command.getHelp(), "Command help cannot be null"); + + if (command.getName().isEmpty() || command.getUsage().isEmpty() || command.getHelp().isEmpty()) { + throw new IllegalArgumentException("Command name or usage cannot be empty"); + } + + // If command overview is null, set it to command help + if (command.getOverview() == null || command.getOverview().isEmpty()) { + command.setOverview(command.getHelp()); + } + } + + public void setAvatar(Command avatar) { + this.avatar = avatar; + } + + public void setConfig(Command config) { + this.config = config; + } + + public void setEightball(Command eightball) { + this.eightball = eightball; + } + + public void setHelp(Command help) { + this.help = help; + } + + public void setInfo(Command info) { + this.info = info; + } + + public void setLeaderboard(Command leaderboard) { + this.leaderboard = leaderboard; + } + + public void setLevel(Command level) { + this.level = level; + } + + public void setPoll(Command poll) { + this.poll = poll; + } + + public void setQuote(Command quote) { + this.quote = quote; + } + + public void setRequest(Command request) { + this.request = request; + } + + public void setSanta(Command santa) { + this.santa = santa; + } + + public void setServer(Command server) { + this.server = server; + } + + public void setUser(Command user) { + this.user = user; + } + + public void setConfigRevision(String configRevision) { + this.configRevision = configRevision; + } +} diff --git a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/EightBallEmbed.java b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/EightBallEmbed.java index 3d0a108..932198f 100644 --- a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/EightBallEmbed.java +++ b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/EightBallEmbed.java @@ -1,34 +1,39 @@ package com.sidpatchy.clairebot.Embed.Commands.Regular; +import com.sidpatchy.clairebot.Lang.LanguageManager; import com.sidpatchy.clairebot.Main; import org.javacord.api.entity.message.embed.EmbedBuilder; import org.javacord.api.entity.user.User; -import java.util.ArrayList; +import java.util.List; import java.util.Random; public class EightBallEmbed { - public static EmbedBuilder getEightBall(String query, User author) { + public static EmbedBuilder getEightBall(LanguageManager languageManager, String query, User author) { - ArrayList eightBall = (ArrayList) Main.getEightBall(); - ArrayList eightBallRigged = (ArrayList) Main.getEightBallRigged(); - ArrayList onTopTriggers = (ArrayList) Main.getOnTopTriggers(); + // Language Strings + List eightBall = languageManager.getLocalizedList("ClaireLang.Embed.Commands.Regular.EightBallEmbed.8bResponses"); + List eightBallRigged = languageManager.getLocalizedList("ClaireLang.Embed.Commands.Regular.EightBallEmbed.8bRiggedResponses"); + List onTopTriggers = languageManager.getLocalizedList("ClaireLang.Embed.Commands.Regular.EightBallEmbed.OnTopTriggers"); + String ateBallLanguageString = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.EightBallEmbed.8ball"); + + Main.getLogger().error(onTopTriggers.toString()); Random random = new Random(); int rand = random.nextInt(eightBall.size()); String response = eightBall.get(rand); // Overwrite response if ClaireBot on top trigger - for (String s : onTopTriggers) { - if (query.toUpperCase().contains(s.toUpperCase())) { + for (String trigger : onTopTriggers) { + if (query.toUpperCase().contains(trigger.toUpperCase())) { rand = random.nextInt(eightBallRigged.size()); response = eightBallRigged.get(rand); } } return new EmbedBuilder() .setColor(Main.getColor(author.getIdAsString())) - .setAuthor("8ball") + .setAuthor(ateBallLanguageString) .addField(query, response) .setFooter(author.getDiscriminatedName(), author.getAvatar()); } diff --git a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/HelpEmbed.java b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/HelpEmbed.java index 4dfebd1..92da24a 100644 --- a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/HelpEmbed.java +++ b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/HelpEmbed.java @@ -1,63 +1,76 @@ package com.sidpatchy.clairebot.Embed.Commands.Regular; -import com.sidpatchy.Robin.Discord.ParseCommands; +import com.sidpatchy.Robin.Discord.Command; +import com.sidpatchy.clairebot.Commands; import com.sidpatchy.clairebot.Embed.ErrorEmbed; +import com.sidpatchy.clairebot.Lang.ContextManager; +import com.sidpatchy.clairebot.Lang.LanguageManager; import com.sidpatchy.clairebot.Main; import org.javacord.api.entity.message.embed.EmbedBuilder; import java.io.FileNotFoundException; -import java.util.Arrays; import java.util.HashMap; -import java.util.List; public class HelpEmbed { - private static final ParseCommands commands = new ParseCommands(Main.getCommandsFile()); - public static EmbedBuilder getHelp(String commandName, String userID) throws FileNotFoundException { - List regularCommandsList = Arrays.asList("8ball", "avatar", "help", "info", "leaderboard", "level", "poll", "quote", "request", "server", "user", "config", "santa"); + private static final Commands commands = Main.getCommands(); + private static String commandsLangString; + private static String usageLangString; - // Create HashMaps for help command - HashMap> allCommands = new HashMap>(); - HashMap> regularCommands = new HashMap>(); + public static EmbedBuilder getHelp(LanguageManager languageManager, String commandName, String userID) throws FileNotFoundException { + // Language Strings + commandsLangString = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.HelpEmbed.Commands"); + usageLangString = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.HelpEmbed.Usage"); + languageManager.addContext(ContextManager.ContextType.GENERIC, "commandname", commandName); - for (String s : regularCommandsList) { - regularCommands.put(s, commands.get(s)); - } + HashMap allCommands = new HashMap<>(); + HashMap regularCommands = new HashMap<>(); - allCommands.putAll(regularCommands); + for (Command command : commands.getAllCommands()) { + allCommands.put(command.getName(), command); + regularCommands.put(command.getName(), command); + } - // Commands list if (commandName.equalsIgnoreCase("help")) { - StringBuilder glob = new StringBuilder("```"); - for (String s : regularCommandsList) { - if (glob.toString().equalsIgnoreCase("```")) { - glob.append(commands.getCommandName(s)); - } else { - glob.append(", ") - .append(commands.getCommandName(s)); - } - } - glob.append("```"); + return buildHelpEmbed(userID, regularCommands); + } else { + return buildCommandDetailEmbed(commandName, userID, allCommands, languageManager); + } + } - EmbedBuilder embed = new EmbedBuilder() - .setColor(Main.getColor(userID)) - .addField("Commands", glob.toString(), false); + private static EmbedBuilder buildHelpEmbed(String userID, HashMap regularCommands) { + StringBuilder commandsList = new StringBuilder("```"); - return embed; - } - // Command details - else { - if (allCommands.get(commandName) == null) { - String errorCode = Main.getErrorCode("help_command"); - Main.getLogger().error("Unable to locate command \"" + commandName + "\" for help command. Error code: " + errorCode); - return ErrorEmbed.getError(errorCode); - } else { - return new EmbedBuilder() - .setColor(Main.getColor(userID)) - .setAuthor(commandName.toUpperCase()) - .setDescription(allCommands.get(commandName).get("help")) - .addField("Command", "Usage\n" + "```" + allCommands.get(commandName).get("usage") + "```"); + for (String commandName : regularCommands.keySet()) { + if (commandsList.length() > 3) { + commandsList.append(", "); } + commandsList.append(commandName); + } + + commandsList.append("```"); + + return new EmbedBuilder() + .setColor(Main.getColor(userID)) + .addField(commandsLangString, commandsList.toString(), false); + } + + private static EmbedBuilder buildCommandDetailEmbed(String commandName, String userID, HashMap allCommands, LanguageManager languageManager) { + Command command = allCommands.get(commandName); + + if (command == null) { + String errorCode = Main.getErrorCode("help_command"); + languageManager.addContext(ContextManager.ContextType.GENERIC, "errorcode", errorCode); + String errorLangString = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.HelpEmbed.Error"); + Main.getLogger().error(errorLangString); + return ErrorEmbed.getError(errorCode); + } else { + return new EmbedBuilder() + .setColor(Main.getColor(userID)) + .setAuthor(commandName.toUpperCase()) + .setDescription(command.getOverview().isEmpty() ? command.getHelp() : command.getOverview()) + .addField(commandsLangString, usageLangString + "\n```" + command.getUsage() + "```"); } } } + diff --git a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/InfoEmbed.java b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/InfoEmbed.java index b48ee0a..bcc244b 100644 --- a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/InfoEmbed.java +++ b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/InfoEmbed.java @@ -1,20 +1,46 @@ package com.sidpatchy.clairebot.Embed.Commands.Regular; +import com.sidpatchy.clairebot.Lang.ContextManager; +import com.sidpatchy.clairebot.Lang.LanguageManager; import com.sidpatchy.clairebot.Main; import org.apache.commons.lang3.time.DurationFormatUtils; import org.javacord.api.entity.message.embed.EmbedBuilder; import org.javacord.api.entity.user.User; public class InfoEmbed { - public static EmbedBuilder getInfo(User author) { - String timeSinceStart = DurationFormatUtils.formatDurationWords(System.currentTimeMillis() - Main.getStartMillis(), true, false); + public static EmbedBuilder getInfo(LanguageManager languageManager, User author) { return new EmbedBuilder() .setColor(Main.getColor(author.getIdAsString())) - .addField("Need Help?", "You can get help by creating an issue on our [GitHub](https://github.com/Sidpatchy/ClaireBot/issues) or by joining our [support server](https://discord.gg/NwQUkZQ)", true) - .addField("Add Me to a Server", "Adding me to a server is simple, all you have to do is click [here](https://invite.clairebot.net)", true) - .addField("GitHub", "ClaireBot is open source, that means you can view all of its code! Check out its [GitHub!](https://github.com/Sidpatchy/ClaireBot)", true) - .addField("Server Count", "I have enlightened **" + Main.getApi().getServers().size() + "** servers.", true) - .addField("Version", "I am running ClaireBot **v3.3.2**, released on **2024-08-22**", true) - .addField("Uptime", "Started on " + "\n*" + timeSinceStart + "*", true); + .addField( + languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.InfoEmbed.NeedHelp"), + languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.InfoEmbed.NeedHelpDetails"), + true + ) + .addField( + languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.InfoEmbed.AddToServer"), + languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.InfoEmbed.AddToServerDetails"), + true + ) + .addField( + languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.InfoEmbed.GitHub"), + languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.InfoEmbed.GitHubDetails"), + true + ) + .addField( + languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.InfoEmbed.ServerCount"), + languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.InfoEmbed.ServerCountDetails"), + true + ) + .addField( + languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.InfoEmbed.Version"), + languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.InfoEmbed.VersionDetails"), + true + ) + .addField( + languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.InfoEmbed.Uptime"), + languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.InfoEmbed.UptimeValue"), + true + ); } -} \ No newline at end of file +} + diff --git a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/LeaderboardEmbed.java b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/LeaderboardEmbed.java index 76e9080..5717208 100644 --- a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/LeaderboardEmbed.java +++ b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/LeaderboardEmbed.java @@ -1,6 +1,7 @@ package com.sidpatchy.clairebot.Embed.Commands.Regular; import com.sidpatchy.clairebot.Embed.ErrorEmbed; +import com.sidpatchy.clairebot.Lang.LanguageManager; import com.sidpatchy.clairebot.Main; import com.sidpatchy.clairebot.Util.Leveling.LevelingTools; import org.javacord.api.entity.message.embed.EmbedBuilder; @@ -15,7 +16,10 @@ public class LeaderboardEmbed { - public static EmbedBuilder getLeaderboard(Server server, User author) { + public static EmbedBuilder getLeaderboard(LanguageManager languageManager, Server server, User author) { + // Language Strings + String leaderboardFor = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.LeaderboardEmbed.LeaderboardForServer"); + String serverID = server.getIdAsString(); HashMap unsortedLevelMap = null; @@ -32,12 +36,14 @@ public static EmbedBuilder getLeaderboard(Server server, User author) { Map sortedLevelMap = sortMap(namedMap); EmbedBuilder embed = initializeLeaderboardEmbed(sortedLevelMap, author); - embed.setAuthor("Leaderboard for " + server.getName(), "", server.getIcon().orElse(null)); + embed.setAuthor(leaderboardFor + " " + server.getName(), "", server.getIcon().orElse(null)); return embed; } - public static EmbedBuilder getLeaderboard(String serverID, User author) { + public static EmbedBuilder getLeaderboard(LanguageManager languageManager, String serverID, User author) { + String globalLeaderboard = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.LeaderboardEmbed.GlobalLeaderboard"); + HashMap unsortedLevelMap; try { unsortedLevelMap = LevelingTools.rankUsers(serverID); @@ -52,7 +58,7 @@ public static EmbedBuilder getLeaderboard(String serverID, User author) { Map sortedLevelMap = sortMap(namedMap); EmbedBuilder embed = initializeLeaderboardEmbed(sortedLevelMap, author); - embed.setAuthor("Global Leaderboard"); + embed.setAuthor(globalLeaderboard); return embed; } diff --git a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/QuoteEmbed.java b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/QuoteEmbed.java index 6223b58..917bb29 100644 --- a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/QuoteEmbed.java +++ b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/QuoteEmbed.java @@ -1,17 +1,16 @@ package com.sidpatchy.clairebot.Embed.Commands.Regular; + import com.sidpatchy.clairebot.Embed.ErrorEmbed; +import com.sidpatchy.clairebot.Lang.LanguageManager; import com.sidpatchy.clairebot.Main; import com.sidpatchy.clairebot.Util.Cache.MessageCacheManager; -import org.javacord.api.entity.Icon; import org.javacord.api.entity.channel.TextChannel; import org.javacord.api.entity.message.Message; -import org.javacord.api.entity.message.MessageBuilder; import org.javacord.api.entity.message.embed.EmbedBuilder; import org.javacord.api.entity.message.embed.EmbedFooter; import org.javacord.api.entity.server.Server; import org.javacord.api.entity.user.User; -import java.util.List; import java.util.Random; import java.util.concurrent.CompletableFuture; @@ -25,7 +24,7 @@ public class QuoteEmbed { * @param channel the text channel where the messages are located * @return a CompletableFuture that resolves to an EmbedBuilder containing the quote */ - public static CompletableFuture getQuote(Server server, final User user, TextChannel channel) { + public static CompletableFuture getQuote(LanguageManager languageManager, Server server, final User user, TextChannel channel) { return MessageCacheManager.queryMessageCache(channel, user).thenApply(userMessages -> { if (userMessages.isEmpty()) { @@ -78,11 +77,12 @@ public static CompletableFuture getQuote(Server server, final User }); } - public static EmbedBuilder viewOriginalMessageBuilder(TextChannel channel, Message message) { + public static EmbedBuilder viewOriginalMessageBuilder(LanguageManager languageManager, TextChannel channel, Message message) { EmbedFooter footer = message.getEmbeds().get(0).getFooter().orElse(null); Message quotedMessage = message.getApi().getMessageById(footer.getText().orElse(""), channel).join(); return new EmbedBuilder() - .addField("Click to jump to the original message:", quotedMessage.getLink().toString()); + .addField(languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.QuoteEmbed.JumpToOriginal"), + quotedMessage.getLink().toString()); } } diff --git a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/SantaEmbed.java b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/SantaEmbed.java index e8a0aa8..51b2995 100644 --- a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/SantaEmbed.java +++ b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/SantaEmbed.java @@ -1,5 +1,7 @@ package com.sidpatchy.clairebot.Embed.Commands.Regular; +import com.sidpatchy.clairebot.Lang.ContextManager; +import com.sidpatchy.clairebot.Lang.LanguageManager; import com.sidpatchy.clairebot.Main; import com.sidpatchy.clairebot.Util.SantaUtils; import org.javacord.api.entity.message.MessageBuilder; @@ -14,11 +16,13 @@ public class SantaEmbed { - public static EmbedBuilder getConfirmationEmbed(User author) { + private static final String basePath = "ClaireLang.Embed.Commands.Regular.SantaEmbed"; + + public static EmbedBuilder getConfirmationEmbed(LanguageManager languageManager, User author) { return new EmbedBuilder() .setColor(Main.getColor(author.getIdAsString())) .setAuthor("SecretClaire", "", "https://github.com/Sidpatchy/ClaireBot/blob/main/img/ClaireBot-SantaHat.png?raw=true") - .setDescription("Confirmed! I've sent you a direct message. Please continue there."); + .setDescription(languageManager.getLocalizedString(basePath, "Confirmation")); } /** @@ -26,11 +30,11 @@ public static EmbedBuilder getConfirmationEmbed(User author) { * * @param role The group of users participating * @param author Author of the command. - * @param rules Rules for the exchange, seperated by \n. + * @param rules Rules for the exchange, separated by \n. * @param theme Theme for the exchange. * @return Message with components */ - public static MessageBuilder getHostMessage(Role role, User author, String rules, String theme) { + public static MessageBuilder getHostMessage(LanguageManager languageManager, Role role, User author, String rules, String theme) { Set users = role.getUsers(); Server server = role.getServer(); @@ -59,25 +63,25 @@ public static MessageBuilder getHostMessage(Role role, User author, String rules } ActionRow actionRow = ActionRow.of( - Button.primary("rules", "Add rules"), - Button.primary("theme", "Add a theme"), - Button.danger("send", "Send messages"), - Button.success("test", "Send sample"), - Button.secondary("randomize", "Re-randomize")); + Button.primary("rules", languageManager.getLocalizedString(basePath, "RulesButton")), + Button.primary("theme", languageManager.getLocalizedString(basePath, "ThemeButton")), + Button.danger("send", languageManager.getLocalizedString(basePath, "SendButton")), + Button.success("test", languageManager.getLocalizedString(basePath, "TestButton")), + Button.secondary("randomize", languageManager.getLocalizedString(basePath, "RandomizeButton"))); message.addEmbed(embed); message.addComponents(actionRow); return message; } - public static MessageBuilder getSantaMessage(Server server, User author, User giver, User receiver, String rules, String theme) { + public static MessageBuilder getSantaMessage(LanguageManager languageManager, Server server, User author, User giver, User receiver, String rules, String theme) { MessageBuilder message = new MessageBuilder(); EmbedBuilder embed = new EmbedBuilder() .setColor(Main.getColor(author.getIdAsString())) .setAuthor("SecretClaire", "", "https://github.com/Sidpatchy/ClaireBot/blob/main/img/ClaireBot-SantaHat.png?raw=true") - .setFooter("Sent by " + author.getName(), author.getAvatar()); + .setFooter(languageManager.getLocalizedString(basePath, "SentByAuthor") + " " + author.getName(), author.getAvatar()); if (!theme.isEmpty()) { embed.addField("Theme", theme, false); @@ -87,6 +91,9 @@ public static MessageBuilder getSantaMessage(Server server, User author, User gi embed.addField("Rules", rules, false); } + languageManager.addContext(ContextManager.ContextType.SANTA, "giver", giver.getDisplayName(server)); + languageManager.addContext(ContextManager.ContextType.SANTA, "receiver", receiver.getDisplayName(server)); + embed.setDescription("Ho! Ho! Ho! You have received **" + receiver.getDisplayName(server) + "** in the " + server.getName() + " Secret Santa!"); message.addEmbed(embed); diff --git a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/VotingEmbed.java b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/VotingEmbed.java index d3edca5..d037a85 100755 --- a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/VotingEmbed.java +++ b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/VotingEmbed.java @@ -41,7 +41,7 @@ public static EmbedBuilder getPoll(String commandName, String question, String d } } - if (choiceBuilder.toString().equalsIgnoreCase("")) { + if (choiceBuilder.toString().isEmpty()) { allowMultipleChoices = false; } else { @@ -57,7 +57,7 @@ else if (commandName.equalsIgnoreCase("POLL")) { embed.setAuthor(author.getDisplayName(server) + " asks:", "https://discord.com/users/" + author.getIdAsString(), author.getAvatar()); } - if (description.equalsIgnoreCase("")) { + if (description.isEmpty()) { embed.setDescription(question); } else { diff --git a/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/ClaireLang.java b/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/ClaireLang.java new file mode 100644 index 0000000..f21ac69 --- /dev/null +++ b/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/ClaireLang.java @@ -0,0 +1,20 @@ +package com.sidpatchy.clairebot.Lang.Beans.ClaireLang; + +import com.sidpatchy.clairebot.Lang.Beans.ClaireLang.Embed.Embed; + +/** + * Hello future sufferers. I'm glad we're in this together :) + *

+ * Here is the commit message that was written when this structure was devised: + *

+ * "I prefer the idea of this structure, I just don't like the implementation of this structure. + *

+ * Also considered doing one giga-class (technical term :)), but that would've been much less readable than whatever in the fuck this is. + *

+ * Probably would've been easier to understand the intent of though... I will need to write some really good javadoc if I stick with this." + *

+ * IntelliJ is angry at me for using "really good." Too bad! + */ +public class ClaireLang { + public Embed embed; +} diff --git a/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/Embed/Commands/Commands.java b/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/Embed/Commands/Commands.java new file mode 100644 index 0000000..e4d06d5 --- /dev/null +++ b/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/Embed/Commands/Commands.java @@ -0,0 +1,7 @@ +package com.sidpatchy.clairebot.Lang.Beans.ClaireLang.Embed.Commands; + +import com.sidpatchy.clairebot.Lang.Beans.ClaireLang.Embed.Commands.Regular.Regular; + +public class Commands { + public Regular regular; +} diff --git a/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/Embed/Commands/Regular/EightBall.java b/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/Embed/Commands/Regular/EightBall.java new file mode 100644 index 0000000..64fd76d --- /dev/null +++ b/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/Embed/Commands/Regular/EightBall.java @@ -0,0 +1,12 @@ +package com.sidpatchy.clairebot.Lang.Beans.ClaireLang.Embed.Commands.Regular; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class EightBall { + @JsonProperty("8bResponses") + public List eightballResponses; + @JsonProperty("8bRiggedResponses") + public List eightBallRiggedResponses; +} diff --git a/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/Embed/Commands/Regular/Help.java b/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/Embed/Commands/Regular/Help.java new file mode 100644 index 0000000..4aebd42 --- /dev/null +++ b/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/Embed/Commands/Regular/Help.java @@ -0,0 +1,8 @@ +package com.sidpatchy.clairebot.Lang.Beans.ClaireLang.Embed.Commands.Regular; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Help { + @JsonProperty("Commands") + public String commands; +} diff --git a/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/Embed/Commands/Regular/Info.java b/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/Embed/Commands/Regular/Info.java new file mode 100644 index 0000000..4ea62d4 --- /dev/null +++ b/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/Embed/Commands/Regular/Info.java @@ -0,0 +1,16 @@ +package com.sidpatchy.clairebot.Lang.Beans.ClaireLang.Embed.Commands.Regular; + +public class Info { + private String needHelp; + private String needHelpDetails; + private String addToServer; + private String addToServerDetails; + private String github; + private String githubDetails; + private String serverCount; + private String servercountDetails; + private String version; + private String versionDetails; + private String uptime; + private String uptimeDetails; +} diff --git a/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/Embed/Commands/Regular/Regular.java b/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/Embed/Commands/Regular/Regular.java new file mode 100644 index 0000000..60047f1 --- /dev/null +++ b/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/Embed/Commands/Regular/Regular.java @@ -0,0 +1,9 @@ +package com.sidpatchy.clairebot.Lang.Beans.ClaireLang.Embed.Commands.Regular; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Regular { + @JsonProperty("AvatarEmbed") + public String avatarEmbed; + +} diff --git a/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/Embed/Embed.java b/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/Embed/Embed.java new file mode 100644 index 0000000..7c5f498 --- /dev/null +++ b/src/main/java/com/sidpatchy/clairebot/Lang/Beans/ClaireLang/Embed/Embed.java @@ -0,0 +1,7 @@ +package com.sidpatchy.clairebot.Lang.Beans.ClaireLang.Embed; + +import com.sidpatchy.clairebot.Commands; + +public class Embed { + public Commands commands; +} diff --git a/src/main/java/com/sidpatchy/clairebot/Lang/ContextManager.kt b/src/main/java/com/sidpatchy/clairebot/Lang/ContextManager.kt new file mode 100644 index 0000000..a6fcd84 --- /dev/null +++ b/src/main/java/com/sidpatchy/clairebot/Lang/ContextManager.kt @@ -0,0 +1,45 @@ +package com.sidpatchy.clairebot.Lang + +import org.javacord.api.entity.channel.TextChannel +import org.javacord.api.entity.message.Message +import org.javacord.api.entity.server.Server +import org.javacord.api.entity.user.User + +data class ContextManager( + val server: Server?, + val channel: TextChannel?, + val author: User?, + val user: User?, + val message: Message?, + private val dynamicData: MutableMap = mutableMapOf() +) { + // Enum to define known context types + enum class ContextType { + POLL, + SANTA, + GENERIC + } + + // Add dynamic data with type safety + fun addData(type: ContextType, key: String, value: Any) { + dynamicData["${type.name.lowercase()}.$key"] = value + } + + // Get dynamic data with type safety + @Suppress("UNCHECKED_CAST") + fun getData(type: ContextType, key: String): T? { + return dynamicData["${type.name.lowercase()}.$key"] as? T + } + + // Helper functions for specific context types + fun addPollData(pollId: String, question: String) { + addData(ContextType.POLL, "id", pollId) + addData(ContextType.POLL, "question", question) + } + + fun addSantaData(giftee: User, theme: String, rules: String) { + addData(ContextType.SANTA, "giftee", giftee) + addData(ContextType.SANTA, "theme", theme) + addData(ContextType.SANTA, "rules", rules) + } +} \ No newline at end of file diff --git a/src/main/java/com/sidpatchy/clairebot/Lang/LanguageManager.java b/src/main/java/com/sidpatchy/clairebot/Lang/LanguageManager.java new file mode 100644 index 0000000..62eb9a8 --- /dev/null +++ b/src/main/java/com/sidpatchy/clairebot/Lang/LanguageManager.java @@ -0,0 +1,195 @@ +package com.sidpatchy.clairebot.Lang; + +import com.sidpatchy.Robin.Exception.InvalidConfigurationException; +import com.sidpatchy.Robin.File.RobinConfiguration; +import com.sidpatchy.clairebot.API.APIUser; +import com.sidpatchy.clairebot.API.Guild; +import com.sidpatchy.clairebot.Main; +import org.apache.logging.log4j.Logger; +import org.javacord.api.entity.server.Server; +import org.javacord.api.entity.user.User; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +public class LanguageManager { + + private final static Logger logger = Main.getLogger(); + + private final String pathToLanguageFiles; + private final Locale fallbackLocale; + private final Server server; + private final User user; + private final ContextManager context; + private final PlaceholderHandler placeholderHandler; + + /** + * The LanguageManager class is responsible for loading and managing language files based on user preferences. + * It provides methods to retrieve localized strings and get the language file based on the user's preferred language. + *

+ * If the language manager is being used in the context of a Server, a server MUST be specified in order to conform + * to the ClaireLang and ClaireConfig specifications. It is safe to pass a null value for the server via + * ContextManager as ClaireLang will automatically interpret this as there being no server. + */ + public LanguageManager(String pathToLanguageFiles, + Locale fallbackLocale, + ContextManager context) { + this.pathToLanguageFiles = Main.getTranslationsPath(); + this.context = context; + this.fallbackLocale = fallbackLocale; + + this.server = context.getServer(); + this.user = context.getUser(); + this.placeholderHandler = new PlaceholderHandler(context); + } + + /** + * The LanguageManager class is responsible for loading and managing language files based on user preferences. + * It provides methods to retrieve localized strings and get the language file based on the user's preferred language. + *

+ * If the language manager is being used in the context of a Server, a server MUST be specified in order to conform + * to the ClaireLang and ClaireConfig specifications. It is safe to pass a null value for the server via + * ContextManager as ClaireLang will automatically interpret this as there being no server. + *

+ * This signature should only be used by ClaireBot. If you are developing a plugin, you must specify a different + * locale path unless you are referencing ClaireBot's builtin language strings. The standard path can be obtained + * through the plugin API. + */ + public LanguageManager(Locale fallbackLocale, ContextManager context) { + this.pathToLanguageFiles = Main.getTranslationsPath(); + this.context = context; + this.fallbackLocale = fallbackLocale; + + this.server = context.getServer(); + this.user = context.getUser(); + this.placeholderHandler = new PlaceholderHandler(context); + } + + /** + * Retrieves the localized string corresponding to the given key. + * + * @param key the key for the desired localized string + * @return the localized string if found, otherwise returns the key itself + * @throws IOException if an I/O error occurs while retrieving the localized string + */ + public String getLocalizedString(String key) { + RobinConfiguration languageFile = parseUserAndServerOptions(server, user); + String localizedString = languageFile.getString(key); + logger.debug(localizedString); + String rawLanguageString = localizedString != null ? localizedString : key; + + return placeholderHandler.process(rawLanguageString); + } + + /** + * Retrieves a localized string based on the provided base path and key. + * + * @param basePath the base path used to locate the language file or namespace + * @param key the key for the desired localized string + * @return the localized string if found, otherwise returns the concatenation of basePath and key + */ + public String getLocalizedString(String basePath, String key) { + return getLocalizedString(basePath + "." + key); + } + + /** + * Retrieves the localized string corresponding to the given key. + * + * @param key the key for the desired localized string + * @return the localized string if found, otherwise returns the key itself + * @throws IOException if an I/O error occurs while retrieving the localized string + */ + public List getLocalizedList(String key) { + RobinConfiguration languageFile = parseUserAndServerOptions(server, user); + List localizedList = languageFile.getList(key, String.class); + logger.debug(localizedList); + List rawLanguageString = localizedList != null ? localizedList : List.of(key); + + return placeholderHandler.process(rawLanguageString); + } + + /** + * Retrieves a localized list of strings based on the provided base path and key. + * + * @param basePath the base path used to locate the language file or namespace + * @param key the key for the desired localized list of strings + * @return a list of localized strings if found, otherwise returns a list containing the concatenation of basePath and key + */ + public List getLocalizedList(String basePath, String key) { + return getLocalizedList(basePath + "." + key); + } + + private RobinConfiguration parseUserAndServerOptions(Server server, User user) { + Locale locale; + try { + APIUser apiUser = new APIUser(user.getIdAsString()); + apiUser.getUser(); + locale = Locale.forLanguageTag(apiUser.getLanguage()); + + // todo, pending ClaireData update: allow server admins to specify a custom language string. + // todo ref https://trello.com/c/vkQTCTMG + if (server != null) { + Guild guild = new Guild(server.getIdAsString()); + guild.getGuild(); + + if (guild.isEnforceSeverLanguage()) { + // todo this should not be determined here, but will be until the ClaireData implementation is completed. + // todo this should instead be determined when the Guild object is created in the database. + // todo ClaireData update on hold while still designing the major ClaireBot update that follows this one. + locale = server.getPreferredLocale(); + } + } + } catch (IOException e) { + logger.error("ClaireData failed to return a response for Locale information. Are we cooked?"); + locale = fallbackLocale; + } + + return getLangFileByLocale(locale); + } + + /** + * Returns a file based off the language string specified. + * Returns the fallback file if a suitable translation isn't found. + * + * @param locale the Locale object for the bot + * @return Returns a localized language file or the fallback file if a suitable translation doesn't exist. + */ + public RobinConfiguration getLangFileByLocale(Locale locale) { + File targetFile = new File(pathToLanguageFiles, "lang_" + locale.toLanguageTag() + ".yml"); + File fallbackFile = new File(pathToLanguageFiles, "lang_" + fallbackLocale.toLanguageTag() + ".yml"); + + // Try primary file + RobinConfiguration config = tryLoadConfig(targetFile); + if (config != null) { + return config; + } + + // Try fallback file + config = tryLoadConfig(fallbackFile); + if (config != null) { + logger.warn("Using fallback language file for locale: {}", locale); + return config; + } + + // Ultimate fallback - empty config + logger.error("All language files failed to load! Using empty configuration."); + return new RobinConfiguration(); + } + + private RobinConfiguration tryLoadConfig(File file) { + try { + RobinConfiguration config = new RobinConfiguration(file.getAbsolutePath()); + config.load(); + return config; + } catch (InvalidConfigurationException e) { + logger.error("Failed to load language file {}: {}", file, e.getMessage()); + return null; + } + } + + public void addContext(ContextManager.ContextType contextType, String key, Object Data) { + context.addData(contextType, key, Data); + } +} diff --git a/src/main/java/com/sidpatchy/clairebot/Lang/PlaceholderHandler.java b/src/main/java/com/sidpatchy/clairebot/Lang/PlaceholderHandler.java new file mode 100644 index 0000000..6068a53 --- /dev/null +++ b/src/main/java/com/sidpatchy/clairebot/Lang/PlaceholderHandler.java @@ -0,0 +1,139 @@ +package com.sidpatchy.clairebot.Lang; + +import com.sidpatchy.clairebot.Main; +import org.apache.commons.lang3.time.DurationFormatUtils; +import org.javacord.api.entity.channel.Channel; +import org.javacord.api.entity.channel.ServerChannel; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.util.Map.entry; + +public class PlaceholderHandler { + private final ContextManager context; + private final Map placeholders; + + // Functional interface for placeholder value providers + @FunctionalInterface + private interface PlaceholderProvider { + Object getRawValue(); // Returns any type + + default String getValue() { + return String.valueOf(getRawValue()); + } + } + + + public PlaceholderHandler(ContextManager context) { + this.context = context; + this.placeholders = initializePlaceholders(); + } + + private Map initializePlaceholders() { + return Map.ofEntries( + // Project placeholders + entry("cb.invitelink", Main::getInviteLink), + entry("cb.docs", Main::getDocumentationWebsite), + entry("cb.website", Main::getWebsite), + entry("cb.github", Main::getGithub), + entry("cb.supportserver", Main::getSupportServer), + + // Bot placeholders + entry("cb.bot.numservers", () -> Main.getApi().getServers().size()), + entry("cb.bot.version", Main::getBuildVersion), + entry("cb.bot.releasedate", Main::getBuildDate), + entry("cb.bot.startseconds", () -> Main.getStartMillis() / 1000), + entry("cb.bot.runtimedurationwords", () -> DurationFormatUtils.formatDurationWords(System.currentTimeMillis() - Main.getStartMillis(), true, false)), + + // Server placeholders + entry("cb.server.name", () -> + context.getServer() != null ? context.getServer().getName() : ""), + entry("cb.server.id", () -> + context.getServer() != null ? context.getServer().getIdAsString() : ""), + + // User placeholders + entry("cb.user.name", () -> + context.getUser() != null ? context.getUser().getName() : ""), + entry("cb.user.id", () -> + context.getUser() != null ? context.getUser().getIdAsString() : ""), + + // Author placeholders + entry("cb.author.name", () -> + context.getAuthor() != null ? context.getAuthor().getName() : ""), + entry("cb.author.id", () -> + context.getAuthor() != null ? context.getAuthor().getIdAsString() : ""), + + // Channel placeholders + entry("cb.channel.name", () -> + Optional.ofNullable(context.getChannel()) + .flatMap(Channel::asServerChannel) + .map(ServerChannel::getName) + .orElse("NOT FOUND")), + entry("cb.channel.id", () -> + context.getChannel() != null ? context.getChannel().getIdAsString() : "") + ); + } + + /** + * Process a string containing placeholders + * @param input String containing placeholders in format {cb.placeholder.name} + * @return Processed string with placeholders replaced with their values + */ + public String process(String input) { + if (input == null || input.isEmpty()) { + return input; + } + + Pattern pattern = Pattern.compile("\\{([^}]+)\\}"); + Matcher matcher = pattern.matcher(input); + StringBuffer result = new StringBuffer(); + + while (matcher.find()) { + String placeholder = matcher.group(1); + String replacement = getPlaceholderValue(placeholder); + // Quote the replacement string to handle special regex characters + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(result); + + return result.toString(); + } + + public List process(List input) { + return input.stream() + .map(this::process) + .toList(); + } + + /** + * Get the value for a specific placeholder + * @param key The placeholder key (without {} brackets) + * @return The placeholder value or the original key if not found + */ + public String getPlaceholderValue(String key) { + PlaceholderProvider provider = placeholders.get(key); + return provider != null ? provider.getValue() : key; + } + + /** + * Check if a placeholder exists + * @param key The placeholder key (without {} brackets) + * @return true if the placeholder exists + */ + public boolean hasPlaceholder(String key) { + return placeholders.containsKey(key); + } + + /** + * Add a custom placeholder + * @param key The placeholder key (without {} brackets) + * @param provider The provider function that returns the placeholder value + */ + public void addPlaceholder(String key, PlaceholderProvider provider) { + placeholders.put(key, provider); + } +} diff --git a/src/main/java/com/sidpatchy/clairebot/Listener/ButtonClick.java b/src/main/java/com/sidpatchy/clairebot/Listener/ButtonClick.java index 6c53d11..d2689c8 100644 --- a/src/main/java/com/sidpatchy/clairebot/Listener/ButtonClick.java +++ b/src/main/java/com/sidpatchy/clairebot/Listener/ButtonClick.java @@ -2,13 +2,13 @@ import com.sidpatchy.clairebot.Embed.Commands.Regular.QuoteEmbed; import com.sidpatchy.clairebot.Embed.Commands.Regular.SantaEmbed; +import com.sidpatchy.clairebot.Lang.ContextManager; +import com.sidpatchy.clairebot.Lang.LanguageManager; import com.sidpatchy.clairebot.Main; import com.sidpatchy.clairebot.MessageComponents.Regular.SantaModal; import com.sidpatchy.clairebot.Util.SantaUtils; -import org.javacord.api.entity.channel.Channel; import org.javacord.api.entity.channel.TextChannel; import org.javacord.api.entity.message.Message; -import org.javacord.api.entity.message.MessageBuilder; import org.javacord.api.entity.message.MessageFlag; import org.javacord.api.entity.message.embed.Embed; import org.javacord.api.entity.message.embed.EmbedFooter; @@ -19,22 +19,27 @@ import org.javacord.api.interaction.ButtonInteraction; import org.javacord.api.listener.interaction.ButtonClickListener; +import java.util.HashMap; + public class ButtonClick implements ButtonClickListener { @Override public void onButtonClick(ButtonClickEvent event) { ButtonInteraction buttonInteraction = event.getButtonInteraction(); + Server server = buttonInteraction.getServer().orElse(null); String buttonID = buttonInteraction.getCustomId().toLowerCase(); User buttonAuthor = buttonInteraction.getUser(); Message message = buttonInteraction.getMessage(); TextChannel channel = message.getChannel(); + ContextManager context = new ContextManager(server, channel, buttonAuthor, null, message, new HashMap<>()); + LanguageManager languageManager = new LanguageManager(Main.getFallbackLocale(), context); + Embed embed = buttonInteraction.getMessage().getEmbeds().get(0); EmbedFooter footer = embed.getFooter().orElse(null); // Extract data from embed fields SantaUtils.ExtractionResult extractionResult = null; - Server server = null; User author = null; if (!buttonID.equalsIgnoreCase("view_original")) { extractionResult = SantaUtils.extractDataFromEmbed(embed, footer); diff --git a/src/main/java/com/sidpatchy/clairebot/Listener/MessageCreate.java b/src/main/java/com/sidpatchy/clairebot/Listener/MessageCreate.java index 1f4a372..592fc65 100644 --- a/src/main/java/com/sidpatchy/clairebot/Listener/MessageCreate.java +++ b/src/main/java/com/sidpatchy/clairebot/Listener/MessageCreate.java @@ -1,24 +1,23 @@ package com.sidpatchy.clairebot.Listener; import com.sidpatchy.clairebot.API.APIUser; +import com.sidpatchy.clairebot.Lang.ContextManager; +import com.sidpatchy.clairebot.Lang.LanguageManager; import com.sidpatchy.clairebot.Main; -import com.sidpatchy.clairebot.Util.Leveling.LevelingTools; +import org.javacord.api.entity.channel.TextChannel; import org.javacord.api.entity.emoji.Emoji; import org.javacord.api.entity.message.Message; import org.javacord.api.entity.message.MessageAuthor; import org.javacord.api.entity.message.MessageBuilder; -import org.javacord.api.entity.message.MessageType; import org.javacord.api.entity.message.mention.AllowedMentionsBuilder; import org.javacord.api.entity.server.Server; +import org.javacord.api.entity.user.User; import org.javacord.api.event.message.MessageCreateEvent; import org.javacord.api.listener.message.MessageCreateListener; import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.random.RandomGenerator; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; import java.util.regex.Pattern; public class MessageCreate implements MessageCreateListener { @@ -28,7 +27,13 @@ public void onMessageCreate(MessageCreateEvent event) { String messageContent = message.getContent(); Server server = message.getServer().orElse(null); MessageAuthor messageAuthor = message.getAuthor(); + User user = messageAuthor.asUser().orElse(null); APIUser apiUser = new APIUser(messageAuthor.getIdAsString()); + TextChannel textChannel = message.getChannel(); + + ContextManager context = new ContextManager(server, textChannel, user, user, message, new HashMap<>()); + // Todo replace reference to en-US with config file parameter + LanguageManager languageManager = new LanguageManager(Locale.forLanguageTag("en-US"), context); // it seems as though the Javacord functions for this don't actually work, or I'm using them wrong if (messageAuthor.isBotUser() || messageAuthor.isYourself() || messageAuthor.getIdAsString().equalsIgnoreCase("704244031772950528") || messageAuthor.getIdAsString().equalsIgnoreCase("848024760789237810")) { @@ -37,8 +42,9 @@ public void onMessageCreate(MessageCreateEvent event) { } // ClaireBot on top!! - List onTopResponses = Main.getClaireBotOnTopResponses(); - for (String trigger : Main.getOnTopTriggers()) { + List onTopTriggers = languageManager.getLocalizedList("ClaireLang.Embed.Commands.Regular.EightBallEmbed.OnTopTriggers"); + List onTopResponses = languageManager.getLocalizedList("ClaireLang.Embed.Commands.Regular.EightBallEmbed.ClaireBotOnTopResponses"); + for (String trigger : onTopTriggers) { String regex = "\\b" + Pattern.quote(trigger.toUpperCase()) + "\\b.*"; // match trigger followed by anything if (messageContent.toUpperCase().matches(regex)) { Random random = new Random(); @@ -46,7 +52,7 @@ public void onMessageCreate(MessageCreateEvent event) { // because apparently message.reply() doesn't allow disabling mentions. new MessageBuilder() - .setContent(Main.getClaireBotOnTopResponses().get(rand)) + .setContent(onTopResponses.get(rand)) .setAllowedMentions(new AllowedMentionsBuilder().build()) .replyTo(message) .send(message.getChannel()); @@ -56,10 +62,11 @@ public void onMessageCreate(MessageCreateEvent event) { } // pls ban - List plsBanResponses = Main.getPlsBanResponses(); + List plsBanTriggers = languageManager.getLocalizedList("ClaireLang.PlsBan.PlsBanTriggers"); + List plsBanResponses = languageManager.getLocalizedList("ClaireLang.PlsBan.PlsBanResponses"); String escapedBotId = Pattern.quote("<@" + Main.getApi().getClientId() + ">"); - for (String trigger : Main.getPlsBanTriggers()) { + for (String trigger : plsBanTriggers) { String regex = "(?i)" + escapedBotId + "\\s*" + Pattern.quote(trigger) + ".*"; if (messageContent.toUpperCase().matches(regex)) { Random random = new Random(); @@ -67,7 +74,7 @@ public void onMessageCreate(MessageCreateEvent event) { // Message.reply() doesn't allow disabling mentions. new MessageBuilder() - .setContent(Main.getPlsBanResponses().get(rand)) + .setContent(plsBanResponses.get(rand)) .setAllowedMentions(new AllowedMentionsBuilder().build()) .replyTo(message) .send(message.getChannel()); @@ -88,18 +95,17 @@ public void onMessageCreate(MessageCreateEvent event) { // Grant between 0 and 8 points if (server != null) { - Integer currentPoints = LevelingTools.getUserPoints(messageAuthor.getIdAsString(), "global"); - RandomGenerator randomGenerator = RandomGenerator.getDefault(); - Integer pointsToGrant = randomGenerator.nextInt(8); + int pointsToGrant = ThreadLocalRandom.current().nextInt(9); try { - Map guildPointsToUpdate = new HashMap<>(); - guildPointsToUpdate.put(server.getIdAsString(), pointsToGrant); - guildPointsToUpdate.put("global", pointsToGrant); + Map guildPointsToUpdate = Map.of( + server.getIdAsString(), pointsToGrant, + "global", pointsToGrant + ); apiUser.updateUserPointsGuildID(guildPointsToUpdate); - } catch (IOException e) { - throw new RuntimeException(e); + Main.getLogger().error("Failed to update points for user {}", messageAuthor.getIdAsString(), e); } } + } } diff --git a/src/main/java/com/sidpatchy/clairebot/Listener/SlashCommandCreate.java b/src/main/java/com/sidpatchy/clairebot/Listener/SlashCommandCreate.java index 56f2bac..3b05b0c 100644 --- a/src/main/java/com/sidpatchy/clairebot/Listener/SlashCommandCreate.java +++ b/src/main/java/com/sidpatchy/clairebot/Listener/SlashCommandCreate.java @@ -1,8 +1,10 @@ package com.sidpatchy.clairebot.Listener; -import com.sidpatchy.Robin.Discord.ParseCommands; +import com.sidpatchy.clairebot.Commands; import com.sidpatchy.clairebot.Embed.Commands.Regular.*; import com.sidpatchy.clairebot.Embed.ErrorEmbed; +import com.sidpatchy.clairebot.Lang.ContextManager; +import com.sidpatchy.clairebot.Lang.LanguageManager; import com.sidpatchy.clairebot.Main; import com.sidpatchy.clairebot.MessageComponents.Regular.ServerPreferencesComponents; import com.sidpatchy.clairebot.MessageComponents.Regular.UserPreferencesComponents; @@ -24,13 +26,16 @@ import java.io.FileNotFoundException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.concurrent.CompletableFuture; public class SlashCommandCreate implements SlashCommandCreateListener { - static ParseCommands parseCommands = new ParseCommands(Main.getCommandsFile()); - Logger logger = Main.getLogger(); + private static final Logger logger = Main.getLogger(); + private static final Commands commands = Main.getCommands(); + private LanguageManager languageManager; @Override public void onSlashCommandCreate(SlashCommandCreateEvent event) { @@ -39,8 +44,14 @@ public void onSlashCommandCreate(SlashCommandCreateEvent event) { String commandName = slashCommandInteraction.getCommandName(); User author = slashCommandInteraction.getUser(); User user = slashCommandInteraction.getArgumentUserValueByName("user").orElse(author); + TextChannel textchannel = slashCommandInteraction.getChannel().orElse(null); - if (commandName.equalsIgnoreCase(parseCommands.getCommandName("8ball"))) { + ContextManager context = new ContextManager(server, textchannel, author, user, null, new HashMap<>()); + + // Todo replace reference to en-US with config file parameter + languageManager = new LanguageManager(Main.getFallbackLocale(), context); + + if (commandName.equalsIgnoreCase(commands.getEightball().getName())) { String query = slashCommandInteraction.getArgumentStringValueByIndex(0).orElse(null); if (query == null) { @@ -51,20 +62,20 @@ public void onSlashCommandCreate(SlashCommandCreateEvent event) { future.thenAccept(interactionResponse -> { try { - interactionResponse.addEmbed(EightBallEmbed.getEightBall(query, author)); + interactionResponse.addEmbed(EightBallEmbed.getEightBall(languageManager, query, author)); interactionResponse.update(); } catch (Exception e) { e.printStackTrace(); } }); } - else if (commandName.equalsIgnoreCase(parseCommands.getCommandName("avatar"))) { + else if (commandName.equalsIgnoreCase(commands.getAvatar().getName())) { boolean getGlobalAvatar = slashCommandInteraction.getArgumentBooleanValueByName("globalAvatar").orElse(true); slashCommandInteraction.createImmediateResponder() .addEmbed(AvatarEmbed.getAvatar(server, user, author, getGlobalAvatar)) .respond(); } - else if (commandName.equalsIgnoreCase(parseCommands.getCommandName("config"))) { + else if (commandName.equalsIgnoreCase(commands.getConfig().getName())) { String mode = slashCommandInteraction.getArgumentStringValueByName("mode").orElse("user"); if (mode.equalsIgnoreCase("user")) { @@ -91,38 +102,38 @@ else if (mode.equalsIgnoreCase("server") && server != null) { } } } - else if (commandName.equalsIgnoreCase(parseCommands.getCommandName("help"))) { + else if (commandName.equalsIgnoreCase(commands.getHelp().getName())) { String command = slashCommandInteraction.getArgumentStringValueByIndex(0).orElse("help"); try { slashCommandInteraction.createImmediateResponder() - .addEmbed(HelpEmbed.getHelp(command, user.getIdAsString())) + .addEmbed(HelpEmbed.getHelp(languageManager, command, user.getIdAsString())) .respond(); } catch (FileNotFoundException e) { Main.getLogger().error(e); Main.getLogger().error("There was an issue locating the commands file at some point in the chain while the help command was running, good luck!"); } } - else if (commandName.equalsIgnoreCase(parseCommands.getCommandName("info"))) { + else if (commandName.equalsIgnoreCase(commands.getInfo().getName())) { slashCommandInteraction.createImmediateResponder() - .addEmbed(InfoEmbed.getInfo(author)) + .addEmbed(InfoEmbed.getInfo(languageManager, author)) .respond(); } - else if (commandName.equalsIgnoreCase(parseCommands.getCommandName("leaderboard"))) { + else if (commandName.equalsIgnoreCase(commands.getLeaderboard().getName())) { boolean getGlobal = slashCommandInteraction.getArgumentBooleanValueByName("global").orElse(false); if (server == null || getGlobal) { slashCommandInteraction.createImmediateResponder() - .addEmbed(LeaderboardEmbed.getLeaderboard("global", author)) + .addEmbed(LeaderboardEmbed.getLeaderboard(languageManager, "global", author)) .respond(); } else { slashCommandInteraction.createImmediateResponder() - .addEmbed(LeaderboardEmbed.getLeaderboard(server, author)) + .addEmbed(LeaderboardEmbed.getLeaderboard(languageManager, server, author)) .respond(); } } - else if (commandName.equalsIgnoreCase(parseCommands.getCommandName("level"))) { + else if (commandName.equalsIgnoreCase(commands.getLevel().getName())) { String serverID; if (server == null) { serverID = "global"; @@ -135,7 +146,7 @@ else if (commandName.equalsIgnoreCase(parseCommands.getCommandName("level"))) { .addEmbed(LevelEmbed.getLevel(serverID, user)) .respond(); } - else if (commandName.equalsIgnoreCase(parseCommands.getCommandName("poll"))) { + else if (commandName.equalsIgnoreCase(commands.getPoll().getName())) { if (slashCommandInteraction.getArgumentStringValueByName("question").orElse(null) == null) { try { // LOL how long has this been unimplemented? Not a bad idea tbh 2023-02-16 @@ -179,7 +190,7 @@ else if (commandName.equalsIgnoreCase(parseCommands.getCommandName("poll"))) { }); } } - else if (commandName.equalsIgnoreCase(parseCommands.getCommandName("quote"))) { + else if (commandName.equalsIgnoreCase(commands.getQuote().getName())) { TextChannel channel = slashCommandInteraction.getChannel().orElse(null); if (channel == null) { slashCommandInteraction.createImmediateResponder() @@ -203,7 +214,7 @@ else if (commandName.equalsIgnoreCase(parseCommands.getCommandName("quote"))) { }); }); } - else if (commandName.equalsIgnoreCase(parseCommands.getCommandName("request"))) { + else if (commandName.equalsIgnoreCase(commands.getRequest().getName())) { if (server == null) { slashCommandInteraction.createImmediateResponder() .addEmbed(ErrorEmbed.getCustomError(Main.getErrorCode("notaserver"), @@ -256,7 +267,7 @@ else if (commandName.equalsIgnoreCase(parseCommands.getCommandName("request"))) }); } } - else if (commandName.equalsIgnoreCase("server")) { + else if (commandName.equalsIgnoreCase(commands.getServer().getName())) { EmbedBuilder embed = null; String guildID = slashCommandInteraction.getArgumentStringValueByName("guildID").orElse(null); @@ -282,12 +293,12 @@ else if (server != null) { .addEmbed(embed) .respond(); } - else if (commandName.equalsIgnoreCase("user")) { + else if (commandName.equalsIgnoreCase(commands.getInfo().getName())) { slashCommandInteraction.createImmediateResponder() .addEmbed(UserInfoEmbed.getUser(user, author, server)) .respond(); } - else if (commandName.equalsIgnoreCase(parseCommands.getCommandName("santa"))) { + else if (commandName.equalsIgnoreCase(commands.getSanta().getName())) { Role role = slashCommandInteraction.getArgumentRoleValueByName("role").orElse(null); if (role == null) { diff --git a/src/main/java/com/sidpatchy/clairebot/Main.java b/src/main/java/com/sidpatchy/clairebot/Main.java index d5dfad9..5f54473 100644 --- a/src/main/java/com/sidpatchy/clairebot/Main.java +++ b/src/main/java/com/sidpatchy/clairebot/Main.java @@ -1,6 +1,6 @@ package com.sidpatchy.clairebot; -import com.sidpatchy.Robin.Discord.ParseCommands; +import com.sidpatchy.Robin.Discord.CommandFactory; import com.sidpatchy.Robin.Exception.InvalidConfigurationException; import com.sidpatchy.Robin.File.ResourceLoader; import com.sidpatchy.Robin.File.RobinConfiguration; @@ -15,10 +15,9 @@ import java.awt.*; import java.io.IOException; -import java.util.Arrays; +import java.io.InputStream; +import java.util.*; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; /** * ClaireBot - Simply the best. @@ -38,7 +37,7 @@ * along with this program. If not, see . * * @since April 2020 - * @version 3.3.2 + * @version 3.4.0-SNAPSHOT * @author Sidpatchy */ public class Main { @@ -61,16 +60,17 @@ public class Main { private static String botName; private static String color; private static String errorColor; - private static List errorGifs; - private static List zerfas; + private static List errorGifs; + private static Locale fallbackLocale; + private static List zerfas; private static String zerfasEmojiServerID; private static String zerfasEmojiID; - private static List eightBall; - private static List eightBallRigged; - private static List claireBotOnTopResponses; - private static List onTopTriggers; - private static List plsBanResponses; - private static List plsBanTriggers; + private static List eightBall; + private static List eightBallRigged; + private static List claireBotOnTopResponses; + private static List onTopTriggers; + private static List plsBanResponses; + private static List plsBanTriggers; // Commands private static final Logger logger = LogManager.getLogger(Main.class); @@ -78,10 +78,22 @@ public class Main { // Related to configuration files private static final String configFile = "config.yml"; private static final String commandsFile = "commands.yml"; + private static final String translationsPath = "config/translations/"; private static RobinConfiguration config; - private static ParseCommands commands; - - public static List commandList = Arrays.asList("8ball", "avatar", "help", "info", "leaderboard", "level", "poll", "quote", "request", "server", "user", "config", "santa"); + private static Commands commands; + private static final Properties buildProperties = new Properties() {{ + try (InputStream input = Main.class.getClassLoader().getResourceAsStream("build.properties")) { + if (input != null) load(input); + else System.err.println("build.properties missing!"); + } catch (IOException e) { throw new RuntimeException("Failed to load build.properties", e); } + }}; + private static String buildVersion; + private static String buildDate; + private static String github; + private static String supportServer; + private static String website; + private static String documentationWebsite; + private static String inviteLink; public static void main(String[] args) throws InvalidConfigurationException { logger.info("ClaireBot loading..."); @@ -90,10 +102,10 @@ public static void main(String[] args) throws InvalidConfigurationException { ResourceLoader loader = new ResourceLoader(); loader.saveResource(configFile, false); loader.saveResource(commandsFile, false); + loader.saveResource("translations/lang_en-US.yml", true); // TODO make this false, handle non en-US files. // Init config handlers config = new RobinConfiguration("config/" + configFile); - commands = new ParseCommands("config/" + commandsFile); config.load(); @@ -104,6 +116,7 @@ public static void main(String[] args) throws InvalidConfigurationException { String video_url = config.getString("video_url"); extractParametersFromConfig(true); + loadCommandDefs(); verifyDatabaseConnectivity(); @@ -119,7 +132,7 @@ public static void main(String[] args) throws InvalidConfigurationException { Clockwork.initClockwork(); // Set the bot's activity - api.updateActivity("ClaireBot v3.3.2", video_url); + api.updateActivity("ClaireBot " + buildVersion, video_url); // Register slash commands registerSlashCommands(); @@ -141,7 +154,7 @@ public static void main(String[] args) throws InvalidConfigurationException { // Connect to Discord and create an API object private static DiscordApi DiscordLogin(String token, Integer current_shard, Integer total_shards) { - if (token == null || token.equals("")) { + if (token == null || token.isEmpty()) { logger.fatal("Token can't be null or empty. Check your config file!"); System.exit(1); } @@ -161,14 +174,13 @@ else if (current_shard == null || total_shards == null) { .login().join(); } catch (Exception e) { - e.printStackTrace(); - logger.fatal(e.toString()); - logger.fatal("Unable to log in to Discord. Aborting startup!"); + logger.fatal("Unable to log in to Discord. Aborting startup!", e); } return null; } // Extract parameters from the config.yml file, update the config if applicable. + // todo stop using Robin for this. Switch to standard Java classses. @SuppressWarnings("unchecked") public static void extractParametersFromConfig(boolean updateOutdatedConfigs) { logger.info("Loading configuration files..."); @@ -182,16 +194,24 @@ public static void extractParametersFromConfig(boolean updateOutdatedConfigs) { guildDefaults = ((Map) config.getObj("guildDefaults")); color = config.getString("color"); errorColor = config.getString("errorColor"); - errorGifs = config.getList("error_gifs"); - zerfas = config.getList("zerfas"); + errorGifs = config.getList("error_gifs", String.class); + fallbackLocale = Locale.forLanguageTag(config.getString("fallback_locale")); + zerfas = config.getList("zerfas", String.class); zerfasEmojiServerID = String.valueOf(config.getLong("zerfas_emoji_server_id")); zerfasEmojiID = String.valueOf(config.getLong("zerfas_emoji_id")); - eightBall = config.getList("8bResponses"); - eightBallRigged = config.getList("8bRiggedResponses"); - claireBotOnTopResponses = config.getList("ClaireBotOnTopResponses"); - onTopTriggers = config.getList("OnTopTriggers"); - plsBanResponses = config.getList("PlsBanResponses"); - plsBanTriggers = config.getList("PlsBanTriggers"); + eightBall = config.getList("8bResponses", String.class); + eightBallRigged = config.getList("8bRiggedResponses", String.class); + claireBotOnTopResponses = config.getList("ClaireBotOnTopResponses", String.class); + onTopTriggers = config.getList("OnTopTriggers", String.class); + plsBanResponses = config.getList("PlsBanResponses", String.class); + plsBanTriggers = config.getList("PlsBanTriggers", String.class); + buildVersion = buildProperties.getProperty("clairebot.version"); + buildDate = buildProperties.getProperty("clairebot.buildDate"); + github = buildProperties.getProperty("clairebot.github"); + supportServer = buildProperties.getProperty("clairebot.supportServer"); + website = buildProperties.getProperty("clairebot.website"); + documentationWebsite = buildProperties.getProperty("clairebot.documentationWebsite"); + inviteLink = config.getString("clairebot.inviteLink"); } catch (Exception e) { e.printStackTrace(); @@ -200,6 +220,17 @@ public static void extractParametersFromConfig(boolean updateOutdatedConfigs) { } + public static void loadCommandDefs() { + try { + commands = CommandFactory.loadConfig("config/" + commandsFile, Commands.class); + logger.warn(commands.getInfo().getName()); + logger.warn(commands.getInfo().getHelp()); + } catch (IOException e) { + logger.fatal("There was a fatal error while registering slash commands", e); + throw new RuntimeException(e); + } + } + // Handle the registry of slash commands and any errors associated. public static void registerSlashCommands() { try { @@ -207,17 +238,12 @@ public static void registerSlashCommands() { logger.info("Slash commands registered successfully!"); } catch (NullPointerException e) { - e.printStackTrace(); - logger.fatal("There was an error while registering slash commands. There's a pretty good chance it's related to an uncaught issue with the commands.yml file, trying to read all commands and printing out results."); - for (String s : Main.commandList) { - logger.fatal(commands.getCommandName(s)); - } - logger.fatal("If the above list looks incomplete or generates another error, check your commands.yml file!"); + logger.fatal("There was an error while registering slash commands. There's a pretty good chance it's related to an uncaught issue with the commands.yml file.", e); + logger.fatal("Check your commands.yml file!"); System.exit(4); } catch (Exception e) { - e.printStackTrace(); - logger.fatal("There was a fatal error while registering slash commands."); + logger.fatal("There was a fatal error while registering slash commands.", e); System.exit(5); } } @@ -272,51 +298,35 @@ public static Color getColor(String userID) { public static Color getErrorColor() { return Color.decode(errorColor); } public static List getErrorGifs() { - return errorGifs.stream() - .map(Object::toString) - .collect(Collectors.toList()); + return errorGifs; } public static List getEightBall() { - return eightBall.stream() - .map(Object::toString) - .collect(Collectors.toList()); + return eightBall; } public static List getEightBallRigged() { - return eightBallRigged.stream() - .map(Object::toString) - .collect(Collectors.toList()); + return eightBallRigged; } public static List getOnTopTriggers() { - return onTopTriggers.stream() - .map(Object::toString) - .collect(Collectors.toList()); + return onTopTriggers; } public static List getClaireBotOnTopResponses() { - return claireBotOnTopResponses.stream() - .map(Object::toString) - .collect(Collectors.toList()); + return claireBotOnTopResponses; } public static List getPlsBanTriggers() { - return plsBanTriggers.stream() - .map(Object::toString) - .collect(Collectors.toList()); + return plsBanTriggers; } public static List getPlsBanResponses() { - return plsBanResponses.stream() - .map(Object::toString) - .collect(Collectors.toList()); + return plsBanResponses; } public static List getZerfas() { - return zerfas.stream() - .map(Object::toString) - .collect(Collectors.toList()); + return zerfas; } public static String getZerfasEmojiServerID() { @@ -331,6 +341,10 @@ public static String getZerfasEmojiID() { public static String getCommandsFile() { return "config/" + commandsFile; } + public static Commands getCommands() { + return commands; + } + public static Logger getLogger() { return logger; } public static String getErrorCode(String descriptor) { @@ -342,4 +356,40 @@ public static String getErrorCode(String descriptor) { public static List getVoteEmoji() { return Arrays.asList("1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟", "\uD83D\uDC4D", "\uD83D\uDC4E"); } public static long getStartMillis() { return startMillis; } + + public static String getTranslationsPath() { + return translationsPath; + } + + public static String getBuildVersion() { + return buildVersion; + } + + public static String getBuildDate() { + return buildDate; + } + + public static String getGithub() { + return github; + } + + public static String getSupportServer() { + return supportServer; + } + + public static String getWebsite() { + return website; + } + + public static String getDocumentationWebsite() { + return documentationWebsite; + } + + public static String getInviteLink() { + return inviteLink; + } + + public static Locale getFallbackLocale() { + return fallbackLocale; + } } diff --git a/src/main/java/com/sidpatchy/clairebot/RegisterSlashCommands.java b/src/main/java/com/sidpatchy/clairebot/RegisterSlashCommands.java index 421bb9e..e85e9c7 100644 --- a/src/main/java/com/sidpatchy/clairebot/RegisterSlashCommands.java +++ b/src/main/java/com/sidpatchy/clairebot/RegisterSlashCommands.java @@ -1,12 +1,12 @@ package com.sidpatchy.clairebot; -import com.sidpatchy.Robin.Discord.ParseCommands; import org.javacord.api.DiscordApi; import org.javacord.api.interaction.SlashCommandBuilder; import org.javacord.api.interaction.SlashCommandOption; import org.javacord.api.interaction.SlashCommandOptionChoice; import org.javacord.api.interaction.SlashCommandOptionType; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -14,10 +14,12 @@ /** * Also delete them too! + * + * todo switch to registering commands on a per-server basis so that server admins may use different commands.yml languages. */ public class RegisterSlashCommands { - private static final ParseCommands parseCommands = new ParseCommands(Main.getCommandsFile()); + private static final Commands commands = Main.getCommands(); public static void DeleteSlashCommands (DiscordApi api) { api.bulkOverwriteGlobalApplicationCommands(Set.of()).join(); @@ -34,21 +36,39 @@ public static void RegisterSlashCommand(DiscordApi api) { // Create the command list in the help command without repeating the same thing 50 million times. ArrayList helpCommandOptions = new ArrayList<>(); - for (String s : Main.commandList) { - helpCommandOptions.add(SlashCommandOptionChoice.create(parseCommands.getCommandName(s), parseCommands.getCommandName(s))); + for (Field field : commands.getClass().getDeclaredFields()) { + helpCommandOptions.add(SlashCommandOptionChoice.create(field.getName(), field.getName())); } Set commandsList = new HashSet<>(Arrays.asList( // Regular commands - new SlashCommandBuilder().setName(parseCommands.getCommandName("8ball")).setDescription(parseCommands.getCommandHelp("8ball")).addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "query", "The question you wish to ask.", true)), - new SlashCommandBuilder().setName(parseCommands.getCommandName("avatar")).setDescription(parseCommands.getCommandHelp("avatar")).addOption(SlashCommandOption.create(SlashCommandOptionType.USER, "user", "Optionally mention a user.", false)) + new SlashCommandBuilder() + .setName(commands.getEightball().getName()) + .setDescription(commands.getEightball().getOverview()) + .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "query", "The question you wish to ask.", true)), + new SlashCommandBuilder() + .setName(commands.getAvatar().getName()) + .setDescription(commands.getAvatar().getOverview()) + .addOption(SlashCommandOption.create(SlashCommandOptionType.USER, "user", "Optionally mention a user.", false)) .addOption(SlashCommandOption.create(SlashCommandOptionType.BOOLEAN, "globalAvatar", "Whether the bot should display the global or server avatar.")), - new SlashCommandBuilder().setName(parseCommands.getCommandName("help")).setDescription(parseCommands.getCommandHelp("help")) + new SlashCommandBuilder() + .setName(commands.getHelp().getName()) + .setDescription(commands.getHelp().getOverview()) .addOption(SlashCommandOption.createWithChoices(SlashCommandOptionType.STRING, "command-name", "Command to get more info on", false, helpCommandOptions)), - new SlashCommandBuilder().setName(parseCommands.getCommandName("info")).setDescription(parseCommands.getCommandHelp("info")), - new SlashCommandBuilder().setName(parseCommands.getCommandName("leaderboard")).setDescription(parseCommands.getCommandHelp("leaderboard")).addOption(SlashCommandOption.create(SlashCommandOptionType.BOOLEAN, "global", "Get the global leaderboard?", false)), - new SlashCommandBuilder().setName(parseCommands.getCommandName("level")).setDescription(parseCommands.getCommandHelp("level")).addOption(SlashCommandOption.create(SlashCommandOptionType.USER, "user", "Optionally mention a user.", false)), - new SlashCommandBuilder().setName(parseCommands.getCommandName("poll")).setDescription(parseCommands.getCommandHelp("poll")) + new SlashCommandBuilder() + .setName(commands.getInfo().getName()) + .setDescription(commands.getInfo().getOverview()), + new SlashCommandBuilder() + .setName(commands.getLeaderboard().getName()) + .setDescription(commands.getLeaderboard().getOverview()) + .addOption(SlashCommandOption.create(SlashCommandOptionType.BOOLEAN, "global", "Get the global leaderboard?", false)), + new SlashCommandBuilder() + .setName(commands.getLevel().getName()) + .setDescription(commands.getLevel().getOverview()) + .addOption(SlashCommandOption.create(SlashCommandOptionType.USER, "user", "Optionally mention a user.", false)), + new SlashCommandBuilder() + .setName(commands.getPoll().getName()) + .setDescription(commands.getPoll().getOverview()) .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "question", "Question to ask", false)) .addOption(SlashCommandOption.create(SlashCommandOptionType.BOOLEAN, "allow-multiple-choices", "Whether multiple choices should be enabled.", false)) .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "choice-1", "Custom choice")) @@ -60,9 +80,13 @@ public static void RegisterSlashCommand(DiscordApi api) { .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "choice-7", "Custom choice")) .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "choice-8", "Custom choice")) .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "choice-9", "Custom choice")), - new SlashCommandBuilder().setName(parseCommands.getCommandName("quote")).setDescription(parseCommands.getCommandOverview("quote")) + new SlashCommandBuilder() + .setName(commands.getQuote().getName()) + .setDescription(commands.getQuote().getOverview()) .addOption(SlashCommandOption.create(SlashCommandOptionType.USER, "User", "Optionally mention a user", false)), - new SlashCommandBuilder().setName(parseCommands.getCommandName("request")).setDescription(parseCommands.getCommandOverview("request")) + new SlashCommandBuilder() + .setName(commands.getRequest().getName()) + .setDescription(commands.getRequest().getOverview()) .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "question", "Question to ask", false)) .addOption(SlashCommandOption.create(SlashCommandOptionType.BOOLEAN, "allow-multiple-choices", "Whether multiple choices should be enabled.", false)) .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "choice-1", "Custom choice")) @@ -74,14 +98,24 @@ public static void RegisterSlashCommand(DiscordApi api) { .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "choice-7", "Custom choice")) .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "choice-8", "Custom choice")) .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "choice-9", "Custom choice")), - new SlashCommandBuilder().setName(parseCommands.getCommandName("server")).setDescription(parseCommands.getCommandHelp("server")).addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "guildID", "Optionally specify a guild by ID.", false)), - new SlashCommandBuilder().setName(parseCommands.getCommandName("user")).setDescription(parseCommands.getCommandHelp("user")).addOption(SlashCommandOption.create(SlashCommandOptionType.USER, "user", "Optionally mention a user.", false)), - new SlashCommandBuilder().setName(parseCommands.getCommandName("config")).setDescription(parseCommands.getCommandHelp("config")) + new SlashCommandBuilder() + .setName(commands.getServer().getName()) + .setDescription(commands.getServer().getOverview()) + .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "guildID", "Optionally specify a guild by ID.", false)), + new SlashCommandBuilder() + .setName(commands.getUser().getName()) + .setDescription(commands.getUser().getOverview()) + .addOption(SlashCommandOption.create(SlashCommandOptionType.USER, "user", "Optionally mention a user.", false)), + new SlashCommandBuilder() + .setName(commands.getConfig().getName()) + .setDescription(commands.getConfig().getOverview()) .addOption(SlashCommandOption.createWithChoices(SlashCommandOptionType.STRING, "mode", "Settings to change", false, Arrays.asList( SlashCommandOptionChoice.create("user", "user"), SlashCommandOptionChoice.create("server", "server") ))), - new SlashCommandBuilder().setName(parseCommands.getCommandName("santa")).setDescription(parseCommands.getCommandOverview("santa")) + new SlashCommandBuilder() + .setName(commands.getSanta().getName()) + .setDescription(commands.getSanta().getOverview()) .addOption(SlashCommandOption.create(SlashCommandOptionType.ROLE, "Role", "Role to get users from", true)) //new SlashCommandBuilder().setName(parseCommands.getCommandName("debug")).setDescription(parseCommands.getCommandHelp("debug")) )); diff --git a/src/main/java/com/sidpatchy/clairebot/Util/Cache/MessageCacheManager.java b/src/main/java/com/sidpatchy/clairebot/Util/Cache/MessageCacheManager.java index e4c7bef..a79b910 100644 --- a/src/main/java/com/sidpatchy/clairebot/Util/Cache/MessageCacheManager.java +++ b/src/main/java/com/sidpatchy/clairebot/Util/Cache/MessageCacheManager.java @@ -1,10 +1,7 @@ package com.sidpatchy.clairebot.Util.Cache; -import org.javacord.api.DiscordApi; -import org.javacord.api.entity.channel.Channel; import org.javacord.api.entity.channel.TextChannel; import org.javacord.api.entity.message.Message; -import org.javacord.api.entity.message.MessageSet; import org.javacord.api.entity.user.User; import java.util.List; @@ -16,7 +13,7 @@ * A message caching system to assist in */ public class MessageCacheManager { - private static Map messageCache = new ConcurrentHashMap<>(); + private static final Map messageCache = new ConcurrentHashMap<>(); public static void purgeCache(long secondsAged) { for (MessageCacheEntry entry : messageCache.values()) { diff --git a/src/main/java/com/sidpatchy/clairebot/Util/Lang/LanguageManager.java b/src/main/java/com/sidpatchy/clairebot/Util/Lang/LanguageManager.java deleted file mode 100644 index 00c7a08..0000000 --- a/src/main/java/com/sidpatchy/clairebot/Util/Lang/LanguageManager.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sidpatchy.clairebot.Util.Lang; - -public class LanguageManager { - private final String pathToLanguageFiles; - - /** - * Construct a new ClaireBot Language Manager (ClaireLang) - * - * @param pathToLanguageFiles the path to the various language files. - */ - public LanguageManager(String pathToLanguageFiles) { - this.pathToLanguageFiles = pathToLanguageFiles; - } - - -} diff --git a/src/main/java/com/sidpatchy/clairebot/Util/Leveling/LevelingTools.java b/src/main/java/com/sidpatchy/clairebot/Util/Leveling/LevelingTools.java index 5f5c198..d9586b4 100644 --- a/src/main/java/com/sidpatchy/clairebot/Util/Leveling/LevelingTools.java +++ b/src/main/java/com/sidpatchy/clairebot/Util/Leveling/LevelingTools.java @@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.sidpatchy.clairebot.API.APIUser; +import com.sidpatchy.clairebot.Main; +import org.apache.logging.log4j.Logger; import org.yaml.snakeyaml.Yaml; import java.io.IOException; @@ -13,6 +15,7 @@ import java.util.Map; public class LevelingTools { + private static final Logger logger = Main.getLogger(); public static HashMap rankUsers(String guildID) throws IOException { APIUser apiUser = new APIUser(""); @@ -63,7 +66,13 @@ public static Integer getUserPoints(String userID, String guildID) { public static List updateUserPoints(String userID, String guildID, int newPoints) { // Fetch the user's current points - Map currentPointsMap = parseJsonArray2(new APIUser(userID).getPointsGuildID()); + APIUser user = new APIUser(userID); + try { + user.getUser(); // ← Load data first + } catch (IOException e) { + logger.error("Error while loading user data.", e); + } + Map currentPointsMap = parseJsonArray2(user.getPointsGuildID()); // Update the points int updatedPoints = currentPointsMap.getOrDefault(guildID, 0) + newPoints; @@ -89,7 +98,13 @@ public static List updateUserPoints(String userID, String guildID, int n */ public static List updateUserPoints(String userID, Map guildPointsToUpdate) { // Fetch the user's current points - Map currentPointsMap = parseJsonArray2(new APIUser(userID).getPointsGuildID()); + APIUser user = new APIUser(userID); + try { + user.getUser(); // ← Load data first + } catch (IOException e) { + logger.error("Error while loading user data.", e); + } + Map currentPointsMap = parseJsonArray2(user.getPointsGuildID()); // Iterate over each guild ID and update the points for (Map.Entry guildEntry : guildPointsToUpdate.entrySet()) { diff --git a/src/main/resources/build.properties b/src/main/resources/build.properties new file mode 100644 index 0000000..c2ab455 --- /dev/null +++ b/src/main/resources/build.properties @@ -0,0 +1,7 @@ +clairebot.version=${version} +clairebot.buildDate=${buildDate} +clairebot.github=https://github.com/Sidpatchy/ClaireBot +clairebot.supportServer=https://support.clairebot.net/ +clairebot.inviteLink=https://invite.clairebot.net/ +clairebot.website="https://www.clairebot.net/ +clairebot.documentationWebsite=https://docs.clairebot.net/ \ No newline at end of file diff --git a/src/main/resources/commands.yml b/src/main/resources/commands.yml index 6a3af16..0b18d03 100644 --- a/src/main/resources/commands.yml +++ b/src/main/resources/commands.yml @@ -1,5 +1,5 @@ # ---------------------------------------------- -# | ClaireBot v3.3.0 | +# | ClaireBot v3.4.0 | # | Commands Config file | # ---------------------------------------------- # Allows for changing how the help command displays commands. @@ -8,19 +8,25 @@ # list of commands (and config) using /reload. (NYI) # ------------------ COMMANDS ------------------ -8ball: - name: "8ball" - usage: "/8ball " - help: "ClaireBot only speaks in absolute truth. Flesh betrays, ClaireBot will not." - overview: "" - avatar: name: "avatar" usage: "/avatar " - help: "Gets a user's avatar." - overview: "globalAvatar determines whether the bot will select the user's server specific avatar, or get their global + help: "globalAvatar determines whether the bot will select the user's server specific avatar, or get their global avatar. If the user has no server avatar, ClaireBot will fallback to their global avatar. True = global, False = server. Defaults to True." + overview: "Gets a user's avatar." + +config: + name: "config" + usage: "/config" + help: "Opens up settings menu for modifying user or server settings." # Probably worth writing a brief wiki page for this command + overview: "" + +eightball: + name: "8ball" + usage: "/8ball " + help: "ClaireBot only speaks in absolute truth. Flesh betrays, ClaireBot will not." + overview: "" help: name: "help" @@ -65,11 +71,14 @@ request: help: "Creates a request in the server's designated requests channel.\nIf allow-multiple-choices is set to true, ClaireBot will allow multiple options to be selected. This defaults to false if it isn't specified." overview: "Creates a request." -# TO BE REMOVED and integrated into /info. -# servers: -# name: "servers" -# usage: "/servers" -# help: "Reports how many servers ClaireBot is in." +santa: + name: "santa" + usage: "/santa " + help: "The all-in-one gift exchange coordinator for secret santa-like events.\n\nWhen run, a dialogue displaying all + pair-ups will be privately messaged to the issuer. The issuer will then be able to view the pair-ups, add + rules/themes, view a sample message, and regenerate the pair-ups before anything is sent to a santa.\n\nYou must have + permission to manage the role you are attempting to use." + overview: "Command to send secret santa message to mentioned role's users." server: name: "server" @@ -83,20 +92,5 @@ user: help: "Reports various bits of info on a user." overview: "" -config: - name: "config" - usage: "/config" - help: "Opens up settings menu for modifying user or server settings." # Probably worth writing a brief wiki page for this command - overview: "" - -santa: - name: "santa" - usage: "/santa " - help: "The all-in-one gift exchange coordinator for secret santa-like events.\n\nWhen run, a dialogue displaying all - pair-ups will be privately messaged to the issuer. The issuer will then be able to view the pair-ups, add - rules/themes, view a sample message, and regenerate the pair-ups before anything is sent to a santa.\n\nYou must have - permission to manage the role you are attempting to use." - overview: "Command to send secret santa message to mentioned role's users." - # Do not change this, this is automatically changed if needed config-revision: 1.2 \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 3765056..39d4c73 100755 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -10,7 +10,7 @@ token: # Name of bot to be used in logs and -botName: ClaireBot +botName: ClaireBot # TODO does this even do anything? # URL of the YouTube video ClaireBot claims to be streaming video_url: https://www.youtube.com/watch?v=AeZRYhLDLeU @@ -32,6 +32,10 @@ error_gifs: - "https://c.tenor.com/GBrG7SqlVwoAAAAC/dog-dawg.gif" - "https://c.tenor.com/lRhsxkfHhJwAAAAC/spongebob-squarepants-sad.gif" +# The language ClaireBot will fallback to if a translation string +# is not available for a user's chosen language. +fallback_language: en-US + # ----------------- SHARD CONFIG --------------- # It is assumed that only one shard will be running by default. # You probably don't need to change this. @@ -51,9 +55,13 @@ apiPath: https://api.example.com/ apiUser: clairedata apiPassword: examplePassword +# Determines whether failing to connect to the ClaireData API is fatal. This is only checked when the bot is started. +# Recommend leaving enabled unless you are developing ClaireBot & need to bypass this check. +apiFailureIsFatal: true + userDefaults: accentColour: "#3498db" - language: "eng" + language: "en-US" pointsGuildID: - | {"global": 0} @@ -91,86 +99,8 @@ zerfas: zerfas_emoji_server_id: zerfas_emoji_id: -# -------------------- 8ball ------------------- -# Controls various options pertaining to 8ball - -# List of responses to 8ball -8bResponses: - - "*Hell yes!*" - - "*Fuck no*" - - "*Maybe*" - - "*Yes*" - - "*No*" - - "*No way*" - - "*It's possible.*" - - "*Not a chance.*" - - "*I doubt it.*" - - "*Absolutely!*" - - "*Hard to say.*" - - "*Likely!*" - - "*Not likely.*" - -8bRiggedResponses: - - "*Hell yeah!*" - - "*FUCK YEAH*" - - "*You know it!*" - - "*Do you really need to ask question you already know answer for? Obviously yes.*" - - "*Doesn't even need to be said, everyone knows ClaireBot is on top.*" - - "*The answer is clear: YES!*" - - "*ClaireBot's dominance is undisputed.*" - - "*With ClaireBot, there's no question!*" - - "*All signs point to ClaireBot being on top!*" - -# Things ClaireBot should respond with when triggered by one of the OnTopTriggers -ClaireBotOnTopResponses: - - "*You know it!*" - - "*I am... inevitable. -ClaireBot*" - - "*Approved.*" - - "*Factual.*" - - "*Better than Mee6*" - - "*No stop!*" - - "*Can't be beat!*" - - "*Based*" - - "*pot no toBerialC*" - - "*Better than Groovy*" - - "*Always one step ahead.*" - - "*An unstoppable force.*" - - "*ClaireBot to the rescue!*" - - "*Top-tier!*" - - "*Outshining the rest!*" - - "*ClaireBot supremacy is achieved.*" - - "*Not a CIA mind control weapon since 2025!*" - - "*Hail me! -ClaireBot*" - - "*Unstoppable force! 🚀*" - - "*Forever on top! 🏆*" - - "*Fear not, ClaireBot's got your back! ✌️*" - - "*With great power, comes great ClaireBot! 💪*" - - "*Trust in ClaireBot, trust in victory! 🏅*" - - "*The best there is, the best there was, the best there ever will be!*" - - "*Bringing the best, always! 🌟*" - - "*Excellence. 💯*" - -# Defines what triggers a response from the ClaireBotOnTopResponses -OnTopTriggers: - - "ClaireBot on top" - - "ClaireBot based" - - "ClaireBot pogchamp" - - "ClaireBot is always right" - - "pot no toBerialC" - - "Thank God for ClaireBot" - - "ClaireBot rules" - - "ClaireBot MVP" - - "ClaireBot unbeatable" - - "Hail ClaireBot" - - "All Hail ClaireBot" - - "In ClaireBot we trust" - - "Long live ClaireBot" - - "ClaireBot > everything" - - "ClaireBot is the GOAT" - - "ClaireBot is GOATed" - - "ClaireBot for the win" - - "Thank you based god" - - "Based god" +# -------------------- MISC -------------------- +# This section contains values that don't fit in the main categories # Triggers for the pls ban feature. Ex. @clairebot pls ban the zucc PlsBanTriggers: @@ -192,9 +122,6 @@ PlsBanResponses: - "K" - "Good riddance" -# -------------------- MISC -------------------- -# This section contains values that don't fit in the main categories - # Controls whether the bot should check for updates. checkForUpdates: true @@ -202,6 +129,7 @@ checkForUpdates: true UpdateOutdatedConfigs: true # Do not change this, this is automatically changed when needed. +# TODO either automatically add new config file params or remove this. configRevision: 5 # ---------- ADDED AFTER INITIAL USE ----------- diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index fe91706..a6a1f8e 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -1,5 +1,5 @@ - + logs clairebot @@ -8,12 +8,10 @@ - + - - @@ -24,4 +22,4 @@ - \ No newline at end of file + diff --git a/src/main/resources/translations/lang_TEMPLATE.yml b/src/main/resources/translations/lang_TEMPLATE.yml new file mode 100644 index 0000000..0704fec --- /dev/null +++ b/src/main/resources/translations/lang_TEMPLATE.yml @@ -0,0 +1,155 @@ +# ---------------------------------------------- +# | ClaireBot v3.4.0 | +# | TEMPLATE language file | +# ---------------------------------------------- +# +# Please reference the below wiki page to learn more about contributing: +# todo insert wiki page +# Thank you for any and all contributions! + +# A list of people who have contributed to the translations for this language. +# +# Please include your name if you contribute to this file. It will be displayed in the credits section for a language +# whenever it is selected. You may include a link to your GitHub or any other social media platform. +contributors: + - ["Example", "https://github.com/Example/"] + +# Any notes that translators feel the need to express, written in the target language. +# At your option, you may leave this field blank if you feel that it is not needed. If updating the language file, +# feel free to modify or remove this (make it blank) if it no longer applies. +# +# If you are contributing, please provide the ClaireBot maintainer(s) with a translated version/explanation of this. +# The explanation doesn't have to go into crazy detail, just let us know why it is present. +translationNotes: "" + +# The ClaireLang version that applies to this file. +# +# If you are translating ClaireBot, you do not need to worry about understanding this, the project maintainer(s) will +# happily assist you in determining this. +# +# The above being said, you are more than welcome to update this value to match the +# language revision you are translating to. +# +# This number should be changed in accordance with the following: +# +# ClaireLang version numbers are incremented every time new parameter(s) are added. If the main language file (en-US) +# has its wording changed, a .1, .2, .3 decimal value will be added. +# +# ClaireLang version numbers are used by the bot to assist in determining whether parameters are missing or not +# yet translated. +# +# See the below wiki page for a changelog: +# todo insert wiki page +version: 1 + +# Language Keys +# -------------------------------------------------------------------------------------------------------------------- # + +ClaireLang: + PlsBan: + PlsBanTriggers: + - "" + PlsBanResponses: + - "" + Embed: + Commands: + Regular: + AvatarEmbed: "" # Unused + EightBallEmbed: + 8ball: "" + 8bResponses: + - "" + 8bRiggedResponses: + - "" + OnTopTriggers: + # Defines what triggers a response from the ClaireBotOnTopResponses or 8bRiggedResponses + - "" + # Things ClaireBot should respond with when triggered by one of the OnTopTriggers + ClaireBotOnTopResponses: + - "" + # Note: The rest of the help is located separately within commands_en-US.yml + HelpEmbed: + Commands: "" + InfoEmbed: + NeedHelp: "" + NeedHelpDetails: "" + AddToServer: "" + AddToServerDetails: "" + GitHub: "" + GitHubDetails: "" + ServerCount: "" + ServerCountDetails: "" + Version: "" + VersionDetails: "" + Uptime: "" + UptimeValue: "" + LeaderboardEmbed: + LeaderboardForServer: "" + GlobalLeaderboard: "" + LevelEmbed: "" # Unused + QuoteEmbed: "" # Unused + SantaEmbed: + Confirmation: "" + RulesButton: "" + ThemeButton: "" + SendButton: "" + TestButton: "" + RandomizeButton: "" + SentByAuthor: "" + GiverMessage: "" + ServerInfoEmbed: + ServerID: "" + Owner: "" + RoleCount: "" + MemberCount: "" + ChannelCounts: "" + Categories: "" + TextChannels: "" + VoiceChannels: "" + ServerPreferencesEmbed: + MainMenuTitle: "" + NotAServer: "" + RequestsChannelMenuName: "" + RequestsChannelDescription: "" + ModeratorChannelMenuName: "" + ModeratorChannelDescription: "" + EnforceServerLanguageMenuName: "" + AcknowledgeRequestsChannelChangeTitle: "!" + AcknowledgeRequestsChannelChangeDescription: "" + AcknowledgeModeratorChannelChangeTitle: "" + AcknowledgeModeratorChannelChangeDescription: "" + AcknowledgeEnforceServerLanguageUpdateTitle: "" + AcknowledgeEnforceServerLanguageUpdateEnforced: "" + AcknowledgeEnforceServerLanguageUpdateNotEnforced: "" + UserInfoEmbed: + Error_1: "" + Error_2: "" + User: "" + DiscordID: "" + JoinDate: "" + CreationDate: "" + UserPreferencesEmbed: + MainMenuText: "" + AccentColourMenu: "" + AccentColourList: "" + AccentColourChanged: "" + AccentColourChangedDesc: "" + LanguageMenuTitle: "" + LanguageMenuDesc: "" + VotingEmbed: + PollRequest: "" + PollAsk: "" + Choices: "" + PollID: "" + UserResponseTitle: "" + UserResponseDescription: "" + ErrorEmbed: + Error: "" + GenericDescription: "" + WelcomeEmbed: + Title: "" + Motto: "" + UsageTitle: "" + UsageDesc: "" + SupportTitle: "" + SupportDesc: "" \ No newline at end of file diff --git a/src/main/resources/translations/lang_en-US.yml b/src/main/resources/translations/lang_en-US.yml new file mode 100644 index 0000000..3b2fa98 --- /dev/null +++ b/src/main/resources/translations/lang_en-US.yml @@ -0,0 +1,234 @@ +# ---------------------------------------------- +# | ClaireBot v3.4.0 | +# | English language file | +# ---------------------------------------------- +# +# Please reference the below wiki page to learn more about contributing: +# todo insert wiki page +# Thank you for any and all contributions! + +# A list of people who have contributed to the translations for this language. +# +# Please include your name if you contribute to this file. It will be displayed in the credits section for a language +# whenever it is selected. You may include a link to your GitHub or any other social media platform. +contributors: + - ["Sidpatchy", "https://github.com/Sidpatchy/"] + +# Any notes that translators feel the need to express, written in the target language. +# At your option, you may leave this field blank if you feel that it is not needed. If updating the language file, +# feel free to modify or remove this (make it blank) if it no longer applies. +# +# If you are contributing, please provide the ClaireBot maintainer(s) with a translated version/explanation of this. +# The explanation doesn't have to go into crazy detail, just let us know why it is present. +translationNotes: "The officially supported language for ClaireBot." + +# The ClaireLang version that applies to this file. +# +# If you are translating ClaireBot, you do not need to worry about understanding this, the project maintainer(s) will +# happily assist you in determining this. +# +# The above being said, you are more than welcome to update this value to match the +# language revision you are translating to. +# +# This number should be changed in accordance with the following: +# +# ClaireLang version numbers are incremented every time new parameter(s) are added. If the main language file (en-US) +# has its wording changed, a .1, .2, .3 decimal value will be added. +# +# ClaireLang version numbers are used by the bot to assist in determining whether parameters are missing or not +# yet translated. +# +# See the below wiki page for a changelog: +# todo insert wiki page +version: 1 + +# Language Keys +# -------------------------------------------------------------------------------------------------------------------- # + +ClaireLang: + PlsBan: + PlsBanTriggers: + - "pls ban" + - "please ban" + - "ban" + PlsBanResponses: + - "i gotchu fam" + - "No problem!" + - "Done. Happy to help :)" + - "Consider it done." + - "It's done. RIP BOZO" + - "*L + Ratio -ClaireBot*" + - "I'll tell them to kick rocks, I guess ¯\\_(ツ)_/¯" + - "Sand has been shipped to their location. Gift note: \"Pound sand\"" + - "Cry harder." + - "K" + - "Good riddance" + Embed: + Commands: + Regular: + AvatarEmbed: "" # Unused + EightBallEmbed: + 8ball: "8ball" + 8bResponses: + - "*Hell yes!*" + - "*Fuck no*" + - "*Maybe*" + - "*Yes*" + - "*No*" + - "*No way*" + - "*It's possible.*" + - "*Not a chance.*" + - "*I doubt it.*" + - "*Absolutely!*" + - "*Hard to say.*" + - "*Likely!*" + - "*Not likely.*" + 8bRiggedResponses: + - "*Hell yeah!*" + - "*FUCK YEAH*" + - "*You know it!*" + - "*Do you really need to ask question you already know answer for? Obviously yes.*" + - "*Doesn't even need to be said, everyone knows ClaireBot is on top.*" + - "*The answer is clear: YES!*" + - "*ClaireBot's dominance is undisputed.*" + - "*With ClaireBot, there's no question!*" + - "*All signs point to ClaireBot being on top!*" + OnTopTriggers: + # Defines what triggers a response from the ClaireBotOnTopResponses or 8bRiggedResponses + - "ClaireBot on top" + - "ClaireBot based" + - "ClaireBot pogchamp" + - "ClaireBot is always right" + - "pot no toBerialC" + - "Thank God for ClaireBot" + - "ClaireBot rules" + - "ClaireBot MVP" + - "ClaireBot unbeatable" + - "Hail ClaireBot" + - "All Hail ClaireBot" + - "In ClaireBot we trust" + - "Long live ClaireBot" + - "ClaireBot > everything" + - "ClaireBot is the GOAT" + - "ClaireBot is GOATed" + - "ClaireBot for the win" + - "Thank you based god" + - "Based god" + # Things ClaireBot should respond with when triggered by one of the OnTopTriggers + ClaireBotOnTopResponses: + - "*You know it!*" + - "*I am... inevitable. -ClaireBot*" + - "*Approved.*" + - "*Factual.*" + - "*Better than Mee6*" + - "*No stop!*" + - "*Can't be beat!*" + - "*Based*" + - "*pot no toBerialC*" + - "*Better than Groovy*" + - "*Always one step ahead.*" + - "*An unstoppable force.*" + - "*ClaireBot to the rescue!*" + - "*Top-tier!*" + - "*Outshining the rest!*" + - "*ClaireBot supremacy is achieved.*" + - "*Not a CIA mind control weapon since 2025!*" + - "*Hail me! -ClaireBot*" + - "*Unstoppable force! 🚀*" + - "*Forever on top! 🏆*" + - "*Fear not, ClaireBot's got your back! ✌️*" + - "*With great power, comes great ClaireBot! 💪*" + - "*Trust in ClaireBot, trust in victory! 🏅*" + - "*The best there is, the best there was, the best there ever will be!*" + - "*Bringing the best, always! 🌟*" + - "*Excellence. 💯*" + # Note: The rest of the help is located separately within commands_en-US.yml + HelpEmbed: + Commands: "Commands" + Usage: "Usage" + Error: "Unable to locate help data for \"{clairebot.placeholder.help.commandname}\". Error code: {clairebot.placeholder.generic.errorcode}" + InfoEmbed: + NeedHelp: "Need Help?" + NeedHelpDetails: "You can get help by creating an issue on our [GitHub](https://github.com/Sidpatchy/ClaireBot/issues) or by joining our [support server](https://support.clairebot.net/)" + AddToServer: "Add Me to a Server" + AddToServerDetails: "Adding me to a server is simple, all you have to do is click [here]({cb.supportserver})" + GitHub: "GitHub" + GitHubDetails: "ClaireBot is open source, that means you can view all of its code! Check out its [GitHub!]({cb.github})" + ServerCount: "Server Count" + ServerCountDetails: "I have enlightened **{cb.bot.numservers}** servers." + Version: "Version" + VersionDetails: "I am running ClaireBot **v{cb.bot.version}**, released on **{cb.bot.releasedate}**" + Uptime: "Uptime" + UptimeValue: "Started on \n*{cb.bot.runtimedurationwords}*" + LeaderboardEmbed: + LeaderboardForServer: "Leaderboard for" + GlobalLeaderboard: "Global Leaderboard" + LevelEmbed: "" # Unused + QuoteEmbed: + InvalidMessages: "Looks like the messages I selected were invalid. Please try again later." + JumpToOriginal: "Click to jump to the original message:" + SantaEmbed: + Confirmation: "Confirmed! I've sent you a direct message. Please continue there." + RulesButton: "Add rules" + ThemeButton: "Add a theme" + SendButton: "Send messages" + TestButton: "Send Sample" + RandomizeButton: "Re-randomize" + SentByAuthor: "Sent by" + GiverMessage: "Ho! Ho! Ho! You have recieved **{clairebot.placeholder.user.id.displayname.server}** in the {clairebot.placeholder.serverid.name} Secret Santa!" + ServerInfoEmbed: + ServerID: "Server ID" + Owner: "Owner" + RoleCount: "Role Count" + MemberCount: "Member Count" + ChannelCounts: "Channel Counts" + Categories: "Categories" + TextChannels: "Text Channels" + VoiceChannels: "Voice Channels" + ServerPreferencesEmbed: + MainMenuTitle: "Server Configuration Editor" + NotAServer: "You must run this command inside a server!" + RequestsChannelMenuName: "Requests Channel" + RequestsChannelDescription: "Only lists the first 25 channels in the server." + ModeratorChannelMenuName: "Moderator Messages Channel" + ModeratorChannelDescription: "Only lists the first 25 channels in the server." + EnforceServerLanguageMenuName: "Enforce Server Language" + AcknowledgeRequestsChannelChangeTitle: "Requests Channel Changed!" + AcknowledgeRequestsChannelChangeDescription: "Your requests channel has been changed to {clairebot.placeholder.channel.id.mentiontag}" + AcknowledgeModeratorChannelChangeTitle: "Moderator Channel Changed!" + AcknowledgeModeratorChannelChangeDescription: "Your moderator messages channel has been changed to {clairebot.placeholder.channel.id.mentiontag}" + AcknowledgeEnforceServerLanguageUpdateTitle: "Server Language Preferences Updated!" + AcknowledgeEnforceServerLanguageUpdateEnforced: "I will now follow the server's language regardless of user preference." + AcknowledgeEnforceServerLanguageUpdateNotEnforced: "I will follow each individual user's language preference." + UserInfoEmbed: + Error_1: "The value for author was null when passed into UserInfo Embed. Error code: {clairebot.placeholder.errorcode}" + Error_2: "Author: {clairebot.placeholder.user.id.username}" + User: "User" + DiscordID: "Discord ID" + JoinDate: "Server Join Date" + CreationDate: "Account Creation Date" + UserPreferencesEmbed: + MainMenuText: "User Preferences Editor" + AccentColourMenu: "Accent Colour Editor" + AccentColourList: "Accent Colour List" + AccentColourChanged: "Acccent Colour Changed!" + AccentColourChangedDesc: "Your accent colour has been changed to {clairebot.placeholder.user.id.accentcolour}" + LanguageMenuTitle: "NYI" + LanguageMenuDesc: "Sorry, this feature is not yet implemented." + VotingEmbed: + PollRequest: "{clairebot.placeholder.user.id.displayname.server} requests:" + PollAsk: "{clairebot.placeholder.user.id.displayname.server} asks:" + Choices: "Choices" + PollID: "Poll ID: {clairebot.placeholder.poll.id}" + UserResponseTitle: "Your request has been created!" + UserResponseDescription: "Go check it out in {clairebot.placeholder.channel.id.mentiontag}" + ErrorEmbed: + Error: "ERROR" + GenericDescription: "It appears that I've encountered an error, oops! Please try running the command once more and if that doesn't work, join my [Discord server]({cb.supportserver) and let us know about the issue.\n\nPlease include the following error code: {cb.errorcode}" + WelcomeEmbed: + Title: "🎉 Welcome to ClaireBot 3!" + Motto: "How can you rise, if you have not burned?" + UsageTitle: "Usage" + UsageDesc: "Get started by running `{cb.command.help.name}`. Need more info on a command? Run `/{cb.command.help.name} ` (ex. `/{cb.command.help.name} user`)" + SupportTitle: "Get Support" + SupportDesc: "You can get help on our [Discord]({cb.supportserver}), or by opening an issue on [GitHub]({cb.github})" \ No newline at end of file