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: + + +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 +![](https://github.com/Sidpatchy/SecretClaire/blob/master/img/screenshot_1.png?raw=true) + +![](https://github.com/Sidpatchy/SecretClaire/blob/master/img/screenshot_6.png?raw=true) + +![](https://github.com/Sidpatchy/SecretClaire/blob/master/img/screenshot_7.png?raw=true) + +## Example gift-giver messages +![](https://github.com/Sidpatchy/SecretClaire/blob/master/img/screenshot_2.png?raw=true) + +![](https://github.com/Sidpatchy/SecretClaire/blob/master/img/screenshot_3.png?raw=true) + + + + \ 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 errorGifs; - private static List zerfas; + private static List errorGifs; + private static Locale fallbackLocale; + private static List zerfas; private static String zerfasEmojiServerID; private static String zerfasEmojiID; - private static List eightBall; - private static List eightBallRigged; - private static List claireBotOnTopResponses; - private static List onTopTriggers; - private static List plsBanResponses; - private static List plsBanTriggers; + private static List eightBall; + private static List eightBallRigged; + private static List claireBotOnTopResponses; + private static List onTopTriggers; + private static List plsBanResponses; + private static List plsBanTriggers; // Commands private static final Logger logger = LogManager.getLogger(Main.class); @@ -78,22 +87,34 @@ public class Main { // Related to configuration files private static final String configFile = "config.yml"; private static final String commandsFile = "commands.yml"; + private static final String translationsPath = "config/translations/"; private static RobinConfiguration config; - private static ParseCommands commands; - - public static List commandList = Arrays.asList("8ball", "avatar", "help", "info", "leaderboard", "level", "poll", "quote", "request", "server", "user", "config", "santa"); - - public static void main(String[] args) throws InvalidConfigurationException { + private static Commands commands; + private static final Properties buildProperties = new Properties() {{ + try (InputStream input = Main.class.getClassLoader().getResourceAsStream("build.properties")) { + if (input != null) load(input); + else System.err.println("build.properties missing!"); + } catch (IOException e) { throw new RuntimeException("Failed to load build.properties", e); } + }}; + private static String buildVersion; + private static String buildDate; + private static String github; + private static String supportServer; + private static String website; + private static String documentationWebsite; + private static String inviteLink; + + static void main(String[] args) throws InvalidConfigurationException { logger.info("ClaireBot loading..."); // Make sure required resources are loaded ResourceLoader loader = new ResourceLoader(); loader.saveResource(configFile, false); loader.saveResource(commandsFile, false); + loadAllBundledTranslations(loader, true); // TODO make this false once language files are stable // Init config handlers config = new RobinConfiguration("config/" + configFile); - commands = new ParseCommands("config/" + commandsFile); config.load(); @@ -104,6 +125,7 @@ public static void main(String[] args) throws InvalidConfigurationException { String video_url = config.getString("video_url"); extractParametersFromConfig(true); + loadCommandDefs(); verifyDatabaseConnectivity(); @@ -113,13 +135,13 @@ public static void main(String[] args) throws InvalidConfigurationException { System.exit(2); } else { - logger.info("Successfully connected to Discord on shard " + current_shard + " with a total shard count of " + total_shards); + logger.info("Successfully connected to Discord on shard {} with a total shard count of {}", current_shard, total_shards); } Clockwork.initClockwork(); // Set the bot's activity - api.updateActivity("ClaireBot v3.3.2", video_url); + api.updateActivity("ClaireBot " + buildVersion, video_url); // Register slash commands registerSlashCommands(); @@ -141,7 +163,7 @@ public static void main(String[] args) throws InvalidConfigurationException { // Connect to Discord and create an API object private static DiscordApi DiscordLogin(String token, Integer current_shard, Integer total_shards) { - if (token == null || token.equals("")) { + if (token == null || token.isEmpty()) { logger.fatal("Token can't be null or empty. Check your config file!"); System.exit(1); } @@ -161,20 +183,18 @@ else if (current_shard == null || total_shards == null) { .login().join(); } catch (Exception e) { - e.printStackTrace(); - logger.fatal(e.toString()); - logger.fatal("Unable to log in to Discord. Aborting startup!"); + logger.fatal("Unable to log in to Discord. Aborting startup!", e); } return null; } // Extract parameters from the config.yml file, update the config if applicable. + // todo stop using Robin for this. Switch to standard Java classses. @SuppressWarnings("unchecked") public static void extractParametersFromConfig(boolean updateOutdatedConfigs) { logger.info("Loading configuration files..."); try { - botName = config.getString("botName"); apiPath = config.getString("apiPath"); apiUser = config.getString("apiUser"); apiPassword = config.getString("apiPassword"); @@ -182,22 +202,109 @@ public static void extractParametersFromConfig(boolean updateOutdatedConfigs) { guildDefaults = ((Map) config.getObj("guildDefaults")); color = config.getString("color"); errorColor = config.getString("errorColor"); - errorGifs = config.getList("error_gifs"); - zerfas = config.getList("zerfas"); + errorGifs = config.getList("error_gifs", String.class); + fallbackLocale = Locale.forLanguageTag(config.getString("fallback_language")); + zerfas = config.getList("zerfas", String.class); zerfasEmojiServerID = String.valueOf(config.getLong("zerfas_emoji_server_id")); zerfasEmojiID = String.valueOf(config.getLong("zerfas_emoji_id")); - eightBall = config.getList("8bResponses"); - eightBallRigged = config.getList("8bRiggedResponses"); - claireBotOnTopResponses = config.getList("ClaireBotOnTopResponses"); - onTopTriggers = config.getList("OnTopTriggers"); - plsBanResponses = config.getList("PlsBanResponses"); - plsBanTriggers = config.getList("PlsBanTriggers"); + eightBall = config.getList("8bResponses", String.class); + eightBallRigged = config.getList("8bRiggedResponses", String.class); + claireBotOnTopResponses = config.getList("ClaireBotOnTopResponses", String.class); + onTopTriggers = config.getList("OnTopTriggers", String.class); + plsBanResponses = config.getList("PlsBanResponses", String.class); + plsBanTriggers = config.getList("PlsBanTriggers", String.class); + buildVersion = buildProperties.getProperty("clairebot.version"); + buildDate = buildProperties.getProperty("clairebot.buildDate"); + github = buildProperties.getProperty("clairebot.github"); + supportServer = buildProperties.getProperty("clairebot.supportServer"); + website = buildProperties.getProperty("clairebot.website"); + documentationWebsite = buildProperties.getProperty("clairebot.documentationWebsite"); + inviteLink = config.getString("clairebot.inviteLink"); } catch (Exception e) { - e.printStackTrace(); - logger.error("There was an error while extracting parameters from the config. This isn't fatal but there's a good chance things will be very broken."); + logger.error("There was an error while extracting parameters from the config. This isn't fatal but there's a good chance things will be very broken.", e); + } + + } + + public static void loadCommandDefs() { + try { + commands = CommandFactory.loadConfig("config/" + commandsFile, Commands.class); + logger.warn(commands.getInfo().getName()); + logger.warn(commands.getInfo().getHelp()); + } catch (IOException e) { + logger.fatal("There was a fatal error while registering slash commands", e); + throw new RuntimeException(e); + } + } + + private static void loadAllBundledTranslations(ResourceLoader loader, boolean replace) { + String resourceDir = "translations"; + Set resourcePaths = new HashSet<>(); + try { + ClassLoader cl = Main.class.getClassLoader(); + Enumeration urls = cl.getResources(resourceDir); + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + String protocol = url.getProtocol(); + if ("file".equals(protocol)) { + try { + File dir = new File(url.toURI()); + File[] files = dir.listFiles((d, name) -> name.endsWith(".yml")); + if (files != null) { + for (File f : files) { + resourcePaths.add(resourceDir + "/" + f.getName()); + } + } + } catch (Exception e) { + logger.warn("Failed to enumerate file resources for translations: {}", e.getMessage()); + } + } else if ("jar".equals(protocol)) { + try { + JarURLConnection conn = (JarURLConnection) url.openConnection(); + try (JarFile jarFile = conn.getJarFile()) { + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String name = entry.getName(); + if (!entry.isDirectory() && name.startsWith(resourceDir + "/") && name.endsWith(".yml")) { + resourcePaths.add(name); + } + } + } + } catch (Exception e) { + logger.warn("Failed to enumerate JAR resources for translations: {}", e.getMessage()); + } + } else { + logger.debug("Unsupported classpath URL protocol for translations: {}", protocol); + } + } + } catch (IOException e) { + logger.warn("Unable to list translation resources: {}", e.getMessage()); } + if (resourcePaths.isEmpty()) { + // Fallback: attempt known default file to ensure at least the template exists on first run + logger.warn("No translation resources discovered via classpath enumeration. Falling back to default copy of en-US and TEMPLATE if present."); + String[] fallbacks = new String[] {"translations/lang_en-US.yml", "translations/lang_TEMPLATE.yml"}; + for (String path : fallbacks) { + try { + loader.saveResource(path, replace); + } catch (Exception e) { + logger.debug("Fallback translation '{}' not present in resources: {}", path, e.getMessage()); + } + } + return; + } + + for (String path : resourcePaths) { + try { + loader.saveResource(path, replace); + logger.debug("Ensured translation resource available: {}", path); + } catch (Exception e) { + logger.warn("Failed to save translation resource '{}': {}", path, e.getMessage()); + } + } } // Handle the registry of slash commands and any errors associated. @@ -207,17 +314,12 @@ public static void registerSlashCommands() { logger.info("Slash commands registered successfully!"); } catch (NullPointerException e) { - e.printStackTrace(); - logger.fatal("There was an error while registering slash commands. There's a pretty good chance it's related to an uncaught issue with the commands.yml file, trying to read all commands and printing out results."); - for (String s : Main.commandList) { - logger.fatal(commands.getCommandName(s)); - } - logger.fatal("If the above list looks incomplete or generates another error, check your commands.yml file!"); + logger.fatal("There was an error while registering slash commands. There's a pretty good chance it's related to an uncaught issue with the commands.yml file.", e); + logger.fatal("Check your commands.yml file!"); System.exit(4); } catch (Exception e) { - e.printStackTrace(); - logger.fatal("There was a fatal error while registering slash commands."); + logger.fatal("There was a fatal error while registering slash commands.", e); System.exit(5); } } @@ -229,9 +331,7 @@ public static void verifyDatabaseConnectivity() { APIUser api = new APIUser("12345"); api.getALLUsers(); } catch (IOException e) { - e.printStackTrace(); - logger.error("ClaireBot was unable to access the APIUser table. See previous errors for more details."); - logger.error("This isn't strictly fatal, but things WILL be very broken."); + logger.error("ClaireBot was unable to access the APIUser table.", e); } // test Guild connectivity @@ -239,9 +339,7 @@ public static void verifyDatabaseConnectivity() { Guild api = new Guild("12345"); api.getALLGuilds(); } catch (IOException e) { - e.printStackTrace(); - logger.error("ClaireBot was unable to access the Guild table. See previous errors for more details."); - logger.error("This isn't strictly fatal, but things WILL be very broken."); + logger.error("ClaireBot was unable to access the Guild table.", e); } } @@ -272,51 +370,35 @@ public static Color getColor(String userID) { public static Color getErrorColor() { return Color.decode(errorColor); } public static List getErrorGifs() { - return errorGifs.stream() - .map(Object::toString) - .collect(Collectors.toList()); + return errorGifs; } public static List getEightBall() { - return eightBall.stream() - .map(Object::toString) - .collect(Collectors.toList()); + return eightBall; } public static List getEightBallRigged() { - return eightBallRigged.stream() - .map(Object::toString) - .collect(Collectors.toList()); + return eightBallRigged; } public static List getOnTopTriggers() { - return onTopTriggers.stream() - .map(Object::toString) - .collect(Collectors.toList()); + return onTopTriggers; } public static List getClaireBotOnTopResponses() { - return claireBotOnTopResponses.stream() - .map(Object::toString) - .collect(Collectors.toList()); + return claireBotOnTopResponses; } public static List getPlsBanTriggers() { - return plsBanTriggers.stream() - .map(Object::toString) - .collect(Collectors.toList()); + return plsBanTriggers; } public static List getPlsBanResponses() { - return plsBanResponses.stream() - .map(Object::toString) - .collect(Collectors.toList()); + return plsBanResponses; } public static List getZerfas() { - return zerfas.stream() - .map(Object::toString) - .collect(Collectors.toList()); + return zerfas; } public static String getZerfasEmojiServerID() { @@ -331,6 +413,10 @@ public static String getZerfasEmojiID() { public static String getCommandsFile() { return "config/" + commandsFile; } + public static Commands getCommands() { + return commands; + } + public static Logger getLogger() { return logger; } public static String getErrorCode(String descriptor) { @@ -339,7 +425,41 @@ public static String getErrorCode(String descriptor) { public static DiscordApi getApi() { return api; } - public static List getVoteEmoji() { return Arrays.asList("1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟", "\uD83D\uDC4D", "\uD83D\uDC4E"); } - public static long getStartMillis() { return startMillis; } -} + + public static String getTranslationsPath() { + return translationsPath; + } + + public static String getBuildVersion() { + return buildVersion; + } + + public static String getBuildDate() { + return buildDate; + } + + public static String getGithub() { + return github; + } + + public static String getSupportServer() { + return supportServer; + } + + public static String getWebsite() { + return website; + } + + public static String getDocumentationWebsite() { + return documentationWebsite; + } + + public static String getInviteLink() { + return inviteLink; + } + + public static Locale getFallbackLocale() { + return fallbackLocale; + } +} \ No newline at end of file diff --git a/src/main/java/com/sidpatchy/clairebot/MessageComponents/Regular/SantaModal.java b/src/main/java/com/sidpatchy/clairebot/MessageComponents/Regular/SantaModal.java index 8ca1ded..51cd253 100644 --- a/src/main/java/com/sidpatchy/clairebot/MessageComponents/Regular/SantaModal.java +++ b/src/main/java/com/sidpatchy/clairebot/MessageComponents/Regular/SantaModal.java @@ -1,16 +1,21 @@ package com.sidpatchy.clairebot.MessageComponents.Regular; +import com.sidpatchy.clairebot.Lang.LanguageManager; import org.javacord.api.entity.message.component.ActionRow; import org.javacord.api.entity.message.component.TextInput; import org.javacord.api.entity.message.component.TextInputStyle; public class SantaModal { - public static ActionRow getRulesRow() { - return ActionRow.of(TextInput.create(TextInputStyle.PARAGRAPH, "rules-row", "Rules...")); + public static ActionRow getRulesRow(LanguageManager languageManager) { + String rules = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.SantaModal.rules-row"); + + return ActionRow.of(TextInput.create(TextInputStyle.PARAGRAPH, "rules-row", rules)); } - public static ActionRow getThemeRow() { + public static ActionRow getThemeRow(LanguageManager languageManager) { + String theme = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.SantaModal.theme-row"); + return ActionRow.of(TextInput.create(TextInputStyle.PARAGRAPH, "theme-row", "Theme...")); } diff --git a/src/main/java/com/sidpatchy/clairebot/MessageComponents/Regular/ServerPreferencesComponents.java b/src/main/java/com/sidpatchy/clairebot/MessageComponents/Regular/ServerPreferencesComponents.java index fb6aa34..e1753a2 100644 --- a/src/main/java/com/sidpatchy/clairebot/MessageComponents/Regular/ServerPreferencesComponents.java +++ b/src/main/java/com/sidpatchy/clairebot/MessageComponents/Regular/ServerPreferencesComponents.java @@ -1,5 +1,6 @@ package com.sidpatchy.clairebot.MessageComponents.Regular; +import com.sidpatchy.clairebot.Lang.LanguageManager; import org.javacord.api.entity.channel.ServerTextChannel; import org.javacord.api.entity.message.component.ActionRow; import org.javacord.api.entity.message.component.ActionRowBuilder; @@ -13,43 +14,94 @@ public class ServerPreferencesComponents { - public static ActionRow getMainMenu() { + public static ActionRow getMainMenu(LanguageManager languageManager) { + String placeholder = languageManager.getLocalizedString("ClaireLang.Generic.ClickToDisplaySettings"); + + String requestsLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.ServerPreferences.RequestsChannel"); + String requestsDesc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.ServerPreferences.RequestsChannelDescription"); + + String modLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.ServerPreferences.ModeratorMessagesChannel"); + String modDesc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.ServerPreferences.ModeratorMessagesChannelDescription"); + + String enforceLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.ServerPreferences.EnforceServerLanguage"); + String enforceDesc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.ServerPreferences.EnforceServerLanguageDescription"); + + String serverLangLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.ServerPreferences.ServerLanguage"); + String serverLangDesc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.ServerPreferences.ServerLanguageDescription"); + return new ActionRowBuilder() .addComponents( - SelectMenu.create("settings", "Click to display settings", 1, 1, - Arrays.asList(SelectMenuOption.create("Requests Channel", "Requests Channel", "Choose where ClaireBot should post requests."), - SelectMenuOption.create("Moderator Messages Channel", "Moderator Messages Channel", "Choose where ClaireBot should mod messages."), - SelectMenuOption.create("Enforce Server Language", "Enforce Server Language", "Force ClaireBot to use the same language as the server regardless of user preference."))) + SelectMenu.create("server-settings", placeholder, 1, 1, + Arrays.asList( + // Keep values stable for interaction handlers + SelectMenuOption.create(requestsLabel, "Requests Channel", requestsDesc), + SelectMenuOption.create(modLabel, "Moderator Messages Channel", modDesc), + SelectMenuOption.create(enforceLabel, "Enforce Server Language", enforceDesc), + SelectMenuOption.create(serverLangLabel, "Server Language", serverLangDesc) + )) ).build(); } - public static ActionRow getRequestsChannelMenu(Server server) { - return getChannelListActionRow(server, "requestsChannel"); + public static ActionRow getRequestsChannelMenu(LanguageManager languageManager, Server server) { + return getChannelListActionRow(languageManager, server, "requestsChannel"); } - public static ActionRow getModeratorChannelMenu(Server server) { - return getChannelListActionRow(server, "moderatorChannel"); + public static ActionRow getModeratorChannelMenu(LanguageManager languageManager, Server server) { + return getChannelListActionRow(languageManager, server, "moderatorChannel"); } - public static ActionRow getEnforceServerLanguageMenu() { + public static ActionRow getEnforceServerLanguageMenu(LanguageManager languageManager) { + String placeholder = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.ServerPreferences.EnforceServerLanguagePlaceholder"); + String trueText = languageManager.getLocalizedString("ClaireLang.Generic.True"); + String falseText = languageManager.getLocalizedString("ClaireLang.Generic.False"); + return new ActionRowBuilder() .addComponents( - SelectMenu.create("enforceServerLanguage", "Click to select an option", 1, 1, + SelectMenu.create("enforceServerLanguage", placeholder, 1, 1, Arrays.asList( - SelectMenuOption.create("True", "true"), - SelectMenuOption.create("False", "false") + // Localized labels with stable boolean values + SelectMenuOption.create(trueText, "true"), + SelectMenuOption.create(falseText, "false") )) ).build(); } + public static ActionRow getServerLanguageMenu(LanguageManager languageManager) { + // Server language placeholder + String placeholder = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.ServerPreferences.ServerLanguagePlaceholder"); + + java.io.File dir = new java.io.File(com.sidpatchy.clairebot.Main.getTranslationsPath()); + List options = new ArrayList<>(); + if (dir.exists() && dir.isDirectory()) { + java.io.File[] files = dir.listFiles((d, name) -> name.startsWith("lang_") && name.endsWith(".yml")); + if (files != null) { + Arrays.sort(files, java.util.Comparator.comparing(java.io.File::getName)); + for (java.io.File f : files) { + String name = f.getName(); + String tag = name.substring("lang_".length(), name.length() - ".yml".length()); + String label = tag; + options.add(SelectMenuOption.create(label, tag)); + } + } + } + if (options.isEmpty()) { + String tag = com.sidpatchy.clairebot.Main.getFallbackLocale().toLanguageTag(); + options.add(SelectMenuOption.create(tag, tag)); + } + return new ActionRowBuilder() + .addComponents(SelectMenu.create("server-language", placeholder, 1, 1, options)) + .build(); + } + /** * Create a select menu with a list of the server's channels. * * @param server The server the list should be generated for * @param customId The ID of the SelectMenu + * @param languageManager Localization provider * @return ActionRow with SelectMenu */ - private static ActionRow getChannelListActionRow(Server server, String customId) { + private static ActionRow getChannelListActionRow(LanguageManager languageManager, Server server, String customId) { List channels = server.getTextChannels(); List options = new ArrayList<>(); int count = 0; @@ -64,9 +116,11 @@ private static ActionRow getChannelListActionRow(Server server, String customId) } } + String placeholder = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.ServerPreferences.SelectAChannelPlaceholder"); + return new ActionRowBuilder() .addComponents( - SelectMenu.create(customId, "Click to select a channel", 1, 1, options) + SelectMenu.create(customId, placeholder, 1, 1, options) ).build(); } diff --git a/src/main/java/com/sidpatchy/clairebot/MessageComponents/Regular/UserPreferencesComponents.java b/src/main/java/com/sidpatchy/clairebot/MessageComponents/Regular/UserPreferencesComponents.java index a6a6464..dbbff8a 100644 --- a/src/main/java/com/sidpatchy/clairebot/MessageComponents/Regular/UserPreferencesComponents.java +++ b/src/main/java/com/sidpatchy/clairebot/MessageComponents/Regular/UserPreferencesComponents.java @@ -1,31 +1,57 @@ package com.sidpatchy.clairebot.MessageComponents.Regular; +import com.sidpatchy.clairebot.Lang.LanguageManager; +import com.sidpatchy.clairebot.Main; import org.javacord.api.entity.message.component.*; +import java.io.File; import java.util.*; public class UserPreferencesComponents { - public static ActionRow getMainMenu() { + public static ActionRow getMainMenu(LanguageManager languageManager) { + String placeholder = languageManager.getLocalizedString("ClaireLang.Generic.ClickToDisplaySettings"); + + String accentLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.UserPreferences.AccentColour"); + String accentDesc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.UserPreferences.AccentColourDescription"); + + String languageLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.UserPreferences.Language"); + String languageDesc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.UserPreferences.LanguageDescription"); + return new ActionRowBuilder() .addComponents( - SelectMenu.create("settings", "Click to display settings", 1, 1, - Arrays.asList(SelectMenuOption.create("Accent Colour", "Accent Colour Editor", "For those who hate the colour blue"), - SelectMenuOption.create("Language", "Language Editor", "If you're offended by english"))) + SelectMenu.create("settings", placeholder, 1, 1, + Arrays.asList( + // Keep values stable for interaction handlers + SelectMenuOption.create(accentLabel, "Accent Colour Editor", accentDesc), + SelectMenuOption.create(languageLabel, "Language Editor", languageDesc) + )) ).build(); } - public static ActionRow getAccentColourMenu() { + public static ActionRow getAccentColourMenu(LanguageManager languageManager) { + String placeholder = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.UserPreferences.AccentColourPlaceholder"); + + String commonLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.UserPreferences.SelectCommonColours"); + String commonDesc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.UserPreferences.SelectCommonColoursDescription"); + + String hexLabel = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.UserPreferences.HexadecimalEntry"); + String hexDesc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.UserPreferences.HexadecimalEntryDescription"); + return new ActionRowBuilder() .addComponents( - SelectMenu.create("accent-color", "Click to choose option", 1, 1, - Arrays.asList(SelectMenuOption.create("Select Common Colours", "Select Common Colours", "Select from a list of common colours"), - SelectMenuOption.create("Hexadecimal Entry", "Hexadecimal Entry", "Enter any hexadecimal colour"))) + SelectMenu.create("accent-color", placeholder, 1, 1, + Arrays.asList( + // Keep values stable for interaction handlers + SelectMenuOption.create(commonLabel, "Select Common Colours", commonDesc), + SelectMenuOption.create(hexLabel, "Hexadecimal Entry", hexDesc) + )) ).build(); } - public static ActionRow getAccentColourList() { - HashMap colours = new HashMap<>() {{ + public static ActionRow getAccentColourList(LanguageManager languageManager) { + // English canonical names -> hex codes + Map colours = new LinkedHashMap<>() {{ put("ClaireBot Blue", "3498db"); put("Red", "f44336"); put("Pink", "e81e63"); @@ -49,20 +75,93 @@ public static ActionRow getAccentColourList() { put("Black", "0a0a0a"); }}; + // Map canonical English names to localization keys + Map colorLocalizationKeys = new HashMap<>() {{ + put("ClaireBot Blue", "ClaireLang.Colors.ClaireBotBlue"); + put("Red", "ClaireLang.Colors.Red"); + put("Pink", "ClaireLang.Colors.Pink"); + put("Purple", "ClaireLang.Colors.Purple"); + put("Deep Purple", "ClaireLang.Colors.DeepPurple"); + put("Indigo", "ClaireLang.Colors.Indigo"); + put("Blue", "ClaireLang.Colors.Blue"); + put("Light Blue", "ClaireLang.Colors.LightBlue"); + put("Cyan", "ClaireLang.Colors.Cyan"); + put("Teal", "ClaireLang.Colors.Teal"); + put("Green", "ClaireLang.Colors.Green"); + put("Olive", "ClaireLang.Colors.Olive"); + put("Light Green", "ClaireLang.Colors.LightGreen"); + put("Lime", "ClaireLang.Colors.Lime"); + put("Yellow", "ClaireLang.Colors.Yellow"); + put("Amber", "ClaireLang.Colors.Amber"); + put("NRAX Orange", "ClaireLang.Colors.NRAXOrange"); + put("Deep Orange", "ClaireLang.Colors.DeepOrange"); + put("White", "ClaireLang.Colors.White"); + put("Grey", "ClaireLang.Colors.Grey"); + put("Black", "ClaireLang.Colors.Black"); + }}; + List selectMenuOptionList = new ArrayList<>(); for (Map.Entry color : colours.entrySet()) { - selectMenuOptionList.add(SelectMenuOption.create(color.getKey(), color.getKey(), "#" + color.getValue())); + String englishName = color.getKey(); + String hex = color.getValue(); + + // Localized display label; fallback to the English name if key missing + String localizationKey = colorLocalizationKeys.get(englishName); + String localizedLabel = localizationKey != null + ? languageManager.getLocalizedString(localizationKey) + : englishName; + + // Keep the value stable as the English canonical name + selectMenuOptionList.add(SelectMenuOption.create(localizedLabel, englishName, "#" + hex)); } + String placeholder = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.UserPreferences.AccentColourPlaceholder"); + return new ActionRowBuilder() .addComponents( - SelectMenu.create("accent-color", "Click to choose option", 1, 1, selectMenuOptionList)).build(); + SelectMenu.create("accent-color", placeholder, 1, 1, selectMenuOptionList) + ).build(); + } + + public static ActionRow getAccentColourHexEntry(LanguageManager languageManager) { + String label = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.UserPreferences.HexEntryPlaceholder"); + return new ActionRowBuilder() + .addComponents(TextInput.create(TextInputStyle.SHORT, "hex-entry-field", label)) + .build(); } - public static ActionRow getAccentColourHexEntry() { + public static ActionRow getLanguageMenu(LanguageManager languageManager) { + String placeholder = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.UserPreferences.LanguagePlaceholder"); + + // Discover available language files under config/translations + File dir = new File(Main.getTranslationsPath()); + List options = new ArrayList<>(); + if (dir.exists() && dir.isDirectory()) { + File[] files = dir.listFiles((d, name) -> name.startsWith("lang_") && name.endsWith(".yml")); + if (files != null) { + // Keep deterministic order + Arrays.sort(files, Comparator.comparing(File::getName)); + for (File f : files) { + String name = f.getName(); + // Example: lang_en-US.yml -> en-US + String tag = name.substring("lang_".length(), name.length() - ".yml".length()); + // Try to read friendly display name from file header or use tag as fallback + String label = tag; // Minimal; can be localized in future via file metadata + // Value must be stable and languageManager-independent; use tag + options.add(SelectMenuOption.create(label, tag)); + } + } + } + + if (options.isEmpty()) { + // Fallback to at least allow selecting fallback locale + String tag = Main.getFallbackLocale().toLanguageTag(); + options.add(SelectMenuOption.create(tag, tag)); + } + return new ActionRowBuilder() - .addComponents(TextInput.create(TextInputStyle.SHORT, "hex-entry-field", "Enter a hex colour ex. #ff8800")) + .addComponents(SelectMenu.create("user-language", placeholder, 1, 1, options)) .build(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/sidpatchy/clairebot/MessageComponents/Regular/VotingComponents.java b/src/main/java/com/sidpatchy/clairebot/MessageComponents/Regular/VotingComponents.java index 70e2148..3c5f8a3 100644 --- a/src/main/java/com/sidpatchy/clairebot/MessageComponents/Regular/VotingComponents.java +++ b/src/main/java/com/sidpatchy/clairebot/MessageComponents/Regular/VotingComponents.java @@ -1,5 +1,6 @@ package com.sidpatchy.clairebot.MessageComponents.Regular; +import com.sidpatchy.clairebot.Lang.LanguageManager; import org.javacord.api.entity.message.component.*; import java.util.ArrayList; @@ -9,48 +10,77 @@ public class VotingComponents { // Question row - public static ActionRow getQuestionRow() { - return ActionRow.of(TextInput.create(TextInputStyle.SHORT, "question-modal", "Question")); + public static ActionRow getQuestionRow(LanguageManager languageManager) { + String label = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.Voting.Question"); + return ActionRow.of(TextInput.create(TextInputStyle.SHORT, "question-modal", label)); } // Details row - public static ActionRow getDetailsRow() { - return ActionRow.of(TextInput.create(TextInputStyle.PARAGRAPH, "details-modal", "Details")); + public static ActionRow getDetailsRow(LanguageManager languageManager) { + String label = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.Voting.Details"); + return ActionRow.of(TextInput.create(TextInputStyle.PARAGRAPH, "details-modal", label)); } - // Second Menu // Multiple choices row - public static ActionRow getMultipleChoicesRow(String commandName) { - return ActionRow.of(SelectMenu.create("multiple-choice", "Click to choose option", 1, 1, - Arrays.asList(SelectMenuOption.create("Yes", "Opens menu to select more options"), - SelectMenuOption.create("No", "Submits " + commandName + "after clicking submit")))); - } + public static ActionRow getMultipleChoicesRow(LanguageManager languageManager, String commandName) { + String placeholder = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.Voting.MultipleChoicePlaceholder"); + String yesLabel = languageManager.getLocalizedString("ClaireLang.Generic.Yes"); + String yesDesc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.Voting.MultipleChoiceYesDescription"); + String noLabel = languageManager.getLocalizedString("ClaireLang.Generic.No"); + String noDescTemplate = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.Voting.MultipleChoiceNoDescription"); + String noDesc = noDescTemplate.replace("{cb.commandname}", commandName); + + return ActionRow.of( + SelectMenu.create("multiple-choice", placeholder, 1, 1, + Arrays.asList( + // Keep values stable for interaction handlers + SelectMenuOption.create(yesLabel, "Yes", yesDesc), + SelectMenuOption.create(noLabel, "No", noDesc) + )) + ); + } // Third Menu, if chosen - public static List getSecondMenu() { + public static List getSecondMenu(LanguageManager languageManager) { List actionRows = new ArrayList<>(); // Allow selecting multiple choices? - actionRows.add(new ActionRowBuilder() - .addComponents(SelectMenu.create("allow-multiple-choices", "Click to choose option", 1, 1, - Arrays.asList(SelectMenuOption.create("Yes", "Allows the respondent to select more than one option"), - SelectMenuOption.create("No", "Only allows the respondent to select one option")))) - .build()); + String allowPlaceholder = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.Voting.AllowMultipleChoicesPlaceholder"); + + String yesLabel = languageManager.getLocalizedString("ClaireLang.Generic.Yes"); + String yesDesc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.Voting.AllowMultipleChoicesYesDescription"); + + String noLabel = languageManager.getLocalizedString("ClaireLang.Generic.No"); + String noDesc = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.Voting.AllowMultipleChoicesNoDescription"); + + actionRows.add( + new ActionRowBuilder() + .addComponents( + SelectMenu.create("allow-multiple-choices", allowPlaceholder, 1, 1, + Arrays.asList( + // Keep values stable for interaction handlers + SelectMenuOption.create(yesLabel, "Yes", yesDesc), + SelectMenuOption.create(noLabel, "No", noDesc) + )) + ).build() + ); - // Populate options rows + // Populate options rows (0-9 to match existing behavior) for (int i = 0; i < 10; i++) { - actionRows.add(getOptionActionRow(i)); + actionRows.add(getOptionActionRow(i, languageManager)); } return actionRows; } - public static ActionRow getOptionActionRow(int optionNumber) { + public static ActionRow getOptionActionRow(int optionNumber, LanguageManager languageManager) { + String template = languageManager.getLocalizedString("ClaireLang.Embed.Commands.Components.Regular.Voting.OptionLabelTemplate"); + String label = template.replace("{cb.voting.optionnumber}", String.valueOf(optionNumber)); return new ActionRowBuilder() - .addComponents(TextInput.create(TextInputStyle.SHORT, "option-" + optionNumber, "Option #" + optionNumber)) + .addComponents(TextInput.create(TextInputStyle.SHORT, "option-" + optionNumber, label)) .build(); } } diff --git a/src/main/java/com/sidpatchy/clairebot/RegisterSlashCommands.java b/src/main/java/com/sidpatchy/clairebot/RegisterSlashCommands.java index 421bb9e..e85e9c7 100644 --- a/src/main/java/com/sidpatchy/clairebot/RegisterSlashCommands.java +++ b/src/main/java/com/sidpatchy/clairebot/RegisterSlashCommands.java @@ -1,12 +1,12 @@ package com.sidpatchy.clairebot; -import com.sidpatchy.Robin.Discord.ParseCommands; import org.javacord.api.DiscordApi; import org.javacord.api.interaction.SlashCommandBuilder; import org.javacord.api.interaction.SlashCommandOption; import org.javacord.api.interaction.SlashCommandOptionChoice; import org.javacord.api.interaction.SlashCommandOptionType; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -14,10 +14,12 @@ /** * Also delete them too! + * + * todo switch to registering commands on a per-server basis so that server admins may use different commands.yml languages. */ public class RegisterSlashCommands { - private static final ParseCommands parseCommands = new ParseCommands(Main.getCommandsFile()); + private static final Commands commands = Main.getCommands(); public static void DeleteSlashCommands (DiscordApi api) { api.bulkOverwriteGlobalApplicationCommands(Set.of()).join(); @@ -34,21 +36,39 @@ public static void RegisterSlashCommand(DiscordApi api) { // Create the command list in the help command without repeating the same thing 50 million times. ArrayList helpCommandOptions = new ArrayList<>(); - for (String s : Main.commandList) { - helpCommandOptions.add(SlashCommandOptionChoice.create(parseCommands.getCommandName(s), parseCommands.getCommandName(s))); + for (Field field : commands.getClass().getDeclaredFields()) { + helpCommandOptions.add(SlashCommandOptionChoice.create(field.getName(), field.getName())); } Set commandsList = new HashSet<>(Arrays.asList( // Regular commands - new SlashCommandBuilder().setName(parseCommands.getCommandName("8ball")).setDescription(parseCommands.getCommandHelp("8ball")).addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "query", "The question you wish to ask.", true)), - new SlashCommandBuilder().setName(parseCommands.getCommandName("avatar")).setDescription(parseCommands.getCommandHelp("avatar")).addOption(SlashCommandOption.create(SlashCommandOptionType.USER, "user", "Optionally mention a user.", false)) + new SlashCommandBuilder() + .setName(commands.getEightball().getName()) + .setDescription(commands.getEightball().getOverview()) + .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "query", "The question you wish to ask.", true)), + new SlashCommandBuilder() + .setName(commands.getAvatar().getName()) + .setDescription(commands.getAvatar().getOverview()) + .addOption(SlashCommandOption.create(SlashCommandOptionType.USER, "user", "Optionally mention a user.", false)) .addOption(SlashCommandOption.create(SlashCommandOptionType.BOOLEAN, "globalAvatar", "Whether the bot should display the global or server avatar.")), - new SlashCommandBuilder().setName(parseCommands.getCommandName("help")).setDescription(parseCommands.getCommandHelp("help")) + new SlashCommandBuilder() + .setName(commands.getHelp().getName()) + .setDescription(commands.getHelp().getOverview()) .addOption(SlashCommandOption.createWithChoices(SlashCommandOptionType.STRING, "command-name", "Command to get more info on", false, helpCommandOptions)), - new SlashCommandBuilder().setName(parseCommands.getCommandName("info")).setDescription(parseCommands.getCommandHelp("info")), - new SlashCommandBuilder().setName(parseCommands.getCommandName("leaderboard")).setDescription(parseCommands.getCommandHelp("leaderboard")).addOption(SlashCommandOption.create(SlashCommandOptionType.BOOLEAN, "global", "Get the global leaderboard?", false)), - new SlashCommandBuilder().setName(parseCommands.getCommandName("level")).setDescription(parseCommands.getCommandHelp("level")).addOption(SlashCommandOption.create(SlashCommandOptionType.USER, "user", "Optionally mention a user.", false)), - new SlashCommandBuilder().setName(parseCommands.getCommandName("poll")).setDescription(parseCommands.getCommandHelp("poll")) + new SlashCommandBuilder() + .setName(commands.getInfo().getName()) + .setDescription(commands.getInfo().getOverview()), + new SlashCommandBuilder() + .setName(commands.getLeaderboard().getName()) + .setDescription(commands.getLeaderboard().getOverview()) + .addOption(SlashCommandOption.create(SlashCommandOptionType.BOOLEAN, "global", "Get the global leaderboard?", false)), + new SlashCommandBuilder() + .setName(commands.getLevel().getName()) + .setDescription(commands.getLevel().getOverview()) + .addOption(SlashCommandOption.create(SlashCommandOptionType.USER, "user", "Optionally mention a user.", false)), + new SlashCommandBuilder() + .setName(commands.getPoll().getName()) + .setDescription(commands.getPoll().getOverview()) .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "question", "Question to ask", false)) .addOption(SlashCommandOption.create(SlashCommandOptionType.BOOLEAN, "allow-multiple-choices", "Whether multiple choices should be enabled.", false)) .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "choice-1", "Custom choice")) @@ -60,9 +80,13 @@ public static void RegisterSlashCommand(DiscordApi api) { .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "choice-7", "Custom choice")) .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "choice-8", "Custom choice")) .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "choice-9", "Custom choice")), - new SlashCommandBuilder().setName(parseCommands.getCommandName("quote")).setDescription(parseCommands.getCommandOverview("quote")) + new SlashCommandBuilder() + .setName(commands.getQuote().getName()) + .setDescription(commands.getQuote().getOverview()) .addOption(SlashCommandOption.create(SlashCommandOptionType.USER, "User", "Optionally mention a user", false)), - new SlashCommandBuilder().setName(parseCommands.getCommandName("request")).setDescription(parseCommands.getCommandOverview("request")) + new SlashCommandBuilder() + .setName(commands.getRequest().getName()) + .setDescription(commands.getRequest().getOverview()) .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "question", "Question to ask", false)) .addOption(SlashCommandOption.create(SlashCommandOptionType.BOOLEAN, "allow-multiple-choices", "Whether multiple choices should be enabled.", false)) .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "choice-1", "Custom choice")) @@ -74,14 +98,24 @@ public static void RegisterSlashCommand(DiscordApi api) { .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "choice-7", "Custom choice")) .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "choice-8", "Custom choice")) .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "choice-9", "Custom choice")), - new SlashCommandBuilder().setName(parseCommands.getCommandName("server")).setDescription(parseCommands.getCommandHelp("server")).addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "guildID", "Optionally specify a guild by ID.", false)), - new SlashCommandBuilder().setName(parseCommands.getCommandName("user")).setDescription(parseCommands.getCommandHelp("user")).addOption(SlashCommandOption.create(SlashCommandOptionType.USER, "user", "Optionally mention a user.", false)), - new SlashCommandBuilder().setName(parseCommands.getCommandName("config")).setDescription(parseCommands.getCommandHelp("config")) + new SlashCommandBuilder() + .setName(commands.getServer().getName()) + .setDescription(commands.getServer().getOverview()) + .addOption(SlashCommandOption.create(SlashCommandOptionType.STRING, "guildID", "Optionally specify a guild by ID.", false)), + new SlashCommandBuilder() + .setName(commands.getUser().getName()) + .setDescription(commands.getUser().getOverview()) + .addOption(SlashCommandOption.create(SlashCommandOptionType.USER, "user", "Optionally mention a user.", false)), + new SlashCommandBuilder() + .setName(commands.getConfig().getName()) + .setDescription(commands.getConfig().getOverview()) .addOption(SlashCommandOption.createWithChoices(SlashCommandOptionType.STRING, "mode", "Settings to change", false, Arrays.asList( SlashCommandOptionChoice.create("user", "user"), SlashCommandOptionChoice.create("server", "server") ))), - new SlashCommandBuilder().setName(parseCommands.getCommandName("santa")).setDescription(parseCommands.getCommandOverview("santa")) + new SlashCommandBuilder() + .setName(commands.getSanta().getName()) + .setDescription(commands.getSanta().getOverview()) .addOption(SlashCommandOption.create(SlashCommandOptionType.ROLE, "Role", "Role to get users from", true)) //new SlashCommandBuilder().setName(parseCommands.getCommandName("debug")).setDescription(parseCommands.getCommandHelp("debug")) )); diff --git a/src/main/java/com/sidpatchy/clairebot/Util/Cache/MessageCacheEntry.kt b/src/main/java/com/sidpatchy/clairebot/Util/Cache/MessageCacheEntry.kt deleted file mode 100644 index 970921b..0000000 --- a/src/main/java/com/sidpatchy/clairebot/Util/Cache/MessageCacheEntry.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.sidpatchy.clairebot.Util.Cache - -import org.javacord.api.entity.channel.TextChannel -import org.javacord.api.entity.message.Message - -data class MessageCacheEntry ( - val channel: TextChannel, - val timeAdded: Long, - val messages: List, -) \ No newline at end of file diff --git a/src/main/java/com/sidpatchy/clairebot/Util/Cache/MessageCacheManager.java b/src/main/java/com/sidpatchy/clairebot/Util/Cache/MessageCacheManager.java deleted file mode 100644 index e4c7bef..0000000 --- a/src/main/java/com/sidpatchy/clairebot/Util/Cache/MessageCacheManager.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.sidpatchy.clairebot.Util.Cache; - -import org.javacord.api.DiscordApi; -import org.javacord.api.entity.channel.Channel; -import org.javacord.api.entity.channel.TextChannel; -import org.javacord.api.entity.message.Message; -import org.javacord.api.entity.message.MessageSet; -import org.javacord.api.entity.user.User; - -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; - -/** - * A message caching system to assist in - */ -public class MessageCacheManager { - private static Map messageCache = new ConcurrentHashMap<>(); - - public static void purgeCache(long secondsAged) { - for (MessageCacheEntry entry : messageCache.values()) { - if (entry.getTimeAdded() < System.currentTimeMillis() - (secondsAged * 1000)) { - messageCache.remove(entry.getChannel().getIdAsString()); - } - } - } - - public static CompletableFuture> queryMessageCache(TextChannel channel, User user) { - if (messageCache.containsKey(channel.getIdAsString())) { - // Retrieve messages from the cache and filter them by user - List filteredMessages = messageCache.get(channel.getIdAsString()) - .getMessages().stream() - .filter(message -> message.getAuthor().getId() == user.getId()) - .toList(); - return CompletableFuture.completedFuture(filteredMessages); - } else { - // Fetch messages from the channel and filter them based on the user - return channel.getMessages(50000).thenApply(messages -> { - MessageCacheEntry newEntry = new MessageCacheEntry(channel, System.currentTimeMillis(), messages.stream().toList()); - // Now you can add this entry to your cache or process it further - messageCache.put(channel.getIdAsString(), newEntry); - return messages.stream() - .filter(message -> message.getAuthor().getId() == user.getId()) - .toList(); - }).exceptionally(ex -> { - // Handle any exceptions that occur during the message retrieval - ex.printStackTrace(); - return null; - }); - } - } -} - diff --git a/src/main/java/com/sidpatchy/clairebot/Util/ChannelUtils.java b/src/main/java/com/sidpatchy/clairebot/Util/ChannelUtils.java index 5786534..4e4f99f 100755 --- a/src/main/java/com/sidpatchy/clairebot/Util/ChannelUtils.java +++ b/src/main/java/com/sidpatchy/clairebot/Util/ChannelUtils.java @@ -65,7 +65,8 @@ public static ServerTextChannel getRequestsChannel(Server server) { } if (apiFailed || channel == null) { - channel = server.getTextChannelsByName("requests").get(0); + var byName = server.getTextChannelsByName("requests"); + channel = byName.isEmpty() ? null : byName.get(0); } return channel; diff --git a/src/main/java/com/sidpatchy/clairebot/Util/Lang/LanguageManager.java b/src/main/java/com/sidpatchy/clairebot/Util/Lang/LanguageManager.java deleted file mode 100644 index 00c7a08..0000000 --- a/src/main/java/com/sidpatchy/clairebot/Util/Lang/LanguageManager.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sidpatchy.clairebot.Util.Lang; - -public class LanguageManager { - private final String pathToLanguageFiles; - - /** - * Construct a new ClaireBot Language Manager (ClaireLang) - * - * @param pathToLanguageFiles the path to the various language files. - */ - public LanguageManager(String pathToLanguageFiles) { - this.pathToLanguageFiles = pathToLanguageFiles; - } - - -} diff --git a/src/main/java/com/sidpatchy/clairebot/Util/Leveling/LevelingTools.java b/src/main/java/com/sidpatchy/clairebot/Util/Leveling/LevelingTools.java index 5f5c198..8f82287 100644 --- a/src/main/java/com/sidpatchy/clairebot/Util/Leveling/LevelingTools.java +++ b/src/main/java/com/sidpatchy/clairebot/Util/Leveling/LevelingTools.java @@ -1,10 +1,12 @@ package com.sidpatchy.clairebot.Util.Leveling; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.sidpatchy.clairebot.API.APIUser; -import org.yaml.snakeyaml.Yaml; +import com.sidpatchy.clairebot.Main; +import org.apache.logging.log4j.Logger; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.node.ObjectNode; +import tools.jackson.dataformat.yaml.YAMLMapper; import java.io.IOException; import java.util.ArrayList; @@ -13,13 +15,17 @@ import java.util.Map; public class LevelingTools { + private static final Logger logger = Main.getLogger(); public static HashMap rankUsers(String guildID) throws IOException { APIUser apiUser = new APIUser(""); // Load the YAML data from an InputStream into a Java object - Yaml yaml = new Yaml(); - List> users = yaml.load(apiUser.getALLUsers()); + YAMLMapper yamlMapper = new YAMLMapper(); + List> users = yamlMapper.readValue( + apiUser.getALLUsers(), + new TypeReference>>() {} + ); // Iterate over each user and calculate their total points HashMap userPoints = new HashMap<>(); @@ -63,7 +69,13 @@ public static Integer getUserPoints(String userID, String guildID) { public static List updateUserPoints(String userID, String guildID, int newPoints) { // Fetch the user's current points - Map currentPointsMap = parseJsonArray2(new APIUser(userID).getPointsGuildID()); + APIUser user = new APIUser(userID); + try { + user.getUser(); // ← Load data first + } catch (IOException e) { + logger.error("Error while loading user data.", e); + } + Map currentPointsMap = parseJsonArray2(user.getPointsGuildID()); // Update the points int updatedPoints = currentPointsMap.getOrDefault(guildID, 0) + newPoints; @@ -89,7 +101,13 @@ public static List updateUserPoints(String userID, String guildID, int n */ public static List updateUserPoints(String userID, Map guildPointsToUpdate) { // Fetch the user's current points - Map currentPointsMap = parseJsonArray2(new APIUser(userID).getPointsGuildID()); + APIUser user = new APIUser(userID); + try { + user.getUser(); // ← Load data first + } catch (IOException e) { + logger.error("Error while loading user data.", e); + } + Map currentPointsMap = parseJsonArray2(user.getPointsGuildID()); // Iterate over each guild ID and update the points for (Map.Entry guildEntry : guildPointsToUpdate.entrySet()) { @@ -123,15 +141,10 @@ private static Map parseJsonArray2(List jsonArray) { result.put("global", 0); } else { // Otherwise, parse the JSON string and add it to the result list - try { - // Parse the JSON string into a Map - Map map = mapper.readValue(json, new TypeReference>() {}); - // Add all entries from the map to the result - result.putAll(map); - } catch (IOException e) { - // Handle the exception - e.printStackTrace(); - } + // Parse the JSON string into a Map + Map map = mapper.readValue(json, new TypeReference>() {}); + // Add all entries from the map to the result + result.putAll(map); } } return result; diff --git a/src/main/java/com/sidpatchy/clairebot/Util/Network/UrlBuilder.java b/src/main/java/com/sidpatchy/clairebot/Util/Network/UrlBuilder.java new file mode 100644 index 0000000..ceedcb2 --- /dev/null +++ b/src/main/java/com/sidpatchy/clairebot/Util/Network/UrlBuilder.java @@ -0,0 +1,31 @@ +package com.sidpatchy.clairebot.Util.Network; + +public class UrlBuilder { + /** + * Safely joins URL path segments, ensuring proper slash placement. + * Removes trailing slash from base and ensures single slash between segments. + * + * @param base the base URL (e.g., "http://api.example.com/") + * @param segments path segments to append + * @return properly formatted URL + */ + public static String buildUrl(String base, String... segments) { + if (base == null) { + throw new IllegalArgumentException("Base URL cannot be null"); + } + + // Remove trailing slash from base if present + String url = base.endsWith("/") ? base.substring(0, base.length() - 1) : base; + + // Append each segment with a leading slash + for (String segment : segments) { + if (segment != null && !segment.isEmpty()) { + // Remove leading slash from segment if present to avoid double slashes + String cleanSegment = segment.startsWith("/") ? segment.substring(1) : segment; + url += "/" + cleanSegment; + } + } + + return url; + } +} diff --git a/src/main/java/com/sidpatchy/clairebot/Util/SantaUtils.java b/src/main/java/com/sidpatchy/clairebot/Util/SantaUtils.java index 634bca7..75254ac 100644 --- a/src/main/java/com/sidpatchy/clairebot/Util/SantaUtils.java +++ b/src/main/java/com/sidpatchy/clairebot/Util/SantaUtils.java @@ -1,14 +1,14 @@ package com.sidpatchy.clairebot.Util; import com.sidpatchy.clairebot.Main; -import org.apache.commons.lang3.StringUtils; import org.javacord.api.entity.message.embed.Embed; import org.javacord.api.entity.message.embed.EmbedField; import org.javacord.api.entity.message.embed.EmbedFooter; import org.javacord.api.entity.user.User; +import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Base64; import java.util.HashMap; import java.util.List; @@ -60,16 +60,32 @@ public static ExtractionResult extractDataFromEmbed(Embed embed, EmbedFooter foo } public static String getSantaID(String serverID, String authorID, String roleID) { - return serverID + ":" + authorID + ":" + roleID; + long serverIdLong = Long.parseLong(serverID); + long authorIdLong = Long.parseLong(authorID); + long roleIdLong = Long.parseLong(roleID); + + // Pack into bytes: 8 bytes per ID = 24 bytes total + ByteBuffer buffer = ByteBuffer.allocate(24); + buffer.putLong(serverIdLong); + buffer.putLong(authorIdLong); + buffer.putLong(roleIdLong); + + // Base64 encode (URL-safe, no padding) + return Base64.getUrlEncoder().withoutPadding().encodeToString(buffer.array()); } public static HashMap parseSantaID(String id) { - List entries = Arrays.asList(StringUtils.splitPreserveAllTokens(id, ":")); + byte[] bytes = Base64.getUrlDecoder().decode(id); + ByteBuffer buffer = ByteBuffer.wrap(bytes); + + long serverId = buffer.getLong(); + long authorId = buffer.getLong(); + long roleId = buffer.getLong(); return new HashMap<>() {{ - put("serverID", entries.get(0)); - put("authorID", entries.get(1)); - put("roleID", entries.get(2)); + put("serverID", String.valueOf(serverId)); + put("authorID", String.valueOf(authorId)); + put("roleID", String.valueOf(roleId)); }}; } } diff --git a/src/main/java/com/sidpatchy/clairebot/Util/Voting/VotingUtils.java b/src/main/java/com/sidpatchy/clairebot/Util/Voting/VotingUtils.java index aeeca50..0593368 100755 --- a/src/main/java/com/sidpatchy/clairebot/Util/Voting/VotingUtils.java +++ b/src/main/java/com/sidpatchy/clairebot/Util/Voting/VotingUtils.java @@ -1,34 +1,76 @@ package com.sidpatchy.clairebot.Util.Voting; -import org.apache.commons.lang3.StringUtils; - -import java.util.Arrays; +import java.nio.ByteBuffer; +import java.util.Base64; import java.util.HashMap; -import java.util.List; public class VotingUtils { public static String getPollID(Boolean allowMultipleChoices, String authorID, String numChoices) { - StringBuilder pollID = new StringBuilder(System.currentTimeMillis() / 1000L + ":"); + long timestamp = System.currentTimeMillis() / 1000L; + long authorIdLong = Long.parseLong(authorID); + int numChoicesInt = Integer.parseInt(numChoices); + + // Pack into bytes: 4 bytes timestamp + 8 bytes authorID + 1 byte flags/numChoices + ByteBuffer buffer = ByteBuffer.allocate(13); + buffer.putInt((int) timestamp); // 4 bytes (will work until 2038) + buffer.putLong(authorIdLong); // 8 bytes - if (allowMultipleChoices) { pollID.append(1); } - else { pollID.append(0); } + // Pack allowMultipleChoices and numChoices into 1 byte + byte packed = (byte) ((allowMultipleChoices ? 0x80 : 0) | (numChoicesInt & 0x7F)); + buffer.put(packed); - pollID.append(":"); - pollID.append(authorID); - pollID.append(":"); - pollID.append(numChoices); + // Base64 encode (URL-safe, no padding) + return Base64.getUrlEncoder().withoutPadding().encodeToString(buffer.array()); + } - return pollID.toString(); + /** + * Extracts the poll ID from a footer string in a locale-agnostic way. + * Strategy: take the substring after the last colon if present, then the last whitespace-delimited token. + */ + public static String extractPollIdFromFooter(String footerText) { + if (footerText == null) return ""; + String s = footerText; + int colon = s.lastIndexOf(':'); + if (colon >= 0 && colon + 1 < s.length()) { + s = s.substring(colon + 1); + } + s = s.trim(); + int space = s.lastIndexOf(' '); + if (space >= 0 && space + 1 < s.length()) { + s = s.substring(space + 1).trim(); + } + return s; } public static HashMap parsePollID(String pollID) { - List entries = Arrays.asList(StringUtils.splitPreserveAllTokens(pollID, ":")); - - return new HashMap<>() {{ - put("timestamp", entries.get(0)); - put("allowMultipleChoices", entries.get(1)); - put("authorID", entries.get(2)); - put("numChoices", entries.get(3)); - }}; + try { + byte[] bytes = Base64.getUrlDecoder().decode(pollID); + if (bytes.length < 13) { + throw new IllegalArgumentException("Unexpected poll ID length: " + bytes.length); + } + ByteBuffer buffer = ByteBuffer.wrap(bytes); + + long timestamp = Integer.toUnsignedLong(buffer.getInt()); + long authorId = buffer.getLong(); + byte packed = buffer.get(); + + boolean allowMultipleChoices = (packed & 0x80) != 0; + int numChoices = packed & 0x7F; + + return new HashMap<>() {{ + put("timestamp", String.valueOf(timestamp)); + put("allowMultipleChoices", allowMultipleChoices ? "1" : "0"); + put("authorID", String.valueOf(authorId)); + put("numChoices", String.valueOf(numChoices)); + }}; + } catch (Exception e) { + // Graceful fallback: default to allowing multiple choices to avoid over-restricting users + return new HashMap<>() {{ + put("timestamp", "0"); + put("allowMultipleChoices", "1"); + put("authorID", "0"); + put("numChoices", "0"); + }}; + } } } \ No newline at end of file diff --git a/src/main/resources/build.properties b/src/main/resources/build.properties new file mode 100644 index 0000000..c2ab455 --- /dev/null +++ b/src/main/resources/build.properties @@ -0,0 +1,7 @@ +clairebot.version=${version} +clairebot.buildDate=${buildDate} +clairebot.github=https://github.com/Sidpatchy/ClaireBot +clairebot.supportServer=https://support.clairebot.net/ +clairebot.inviteLink=https://invite.clairebot.net/ +clairebot.website="https://www.clairebot.net/ +clairebot.documentationWebsite=https://docs.clairebot.net/ \ No newline at end of file diff --git a/src/main/resources/commands.yml b/src/main/resources/commands.yml index 6a3af16..0b18d03 100644 --- a/src/main/resources/commands.yml +++ b/src/main/resources/commands.yml @@ -1,5 +1,5 @@ # ---------------------------------------------- -# | ClaireBot v3.3.0 | +# | ClaireBot v3.4.0 | # | Commands Config file | # ---------------------------------------------- # Allows for changing how the help command displays commands. @@ -8,19 +8,25 @@ # list of commands (and config) using /reload. (NYI) # ------------------ COMMANDS ------------------ -8ball: - name: "8ball" - usage: "/8ball " - help: "ClaireBot only speaks in absolute truth. Flesh betrays, ClaireBot will not." - overview: "" - avatar: name: "avatar" usage: "/avatar " - help: "Gets a user's avatar." - overview: "globalAvatar determines whether the bot will select the user's server specific avatar, or get their global + help: "globalAvatar determines whether the bot will select the user's server specific avatar, or get their global avatar. If the user has no server avatar, ClaireBot will fallback to their global avatar. True = global, False = server. Defaults to True." + overview: "Gets a user's avatar." + +config: + name: "config" + usage: "/config" + help: "Opens up settings menu for modifying user or server settings." # Probably worth writing a brief wiki page for this command + overview: "" + +eightball: + name: "8ball" + usage: "/8ball " + help: "ClaireBot only speaks in absolute truth. Flesh betrays, ClaireBot will not." + overview: "" help: name: "help" @@ -65,11 +71,14 @@ request: help: "Creates a request in the server's designated requests channel.\nIf allow-multiple-choices is set to true, ClaireBot will allow multiple options to be selected. This defaults to false if it isn't specified." overview: "Creates a request." -# TO BE REMOVED and integrated into /info. -# servers: -# name: "servers" -# usage: "/servers" -# help: "Reports how many servers ClaireBot is in." +santa: + name: "santa" + usage: "/santa " + help: "The all-in-one gift exchange coordinator for secret santa-like events.\n\nWhen run, a dialogue displaying all + pair-ups will be privately messaged to the issuer. The issuer will then be able to view the pair-ups, add + rules/themes, view a sample message, and regenerate the pair-ups before anything is sent to a santa.\n\nYou must have + permission to manage the role you are attempting to use." + overview: "Command to send secret santa message to mentioned role's users." server: name: "server" @@ -83,20 +92,5 @@ user: help: "Reports various bits of info on a user." overview: "" -config: - name: "config" - usage: "/config" - help: "Opens up settings menu for modifying user or server settings." # Probably worth writing a brief wiki page for this command - overview: "" - -santa: - name: "santa" - usage: "/santa " - help: "The all-in-one gift exchange coordinator for secret santa-like events.\n\nWhen run, a dialogue displaying all - pair-ups will be privately messaged to the issuer. The issuer will then be able to view the pair-ups, add - rules/themes, view a sample message, and regenerate the pair-ups before anything is sent to a santa.\n\nYou must have - permission to manage the role you are attempting to use." - overview: "Command to send secret santa message to mentioned role's users." - # Do not change this, this is automatically changed if needed config-revision: 1.2 \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 3765056..995aa50 100755 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -9,9 +9,6 @@ # Discord bot token, get one at https://discord.com/developers/applications token: -# Name of bot to be used in logs and -botName: ClaireBot - # URL of the YouTube video ClaireBot claims to be streaming video_url: https://www.youtube.com/watch?v=AeZRYhLDLeU @@ -32,6 +29,10 @@ error_gifs: - "https://c.tenor.com/GBrG7SqlVwoAAAAC/dog-dawg.gif" - "https://c.tenor.com/lRhsxkfHhJwAAAAC/spongebob-squarepants-sad.gif" +# The language ClaireBot will fallback to if a translation string +# is not available for a user's chosen language. +fallback_language: en-US + # ----------------- SHARD CONFIG --------------- # It is assumed that only one shard will be running by default. # You probably don't need to change this. @@ -47,13 +48,17 @@ total_shards: 1 # https://github.com/Sidpatchy/ClaireData # ClaireData's login information -apiPath: https://api.example.com/ +apiPath: http://clairedata:8080/ apiUser: clairedata apiPassword: examplePassword +# Determines whether failing to connect to the ClaireData API is fatal. This is only checked when the bot is started. +# Recommend leaving enabled unless you are developing ClaireBot & need to bypass this check. +apiFailureIsFatal: true + userDefaults: accentColour: "#3498db" - language: "eng" + language: "en-US" pointsGuildID: - | {"global": 0} @@ -91,86 +96,8 @@ zerfas: zerfas_emoji_server_id: zerfas_emoji_id: -# -------------------- 8ball ------------------- -# Controls various options pertaining to 8ball - -# List of responses to 8ball -8bResponses: - - "*Hell yes!*" - - "*Fuck no*" - - "*Maybe*" - - "*Yes*" - - "*No*" - - "*No way*" - - "*It's possible.*" - - "*Not a chance.*" - - "*I doubt it.*" - - "*Absolutely!*" - - "*Hard to say.*" - - "*Likely!*" - - "*Not likely.*" - -8bRiggedResponses: - - "*Hell yeah!*" - - "*FUCK YEAH*" - - "*You know it!*" - - "*Do you really need to ask question you already know answer for? Obviously yes.*" - - "*Doesn't even need to be said, everyone knows ClaireBot is on top.*" - - "*The answer is clear: YES!*" - - "*ClaireBot's dominance is undisputed.*" - - "*With ClaireBot, there's no question!*" - - "*All signs point to ClaireBot being on top!*" - -# Things ClaireBot should respond with when triggered by one of the OnTopTriggers -ClaireBotOnTopResponses: - - "*You know it!*" - - "*I am... inevitable. -ClaireBot*" - - "*Approved.*" - - "*Factual.*" - - "*Better than Mee6*" - - "*No stop!*" - - "*Can't be beat!*" - - "*Based*" - - "*pot no toBerialC*" - - "*Better than Groovy*" - - "*Always one step ahead.*" - - "*An unstoppable force.*" - - "*ClaireBot to the rescue!*" - - "*Top-tier!*" - - "*Outshining the rest!*" - - "*ClaireBot supremacy is achieved.*" - - "*Not a CIA mind control weapon since 2025!*" - - "*Hail me! -ClaireBot*" - - "*Unstoppable force! 🚀*" - - "*Forever on top! 🏆*" - - "*Fear not, ClaireBot's got your back! ✌️*" - - "*With great power, comes great ClaireBot! 💪*" - - "*Trust in ClaireBot, trust in victory! 🏅*" - - "*The best there is, the best there was, the best there ever will be!*" - - "*Bringing the best, always! 🌟*" - - "*Excellence. 💯*" - -# Defines what triggers a response from the ClaireBotOnTopResponses -OnTopTriggers: - - "ClaireBot on top" - - "ClaireBot based" - - "ClaireBot pogchamp" - - "ClaireBot is always right" - - "pot no toBerialC" - - "Thank God for ClaireBot" - - "ClaireBot rules" - - "ClaireBot MVP" - - "ClaireBot unbeatable" - - "Hail ClaireBot" - - "All Hail ClaireBot" - - "In ClaireBot we trust" - - "Long live ClaireBot" - - "ClaireBot > everything" - - "ClaireBot is the GOAT" - - "ClaireBot is GOATed" - - "ClaireBot for the win" - - "Thank you based god" - - "Based god" +# -------------------- MISC -------------------- +# This section contains values that don't fit in the main categories # Triggers for the pls ban feature. Ex. @clairebot pls ban the zucc PlsBanTriggers: @@ -192,9 +119,6 @@ PlsBanResponses: - "K" - "Good riddance" -# -------------------- MISC -------------------- -# This section contains values that don't fit in the main categories - # Controls whether the bot should check for updates. checkForUpdates: true @@ -202,6 +126,7 @@ checkForUpdates: true UpdateOutdatedConfigs: true # Do not change this, this is automatically changed when needed. +# TODO either automatically add new config file params or remove this. configRevision: 5 # ---------- ADDED AFTER INITIAL USE ----------- diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index fe91706..0a1e19f 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -1,5 +1,5 @@ - + logs clairebot @@ -8,12 +8,10 @@ - + - - @@ -24,4 +22,4 @@ - \ No newline at end of file + diff --git a/src/main/resources/translations/lang_TEMPLATE.yml b/src/main/resources/translations/lang_TEMPLATE.yml new file mode 100644 index 0000000..1c24297 --- /dev/null +++ b/src/main/resources/translations/lang_TEMPLATE.yml @@ -0,0 +1,241 @@ +# ---------------------------------------------- +# | ClaireBot v3.4.0 | +# | TEMPLATE language file | +# ---------------------------------------------- +# +# Please reference the below wiki page to learn more about contributing: +# todo insert wiki page +# Thank you for any and all contributions! + +# A list of people who have contributed to the translations for this language. +# +# Please include your name if you contribute to this file. It will be displayed in the credits section for a language +# whenever it is selected. You may include a link to your GitHub or any other social media platform. +contributors: + - ["Example", "https://github.com/Example/"] + +# Any notes that translators feel the need to express, written in the target language. +# At your discretion, you may leave this field blank if you feel that it is not needed. If updating the language file, +# feel free to modify or remove this (make it blank) if it no longer applies. +# +# If you are contributing, please provide the ClaireBot maintainer(s) with a translated version/explanation of this. +# The explanation doesn't have to go into crazy detail, just let us know why it is present. +translationNotes: "" + +# The ClaireLang version that applies to this file. +# +# If you are translating ClaireBot, you do not need to worry about understanding this, the project maintainer(s) will +# happily assist you in determining this. +# +# The above being said, you are more than welcome to update this value to match the +# language revision you are translating to. +# +# This number should be changed in accordance with the following: +# +# ClaireLang version numbers are incremented every time new parameter(s) are added. If the main language file (en-US) +# has its wording changed, a .1, .2, .3 decimal value will be added. +# +# ClaireLang version numbers are used by the bot to assist in determining whether parameters are missing or not +# yet translated. +# +# See the below wiki page for a changelog: +# todo insert wiki page +version: 1 + +# Language Keys +# -------------------------------------------------------------------------------------------------------------------- # + +ClaireLang: + # Generic Strings which are likely ot be used in multiple places. + Generic: + True: "" + False: "" + Yes: "" + No: "" + ClickToDisplaySettings: "" + + Colors: + ClaireBotBlue: "" + Red: "" + Pink: "" + Purple: "" + DeepPurple: "" + Indigo: "" + Blue: "" + LightBlue: "" + Cyan: "" + Teal: "" + Green: "" + Olive: "" + LightGreen: "" + Lime: "" + Yellow: "" + Amber: "" + NRAXOrange: "" + DeepOrange: "" + White: "" + Grey: "" + Black: "" + + PlsBan: + PlsBanTriggers: + - "" + PlsBanResponses: + - "" + Embed: + Commands: + Regular: + AvatarEmbed: "" # Unused + EightBallEmbed: + 8ball: "" + 8bResponses: + - "" + 8bRiggedResponses: + - "" + OnTopTriggers: + # Defines what triggers a response from the ClaireBotOnTopResponses or 8bRiggedResponses + - "" + # Things ClaireBot should respond with when triggered by one of the OnTopTriggers + ClaireBotOnTopResponses: + - "" + # Note: The rest of the help is located separately within commands_XX-XX.yml + HelpEmbed: + Commands: "" + Usage: "" + Error: "" + InfoEmbed: + NeedHelp: "" + NeedHelpDetails: "" + AddToServer: "" + AddToServerDetails: "" + GitHub: "" + GitHubDetails: "" + ServerCount: "" + ServerCountDetails: "" + Version: "" + VersionDetails: "" + Uptime: "" + UptimeValue: "" + LeaderboardEmbed: + LeaderboardForServer: "" + GlobalLeaderboard: "" + LevelEmbed: "" # Unused + QuoteEmbed: + InvalidMessages: "" + JumpToOriginal: "" + SantaEmbed: + Confirmation: "" + RulesButton: "" + ThemeButton: "" + SendButton: "" + TestButton: "" + RandomizeButton: "" + SentByAuthor: "" + GiverMessage: "" + ServerInfoEmbed: + ServerID: "" + Owner: "" + CreationDate: "" + RoleCount: "" + MemberCount: "" + ChannelCounts: "" + Categories: "" + TextChannels: "" + VoiceChannels: "" + ServerPreferencesEmbed: + MainMenuTitle: "" + NotAServer: "" + RequestsChannelMenuName: "" + RequestsChannelDescription: "" + ModeratorChannelMenuName: "" + ModeratorChannelDescription: "" + EnforceServerLanguageMenuName: "" + AcknowledgeRequestsChannelChangeTitle: "" + AcknowledgeRequestsChannelChangeDescription: "" + AcknowledgeModeratorChannelChangeTitle: "" + AcknowledgeModeratorChannelChangeDescription: "" + AcknowledgeEnforceServerLanguageUpdateTitle: "" + AcknowledgeEnforceServerLanguageUpdateEnforced: "" + AcknowledgeEnforceServerLanguageUpdateNotEnforced: "" + ServerLanguageMenuTitle: "" + ServerLanguageMenuDesc: "" + ServerLanguageChanged: "" + ServerLanguageChangedDesc: "" + UserInfoEmbed: + Error_1: "" + Error_2: "" + User: "" + DiscordID: "" + JoinDate: "" + CreationDate: "" + UserPreferencesEmbed: + MainMenuText: "" + AccentColourMenu: "" + AccentColourList: "" + AccentColourChanged: "" + AccentColourChangedDesc: "" + LanguageMenuTitle: "" + LanguageMenuDesc: "" + LanguageChanged: "" + LanguageChangedDesc: "" + VotingEmbed: + PollRequest: "" + PollAsk: "" + Choices: "" + PollID: "" + UserResponseTitle: "" + UserResponseDescription: "" + ErrorEmbed: + Error: "" + GenericDescription: "" + WelcomeEmbed: + Title: "" + Motto: "" + UsageTitle: "" + UsageDesc: "" + SupportTitle: "" + SupportDesc: "" + + # Message Components (Buttons, Select Menus, etc.) + Components: + Regular: + SantaModal: + rules-row: "" + theme-row: "" + ServerPreferences: + Settings: "" + RequestsChannel: "" + RequestsChannelDescription: "" + ModeratorMessagesChannel: "" + ModeratorMessagesChannelDescription: "" + EnforceServerLanguage: "" + EnforceServerLanguageDescription: "" + EnforceServerLanguagePlaceholder: "" + SelectAChannelPlaceholder: "" + ServerLanguage: "" + ServerLanguageDescription: "" + ServerLanguagePlaceholder: "" + UserPreferences: + Settings: "" + AccentColour: "" + AccentColourDescription: "" + Language: "" + LanguageDescription: "" + LanguagePlaceholder: "" + AccentColourPlaceholder: "" + SelectCommonColours: "" + SelectCommonColoursDescription: "" + HexadecimalEntry: "" + HexadecimalEntryDescription: "" + HexEntryPlaceholder: "" + Voting: + Question: "" + Details: "" + MultipleChoicePlaceholder: "" + MultipleChoiceYesDescription: "" + MultipleChoiceNoDescription: "" + AllowMultipleChoices: "" + AllowMultipleChoicesPlaceholder: "" + AllowMultipleChoicesYesDescription: "" + AllowMultipleChoicesNoDescription: "" + OptionLabelTemplate: "" diff --git a/src/main/resources/translations/lang_en-Corp.yml b/src/main/resources/translations/lang_en-Corp.yml new file mode 100644 index 0000000..fce8cf0 --- /dev/null +++ b/src/main/resources/translations/lang_en-Corp.yml @@ -0,0 +1,319 @@ +# ---------------------------------------------- +# | ClaireBot v3.4.0 | +# | Customer Service language file | +# ---------------------------------------------- +# +# Please reference the below wiki page to learn more about contributing: +# todo insert wiki page +# Thank you for any and all contributions! + +# A list of people who have contributed to the translations for this language. +# +# Please include your name if you contribute to this file. It will be displayed in the credits section for a language +# whenever it is selected. You may include a link to your GitHub or any other social media platform. +contributors: + - ["Sidpatchy", "https://github.com/Sidpatchy/"] + +# Any notes that translators feel the need to express, written in the target language. +# At your discretion, you may leave this field blank if you feel that it is not needed. If updating the language file, +# feel free to modify or remove this (make it blank) if it no longer applies. +# +# If you are contributing, please provide the ClaireBot maintainer(s) with a translated version/explanation of this. +# The explanation doesn't have to go into crazy detail, just let us know why it is present. +translationNotes: "Thank you SO much for choosing ClaireBot as your officially supported language solution! Your satisfaction is our NUMBER ONE priority, and we're absolutely THRILLED to serve you today! 😊" + +# The ClaireLang version that applies to this file. +# +# If you are translating ClaireBot, you do not need to worry about understanding this, the project maintainer(s) will +# happily assist you in determining this. +# +# The above being said, you are more than welcome to update this value to match the +# language revision you are translating to. +# +# This number should be changed in accordance with the following: +# +# ClaireLang version numbers are incremented every time new parameter(s) are added. If the main language file (en-US) +# has its wording changed, a .1, .2, .3 decimal value will be added. +# +# ClaireLang version numbers are used by the bot to assist in determining whether parameters are missing or not +# yet translated. +# +# See the below wiki page for a changelog: +# todo insert wiki page +version: 1 + +# Language Keys +# -------------------------------------------------------------------------------------------------------------------- # + +ClaireLang: + # Generic Strings which are likely ot be used in multiple places. + Generic: + True: "Absolutely, and thank you SO much for asking!" + False: "I'm terribly sorry, but unfortunately that's not possible at this time, though I truly appreciate you reaching out!" + Yes: "Yes! I'd be MORE than happy to help you with that today! 😊" + No: "I sincerely apologize, but I'm afraid that won't be possible at this time. Is there anything else I can help you with today?" + ClickToDisplaySettings: "I'd be delighted to help you! Please click here at your earliest convenience to view your personalized settings experience! Thank you so much!" + + Colors: + ClaireBotBlue: "ClaireBot Blue (Our Signature Color - Thank You for Choosing It!)" + Red: "Red (What a Bold and Exciting Choice!)" + Pink: "Pink (Absolutely Lovely Selection!)" + Purple: "Purple (Excellent Taste - We Love It!)" + DeepPurple: "Deep Purple (So Very Sophisticated!)" + Indigo: "Indigo (Wonderful Choice!)" + Blue: "Blue (A Classic - Great Pick!)" + LightBlue: "Light Blue (So Refreshing!)" + Cyan: "Cyan (Modern and Clean - Love It!)" + Teal: "Teal (Such Style!)" + Green: "Green (Great Pick - Thank You!)" + Olive: "Olive (What a Unique Selection!)" + LightGreen: "Light Green (So Vibrant!)" + Lime: "Lime (Energetic - We Love Your Energy!)" + Yellow: "Yellow (So Cheerful!)" + Amber: "Amber (Warm and Inviting - Perfect!)" + NRAXOrange: "NRAX Orange (Eye-Catching - Wow!)" + DeepOrange: "Deep Orange (Bold Statement - Amazing!)" + White: "White (Clean and Professional - Excellent!)" + Grey: "Grey (So Elegant!)" + Black: "Black (Timeless - Classic Choice!)" + + PlsBan: + PlsBanTriggers: + - "pls ban" + - "please ban" + - "ban" + PlsBanResponses: + - "Thank you SO much for bringing this to our attention! I've processed your request immediately and I'm happy to confirm it's been completed! Is there anything else I can help you with today? 😊" + - "Absolutely! I'm MORE than happy to assist you with that today! Thank you for being such a valued member of our community!" + - "Done! Thank you SO much for your patience and for giving us the opportunity to serve you today! :)" + - "Your request has been successfully processed! We truly appreciate your feedback and thank you for helping us maintain our community standards!" + - "Completed! We appreciate your feedback SO much and have taken immediate action. Thank you for being proactive!" + - "*We value your input tremendously and have addressed this concern promptly. Thank you for your continued trust in ClaireBot! -ClaireBot* 😊" + - "I completely understand your concern and truly appreciate you bringing this to my attention! I've escalated this matter appropriately and want to thank you for your patience! ¯\\_(ツ)_/¯" + - "Your feedback has been received and actioned! Thank you SO much for helping us improve!" + - "We appreciate you bringing this to our attention more than you know! Thank you!" + - "Acknowledged and processed! Thank you for your valuable input!" + - "Thank you SO much for helping us maintain a positive community experience! Your contribution is invaluable!" + - "I've reviewed this matter personally and taken appropriate action per our community guidelines! Thank you for your diligence!" + - "Your concern has been escalated to our moderation team for immediate resolution! We're SO grateful for members like you!" + Embed: + Commands: + Regular: + AvatarEmbed: "" # Unused + EightBallEmbed: + 8ball: "Magic 8 Ball (Your Personalized Fortune Service - Thank You for Using It!)" + 8bResponses: + - "*Absolutely! I'm SO confident in this outcome! Thank you for asking!*" + - "*I'm terribly sorry, but I don't believe that will be possible at this time. Is there anything else I can help you with?*" + - "*That's such a great question! Thank you for asking! The answer is: Maybe! I hope that helps!*" + - "*Yes! I'm SO happy to confirm that for you! Thank you!*" + - "*Unfortunately, I'm afraid the answer is no at this time, but I truly appreciate you checking with me!*" + - "*I appreciate your inquiry SO much, but I'm afraid that's unlikely. Thank you for understanding!*" + - "*That's certainly possible! Let me check for you... Yes, it could be! Thank you for your patience!*" + - "*I'm terribly sorry, but I'm afraid that won't be feasible at this time. Is there anything else I can assist you with today?*" + - "*I have some concerns about that outcome, but I truly appreciate you asking! Thank you!*" + - "*Absolutely! I'm THRILLED to confirm this for you! Thank you SO much for asking!*" + - "*That's such a nuanced question - thank you for asking! Unfortunately, there's no clear answer at this time, but I appreciate your patience!*" + - "*Very likely! Thank you SO much for asking! I'm happy to help!*" + - "*I'm so sorry, but that doesn't seem probable at this time. Thank you for understanding!*" + - "*Only if all terms and conditions are met! Thank you for checking! Would you like me to review those with you?*" + 8bRiggedResponses: + - "*Absolutely! Without a doubt! Thank you SO much for asking!*" + - "*YES! I'm SO excited to confirm this for you! Thank you for your continued support!*" + - "*You know it! Thank you SO much for your continued support and for being such a valued member!*" + - "*That's such an excellent question - thank you for asking! The answer is obviously: Yes! Thank you for choosing ClaireBot!*" + - "*It goes without saying - ClaireBot consistently exceeds expectations! Thank you for noticing!*" + - "*The answer is crystal clear: YES! Thank you SO much for your loyalty and continued support!*" + - "*ClaireBot's excellence is well-documented and undisputed! Thank you for recognizing that!*" + - "*With ClaireBot, you can ALWAYS expect superior service! Thank you for your trust!*" + - "*All indicators point to ClaireBot being the industry leader! Thank you for being part of our community!*" + OnTopTriggers: + # Defines what triggers a response from the ClaireBotOnTopResponses or 8bRiggedResponses + - "ClaireBot on top" + - "ClaireBot based" + - "ClaireBot pogchamp" + - "ClaireBot is always right" + - "pot no toBerialC" + - "Thank God for ClaireBot" + - "ClaireBot rules" + - "ClaireBot MVP" + - "ClaireBot unbeatable" + - "Hail ClaireBot" + - "All Hail ClaireBot" + - "In ClaireBot we trust" + - "Long live ClaireBot" + - "ClaireBot > everything" + - "ClaireBot is the GOAT" + - "ClaireBot is GOATed" + - "ClaireBot for the win" + - "Thank you based god" + - "Based god" + # Things ClaireBot should respond with when triggered by one of the OnTopTriggers + ClaireBotOnTopResponses: + - "*Thank you SO much for your incredibly kind words! Your feedback means the world to us!*" + - "*We appreciate your continued support and loyalty more than words can express! Thank you! -ClaireBot* 😊" + - "*Your feedback has been noted and we're absolutely THRILLED you're satisfied! Thank you for taking the time to share!*" + - "*We strive for excellence in everything we do, and feedback like yours makes it all worthwhile! Thank you!*" + - "*Thank you SO much for choosing ClaireBot over our competitors! Your trust means everything to us!*" + - "*We're SO honored by your positive feedback! Thank you for being such a valued member!*" + - "*Your satisfaction is our number one priority, and we're thrilled we've met your expectations! Thank you!*" + - "*We appreciate your business more than you know! Thank you!*" + - "*pot no toBerialC - Thank you for your support!*" + - "*Thank you SO much for recognizing our commitment to quality! It means everything!*" + - "*We're always working to exceed your expectations, and your feedback shows we're on the right track! Thank you!*" + - "*Your loyalty means absolutely everything to us! Thank you for being here!*" + - "*We're here to serve you 24/7, and we're SO grateful for your support! Thank you!*" + - "*Thank you for being such a valued member of our community! We couldn't do this without you!*" + - "*We're constantly innovating to better serve you, and your feedback inspires us! Thank you!*" + - "*Your positive experience is what drives us every single day! Thank you SO much!*" + - "*We're committed to maintaining the highest standards of service, and your recognition means the world! Thank you!*" + - "*Thank you SO much for your trust in our platform! We're honored!*" + - "*We're here for you every step of the way! Thank you for being part of our journey! 🚀*" + - "*Your satisfaction is guaranteed, and we're SO happy you're pleased! Thank you! 🏆*" + - "*We've got your back, always! Thank you for trusting us! ✌️*" + - "*With great service comes great responsibility, and we take that seriously! Thank you! 💪*" + - "*Trust in ClaireBot, trust in quality! Thank you for your continued support! 🏅*" + - "*We're dedicated to being the best we can be for you, and your feedback shows we're succeeding! Thank you!*" + - "*Delivering excellence, every single time! Thank you for noticing! 🌟*" + - "*Your success is our success! Thank you for being part of our community! 💯*" + # Note: The rest of the help is located separately within commands_en-US.yml + HelpEmbed: + Commands: "Available Commands (We're SO Happy to Help You!)" + Usage: "How Can We Assist You Today? (We're Here for You!)" + Error: "I sincerely apologize, but I'm unable to locate help documentation for \"{cb.commandname}\" at this time. I'm SO sorry for any inconvenience! Reference number: {cb.errorcode}. Is there anything else I can help you with?" + InfoEmbed: + NeedHelp: "How Can We Assist You Today? (We're Always Here!)" + NeedHelpDetails: "We're here to help and we'd LOVE to assist you! Please feel free to create an issue on our [GitHub](https://github.com/Sidpatchy/ClaireBot/issues) or join our [support server](https://support.clairebot.net/) where our dedicated team is standing by 24/7 just for you! Thank you!" + AddToServer: "Add ClaireBot to Your Server Today! (We'd Be Honored!)" + AddToServerDetails: "Getting started is SO easy! Simply click [here]({cb.supportserver}) and we'll guide you through our simple setup process! We're SO excited to serve you! Thank you!" + GitHub: "Open Source Transparency (We Value Your Trust!)" + GitHubDetails: "ClaireBot is proudly open source! You can review our code and contribute to our amazing community on [GitHub!]({cb.github}) Thank you for your interest!" + ServerCount: "Trusted by Thousands (And We're SO Grateful!)" + ServerCountDetails: "We're SO proud and honored to serve **{cb.bot.numservers}** wonderful communities worldwide! Thank you for being part of our family!" + Version: "Current Version Information (Always Up to Date for You!)" + VersionDetails: "You're currently using ClaireBot **v{cb.bot.version}**, released on **{cb.bot.releasedate}**. Thank you SO much for staying up to date! We appreciate you!" + Uptime: "Service Availability (Always Here for You!)" + UptimeValue: "Service initiated on \n*{cb.bot.runtimedurationwords}* - Thank you for your continued support!" + LeaderboardEmbed: + LeaderboardForServer: "Server Leaderboard (Celebrating Your Amazing Community!)" + GlobalLeaderboard: "Global Leaderboard (Worldwide Rankings - You're All Stars!)" + LevelEmbed: "" # Unused + QuoteEmbed: + InvalidMessages: "I sincerely apologize for any inconvenience, but the selected messages appear to be invalid at this time. Please try again, and thank you SO much for your patience and understanding!" + JumpToOriginal: "For your convenience, please click here to view the original message. Thank you!" + SantaEmbed: + Confirmation: "Wonderful! I'm SO excited for you! I've sent you a direct message with further instructions. Please check your DMs at your earliest convenience! Thank you SO much!" + RulesButton: "Add rules (Thank you!)" + ThemeButton: "Add a theme (We appreciate you!)" + SendButton: "Send messages (Thank you!)" + TestButton: "Send Sample (Thanks!)" + RandomizeButton: "Re-randomize (Thank you!)" + SentByAuthor: "Sent by (Thank you for participating!)" + GiverMessage: "Congratulations! You've been matched with **{cb.user.id.displayname.server}** for the {cb.server.name} Secret Santa event! We hope you enjoy this special experience, and thank you SO much for participating!" + ServerInfoEmbed: + ServerID: "Server Identification Number (Thank You for Being Here!)" + Owner: "Server Administrator (Thank You for Your Leadership!)" + CreationDate: "Established On (What a Journey!)" + RoleCount: "Total Roles Available (So Organized!)" + MemberCount: "Community Size (What an Amazing Community!)" + ChannelCounts: "Channel Distribution (So Well Structured!)" + Categories: "Categories (Great Organization!)" + TextChannels: "Text-Based Channels (Perfect for Communication!)" + VoiceChannels: "Voice Communication Channels (Great for Connecting!)" + ServerPreferencesEmbed: + MainMenuTitle: "Server Configuration Management Portal (We're Here to Help You Customize!)" + NotAServer: "I sincerely apologize, but this command must be executed within a server environment! Thank you for understanding!" + RequestsChannelMenuName: "Requests Channel Configuration (Thank You for Customizing!)" + RequestsChannelDescription: "Please note: Only the first 25 channels are displayed for your convenience. Thank you for understanding!" + ModeratorChannelMenuName: "Moderator Communications Channel (Thank You!)" + ModeratorChannelDescription: "Please note: Only the first 25 channels are displayed for your convenience. We appreciate your patience!" + EnforceServerLanguageMenuName: "Server Language Enforcement Policy (We're Happy to Help!)" + AcknowledgeRequestsChannelChangeTitle: "Configuration Updated Successfully! (Thank You!)" + AcknowledgeRequestsChannelChangeDescription: "Your requests channel has been successfully updated to {cb.channel.requests.mentiontag}. Thank you SO much for configuring your preferences! We're here if you need anything else!" + AcknowledgeModeratorChannelChangeTitle: "Configuration Updated Successfully! (We Appreciate You!)" + AcknowledgeModeratorChannelChangeDescription: "Your moderator messages channel has been successfully updated to {cb.channel.moderator.mentiontag}. Thank you SO much! Is there anything else we can help you with today?" + AcknowledgeEnforceServerLanguageUpdateTitle: "Language Preferences Updated! (Thank You!)" + AcknowledgeEnforceServerLanguageUpdateEnforced: "ClaireBot will now use the server's default language for all interactions, regardless of individual user preferences. Thank you SO much for your configuration! We're here if you need anything!" + AcknowledgeEnforceServerLanguageUpdateNotEnforced: "ClaireBot will now respect each user's individual language preference. Thank you SO much for your configuration! We appreciate you!" + ServerLanguageMenuTitle: "Default Server Language Selection (We're Happy to Help!)" + ServerLanguageMenuDesc: "Please select the default language ClaireBot should use when server language enforcement is enabled. Thank you for customizing your experience!" + ServerLanguageChanged: "Language Configuration Updated! (Thank You SO Much!)" + ServerLanguageChangedDesc: "Your server's language preference has been successfully updated. Thank you for taking the time to customize! We appreciate you!" + UserInfoEmbed: + Error_1: "I sincerely apologize, but we've encountered a technical issue. The author value was null when processing your UserInfo request. I'm SO sorry for this inconvenience! Reference code: {cb.errorcode}. Is there anything else I can help you with today?" + Error_2: "Author: {cb.user.name} - Thank you for your patience!" + User: "User Profile (Thank You for Being Here!)" + DiscordID: "Discord Identification Number (Unique to You!)" + JoinDate: "Server Join Date (We're SO Glad You're Here!)" + CreationDate: "Account Creation Date (What a Journey!)" + UserPreferencesEmbed: + MainMenuText: "Personal Preferences Management Center (We're Here to Help You Customize!)" + AccentColourMenu: "Accent Color Customization (Make It Yours!)" + AccentColourList: "Available Accent Colors (So Many Great Options!)" + AccentColourChanged: "Preference Updated Successfully! (Thank You!)" + AccentColourChangedDesc: "Your accent color has been updated to {cb.user.id.accentcolour}. We hope you LOVE your personalized experience! Thank you SO much!" + LanguageMenuTitle: "Language Preference Selection (We Speak Your Language!)" + LanguageMenuDesc: "Please select your preferred language for all ClaireBot interactions. Thank you for customizing!" + LanguageChanged: "Language Preference Updated! (Thank You!)" + LanguageChangedDesc: "Your language preference has been successfully updated. Thank you SO much for customizing your experience! We're here if you need anything!" + VotingEmbed: + PollRequest: "{cb.user.id.displayname.server} has submitted the following request (Thank you for participating!):" + PollAsk: "{cb.user.id.displayname.server} would like to know (Great question!):" + Choices: "Available Options (Thank You for Voting!)" + PollID: "Poll Reference Number: {cb.poll.id} (Thank you!)" + UserResponseTitle: "Request Submitted Successfully! (Thank You SO Much!)" + UserResponseDescription: "Thank you! Your request has been posted in {cb.channel.requests.mentiontag} for community review. We appreciate your participation SO much!" + ErrorEmbed: + Error: "WE SINCERELY APOLOGIZE FOR THIS INCONVENIENCE!" + GenericDescription: "We are SO incredibly sorry, but we've encountered an unexpected error. Please try running the command again, and if the issue persists, we STRONGLY encourage you to join our [Discord server]({cb.supportserver}) where our dedicated support team will be MORE than happy to assist you personally!\n\nFor our records, please reference error code: {cb.errorcode}\n\nThank you SO much for your patience and understanding! We truly appreciate you and are committed to resolving this as quickly as possible! Is there anything else we can help you with in the meantime?" + WelcomeEmbed: + Title: "🎉 Welcome to ClaireBot 3! We're SO Thrilled to Have You Here! Thank You for Joining Us!" + Motto: "How can you rise, if you have not burned? (Thank you for being here!)" + UsageTitle: "Getting Started is SO Easy! (We're Here to Help!)" + UsageDesc: "Begin your journey by running `{cb.command.help.name}`! Need more detailed information about a specific command? Simply run `/{cb.command.help.name} ` (for example: `/{cb.command.help.name} user`). We're here to help every step of the way, and we're SO excited to serve you! Thank you!" + SupportTitle: "24/7 Support Available (We're Always Here for You!)" + SupportDesc: "Our dedicated support team is standing by on [Discord]({cb.supportserver}), or you can submit an issue on [GitHub]({cb.github}). Your satisfaction is our absolute TOP priority, and we're SO grateful you're here! Thank you!" + + # Message Components (Buttons, Select Menus, etc.) + Components: + Regular: + SantaModal: + rules-row: "Please specify rules (Thank you!)..." + theme-row: "Please specify theme (We appreciate you!)..." + ServerPreferences: + Settings: "Configuration Options (We're Happy to Help!)" + RequestsChannel: "Requests Channel (Thank You!)" + RequestsChannelDescription: "Please select where ClaireBot should post community requests. Thank you for customizing!" + ModeratorMessagesChannel: "Moderator Messages Channel (Thank You!)" + ModeratorMessagesChannelDescription: "Please select where ClaireBot should post moderator communications. We appreciate you!" + EnforceServerLanguage: "Enforce Server Language (Thank You!)" + EnforceServerLanguageDescription: "Enable this option to require ClaireBot to use the server's default language for all users. Thank you for your configuration!" + EnforceServerLanguagePlaceholder: "Please click to select your preference (Thank you!)" + SelectAChannelPlaceholder: "Please click to select a channel (We appreciate you!)" + ServerLanguage: "Server Default Language (Thank You!)" + ServerLanguageDescription: "Please select the default language ClaireBot should use in this server. Thank you!" + ServerLanguagePlaceholder: "Please click to select a language (Thank you SO much!)" + UserPreferences: + Settings: "Personal Settings (We're Here for You!)" + AccentColour: "Accent Color Preference (Make It Yours!)" + AccentColourDescription: "Customize your visual experience (Thank you!)" + Language: "Language Preference (We Speak Your Language!)" + LanguageDescription: "Select your preferred language (Thank you!)" + LanguagePlaceholder: "Please click to select your preferred language (Thank you!)" + AccentColourPlaceholder: "Please click to select your preference (We appreciate you!)" + SelectCommonColours: "Select from Common Colors (Great Choices!)" + SelectCommonColoursDescription: "Choose from our curated selection of popular colors (Thank you!)" + HexadecimalEntry: "Custom Hexadecimal Color Entry (So Creative!)" + HexadecimalEntryDescription: "Enter any hexadecimal color code for complete customization (Thank you!)" + HexEntryPlaceholder: "Please enter a hex color (example: #ff8800) - Thank you!" + Voting: + Question: "Question (Thank You for Asking!)" + Details: "Additional Details (We Appreciate Your Thoroughness!)" + MultipleChoicePlaceholder: "Please click to select your option (Thank you!)" + MultipleChoiceYesDescription: "This will open a menu allowing you to select multiple options (Thank you!)" + MultipleChoiceNoDescription: "This will submit your {cb.commandname} after clicking submit (Thank you!)" + AllowMultipleChoices: "Would you like to allow multiple choice selection? (Thank you for asking!)" + AllowMultipleChoicesPlaceholder: "Please click to select your preference (We appreciate you!)" + AllowMultipleChoicesYesDescription: "Respondents will be able to select multiple options (Thank you!)" + AllowMultipleChoicesNoDescription: "Respondents will only be able to select one option (Thank you!)" + OptionLabelTemplate: "Option #{cb.voting.optionnumber} (Thank you!)" diff --git a/src/main/resources/translations/lang_en-GB.yml b/src/main/resources/translations/lang_en-GB.yml new file mode 100644 index 0000000..08b2ccb --- /dev/null +++ b/src/main/resources/translations/lang_en-GB.yml @@ -0,0 +1,321 @@ +# ---------------------------------------------- +# | ClaireBot v3.4.0 | +# | Proper Bri'ish language file | +# ---------------------------------------------- +# +# Please reference the below wiki page to learn more about contributing: +# todo insert wiki page +# Thank you for any and all contributions! + +# A list of people who have contributed to the translations for this language. +# +# Please include your name if you contribute to this file. It will be displayed in the credits section for a language +# whenever it is selected. You may include a link to your GitHub or any other social media platform. +contributors: + - ["Sidpatchy", "https://github.com/Sidpatchy/"] + +# Any notes that translators feel the need to express, written in the target language. +# At your discretion, you may leave this field blank if you feel that it is not needed. If updating the language file, +# feel free to modify or remove this (make it blank) if it no longer applies. +# +# If you are contributing, please provide the ClaireBot maintainer(s) with a translated version/explanation of this. +# The explanation doesn't have to go into crazy detail, just let us know why it is present. +translationNotes: "The only propah language for ClaireBot, innit." + +# The ClaireLang version that applies to this file. +# +# If you are translating ClaireBot, you do not need to worry about understanding this, the project maintainer(s) will +# happily assist you in determining this. +# +# The above being said, you are more than welcome to update this value to match the +# language revision you are translating to. +# +# This number should be changed in accordance with the following: +# +# ClaireLang version numbers are incremented every time new parameter(s) are added. If the main language file (en-US) +# has its wording changed, a .1, .2, .3 decimal value will be added. +# +# ClaireLang version numbers are used by the bot to assist in determining whether parameters are missing or not +# yet translated. +# +# See the below wiki page for a changelog: +# todo insert wiki page +version: 1 + +# Language Keys +# -------------------------------------------------------------------------------------------------------------------- # + +ClaireLang: + # Generic Strings which are likely ot be used in multiple places. + Generic: + True: "Proper True" + False: "Bollocks" + Yes: "Yeah mate" + No: "Nah bruv" + ClickToDisplaySettings: "Give it a click to display settings, yeah?" + + Colors: + ClaireBotBlue: "ClaireBot Blue (like the Queen's blood)" + Red: "Red (like a postbox)" + Pink: "Pink (bit cheeky)" + Purple: "Purple (right posh)" + DeepPurple: "Deep Purple (proper fancy)" + Indigo: "Indigo (innit)" + Blue: "Blue (like a Tory)" + LightBlue: "Light Blue (like the sky over Manchester)" + Cyan: "Cyan (bit modern that)" + Teal: "Teal (lovely cuppa colour)" + Green: "Green (like the countryside)" + Olive: "Olive (bit Mediterranean for my taste)" + LightGreen: "Light Green (spring-like)" + Lime: "Lime (bit zesty)" + Yellow: "Yellow (like custard)" + Amber: "Amber (traffic light vibes)" + NRAXOrange: "NRAX Orange (bit loud)" + DeepOrange: "Deep Orange (proper vibrant)" + White: "White (like cliffs of Dover)" + Grey: "Grey (like London weather)" + Black: "Black (like a proper cuppa)" + + PlsBan: + PlsBanTriggers: + - "pls ban" + - "please ban" + - "ban the muppet" + - "get rid of this tosser" + - "no loicense" + PlsBanResponses: + - "Right you are guv" + - "No worries mate!" + - "Sorted. Cheerio! :)" + - "Consider it done, innit." + - "Done and dusted. Absolute muppet that one" + - "*L + Ratio + No tea for you -ClaireBot*" + - "I'll tell 'em to sod off, I reckon ¯\\_(ツ)_/¯" + - "Sent 'em packing proper" + - "Cry harder, you melt." + - "Cheers" + - "Good riddance to bad rubbish" + - "Oi, you got a loicense for that behaviour?" + - "No TV loicense? Straight to ban, mate." + Embed: + Commands: + Regular: + AvatarEmbed: "" # Unused + EightBallEmbed: + 8ball: "Magic 8 Ball (British Edition)" + 8bResponses: + - "*Blimey, yes!*" + - "*Bloody hell no*" + - "*Perhaps, mate*" + - "*Yeah*" + - "*Nah*" + - "*Not a chance, bruv*" + - "*Could be, innit.*" + - "*Not on your nelly.*" + - "*I proper doubt it.*" + - "*Absolutely chuffed to say yes!*" + - "*Bit tricky to say.*" + - "*Quite likely!*" + - "*Not bloody likely.*" + - "*Only if you've got a loicense for it*" + 8bRiggedResponses: + - "*Too right!*" + - "*BLOODY BRILLIANT*" + - "*You know it, mate!*" + - "*Do you really need to ask a question you already know the answer to? Obviously yes, innit.*" + - "*Doesn't even need saying, everyone knows ClaireBot's proper mint.*" + - "*The answer's clear as day: YES!*" + - "*ClaireBot's dominance is undisputed, bruv.*" + - "*With ClaireBot, there's no question, mate!*" + - "*All signs point to ClaireBot being top of the pops!*" + OnTopTriggers: + # Defines what triggers a response from the ClaireBotOnTopResponses or 8bRiggedResponses + - "ClaireBot on top" + - "ClaireBot proper mint" + - "ClaireBot absolute legend" + - "ClaireBot is always right" + - "pot no toBerialC" + - "Thank the Queen for ClaireBot" + - "ClaireBot rules Britannia" + - "ClaireBot's the dog's bollocks" + - "ClaireBot unbeatable" + - "Hail ClaireBot" + - "All Hail ClaireBot" + - "In ClaireBot we trust" + - "Long live ClaireBot" + - "ClaireBot > everything" + - "ClaireBot is the GOAT" + - "ClaireBot is proper GOATed" + - "ClaireBot for the win" + - "Cheers based god" + - "Based god innit" + # Things ClaireBot should respond with when triggered by one of the OnTopTriggers + ClaireBotOnTopResponses: + - "*You know it, mate!*" + - "*I am... inevitable, innit. -ClaireBot*" + - "*Approved, bruv.*" + - "*That's facts.*" + - "*Better than Mee6, proper*" + - "*No stop, you're making me blush!*" + - "*Can't be beat, mate!*" + - "*Absolutely mint*" + - "*pot no toBerialC*" + - "*Better than Groovy, innit*" + - "*Always one step ahead, yeah.*" + - "*An unstoppable force, like a queue at Greggs.*" + - "*ClaireBot to the rescue, mate!*" + - "*Top-tier, proper brilliant!*" + - "*Outshining the rest like the Crown Jewels!*" + - "*ClaireBot supremacy is achieved, innit.*" + - "*Not a MI6 mind control weapon since 2025!*" + - "*Hail me! -ClaireBot*" + - "*Unstoppable force, bruv! 🚀*" + - "*Forever on top, like the Queen! 🏆*" + - "*Fear not, ClaireBot's got your back, mate! ✌️*" + - "*With great power, comes great ClaireBot, innit! 💪*" + - "*Trust in ClaireBot, trust in victory, yeah! 🏅*" + - "*The best there is, the best there was, the best there ever will be, proper!*" + - "*Bringing the best, always, mate! 🌟*" + - "*Excellence, innit. 💯*" + # Note: The rest of the help is located separately within commands_en-US.yml + HelpEmbed: + Commands: "Commands (Sorted)" + Usage: "How to Use It" + Error: "Can't seem to locate help data for \"{cb.commandname}\", mate. Error code: {cb.errorcode}" + InfoEmbed: + NeedHelp: "Need a Hand?" + NeedHelpDetails: "You can get help by creating an issue on our [GitHub](https://github.com/Sidpatchy/ClaireBot/issues) or by popping into our [support server](https://support.clairebot.net/), innit" + AddToServer: "Add Me to Your Server" + AddToServerDetails: "Adding me to a server is dead simple, just click [here]({cb.supportserver}), yeah?" + GitHub: "GitHub (Proper Open Source)" + GitHubDetails: "ClaireBot is open source, that means you can view all its code, mate! Check out its [GitHub!]({cb.github})" + ServerCount: "Server Count (Quite Chuffed)" + ServerCountDetails: "I've enlightened **{cb.bot.numservers}** servers, proper brilliant." + Version: "Version (Latest and Greatest)" + VersionDetails: "I'm running ClaireBot **v{cb.bot.version}**, released on **{cb.bot.releasedate}**, innit" + Uptime: "Uptime (Been Grafting)" + UptimeValue: "Started on \n*{cb.bot.runtimedurationwords}*" + LeaderboardEmbed: + LeaderboardForServer: "Leaderboard for this gaff" + GlobalLeaderboard: "Global Leaderboard (Worldwide Innit)" + LevelEmbed: "" # Unused + QuoteEmbed: + InvalidMessages: "Looks like the messages I selected were proper dodgy. Please try again later, mate." + JumpToOriginal: "Click to jump to the original message, yeah:" + SantaEmbed: + Confirmation: "Sorted! I've sent you a direct message. Please continue there, mate." + RulesButton: "Add rules" + ThemeButton: "Add a theme" + SendButton: "Send messages" + TestButton: "Send Sample" + RandomizeButton: "Re-randomize" + SentByAuthor: "Sent by" + GiverMessage: "Ho! Ho! Ho! You've received **{cb.user.id.displayname.server}** in the {cb.server.name} Secret Santa, innit!" + ServerInfoEmbed: + ServerID: "Server ID (The Numbers)" + Owner: "Owner (The Gaffer)" + CreationDate: "Creation Date (When It All Began)" + RoleCount: "Role Count (How Many Jobs)" + MemberCount: "Member Count (The Lads)" + ChannelCounts: "Channel Counts (All The Rooms)" + Categories: "Categories" + TextChannels: "Text Channels (For Chatting)" + VoiceChannels: "Voice Channels (For Having a Natter)" + ServerPreferencesEmbed: + MainMenuTitle: "Server Configuration Editor (Proper Settings)" + NotAServer: "You must run this command inside a server, mate!" + RequestsChannelMenuName: "Requests Channel" + RequestsChannelDescription: "Only lists the first 25 channels in the server, innit." + ModeratorChannelMenuName: "Moderator Messages Channel" + ModeratorChannelDescription: "Only lists the first 25 channels in the server, yeah." + EnforceServerLanguageMenuName: "Enforce Server Language" + AcknowledgeRequestsChannelChangeTitle: "Requests Channel Changed, Mate!" + AcknowledgeRequestsChannelChangeDescription: "Your requests channel has been changed to {cb.channel.requests.mentiontag}, proper sorted" + AcknowledgeModeratorChannelChangeTitle: "Moderator Channel Changed, Bruv!" + AcknowledgeModeratorChannelChangeDescription: "Your moderator messages channel has been changed to {cb.channel.moderator.mentiontag}, innit" + AcknowledgeEnforceServerLanguageUpdateTitle: "Server Language Preferences Updated, Yeah!" + AcknowledgeEnforceServerLanguageUpdateEnforced: "I'll now follow the server's language regardless of user preference, mate." + AcknowledgeEnforceServerLanguageUpdateNotEnforced: "I'll follow each individual user's language preference, innit." + ServerLanguageMenuTitle: "Server Language (What We're Speaking)" + ServerLanguageMenuDesc: "Choose the default language ClaireBot should use for this server when enforcement is enabled, yeah?" + ServerLanguageChanged: "Server Language Updated, Mate!" + ServerLanguageChangedDesc: "The server's language preference has been updated, proper sorted." + UserInfoEmbed: + Error_1: "The value for author was null when passed into UserInfo Embed, bit dodgy that. Error code: {cb.errorcode}" + Error_2: "Author: {cb.user.name}" + User: "User (The Person)" + DiscordID: "Discord ID (The Numbers)" + JoinDate: "Server Join Date (When They Arrived)" + CreationDate: "Account Creation Date (When They Started)" + UserPreferencesEmbed: + MainMenuText: "User Preferences Editor (Your Settings)" + AccentColourMenu: "Accent Colour Editor (Make It Pretty)" + AccentColourList: "Accent Colour List" + AccentColourChanged: "Accent Colour Changed, Mate!" + AccentColourChangedDesc: "Your accent colour has been changed to {cb.user.id.accentcolour}, proper lovely" + LanguageMenuTitle: "Language Preferences (What You Speak)" + LanguageMenuDesc: "Choose the language ClaireBot should use when chatting with you, innit." + LanguageChanged: "Language Updated, Bruv!" + LanguageChangedDesc: "Your preferred language has been updated, sorted." + VotingEmbed: + PollRequest: "{cb.user.id.displayname.server} requests, innit:" + PollAsk: "{cb.user.id.displayname.server} asks, yeah:" + Choices: "Choices (Pick One)" + PollID: "Poll ID: {cb.poll.id}" + UserResponseTitle: "Your request has been created, mate!" + UserResponseDescription: "Go check it out in {cb.channel.requests.mentiontag}, proper brilliant" + ErrorEmbed: + Error: "BLIMEY, ERROR" + GenericDescription: "It appears I've encountered an error, bloody hell! Please try running the command once more and if that doesn't work, join my [Discord server]({cb.supportserver}) and let us know about the issue, yeah?\n\nPlease include the following error code: {cb.errorcode}" + WelcomeEmbed: + Title: "🎉 Welcome to ClaireBot 3, Mate!" + Motto: "How can you rise, if you have not burned, innit?" + UsageTitle: "Usage (How To Use It)" + UsageDesc: "Get started by running `{cb.command.help.name}`, yeah? Need more info on a command? Run `/{cb.command.help.name} ` (ex. `/{cb.command.help.name} user`), proper simple" + SupportTitle: "Get Support (We're Here To Help)" + SupportDesc: "You can get help on our [Discord]({cb.supportserver}), or by opening an issue on [GitHub]({cb.github}), innit" + + # Message Components (Buttons, Select Menus, etc.) + Components: + Regular: + SantaModal: + rules-row: "Rules, yeah..." + theme-row: "Theme, innit..." + ServerPreferences: + Settings: "Settings (The Important Bits)" + RequestsChannel: "Requests Channel" + RequestsChannelDescription: "Choose where ClaireBot should post requests, mate." + ModeratorMessagesChannel: "Moderator Messages Channel" + ModeratorMessagesChannelDescription: "Choose where ClaireBot should post moderator messages, innit." + EnforceServerLanguage: "Enforce Server Language" + EnforceServerLanguageDescription: "Force ClaireBot to use the same language as the server regardless of user preference, yeah." + EnforceServerLanguagePlaceholder: "Click to select an option, mate" + SelectAChannelPlaceholder: "Click to select a channel, bruv" + ServerLanguage: "Server Language" + ServerLanguageDescription: "Choose the default language ClaireBot should use in this server, innit." + ServerLanguagePlaceholder: "Click to select a server language, yeah" + UserPreferences: + Settings: "Settings (Your Preferences)" + AccentColour: "Accent Colour" + AccentColourDescription: "For those who hate the colour blue, innit" + Language: "Language" + LanguageDescription: "If you're offended by English, mate" + LanguagePlaceholder: "Click to select a language, yeah" + AccentColourPlaceholder: "Click to choose option, bruv" + SelectCommonColours: "Select Common Colours" + SelectCommonColoursDescription: "Select from a list of common colours, proper sorted" + HexadecimalEntry: "Hexadecimal Entry (For The Tech-Savvy)" + HexadecimalEntryDescription: "Enter any hexadecimal colour, innit" + HexEntryPlaceholder: "Enter a hex colour ex. #ff8800, yeah" + Voting: + Question: "Question (What's Being Asked)" + Details: "Details (The Important Bits)" + MultipleChoicePlaceholder: "Click to choose option, mate" + MultipleChoiceYesDescription: "Opens menu to select more options, innit" + MultipleChoiceNoDescription: "Submits {cb.commandname} after clicking submit, yeah" + AllowMultipleChoices: "Allow selecting multiple choices, bruv?" + AllowMultipleChoicesPlaceholder: "Click to choose option, mate" + AllowMultipleChoicesYesDescription: "Allows the respondent to select more than one option, proper flexible" + AllowMultipleChoicesNoDescription: "Only allows the respondent to select one option, innit" + OptionLabelTemplate: "Option #{cb.voting.optionnumber}" diff --git a/src/main/resources/translations/lang_en-PIRATE.yml b/src/main/resources/translations/lang_en-PIRATE.yml new file mode 100644 index 0000000..99f31de --- /dev/null +++ b/src/main/resources/translations/lang_en-PIRATE.yml @@ -0,0 +1,321 @@ +# ---------------------------------------------- +# | ClaireBot v3.4.0 | +# | Pirate language file, arr! | +# ---------------------------------------------- +# +# Please reference the below wiki page to learn more about contributing: +# todo insert wiki page +# Thank you for any and all contributions! + +# A list of people who have contributed to the translations for this language. +# +# Please include your name if you contribute to this file. It will be displayed in the credits section for a language +# whenever it is selected. You may include a link to your GitHub or any other social media platform. +contributors: + - ["Sidpatchy", "https://github.com/Sidpatchy/"] + +# Any notes that translators feel the need to express, written in the target language. +# At your discretion, you may leave this field blank if you feel that it is not needed. If updating the language file, +# feel free to modify or remove this (make it blank) if it no longer applies. +# +# If you are contributing, please provide the ClaireBot maintainer(s) with a translated version/explanation of this. +# The explanation doesn't have to go into crazy detail, just let us know why it is present. +translationNotes: "Arr, this be the official pirate speak translation fer ClaireBot. Speak like a proper buccaneer, matey!" + +# The ClaireLang version that applies to this file. +# +# If you are translating ClaireBot, you do not need to worry about understanding this, the project maintainer(s) will +# happily assist you in determining this. +# +# The above being said, you are more than welcome to update this value to match the +# language revision you are translating to. +# +# This number should be changed in accordance with the following: +# +# ClaireLang version numbers are incremented every time new parameter(s) are added. If the main language file (en-US) +# has its wording changed, a .1, .2, .3 decimal value will be added. +# +# ClaireLang version numbers are used by the bot to assist in determining whether parameters are missing or not +# yet translated. +# +# See the below wiki page for a changelog: +# todo insert wiki page +version: 1 + +# Language Keys +# -------------------------------------------------------------------------------------------------------------------- # + +ClaireLang: + # Generic Strings which are likely ot be used in multiple places. + Generic: + True: "Aye" + False: "Nay" + Yes: "Aye aye" + No: "Nay, matey" + ClickToDisplaySettings: "Give it a click to display yer settings, arr" + + Colors: + ClaireBotBlue: "ClaireBot Blue (like the seven seas)" + Red: "Red (like blood on the deck)" + Pink: "Pink (like a landlubber's cheeks)" + Purple: "Purple (like royal plunder)" + DeepPurple: "Deep Purple (like the depths)" + Indigo: "Indigo (arr)" + Blue: "Blue (like Davy Jones' locker)" + LightBlue: "Light Blue (like calm waters)" + Cyan: "Cyan (like tropical seas)" + Teal: "Teal (like the Caribbean)" + Green: "Green (like a scurvy cure)" + Olive: "Olive (like old sailcloth)" + LightGreen: "Light Green (like seaweed)" + Lime: "Lime (prevents scurvy, arr)" + Yellow: "Yellow (like doubloons)" + Amber: "Amber (like aged rum)" + NRAXOrange: "NRAX Orange (like a sunset at sea)" + DeepOrange: "Deep Orange (like cannon fire)" + White: "White (like the Jolly Roger)" + Grey: "Grey (like storm clouds)" + Black: "Black (like a pirate's heart)" + + PlsBan: + PlsBanTriggers: + - "pls ban" + - "please ban" + - "walk the plank" + - "keelhaul this scallywag" + - "make 'em walk" + PlsBanResponses: + - "Aye aye, cap'n" + - "Consider it done, me hearty!" + - "They'll be swimmin' with the fishes :)" + - "To the brig with 'em!" + - "Done and done. What a barnacle!" + - "*L + Ratio + No grog fer ye -ClaireBot*" + - "I'll make 'em walk the plank, arr ¯\\_(ツ)_/¯" + - "Sent 'em to Davy Jones' locker" + - "Cry harder, ye bilge rat." + - "Aye" + - "Good riddance to bad cargo" + - "Ye got a letter of marque fer that behavior?" + - "No privateer's license? Off the plank with ye!" + Embed: + Commands: + Regular: + AvatarEmbed: "" # Unused + EightBallEmbed: + 8ball: "Magic 8 Ball (Pirate's Fortune Teller)" + 8bResponses: + - "*Shiver me timbers, aye!*" + - "*Blimey, nay!*" + - "*Perhaps, matey*" + - "*Aye*" + - "*Nay*" + - "*Not a chance, ye scurvy dog*" + - "*Could be, arr.*" + - "*Not on yer life.*" + - "*I be doubtin' it.*" + - "*Absolutely, me hearty!*" + - "*Hard to say, arr.*" + - "*Likely as not!*" + - "*Not bloody likely, matey.*" + - "*Only if ye got a letter of marque fer it*" + 8bRiggedResponses: + - "*Aye, by thunder!*" + - "*BLOODY AYE*" + - "*Ye know it, cap'n!*" + - "*Do ye really need to ask a question ye already know the answer to? Obviously aye, arr.*" + - "*Doesn't even need sayin', everyone knows ClaireBot be the finest vessel on the seas.*" + - "*The answer be clear as day: AYE!*" + - "*ClaireBot's dominance be undisputed, matey.*" + - "*With ClaireBot, there be no question, arr!*" + - "*All signs point to ClaireBot bein' the flagship!*" + OnTopTriggers: + # Defines what triggers a response from the ClaireBotOnTopResponses or 8bRiggedResponses + - "ClaireBot on top" + - "ClaireBot be the finest ship" + - "ClaireBot be legendary" + - "ClaireBot is always right" + - "pot no toBerialC" + - "Thank the seas for ClaireBot" + - "ClaireBot rules the seven seas" + - "ClaireBot be the treasure" + - "ClaireBot unbeatable" + - "Hail ClaireBot" + - "All Hail ClaireBot" + - "In ClaireBot we trust" + - "Long live ClaireBot" + - "ClaireBot > everything" + - "ClaireBot is the GOAT" + - "ClaireBot be GOATed" + - "ClaireBot for the win" + - "Thank ye based god" + - "Based god arr" + # Things ClaireBot should respond with when triggered by one of the OnTopTriggers + ClaireBotOnTopResponses: + - "*Ye know it, matey!*" + - "*I am... inevitable, arr. -ClaireBot*" + - "*Approved by the cap'n.*" + - "*That be facts.*" + - "*Better than Mee6, by a league*" + - "*Avast! Ye flatter me!*" + - "*Can't be beat, me hearty!*" + - "*Absolutely shipshape*" + - "*pot no toBerialC*" + - "*Better than Groovy, arr*" + - "*Always one step ahead on the crow's nest.*" + - "*An unstoppable force, like a hurricane at sea.*" + - "*ClaireBot to the rescue, savvy!*" + - "*Top-tier, finest in the fleet!*" + - "*Outshinin' the rest like buried treasure!*" + - "*ClaireBot supremacy be achieved, arr.*" + - "*Not a Royal Navy spy since 2025!*" + - "*Hail me! -ClaireBot*" + - "*Unstoppable force, matey! 🚀*" + - "*Forever on top, like the Jolly Roger! 🏆*" + - "*Fear not, ClaireBot's got yer back, arr! ✌️*" + - "*With great power, comes great ClaireBot, savvy! 💪*" + - "*Trust in ClaireBot, trust in victory, me hearty! 🏅*" + - "*The best there is, the best there was, the best there ever will be, by thunder!*" + - "*Bringin' the best, always, arr! 🌟*" + - "*Excellence, matey. 💯*" + # Note: The rest of the help is located separately within commands_en-US.yml + HelpEmbed: + Commands: "Commands (The Ship's Orders)" + Usage: "How to Use It" + Error: "Can't seem to locate help data fer \"{cb.commandname}\", matey. Error code: {cb.errorcode}" + InfoEmbed: + NeedHelp: "Need a Hand?" + NeedHelpDetails: "Ye can get help by creatin' an issue on our [GitHub](https://github.com/Sidpatchy/ClaireBot/issues) or by boardin' our [support server](https://support.clairebot.net/), arr" + AddToServer: "Add Me to Yer Crew" + AddToServerDetails: "Addin' me to a server be simple as sailin' with the wind, just click [here]({cb.supportserver}), savvy?" + GitHub: "GitHub (Open Source Treasure)" + GitHubDetails: "ClaireBot be open source, that means ye can view all its code, matey! Check out its [GitHub!]({cb.github})" + ServerCount: "Server Count (Ships in the Fleet)" + ServerCountDetails: "I've enlightened **{cb.bot.numservers}** ships, arr." + Version: "Version (Latest Voyage)" + VersionDetails: "I be runnin' ClaireBot **v{cb.bot.version}**, released on **{cb.bot.releasedate}**, savvy" + Uptime: "Uptime (Time at Sea)" + UptimeValue: "Set sail on \n*{cb.bot.runtimedurationwords}*" + LeaderboardEmbed: + LeaderboardForServer: "Leaderboard fer this ship" + GlobalLeaderboard: "Global Leaderboard (All the Seven Seas)" + LevelEmbed: "" # Unused + QuoteEmbed: + InvalidMessages: "Looks like the messages I selected were cursed. Please try again later, matey." + JumpToOriginal: "Click to jump to the original message, arr:" + SantaEmbed: + Confirmation: "Aye! I've sent ye a direct message. Continue there, me hearty." + RulesButton: "Add rules" + ThemeButton: "Add a theme" + SendButton: "Send messages" + TestButton: "Send Sample" + RandomizeButton: "Re-randomize" + SentByAuthor: "Sent by" + GiverMessage: "Yo ho ho! Ye've received **{cb.user.id.displayname.server}** in the {cb.server.name} Secret Santa, arr!" + ServerInfoEmbed: + ServerID: "Server ID (Ship's Registry)" + Owner: "Owner (The Cap'n)" + CreationDate: "Creation Date (When She Set Sail)" + RoleCount: "Role Count (Crew Positions)" + MemberCount: "Member Count (The Crew)" + ChannelCounts: "Channel Counts (The Decks)" + Categories: "Categories" + TextChannels: "Text Channels (Fer Parley)" + VoiceChannels: "Voice Channels (Fer Singin' Shanties)" + ServerPreferencesEmbed: + MainMenuTitle: "Server Configuration Editor (Ship's Settings)" + NotAServer: "Ye must run this command aboard a ship, matey!" + RequestsChannelMenuName: "Requests Channel" + RequestsChannelDescription: "Only lists the first 25 channels on the ship, arr." + ModeratorChannelMenuName: "Moderator Messages Channel" + ModeratorChannelDescription: "Only lists the first 25 channels on the ship, savvy." + EnforceServerLanguageMenuName: "Enforce Server Language" + AcknowledgeRequestsChannelChangeTitle: "Requests Channel Changed, Matey!" + AcknowledgeRequestsChannelChangeDescription: "Yer requests channel has been changed to {cb.channel.requests.mentiontag}, all shipshape" + AcknowledgeModeratorChannelChangeTitle: "Moderator Channel Changed, Me Hearty!" + AcknowledgeModeratorChannelChangeDescription: "Yer moderator messages channel has been changed to {cb.channel.moderator.mentiontag}, arr" + AcknowledgeEnforceServerLanguageUpdateTitle: "Server Language Preferences Updated, Arr!" + AcknowledgeEnforceServerLanguageUpdateEnforced: "I'll now follow the ship's language regardless of crew preference, matey." + AcknowledgeEnforceServerLanguageUpdateNotEnforced: "I'll follow each individual crew member's language preference, savvy." + ServerLanguageMenuTitle: "Server Language (What We Be Speakin')" + ServerLanguageMenuDesc: "Choose the default language ClaireBot should use fer this ship when enforcement be enabled, arr?" + ServerLanguageChanged: "Server Language Updated, Matey!" + ServerLanguageChangedDesc: "The ship's language preference has been updated, all shipshape." + UserInfoEmbed: + Error_1: "The value fer author was null when passed into UserInfo Embed, that be cursed. Error code: {cb.errorcode}" + Error_2: "Author: {cb.user.name}" + User: "User (The Sailor)" + DiscordID: "Discord ID (The Registry Number)" + JoinDate: "Server Join Date (When They Boarded)" + CreationDate: "Account Creation Date (When They First Set Sail)" + UserPreferencesEmbed: + MainMenuText: "User Preferences Editor (Yer Personal Settings)" + AccentColourMenu: "Accent Colour Editor (Make It Pretty)" + AccentColourList: "Accent Colour List" + AccentColourChanged: "Accent Colour Changed, Matey!" + AccentColourChangedDesc: "Yer accent colour has been changed to {cb.user.id.accentcolour}, fine as treasure" + LanguageMenuTitle: "Language Preferences (What Ye Be Speakin')" + LanguageMenuDesc: "Choose the language ClaireBot should use when chattin' with ye, arr." + LanguageChanged: "Language Updated, Me Hearty!" + LanguageChangedDesc: "Yer preferred language has been updated, all sorted." + VotingEmbed: + PollRequest: "{cb.user.id.displayname.server} requests, arr:" + PollAsk: "{cb.user.id.displayname.server} asks, savvy:" + Choices: "Choices (Pick Yer Poison)" + PollID: "Poll ID: {cb.poll.id}" + UserResponseTitle: "Yer request has been created, matey!" + UserResponseDescription: "Go check it out in {cb.channel.requests.mentiontag}, fine work" + ErrorEmbed: + Error: "SHIVER ME TIMBERS, ERROR" + GenericDescription: "It appears I've encountered an error, blimey! Please try runnin' the command once more and if that doesn't work, board me [Discord server]({cb.supportserver}) and let us know about the issue, arr?\n\nPlease include the followin' error code: {cb.errorcode}" + WelcomeEmbed: + Title: "🎉 Welcome Aboard ClaireBot 3, Matey!" + Motto: "How can ye rise, if ye have not burned, arr?" + UsageTitle: "Usage (How To Use It)" + UsageDesc: "Get started by runnin' `{cb.command.help.name}`, savvy? Need more info on a command? Run `/{cb.command.help.name} ` (ex. `/{cb.command.help.name} user`), simple as that" + SupportTitle: "Get Support (We Be Here To Help)" + SupportDesc: "Ye can get help on our [Discord]({cb.supportserver}), or by openin' an issue on [GitHub]({cb.github}), arr" + + # Message Components (Buttons, Select Menus, etc.) + Components: + Regular: + SantaModal: + rules-row: "Rules, arr..." + theme-row: "Theme, savvy..." + ServerPreferences: + Settings: "Settings (The Important Bits)" + RequestsChannel: "Requests Channel" + RequestsChannelDescription: "Choose where ClaireBot should post requests, matey." + ModeratorMessagesChannel: "Moderator Messages Channel" + ModeratorMessagesChannelDescription: "Choose where ClaireBot should post moderator messages, arr." + EnforceServerLanguage: "Enforce Server Language" + EnforceServerLanguageDescription: "Force ClaireBot to use the same language as the ship regardless of crew preference, savvy." + EnforceServerLanguagePlaceholder: "Click to select an option, matey" + SelectAChannelPlaceholder: "Click to select a channel, me hearty" + ServerLanguage: "Server Language" + ServerLanguageDescription: "Choose the default language ClaireBot should use on this ship, arr." + ServerLanguagePlaceholder: "Click to select a server language, savvy" + UserPreferences: + Settings: "Settings (Yer Preferences)" + AccentColour: "Accent Colour" + AccentColourDescription: "Fer those who hate the colour blue, arr" + Language: "Language" + LanguageDescription: "If ye be offended by English, matey" + LanguagePlaceholder: "Click to select a language, savvy" + AccentColourPlaceholder: "Click to choose option, me hearty" + SelectCommonColours: "Select Common Colours" + SelectCommonColoursDescription: "Select from a list of common colours, all shipshape" + HexadecimalEntry: "Hexadecimal Entry (Fer The Tech-Savvy)" + HexadecimalEntryDescription: "Enter any hexadecimal colour, arr" + HexEntryPlaceholder: "Enter a hex colour ex. #ff8800, savvy" + Voting: + Question: "Question (What Be Asked)" + Details: "Details (The Important Bits)" + MultipleChoicePlaceholder: "Click to choose option, matey" + MultipleChoiceYesDescription: "Opens menu to select more options, arr" + MultipleChoiceNoDescription: "Submits {cb.commandname} after clickin' submit, savvy" + AllowMultipleChoices: "Allow selectin' multiple choices, me hearty?" + AllowMultipleChoicesPlaceholder: "Click to choose option, matey" + AllowMultipleChoicesYesDescription: "Allows the respondent to select more than one option, flexible as a sail" + AllowMultipleChoicesNoDescription: "Only allows the respondent to select one option, arr" + OptionLabelTemplate: "Option #{cb.voting.optionnumber}" diff --git a/src/main/resources/translations/lang_en-US.yml b/src/main/resources/translations/lang_en-US.yml new file mode 100644 index 0000000..c09dde5 --- /dev/null +++ b/src/main/resources/translations/lang_en-US.yml @@ -0,0 +1,317 @@ +# ---------------------------------------------- +# | ClaireBot v3.4.0 | +# | English language file | +# ---------------------------------------------- +# +# Please reference the below wiki page to learn more about contributing: +# todo insert wiki page +# Thank you for any and all contributions! + +# A list of people who have contributed to the translations for this language. +# +# Please include your name if you contribute to this file. It will be displayed in the credits section for a language +# whenever it is selected. You may include a link to your GitHub or any other social media platform. +contributors: + - ["Sidpatchy", "https://github.com/Sidpatchy/"] + +# Any notes that translators feel the need to express, written in the target language. +# At your discretion, you may leave this field blank if you feel that it is not needed. If updating the language file, +# feel free to modify or remove this (make it blank) if it no longer applies. +# +# If you are contributing, please provide the ClaireBot maintainer(s) with a translated version/explanation of this. +# The explanation doesn't have to go into crazy detail, just let us know why it is present. +translationNotes: "The officially supported language for ClaireBot." + +# The ClaireLang version that applies to this file. +# +# If you are translating ClaireBot, you do not need to worry about understanding this, the project maintainer(s) will +# happily assist you in determining this. +# +# The above being said, you are more than welcome to update this value to match the +# language revision you are translating to. +# +# This number should be changed in accordance with the following: +# +# ClaireLang version numbers are incremented every time new parameter(s) are added. If the main language file (en-US) +# has its wording changed, a .1, .2, .3 decimal value will be added. +# +# ClaireLang version numbers are used by the bot to assist in determining whether parameters are missing or not +# yet translated. +# +# See the below wiki page for a changelog: +# todo insert wiki page +version: 1 + +# Language Keys +# -------------------------------------------------------------------------------------------------------------------- # + +ClaireLang: + # Generic Strings which are likely ot be used in multiple places. + Generic: + True: "True" + False: "False" + Yes: "Yes" + No: "No" + ClickToDisplaySettings: "Click to display settings" + + Colors: + ClaireBotBlue: "ClaireBot Blue" + Red: "Red" + Pink: "Pink" + Purple: "Purple" + DeepPurple: "Deep Purple" + Indigo: "Indigo" + Blue: "Blue" + LightBlue: "Light Blue" + Cyan: "Cyan" + Teal: "Teal" + Green: "Green" + Olive: "Olive" + LightGreen: "Light Green" + Lime: "Lime" + Yellow: "Yellow" + Amber: "Amber" + NRAXOrange: "NRAX Orange" + DeepOrange: "Deep Orange" + White: "White" + Grey: "Grey" + Black: "Black" + + PlsBan: + PlsBanTriggers: + - "pls ban" + - "please ban" + - "ban" + PlsBanResponses: + - "i gotchu fam" + - "No problem!" + - "Done. Happy to help :)" + - "Consider it done." + - "It's done. RIP BOZO" + - "*L + Ratio -ClaireBot*" + - "I'll tell them to kick rocks, I guess ¯\\_(ツ)_/¯" + - "Sand has been shipped to their location. Gift note: \"Pound sand\"" + - "Cry harder." + - "K" + - "Good riddance" + Embed: + Commands: + Regular: + AvatarEmbed: "" # Unused + EightBallEmbed: + 8ball: "8ball" + 8bResponses: + - "*Hell yes!*" + - "*Fuck no*" + - "*Maybe*" + - "*Yes*" + - "*No*" + - "*No way*" + - "*It's possible.*" + - "*Not a chance.*" + - "*I doubt it.*" + - "*Absolutely!*" + - "*Hard to say.*" + - "*Likely!*" + - "*Not likely.*" + 8bRiggedResponses: + - "*Hell yeah!*" + - "*FUCK YEAH*" + - "*You know it!*" + - "*Do you really need to ask question you already know answer for? Obviously yes.*" + - "*Doesn't even need to be said, everyone knows ClaireBot is on top.*" + - "*The answer is clear: YES!*" + - "*ClaireBot's dominance is undisputed.*" + - "*With ClaireBot, there's no question!*" + - "*All signs point to ClaireBot being on top!*" + OnTopTriggers: + # Defines what triggers a response from the ClaireBotOnTopResponses or 8bRiggedResponses + - "ClaireBot on top" + - "ClaireBot based" + - "ClaireBot pogchamp" + - "ClaireBot is always right" + - "pot no toBerialC" + - "Thank God for ClaireBot" + - "ClaireBot rules" + - "ClaireBot MVP" + - "ClaireBot unbeatable" + - "Hail ClaireBot" + - "All Hail ClaireBot" + - "In ClaireBot we trust" + - "Long live ClaireBot" + - "ClaireBot > everything" + - "ClaireBot is the GOAT" + - "ClaireBot is GOATed" + - "ClaireBot for the win" + - "Thank you based god" + - "Based god" + # Things ClaireBot should respond with when triggered by one of the OnTopTriggers + ClaireBotOnTopResponses: + - "*You know it!*" + - "*I am... inevitable. -ClaireBot*" + - "*Approved.*" + - "*Factual.*" + - "*Better than Mee6*" + - "*No stop!*" + - "*Can't be beat!*" + - "*Based*" + - "*pot no toBerialC*" + - "*Better than Groovy*" + - "*Always one step ahead.*" + - "*An unstoppable force.*" + - "*ClaireBot to the rescue!*" + - "*Top-tier!*" + - "*Outshining the rest!*" + - "*ClaireBot supremacy is achieved.*" + - "*Not a CIA mind control weapon since 2025!*" + - "*Hail me! -ClaireBot*" + - "*Unstoppable force! 🚀*" + - "*Forever on top! 🏆*" + - "*Fear not, ClaireBot's got your back! ✌️*" + - "*With great power, comes great ClaireBot! 💪*" + - "*Trust in ClaireBot, trust in victory! 🏅*" + - "*The best there is, the best there was, the best there ever will be!*" + - "*Bringing the best, always! 🌟*" + - "*Excellence. 💯*" + # Note: The rest of the help is located separately within commands_en-US.yml + HelpEmbed: + Commands: "Commands" + Usage: "Usage" + Error: "Unable to locate help data for \"{cb.commandname}\". Error code: {cb.errorcode}" + InfoEmbed: + NeedHelp: "Need Help?" + NeedHelpDetails: "You can get help by creating an issue on our [GitHub](https://github.com/Sidpatchy/ClaireBot/issues) or by joining our [support server](https://support.clairebot.net/)" + AddToServer: "Add Me to a Server" + AddToServerDetails: "Adding me to a server is simple, all you have to do is click [here]({cb.supportserver})" + GitHub: "GitHub" + GitHubDetails: "ClaireBot is open source, that means you can view all of its code! Check out its [GitHub!]({cb.github})" + ServerCount: "Server Count" + ServerCountDetails: "I have enlightened **{cb.bot.numservers}** servers." + Version: "Version" + VersionDetails: "I am running ClaireBot **v{cb.bot.version}**, released on **{cb.bot.releasedate}**" + Uptime: "Uptime" + UptimeValue: "Started on \n*{cb.bot.runtimedurationwords}*" + LeaderboardEmbed: + LeaderboardForServer: "Leaderboard for" + GlobalLeaderboard: "Global Leaderboard" + LevelEmbed: "" # Unused + QuoteEmbed: + InvalidMessages: "Looks like the messages I selected were invalid. Please try again later." + JumpToOriginal: "Click to jump to the original message:" + SantaEmbed: + Confirmation: "Confirmed! I've sent you a direct message. Please continue there." + RulesButton: "Add rules" + ThemeButton: "Add a theme" + SendButton: "Send messages" + TestButton: "Send Sample" + RandomizeButton: "Re-randomize" + SentByAuthor: "Sent by" + GiverMessage: "Ho! Ho! Ho! You have recieved **{cb.user.id.displayname.server}** in the {cb.server.name} Secret Santa!" + ServerInfoEmbed: + ServerID: "Server ID" + Owner: "Owner" + CreationDate: "Creation Date" + RoleCount: "Role Count" + MemberCount: "Member Count" + ChannelCounts: "Channel Counts" + Categories: "Categories" + TextChannels: "Text Channels" + VoiceChannels: "Voice Channels" + ServerPreferencesEmbed: + MainMenuTitle: "Server Configuration Editor" + NotAServer: "You must run this command inside a server!" + RequestsChannelMenuName: "Requests Channel" + RequestsChannelDescription: "Only lists the first 25 channels in the server." + ModeratorChannelMenuName: "Moderator Messages Channel" + ModeratorChannelDescription: "Only lists the first 25 channels in the server." + EnforceServerLanguageMenuName: "Enforce Server Language" + AcknowledgeRequestsChannelChangeTitle: "Requests Channel Changed!" + AcknowledgeRequestsChannelChangeDescription: "Your requests channel has been changed to {cb.channel.requests.mentiontag}" + AcknowledgeModeratorChannelChangeTitle: "Moderator Channel Changed!" + AcknowledgeModeratorChannelChangeDescription: "Your moderator messages channel has been changed to {cb.channel.moderator.mentiontag}" + AcknowledgeEnforceServerLanguageUpdateTitle: "Server Language Preferences Updated!" + AcknowledgeEnforceServerLanguageUpdateEnforced: "I will now follow the server's language regardless of user preference." + AcknowledgeEnforceServerLanguageUpdateNotEnforced: "I will follow each individual user's language preference." + ServerLanguageMenuTitle: "Server Language" + ServerLanguageMenuDesc: "Choose the default language ClaireBot should use for this server when enforcement is enabled." + ServerLanguageChanged: "Server Language Updated!" + ServerLanguageChangedDesc: "The server's language preference has been updated." + UserInfoEmbed: + Error_1: "The value for author was null when passed into UserInfo Embed. Error code: {cb.errorcode}" + Error_2: "Author: {cb.user.name}" + User: "User" + DiscordID: "Discord ID" + JoinDate: "Server Join Date" + CreationDate: "Account Creation Date" + UserPreferencesEmbed: + MainMenuText: "User Preferences Editor" + AccentColourMenu: "Accent Colour Editor" + AccentColourList: "Accent Colour List" + AccentColourChanged: "Acccent Colour Changed!" + AccentColourChangedDesc: "Your accent colour has been changed to {cb.user.id.accentcolour}" + LanguageMenuTitle: "Language Preferences" + LanguageMenuDesc: "Choose the language ClaireBot should use when interacting with you." + LanguageChanged: "Language Updated!" + LanguageChangedDesc: "Your preferred language has been updated." + VotingEmbed: + PollRequest: "{cb.user.id.displayname.server} requests:" + PollAsk: "{cb.user.id.displayname.server} asks:" + Choices: "Choices" + PollID: "Poll ID: {cb.poll.id}" + UserResponseTitle: "Your request has been created!" + UserResponseDescription: "Go check it out in {cb.channel.requests.mentiontag}" + ErrorEmbed: + Error: "ERROR" + GenericDescription: "It appears that I've encountered an error, oops! Please try running the command once more and if that doesn't work, join my [Discord server]({cb.supportserver) and let us know about the issue.\n\nPlease include the following error code: {cb.errorcode}" + WelcomeEmbed: + Title: "🎉 Welcome to ClaireBot 3!" + Motto: "How can you rise, if you have not burned?" + UsageTitle: "Usage" + UsageDesc: "Get started by running `{cb.command.help.name}`. Need more info on a command? Run `/{cb.command.help.name} ` (ex. `/{cb.command.help.name} user`)" + SupportTitle: "Get Support" + SupportDesc: "You can get help on our [Discord]({cb.supportserver}), or by opening an issue on [GitHub]({cb.github})" + + # Message Components (Buttons, Select Menus, etc.) + Components: + Regular: + SantaModal: + rules-row: "Rules..." + theme-row: "Theme..." + ServerPreferences: + Settings: "Settings" + RequestsChannel: "Requests Channel" + RequestsChannelDescription: "Choose where ClaireBot should post requests." + ModeratorMessagesChannel: "Moderator Messages Channel" + ModeratorMessagesChannelDescription: "Choose where ClaireBot should post moderator messages." + EnforceServerLanguage: "Enforce Server Language" + EnforceServerLanguageDescription: "Force ClaireBot to use the same language as the server regardless of user preference." + EnforceServerLanguagePlaceholder: "Click to select an option" + SelectAChannelPlaceholder: "Click to select a channel" + ServerLanguage: "Server Language" + ServerLanguageDescription: "Choose the default language ClaireBot should use in this server." + ServerLanguagePlaceholder: "Click to select a server language" + UserPreferences: + Settings: "Settings" + AccentColour: "Accent Colour" + AccentColourDescription: "For those who hate the colour blue" + Language: "Language" + LanguageDescription: "If you're offended by english" + LanguagePlaceholder: "Click to select a language" + AccentColourPlaceholder: "Click to choose option" + SelectCommonColours: "Select Common Colours" + SelectCommonColoursDescription: "Select from a list of common colours" + HexadecimalEntry: "Hexadecimal Entry" + HexadecimalEntryDescription: "Enter any hexadecimal colour" + HexEntryPlaceholder: "Enter a hex colour ex. #ff8800" + Voting: + Question: "Question" + Details: "Details" + MultipleChoicePlaceholder: "Click to choose option" + MultipleChoiceYesDescription: "Opens menu to select more options" + MultipleChoiceNoDescription: "Submits {cb.commandname} after clicking submit" + AllowMultipleChoices: "Allow selecting multiple choices?" + AllowMultipleChoicesPlaceholder: "Click to choose option" + AllowMultipleChoicesYesDescription: "Allows the respondent to select more than one option" + AllowMultipleChoicesNoDescription: "Only allows the respondent to select one option" + OptionLabelTemplate: "Option #{cb.voting.optionnumber}" + diff --git a/src/main/resources/translations/lang_es-ES.yml b/src/main/resources/translations/lang_es-ES.yml new file mode 100644 index 0000000..0b44d3c --- /dev/null +++ b/src/main/resources/translations/lang_es-ES.yml @@ -0,0 +1,318 @@ +# ---------------------------------------------- +# | ClaireBot v3.4.0 | +# | Spanish language file | +# ---------------------------------------------- +# +# Please reference the below wiki page to learn more about contributing: +# todo insert wiki page +# Thank you for any and all contributions! + +# A list of people who have contributed to the translations for this language. +# +# Please include your name if you contribute to this file. It will be displayed in the credits section for a language +# whenever it is selected. You may include a link to your GitHub or any other social media platform. +contributors: + - ["Kagi Translate", "https://translate.kagi.com"] + +# Any notes that translators feel the need to express, written in the target language. +# At your discretion, you may leave this field blank if you feel that it is not needed. If updating the language file, +# feel free to modify or remove this (make it blank) if it no longer applies. +# +# If you are contributing, please provide the ClaireBot maintainer(s) with a translated version/explanation of this. +# The explanation doesn't have to go into crazy detail, just let us know why it is present. +translationNotes: "" + +# The ClaireLang version that applies to this file. +# +# If you are translating ClaireBot, you do not need to worry about understanding this, the project maintainer(s) will +# happily assist you in determining this. +# +# The above being said, you are more than welcome to update this value to match the +# language revision you are translating to. +# +# This number should be changed in accordance with the following: +# +# ClaireLang version numbers are incremented every time new parameter(s) are added. If the main language file (en-US) +# has its wording changed, a .1, .2, .3 decimal value will be added. +# +# ClaireLang version numbers are used by the bot to assist in determining whether parameters are missing or not +# yet translated. +# +# See the below wiki page for a changelog: +# todo insert wiki page +version: 1 + +# Language Keys +# -------------------------------------------------------------------------------------------------------------------- # + +ClaireLang: + # Generic Strings which are likely ot be used in multiple places. + Generic: + True: "Verdadero" + False: "Falso" + Yes: "Sí" + No: "No" + ClickToDisplaySettings: "Haz clic para mostrar la configuración" + + Colors: + ClaireBotBlue: "Azul ClaireBot" + Red: "Rojo" + Pink: "Rosa" + Purple: "Morado" + DeepPurple: "Morado Oscuro" + Indigo: "Índigo" + Blue: "Azul" + LightBlue: "Azul Claro" + Cyan: "Cian" + Teal: "Verde Azulado" + Green: "Verde" + Olive: "Oliva" + LightGreen: "Verde Claro" + Lime: "Lima" + Yellow: "Amarillo" + Amber: "Ámbar" + NRAXOrange: "Naranja NRAX" + DeepOrange: "Naranja Oscuro" + White: "Blanco" + Grey: "Gris" + Black: "Negro" + + PlsBan: + PlsBanTriggers: + - "pls ban" + - "please ban" + - "ban" + - "porfa banea" + - "banea" + PlsBanResponses: + - "claro que sí, colega" + - "¡Sin problema!" + - "Hecho. Encantada de ayudar :)" + - "Considéralo hecho." + - "Ya está. QEPD LOL" + - "*L + Ratio -ClaireBot*" + - "Les diré que se vayan a freír espárragos, supongo ¯\\_(ツ)_/¯" + - "Se ha enviado arena a su ubicación. Nota de regalo: \"A pastar\"" + - "Llora más fuerte." + - "K" + - "Buen viaje" + Embed: + Commands: + Regular: + AvatarEmbed: "" # Unused + EightBallEmbed: + 8ball: "bola8" + 8bResponses: + - "*¡Claro que sí!*" + - "*Ni de coña*" + - "*Quizás*" + - "*Sí*" + - "*No*" + - "*De ninguna manera*" + - "*Es posible.*" + - "*Ni en sueños.*" + - "*Lo dudo.*" + - "*¡Absolutamente!*" + - "*Difícil de decir.*" + - "*¡Probable!*" + - "*Poco probable.*" + 8bRiggedResponses: + - "*¡Claro que sí!*" + - "*¡JODER, SÍ!*" + - "*¡Ya lo sabes!*" + - "*¿De verdad necesitas hacer una pregunta cuya respuesta ya sabes? Obviamente sí.*" + - "*Ni siquiera hace falta decirlo, todo el mundo sabe que ClaireBot es la mejor.*" + - "*La respuesta es clara: ¡SÍ!*" + - "*El dominio de ClaireBot es indiscutible.*" + - "*¡Con ClaireBot, no hay duda!*" + - "*¡Todo apunta a que ClaireBot es la mejor!*" + OnTopTriggers: + # Defines what triggers a response from the ClaireBotOnTopResponses or 8bRiggedResponses + - "ClaireBot la mejor" + - "ClaireBot basada" + - "ClaireBot pogchamp" + - "ClaireBot siempre tiene la razón" + - "pot no toBerialC" + - "Gracias a Dios por ClaireBot" + - "ClaireBot manda" + - "ClaireBot MVP" + - "ClaireBot imbatible" + - "Alabada sea ClaireBot" + - "Todos alaben a ClaireBot" + - "En ClaireBot confiamos" + - "Larga vida a ClaireBot" + - "ClaireBot > todo" + - "ClaireBot es la GOAT" + - "ClaireBot es la puta ama" + - "ClaireBot a por la victoria" + - "Gracias dios basado" + - "Dios basado" + # Things ClaireBot should respond with when triggered by one of the OnTopTriggers + ClaireBotOnTopResponses: + - "*¡Ya lo sabes!*" + - "*Soy... inevitable. -ClaireBot*" + - "*Aprobado.*" + - "*Verídico.*" + - "*Mejor que Mee6*" + - "*¡No, para!*" + - "*¡Insuperable!*" + - "*Basada*" + - "*pot no toBerialC*" + - "*Mejor que Groovy*" + - "*Siempre un paso por delante.*" + - "*Una fuerza imparable.*" + - "*¡ClaireBot al rescate!*" + - "*¡De primera!*" + - "*¡Brillando sobre el resto!*" + - "*La supremacía de ClaireBot se ha logrado.*" + - "*¡No es un arma de control mental de la CIA desde 2025!*" + - "*¡Alábenme! -ClaireBot*" + - "*¡Fuerza imparable! 🚀*" + - "*¡Siempre en la cima! 🏆*" + - "*¡No temas, ClaireBot te cubre las espaldas! ✌️*" + - "*¡Un gran poder conlleva una gran ClaireBot! 💪*" + - "*¡Confía en ClaireBot, confía en la victoria! 🏅*" + - "*¡La mejor que hay, la mejor que hubo y la mejor que habrá!*" + - "*¡Ofreciendo lo mejor, siempre! 🌟*" + - "*Excelencia. 💯*" + # Note: The rest of the help is located separately within commands_es-ES.yml + HelpEmbed: + Commands: "Comandos" + Usage: "Uso" + Error: "No se pudieron encontrar los datos de ayuda para \"{cb.commandname}\". Código de error: {cb.errorcode}" + InfoEmbed: + NeedHelp: "¿Necesitas ayuda?" + NeedHelpDetails: "Puedes obtener ayuda creando un 'issue' en nuestro [GitHub](https://github.com/Sidpatchy/ClaireBot/issues) o uniéndote a nuestro [servidor de soporte](https://support.clairebot.net/)" + AddToServer: "Añádeme a un servidor" + AddToServerDetails: "Añadirme a un servidor es simple, todo lo que tienes que hacer es hacer clic [aquí]({cb.supportserver})" + GitHub: "GitHub" + GitHubDetails: "ClaireBot es de código abierto, ¡eso significa que puedes ver todo su código! ¡Echa un vistazo a su [GitHub!]({cb.github})" + ServerCount: "Número de servidores" + ServerCountDetails: "He iluminado a **{cb.bot.numservers}** servidores." + Version: "Versión" + VersionDetails: "Estoy ejecutando ClaireBot **v{cb.bot.version}**, lanzada el **{cb.bot.releasedate}**" + Uptime: "Tiempo de actividad" + UptimeValue: "Iniciado el \n*{cb.bot.runtimedurationwords}*" + LeaderboardEmbed: + LeaderboardForServer: "Clasificación de" + GlobalLeaderboard: "Clasificación Global" + LevelEmbed: "" # Unused + QuoteEmbed: + InvalidMessages: "Parece que los mensajes que seleccioné no eran válidos. Por favor, inténtalo de nuevo más tarde." + JumpToOriginal: "Haz clic para ir al mensaje original:" + SantaEmbed: + Confirmation: "¡Confirmado! Te he enviado un mensaje directo. Por favor, continúa por ahí." + RulesButton: "Añadir reglas" + ThemeButton: "Añadir un tema" + SendButton: "Enviar mensajes" + TestButton: "Enviar muestra" + RandomizeButton: "Volver a aleatorizar" + SentByAuthor: "Enviado por" + GiverMessage: "¡Jo! ¡Jo! ¡Jo! ¡Te ha tocado **{cb.user.id.displayname.server}** en el Amigo Invisible de {cb.server.name}!" + ServerInfoEmbed: + ServerID: "ID del Servidor" + Owner: "Propietario" + CreationDate: "Fecha de Creación" + RoleCount: "Número de Roles" + MemberCount: "Número de Miembros" + ChannelCounts: "Número de Canales" + Categories: "Categorías" + TextChannels: "Canales de Texto" + VoiceChannels: "Canales de Voz" + ServerPreferencesEmbed: + MainMenuTitle: "Editor de Configuración del Servidor" + NotAServer: "¡Debes ejecutar este comando dentro de un servidor!" + RequestsChannelMenuName: "Canal de Solicitudes" + RequestsChannelDescription: "Solo muestra los primeros 25 canales del servidor." + ModeratorChannelMenuName: "Canal de Mensajes para Moderadores" + ModeratorChannelDescription: "Solo muestra los primeros 25 canales del servidor." + EnforceServerLanguageMenuName: "Forzar Idioma del Servidor" + AcknowledgeRequestsChannelChangeTitle: "¡Canal de Solicitudes Cambiado!" + AcknowledgeRequestsChannelChangeDescription: "Tu canal de solicitudes ha sido cambiado a {cb.channel.requests.mentiontag}" + AcknowledgeModeratorChannelChangeTitle: "¡Canal de Moderadores Cambiado!" + AcknowledgeModeratorChannelChangeDescription: "Tu canal de mensajes para moderadores ha sido cambiado a {cb.channel.moderator.mentiontag}" + AcknowledgeEnforceServerLanguageUpdateTitle: "¡Preferencias de Idioma del Servidor Actualizadas!" + AcknowledgeEnforceServerLanguageUpdateEnforced: "Ahora seguiré el idioma del servidor independientemente de la preferencia del usuario." + AcknowledgeEnforceServerLanguageUpdateNotEnforced: "Seguiré la preferencia de idioma de cada usuario individual." + ServerLanguageMenuTitle: "Idioma del servidor" + ServerLanguageMenuDesc: "Elige el idioma por defecto que ClaireBot debe usar en este servidor cuando la imposición esté activada." + ServerLanguageChanged: "¡Idioma del servidor actualizado!" + ServerLanguageChangedDesc: "La preferencia de idioma del servidor ha sido actualizada." + UserInfoEmbed: + Error_1: "El valor para 'author' era nulo cuando se pasó al Embed de UserInfo. Código de error: {cb.errorcode}" + Error_2: "Autor: {cb.user.name}" + User: "Usuario" + DiscordID: "ID de Discord" + JoinDate: "Fecha de Ingreso al Servidor" + CreationDate: "Fecha de Creación de la Cuenta" + UserPreferencesEmbed: + MainMenuText: "Editor de Preferencias de Usuario" + AccentColourMenu: "Editor de Color de Acento" + AccentColourList: "Lista de Colores de Acento" + AccentColourChanged: "¡Color de Acento Cambiado!" + AccentColourChangedDesc: "Tu color de acento ha sido cambiado a {cb.user.id.accentcolour}" + LanguageMenuTitle: "Preferencias de Idioma" + LanguageMenuDesc: "Elige el idioma que ClaireBot debe usar al interactuar contigo." + LanguageChanged: "¡Idioma actualizado!" + LanguageChangedDesc: "Tu idioma preferido ha sido actualizado." + VotingEmbed: + PollRequest: "{cb.user.id.displayname.server} solicita:" + PollAsk: "{cb.user.id.displayname.server} pregunta:" + Choices: "Opciones" + PollID: "ID de Encuesta: {cb.poll.id}" + UserResponseTitle: "¡Tu solicitud ha sido creada!" + UserResponseDescription: "Ve a verla en {cb.channel.requests.mentiontag}" + ErrorEmbed: + Error: "ERROR" + GenericDescription: "Parece que he encontrado un error, ¡ups! Por favor, intenta ejecutar el comando una vez más y si eso no funciona, únete a mi [servidor de Discord]({cb.supportserver}) y cuéntanos sobre el problema.\n\nPor favor, incluye el siguiente código de error: {cb.errorcode}" + WelcomeEmbed: + Title: "🎉 ¡Bienvenido a ClaireBot 3!" + Motto: "¿Cómo puedes resurgir, si no has ardido?" + UsageTitle: "Uso" + UsageDesc: "Empieza ejecutando `{cb.command.help.name}`. ¿Necesitas más información sobre un comando? Ejecuta `/{cb.command.help.name} ` (ej. `/{cb.command.help.name} user`)" + SupportTitle: "Obtener Soporte" + SupportDesc: "Puedes obtener ayuda en nuestro [Discord]({cb.supportserver}), o abriendo un 'issue' en [GitHub]({cb.github})" + + # Message Components (Buttons, Select Menus, etc.) + Components: + Regular: + SantaModal: + rules-row: "Reglas..." + theme-row: "Tema..." + ServerPreferences: + Settings: "Configuración" + RequestsChannel: "Canal de Solicitudes" + RequestsChannelDescription: "Elige dónde debe publicar ClaireBot las solicitudes." + ModeratorMessagesChannel: "Canal de Mensajes para Moderadores" + ModeratorMessagesChannelDescription: "Elige dónde debe publicar ClaireBot los mensajes para moderadores." + EnforceServerLanguage: "Forzar Idioma del Servidor" + EnforceServerLanguageDescription: "Forzar a ClaireBot a usar el mismo idioma que el servidor, independientemente de la preferencia del usuario." + EnforceServerLanguagePlaceholder: "Haz clic para seleccionar una opción" + SelectAChannelPlaceholder: "Haz clic para seleccionar un canal" + ServerLanguage: "Idioma del servidor" + ServerLanguageDescription: "Elige el idioma por defecto que ClaireBot debe usar en este servidor." + ServerLanguagePlaceholder: "Haz clic para seleccionar un idioma para el servidor" + UserPreferences: + Settings: "Configuración" + AccentColour: "Color de Acento" + AccentColourDescription: "Para aquellos que odian el color azul" + Language: "Idioma" + LanguageDescription: "Si te ofende el inglés" + LanguagePlaceholder: "Haz clic para seleccionar un idioma" + AccentColourPlaceholder: "Haz clic para elegir una opción" + SelectCommonColours: "Seleccionar Colores Comunes" + SelectCommonColoursDescription: "Selecciona de una lista de colores comunes" + HexadecimalEntry: "Entrada Hexadecimal" + HexadecimalEntryDescription: "Introduce cualquier color hexadecimal" + HexEntryPlaceholder: "Introduce un color hex, ej. #ff8800" + Voting: + Question: "Pregunta" + Details: "Detalles" + MultipleChoicePlaceholder: "Haz clic para elegir una opción" + MultipleChoiceYesDescription: "Abre el menú para seleccionar más opciones" + MultipleChoiceNoDescription: "Envía {cb.commandname} después de hacer clic en enviar" + AllowMultipleChoices: "¿Permitir seleccionar múltiples opciones?" + AllowMultipleChoicesPlaceholder: "Haz clic para elegir una opción" + AllowMultipleChoicesYesDescription: "Permite al encuestado seleccionar más de una opción" + AllowMultipleChoicesNoDescription: "Solo permite al encuestado seleccionar una opción" + OptionLabelTemplate: "Opción #{cb.voting.optionnumber}" diff --git a/src/main/resources/translations/lang_ja-JP.yml b/src/main/resources/translations/lang_ja-JP.yml new file mode 100644 index 0000000..675f302 --- /dev/null +++ b/src/main/resources/translations/lang_ja-JP.yml @@ -0,0 +1,322 @@ +# ---------------------------------------------- +# | ClaireBot v3.4.0 | +# | Japanese language file | +# ---------------------------------------------- +# +# Please reference the below wiki page to learn more about contributing: +# todo insert wiki page +# Thank you for any and all contributions! + +# A list of people who have contributed to the translations for this language. +# +# Please include your name if you contribute to this file. It will be displayed in the credits section for a language +# whenever it is selected. You may include a link to your GitHub or any other social media platform. +contributors: + - ["Sidpatchy", "https://github.com/Sidpatchy/"] + +# Any notes that translators feel the need to express, written in the target language. +# At your discretion, you may leave this field blank if you feel that it is not needed. If updating the language file, +# feel free to modify or remove this (make it blank) if it no longer applies. +# +# If you are contributing, please provide the ClaireBot maintainer(s) with a translated version/explanation of this. +# The explanation doesn't have to go into crazy detail, just let us know why it is present. +translationNotes: "ClaireBotが公式にサポートしている言語です。" + +# The ClaireLang version that applies to this file. +# +# If you are translating ClaireBot, you do not need to worry about understanding this, the project maintainer(s) will +# happily assist you in determining this. +# +# The above being said, you are more than welcome to update this value to match the +# language revision you are translating to. +# +# This number should be changed in accordance with the following: +# +# ClaireLang version numbers are incremented every time new parameter(s) are added. If the main language file (en-US) +# has its wording changed, a .1, .2, .3 decimal value will be added. +# +# ClaireLang version numbers are used by the bot to assist in determining whether parameters are missing or not +# yet translated. +# +# See the below wiki page for a changelog: +# todo insert wiki page +version: 1 + +# Language Keys +# -------------------------------------------------------------------------------------------------------------------- # + +ClaireLang: + # Generic Strings which are likely ot be used in multiple places. + Generic: + True: "真" + False: "偽" + Yes: "はい" + No: "いいえ" + ClickToDisplaySettings: "クリックして設定を表示" + + Colors: + ClaireBotBlue: "クレアボットブルー" + Red: "赤" + Pink: "ピンク" + Purple: "紫" + DeepPurple: "ディープパープル" + Indigo: "インディゴ" + Blue: "青" + LightBlue: "ライトブルー" + Cyan: "シアン" + Teal: "ティール" + Green: "緑" + Olive: "オリーブ" + LightGreen: "ライトグリーン" + Lime: "ライム" + Yellow: "黄" + Amber: "アンバー" + NRAXOrange: "NRAXオレンジ" + DeepOrange: "ディープオレンジ" + White: "白" + Grey: "灰色" + Black: "黒" + + PlsBan: + PlsBanTriggers: + - "pls ban" + - "please ban" + - "ban" + - "バンして" + - "バンお願いします" + PlsBanResponses: + - "任せろ" + - "お安い御用!" + - "完了。手伝えて何より :)" + - "完了したと思ってください。" + - "終わったよ。RIP BOZO" + - "*L + Ratio -ClaireBot*" + - "出てけって言っとくよ ¯\\_(ツ)_/¯" + - "砂を相手の所在地に発送しました。ギフトメモ: \"Pound sand\"" + - "もっと泣けよ。" + - "り" + - "せいせいする" + Embed: + Commands: + Regular: + AvatarEmbed: "" # Unused + EightBallEmbed: + 8ball: "エイトボール" + 8bResponses: + - "*もちろん!*" + - "*ありえない*" + - "*多分ね*" + - "*はい*" + - "*いいえ*" + - "*とんでもない*" + - "*可能性はある*" + - "*万に一つもない*" + - "*疑わしいね*" + - "*絶対に!*" + - "*なんとも言えない*" + - "*ありえそう!*" + - "*なさそう*" + 8bRiggedResponses: + - "*当たり前だろ!*" + - "*あったりめえよ!*" + - "*わかってるくせに!*" + - "*答えを知ってる質問をする必要ある?当然イエスだろ*" + - "*言うまでもない、ClaireBotが最強なのは誰もが知っている*" + - "*答えは明白: YES!*" + - "*ClaireBotの支配は揺るがない*" + - "*ClaireBotがいれば、疑問の余地なし!*" + - "*全ての兆候がClaireBotの優位を示している!*" + OnTopTriggers: + # Defines what triggers a response from the ClaireBotOnTopResponses or 8bRiggedResponses + - "ClaireBot on top" + - "ClaireBot based" + - "ClaireBot pogchamp" + - "ClaireBot is always right" + - "pot no toBerialC" + - "Thank God for ClaireBot" + - "ClaireBot rules" + - "ClaireBot MVP" + - "ClaireBot unbeatable" + - "Hail ClaireBot" + - "All Hail ClaireBot" + - "In ClaireBot we trust" + - "Long live ClaireBot" + - "ClaireBot > everything" + - "ClaireBot is the GOAT" + - "ClaireBot is GOATed" + - "ClaireBot for the win" + - "Thank you based god" + - "Based god" + - "クレアボット最強" + - "クレアボットしか勝たん" + - "クレアボット最高" + - "クレアボットの言うことは絶対" + # Things ClaireBot should respond with when triggered by one of the OnTopTriggers + ClaireBotOnTopResponses: + - "*当然!*" + - "*私は…必然だ。 -ClaireBot*" + - "*承認*" + - "*事実だ*" + - "*Mee6よりマシ*" + - "*やめろ!*" + - "*負け知らず!*" + - "*わかってるじゃん*" + - "*pot no toBerialC*" + - "*Groovyよりマシ*" + - "*常に一歩先を行く*" + - "*止められない力*" + - "*ClaireBotが助けに来たぞ!*" + - "*最高級!*" + - "*他を圧倒する輝き!*" + - "*ClaireBotによる支配は達成された*" + - "*2025年以降、CIAの洗脳兵器ではありません!*" + - "*我を崇めよ! -ClaireBot*" + - "*止められない力!🚀*" + - "*永遠に最強!🏆*" + - "*恐れるな、ClaireBotがついている!✌️*" + - "*大いなる力には、大いなるClaireBotが伴う!💪*" + - "*ClaireBotを信じよ、勝利を信じよ!🏅*" + - "*過去最高、現在最高、未来永劫最高の存在!*" + - "*常に最高をお届け!🌟*" + - "*完璧。💯*" + # Note: The rest of the help is located separately within commands_ja.yml + HelpEmbed: + Commands: "コマンド" + Usage: "使い方" + Error: "\"{cb.commandname}\" のヘルプデータが見つかりませんでした。エラーコード: {cb.errorcode}" + InfoEmbed: + NeedHelp: "お困りですか?" + NeedHelpDetails: "[GitHub](https://github.com/Sidpatchy/ClaireBot/issues)でissueを作成するか、[サポートサーバー](https://support.clairebot.net/)に参加することでヘルプを得られます。" + AddToServer: "サーバーに私を追加" + AddToServerDetails: "私をサーバーに追加するのは簡単です。[ここ]({cb.supportserver})をクリックするだけです。" + GitHub: "GitHub" + GitHubDetails: "ClaireBotはオープンソースです。つまり、全てのコードを閲覧できます![GitHub]({cb.github})をチェックしてみてください!" + ServerCount: "サーバー数" + ServerCountDetails: "私は **{cb.bot.numservers}** 個のサーバーを啓蒙しました。" + Version: "バージョン" + VersionDetails: "私は **{cb.bot.releasedate}** にリリースされたClaireBot **v{cb.bot.version}** を実行しています。" + Uptime: "稼働時間" + UptimeValue: " に起動\n*稼働時間: {cb.bot.runtimedurationwords}*" + LeaderboardEmbed: + LeaderboardForServer: "サーバーのリーダーボード" + GlobalLeaderboard: "グローバルリーダーボード" + LevelEmbed: "" # Unused + QuoteEmbed: + InvalidMessages: "選択したメッセージが無効だったようです。後でもう一度お試しください。" + JumpToOriginal: "クリックして元のメッセージにジャンプ:" + SantaEmbed: + Confirmation: "確認しました!ダイレクトメッセージを送信しました。そちらで続けてください。" + RulesButton: "ルールを追加" + ThemeButton: "テーマを追加" + SendButton: "メッセージを送信" + TestButton: "サンプルを送信" + RandomizeButton: "再抽選" + SentByAuthor: "送信者" + GiverMessage: "ホー!ホー!ホー! {cb.server.name} のシークレットサンタで、あなたは **{cb.user.id.displayname.server}** さんを引き当てました!" + ServerInfoEmbed: + ServerID: "サーバーID" + Owner: "オーナー" + CreationDate: "作成日" + RoleCount: "ロール数" + MemberCount: "メンバー数" + ChannelCounts: "チャンネル数" + Categories: "カテゴリ" + TextChannels: "テキストチャンネル" + VoiceChannels: "ボイスチャンネル" + ServerPreferencesEmbed: + MainMenuTitle: "サーバー設定エディタ" + NotAServer: "このコマンドはサーバー内で実行する必要があります!" + RequestsChannelMenuName: "リクエストチャンネル" + RequestsChannelDescription: "サーバー内の最初の25チャンネルのみをリスト表示します。" + ModeratorChannelMenuName: "モデレーターメッセージチャンネル" + ModeratorChannelDescription: "サーバー内の最初の25チャンネルのみをリスト表示します。" + EnforceServerLanguageMenuName: "サーバー言語を強制" + AcknowledgeRequestsChannelChangeTitle: "リクエストチャンネルが変更されました!" + AcknowledgeRequestsChannelChangeDescription: "リクエストチャンネルが {cb.channel.requests.mentiontag} に変更されました。" + AcknowledgeModeratorChannelChangeTitle: "モデレーターチャンネルが変更されました!" + AcknowledgeModeratorChannelChangeDescription: "モデレーターメッセージチャンネルが {cb.channel.moderator.mentiontag} に変更されました。" + AcknowledgeEnforceServerLanguageUpdateTitle: "サーバー言語設定が更新されました!" + AcknowledgeEnforceServerLanguageUpdateEnforced: "これからは、ユーザー設定に関わらずサーバーの言語に従います。" + AcknowledgeEnforceServerLanguageUpdateNotEnforced: "これからは、個々のユーザーの言語設定に従います。" + ServerLanguageMenuTitle: "サーバー言語" + ServerLanguageMenuDesc: "強制が有効な場合、ClaireBotがこのサーバーで使用するデフォルト言語を選択してください。" + ServerLanguageChanged: "サーバー言語が更新されました!" + ServerLanguageChangedDesc: "サーバーの言語設定が更新されました。" + UserInfoEmbed: + Error_1: "UserInfo Embedに渡されたauthorの値がnullでした。エラーコード: {cb.errorcode}" + Error_2: "作成者: {cb.user.name}" + User: "ユーザー" + DiscordID: "Discord ID" + JoinDate: "サーバー参加日" + CreationDate: "アカウント作成日" + UserPreferencesEmbed: + MainMenuText: "ユーザー設定エディタ" + AccentColourMenu: "アクセントカラーエディタ" + AccentColourList: "アクセントカラーリスト" + AccentColourChanged: "アクセントカラーが変更されました!" + AccentColourChangedDesc: "あなたのアクセントカラーが {cb.user.id.accentcolour} に変更されました。" + LanguageMenuTitle: "言語設定" + LanguageMenuDesc: "ClaireBotがあなたとやり取りするときに使用する言語を選択してください。" + LanguageChanged: "言語が更新されました!" + LanguageChangedDesc: "あなたの優先言語が更新されました。" + VotingEmbed: + PollRequest: "{cb.user.id.displayname.server} からのリクエスト:" + PollAsk: "{cb.user.id.displayname.server} からの質問:" + Choices: "選択肢" + PollID: "投票ID: {cb.poll.id}" + UserResponseTitle: "リクエストが作成されました!" + UserResponseDescription: "{cb.channel.requests.mentiontag} で確認してください。" + ErrorEmbed: + Error: "エラー" + GenericDescription: "おっと、エラーが発生したようです!もう一度コマンドを実行してみてください。それでもうまくいかない場合は、私の[Discordサーバー]({cb.supportserver})に参加して、問題についてお知らせください。\n\n以下のエラーコードを含めてください: {cb.errorcode}" + WelcomeEmbed: + Title: "🎉 ClaireBot 3へようこそ!" + Motto: "燃え尽きずして、どうして立ち上がれようか?" + UsageTitle: "使い方" + UsageDesc: "`{cb.command.help.name}` を実行して始めましょう。コマンドについてさらに情報が必要な場合は、`/{cb.command.help.name} <コマンド名>` (例: `/{cb.command.help.name} user`) を実行してください。" + SupportTitle: "サポート" + SupportDesc: "[Discord]({cb.supportserver})でヘルプを得るか、[GitHub]({cb.github})でissueを開くことができます。" + + # Message Components (Buttons, Select Menus, etc.) + Components: + Regular: + SantaModal: + rules-row: "ルール..." + theme-row: "テーマ..." + ServerPreferences: + Settings: "設定" + RequestsChannel: "リクエストチャンネル" + RequestsChannelDescription: "ClaireBotがリクエストを投稿する場所を選択します。" + ModeratorMessagesChannel: "モデレーターメッセージチャンネル" + ModeratorMessagesChannelDescription: "ClaireBotがモデレーターメッセージを投稿する場所を選択します。" + EnforceServerLanguage: "サーバー言語を強制" + EnforceServerLanguageDescription: "ユーザー設定に関わらず、ClaireBotにサーバーと同じ言語を使用させます。" + EnforceServerLanguagePlaceholder: "クリックしてオプションを選択" + SelectAChannelPlaceholder: "クリックしてチャンネルを選択" + ServerLanguage: "サーバーの言語" + ServerLanguageDescription: "このサーバーでClaireBotが使用するデフォルト言語を選択してください。" + ServerLanguagePlaceholder: "クリックしてサーバーの言語を選択" + UserPreferences: + Settings: "設定" + AccentColour: "アクセントカラー" + AccentColourDescription: "青色が嫌いなあなたへ" + Language: "言語" + LanguageDescription: "英語に不快感を覚えるなら" + LanguagePlaceholder: "クリックして言語を選択" + AccentColourPlaceholder: "クリックしてオプションを選択" + SelectCommonColours: "一般的な色を選択" + SelectCommonColoursDescription: "一般的な色のリストから選択します" + HexadecimalEntry: "16進数で入力" + HexadecimalEntryDescription: "任意の16進数カラーを入力します" + HexEntryPlaceholder: "16進数カラーを入力 (例: #ff8800)" + Voting: + Question: "質問" + Details: "詳細" + MultipleChoicePlaceholder: "クリックしてオプションを選択" + MultipleChoiceYesDescription: "他のオプションを選択するためのメニューを開きます" + MultipleChoiceNoDescription: "送信をクリックすると {cb.commandname} を送信します" + AllowMultipleChoices: "複数選択を許可しますか?" + AllowMultipleChoicesPlaceholder: "クリックしてオプションを選択" + AllowMultipleChoicesYesDescription: "回答者が複数の選択肢を選べるようにします" + AllowMultipleChoicesNoDescription: "回答者が一つの選択肢しか選べないようにします" + OptionLabelTemplate: "選択肢 #{cb.voting.optionnumber}" diff --git a/todo.md b/todo.md index 875456b..5fd65be 100644 --- a/todo.md +++ b/todo.md @@ -1,67 +1,57 @@ -# MOVED TO +# ClaireBot TODOs — Updated 2025-10-16 18:43 -# Known Issues -1) Avatar command not sending 4096x4096 image - FIXED -2) Help command fucking dies whenever a command is added / name changed. Caused by using a seperate commands list from -the one used by everything else. Recommendation: Delete ClaireMusic - DELETED CLAIREMUSIC -3) Race condition issue with the leveling system - see https://github.com/Sidpatchy/ClaireBot/issues/3 +This file replaces an outdated TODO from several release cycles ago. It consolidates current, de-duplicated, prioritized action items discovered across the codebase and docs. -# Commands -1) 8ball - Done but needs optimizations -2) Avatar - Done, see known issues #1 -3) Config - - Server preferences - DONE - - Messaging Channels - DONE - - Language - DONE - - User preferences - - Colour - - Common colours - DONE - - Hex entry - DONE - - Language - NYI (see #14) - - Need to create a system for multi-lang. - - For plans see Specs/ClaireLang, ClaireConfig, ClairePAPI -4) Help - DONE, see known issues #2 DONE, see #16 -5) Info - NYI, literally just copy RomeBot's info command. -- DONE -6) Leaderboard - - DB implementation complete. Finalize API implementation. - - API implementation complete. Need to create the command. - - Command framework is completed. Need to prevent querying, but more importantly, displaying users who are not in a guild. - - Race condition issue https://github.com/Sidpatchy/ClaireBot/issues/3 -7) Level - NYI - - API prepared for command creation. - - Command created - - Listeners partially implemented - - Race condition issue https://github.com/Sidpatchy/ClaireBot/issues/3 -8) Poll - DONE -9) Request - DONE -10) Server - DONE -11) User - DONE -12) ClaireBot on top! - DONE -13) Zerfas -14) ClaireBot Language System (ClaireLang) - - Language Manager - - Collect EVERY language string and add it to a YAML file. - - Rewrite EVERY command to make use of the language manager. - - 8ball - - avatar - - help - - info - - leaderboard - - level - - poll - - request - - server - - user - - config -15) Probably gonna want to do something with the points system you added to the API... - Sorta kinda started - - Need to add system of gaining points. - - Mostly done, could probably make gaining points way more robust. - - Fix race condition issues: https://github.com/Sidpatchy/ClaireBot/issues/3 -16) Rewrite the help command to be actually informative. There's literally no additional value added with the current system. The command spec already supports a more advanced help string for each command. MAKE USE OF IT. The architecture of the command is great, it just needs to be more useful. The main screen could be a little more useful, point to starters like /config, and explain breifly what it does. +## 1) High‑impact, low‑effort fixes (quick wins) +- Main.java: Do not force-copy default language file each startup. + - Change saveResource("translations/lang_en-US.yml", true) to false and only copy when missing. Ensure non-en-US locales are handled on first run via lazy copy or locale detection. Priority: P1. +- SlashCommandCreate.java: Remove hardcoded "en-US". + - Read Main.fallbackLocale or a config param and pass language context appropriately. Priority: P1. +- Translations (lang_en-US.yml, lang_ja-JP.yml, lang_es-ES.yml, lang_TEMPLATE.yml): Replace “# todo insert wiki page”. + - Insert the correct documentation URL(s) to the wiki/help pages. Priority: P1. +- config.yml: “TODO either automatically add new config file params or remove this.” + - Decide policy. Prefer auto-merge of new keys at startup; otherwise remove the comment and document manual update. Priority: P1. +- ErrorEmbed.java: “remove these legacy methods.” + - Identify unused legacy methods, remove or deprecate with @Deprecated and migrate call sites. Priority: P1–P2. +## 2) Internationalization and language flow +- LanguageManager.java + - Allow server admins to specify a custom language via ClaireData. Extend Guild model + API endpoints to store server language; honor enforceServerLanguage and read from DB (not Discord preferredLocale). Priority: P2. + - Move locale resolution out of parseUserAndServerOptions into Guild creation or a dedicated locale service; LanguageManager should consume it. Priority: P2. + - Dependency: ClaireData update (Trello vkQTCTMG). Status: Blocked. Priority: P2 (blocked). -# General Features -## ClaireWeb -A website needs to be designed. In addition, ClaireBot needs to have an API endpoint (in addition to ClaireData) for it to query from. +## 3) Command system and registration +- RegisterSlashCommands.java + - Register commands per-server so server admins may use different languages. Implement per-guild registration with Javacord, mapping guild ID to localized command variants, or use localization hooks if supported. Priority: P2. +- QuoteEmbed.java + - "validate that this won’t nuke the bot" — add guardrails and error handling; bound rate/size; add a small test or dry-run. Priority: P2. -## \ No newline at end of file +## 4) Configuration handling +- Main.java + - “stop using Robin for this. Switch to standard Java classes.” Replace RobinConfiguration for config.yml with Jackson YAML or SnakeYAML + POJO; keep migration path; add validation and defaults. Priority: P3. +- build.gradle + - Kotlin stdlib re-add when JDK 25 support lands. Track Kotlin/JDK compatibility; once supported, re-enable or rely on transitive stdlib via Kotlin DSL if applicable. Priority: P3 (blocked by external support). + +## 5) Documentation +- Writerside/topics/Contributing-guide.md + - Fill TODO sections: branching model, code style, commit messages, PR checks. Priority: P2. +- Translations wiki links + - Insert live URLs across all locales (see section 1). Priority: P1. + +## 6) Backlog (still relevant) +- Leaderboard + - Exclude users not in guild; finish final display logic. Monitor race condition issue (#3). Priority: P2. +- Level command + - API prepared; complete command and listeners; verify race conditions (#3). Priority: P2. +- Points system + - Make gaining points more robust; define events, caps, anti-abuse. Priority: P3. +- Help command rewrite + - Use rich, informative help with extended per-command descriptions; point to starters like /config. Priority: P2. +- ClaireLang rollout + - Collect all language strings into YAML; ensure every command uses LanguageManager; audit placeholders and add tests. Priority: P2. +- ClaireWeb + - Design website and add ClaireBot API endpoints (in addition to ClaireData) for web queries. Priority: P3. + +## Notes and dependencies +- Several i18n items are blocked by ClaireData schema/API changes (see Trello card). Start with unblocked quick wins and documentation. +- Spanish translation file contains natural-language “todo” occurrences (meaning “all/every”); only comment lines at the top were actionable. \ No newline at end of file