diff --git a/Writerside/c.list b/Writerside/c.list
new file mode 100644
index 0000000..c4c77a2
--- /dev/null
+++ b/Writerside/c.list
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Writerside/cd.tree b/Writerside/cd.tree
new file mode 100644
index 0000000..7c27a81
--- /dev/null
+++ b/Writerside/cd.tree
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Writerside/cfg/buildprofiles.xml b/Writerside/cfg/buildprofiles.xml
new file mode 100644
index 0000000..1edc2ec
--- /dev/null
+++ b/Writerside/cfg/buildprofiles.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+ true
+
+
+
+
diff --git a/Writerside/cfg/glossary.xml b/Writerside/cfg/glossary.xml
new file mode 100644
index 0000000..22bec6b
--- /dev/null
+++ b/Writerside/cfg/glossary.xml
@@ -0,0 +1,7 @@
+
+
+
+
+ Description of what "foo" is.
+
+
\ No newline at end of file
diff --git a/Writerside/redirection-rules.xml b/Writerside/redirection-rules.xml
new file mode 100644
index 0000000..6a7071a
--- /dev/null
+++ b/Writerside/redirection-rules.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+ Created after removal of "wasd" from ClaireBot Docs
+ wasd.html
+
+
+ Created after removal of "Command" from ClaireBot Docs
+ Command.html
+
+
\ No newline at end of file
diff --git a/Writerside/topics/8ball.md b/Writerside/topics/8ball.md
new file mode 100644
index 0000000..dff5e0e
--- /dev/null
+++ b/Writerside/topics/8ball.md
@@ -0,0 +1,21 @@
+# /8ball
+
+8ball is exactly what it sounds like, though it's worth noting that ClaireBot only speaks in truth. Remember kids,
+flesh betrays, ClaireBot will not.
+
+## Command
+
+### Syntax
+
+```shell
+/8ball [query]
+```
+
+### Options
+
+query
+: The question you wish to consult ClaireBot about.
+
+
+
+
\ No newline at end of file
diff --git a/Writerside/topics/Archived-Overview.md b/Writerside/topics/Archived-Overview.md
new file mode 100644
index 0000000..4239386
--- /dev/null
+++ b/Writerside/topics/Archived-Overview.md
@@ -0,0 +1,3 @@
+# Archived
+
+This section contains various bits and bobs that are not particularly relevant anymore, but are retained for posterity.
\ No newline at end of file
diff --git a/Writerside/topics/Beyond-Javacord.md b/Writerside/topics/Beyond-Javacord.md
new file mode 100644
index 0000000..49b83ce
--- /dev/null
+++ b/Writerside/topics/Beyond-Javacord.md
@@ -0,0 +1,24 @@
+# Beyond Javacord
+
+Since day one, ClaireBot 3 has been based on Javacord due to it being the easiest Java-based Discord API wrapper
+to work with. As far as I am concerned, that remains true today. On top of that, it has great documentation
+and a lovely community.
+
+Despite how much I like Javacord, I recognize one major issue with it:
+it's development has come to a complete standstill.
+
+**This leaves me with two realistic options**:
+1. Contribute to Javacord and work to bring it up to date.
+2. Migrate to a different API wrapper (JDA, Discord4J, etc.)
+
+## Option #1: Contributing to Javacord
+In an ideal world, this is the path I'd choose, however, Javacord is quite out of date at this point, and I'd rather
+put that effort into improving ClaireBot or working on one of my various other hobbies... which I have too many of.
+
+Javacord is currently using the latest Discord API version (v10, see: [Javacord.java](https://github.com/Javacord/Javacord/blob/aa5afd1dde791a8811ccdbc881b44d29cb629699/javacord-api/src/main/java/org/javacord/api/Javacord.java#L95) and [Discord Develpoer Portal](https://discord.com/developers/docs/reference)),
+sooooo it wouldn't be completely out of the realm of reason to implement new features.
+
+## Option #2:
+From a quick glance, Discord4J seems like the best option if ClaireBot were to switch libraries. It uses Spring's Mono
+classes which _can_ be converted to CompletableFuture. This would hopefully eliminate the need to do major rewrites to
+large parts of ClaireBot as structure could remain quite similar.
\ No newline at end of file
diff --git a/Writerside/topics/Contributing-guide.md b/Writerside/topics/Contributing-guide.md
new file mode 100644
index 0000000..405d0af
--- /dev/null
+++ b/Writerside/topics/Contributing-guide.md
@@ -0,0 +1,178 @@
+# Contributing
+
+We welcome pull requests!
+
+
+# Contributing to ClaireBot
+
+First off, thanks for taking the time to contribute! ❤️
+
+All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
+
+> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
+> - Star the project
+> - Tweet about it
+> - Refer this project in your project's readme
+> - Mention the project at local meetups and tell your friends/colleagues
+
+
+## Table of Contents
+
+- [I Have a Question](#i-have-a-question)
+ - [I Want To Contribute](#i-want-to-contribute)
+ - [Reporting Bugs](#reporting-bugs)
+ - [Suggesting Enhancements](#suggesting-enhancements)
+ - [Your First Code Contribution](#your-first-code-contribution)
+ - [Improving The Documentation](#documentation)
+- [Styleguide](#style-guides)
+ - [Commit Messages](#commit-messages)
+ - [Documentation](#documentation)
+- [Join The Project Team](#join-the-project-team)
+
+
+
+## I Have a Question
+
+> If you want to ask a question, we assume that you have read the available [Documentation](https://docs.clairebot.net).
+
+Before you ask a question, it is best to search for existing [Issues](https://github.com/Sidpatchy/ClaireBot/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.
+
+If you then still feel the need to ask a question and need clarification, we recommend the following:
+
+- Open an [Issue](https://github.com/Sidpatchy/ClaireBot/issues/new).
+- Provide as much context as you can about what you're running into.
+- Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant.
+
+We will then take care of the issue as soon as possible.
+
+
+
+## I Want To Contribute
+
+> ### Legal Notice
+> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project licence.
+
+### Reporting Bugs
+
+
+#### Before Submitting a Bug Report
+
+A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.
+
+- Make sure that you are using the latest version.
+- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://docs.clairebot.net). If you are looking for support, you might want to check [this section](#i-have-a-question)).
+- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/Sidpatchy/ClaireBot/issues?q=label%3Abug).
+- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.
+- Collect information about the bug:
+ - Stack trace (Traceback)
+ - OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
+ - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
+ - Possibly your input and the output
+ - Can you reliably reproduce the issue? And can you also reproduce it with older versions?
+
+
+#### How Do I Submit a Good Bug Report?
+
+> You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to .
+
+
+We use GitHub issues to track bugs and errors. If you run into an issue with the project:
+
+- Open an [Issue](https://github.com/Sidpatchy/ClaireBot/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.)
+- Explain the behavior you would expect and the actual behavior.
+- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case.
+- Provide the information you collected in the previous section.
+
+Once it's filed:
+
+- The project team will label the issue accordingly.
+- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced.
+- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution).
+
+
+
+
+### Suggesting Enhancements
+
+This section guides you through submitting an enhancement suggestion for ClaireBot, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions.
+
+
+#### Before Submitting an Enhancement
+
+- Make sure that you are using the latest version.
+- Read the [documentation](https://docs.clairebot.net) carefully and find out if the functionality is already covered, maybe by an individual configuration.
+- Perform a [search](https://github.com/Sidpatchy/ClaireBot/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
+- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we prefer features that will be useful to the majority of our users and not just a small subset. Features like these will be prioritized lower.
+
+
+#### How Do I Submit a Good Enhancement Suggestion?
+
+Enhancement suggestions are tracked as [GitHub issues](https://github.com/Sidpatchy/ClaireBot/issues).
+
+- Use a **clear and descriptive title** for the issue to identify the suggestion.
+- Provide a **step-by-step description of the suggested enhancement** in as many details as possible.
+- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you.
+- You may want to **include screenshots or screen recordings** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [LICEcap](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and the built-in [screen recorder in GNOME](https://help.gnome.org/users/gnome-help/stable/screen-shot-record.html.en) or [SimpleScreenRecorder](https://github.com/MaartenBaert/ssr) on Linux.
+- **Explain why this enhancement would be useful** to most ClaireBot users. You may also want to point out the other projects that solved it better and which could serve as inspiration.
+
+
+
+### Your First Code Contribution
+
+
+ClaireBot is primarily developed using IntelliJ IDEA. Other IDEs are acceptable, they are just not documented here due
+to the maintainer's unfamiliarity with them.
+
+#### Setting Up a Working Copy of ClaireBot
+You have two main options for setting up ClaireBot.
+
+##### ClaireBot Docker
+The easiest way to set up ClaireBot/ClaireData is to use the Docker container provided at
+[Sidpatchy/ClaireBot-Docker](https://github.com/Sidpatchy/ClaireBot-Docker). The repo includes documentation in the
+README.md for getting started. It is written with Linux in mind, so if you're on Windows, you'll want to look into
+WSL2 or Docker Desktop.
+
+##### Manually Setting Up ClaireBot
+
+
+## Style Guides
+### Commit Messages
+We prefer [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) to keep changelogs tidy:
+```
+feat: A new feature
+fix: A bug fix
+docs: Documentation changes
+style: Code formatting or style adjustments (no functional changes)
+refactor: Code restructuring without altering behavior
+test: Adding or modifying tests
+chore: Maintenance tasks or tooling updates
+```
+
+### Documentation
+- Submit pull requests to [Sidpatchy/ClaireBot-docs](https://github.com/Sidpatchy/ClaireBot-Docs)
+- Avoid using terms like 'simply', 'easy', and 'just'
+ - If someone's reading the docs, it's not 'simple'
+ - See: [https://justsimply.dev/](https://justsimply.dev/)
+
+## Join The Project Team
+
+
+
+## Attribution
+This guide is based on the [contributing.md](https://contributing.md/generator)!
\ No newline at end of file
diff --git a/Writerside/topics/Interface-Standardization-1.md b/Writerside/topics/Interface-Standardization-1.md
new file mode 100644
index 0000000..79237c1
--- /dev/null
+++ b/Writerside/topics/Interface-Standardization-1.md
@@ -0,0 +1,23 @@
+# Interface Standardization and Updates #1
+A place to document various interface changes that need to occur.
+
+## Issue #1
+ClaireBot currently uses a mix of CamelCase and kebab-case in user-facing resources. This should be standardized
+to kebab-case.
+
+Internal naming will retain CamelCase (variable names, etc.). All user-facing elements need to be transitioned
+to kebab-casing.
+
+## Issue #2
+/help command info should include a link to the relevant documentation webpage. This is blocked pending completion of
+the documentation for all commands.
+
+Will either need to update Robin's implementation of the commands.yml standard, or just pull that implementation into
+ClaireBot.
+
+## Issue #3
+What happens if there is no requests channel??? I'm pretty sure ClaireBot will just throw an error and to the user,
+fail completely silently. This is very poor user experience.
+
+## Issue #4
+/user doesn't do very much right now. More fields should be considered for addition.
\ No newline at end of file
diff --git a/Writerside/topics/Requests-Channel.md b/Writerside/topics/Requests-Channel.md
new file mode 100644
index 0000000..5edb1fe
--- /dev/null
+++ b/Writerside/topics/Requests-Channel.md
@@ -0,0 +1,27 @@
+# Requests Channel
+
+The requests channel for a server is determined via the following steps:
+
+
Getting a Request Channel:
+
+
Step 1: Check ClaireData API
+
+
Success: Use the configured channel
+
Failure: Move to Step 2
+
+
+
Step 2: Search for Default Channel
+
+
Look for channel named "requests"
+
+
Found: Use this channel
+
Not Found: Request fails
+
+
+
+
+
+
+
+
+If no channel is found, the /request will fail.
\ No newline at end of file
diff --git a/Writerside/topics/avatar.md b/Writerside/topics/avatar.md
new file mode 100644
index 0000000..f1d858d
--- /dev/null
+++ b/Writerside/topics/avatar.md
@@ -0,0 +1,29 @@
+# /avatar
+
+Command to display a user's profile image.
+
+## Command
+
+### Syntax
+
+```shell
+/avatar [optional: user] [optional: globalAvatar]
+```
+
+### Options
+
+user (optional)
+: The user whose avatar you'd like to display.
+ If left blank, ClaireBot will display the avatar of the user who issued the command.
+
+
+globalAvatar (optional)
+: If left blank, defaults to true Options:
+- **True**: ClaireBot will display the user's global avatar.
+- **False**: ClaireBot will display the user's server-specific avatar.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Writerside/topics/config-server.md b/Writerside/topics/config-server.md
new file mode 100644
index 0000000..032acfa
--- /dev/null
+++ b/Writerside/topics/config-server.md
@@ -0,0 +1,31 @@
+# /config server
+
+Opens up the server settings page.
+
+## Command
+
+### Syntax
+
+```shell
+/config server
+```
+
+### Options
+
+Requests Channel
+: Allows server administrators to choose where ClaireBot should post users' requests. Lists out the first 25 channels
+in the server's channel list.
+
+Moderator Messages Channel
+: Allows server administrators to choose where ClaireBot should send moderator messages. Lists out the first 25 channels
+in the server's channel list.
+
+Enforce Server Language
+: Allows server administrators to force ClaireBot to use the same language as the server's region settings dictate.
+
**Options**:
+- **True**: ClaireBot will enforce the server's language.
+- **False**: ClaireBot will respect the user's preferences and use their preferred language.
+
+
+
+
\ No newline at end of file
diff --git a/Writerside/topics/config-user.md b/Writerside/topics/config-user.md
new file mode 100644
index 0000000..f797026
--- /dev/null
+++ b/Writerside/topics/config-user.md
@@ -0,0 +1,27 @@
+# /config user
+
+Opens up the user preferences page.
+
+## Command
+
+### Syntax
+
+```shell
+/config user
+```
+
+### Options
+
+Accent Colour
+: For those who hate the colour blue. Allows you to change the accent colour of embeds in ClaireBot's
+responses. Options:
+- **Select Common Colours**: Allows you to select a list of pre-configured colours.
+- **Hexadecimal Entry**: Allows you to enter any hexadecimal (#000000) colour code.
+
+Language
+: For those who are offended by English–or those who prefer a different language.
+
This feature isn't yet implemented. It is currently planned for ClaireBot v3.4.
+
+
+
+
\ No newline at end of file
diff --git a/Writerside/topics/config.md b/Writerside/topics/config.md
new file mode 100644
index 0000000..1953e96
--- /dev/null
+++ b/Writerside/topics/config.md
@@ -0,0 +1,26 @@
+# /config
+
+Interactive method for configuring ClaireBot. Allows for configuring both user and server settings.
+
+#### See also
+- [/config user](config-user.md)
+- [/config server](config-server.md)
+
+## Command
+
+### Syntax
+
+```shell
+/config [optional: mode]
+```
+
+### Options
+
+mode (optional)
+: The configuration section you want to ender. Options:
+- User (default): Configuration options for users, see: [/config user](config-user.md)
+- Server: Configuration options for servers, see: [/config server](config-server.md)
+
+
+
+
\ No newline at end of file
diff --git a/Writerside/topics/help.md b/Writerside/topics/help.md
new file mode 100644
index 0000000..eb7ad97
--- /dev/null
+++ b/Writerside/topics/help.md
@@ -0,0 +1,20 @@
+# /help
+
+Displays info about a given command.
+
+## Command
+
+### Syntax
+
+```shell
+/help [optional: command-name]
+```
+
+### Options
+
+command-name (optional)
+: If specified ClaireBot will provide details on the specified command.
+
+
+
+
\ No newline at end of file
diff --git a/Writerside/topics/info.md b/Writerside/topics/info.md
new file mode 100644
index 0000000..5a82b4e
--- /dev/null
+++ b/Writerside/topics/info.md
@@ -0,0 +1,21 @@
+# /info
+
+The info command reports several details about ClaireBot, including:
+- Where to find help
+- How to add ClaireBot to a server
+- A link to ClaireBot's source code
+- How many servers ClaireBot is a member of
+- ClaireBot's currently running version and release date.
+- ClaireBot's uptime.
+
+## Command
+
+### Syntax
+
+```shell
+/info
+```
+
+
+
+
\ No newline at end of file
diff --git a/Writerside/topics/leaderboard.md b/Writerside/topics/leaderboard.md
new file mode 100644
index 0000000..34ecf92
--- /dev/null
+++ b/Writerside/topics/leaderboard.md
@@ -0,0 +1,23 @@
+# /leaderboard
+
+8ball is exactly what it sounds like, though it's worth noting that ClaireBot only speaks in truth. Remember kids,
+flesh betrays, ClaireBot will not.
+
+## Command
+
+### Syntax
+
+```shell
+/leaderboard [optional: global]
+```
+
+### Options
+
+global (optional)
+: If left blank, defaults to False. Options:
+- **True**: Displays ClaireBot's global leaderboard.
+- **False**: Displays ClaireBot's leaderboard for the server the command was executed in.
+
+
+
+
\ No newline at end of file
diff --git a/Writerside/topics/level.md b/Writerside/topics/level.md
new file mode 100644
index 0000000..9535152
--- /dev/null
+++ b/Writerside/topics/level.md
@@ -0,0 +1,22 @@
+# /level
+
+Displays your ClaireBot level. This is the same thing that would be displayed on the [/leaderboard](leaderboard.md) if
+you (or the queried user) are ranked high enough.
+
+## Command
+
+### Syntax
+
+```shell
+/8ball [optional: user]
+```
+
+### Options
+
+user (optional)
+: The user you wish to get the level of.
+ If left blank, ClaireBot will report the level of whoever executed the command.
+
+
+
+
\ No newline at end of file
diff --git a/Writerside/topics/openapi.yml/API_Reference.md b/Writerside/topics/openapi.yml/API_Reference.md
new file mode 100644
index 0000000..774437f
--- /dev/null
+++ b/Writerside/topics/openapi.yml/API_Reference.md
@@ -0,0 +1,17 @@
+# API Reference
+
+| Endpoint | Method | Description | Documentation |
+|-------------------------------|--------|---------------------|-----------------------------------------|
+| ```/api/v1/user``` | GET | Retrieve all users | [Get all users](Get_all_users.md) |
+| ```/api/v1/user``` | POST | Create a new user | [Create new user](Create_new_user.md) |
+| ```/api/v1/user/{userID}``` | GET | Get user by ID | [Get used by ID](Get_user_by_ID.md) |
+| ```/api/v1/user/{userID}``` | PUT | Update user | [Update user](Update_user.md) |
+| ```/api/v1/user/{userID}``` | DELETE | Delete user | [Delete user](Delete_user.md) |
+| ```/api/v1/guild``` | GET | Retrieve all guilds | [Get all guilds](Get_all_guilds.md) |
+| ```/api/v1/guild``` | POST | Create a new guild | [Create new guild](Create_new_guild.md) |
+| ```/api/v1/guild/{guildID}``` | GET | Get guild by ID | [Get guild by ID](Get_guild_by_ID.md) |
+| ```/api/v1/guild/{guildID}``` | PUT | Update guild | [Update guild](Update_guild.md) |
+| ```/api/v1/guild/{guildID}``` | DELETE | Delete guild | [Delete guild](Delete_guild.md) |
+
+All endpoints require Basic Authentication and accept/return JSON data.
+
diff --git a/Writerside/topics/openapi.yml/Create_new_guild.md b/Writerside/topics/openapi.yml/Create_new_guild.md
new file mode 100644
index 0000000..47b127f
--- /dev/null
+++ b/Writerside/topics/openapi.yml/Create_new_guild.md
@@ -0,0 +1,3 @@
+# Create new guild
+
+
\ No newline at end of file
diff --git a/Writerside/topics/openapi.yml/Create_new_user.md b/Writerside/topics/openapi.yml/Create_new_user.md
new file mode 100644
index 0000000..b71aeb1
--- /dev/null
+++ b/Writerside/topics/openapi.yml/Create_new_user.md
@@ -0,0 +1,3 @@
+# Create new user
+
+
\ No newline at end of file
diff --git a/Writerside/topics/openapi.yml/Delete_guild.md b/Writerside/topics/openapi.yml/Delete_guild.md
new file mode 100644
index 0000000..55d3db6
--- /dev/null
+++ b/Writerside/topics/openapi.yml/Delete_guild.md
@@ -0,0 +1,3 @@
+# Delete guild
+
+
\ No newline at end of file
diff --git a/Writerside/topics/openapi.yml/Delete_user.md b/Writerside/topics/openapi.yml/Delete_user.md
new file mode 100644
index 0000000..f989284
--- /dev/null
+++ b/Writerside/topics/openapi.yml/Delete_user.md
@@ -0,0 +1,3 @@
+# Delete user
+
+
\ No newline at end of file
diff --git a/Writerside/topics/openapi.yml/Get_all_guilds.md b/Writerside/topics/openapi.yml/Get_all_guilds.md
new file mode 100644
index 0000000..0ce3f7c
--- /dev/null
+++ b/Writerside/topics/openapi.yml/Get_all_guilds.md
@@ -0,0 +1,3 @@
+# Get all guilds
+
+
\ No newline at end of file
diff --git a/Writerside/topics/openapi.yml/Get_all_users.md b/Writerside/topics/openapi.yml/Get_all_users.md
new file mode 100644
index 0000000..e00cbec
--- /dev/null
+++ b/Writerside/topics/openapi.yml/Get_all_users.md
@@ -0,0 +1,3 @@
+# Get all users
+
+
\ No newline at end of file
diff --git a/Writerside/topics/openapi.yml/Get_guild_by_ID.md b/Writerside/topics/openapi.yml/Get_guild_by_ID.md
new file mode 100644
index 0000000..6c4e8ac
--- /dev/null
+++ b/Writerside/topics/openapi.yml/Get_guild_by_ID.md
@@ -0,0 +1,3 @@
+# Get guild by ID
+
+
\ No newline at end of file
diff --git a/Writerside/topics/openapi.yml/Get_user_by_ID.md b/Writerside/topics/openapi.yml/Get_user_by_ID.md
new file mode 100644
index 0000000..e855ce5
--- /dev/null
+++ b/Writerside/topics/openapi.yml/Get_user_by_ID.md
@@ -0,0 +1,3 @@
+# Get user by ID
+
+
\ No newline at end of file
diff --git a/Writerside/topics/openapi.yml/Update_guild.md b/Writerside/topics/openapi.yml/Update_guild.md
new file mode 100644
index 0000000..4d3e924
--- /dev/null
+++ b/Writerside/topics/openapi.yml/Update_guild.md
@@ -0,0 +1,3 @@
+# Update guild
+
+
\ No newline at end of file
diff --git a/Writerside/topics/openapi.yml/Update_user.md b/Writerside/topics/openapi.yml/Update_user.md
new file mode 100644
index 0000000..3a7e726
--- /dev/null
+++ b/Writerside/topics/openapi.yml/Update_user.md
@@ -0,0 +1,3 @@
+# Update user
+
+
\ No newline at end of file
diff --git a/Writerside/topics/openapi.yml/openapi.yml b/Writerside/topics/openapi.yml/openapi.yml
new file mode 100644
index 0000000..1b3bdc1
--- /dev/null
+++ b/Writerside/topics/openapi.yml/openapi.yml
@@ -0,0 +1,247 @@
+openapi: 3.0.0
+info:
+ title: ClaireData API
+ version: 1.0.0
+ description: Spring Boot REST API for managing users and guilds
+
+tags:
+ - name: User Controller
+ description: Endpoints for user management
+ - name: Guild Controller
+ description: Endpoints for guild management
+
+paths:
+ /api/v1/user:
+ get:
+ tags:
+ - User Controller
+ operationId: getAllUsers
+ summary: Get all users
+ responses:
+ '200':
+ description: List of all users retrieved successfully
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/User'
+
+ post:
+ tags:
+ - User Controller
+ operationId: addUser
+ summary: Create new user
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/User'
+ responses:
+ '200':
+ description: User created successfully
+
+ /api/v1/user/{userID}:
+ get:
+ tags:
+ - User Controller
+ operationId: getUserById
+ summary: Get user by ID
+ parameters:
+ - name: userID
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: User found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/User'
+ '404':
+ description: User not found
+
+ put:
+ tags:
+ - User Controller
+ operationId: updateUser
+ summary: Update user
+ parameters:
+ - name: userID
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/User'
+ responses:
+ '200':
+ description: User updated successfully
+
+ delete:
+ tags:
+ - User Controller
+ operationId: deleteUserById
+ summary: Delete user
+ parameters:
+ - name: userID
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: User deleted successfully
+
+ /api/v1/guild:
+ get:
+ tags:
+ - Guild Controller
+ operationId: getAllGuilds
+ summary: Get all guilds
+ responses:
+ '200':
+ description: List of all guilds retrieved successfully
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Guild'
+
+ post:
+ tags:
+ - Guild Controller
+ operationId: addGuild
+ summary: Create new guild
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Guild'
+ responses:
+ '200':
+ description: Guild created successfully
+
+ /api/v1/guild/{guildID}:
+ get:
+ tags:
+ - Guild Controller
+ operationId: getGuildById
+ summary: Get guild by ID
+ parameters:
+ - name: guildID
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Guild found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Guild'
+ '404':
+ description: Guild not found
+
+ put:
+ tags:
+ - Guild Controller
+ operationId: updateGuild
+ summary: Update guild
+ parameters:
+ - name: guildID
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Guild'
+ responses:
+ '200':
+ description: Guild updated successfully
+
+ delete:
+ tags:
+ - Guild Controller
+ operationId: deleteGuildById
+ summary: Delete guild
+ parameters:
+ - name: guildID
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Guild deleted successfully
+
+components:
+ schemas:
+ User:
+ type: object
+ required:
+ - userID
+ properties:
+ userID:
+ type: string
+ accentColour:
+ type: string
+ description: Hex color code with '#' prefix
+ example: "#FF0000"
+ language:
+ type: string
+ description: ISO 639-3 language code
+ example: "eng"
+ pointsGuildID:
+ type: array
+ items:
+ type: string
+ pointsMessages:
+ type: array
+ items:
+ type: integer
+ deprecated: true
+ pointsVoiceChat:
+ type: array
+ items:
+ type: integer
+ deprecated: true
+
+ Guild:
+ type: object
+ required:
+ - guildID
+ properties:
+ guildID:
+ type: string
+ requestsChannelID:
+ type: string
+ description: Discord channel ID for requests
+ moderatorMessagesChannelID:
+ type: string
+ description: Discord channel ID for moderator messages
+ enforceServerLanguage:
+ type: boolean
+ description: Whether to enforce server language settings
+
+ securitySchemes:
+ BasicAuth:
+ type: http
+ scheme: basic
+
+security:
+ - BasicAuth: []
diff --git a/Writerside/topics/overview.md b/Writerside/topics/overview.md
new file mode 100644
index 0000000..3d759d5
--- /dev/null
+++ b/Writerside/topics/overview.md
@@ -0,0 +1,70 @@
+# Overview
+
+> This documentation site is currently a work in progress. While efforts have been made to ensure accuracy, some
+> details may be incomplete or subject to change. Critical information should be verified independently. Once the
+> initial version is finalized, the documentation will be open-sourced and available at
+> [Sidpatchy/ClaireBot-Docs](https://github.com/Sidpatchy/ClaireBot-Docs).
+{style="warning"}
+
+Documentation for ClaireBot, ClaireData, and ClaireBot Docker all in one place, authored using Jetbrains Writerside.
+
+To contribute to these docs, or ClaireBot in general, please see: [Contributing](Contributing-guide.md)
+
+## What is ClaireBot?
+
+ClaireBot is a Discord bot written in Java with a focus on ease-of-use and beauty.
+
+### Background
+ClaireBot 1 and 2 were initially built for a group of friends to use, and it was only used on a handful of servers.
+
+As time went on, I grew more and more interested in using ClaireBot elsewhere, in my public servers, so I rewrote bits
+and pieces of her to work in different servers, and, eventually had rewritten nearly everything. This was ClaireBot 2.
+
+Over time, I wanted more, and more. But ClaireBot 2 was still using an architecture very similar to the original. It was
+a pain to continue developing new features as I'd find myself tripping over technical debt nearly constantly, but I
+persisted. It wasn't worth taking the time to rewrite ALL of ClaireBot, at least that's what I had told my self.
+
+Eventually, Discord.py was discontinued, this forced my hand. If I wanted to keep developing ClaireBot, I would have
+to rewrite it from the ground up. Thus, ClaireBot 3 was born.
+
+It took me over a year and a half to finally finish the port, because procrastination is strong. That leads us to today.
+
+### Present Day
+
+ClaireBot is currently developed primarily in Java, some components have been and are in Kotlin, but this is currently
+a very minor portion of the codebase.
+
+ClaireBot uses the Javacord library for interacting with Discord. Without getting sidetracked too much, this is subject
+to change as Javacord's development has greatly slowed (see: [Beyond Javacord](Beyond-Javacord.md)).
+
+At the time of writing ClaireBot (v3.3.2) has the following commands:
+
+| Command | Description | Documentation |
+|--------------|-------------------------------------------------------------------------------------------------|------------------------|
+| /8ball | Exactly what it sounds like, though it's worth noting that ClaireBot only speaks in truth. | [Docs](8ball.md) |
+| /avatar | Gets a user's profile image. | [Docs](avatar.md) |
+| /help | How use ClaireBot??? | [Docs](help.md) |
+| /info | Displays various nuggets of information (uptime, version, support info, etc.). | [Docs](info.md) |
+| /leaderboard | Provides details on ClaireBot's (currently rudimentary) implementation of a Leaderboard system. | [Docs](leaderboard.md) |
+| /level | Displays your ClaireBot level (tied together with the leaderboard system). | [Docs](level.md) |
+| /poll | Creates a poll. Leave arguments empty for an interactive popup. | [Docs](poll.md) |
+| /quote | Picks a random message from the channel the command is executed in and displays it. | [Docs](quote.md) |
+| /request | Same system as /poll, but directs it to the server's requests channel. | [Docs](poll.md) |
+| /server | Reports various bits of info about the (optionally specified) server / guild. | [Docs](server.md) |
+| /user | Reports various bits of info about the specified user. | [Docs](user.md) |
+| /config | Allows for modifying user or server preferences. | [Docs](config.md) |
+| /santa | Tool for organizing secret santa gift exchanges. | [Docs](santa.md) |
+
+## Glossary
+
+ClaireBot
+: ClaireBot refers to both the ClaireBot project as a whole, and the Discord bot component.
+
The Discord bot component is the key piece of software that users interact with. It is what communicates with
+the Discord API and responds to user calls.
+
+ClaireData
+: ClaireData is the piece of software used to store long-term, persistent data. It serves a REST API that ClaireBot
+interfaces with. ClaireData uses Postgres as a database.
+
+ClaireBot Docker
+: A series of Docker containers, and a docker-compose.yml for quickly and easily setting up a working copy of ClaireBot.
\ No newline at end of file
diff --git a/Writerside/topics/poll.md b/Writerside/topics/poll.md
new file mode 100644
index 0000000..d01c506
--- /dev/null
+++ b/Writerside/topics/poll.md
@@ -0,0 +1,42 @@
+# /poll & /request
+
+Allows for creating interactive, inline polls and requests.
+
+Polls and requests are effectively the same system. In fact, they use identical backend code. The key difference is that
+Polls are inserted into the channel where the command is run, and requests are sent to the server's requests channel.
+
+For more info on how the requests channel is selected, please see: [_Requests Channel_](Requests-Channel.md)
+
+## Command
+
+### Syntax
+
+```shell
+/poll [question] [optional: allow-multiple-choices] [optional: queries]
+/poll
+```
+```shell
+/request [question] [optional: allow-multiple-choices] [optional: queries]
+/request
+```
+
+### Options
+
+question
+: The main question of your poll.
+
+allow-multiple-choices (optional)
+: Determines whether Clairebot will allow users to vote for multiple options.
+
+queries (optional)
+: Allows for specifying multiple choice answers. You can have up to 9 choices.
+
+When run with no options
+: If none of the previous options are specified, ClaireBot will send your Discord client a popup window allowing you to
+create a poll / request in a more graphical manner.
+
This is better for situations where you have a lot to type out, or when you want to avoid fiddling
+with the various options.
+
+
+
+
\ No newline at end of file
diff --git a/Writerside/topics/quote.md b/Writerside/topics/quote.md
new file mode 100644
index 0000000..85e920f
--- /dev/null
+++ b/Writerside/topics/quote.md
@@ -0,0 +1,36 @@
+# /quote
+
+Picks a random message from the channel the command is executed in and displays it.
+
+On Discord desktop, you can click the author's name to jump to the original message.
+
+On desktop and mobile, you can select the "View Original" button to jump to the original message.
+ClaireBot will send an ephemeral message which contains a link to the original.
+
+### Known Issues
+
+/quote is ungodly levels of inefficient reference [ClaireBot #4](https://github.com/Sidpatchy/ClaireBot/issues/4) to
+track this issue.
+
+Functions by pulling down (up-to) 50,000 of the most recently posted messages in the channel the
+command was executed in. After this, the bot will attempt to pick a random message from the selected user.
+
+All this is to say it is very slow. Be patient.
+
+## Command
+
+### Syntax
+
+```shell
+/quote [optional: user]
+```
+
+### Options
+
+user (optional)
+: The user you wish to quote.
+ If left blank, ClaireBot will quote whoever executed the command.
+
+
+
+
\ No newline at end of file
diff --git a/Writerside/topics/santa.md b/Writerside/topics/santa.md
new file mode 100644
index 0000000..12d5eb6
--- /dev/null
+++ b/Writerside/topics/santa.md
@@ -0,0 +1,37 @@
+# /santa
+
+SecretClaire is *the* all-in-one solution for facilitating secret santa-style gift exchanges on Discord.
+Use `/help santa` or `/santa` to get started!
+
+SecretClaire which was originally a standalone project that got ported into ClaireBot
+(see: [Sidpatchy/SecretClaire](https://github.com/Sidpatchy/SecretClaire)).
+
+## Command
+
+### Syntax
+
+```shell
+/santa [role]
+```
+
+### Options
+
+role
+: In the case of SecretClaire the sole purpose of using a role is to allow for quickly and easily grouping together
+users.
+
+## Example Organizer Messages
+
+
+
+
+
+
+## Example gift-giver messages
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Writerside/topics/server.md b/Writerside/topics/server.md
new file mode 100644
index 0000000..988573d
--- /dev/null
+++ b/Writerside/topics/server.md
@@ -0,0 +1,28 @@
+# /server
+
+Reports various bits of info about the (optionally specified) server / guild, including:
+- Owner
+- Creation Date
+- Number of Roles
+- Number of Members
+- Number of Channels
+ - Categories
+ - Text Channels
+ - Voice Channels
+
+## Command
+
+### Syntax
+
+```shell
+/server [optional: guildID]
+```
+
+### Options
+
+guildID (optional)
+: The ID of the guild you wish to gather info on. ClaireBot must be a member of the server.
+
+
+
+
\ No newline at end of file
diff --git a/Writerside/topics/user.md b/Writerside/topics/user.md
new file mode 100644
index 0000000..0974e73
--- /dev/null
+++ b/Writerside/topics/user.md
@@ -0,0 +1,23 @@
+# /user
+
+Reports various bits of info about a user, including:
+- Discord ID
+- Account Creation Date
+
+## Command
+
+### Syntax
+
+```shell
+/user [optional: user]
+```
+
+### Options
+
+user (optional)
+: The user you wish to get the details of.
+ If left blank, ClaireBot will report the details of whoever executed the command.
+
+
+
+
\ No newline at end of file
diff --git a/Writerside/v.list b/Writerside/v.list
new file mode 100644
index 0000000..2d12cb3
--- /dev/null
+++ b/Writerside/v.list
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/Writerside/writerside.cfg b/Writerside/writerside.cfg
new file mode 100644
index 0000000..ff2222a
--- /dev/null
+++ b/Writerside/writerside.cfg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 536d095..c9be67c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,46 +1,59 @@
plugins {
- id 'com.github.johnrengelman.shadow' version '8.1.1'
- id 'org.jetbrains.kotlin.jvm' version '1.9.22'
+ id 'com.gradleup.shadow' version '9.2.2'
+ id 'org.jetbrains.kotlin.jvm' version '2.2.20'
id 'java'
}
jar {
- manifest.attributes(
- 'Main-Class': 'com.sidpatchy.clairebot.Main',
- 'Multi-Release': 'true'
- )
+ manifest {
+ attributes(
+ 'Main-Class': 'com.sidpatchy.clairebot.Main',
+ 'Multi-Release': 'true'
+ )
+ }
}
-group 'com.sidpatchy'
-version '3.3.3'
+group = 'com.sidpatchy'
+version = '3.4.0-beta.1'
-sourceCompatibility = 17
-targetCompatibility = 17
+processResources {
+ filesMatching('**/build.properties') {
+ expand(
+ version: project.version,
+ buildDate: new Date().format("yyyy-MM-dd HH:mm:ss")
+ )
+ }
+}
+
+kotlin {
+ jvmToolchain(25)
+}
repositories {
+ mavenLocal()
mavenCentral()
- maven { url 'https://m2.dv8tion.net/releases' }
- maven { url 'https://jitpack.io' }
+ maven { url = 'https://m2.dv8tion.net/releases' }
+ maven { url = 'https://jitpack.io' }
}
dependencies {
- testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.1'
- testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.1'
+ testImplementation 'org.junit.jupiter:junit-jupiter-api:6.0.0'
+ testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:6.0.0'
- implementation 'com.github.Sidpatchy:Robin:2.0.0'
+ implementation 'com.github.Sidpatchy:Robin:2.2.5'
implementation 'org.javacord:javacord:3.8.0'
- implementation 'org.apache.logging.log4j:log4j-api:2.22.1'
- implementation 'org.apache.logging.log4j:log4j-core:2.22.1'
- implementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.22.1'
- implementation 'org.apache.commons:commons-lang3:3.14.0'
+ implementation 'org.apache.logging.log4j:log4j-api:2.25.2'
+ implementation 'org.apache.logging.log4j:log4j-core:2.25.2'
+ implementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.25.2'
+ implementation 'org.apache.commons:commons-lang3:3.19.0'
- implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.22'
+// implementation 'org.jetbrains.kotlin:kotlin-stdlib:2.2.20' TODO this can be considered for re-addition once JDK 25 support is added
- implementation 'org.yaml:snakeyaml:2.0'
- implementation 'org.json:json:20231013'
- implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.16.1'
+ implementation 'org.json:json:20250517'
+ implementation 'tools.jackson.dataformat:jackson-dataformat-xml:3.0.0'
+ implementation 'tools.jackson.dataformat:jackson-dataformat-yaml:3.0.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/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 48c0a02..d706aba 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/src/main/java/com/sidpatchy/clairebot/API/APIUser.java b/src/main/java/com/sidpatchy/clairebot/API/APIUser.java
index d86d23c..9f7415a 100644
--- a/src/main/java/com/sidpatchy/clairebot/API/APIUser.java
+++ b/src/main/java/com/sidpatchy/clairebot/API/APIUser.java
@@ -1,13 +1,14 @@
package com.sidpatchy.clairebot.API;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ObjectNode;
import com.sidpatchy.Robin.File.RobinConfiguration;
import com.sidpatchy.clairebot.Main;
import com.sidpatchy.clairebot.Util.Leveling.LevelingTools;
import com.sidpatchy.clairebot.Util.Network.DELETE;
import com.sidpatchy.clairebot.Util.Network.POST;
import com.sidpatchy.clairebot.Util.Network.PUT;
+import com.sidpatchy.clairebot.Util.Network.UrlBuilder;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.node.ObjectNode;
import java.io.IOException;
import java.io.InputStreamReader;
@@ -15,8 +16,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;
@@ -34,7 +35,7 @@ public APIUser(String userID) {
*/
public void getUser() throws IOException {
try {
- user.loadFromURL(Main.getApiUser(), Main.getApiPassword(), Main.getApiPath() + "api/v1/user/" + userID);
+ user.loadFromURL(Main.getApiUser(), Main.getApiPassword(), UrlBuilder.buildUrl(Main.getApiPath(), "api/v1/user", userID));
}
catch (Exception e) {
if (createNewWithDefaults) {
@@ -72,72 +73,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));
+ post.postToURL(UrlBuilder.buildUrl(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(UrlBuilder.buildUrl(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 +143,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 +153,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,
@@ -172,7 +172,7 @@ public void updateUserPointsGuildID(ArrayList pointsGuildID) throws IOEx
public void deleteUser() throws IOException {
DELETE delete = new DELETE();
- delete.deleteToURL(Main.getApiPath() + "api/v1/user/" + userID);
+ delete.deleteToURL(UrlBuilder.buildUrl(Main.getApiPath(), "api/v1/user", userID));
}
/**
@@ -187,18 +187,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();
}
@@ -211,7 +211,7 @@ public InputStreamReader getALLUsers() throws IOException {
URL url;
InputStreamReader reader;
- String link = Main.getApiPath() + "api/v1/user/";
+ String link = UrlBuilder.buildUrl(Main.getApiPath(), "api/v1/user");
try {
url = new URL(link);
URLConnection uc = url.openConnection();
diff --git a/src/main/java/com/sidpatchy/clairebot/API/Guild.java b/src/main/java/com/sidpatchy/clairebot/API/Guild.java
index 1a20042..d68b943 100644
--- a/src/main/java/com/sidpatchy/clairebot/API/Guild.java
+++ b/src/main/java/com/sidpatchy/clairebot/API/Guild.java
@@ -5,6 +5,7 @@
import com.sidpatchy.clairebot.Util.Network.DELETE;
import com.sidpatchy.clairebot.Util.Network.POST;
import com.sidpatchy.clairebot.Util.Network.PUT;
+import com.sidpatchy.clairebot.Util.Network.UrlBuilder;
import java.io.IOException;
import java.io.InputStreamReader;
@@ -29,7 +30,7 @@ public Guild(String guildID) {
*/
public void getGuild() throws IOException {
try {
- guild.loadFromURL(Main.getApiUser(), Main.getApiPassword(), Main.getApiPath() + "api/v1/guild/" + guildID);
+ guild.loadFromURL(Main.getApiUser(), Main.getApiPassword(), UrlBuilder.buildUrl(Main.getApiPath(), "api/v1/guild", guildID));
}
catch (Exception e) {
if (createNewWithDefaults) {
@@ -57,14 +58,20 @@ public boolean isEnforceSeverLanguage() {
return (boolean) guild.getObj("enforceServerLanguage");
}
+ public String getLocale() {
+ return guild.getString("locale");
+ }
+
public void createGuild(String requestsChannelID,
String moderatorMessagesChannelID,
- boolean enforceServerLanguage) throws IOException {
+ boolean enforceServerLanguage,
+ String locale) throws IOException {
POST post = new POST();
- post.postToURL(Main.getApiPath() + "api/v1/guild/", guildConstructor(
+ post.postToURL(UrlBuilder.buildUrl(Main.getApiPath(), "api/v1/guild"), guildConstructor(
requestsChannelID,
moderatorMessagesChannelID,
- enforceServerLanguage
+ enforceServerLanguage,
+ locale
));
}
@@ -74,22 +81,25 @@ public void createGuildWithDefaults() {
try {
createGuild((String) defaults.get("requestsChannelID"),
(String) defaults.get("moderatorMessagesChannelID"),
- (boolean) defaults.get("enforceServerLanguage"));
+ (boolean) defaults.get("enforceServerLanguage"),
+ (String) defaults.get("locale"));
}
// top 10 bad ideas #1
- catch (Exception ignored) {
- Main.getLogger().error("Unable to create user with defaults.");
+ catch (Exception e) {
+ Main.getLogger().error("Unable to create user with defaults.", e);
}
}
public void updateGuild(String requestsChannelID,
String moderatorMessagesChannelID,
- boolean enforceServerLanguage) throws IOException {
+ boolean enforceServerLanguage,
+ String locale) throws IOException {
PUT put = new PUT();
- put.putToURL(Main.getApiPath() + "api/v1/guild/" + guildID, guildConstructor(
+ put.putToURL(UrlBuilder.buildUrl(Main.getApiPath(), "api/v1/guild", guildID), guildConstructor(
requestsChannelID,
moderatorMessagesChannelID,
- enforceServerLanguage
+ enforceServerLanguage,
+ locale
));
}
@@ -97,7 +107,8 @@ public void updateRequestsChannelID(String requestsChannelID) throws IOException
updateGuild(
requestsChannelID,
getModeratorMessagesChannelID(),
- isEnforceSeverLanguage()
+ isEnforceSeverLanguage(),
+ getLocale()
);
}
@@ -105,7 +116,8 @@ public void updateModeratorMessagesChannelID(String moderatorMessagesChannelID)
updateGuild(
getRequestsChannelID(),
moderatorMessagesChannelID,
- isEnforceSeverLanguage()
+ isEnforceSeverLanguage(),
+ getLocale()
);
}
@@ -113,23 +125,35 @@ public void updateEnforceServerLanguage(boolean enforceServerLanguage) throws IO
updateGuild(
getRequestsChannelID(),
getModeratorMessagesChannelID(),
- enforceServerLanguage
+ enforceServerLanguage,
+ getLocale()
+ );
+ }
+
+ public void updateLocale(String locale) throws IOException {
+ updateGuild(
+ getRequestsChannelID(),
+ getModeratorMessagesChannelID(),
+ isEnforceSeverLanguage(),
+ locale
);
}
public void deleteGuild() throws IOException {
DELETE delete = new DELETE();
- delete.deleteToURL(Main.getApiPath() + "api/v1/guild/" + guildID);
+ delete.deleteToURL(UrlBuilder.buildUrl(Main.getApiPath(), "api/v1/guild", guildID));
}
public String guildConstructor(String requestsChannelID,
String moderatorMessagesChannelID,
- boolean enforceServerLanguage) {
+ boolean enforceServerLanguage,
+ String locale) {
return "{" +
"\"guildID\":\"" + guildID + "\"," +
"\"requestsChannelID\":\""+ requestsChannelID + "\"," +
"\"moderatorMessagesChannelID\":\"" + moderatorMessagesChannelID + "\"," +
- "\"enforceServerLanguage\":\"" + enforceServerLanguage + "\"" +
+ "\"enforceServerLanguage\":\"" + enforceServerLanguage + "\"," +
+ "\"locale\":\"" + locale + "\"" +
"}";
}
@@ -141,7 +165,7 @@ public InputStreamReader getALLGuilds() throws IOException {
URL url;
InputStreamReader reader;
- String link = Main.getApiPath() + "api/v1/guild/";
+ String link = UrlBuilder.buildUrl(Main.getApiPath(), "api/v1/guild");
try {
url = new URL(link);
URLConnection uc = url.openConnection();
diff --git a/src/main/java/com/sidpatchy/clairebot/Clockwork.java b/src/main/java/com/sidpatchy/clairebot/Clockwork.java
index d9970b6..ba2e84c 100644
--- a/src/main/java/com/sidpatchy/clairebot/Clockwork.java
+++ b/src/main/java/com/sidpatchy/clairebot/Clockwork.java
@@ -31,7 +31,6 @@ public static void initClockwork() {
}
-@SuppressWarnings("unchecked")
class Helper extends TimerTask {
RobinConfiguration config = new RobinConfiguration();
@@ -40,7 +39,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..4421e18 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().debug(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..92d076f 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(languageManager, 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..0b9f7b2 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,44 @@
package com.sidpatchy.clairebot.Embed.Commands.Regular;
+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..7f1a11b 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,44 +16,47 @@
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;
try {
unsortedLevelMap = LevelingTools.rankUsers(serverID);
} catch (IOException e) {
- e.printStackTrace();
String errorCode = Main.getErrorCode("loclead");
- Main.getLogger().error("Failed to query database while generating leaderboard. Ref: " + errorCode);
- return ErrorEmbed.getError(errorCode);
+ Main.getLogger().error("Failed to query database while generating leaderboard. Ref: {}", errorCode, e);
+ return ErrorEmbed.getError(languageManager, errorCode);
}
Map namedMap = convertUserIDstoNames(unsortedLevelMap);
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);
} catch (IOException e) {
- e.printStackTrace();
String errorCode = Main.getErrorCode("globlead");
- Main.getLogger().error("Failed to query database while generating leaderboard. Ref: " + errorCode);
- return ErrorEmbed.getError(errorCode);
+ Main.getLogger().error("Failed to query database while generating leaderboard. Ref: {}", errorCode, e);
+ return ErrorEmbed.getError(languageManager, errorCode);
}
Map namedMap = convertUserIDstoNames(unsortedLevelMap);
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..032a708 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,11 +1,10 @@
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;
@@ -25,12 +24,16 @@ 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 channel.getMessages(50000).thenApply(messages -> {
+ List userMessages = new java.util.ArrayList<>(messages.stream()
+ .filter(message -> message.getAuthor().getId() == user.getId())
+ .toList());
- return MessageCacheManager.queryMessageCache(channel, user).thenApply(userMessages -> {
if (userMessages.isEmpty()) {
// user not sent messages
- return ErrorEmbed.getError(Main.getErrorCode("UserNotInSet"));
+ return ErrorEmbed.getError(languageManager, Main.getErrorCode("UserNotInSet"));
}
Random random = new Random();
@@ -70,7 +73,7 @@ public static CompletableFuture getQuote(Server server, final User
// Fallback for if all messages checked were invalid
if (!messageSelected) {
- embed = ErrorEmbed.getCustomError(Main.getErrorCode("invalidMessages"),
+ embed = ErrorEmbed.getCustomError(languageManager, Main.getErrorCode("invalidMessages"),
"Looks like the messages I selected were invalid. Please try again later.");
}
@@ -85,4 +88,4 @@ public static EmbedBuilder viewOriginalMessageBuilder(TextChannel channel, Messa
return new EmbedBuilder()
.addField("Click to jump to the original message:", quotedMessage.getLink().toString());
}
-}
+}
\ No newline at end of file
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..fe615c8 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();
@@ -38,8 +42,14 @@ public static MessageBuilder getHostMessage(Role role, User author, String rules
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(SantaUtils.getSantaID(server.getIdAsString(), author.getIdAsString(), role.getIdAsString()), server.getIcon().orElse(null));
+ .setAuthor("SecretClaire", "", "https://github.com/Sidpatchy/ClaireBot/blob/main/img/ClaireBot-SantaHat.png?raw=true");
+
+ String footerText = SantaUtils.getSantaID(server.getIdAsString(), author.getIdAsString(), role.getIdAsString());
+ if (server.getIcon().isPresent()) {
+ embed.setFooter(footerText, server.getIcon().get());
+ } else {
+ embed.setFooter(footerText);
+ }
if (!theme.isEmpty()) {
embed.addField("Theme", theme, false);
@@ -59,25 +69,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 +97,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);
@@ -119,4 +132,70 @@ private static HashMap assignSecretSanta(Set participants) {
return users;
}
+
+ public static EmbedBuilder buildHostEmbedFromPairs(LanguageManager languageManager, Role role, User author, String rules, String theme, java.util.List givers, java.util.List receivers) {
+ Server server = role.getServer();
+
+ EmbedBuilder embed = new EmbedBuilder()
+ .setColor(Main.getColor(author.getIdAsString()))
+ .setAuthor("SecretClaire", "", "https://github.com/Sidpatchy/ClaireBot/blob/main/img/ClaireBot-SantaHat.png?raw=true");
+
+ String footerText = SantaUtils.getSantaID(server.getIdAsString(), author.getIdAsString(), role.getIdAsString());
+ if (server.getIcon().isPresent()) {
+ embed.setFooter(footerText, server.getIcon().get());
+ } else {
+ embed.setFooter(footerText);
+ }
+
+ if (!theme.isEmpty()) {
+ embed.addField("Theme", theme, false);
+ }
+
+ if (!rules.isEmpty()) {
+ embed.addField("Rules", rules, false);
+ }
+
+ for (int i = 0; i < Math.min(givers.size(), receivers.size()); i++) {
+ User giver = givers.get(i);
+ User receiver = receivers.get(i);
+ embed.addField(giver.getIdAsString(), giver.getNicknameMentionTag() + " → " + receiver.getNicknameMentionTag(), false);
+ }
+
+ return embed;
+ }
+
+ // Builds a fresh randomized host embed (re-rolls pairs) and preserves footer/format
+ public static EmbedBuilder buildHostEmbedRandomized(LanguageManager languageManager, Role role, User author, String rules, String theme) {
+ Server server = role.getServer();
+
+ EmbedBuilder embed = new EmbedBuilder()
+ .setColor(Main.getColor(author.getIdAsString()))
+ .setAuthor("SecretClaire", "", "https://github.com/Sidpatchy/ClaireBot/blob/main/img/ClaireBot-SantaHat.png?raw=true");
+
+ String footerText = SantaUtils.getSantaID(server.getIdAsString(), author.getIdAsString(), role.getIdAsString());
+ if (server.getIcon().isPresent()) {
+ embed.setFooter(footerText, server.getIcon().get());
+ } else {
+ embed.setFooter(footerText);
+ }
+
+ if (!theme.isEmpty()) {
+ embed.addField("Theme", theme, false);
+ }
+
+ if (!rules.isEmpty()) {
+ embed.addField("Rules", rules, false);
+ }
+
+ // Re-randomize using the role's current users
+ Set users = role.getUsers();
+ HashMap santaList = assignSecretSanta(users);
+ for (Map.Entry userPair : santaList.entrySet()) {
+ User giver = userPair.getKey();
+ User receiver = userPair.getValue();
+ embed.addField(giver.getIdAsString(), giver.getNicknameMentionTag() + " → " + receiver.getNicknameMentionTag(), false);
+ }
+
+ return embed;
+ }
}
diff --git a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/ServerInfoEmbed.java b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/ServerInfoEmbed.java
index 60c5512..b8b4284 100644
--- a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/ServerInfoEmbed.java
+++ b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/ServerInfoEmbed.java
@@ -1,31 +1,50 @@
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.server.Server;
+import java.awt.*;
+
public class ServerInfoEmbed {
- public static EmbedBuilder getServerInfo(Server server, String userID) {
+ public static EmbedBuilder getServerInfo(LanguageManager languageManager, Server server, String userID) {
+ Color color = Main.getColor(userID);
+ String authorName = server.getName();
+ String footerLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerInfoEmbed.ServerID");
+ String footerText = footerLabel + ": " + server.getIdAsString();
+ String ownerLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerInfoEmbed.Owner");
+ String creationDateLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerInfoEmbed.CreationDate");
+ String roleCountLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerInfoEmbed.RoleCount");
+ String memberCountLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerInfoEmbed.MemberCount");
+ String channelCountsLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerInfoEmbed.ChannelCounts");
+ String categoriesLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerInfoEmbed.Categories");
+ String textChannelsLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerInfoEmbed.TextChannels");
+ String voiceChannelsLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerInfoEmbed.VoiceChannels");
+
+ String channelCountsValue =
+ "⦁ " + categoriesLabel + ": " + server.getChannelCategories().size() +
+ "\n⦁ " + textChannelsLabel + ": " + server.getTextChannels().size() +
+ "\n⦁ " + voiceChannelsLabel + ": " + server.getVoiceChannels().size();
+
+ // Build embed (keeps the inline, minimal style)
EmbedBuilder embed = new EmbedBuilder()
- .setColor(Main.getColor(userID))
- .setAuthor(server.getName())
- .setFooter("Server ID: " + server.getIdAsString());
+ .setColor(color)
+ .setAuthor(authorName)
+ .setFooter(footerText);
server.getIcon().ifPresent(embed::setThumbnail);
server.getOwner().ifPresent(owner -> {
- embed.addField("Owner", owner.getDiscriminatedName(), false);
+ embed.addField(ownerLabel, owner.getDiscriminatedName(), false);
});
- embed.addField("Creation Date", "");
-
- embed.addField("Role Count", String.valueOf(server.getRoles().size()), false);
- embed.addField("Member Count", String.valueOf(server.getMemberCount()), false);
- embed.addField("Channel Counts", "⦁ Categories: " + server.getChannelCategories().size() +
- "\n⦁ Text Channels: " + server.getTextChannels().size() +
- "\n⦁ Voice Channels: " + server.getVoiceChannels().size(), false);
+ embed.addField(creationDateLabel, "");
+ embed.addField(roleCountLabel, String.valueOf(server.getRoles().size()), false);
+ embed.addField(memberCountLabel, String.valueOf(server.getMemberCount()), false);
+ embed.addField(channelCountsLabel, channelCountsValue, false);
return embed;
}
diff --git a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/ServerPreferencesEmbed.java b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/ServerPreferencesEmbed.java
index 6be269d..c7d75d7 100644
--- a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/ServerPreferencesEmbed.java
+++ b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/ServerPreferencesEmbed.java
@@ -2,6 +2,8 @@
import com.sidpatchy.clairebot.API.Guild;
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.channel.ServerTextChannel;
import org.javacord.api.entity.message.embed.EmbedBuilder;
@@ -10,33 +12,56 @@
public class ServerPreferencesEmbed {
- public static EmbedBuilder getMainMenu(User author) {
- return createGenericMenuEmbed(author, "Server Configuration Editor");
+ public static EmbedBuilder getMainMenu(LanguageManager languageManager, User author) {
+ // Temp/localized variables
+ String title = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerPreferencesEmbed.MainMenuTitle");
+
+ return createGenericMenuEmbed(author, title);
}
- public static EmbedBuilder getNotServerMenu() {
- return ErrorEmbed.getCustomError(Main.getErrorCode("notaserver"),
- "You must run this command inside a server!");
+ public static EmbedBuilder getNotServerMenu(LanguageManager languageManager) {
+ // Temp/localized variables
+ String notServerMsg = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerPreferencesEmbed.NotAServer");
+
+ return ErrorEmbed.getCustomError(languageManager, Main.getErrorCode("notaserver"), notServerMsg);
}
- public static EmbedBuilder getRequestsChannelMenu(User author) {
- return createGenericMenuEmbed(author, "Requests Channel")
- .setDescription("Only lists the first 25 channels in the server.");
+ public static EmbedBuilder getRequestsChannelMenu(LanguageManager languageManager, User author) {
+ // Temp/localized variables
+ String menuName = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerPreferencesEmbed.RequestsChannelMenuName");
+ String desc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerPreferencesEmbed.RequestsChannelDescription");
+
+ return createGenericMenuEmbed(author, menuName)
+ .setDescription(desc);
}
- public static EmbedBuilder getModeratorChannelMenu(User author) {
- return createGenericMenuEmbed(author, "Moderator Messages Channel")
- .setDescription("Only lists the first 25 channels in the server.");
+ public static EmbedBuilder getModeratorChannelMenu(LanguageManager languageManager, User author) {
+ // Temp/localized variables
+ String menuName = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerPreferencesEmbed.ModeratorChannelMenuName");
+ String desc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerPreferencesEmbed.ModeratorChannelDescription");
+
+ return createGenericMenuEmbed(author, menuName)
+ .setDescription(desc);
}
- public static EmbedBuilder getEnforceServerLangMenu(User author) {
- return createGenericMenuEmbed(author, "Enforce Server Language");
+ public static EmbedBuilder getEnforceServerLangMenu(LanguageManager languageManager, User author) {
+ // Temp/localized variables
+ String menuName = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerPreferencesEmbed.EnforceServerLanguageMenuName");
+
+ return createGenericMenuEmbed(author, menuName);
}
- public static EmbedBuilder getAcknowledgeRequestsChannelChange(Server server, User author, String requestsChannelID) {
+ public static EmbedBuilder getServerLanguageMenu(LanguageManager languageManager, User author) {
+ String title = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerPreferencesEmbed.ServerLanguageMenuTitle");
+ String desc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerPreferencesEmbed.ServerLanguageMenuDesc");
+ return createGenericMenuEmbed(author, title).setDescription(desc);
+ }
+
+ public static EmbedBuilder getAcknowledgeRequestsChannelChange(LanguageManager languageManager, Server server, User author, String requestsChannelID) {
+ // Resolve channel
ServerTextChannel channel = Main.getApi().getServerTextChannelById(requestsChannelID).orElse(null);
if (channel == null) {
- return ErrorEmbed.getError(Main.getErrorCode("channelNotExists"));
+ return ErrorEmbed.getError(languageManager, Main.getErrorCode("channelNotExists"));
}
try {
@@ -44,20 +69,27 @@ public static EmbedBuilder getAcknowledgeRequestsChannelChange(Server server, Us
guild.getGuild();
guild.updateRequestsChannelID(requestsChannelID);
+ // Temp/localized variables
+ String title = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerPreferencesEmbed.AcknowledgeRequestsChannelChangeTitle");
+ String mention = "<#" + channel.getIdAsString() + ">";
+ languageManager.addContext(ContextManager.ContextType.GENERIC, "cb.channel.requests.mentiontag", mention);
+ String desc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerPreferencesEmbed.AcknowledgeRequestsChannelChangeDescription");
+
return new EmbedBuilder()
.setColor(Main.getColor(author.getIdAsString()))
- .setAuthor("Requests Channel Changed!")
- .setDescription("Your requests channel has been changed to " + channel.getMentionTag());
+ .setAuthor(title)
+ .setDescription(desc);
} catch (Exception e) {
e.printStackTrace();
- return ErrorEmbed.getError(Main.getErrorCode("updateRequestsChannel"));
+ return ErrorEmbed.getError(languageManager, Main.getErrorCode("updateRequestsChannel"));
}
}
- public static EmbedBuilder getAcknowledgeModeratorChannelChange(Server server, User author, String moderatorChannelID) {
+ public static EmbedBuilder getAcknowledgeModeratorChannelChange(LanguageManager languageManager, Server server, User author, String moderatorChannelID) {
+ // Resolve channel
ServerTextChannel channel = Main.getApi().getServerTextChannelById(moderatorChannelID).orElse(null);
if (channel == null) {
- return ErrorEmbed.getError(Main.getErrorCode("channelNotExists"));
+ return ErrorEmbed.getError(languageManager, Main.getErrorCode("channelNotExists"));
}
try {
@@ -65,17 +97,24 @@ public static EmbedBuilder getAcknowledgeModeratorChannelChange(Server server, U
guild.getGuild();
guild.updateModeratorMessagesChannelID(moderatorChannelID);
+ // Temp/localized variables
+ String title = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerPreferencesEmbed.AcknowledgeModeratorChannelChangeTitle");
+ String mention = "<#" + channel.getIdAsString() + ">";
+ languageManager.addContext(ContextManager.ContextType.GENERIC, "cb.channel.moderator.mentiontag", mention);
+ String desc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerPreferencesEmbed.AcknowledgeModeratorChannelChangeDescription");
+
return new EmbedBuilder()
.setColor(Main.getColor(author.getIdAsString()))
- .setAuthor("Moderator Channel Changed!")
- .setDescription("Your moderator messages channel has been changed to " + channel.getMentionTag());
+ .setAuthor(title)
+ .setDescription(desc);
} catch (Exception e) {
e.printStackTrace();
- return ErrorEmbed.getError(Main.getErrorCode("updateModeratorChannel"));
+ return ErrorEmbed.getError(languageManager, Main.getErrorCode("updateModeratorChannel"));
}
}
- public static EmbedBuilder getAcknowledgeEnforceServerLanguageUpdate(Server server, User author, String newValue) {
+ public static EmbedBuilder getAcknowledgeEnforceServerLanguageUpdate(LanguageManager languageManager, Server server, User author, String newValue) {
+ // Temp/localized variables
boolean value = Boolean.parseBoolean(newValue);
try {
@@ -83,22 +122,38 @@ public static EmbedBuilder getAcknowledgeEnforceServerLanguageUpdate(Server serv
guild.getGuild();
guild.updateEnforceServerLanguage(value);
- EmbedBuilder embed = new EmbedBuilder()
+ String title = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerPreferencesEmbed.AcknowledgeEnforceServerLanguageUpdateTitle");
+ String desc = value
+ ? languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerPreferencesEmbed.AcknowledgeEnforceServerLanguageUpdateEnforced")
+ : languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerPreferencesEmbed.AcknowledgeEnforceServerLanguageUpdateNotEnforced");
+
+ return new EmbedBuilder()
.setColor(Main.getColor(author.getIdAsString()))
- .setAuthor("Server Language Preferences Updated!");
+ .setAuthor(title)
+ .setDescription(desc);
- if (value) {
- embed.setDescription("I will now follow the server's language regardless of user preference.");
- }
- else {
- embed.setDescription("I will allow users to set their own language preferences.");
- }
+ } catch (Exception e) {
+ e.printStackTrace();
+ return ErrorEmbed.getError(languageManager, Main.getErrorCode("updateEnforceServerLang"));
+ }
+ }
+
+ public static EmbedBuilder getAcknowledgeServerLanguageChange(LanguageManager languageManager, Server server, User author, String languageTag) {
+ try {
+ Guild guild = new Guild(server.getIdAsString());
+ guild.getGuild();
+ guild.updateLocale(languageTag);
- return embed;
+ String title = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerPreferencesEmbed.ServerLanguageChanged");
+ String desc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ServerPreferencesEmbed.ServerLanguageChangedDesc");
+ return new EmbedBuilder()
+ .setColor(Main.getColor(author.getIdAsString()))
+ .setAuthor(title)
+ .setDescription(desc);
} catch (Exception e) {
e.printStackTrace();
- return ErrorEmbed.getError(Main.getErrorCode("updateEnforceServerLang"));
+ return ErrorEmbed.getError(languageManager, Main.getErrorCode("updateServerLocale"));
}
}
diff --git a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/UserInfoEmbed.java b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/UserInfoEmbed.java
index 6d052c1..79adc93 100644
--- a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/UserInfoEmbed.java
+++ b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/UserInfoEmbed.java
@@ -1,6 +1,8 @@
package 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 org.apache.commons.lang3.time.DurationFormatUtils;
import org.javacord.api.entity.message.embed.EmbedBuilder;
@@ -11,32 +13,51 @@
public class UserInfoEmbed {
- public static EmbedBuilder getUser(User user, User author, Server server) {
+ public static EmbedBuilder getUser(LanguageManager languageManager, User user, User author, Server server) {
+ // Temp / localized variables block
+ long nowMs = System.currentTimeMillis();
+ long creationMs = user.getCreationTimestamp().toEpochMilli();
+ String creationDateTag = "";
+ String timeSinceCreation = DurationFormatUtils.formatDurationWords(nowMs - creationMs, true, false);
- String creationDate = "";
- String timeSinceCreation = DurationFormatUtils.formatDurationWords(System.currentTimeMillis() - user.getCreationTimestamp().toEpochMilli(), true, false);
+ String userLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.UserInfoEmbed.User");
+ String discordIdLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.UserInfoEmbed.DiscordID");
+ String joinDateLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.UserInfoEmbed.JoinDate");
+ String creationDateLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.UserInfoEmbed.CreationDate");
+ // Null guard for author (use placeholders)
if (author == null) {
String errorCode = Main.getErrorCode("User_Info_Null");
- Main.getLogger().error("The value for author was null when passed into UserInfo Embed. Error code: " + errorCode);
- Main.getLogger().error("Author: " + author.getDiscriminatedName());
- return ErrorEmbed.getError(errorCode);
+ languageManager.addContext(ContextManager.ContextType.GENERIC, "errorcode", errorCode);
+ languageManager.addContext(ContextManager.ContextType.GENERIC, "user.id.username", "null");
+
+ String err1 = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.UserInfoEmbed.Error_1");
+ String err2 = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.UserInfoEmbed.Error_2");
+ Main.getLogger().error(err1);
+ Main.getLogger().error(err2);
+ return ErrorEmbed.getError(languageManager, errorCode);
}
+ String footerText = author.getDiscriminatedName() + " (" + author.getIdAsString() + ")";
+
+ // Build embed
EmbedBuilder embed = new EmbedBuilder()
.setColor(Main.getColor(user.getIdAsString()))
.setThumbnail(user.getAvatar())
- .setAuthor("User\n" + user.getDiscriminatedName())
- .addField("Discord ID", user.getIdAsString(), false);
+ .setAuthor(userLabel + "\n" + user.getDiscriminatedName())
+ .addField(discordIdLabel, user.getIdAsString(), false);
if (server != null) {
- String joinDate = "";
- String timeSinceJoin = DurationFormatUtils.formatDurationWords(System.currentTimeMillis() - user.getJoinedAtTimestamp(server).orElse(Instant.ofEpochMilli(0)).toEpochMilli(), true, false);
- embed.addField("Server Join Date", joinDate + "\n*" + timeSinceJoin + "*", false);
+ Instant joinedAt = user.getJoinedAtTimestamp(server).orElse(Instant.now());
+ long joinMs = joinedAt.toEpochMilli();
+ String joinDateTag = "";
+ long sinceBase = user.getJoinedAtTimestamp(server).orElse(Instant.ofEpochMilli(0)).toEpochMilli();
+ String timeSinceJoin = DurationFormatUtils.formatDurationWords(nowMs - sinceBase, true, false);
+ embed.addField(joinDateLabel, joinDateTag + "\n*" + timeSinceJoin + "*", false);
}
- embed.addField("Account Creation Date", creationDate + "\n*" + timeSinceCreation + "*", false);
- embed.setFooter(author.getDiscriminatedName() + " (" + author.getIdAsString() + ")", author.getAvatar());
+ embed.addField(creationDateLabel, creationDateTag + "\n*" + timeSinceCreation + "*", false)
+ .setFooter(footerText, author.getAvatar());
return embed;
}
diff --git a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/UserPreferencesEmbed.java b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/UserPreferencesEmbed.java
index c5dfb7b..8d83ae6 100644
--- a/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/UserPreferencesEmbed.java
+++ b/src/main/java/com/sidpatchy/clairebot/Embed/Commands/Regular/UserPreferencesEmbed.java
@@ -2,6 +2,7 @@
import com.sidpatchy.clairebot.API.APIUser;
import com.sidpatchy.clairebot.Embed.ErrorEmbed;
+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;
@@ -10,61 +11,94 @@
public class UserPreferencesEmbed {
- /**
- *
- * @param author
- * @return
- */
- public static EmbedBuilder getMainMenu(User author) {
+ public static EmbedBuilder getMainMenu(LanguageManager languageManager, User author) {
+ // Temp/localized variables
+ String title = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.UserPreferencesEmbed.MainMenuText");
+
return new EmbedBuilder()
.setColor(Main.getColor(author.getIdAsString()))
- .setAuthor("User Preferences Editor");
+ .setAuthor(title);
}
- public static EmbedBuilder getAccentColourMenu(User author) {
+ public static EmbedBuilder getAccentColourMenu(LanguageManager languageManager, User author) {
+ // Temp/localized variables
+ String title = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.UserPreferencesEmbed.AccentColourMenu");
+
return new EmbedBuilder()
.setColor(Main.getColor(author.getIdAsString()))
- .setAuthor("Accent Colour Editor");
+ .setAuthor(title);
}
- public static EmbedBuilder getAccentColourListMenu(User author) {
+ public static EmbedBuilder getAccentColourListMenu(LanguageManager languageManager, User author) {
+ // Temp/localized variables
+ String title = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.UserPreferencesEmbed.AccentColourList");
+
return new EmbedBuilder()
.setColor(Main.getColor(author.getIdAsString()))
- .setAuthor("Accent Colour List");
+ .setAuthor(title);
}
/**
* Response when an accent colour has been selected. Updates colour based off passed colourCode.
*
* @param author User updating their colour
- * @param colourCode colour code selected
+ * @param colourCode colour code selected (e.g. #5865F2)
* @return embed
*/
- public static EmbedBuilder getAcknowledgeAccentColourChange(User author, String colourCode) {
+ public static EmbedBuilder getAcknowledgeAccentColourChange(LanguageManager languageManager, User author, String colourCode) {
+ // Temp/localized variables
Color color = Color.decode(colourCode);
try {
- APIUser user = new APIUser(author.getIdAsString());
- user.getUser();
- user.updateUserColour(colourCode);
+ APIUser apiUser = new APIUser(author.getIdAsString());
+ apiUser.getUser();
+ apiUser.updateUserColour(colourCode);
+
+ String title = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.UserPreferencesEmbed.AccentColourChanged");
+ String desc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.UserPreferencesEmbed.AccentColourChangedDesc");
return new EmbedBuilder()
.setColor(color)
- .setAuthor("Accent Colour Changed!")
- .setDescription("Your accent colour has been changed to " + colourCode);
- }
- catch (Exception e){
- e.printStackTrace();
- return ErrorEmbed.getError(Main.getErrorCode("updateAccentColour"));
+ .setAuthor(title)
+ .setDescription(desc);
+ } catch (Exception e) {
+ return ErrorEmbed.getError(languageManager, Main.getErrorCode("updateAccentColour"));
}
-
-
}
- public static EmbedBuilder getLanguageMenu(User author) {
+ public static EmbedBuilder getLanguageMenu(LanguageManager languageManager, User author) {
+ // Temp/localized variables
+ String title = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.UserPreferencesEmbed.LanguageMenuTitle");
+ String desc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.UserPreferencesEmbed.LanguageMenuDesc");
+
return new EmbedBuilder()
.setColor(Main.getColor(author.getIdAsString()))
- .setAuthor("NYI")
- .setDescription("Sorry, this feature is not yet implemented.");
+ .setAuthor(title)
+ .setDescription(desc);
+ }
+
+ /**
+ * Response when a language has been selected. Updates language based on the selected locale tag.
+ *
+ * @param author User updating their language
+ * @param languageTag IETF BCP 47 tag (e.g., en-US)
+ * @return embed
+ */
+ public static EmbedBuilder getAcknowledgeLanguageChange(LanguageManager languageManager, User author, String languageTag) {
+ try {
+ APIUser apiUser = new APIUser(author.getIdAsString());
+ apiUser.getUser();
+ apiUser.updateUserLanguage(languageTag);
+
+ String title = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.UserPreferencesEmbed.LanguageChanged");
+ String desc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.UserPreferencesEmbed.LanguageChangedDesc");
+
+ return new EmbedBuilder()
+ .setColor(Main.getColor(author.getIdAsString()))
+ .setAuthor(title)
+ .setDescription(desc);
+ } catch (Exception e) {
+ return ErrorEmbed.getError(languageManager, Main.getErrorCode("updateLanguage"));
+ }
}
}
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..7903953 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
@@ -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.Voting.VotingUtils;
import org.javacord.api.entity.message.embed.EmbedBuilder;
@@ -11,11 +13,10 @@
public class VotingEmbed {
-
/**
* Called when a user creates a poll or request and specifies a description.
*
- * @param commandName default will be "REQUEST" or "POLL", no reason to maintain two classes that do basically the same thing (aka pulling a ClaireBot 2)
+ * @param commandName default will be "REQUEST" or "POLL"
* @param question The question the user is asking.
* @param description A description of what is being asked.
* @param allowMultipleChoices allow a user to vote for multiple options
@@ -25,42 +26,67 @@ public class VotingEmbed {
* @param numChoices The number of choices
* @return voting embed
*/
- public static EmbedBuilder getPoll(String commandName, String question, String description, Boolean allowMultipleChoices, List choices, Server server, User author, Integer numChoices) {
+ public static EmbedBuilder getPoll(LanguageManager languageManager,
+ String commandName,
+ String question,
+ String description,
+ Boolean allowMultipleChoices,
+ List choices,
+ Server server,
+ User author,
+ Integer numChoices) {
+ // Temp/localized variables block
List emoji = Arrays.asList("1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟", "\uD83D\uDC4D", "\uD83D\uDC4E");
+ String choicesLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.VotingEmbed.Choices");
+ String pollRequestText;
+ String pollAskText;
+ {
+ // Provide display name placeholder for author line
+ languageManager.addContext(ContextManager.ContextType.GENERIC, "user.id.displayname.server", author.getDisplayName(server));
+ pollRequestText = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.VotingEmbed.PollRequest");
+ pollAskText = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.VotingEmbed.PollAsk");
+ }
+ String authorUrl = "https://discord.com/users/" + author.getIdAsString();
EmbedBuilder embed = new EmbedBuilder()
.setColor(Main.getColor(author.getIdAsString()));
+ // Build choices block (up to 10)
StringBuilder choiceBuilder = new StringBuilder();
if (choices != null) {
- for (int i = 0; i < 10; i++) {
- if (choices.get(i) != null) {
- choiceBuilder.append(emoji.get(i)).append(" ").append(choices.get(i)).append("\n");
+ int limit = Math.min(10, choices.size());
+ for (int i = 0; i < limit; i++) {
+ String choice = choices.get(i);
+ if (choice == null || choice.isBlank()) {
+ break;
}
- else {break;}
+ choiceBuilder.append(emoji.get(i)).append(" ").append(choice).append("\n");
}
}
- if (choiceBuilder.toString().equalsIgnoreCase("")) {
+ if (choiceBuilder.isEmpty()) {
allowMultipleChoices = false;
- }
- else {
- embed.addField("Choices", choiceBuilder.toString());
+ } else {
+ embed.addField(choicesLabel, choiceBuilder.toString());
}
- embed.setFooter("Poll ID: " + VotingUtils.getPollID(allowMultipleChoices, author.getIdAsString(), numChoices.toString()));
+ // Compute poll ID after choices logic, then localize footer with placeholder
+ String pollId = VotingUtils.getPollID(allowMultipleChoices, author.getIdAsString(), numChoices.toString());
+ languageManager.addContext(ContextManager.ContextType.GENERIC, "poll.id", pollId);
+ String footer = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.VotingEmbed.PollID");
+ embed.setFooter(footer);
+ // Localized author line
if (commandName.equalsIgnoreCase("REQUEST")) {
- embed.setAuthor(author.getDisplayName(server) + " requests:", "https://discord.com/users/" + author.getIdAsString(), author.getAvatar());
- }
- else if (commandName.equalsIgnoreCase("POLL")) {
- embed.setAuthor(author.getDisplayName(server) + " asks:", "https://discord.com/users/" + author.getIdAsString(), author.getAvatar());
+ embed.setAuthor(pollRequestText, authorUrl, author.getAvatar());
+ } else if (commandName.equalsIgnoreCase("POLL")) {
+ embed.setAuthor(pollAskText, authorUrl, author.getAvatar());
}
- if (description.equalsIgnoreCase("")) {
+ // Question / description
+ if (description == null || description.isEmpty()) {
embed.setDescription(question);
- }
- else {
+ } else {
embed.addField(question, description);
}
@@ -69,30 +95,32 @@ else if (commandName.equalsIgnoreCase("POLL")) {
/**
* Called when a user creates a poll without specifying a description.
- *
- * @param commandName default will be "REQUEST" or "POLL", no reason to maintain two classes that do basically the same thing (aka pulling a ClaireBot 2)
- * @param question The question the user is asking.
- * @param allowMultipleChoices allow a user to vote for multiple options
- * @param choices List of choices as a string
- * @param server The server/guild that the command is being run in
- * @param author The user who ran the command
- * @param numChoices The number of choices
- * @return voting embed
*/
- public static EmbedBuilder getPoll(String commandName, String question, Boolean allowMultipleChoices, List choices, Server server, User author, Integer numChoices) {
- return getPoll(commandName, question, "", allowMultipleChoices, choices, server, author, numChoices);
+ public static EmbedBuilder getPoll(LanguageManager languageManager,
+ String commandName,
+ String question,
+ Boolean allowMultipleChoices,
+ List choices,
+ Server server,
+ User author,
+ Integer numChoices) {
+ return getPoll(languageManager, commandName, question, "", allowMultipleChoices, choices, server, author, numChoices);
}
/**
* The embed we respond to the user with, should ideally be ephemeral.
* @param author the author of the command
- * @param requestsChannelMentionTag the channel the request is being posted in
- * @return
+ * @param requestsChannelMentionTag the channel the request is being posted in (e.g. "<#1234567890>")
*/
- public static EmbedBuilder getUserResponse(User author, String requestsChannelMentionTag) {
+ public static EmbedBuilder getUserResponse(LanguageManager languageManager, User author, String requestsChannelMentionTag) {
+ // Temp/localized variables block
+ String title = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.VotingEmbed.UserResponseTitle");
+ languageManager.addContext(ContextManager.ContextType.GENERIC, "cb.channel.requests.mentiontag", requestsChannelMentionTag);
+ String desc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.VotingEmbed.UserResponseDescription");
+
return new EmbedBuilder()
.setColor(Main.getColor(author.getIdAsString()))
- .setAuthor("Your request has been created!")
- .setDescription("Go check it out in " + requestsChannelMentionTag);
+ .setAuthor(title)
+ .setDescription(desc);
}
}
diff --git a/src/main/java/com/sidpatchy/clairebot/Embed/ErrorEmbed.java b/src/main/java/com/sidpatchy/clairebot/Embed/ErrorEmbed.java
index 4f45107..7cc0d18 100755
--- a/src/main/java/com/sidpatchy/clairebot/Embed/ErrorEmbed.java
+++ b/src/main/java/com/sidpatchy/clairebot/Embed/ErrorEmbed.java
@@ -1,41 +1,83 @@
package com.sidpatchy.clairebot.Embed;
+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.util.ArrayList;
+import java.util.List;
import java.util.Random;
/**
* Called when ClaireBot encounters (and catches) an error, ideally never.
*/
public class ErrorEmbed {
+ public static EmbedBuilder getError(LanguageManager languageManager, String errorCode) {
+ // Temp/localized variables
+ List errorGifs = new ArrayList<>(Main.getErrorGifs());
+ String title = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ErrorEmbed.Error");
- public static EmbedBuilder getError(String errorCode) {
+ languageManager.addContext(ContextManager.ContextType.GENERIC, "errorcode", errorCode);
+ String description = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ErrorEmbed.GenericDescription");
- ArrayList errorGifs = (ArrayList) Main.getErrorGifs();
+ EmbedBuilder embed = new EmbedBuilder()
+ .setColor(Main.getErrorColor())
+ .setAuthor(title)
+ .setDescription(description);
- Random random = new Random();
- int rand = random.nextInt(errorGifs.size());
+ if (!errorGifs.isEmpty()) {
+ int rand = new Random().nextInt(errorGifs.size());
+ embed.setImage(errorGifs.get(rand));
+ }
- return new EmbedBuilder()
- .setColor(Main.getErrorColor())
- .setAuthor("ERROR")
- .setDescription("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](https://support.clairebot.net/) and let us know about the issue."
- + "\n\nPlease include the following error code: " + errorCode)
- .setImage(errorGifs.get(rand));
+ return embed;
}
- public static EmbedBuilder getError(String errorCode, String customMessage) {
- return getError(errorCode).setDescription(customMessage + "\n\nPlease try running the command once more and if that doesn't work, join my [Discord server](https://support.clairebot.net/) and let us know about the issue."
- + "\n\nPlease include the following error code: " + errorCode);
+ public static EmbedBuilder getError(LanguageManager languageManager, String errorCode, String customMessage) {
+ // Temp/localized variables
+ List errorGifs = new ArrayList<>(Main.getErrorGifs());
+ String title = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ErrorEmbed.Error");
+
+ languageManager.addContext(ContextManager.ContextType.GENERIC, "errorcode", errorCode);
+ String generic = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ErrorEmbed.GenericDescription");
+
+ EmbedBuilder embed = new EmbedBuilder()
+ .setColor(Main.getErrorColor())
+ .setAuthor(title)
+ .setDescription(customMessage + "\n\n" + generic);
+
+ if (!errorGifs.isEmpty()) {
+ int rand = new Random().nextInt(errorGifs.size());
+ embed.setImage(errorGifs.get(rand));
+ }
+
+ return embed;
}
- public static EmbedBuilder getCustomError(String errorCode, String message) {
- return getError(errorCode).setDescription(message);
+ public static EmbedBuilder getCustomError(LanguageManager languageManager, String errorCode, String message) {
+ // Temp/localized variables
+ List errorGifs = new ArrayList<>(Main.getErrorGifs());
+ String title = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.ErrorEmbed.Error");
+
+ languageManager.addContext(ContextManager.ContextType.GENERIC, "errorcode", errorCode);
+
+ EmbedBuilder embed = new EmbedBuilder()
+ .setColor(Main.getErrorColor())
+ .setAuthor(title)
+ .setDescription(message);
+
+ if (!errorGifs.isEmpty()) {
+ int rand = new Random().nextInt(errorGifs.size());
+ embed.setImage(errorGifs.get(rand));
+ }
+
+ return embed;
}
- public static EmbedBuilder getLackingPermissions(String message) {
- return getCustomError(Main.getErrorCode(Main.getErrorCode("noPerms")), message);
+ public static EmbedBuilder getLackingPermissions(LanguageManager languageManager, String message) {
+ // Use a specific error code key and localize like a custom error
+ String errorCode = Main.getErrorCode("noPerms");
+ return getCustomError(languageManager, errorCode, message);
}
}
diff --git a/src/main/java/com/sidpatchy/clairebot/Embed/WelcomeEmbed.java b/src/main/java/com/sidpatchy/clairebot/Embed/WelcomeEmbed.java
index a056c89..9caabb8 100644
--- a/src/main/java/com/sidpatchy/clairebot/Embed/WelcomeEmbed.java
+++ b/src/main/java/com/sidpatchy/clairebot/Embed/WelcomeEmbed.java
@@ -1,27 +1,45 @@
package com.sidpatchy.clairebot.Embed;
import com.sidpatchy.clairebot.API.Guild;
+import com.sidpatchy.clairebot.Lang.ContextManager;
+import com.sidpatchy.clairebot.Lang.LanguageManager;
import com.sidpatchy.clairebot.Main;
+import org.apache.logging.log4j.Logger;
import org.javacord.api.entity.message.embed.EmbedBuilder;
import org.javacord.api.entity.server.Server;
import java.io.IOException;
public class WelcomeEmbed {
+ private static final Logger logger = Main.getLogger();
- public static EmbedBuilder getWelcome(Server server) {
+
+ public static EmbedBuilder getWelcome(LanguageManager languageManager, Server server) {
// Initialize the Guild in the database
Guild guild = new Guild(server.getIdAsString());
try {
guild.getGuild();
- } catch (IOException ignored) {}
+ } catch (IOException e) {
+ logger.error("Error while loading guild data.", e);
+ }
+
+ // Temp/localized variables block
+ // If your placeholder handler expects {cb.command.help.name}, provide it here
+ languageManager.addContext(ContextManager.ContextType.GENERIC, "command.help.name", "help");
+
+ String title = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.WelcomeEmbed.Title");
+ String motto = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.WelcomeEmbed.Motto");
+ String usageTitle = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.WelcomeEmbed.UsageTitle");
+ String usageDesc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.WelcomeEmbed.UsageDesc");
+ String supportTitle = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.WelcomeEmbed.SupportTitle");
+ String supportDesc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Regular.WelcomeEmbed.SupportDesc");
EmbedBuilder embed = new EmbedBuilder()
.setColor(Main.getColor(null))
- .addField("\uD83C\uDF89 Welcome to ClaireBot 3!", "How can you rise, if you have not burned?", true)
- .addField("Usage", "Get started by running `/help`. Need more info on a command? Run `/help ` (ex. `/help user`)", false)
- .addField("Get Support", "You can get help on our [Discord](https://support.clairebot.net/), or by opening an issue on [GitHub](https://github.com/Sidpatchy/ClaireBot)");
+ .addField(title, motto, true)
+ .addField(usageTitle, usageDesc, false)
+ .addField(supportTitle, supportDesc);
server.getIcon().ifPresent(embed::setThumbnail);
diff --git a/src/main/java/com/sidpatchy/clairebot/Lang/ContextManager.java b/src/main/java/com/sidpatchy/clairebot/Lang/ContextManager.java
new file mode 100644
index 0000000..5f1acc4
--- /dev/null
+++ b/src/main/java/com/sidpatchy/clairebot/Lang/ContextManager.java
@@ -0,0 +1,61 @@
+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;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public record ContextManager(
+ Server server,
+ TextChannel channel,
+ User author,
+ User user,
+ Message message,
+ Map dynamicData
+) {
+ // Enum to define known context types
+ public enum ContextType {
+ POLL,
+ SANTA,
+ GENERIC
+ }
+
+ // Compact constructor with default empty map
+ public ContextManager {
+ if (dynamicData == null) {
+ dynamicData = new HashMap<>();
+ }
+ }
+
+ // Alternative constructor without dynamicData parameter
+ public ContextManager(Server server, TextChannel channel, User author,
+ User user, Message message) {
+ this(server, channel, author, user, message, new HashMap<>());
+ }
+
+ // Add dynamic data with type safety
+ public void addData(ContextType type, String key, Object value) {
+ dynamicData.put(type.name().toLowerCase() + "." + key, value);
+ }
+
+ // Get dynamic data with type safety
+ @SuppressWarnings("unchecked")
+ public T getData(ContextType type, String key) {
+ return (T) dynamicData.get(type.name().toLowerCase() + "." + key);
+ }
+
+ // Helper functions for specific context types
+ public void addPollData(String pollId, String question) {
+ addData(ContextType.POLL, "id", pollId);
+ addData(ContextType.POLL, "question", question);
+ }
+
+ public void addSantaData(User giftee, String theme, String rules) {
+ addData(ContextType.SANTA, "giftee", giftee);
+ addData(ContextType.SANTA, "theme", theme);
+ addData(ContextType.SANTA, "rules", rules);
+ }
+}
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..e66e0fe
--- /dev/null
+++ b/src/main/java/com/sidpatchy/clairebot/Lang/LanguageManager.java
@@ -0,0 +1,293 @@
+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.server();
+ this.user = context.user();
+ 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.server();
+ this.user = context.user();
+ 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) {
+ Locale locale = resolveEffectiveLocale(server, user);
+ // Load primary and fallback separately to support per-key fallback
+ RobinConfiguration primary = tryLoadConfig(new File(pathToLanguageFiles, "lang_" + locale.toLanguageTag() + ".yml"));
+ RobinConfiguration fallback = tryLoadConfig(new File(pathToLanguageFiles, "lang_" + fallbackLocale.toLanguageTag() + ".yml"));
+
+ String localized = null;
+ if (primary != null) {
+ localized = primary.getString(key);
+ }
+ if (localized == null && fallback != null) {
+ localized = fallback.getString(key);
+ }
+ String raw = localized != null ? localized : key;
+ return placeholderHandler.process(raw);
+ }
+
+ /**
+ * 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) {
+ Locale locale = resolveEffectiveLocale(server, user);
+ RobinConfiguration primary = tryLoadConfig(new File(pathToLanguageFiles, "lang_" + locale.toLanguageTag() + ".yml"));
+ RobinConfiguration fallback = tryLoadConfig(new File(pathToLanguageFiles, "lang_" + fallbackLocale.toLanguageTag() + ".yml"));
+
+ List localizedList = null;
+ if (primary != null) {
+ localizedList = primary.getList(key, String.class);
+ }
+ if (localizedList == null && fallback != null) {
+ localizedList = fallback.getList(key, String.class);
+ }
+ 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 Locale resolveEffectiveLocale(Server server, User user) {
+ Locale locale;
+ try {
+ if (user == null) {
+ locale = fallbackLocale;
+ } else {
+ APIUser apiUser = new APIUser(user.getIdAsString());
+ apiUser.getUser();
+ String rawLang = apiUser.getLanguage();
+
+ if (rawLang == null || rawLang.isBlank()) {
+ locale = fallbackLocale;
+ } else {
+ String normalizedTag = rawLang.replace('_', '-');
+ locale = Locale.forLanguageTag(normalizedTag);
+ if (locale == null || locale.toLanguageTag().equals("und")) {
+ locale = fallbackLocale;
+ }
+ }
+ }
+
+ if (server != null) {
+ Guild guild = new Guild(server.getIdAsString());
+ guild.getGuild();
+
+ if (guild.isEnforceSeverLanguage()) {
+ String guildLocaleTag = guild.getLocale();
+ if (guildLocaleTag != null && !guildLocaleTag.isBlank()) {
+ String normalized = guildLocaleTag.replace('_', '-');
+ Locale parsed = Locale.forLanguageTag(normalized);
+ if (parsed != null && !"und".equals(parsed.toLanguageTag())) {
+ locale = parsed;
+ } else if (server.getPreferredLocale() != null) {
+ locale = server.getPreferredLocale();
+ } else {
+ locale = fallbackLocale;
+ }
+ } else if (server.getPreferredLocale() != null) {
+ locale = server.getPreferredLocale();
+ } else {
+ locale = fallbackLocale;
+ }
+ }
+ }
+ } catch (IOException e) {
+ logger.error("ClaireData failed to return a response for Locale information. Are we cooked?");
+ locale = fallbackLocale;
+ }
+ return locale;
+ }
+
+ private RobinConfiguration parseUserAndServerOptions(Server server, User user) {
+ Locale locale;
+ try {
+ // If user is null (e.g., triggered by a system action or missing context),
+ // default to fallback locale and continue to evaluate server-level overrides.
+ if (user == null) {
+ locale = fallbackLocale;
+ } else {
+ APIUser apiUser = new APIUser(user.getIdAsString());
+ apiUser.getUser();
+ String rawLang = apiUser.getLanguage();
+
+ // Normalize ClaireData language strings (e.g., en_US -> en-US). If empty/null, use fallback.
+ if (rawLang == null || rawLang.isBlank()) {
+ locale = fallbackLocale;
+ } else {
+ String normalizedTag = rawLang.replace('_', '-');
+ locale = Locale.forLanguageTag(normalizedTag);
+ // Guard against Locale.ROOT ("und") resulting from invalid tags
+ if (locale == null || locale.toLanguageTag().equals("und")) {
+ locale = fallbackLocale;
+ }
+ }
+ }
+
+ // 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()) {
+ // Respect stored guild locale when enforcement is enabled.
+ String guildLocaleTag = guild.getLocale();
+ if (guildLocaleTag != null && !guildLocaleTag.isBlank()) {
+ String normalized = guildLocaleTag.replace('_', '-');
+ Locale parsed = Locale.forLanguageTag(normalized);
+ if (parsed != null && !"und".equals(parsed.toLanguageTag())) {
+ locale = parsed;
+ } else if (server.getPreferredLocale() != null) {
+ locale = server.getPreferredLocale();
+ } else {
+ locale = fallbackLocale;
+ }
+ } else if (server.getPreferredLocale() != null) {
+ // Fallback to Discord server preferred locale if guild setting is absent
+ locale = server.getPreferredLocale();
+ } else {
+ locale = fallbackLocale;
+ }
+ }
+ }
+ } 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..ba95532
--- /dev/null
+++ b/src/main/java/com/sidpatchy/clairebot/Lang/PlaceholderHandler.java
@@ -0,0 +1,193 @@
+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.awt.*;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+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)),
+ entry("cb.command.help.name", () -> String.valueOf(Main.getCommands().getHelp().getName())),
+
+ // Server placeholders
+ entry("cb.server.name", () ->
+ context.server() != null ? context.server().getName() : ""),
+ entry("cb.server.id", () ->
+ context.server() != null ? context.server().getIdAsString() : ""),
+
+ // User placeholders
+ entry("cb.user.name", () ->
+ context.user() != null ? context.user().getName() : ""),
+ entry("cb.user.id", () ->
+ context.user() != null ? context.user().getIdAsString() : ""),
+ entry("cb.user.id.mentiontag", () ->
+ context.user() != null ? "<@" + context.user().getIdAsString() + ">" : ""),
+ entry("cb.user.id.accentcolour", () -> {
+ Color color = Main.getColor(Objects.requireNonNull(context.user()).getIdAsString());
+ return String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue());
+ }),
+ entry("cb.user.id.displayname.server", () -> {
+ org.javacord.api.entity.server.Server server = context.server();
+ org.javacord.api.entity.user.User author = context.author();
+ if (author != null) {
+ return (server != null) ? author.getDisplayName(server) : author.getName();
+ }
+
+ org.javacord.api.entity.user.User user = context.user();
+ if (user != null) {
+ return (server != null) ? user.getDisplayName(server) : user.getName();
+ }
+
+ return "";
+ }),
+
+ // Author placeholders
+ entry("cb.author.name", () ->
+ context.author() != null ? context.author().getName() : ""),
+ entry("cb.author.id", () ->
+ context.author() != null ? context.author().getIdAsString() : ""),
+
+ // Channel placeholders
+ entry("cb.channel.name", () ->
+ Optional.ofNullable(context.channel())
+ .flatMap(Channel::asServerChannel)
+ .map(ServerChannel::getName)
+ .orElse("NOT FOUND")),
+ entry("cb.channel.id", () ->
+ context.channel() != null ? context.channel().getIdAsString() : ""),
+ entry("cb.channel.id.mentiontag", () ->
+ context.channel() != null ? "<#" + context.channel().getIdAsString() + ">" : ""),
+ // Specific placeholders used by acknowledgements when the selected channel differs from invoking channel
+ entry("cb.channel.requests.mentiontag", () -> {
+ Object v = context.getData(ContextManager.ContextType.GENERIC, "cb.channel.requests.mentiontag");
+ return v != null ? v.toString() : "";
+ }),
+ entry("cb.channel.moderator.mentiontag", () -> {
+ Object v = context.getData(ContextManager.ContextType.GENERIC, "cb.channel.moderator.mentiontag");
+ return v != null ? v.toString() : "";
+ }),
+
+ // Command placeholders
+ entry("cb.commandname", () ->
+ String.valueOf(Objects.requireNonNull((Object) context.getData(ContextManager.ContextType.GENERIC, "commandname")))),
+ entry("cb.user.id.username", () ->
+ Optional.ofNullable(context.author())
+ .map(org.javacord.api.entity.user.User::getDiscriminatedName)
+ .orElseGet(() -> {
+ Object v = context.getData(ContextManager.ContextType.GENERIC, "user.id.username");
+ return v != null ? v.toString() : "";
+ })),
+ // Voting placeholders
+ entry("cb.poll.id", () ->
+ String.valueOf(Objects.requireNonNull((Object) context.getData(ContextManager.ContextType.GENERIC, "poll.id")))),
+ entry("cb.voting.optionnumber", () ->
+ String.valueOf(Objects.requireNonNull((Object) context.getData(ContextManager.ContextType.GENERIC, "voting.optionnumber")))),
+
+ // Error code
+ entry("cb.errorcode", () ->
+ String.valueOf(Objects.requireNonNull((Object) context.getData(ContextManager.ContextType.GENERIC, "errorcode"))))
+ );
+ }
+
+ /**
+ * 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);
+ StringBuilder result = new StringBuilder();
+
+ 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..741891f 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);
@@ -45,33 +50,35 @@ public void onButtonClick(ButtonClickEvent event) {
switch (buttonID) {
case "rules":
buttonInteraction.respondWithModal("santa-rules-" + message.getIdAsString(), "Update Rules",
- SantaModal.getRulesRow()
+ SantaModal.getRulesRow(languageManager)
);
break;
case "theme":
buttonInteraction.respondWithModal("santa-theme-" + message.getIdAsString(), "Update Theme",
- SantaModal.getThemeRow()
+ SantaModal.getThemeRow(languageManager)
);
break;
case "send":
buttonInteraction.acknowledge();
for (int i = 0; i < extractionResult.givers.size(); i++) {
- SantaEmbed.getSantaMessage(server, author, extractionResult.givers.get(i), extractionResult.receivers.get(i), extractionResult.rules, extractionResult.theme).send(extractionResult.givers.get(i));
+ SantaEmbed.getSantaMessage(languageManager, server, author, extractionResult.givers.get(i), extractionResult.receivers.get(i), extractionResult.rules, extractionResult.theme).send(extractionResult.givers.get(i));
}
break;
case "test":
buttonInteraction.acknowledge();
- SantaEmbed.getSantaMessage(server, author, extractionResult.givers.get(0), extractionResult.receivers.get(0), extractionResult.rules, extractionResult.theme).send(buttonAuthor);
+ SantaEmbed.getSantaMessage(languageManager, server, author, extractionResult.givers.get(0), extractionResult.receivers.get(0), extractionResult.rules, extractionResult.theme).send(buttonAuthor);
break;
case "randomize":
buttonInteraction.acknowledge();
Role role = Main.getApi().getRoleById(extractionResult.santaID.get("roleID")).orElse(null);
- buttonInteraction.getMessage().delete();
- SantaEmbed.getHostMessage(role, buttonAuthor, extractionResult.rules, extractionResult.theme).send(buttonAuthor);
+ // Edit the existing host message in place with a fresh randomized pairing
+ buttonInteraction.getMessage().edit(
+ SantaEmbed.buildHostEmbedRandomized(languageManager, role, author, extractionResult.rules, extractionResult.theme)
+ );
break;
diff --git a/src/main/java/com/sidpatchy/clairebot/Listener/MessageCreate.java b/src/main/java/com/sidpatchy/clairebot/Listener/MessageCreate.java
index 1f4a372..8511a9e 100644
--- a/src/main/java/com/sidpatchy/clairebot/Listener/MessageCreate.java
+++ b/src/main/java/com/sidpatchy/clairebot/Listener/MessageCreate.java
@@ -1,15 +1,17 @@
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;
@@ -18,7 +20,7 @@
import java.util.List;
import java.util.Map;
import java.util.Random;
-import java.util.random.RandomGenerator;
+import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Pattern;
public class MessageCreate implements MessageCreateListener {
@@ -28,7 +30,17 @@ 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();
+
+ // Some messages (e.g., webhooks/system) have no user; skip processing to avoid NPEs in localization
+ if (user == null) {
+ return;
+ }
+
+ ContextManager context = new ContextManager(server, textChannel, user, user, message, new HashMap<>());
+ LanguageManager languageManager = new LanguageManager(Main.getFallbackLocale(), 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 +49,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 +59,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 +69,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 +81,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 +102,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/ModalSubmit.java b/src/main/java/com/sidpatchy/clairebot/Listener/ModalSubmit.java
index 95da653..9f78e43 100644
--- a/src/main/java/com/sidpatchy/clairebot/Listener/ModalSubmit.java
+++ b/src/main/java/com/sidpatchy/clairebot/Listener/ModalSubmit.java
@@ -3,10 +3,14 @@
import com.sidpatchy.clairebot.Embed.Commands.Regular.SantaEmbed;
import com.sidpatchy.clairebot.Embed.Commands.Regular.UserPreferencesEmbed;
import com.sidpatchy.clairebot.Embed.Commands.Regular.VotingEmbed;
+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.Util.ChannelUtils;
import com.sidpatchy.clairebot.Util.SantaUtils;
import org.javacord.api.entity.channel.ServerTextChannel;
+import org.javacord.api.entity.channel.TextChannel;
import org.javacord.api.entity.message.Message;
import org.javacord.api.entity.message.MessageFlag;
import org.javacord.api.entity.message.embed.Embed;
@@ -16,19 +20,30 @@
import org.javacord.api.entity.user.User;
import org.javacord.api.event.interaction.ModalSubmitEvent;
import org.javacord.api.interaction.ModalInteraction;
+import org.javacord.api.interaction.callback.InteractionOriginalResponseUpdater;
import org.javacord.api.listener.interaction.ModalSubmitListener;
+import java.util.HashMap;
+import java.util.concurrent.CompletableFuture;
+
public class ModalSubmit implements ModalSubmitListener {
+ private LanguageManager languageManager;
+
@Override
public void onModalSubmit(ModalSubmitEvent event) {
ModalInteraction modalInteraction = event.getModalInteraction();
+ Server server = modalInteraction.getServer().orElse(null);
+ TextChannel textchannel = modalInteraction.getChannel().orElse(null);
User user = modalInteraction.getUser();
String modalID = modalInteraction.getCustomId();
Main.getLogger().debug(modalID);
+ ContextManager context = new ContextManager(server, textchannel, user, user, null, new HashMap<>());
+ languageManager = new LanguageManager(Main.getFallbackLocale(), context);
+
String voteType = ""; // Allows the Poll/Request feature to distinguish between the two
// Santa related vars
@@ -56,7 +71,7 @@ else if (modalID.startsWith("santa-theme")) {
santaMessage = Main.getApi().getCachedMessageById(santaMessageID).orElse(null);
assert santaMessage != null;
- Embed embed = santaMessage.getEmbeds().get(0);
+ Embed embed = santaMessage.getEmbeds().getFirst();
EmbedFooter footer = embed.getFooter().orElse(null);
extractionResult = SantaUtils.extractDataFromEmbed(embed, footer);
@@ -77,7 +92,7 @@ else if (modalID.startsWith("santa-theme")) {
Main.getLogger().debug(hexColour);
modalInteraction.createImmediateResponder()
- .addEmbed(UserPreferencesEmbed.getAcknowledgeAccentColourChange(user, hexColour))
+ .addEmbed(UserPreferencesEmbed.getAcknowledgeAccentColourChange(languageManager, user, hexColour))
.setFlags(MessageFlag.EPHEMERAL)
.respond();
}
@@ -85,7 +100,6 @@ else if (modalID.startsWith("santa-theme")) {
case "request":
String question = modalInteraction.getTextInputValueByCustomId("question-modal").orElse("");
String description = modalInteraction.getTextInputValueByCustomId("details-modal").orElse("");
- Server server = modalInteraction.getServer().orElse(null);
User author = modalInteraction.getUser();
if (server == null) {
@@ -94,30 +108,49 @@ else if (modalID.startsWith("santa-theme")) {
if (voteType.equalsIgnoreCase("request")) {
ServerTextChannel requestsChannel = ChannelUtils.getRequestsChannel(server);
- modalInteraction.createImmediateResponder()
- .addEmbed(VotingEmbed.getUserResponse(author, requestsChannel.getMentionTag()))
- .setFlags(MessageFlag.EPHEMERAL)
- .respond();
-
- requestsChannel.sendMessage(VotingEmbed.getPoll(voteType, question, description, false, null, server, author, 0));
+ if (requestsChannel == null) {
+ modalInteraction.createImmediateResponder()
+ .addEmbed(ErrorEmbed.getCustomError(languageManager, Main.getErrorCode("requestsChannelMissing"), "A requests channel is not configured for this server. An admin can set one in /config server > Requests Channel."))
+ .setFlags(MessageFlag.EPHEMERAL)
+ .respond();
+ } else {
+ modalInteraction.createImmediateResponder()
+ .addEmbed(VotingEmbed.getUserResponse(languageManager, author, requestsChannel.getMentionTag()))
+ .setFlags(MessageFlag.EPHEMERAL)
+ .respond();
+
+ requestsChannel.sendMessage(VotingEmbed.getPoll(languageManager, voteType, question, description, false, null, server, author, 0));
+ }
}
else if (voteType.equalsIgnoreCase("poll")) {
modalInteraction.createImmediateResponder()
- .addEmbed(VotingEmbed.getPoll(voteType, question, description, false, null, server, author, 0))
+ .addEmbed(VotingEmbed.getPoll(languageManager, voteType, question, description, false, null, server, author, 0))
.respond();
}
break;
case "santa-rules", "santa-theme":
- modalInteraction.createImmediateResponder().respond();
+ // Silently defer the modal response (no visible message), then update the existing host message in place
+ CompletableFuture deferred = modalInteraction.respondLater();
+
+ assert extractionResult != null;
extractionResult.rules = modalInteraction.getTextInputValueByCustomId("rules-row").orElse(extractionResult.rules);
extractionResult.theme = modalInteraction.getTextInputValueByCustomId("theme-row").orElse(extractionResult.theme);
Role role = Main.getApi().getRoleById(extractionResult.santaID.get("roleID")).orElse(null);
assert role != null;
- SantaEmbed.getHostMessage(role, user, extractionResult.rules, extractionResult.theme).send(user);
- santaMessage.delete();
+ // Rebuild the host embed with the existing giver/receiver pairs and edit the original message
+ santaMessage.edit(SantaEmbed.buildHostEmbedFromPairs(languageManager, role, user, extractionResult.rules, extractionResult.theme, extractionResult.givers, extractionResult.receivers));
+ // Finalize and immediately delete the deferred response to avoid leaving a "Bot is thinking…" stub
+ deferred.thenAccept(interactionOriginalResponseUpdater -> {
+ try {
+ interactionOriginalResponseUpdater.setContent("\u200B").update().thenAccept(msg -> {
+ try { msg.delete(); } catch (Exception ignored2) {}
+ });
+ } catch (Exception ignored) {}
+ });
+ break;
}
}
}
diff --git a/src/main/java/com/sidpatchy/clairebot/Listener/SelectMenuChoose.java b/src/main/java/com/sidpatchy/clairebot/Listener/SelectMenuChoose.java
index 67b02c1..97524f8 100644
--- a/src/main/java/com/sidpatchy/clairebot/Listener/SelectMenuChoose.java
+++ b/src/main/java/com/sidpatchy/clairebot/Listener/SelectMenuChoose.java
@@ -3,6 +3,8 @@
import com.sidpatchy.clairebot.Embed.Commands.Regular.ServerPreferencesEmbed;
import com.sidpatchy.clairebot.Embed.Commands.Regular.UserPreferencesEmbed;
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;
@@ -10,15 +12,17 @@
import org.javacord.api.entity.channel.TextChannel;
import org.javacord.api.entity.message.Message;
import org.javacord.api.entity.message.MessageFlag;
-import org.javacord.api.entity.message.embed.EmbedAuthor;
import org.javacord.api.entity.server.Server;
import org.javacord.api.entity.user.User;
import org.javacord.api.event.interaction.SelectMenuChooseEvent;
import org.javacord.api.interaction.SelectMenuInteraction;
import org.javacord.api.listener.interaction.SelectMenuChooseListener;
+import java.util.HashMap;
+
public class SelectMenuChoose implements SelectMenuChooseListener {
Logger logger = Main.getLogger();
+ private LanguageManager languageManager;
@Override
public void onSelectMenuChoose(SelectMenuChooseEvent event) {
@@ -29,132 +33,181 @@ public void onSelectMenuChoose(SelectMenuChooseEvent event) {
Server server = selectMenuInteraction.getServer().orElse(null);
TextChannel channel = selectMenuInteraction.getChannel().orElse(null);
- // Not speaking of message author, rather, the header field
- EmbedAuthor embedAuthor = message.getEmbeds().get(0).getAuthor().orElse(null);
- if (embedAuthor != null && channel != null) {
- String menuName = embedAuthor.getName();
- String label = selectMenuInteraction.getChosenOptions().get(0).getLabel();
- String id = selectMenuInteraction.getChosenOptions().get(0).getValue();
+ ContextManager context = new ContextManager(server, channel, user, user, null, new HashMap<>());
+ languageManager = new LanguageManager(Main.getFallbackLocale(), context);
- // User preferences menu
- if (menuName.equalsIgnoreCase("User Preferences Editor")) {
- selectMenuInteraction.acknowledge();
+ // Route exclusively by customId and stable option values to avoid localization issues
+ String customId = selectMenuInteraction.getCustomId();
+ String value = selectMenuInteraction.getChosenOptions().get(0).getValue();
- if (label.equalsIgnoreCase("Accent Colour")) {
- selectMenuInteraction.createFollowupMessageBuilder()
- .setFlags(MessageFlag.EPHEMERAL)
- .addEmbed(UserPreferencesEmbed.getAccentColourMenu(user))
- .addComponents(UserPreferencesComponents.getAccentColourMenu())
- .send();
- }
- else if (label.equalsIgnoreCase("Language")) {
- selectMenuInteraction.createFollowupMessageBuilder()
- .setFlags(MessageFlag.EPHEMERAL)
- .addEmbed(UserPreferencesEmbed.getLanguageMenu(user))
- .addComponents()
- .send();
- }
+ // User preferences main menu (customId: "settings")
+ if ("settings".equalsIgnoreCase(customId)) {
+ // Values are stable English identifiers set in UserPreferencesComponents
+ if ("Accent Colour Editor".equalsIgnoreCase(value)) {
+ selectMenuInteraction.acknowledge();
+ selectMenuInteraction.createFollowupMessageBuilder()
+ .setFlags(MessageFlag.EPHEMERAL)
+ .addEmbed(UserPreferencesEmbed.getAccentColourMenu(languageManager, user))
+ .addComponents(UserPreferencesComponents.getAccentColourMenu(languageManager))
+ .send();
}
- else if (menuName.equalsIgnoreCase("Accent Colour Editor")) {
- if (label.equalsIgnoreCase("Select Common Colours")) {
- selectMenuInteraction.createFollowupMessageBuilder()
- .setFlags(MessageFlag.EPHEMERAL)
- .addEmbed(UserPreferencesEmbed.getAccentColourListMenu(user))
- .addComponents(UserPreferencesComponents.getAccentColourList())
- .send();
- }
- else if (label.equalsIgnoreCase("Hexadecimal Entry")) {
- message.delete();
- selectMenuInteraction.respondWithModal("hex-entry-modal", "Hex Colour Entry", UserPreferencesComponents.getAccentColourHexEntry());
- }
+ else if ("Language Editor".equalsIgnoreCase(value)) {
selectMenuInteraction.acknowledge();
+ selectMenuInteraction.createFollowupMessageBuilder()
+ .setFlags(MessageFlag.EPHEMERAL)
+ .addEmbed(UserPreferencesEmbed.getLanguageMenu(languageManager, user))
+ .addComponents(UserPreferencesComponents.getLanguageMenu(languageManager))
+ .send();
}
- else if (menuName.equalsIgnoreCase("Accent Colour List")) {
+ else if ("Requests Channel".equalsIgnoreCase(value)
+ || "Moderator Messages Channel".equalsIgnoreCase(value)
+ || "Enforce Server Language".equalsIgnoreCase(value)) {
+ // This block will only be hit if server settings menu mistakenly used the same customId.
+ // We keep it for backward compatibility; prefer using "server-settings" going forward.
selectMenuInteraction.acknowledge();
-
- String accentColour = selectMenuInteraction.getChosenOptions().get(0).getDescription().orElse("");
-
- if (accentColour.isEmpty()) {
+ if (server == null) {
selectMenuInteraction.createFollowupMessageBuilder()
.setFlags(MessageFlag.EPHEMERAL)
- .addEmbed(ErrorEmbed.getError(Main.getErrorCode("accentColourParse")))
+ .addEmbed(ServerPreferencesEmbed.getNotServerMenu(languageManager))
+ .addComponents()
.send();
- }
- else {
+ } else if ("Requests Channel".equalsIgnoreCase(value)) {
selectMenuInteraction.createFollowupMessageBuilder()
.setFlags(MessageFlag.EPHEMERAL)
- .addEmbed(UserPreferencesEmbed.getAcknowledgeAccentColourChange(user, accentColour))
- .addComponents()
+ .addEmbed(ServerPreferencesEmbed.getRequestsChannelMenu(languageManager, user))
+ .addComponents(ServerPreferencesComponents.getRequestsChannelMenu(languageManager, server))
.send();
- }
- }
-
- // Server configuration
- else if (menuName.equalsIgnoreCase("Server Configuration Editor")) {
- selectMenuInteraction.acknowledge();
-
- if (server == null) {
+ } else if ("Moderator Messages Channel".equalsIgnoreCase(value)) {
selectMenuInteraction.createFollowupMessageBuilder()
.setFlags(MessageFlag.EPHEMERAL)
- .addEmbed(ServerPreferencesEmbed.getNotServerMenu())
- .addComponents()
+ .addEmbed(ServerPreferencesEmbed.getModeratorChannelMenu(languageManager, user))
+ .addComponents(ServerPreferencesComponents.getModeratorChannelMenu(languageManager, server))
.send();
- }
- else if (label.equalsIgnoreCase("Requests Channel")) {
+ } else if ("Enforce Server Language".equalsIgnoreCase(value)) {
selectMenuInteraction.createFollowupMessageBuilder()
.setFlags(MessageFlag.EPHEMERAL)
- .addEmbed(ServerPreferencesEmbed.getRequestsChannelMenu(user))
- .addComponents(ServerPreferencesComponents.getRequestsChannelMenu(server))
+ .addEmbed(ServerPreferencesEmbed.getEnforceServerLangMenu(languageManager, user))
+ .addComponents(ServerPreferencesComponents.getEnforceServerLanguageMenu(languageManager))
.send();
}
- else if (label.equalsIgnoreCase("Moderator Messages Channel")) {
+ }
+ }
+ // User preferences accent color submenu and list share customId "accent-color"
+ else if ("accent-color".equalsIgnoreCase(customId)) {
+ // Two possible values from the first submenu, otherwise treat as selecting a color from the list
+ if ("Select Common Colours".equalsIgnoreCase(value)) {
+ selectMenuInteraction.acknowledge();
+ selectMenuInteraction.createFollowupMessageBuilder()
+ .setFlags(MessageFlag.EPHEMERAL)
+ .addEmbed(UserPreferencesEmbed.getAccentColourListMenu(languageManager, user))
+ .addComponents(UserPreferencesComponents.getAccentColourList(languageManager))
+ .send();
+ }
+ else if ("Hexadecimal Entry".equalsIgnoreCase(value)) {
+ // Switch to modal input, delete the menu message to reduce clutter
+ selectMenuInteraction.acknowledge();
+ message.delete();
+ selectMenuInteraction.respondWithModal("hex-entry-modal", "Hex Colour Entry", UserPreferencesComponents.getAccentColourHexEntry(languageManager));
+ }
+ else {
+ // Selecting a specific color from the list; hex is stored as the description
+ selectMenuInteraction.acknowledge();
+ String accentColour = selectMenuInteraction.getChosenOptions().get(0).getDescription().orElse("");
+ if (accentColour.isEmpty()) {
selectMenuInteraction.createFollowupMessageBuilder()
.setFlags(MessageFlag.EPHEMERAL)
- .addEmbed(ServerPreferencesEmbed.getModeratorChannelMenu(user))
- .addComponents(ServerPreferencesComponents.getModeratorChannelMenu(server))
+ .addEmbed(ErrorEmbed.getError(languageManager, Main.getErrorCode("accentColourParse")))
.send();
- }
- else if (label.equalsIgnoreCase("Enforce Server Language")) {
+ } else {
selectMenuInteraction.createFollowupMessageBuilder()
.setFlags(MessageFlag.EPHEMERAL)
- .addEmbed(ServerPreferencesEmbed.getEnforceServerLangMenu(user))
- .addComponents(ServerPreferencesComponents.getEnforceServerLanguageMenu())
+ .addEmbed(UserPreferencesEmbed.getAcknowledgeAccentColourChange(languageManager, user, accentColour))
+ .addComponents()
.send();
}
}
- else if (menuName.equalsIgnoreCase("Requests Channel")) {
- String channelID = selectMenuInteraction.getChosenOptions().get(0).getValue();
-
- selectMenuInteraction.acknowledge();
-
+ }
+ // Server configuration main menu (new customId: "server-settings")
+ else if ("server-settings".equalsIgnoreCase(customId)) {
+ selectMenuInteraction.acknowledge();
+ if (server == null) {
selectMenuInteraction.createFollowupMessageBuilder()
.setFlags(MessageFlag.EPHEMERAL)
- .addEmbed(ServerPreferencesEmbed.getAcknowledgeRequestsChannelChange(server, user, channelID))
+ .addEmbed(ServerPreferencesEmbed.getNotServerMenu(languageManager))
.addComponents()
.send();
- }
- else if (menuName.equalsIgnoreCase("Moderator Messages Channel")) {
- String channelID = selectMenuInteraction.getChosenOptions().get(0).getValue();
-
- selectMenuInteraction.acknowledge();
-
+ } else if ("Requests Channel".equalsIgnoreCase(value)) {
selectMenuInteraction.createFollowupMessageBuilder()
.setFlags(MessageFlag.EPHEMERAL)
- .addEmbed(ServerPreferencesEmbed.getAcknowledgeModeratorChannelChange(server, user, channelID))
- .addComponents()
+ .addEmbed(ServerPreferencesEmbed.getRequestsChannelMenu(languageManager, user))
+ .addComponents(ServerPreferencesComponents.getRequestsChannelMenu(languageManager, server))
.send();
- }
- else if (menuName.equalsIgnoreCase("Enforce Server Language")) {
- String bool = selectMenuInteraction.getChosenOptions().get(0).getValue();
-
- selectMenuInteraction.acknowledge();
-
+ } else if ("Moderator Messages Channel".equalsIgnoreCase(value)) {
selectMenuInteraction.createFollowupMessageBuilder()
.setFlags(MessageFlag.EPHEMERAL)
- .addEmbed(ServerPreferencesEmbed.getAcknowledgeEnforceServerLanguageUpdate(server, user, bool))
- .addComponents()
+ .addEmbed(ServerPreferencesEmbed.getModeratorChannelMenu(languageManager, user))
+ .addComponents(ServerPreferencesComponents.getModeratorChannelMenu(languageManager, server))
+ .send();
+ } else if ("Enforce Server Language".equalsIgnoreCase(value)) {
+ selectMenuInteraction.createFollowupMessageBuilder()
+ .setFlags(MessageFlag.EPHEMERAL)
+ .addEmbed(ServerPreferencesEmbed.getEnforceServerLangMenu(languageManager, user))
+ .addComponents(ServerPreferencesComponents.getEnforceServerLanguageMenu(languageManager))
+ .send();
+ } else if ("Server Language".equalsIgnoreCase(value)) {
+ selectMenuInteraction.createFollowupMessageBuilder()
+ .setFlags(MessageFlag.EPHEMERAL)
+ .addEmbed(ServerPreferencesEmbed.getServerLanguageMenu(languageManager, user))
+ .addComponents(ServerPreferencesComponents.getServerLanguageMenu(languageManager))
.send();
}
}
+ // Server channel selection submenus
+ else if ("requestsChannel".equalsIgnoreCase(customId)) {
+ String channelID = value; // value holds the selected channel ID
+ selectMenuInteraction.acknowledge();
+ selectMenuInteraction.createFollowupMessageBuilder()
+ .setFlags(MessageFlag.EPHEMERAL)
+ .addEmbed(ServerPreferencesEmbed.getAcknowledgeRequestsChannelChange(languageManager, server, user, channelID))
+ .addComponents()
+ .send();
+ }
+ else if ("moderatorChannel".equalsIgnoreCase(customId)) {
+ String channelID = value; // value holds the selected channel ID
+ selectMenuInteraction.acknowledge();
+ selectMenuInteraction.createFollowupMessageBuilder()
+ .setFlags(MessageFlag.EPHEMERAL)
+ .addEmbed(ServerPreferencesEmbed.getAcknowledgeModeratorChannelChange(languageManager, server, user, channelID))
+ .addComponents()
+ .send();
+ }
+ else if ("enforceServerLanguage".equalsIgnoreCase(customId)) {
+ String bool = value;
+ selectMenuInteraction.acknowledge();
+ selectMenuInteraction.createFollowupMessageBuilder()
+ .setFlags(MessageFlag.EPHEMERAL)
+ .addEmbed(ServerPreferencesEmbed.getAcknowledgeEnforceServerLanguageUpdate(languageManager, server, user, bool))
+ .addComponents()
+ .send();
+ }
+ else if ("server-language".equalsIgnoreCase(customId)) {
+ String languageTag = value;
+ selectMenuInteraction.acknowledge();
+ selectMenuInteraction.createFollowupMessageBuilder()
+ .setFlags(MessageFlag.EPHEMERAL)
+ .addEmbed(ServerPreferencesEmbed.getAcknowledgeServerLanguageChange(languageManager, server, user, languageTag))
+ .addComponents()
+ .send();
+ }
+ else if ("user-language".equalsIgnoreCase(customId)) {
+ // Value is the IETF language tag (e.g., en-US)
+ String languageTag = value;
+ selectMenuInteraction.acknowledge();
+ selectMenuInteraction.createFollowupMessageBuilder()
+ .setFlags(MessageFlag.EPHEMERAL)
+ .addEmbed(UserPreferencesEmbed.getAcknowledgeLanguageChange(languageManager, user, languageTag))
+ .addComponents()
+ .send();
+ }
}
}
diff --git a/src/main/java/com/sidpatchy/clairebot/Listener/ServerJoin.java b/src/main/java/com/sidpatchy/clairebot/Listener/ServerJoin.java
index b689d8f..db100c8 100644
--- a/src/main/java/com/sidpatchy/clairebot/Listener/ServerJoin.java
+++ b/src/main/java/com/sidpatchy/clairebot/Listener/ServerJoin.java
@@ -1,6 +1,8 @@
package com.sidpatchy.clairebot.Listener;
import com.sidpatchy.clairebot.Embed.WelcomeEmbed;
+import com.sidpatchy.clairebot.Lang.LanguageManager;
+import com.sidpatchy.clairebot.Main;
import com.sidpatchy.clairebot.Util.ChannelUtils;
import org.javacord.api.entity.channel.TextChannel;
import org.javacord.api.entity.server.Server;
@@ -9,6 +11,8 @@
public class ServerJoin implements ServerJoinListener {
+ private LanguageManager languageManager;
+
/**
*
* Welcome users to ClaireBot when added to a new server.
@@ -19,7 +23,9 @@ public class ServerJoin implements ServerJoinListener {
public void onServerJoin(ServerJoinEvent event) {
Server server = event.getServer();
+ languageManager = new LanguageManager(Main.getFallbackLocale(), null); // this null should probably be fine
+
TextChannel channel = ChannelUtils.getModeratorsOnlyChannel(server);
- channel.sendMessage(WelcomeEmbed.getWelcome(server));
+ channel.sendMessage(WelcomeEmbed.getWelcome(languageManager, server));
}
}
diff --git a/src/main/java/com/sidpatchy/clairebot/Listener/SlashCommandCreate.java b/src/main/java/com/sidpatchy/clairebot/Listener/SlashCommandCreate.java
index 56f2bac..e8695b2 100644
--- a/src/main/java/com/sidpatchy/clairebot/Listener/SlashCommandCreate.java
+++ b/src/main/java/com/sidpatchy/clairebot/Listener/SlashCommandCreate.java
@@ -1,14 +1,17 @@
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;
import com.sidpatchy.clairebot.MessageComponents.Regular.VotingComponents;
import com.sidpatchy.clairebot.Util.ChannelUtils;
import org.apache.logging.log4j.Logger;
+import org.javacord.api.entity.channel.ServerTextChannel;
import org.javacord.api.entity.channel.TextChannel;
import org.javacord.api.entity.message.MessageFlag;
import org.javacord.api.entity.message.component.ActionRow;
@@ -24,13 +27,15 @@
import java.io.FileNotFoundException;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
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,27 +62,27 @@ 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();
+ logger.error("Error while creating eightball embed: ", e);
}
});
}
- 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")) {
slashCommandInteraction.createImmediateResponder()
.setFlags(MessageFlag.EPHEMERAL)
- .addEmbed(UserPreferencesEmbed.getMainMenu(author))
- .addComponents(UserPreferencesComponents.getMainMenu())
+ .addEmbed(UserPreferencesEmbed.getMainMenu(languageManager, author))
+ .addComponents(UserPreferencesComponents.getMainMenu(languageManager))
.respond();
}
else if (mode.equalsIgnoreCase("server") && server != null) {
@@ -79,50 +90,50 @@ else if (mode.equalsIgnoreCase("server") && server != null) {
if (server.isAdmin(author)) {
slashCommandInteraction.createImmediateResponder()
.setFlags(MessageFlag.EPHEMERAL)
- .addEmbed(ServerPreferencesEmbed.getMainMenu(author))
- .addComponents(ServerPreferencesComponents.getMainMenu())
+ .addEmbed(ServerPreferencesEmbed.getMainMenu(languageManager, author))
+ .addComponents(ServerPreferencesComponents.getMainMenu(languageManager))
.respond();
}
else {
slashCommandInteraction.createImmediateResponder()
.setFlags(MessageFlag.EPHEMERAL)
- .addEmbed(ErrorEmbed.getLackingPermissions("You do not have permission to run that command!"))
+ .addEmbed(ErrorEmbed.getLackingPermissions(languageManager, "You do not have permission to run that command!"))
.respond();
}
}
}
- 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,22 +146,22 @@ 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
CompletableFuture pollModal = slashCommandInteraction.respondWithModal("poll", "Create Poll",
- VotingComponents.getQuestionRow(),
- VotingComponents.getDetailsRow()
+ VotingComponents.getQuestionRow(languageManager),
+ VotingComponents.getDetailsRow(languageManager)
);
pollModal.exceptionally(e -> {
- e.printStackTrace();
+ logger.error("Error while creating poll modal: ", e);
return null;
});
}
catch (Exception e) {
- e.printStackTrace();
+ logger.error("Error while creating poll modal: ", e);
}
}
else {
@@ -170,27 +181,35 @@ else if (commandName.equalsIgnoreCase(parseCommands.getCommandName("poll"))) {
int finalNumChoices = numChoices;
slashCommandInteraction.respondLater().thenAccept(interactionOriginalResponseUpdater -> {
- interactionOriginalResponseUpdater.addEmbed(VotingEmbed.getPoll("POLL", question, allowMultipleChoices, choices, server, author, finalNumChoices))
+ interactionOriginalResponseUpdater.addEmbed(VotingEmbed.getPoll(languageManager, "POLL", question, allowMultipleChoices, choices, server, author, finalNumChoices))
.update().thenAccept(message -> {
- message.addReaction("\uD83D\uDC4D"); // 👍 emoji
- message.addReaction("\uD83D\uDC4E"); // 👎 emoji
- message.addReaction(":vote:706373563564949566"); // Custom emoji
+ if (finalNumChoices > 0) {
+ String[] numberEmojis = new String[]{"1️⃣","2️⃣","3️⃣","4️⃣","5️⃣","6️⃣","7️⃣","8️⃣","9️⃣","🔟"};
+ int limit = Math.min(finalNumChoices, numberEmojis.length);
+ for (int i = 0; i < limit; i++) {
+ message.addReaction(numberEmojis[i]);
+ }
+ } else {
+ message.addReaction("\uD83D\uDC4D"); // 👍 emoji
+ message.addReaction("\uD83D\uDC4E"); // 👎 emoji
+ message.addReaction(":vote:706373563564949566"); // Custom emoji
+ }
});
});
}
}
- 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()
- .addEmbed(ErrorEmbed.getError("NotInAChannel"))
+ .addEmbed(ErrorEmbed.getError(languageManager, "NotInAChannel"))
.respond();
return;
}
// Construct response and update message
slashCommandInteraction.respondLater().thenAccept(interactionOriginalResponseUpdater -> {
- QuoteEmbed.getQuote(server, user, channel).thenAccept(embed -> {
+ QuoteEmbed.getQuote(languageManager, server, user, channel).thenAccept(embed -> {
// Create an ActionRow with a button
ActionRow actionRow = ActionRow.of(
Button.primary("view_original", "View Original")
@@ -203,10 +222,10 @@ 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"),
+ .addEmbed(ErrorEmbed.getCustomError(languageManager, Main.getErrorCode("notaserver"),
"You must run this command inside a server!"))
.respond();
return;
@@ -216,17 +235,17 @@ else if (commandName.equalsIgnoreCase(parseCommands.getCommandName("request")))
try {
// LOL how long has this been unimplemented? Not a bad idea tbh 2023-02-16
CompletableFuture pollModal = slashCommandInteraction.respondWithModal("request", "Create Request",
- VotingComponents.getQuestionRow(),
- VotingComponents.getDetailsRow()
+ VotingComponents.getQuestionRow(languageManager),
+ VotingComponents.getDetailsRow(languageManager)
);
pollModal.exceptionally(e -> {
- e.printStackTrace();
+ logger.error("Error while creating request modal: ", e);
return null;
});
}
catch (Exception e) {
- e.printStackTrace();
+ logger.error("Error while creating request modal: ", e);
}
}
else {
@@ -244,72 +263,89 @@ else if (commandName.equalsIgnoreCase(parseCommands.getCommandName("request")))
}
}
- slashCommandInteraction.createImmediateResponder()
- .addEmbed(VotingEmbed.getUserResponse(author, ChannelUtils.getRequestsChannel(server).getMentionTag()))
- .setFlags(MessageFlag.EPHEMERAL)
- .respond();
+ // Resolve requests channel safely
+ ServerTextChannel requestsChannel = ChannelUtils.getRequestsChannel(server);
+ if (requestsChannel == null) {
+ slashCommandInteraction.createImmediateResponder()
+ .setFlags(MessageFlag.EPHEMERAL)
+ .addEmbed(ErrorEmbed.getCustomError(languageManager, Main.getErrorCode("requestsChannelMissing"), "A requests channel is not configured for this server. An admin can set one in /config server > Requests Channel."))
+ .respond();
+ } else {
+ slashCommandInteraction.createImmediateResponder()
+ .addEmbed(VotingEmbed.getUserResponse(languageManager, author, requestsChannel.getMentionTag()))
+ .setFlags(MessageFlag.EPHEMERAL)
+ .respond();
- ChannelUtils.getRequestsChannel(server).sendMessage(VotingEmbed.getPoll("REQUEST", question, allowMultipleChoices, choices, server, author, numChoices)).thenAccept(message -> {
- message.addReaction("\uD83D\uDC4D");
- message.addReaction("\uD83D\uDC4E");
- message.addReaction(":vote:706373563564949566");
- });
+ final int finalNumChoicesReq = numChoices;
+ requestsChannel.sendMessage(VotingEmbed.getPoll(languageManager, "REQUEST", question, allowMultipleChoices, choices, server, author, finalNumChoicesReq)).thenAccept(message -> {
+ if (finalNumChoicesReq > 0) {
+ String[] numberEmojis = new String[]{"1️⃣","2️⃣","3️⃣","4️⃣","5️⃣","6️⃣","7️⃣","8️⃣","9️⃣","🔟"};
+ for (int i = 0; i < finalNumChoicesReq; i++) {
+ message.addReaction(numberEmojis[i]);
+ }
+ } else {
+ message.addReaction("\uD83D\uDC4D");
+ message.addReaction("\uD83D\uDC4E");
+ message.addReaction(":vote:706373563564949566");
+ }
+ });
+ }
}
}
- else if (commandName.equalsIgnoreCase("server")) {
+ else if (commandName.equalsIgnoreCase(commands.getServer().getName())) {
EmbedBuilder embed = null;
String guildID = slashCommandInteraction.getArgumentStringValueByName("guildID").orElse(null);
if (server == null && guildID == null) {
- embed = ErrorEmbed.getCustomError(Main.getErrorCode("no-guild-present"), "A guild must be specified. Either run this command in a server or specify a guild ID.");
+ embed = ErrorEmbed.getCustomError(languageManager, Main.getErrorCode("no-guild-present"), "A guild must be specified. Either run this command in a server or specify a guild ID.");
}
if (guildID != null) {
Server fromGuildID = event.getApi().getServerById(guildID).orElse(null);
if (fromGuildID != null) {
- embed = ServerInfoEmbed.getServerInfo(fromGuildID, user.getIdAsString());
+ embed = ServerInfoEmbed.getServerInfo(languageManager, fromGuildID, user.getIdAsString());
}
else {
- embed = ErrorEmbed.getCustomError(Main.getErrorCode("guildID-invalid"), "Either that guild ID is invalid or I'm not a member of the server.");
+ embed = ErrorEmbed.getCustomError(languageManager, Main.getErrorCode("guildID-invalid"), "Either that guild ID is invalid or I'm not a member of the server.");
}
}
else if (server != null) {
- embed = ServerInfoEmbed.getServerInfo(server, user.getIdAsString());
+ embed = ServerInfoEmbed.getServerInfo(languageManager, server, user.getIdAsString());
}
slashCommandInteraction.createImmediateResponder()
.addEmbed(embed)
.respond();
}
- else if (commandName.equalsIgnoreCase("user")) {
+ else if (commandName.equalsIgnoreCase(commands.getUser().getName())) {
slashCommandInteraction.createImmediateResponder()
- .addEmbed(UserInfoEmbed.getUser(user, author, server))
+ .addEmbed(UserInfoEmbed.getUser(languageManager, 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) {
slashCommandInteraction.createImmediateResponder().addEmbed(
- ErrorEmbed.getError(Main.getErrorCode("RoleMissing"))
+ ErrorEmbed.getError(languageManager, Main.getErrorCode("RoleMissing"))
).respond();
return;
}
if (!author.canManageRole(role)) {
slashCommandInteraction.createImmediateResponder()
- .addEmbed(ErrorEmbed.getLackingPermissions("Sorry! You don't have the permission to run this " +
+ .addEmbed(ErrorEmbed.getLackingPermissions(languageManager, "Sorry! You don't have the permission to run this " +
"command. You must be able to manage the role " + role.getMentionTag() + "."))
.respond();
return;
}
slashCommandInteraction.createImmediateResponder().addEmbed(
- SantaEmbed.getConfirmationEmbed(author)
+ SantaEmbed.getConfirmationEmbed(languageManager, author)
).respond();
- SantaEmbed.getHostMessage(role, author, "", "").send(author);
+ SantaEmbed.getHostMessage(languageManager, role, author, "", "").send(author);
}
}
}
diff --git a/src/main/java/com/sidpatchy/clairebot/Listener/Voting/ModerateReactions.java b/src/main/java/com/sidpatchy/clairebot/Listener/Voting/ModerateReactions.java
index 163c6a0..8e0a816 100644
--- a/src/main/java/com/sidpatchy/clairebot/Listener/Voting/ModerateReactions.java
+++ b/src/main/java/com/sidpatchy/clairebot/Listener/Voting/ModerateReactions.java
@@ -32,25 +32,27 @@ public void onReactionAdd(ReactionAddEvent event) {
assert footer != null;
String footerText = footer.getText().orElse(null);
- if (footerText != null && footerText.contains("Poll ID")) {
- HashMap pollID = VotingUtils.parsePollID(footerText.replace("Poll ID: ", ""));
- boolean allowMultipleChoices = pollID.get("allowMultipleChoices").equalsIgnoreCase("1");
-
- if (!allowMultipleChoices) {
- String emote = event.getEmoji().asUnicodeEmoji().orElse(null);
- User author = event.getUser().orElse(null);
-
- assert author != null;
- if (author.isYourself()) {
- return;
- }
-
- List reacts = message.getReactions();
+ if (footerText != null) {
+ String encoded = VotingUtils.extractPollIdFromFooter(footerText);
+ if (!encoded.isEmpty()) {
+ HashMap pollID = VotingUtils.parsePollID(encoded);
+ boolean allowMultipleChoices = pollID.get("allowMultipleChoices").equalsIgnoreCase("1");
+
+ if (!allowMultipleChoices) {
+ String emote = event.getEmoji().asUnicodeEmoji().orElse(null);
+ User author = event.getUser().orElse(null);
+
+ assert author != null;
+ if (author.isYourself()) {
+ return;
+ }
- for (Reaction reaction : reacts) {
+ List reacts = message.getReactions();
- if (emote != null && !emote.equalsIgnoreCase(reaction.getEmoji().asUnicodeEmoji().orElse(null))) {
- reaction.removeUser(author);
+ for (Reaction reaction : reacts) {
+ if (emote != null && !emote.equalsIgnoreCase(reaction.getEmoji().asUnicodeEmoji().orElse(null))) {
+ reaction.removeUser(author);
+ }
}
}
}
diff --git a/src/main/java/com/sidpatchy/clairebot/Main.java b/src/main/java/com/sidpatchy/clairebot/Main.java
index d5dfad9..b5c06db 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,30 +15,39 @@
import java.awt.*;
import java.io.IOException;
-import java.util.Arrays;
+import java.io.InputStream;
+import java.io.File;
+import java.net.URL;
+import java.net.JarURLConnection;
+import java.util.Enumeration;
+import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
-import java.util.stream.Collectors;
+import java.util.Properties;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
/**
* ClaireBot - Simply the best.
- * Copyright (C) 2021 Sidpatchy
- *
+ * Copyright (C) 2021 Sidpatchy
+ *
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
- *
+ *
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
- *
+ *
* You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
+ * along with this program. If not, see <...>.
*
* @since April 2020
- * @version 3.3.2
+ * @version 3.4.0-SNAPSHOT
* @author Sidpatchy
*/
public class Main {
@@ -58,19 +67,19 @@ public class Main {
private static Map guildDefaults;
// Various parameters extracted from config files
- private static String botName;
private static String color;
private static String errorColor;
- private static List