diff --git a/CHANGELOG.md b/CHANGELOG.md index 03159d5..2c87784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,61 @@ # Changelog +## Account and config command +➕ Added discord `/account` command
+➕ Added discord `/config` command for administrators + +--- + +## Help Command & Event Listeners +➕ Added a help command for discord with nice buttons
+➕ Added Event Listeners for the events: + +- Server starts/stops (plugin is loaded/unloaded) +- Player joins/leaves the Server +- Player gets an advancement +- Player dies + +--- + +## Tab Completions +### v0.11.0-beta +➕ Added Tab Completion to all Minecraft commands + +--- + +## Slash Commands Update +### v0.10.0 +➕ Added Slash Commands!
+↳ Now use discord commands by using a slash (/) instead of the old discordCommandPrefix Entry in the config file

+ +**⚠️ Information about slash commands⚠️**
+ - It can take **up to** an hour until the slash commands are registered + - This is a caching limitation put in place by discord. + - If you do not see any slash commands after this hour, please see your console log for a link! + - You **CAN NOT** use normal message commands like `.setup` anymore. Use `/setup` instead, if you are the discord server **owner** and/or the bot owner. +
+🚫 Removed the old discord commands, that could be used in messages. + +--- + +## BIG Account update +### v0.9.0 +➕ Added Accounts!
+↳ Place an account Tag in your minecraft message, and it will be replaced with a discord ping (if the user has a linked account)!
+↳ Added a new minecraft command `/link`: Link your minecraft and discord accounts!
+↳ Added a new minecraft command `/unlink`: Unlink your minecraft and discord accounts!
+↳ Added a new minecraft command `/account`: Manage your accounts!
+↳ Added a new minecraft command `/whisper`: Whisper to a friend on discord!
+↳ Added a new minecraft command `/dcmsg`: Whisper to a friend on discord!
+↳ Added a new discord command `link`: Link your discord to your minecraft!
+↳ Added a new discord command `whisper`: Whisper your message to a minecraft player!
+↳ Added a new discord command `mcmsg`: Whisper your message to a minecraft player!
+➕ Added some more toggles:
+↳ The bot **can** send message with server commands and player commands to all the channels.
+↳ Toggle, if the footer should be displayed.
+↳ Toggle, if discord command messages should be deleted b the bot.
+ +--- + ## Small bug fixes ### v0.8.4-beta ➕ Added Error message, when then bot does not have the required permissions, to create Channels or/and roles (See issue [29](https://github.com/MaFeLP/MCDC/issues/29)).
@@ -157,7 +214,7 @@ ## v0.3.1-beta ### 🏁 Commenting update! ➕ Added building instructions with Java 8
-➕ Added full Javadoc documentation in [doc](./doc)
+➕ Added full Javadoc documentation in [doc](https://mafelp.github.io/MCDC/doc/development/index.html)
➕ Added full commentary to the full code base
➕ Added [RoleAdmin.java](./src/main/java/com/github/mafelp/discord/RoleAdmin.java) to prepare for Role management in coming builds diff --git a/README.md b/README.md index 278fb2a..de8c163 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,29 @@ +![Java Version](https://img.shields.io/badge/Java%20Version-17-blue) + # MCDC A [Minecraft](https://www.minecraft.net) plugin for [paper servers](https://papermc.io). -## Functions +## Features The bot can currently do all the checked items, unchecked will be implemented in the future. - [X] Display discord messages in the minecraft chat - [X] Discord messages can be sent to the bot via direct message - [X] Discord messages can be sent to any server channel the bot is present on - [X] Display minecraft messages in a discord chat - [X] managing a "#mincraft-server" channel on a specific discord server - - [ ] this includes that only members with a role can see this channel and write in it -

- - [ ] whisper between a discord user and a minecraft user - - [ ] linking between a discord and a minecraft account - + - [X] this includes that only members with a role can see this channel and write in it + - [X] whisper between a discord user and a minecraft user + - [X] linking between a discord and a minecraft account + - [X] Toggle-able: Sending minecraft commands to the discord chats. + - [X] Slash Commands: use slash commands in the discord chat. + - [X] Tab completion in Minecraft +

+ - [ ] A message in a channel that displays all online Members. + - [ ] Migrate more slash commands to discord + - [ ] `/account` + - [ ] `/config` + - [ ] Add a new command: `/help` + - [X] `/help` in minecraft + - [ ] `/help` in discord ## Installation 1. Download the latest [release](https://github.com/MaFeLP/MCDC/releases/) and put it into `/plugins`. @@ -60,12 +70,52 @@ apiToken: 'Your API Token goes here!' # Allowed values: any String discordCommandPrefix: '.' +# Selects if messages that are commands should be deleted after execution. +# Allowed values: +deleteDiscordCommandMessages: false + # Discord Channel IDs to broadcast messages to. channelIDs: - 1234 +# Enables accounts and linking. +# Allowed values +enableLinking: true + +# Allow players to list all the accounts. +# Allowed values +allowListAllAccounts: true + +# Decides, if the config value 'serverName' should be displayed in the footer of discord messages. +# Allowed values +showFooterInMessages: true + +# The value that should be displayed below the name +activity: + # If the bot should have an activity. + enabled: true + # The type of the message, aka. the first word: + # can be set to custom, competing, listening, watching, streaming or playing + type: listening + # The text that should be displayed. + message: "your messages 👀" + +# If the bot should send a message to the listening channels, if a command was executed by ... +sendCommandToDiscord: + # ... a player. + player: false + # ... the server. + server: false + # Permission section for setting permission levels permission: + # The permissions on linking and editing accounts. + accountEdit: + # The OP level needed to remove accounts of players. + level: 3 + # A list of UUIDs of Players who have a wildcard to use this command. + allowedUserUUIDs: + - a unique ID # Permission for minecraft command /config configEdit: diff --git a/bash-scripts/package.sh b/bash-scripts/package.sh new file mode 100644 index 0000000..b275f24 --- /dev/null +++ b/bash-scripts/package.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash + +if [ -z "$2" ] +then + cat < Directory '/tmp/package/plugins/mcdc' and parents has been created!" +echo ":: Copying files" +cp -v defaultConfiguration.yml /tmp/package/plugins/mcdc/config.yml +cp -v LICENSE /tmp/package/plugins/mcdc/LICENSE +cp -v "target/mcdc-$2.jar" /tmp/package/plugins/ + +# Create the config file for the advancements +( + echo ":: Generating advancements.json file" + echo "==> Using Minecraft directory $1" + echo "==> Using Minecraft version 1.18" + echo "==> Using language: en_gb" + cd "$1/assets/" || exit 1 + HASH=$(jq -r '.objects."minecraft/lang/en_gb.json".hash' < "indexes/1.18.json") + DIRECTORY=$(echo "$HASH" | cut -c '-2') + echo "==> Hash for file is: $HASH" + echo "==> Directory of Hash file is: $DIRECTORY" + echo "==> Extracting all advancements from the file..." + echo -n "{" > /tmp/advancements.json + grep ' "advancements.' < "objects/$DIRECTORY/$HASH" | sed -e 's/ "advancements./"/g' | tr '\n' ' ' | tr '\n' ' ' >> /tmp/advancements.json + echo "}" >> /tmp/advancements.json + sed -i 's/, }/}/g' /tmp/advancements.json + jq > "/tmp/advancements_new.json" < "/tmp/advancements.json" + mv "/tmp/advancements_new.json" "/tmp/package/plugins/mcdc/advancements.json" + echo "==> Advancement configuration created at '/tmp/package/plugins/mcdc/advancements.json'" +) + +# Create the packages +( + cd /tmp/package/ || exit 1 + echo ":: Creating packages" + echo "==> Creating zip package" + zip -r -9 "mcdc-$2-package.zip" plugins + echo "==> Creating tar.gz package" + tar czfv "mcdc-$2-package.tar.gz" plugins + echo ":: Creating checksums" + cat > "ReleaseMessage.md" < +## Changes +- See the included Changelog +- **Full Changelog**: https://github.com/MaFeLP/MCDC/compare/vX.XX.X-beta...v$2 + +## Checksums: +
+Click to expand + +- mcdc-$2-package.tar.gz + - MD5: \`$(md5sum "mcdc-$2-package.tar.gz" | cut -d ' ' -f1)\` + - SHA1: \`$(sha1sum "mcdc-$2-package.tar.gz" | cut -d ' ' -f1)\` + - SHA256: \`$(sha256sum "mcdc-$2-package.tar.gz" | cut -d ' ' -f1)\` + - SHA512: \`$(sha512sum "mcdc-$2-package.tar.gz" | cut -d ' ' -f1)\` +- mcdc-$2-package.zip + - MD5: \`$(md5sum "mcdc-$2-package.zip" | cut -d ' ' -f1)\` + - SHA1: \`$(sha1sum "mcdc-$2-package.zip" | cut -d ' ' -f1)\` + - SHA256: \`$(sha256sum "mcdc-$2-package.zip" | cut -d ' ' -f1)\` + - SHA512: \`$(sha512sum "mcdc-$2-package.zip" | cut -d ' ' -f1)\` +- mcdc-$2.jar + - MD5: \`$(md5sum "plugins/mcdc-$2.jar" | cut -d ' ' -f1)\` + - SHA1: \`$(sha1sum "plugins/mcdc-$2.jar" | cut -d ' ' -f1)\` + - SHA256: \`$(sha256sum "plugins/mcdc-$2.jar" | cut -d ' ' -f1)\` + - SHA512: \`$(sha512sum "plugins/mcdc-$2.jar" | cut -d ' ' -f1)\` + +
+EOF + if [ -z "$EDITOR" ] + then + nano "ReleaseMessage.md" + else + "$EDITOR" "ReleaseMessage.md" + fi + echo "Done!" +) + +gh release view "$2" +echo ":: Uploading assets to GitHub" +gh release upload "v$2" "/tmp/package/plugins/mcdc/advancements.json" "/tmp/package/plugins/mcdc/config.yml" "/tmp/package/plugins/mcdc/LICENSE" "/tmp/package/plugins/mcdc-$2.jar" "/tmp/package/mcdc-0.12.0-beta-package.tar.gz" "/tmp/package/mcdc-0.12.0-beta-package.zip" +echo "Printing final Release Message:" +cat "/tmp/package/ReleaseMessage.md" diff --git a/defaultConfiguration.yml b/defaultConfiguration.yml index 4285e84..46004fe 100644 --- a/defaultConfiguration.yml +++ b/defaultConfiguration.yml @@ -25,13 +25,65 @@ apiToken: 'Your API Token goes here!' # Allowed values: any String discordCommandPrefix: '.' +# Selects if messages that are commands should be deleted after execution. +# Allowed values: +deleteDiscordCommandMessages: false + # Discord Channel IDs to broadcast messages to. channelIDs: - 1234 +# Discord Role IDs that hav permission to use slash commands +roleIDs: + - 1234 + +# Enables accounts and linking. +# Allowed values +enableLinking: true + +# Allow players to list all the accounts. +# Allowed values +allowListAllAccounts: true + +# Decides, if the config value 'serverName' should be displayed in the footer of discord messages. +# Allowed values +showFooterInMessages: true + +# The value that should be displayed below the name +activity: + # If the bot should have an activity. + enabled: true + # The type of the message, aka. the first word: + # can be set to custom, competing, listening, watching, streaming or playing + type: listening + # The text that should be displayed. + message: "your messages 👀" + +# If the bot should send a message to the listening channels, if a command was executed by ... +sendCommandToDiscord: + # ... a player. + player: false + # ... the server. + server: false + +# Server events that will trigger a message in discord +events: + - JoinEvent # When a player joins the Server + - LeaveEvent # When a player leaves the Server + - PlayerAdvancementEvent # When a player gets a new Advancement + - PlayerDeathEvent # When a Player dies + - ServerStartupEvent # When the minecraft server starts + - ServerShutdownEvent # When the minecraft server stops # Permission section for setting permission levels permission: + # The permissions on linking and editing accounts. + accountEdit: + # The OP level needed to remove accounts of players. + level: 3 + # A list of UUIDs of Players who have a wildcard to use this command. + allowedUserUUIDs: + - a unique ID # Permission for minecraft command /config configEdit: diff --git a/pom.xml b/pom.xml index a05bd9e..fee5130 100644 --- a/pom.xml +++ b/pom.xml @@ -6,14 +6,14 @@ com.github.mafelp mcdc - 0.8.4-beta + 0.13.0-beta jar Mcdc A Spigot plugin for cross communicating with a discord server - 15 + 17 UTF-8 https://github.com/MaFeLP/MCDC @@ -23,7 +23,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.10.0 ${java.version} ${java.version} @@ -88,13 +88,13 @@ org.javacord javacord - 3.3.0 + 3.4.0 pom org.jetbrains annotations - RELEASE + 23.0.0 compile diff --git a/src/main/java/com/github/mafelp/accounts/Account.java b/src/main/java/com/github/mafelp/accounts/Account.java new file mode 100644 index 0000000..a68fa2b --- /dev/null +++ b/src/main/java/com/github/mafelp/accounts/Account.java @@ -0,0 +1,163 @@ +package com.github.mafelp.accounts; + +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.javacord.api.entity.user.User; + +import java.util.Optional; +import java.util.UUID; + +/** + * The method that stores all necessary information about an account with linked discord user and + * minecraft player. + */ +public class Account { + /** + * The discord user + */ + private final User user; + + /** + * the discord ID of the {@link Account#user}. + */ + private final long userID; + + /** + * The name of the account. The default is the discord name of the {@link Account#user}. + */ + private String username; + + /** + * The tag used to mention a Discord {@link User} in a discord message. + */ + private final String mentionTag; + + /** + * The minecraft {@link OfflinePlayer} to link the discord {@link User} to. + */ + private final OfflinePlayer player; + + /** + * The UUID of the minecraft {@link Account#player}. + */ + private final UUID playerUUID; + + /** + * The constructor to create an account from a minecraft {@link Account#playerUUID} and a discord + * {@link Account#userID}. + * @param user The discord {@link User} to link this account to. + * @param player The minecraft {@link Player} to link this account to. + */ + public Account(User user, OfflinePlayer player) { + this.user = user; + this.userID = user.getId(); + this.mentionTag = user.getMentionTag(); + + this.player = player; + this.username = "@" + player.getName(); + this.playerUUID = player.getUniqueId(); + } + + /** + * The getter for the {@link Account#user}. + * @return The {@link Account#user} field. + */ + public User getUser() { + return user; + } + + /** + * The getter for the {@link Account#userID}. + * @return The {@link Account#userID} field. + */ + public long getUserID() { + return userID; + } + + /** + * The getter for the {@link Account#username}. + * @return The {@link Account#username} field. + */ + public String getUsername() { + return username; + } + + /** + * The setter for the {@link Account#username}. + * @param username the username to set. + * @return The {@link Account#username} field. + */ + public Account setUsername(String username) { + this.username = username; + return this; + } + + /** + * The getter for the {@link Account#mentionTag}. + * @return The {@link Account#mentionTag} field. + */ + public String getMentionTag() { + return mentionTag; + } + + /** + * The getter for the {@link Account#player}. + * @return The {@link Account#player} field. + */ + public OfflinePlayer getPlayer() { + return player; + } + + /** + * The getter for the {@link Account#playerUUID}. + * @return The {@link Account#playerUUID} field. + */ + public UUID getPlayerUUID() { + return playerUUID; + } + + /** + * The method to get an {@link Account} by the {@link OfflinePlayer} it belongs to. + * @param player the {@link OfflinePlayer} to get the {@link Account} of. + * @return the Optional of an {@link Account}: If this account does not exists, it is empty. + */ + public static Optional getByPlayer(OfflinePlayer player) { + for (Account account : AccountManager.getLinkedAccounts()) { + if (account.player.equals(player)) { + return Optional.of(account); + } + } + + return Optional.empty(); + } + + /** + * The method to get an {@link Account} by the {@link User} it belongs to. + * @param user the {@link User} to get the {@link Account} of. + * @return the Optional of an {@link Account}: If this account does not exists, it is empty. + */ + public static Optional getByDiscordUser(User user) { + for (Account account : AccountManager.getLinkedAccounts()) { + if (account.user.equals(user)) { + return Optional.of(account); + } + } + + return Optional.empty(); + } + + /** + * The method to get an {@link Account} by the {@link Account#username} it belongs to. + * @param username the {@link Account#username} to get the {@link Account} of. + * @return the Optional of an {@link Account}: If this account does not exists, it is empty. + */ + public static Optional getByUsername(String username) { + for (Account account : AccountManager.getLinkedAccounts()) { + if (account.getUsername().equals(username)) { + return Optional.of(account); + } + } + + return Optional.empty(); + } +} diff --git a/src/main/java/com/github/mafelp/accounts/AccountLoader.java b/src/main/java/com/github/mafelp/accounts/AccountLoader.java new file mode 100644 index 0000000..2d048da --- /dev/null +++ b/src/main/java/com/github/mafelp/accounts/AccountLoader.java @@ -0,0 +1,96 @@ +package com.github.mafelp.accounts; + +import com.github.mafelp.utils.Logging; +import com.github.mafelp.utils.Settings; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.bukkit.OfflinePlayer; +import org.javacord.api.entity.user.User; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.*; + +/** + * The thread, which loads the accounts into AccountManager.linkedAccounts. + */ +public class AccountLoader extends Thread{ + /** + * The Json Parser used to parse Json from the Accounts File. + */ + private static final JsonParser jsonParser = new JsonParser(); + + /** + * The file in which all the accounts are stored. + */ + private static final File accountFile = new File(Settings.getConfigurationFileDirectory(), "accounts.json"); + + /** + * The method that runs the loading in another thread. + */ + @Override + public void run() { + // Loads the accounts.json file in. + Scanner scanner; + try { + scanner = new Scanner(accountFile); + } catch (FileNotFoundException e) { + Logging.logException(e, "Configuration file could not be found. Aborting Account loading."); + return; + } + + // Reads the contents of the accounts.json file + Logging.debug("Start: Reading Accounts file in."); + + StringBuilder fileInput = new StringBuilder(); + + while (scanner.hasNextLine()) { + fileInput.append(scanner.nextLine()); + } + + String input = fileInput.toString(); + + Logging.debug("accounts.json File input: " + input); + Logging.debug("Trying to parse the accounts file input..."); + + // Parses the file into a JSON Object + JsonElement jsonInput = jsonParser.parse(input); + JsonArray accounts = jsonInput.getAsJsonArray(); + + List linkedAccounts = new ArrayList<>(); + + // Parses all the JSON objects in the accounts array, stored in the accounts.json file + // and then adds them to the list of linked accounts. + for (JsonElement jsonElement : accounts) { + Logging.debug("In Accounts parsing loop."); + + JsonObject jsonObject = jsonElement.getAsJsonObject(); + + final long discordID = jsonObject.get("discordID").getAsLong(); + final String username = jsonObject.get("username").getAsString(); + final UUID minecraftUUID = UUID.fromString(jsonObject.get("minecraftUUID").getAsString()); + + Logging.debug("Getting player value for player with UUID: " + minecraftUUID); + final OfflinePlayer player = Settings.minecraftServer.getOfflinePlayer(minecraftUUID); + + Logging.debug("Getting Discord User from ID: " + discordID); + final User user = Settings.discordApi.getUserById(discordID).join(); + + if (user == null) { + Logging.info("discord user not found. ignoring."); + continue; + } + + Logging.debug("Adding user " + username + " to the list of accounts."); + + linkedAccounts.add(new Account(user, player).setUsername(username)); + } + + Logging.debug("Done parsing accounts. Setting the list to: " + Arrays.toString(linkedAccounts.toArray())); + + // Sets the global list of linked accounts to the array list 'linkedAccounts' so other processes can access them. + AccountManager.setLinkedAccounts(linkedAccounts); + } +} diff --git a/src/main/java/com/github/mafelp/accounts/AccountManager.java b/src/main/java/com/github/mafelp/accounts/AccountManager.java new file mode 100644 index 0000000..2a7e659 --- /dev/null +++ b/src/main/java/com/github/mafelp/accounts/AccountManager.java @@ -0,0 +1,138 @@ +package com.github.mafelp.accounts; + +import com.github.mafelp.utils.Logging; +import com.github.mafelp.utils.Settings; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import org.bukkit.OfflinePlayer; + +import java.io.*; +import java.util.*; + +/** + * Manager for creating, saving and loading linked discord and minecraft accounts. + */ +public class AccountManager { + /** + * The list of all linked accounts + */ + private static List linkedAccounts = new ArrayList<>(); + + /** + * The file in which the accounts are saved. + */ + private static final File accountFile = new File(Settings.getConfigurationFileDirectory(), "accounts.json"); + + /** + * The method to create an account file. + * @throws IOException the exception thrown, when the file could not be created, due to an error. + */ + public static void createAccountsFile() throws IOException { + if (accountFile.exists()) { + // If the file already exists, don't do anything. + Logging.info("accounts file " + accountFile.getAbsolutePath() + " already exists. Not overwriting it."); + } else { + // If the file does not exist, try to create the file and set its contents to '[]' + boolean fileCreationSuccess = accountFile.createNewFile(); + Logging.info("Accounts file creation... Success: " + fileCreationSuccess); + + if (!fileCreationSuccess) + return; + + PrintStream printStream = new PrintStream(new FileOutputStream(accountFile)); + printStream.println("[]"); + printStream.close(); + } + } + + /** + * The method that saves linkedAccounts to a JSON file. + * @throws FileNotFoundException the exception thrown, when the file does not exists, we try to write to. + */ + public static void saveAccounts() throws FileNotFoundException { + JsonArray accounts = new JsonArray(); + + // Creates a JSON Array with all the accounts from the global linkedAccounts list. + for (Account account: linkedAccounts) { + JsonObject accountInfo = new JsonObject(); + + accountInfo.addProperty("discordID", account.getUserID()); + accountInfo.addProperty("username", account.getUsername()); + accountInfo.addProperty("discordMentionTag", account.getMentionTag()); + + accountInfo.addProperty("minecraftUUID", account.getPlayerUUID().toString()); + + accounts.add(accountInfo); + } + + // Prints the accounts array to the accounts file. + PrintStream printStream = new PrintStream(new FileOutputStream(accountFile)); + printStream.print(accounts); + printStream.close(); + } + + /** + * The method that handles starting of the thread, which should load the accounts in. + * @throws IOException The exception that is being thrown, if the accounts.json File does not exists or the {@link AccountLoader} encounters an {@link IOException}. + */ + public static void loadAccounts() throws IOException{ + if (!accountFile.exists()) + createAccountsFile(); + + Thread accountLoaderThread = new AccountLoader(); + accountLoaderThread.setName("AccountLoader"); + accountLoaderThread.start(); + } + + /** + * The getter for the list of Linked Accounts. + * @return The linked Accounts list, currently used. + */ + public static List getLinkedAccounts() { + return linkedAccounts; + } + + /** + * The setter for the list of Linked Accounts. This should only be used by this package! + * @param set The List to set the linked accounts to. + * @return the list, this method has set, aka. the input list. + */ + protected static List setLinkedAccounts(List set) { + linkedAccounts = set; + return linkedAccounts; + } + + /** + * Adds an Account to the list of linked accounts. + * @param account The account to add + * @return the list of all linked Accounts. + */ + public static List addAccount(Account account) { + if (!linkedAccounts.contains(account)) + linkedAccounts.add(account); + return linkedAccounts; + } + + /** + * Removes an account from the linked accounts. + * @param account The account link to be removed + * @return The now list of accounts. + */ + public static List removeAccount(Account account) { + linkedAccounts.removeAll(Collections.singleton(account)); + return linkedAccounts; + } + + /** + * Gets all names of minecraft Players who have an account and all the usernames. + * @return The list of names and usernames. + */ + public static List getAllMinecraftAccountNames() { + List out = new ArrayList<>(); + + for (OfflinePlayer player : Settings.minecraftServer.getOfflinePlayers()) + Account.getByPlayer(player).ifPresent(account -> out.addAll(Arrays.asList(account.getUsername(), account.getPlayer().getName()))); + + return out; + } +} diff --git a/src/main/java/com/github/mafelp/accounts/DiscordLinker.java b/src/main/java/com/github/mafelp/accounts/DiscordLinker.java new file mode 100644 index 0000000..72b208d --- /dev/null +++ b/src/main/java/com/github/mafelp/accounts/DiscordLinker.java @@ -0,0 +1,84 @@ +package com.github.mafelp.accounts; + +import org.bukkit.entity.Player; +import org.javacord.api.entity.user.User; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Random; + +/** + * The class that handles linking on the Discord side of things. + */ +public class DiscordLinker { + /** + * The Map that contains as key the {@link Player} that created the link token and as the Value an {@link Integer} + * between 100,000 and 999,999. + */ + private static final Map linkableAccounts = new HashMap<>(); + + /** + * Checks the {@link DiscordLinker#linkableAccounts} list, if the {@link User} already has a linking token and + * if so, it returns the token from this map. If the user does not have a token yet, it creates one and + * adds it to the {@link DiscordLinker#linkableAccounts} map, associated with the user. + * @param user The user to get the Linking Token from. + * @return The Token used to link the account in minecraft. + */ + public static int getLinkToken(User user) { + if (Account.getByDiscordUser(user).isPresent()) + return -1; + + if (linkableAccounts.containsKey(user)) + return linkableAccounts.get(user); + else { + int linkID = randomLinkToken(); + linkableAccounts.put(user, linkID); + return linkID; + } + } + + /** + * The method used to create a linked {@link Account} with a discord user and the linking ID of a minecraft Player. + * The linkID has to be generated by {@link DiscordLinker#getLinkToken(User)}. + * @param player The minecraft {@link Player} to link the discord {@link User} to. + * @param linkID The ID used for finding the correct discord {@link User}. + * @return If the ID is a valid token, it returns the newly {@link Account}, which was added to the list of accounts. + * If the ID is invalid, it returns an empty account. + */ + public static Optional linkToMinecraft(Player player, int linkID) { + // Iterates over all the discord accounts + for (User u: linkableAccounts.keySet()) { + // if the linkID passed in matches the id generated in the DiscordLinker#getLinkToken function, + // it would create a new Account with the player and the User and adds this account. + if (linkableAccounts.get(u) == linkID) { + Account account = new Account(u, player); + + AccountManager.addAccount(account); + + //removes the current linkID from the map, so the link id would be freed again. + linkableAccounts.remove(u); + + return Optional.of(account); + } + } + + // If the linkID matches none of the ID in the linkableAccounts ID, return noting. + return Optional.empty(); + } + + /** + * The method used to create a new Linking token, aka a {@link Random}, 6-Digit number, that is not already + * in the linkableAccounts list. This prevents two users from having the same linking token. + * @return a random linking token. + */ + private static int randomLinkToken() { + Random random = new Random(); + int r; + do { + r = random.nextInt(899_999) + 100_000; + } while (linkableAccounts.containsValue(r)); + + return r; + } +} diff --git a/src/main/java/com/github/mafelp/accounts/MinecraftLinker.java b/src/main/java/com/github/mafelp/accounts/MinecraftLinker.java new file mode 100644 index 0000000..f74a46e --- /dev/null +++ b/src/main/java/com/github/mafelp/accounts/MinecraftLinker.java @@ -0,0 +1,82 @@ +package com.github.mafelp.accounts; + +import org.bukkit.entity.Player; +import org.javacord.api.entity.user.User; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Random; + +/** + * The class that handles linking on the minecraft side of things. + */ +public class MinecraftLinker { + /** + * The Map that contains as key the {@link Player} that created the link token and as the Value an {@link Integer} + * between 100,000 and 999,999. + */ + private static final Map linkableAccounts = new HashMap<>(); + + /** + * Checks the {@link MinecraftLinker#linkableAccounts} list, if the {@link Player} already has a linking token and + * if so, it returns the token from this map. If the player does not have a token yet, it creates one and + * adds it to the {@link MinecraftLinker#linkableAccounts} map, associated with the player. + * @param player The player to get the Linking Token from. + * @return The Token used to link the account in discord. + */ + public static int getLinkToken(Player player) { + if (Account.getByPlayer(player).isPresent()) + return -1; + + if (linkableAccounts.containsKey(player)) + return linkableAccounts.get(player); + else { + int linkID = randomLinkToken(); + linkableAccounts.put(player, linkID); + return linkID; + } + } + + /** + * The method used to create a new Linking token, aka a {@link Random}, 6-Digit number, that is not already + * in the linkableAccounts list. This prevents two users from having the same linking token. + * @return a random linking token. + */ + private static int randomLinkToken() { + Random random = new Random(); + int r; + do { + r = random.nextInt(899_999) + 100_000; + } while (linkableAccounts.containsValue(r)); + + return r; + } + + /** + * The method used to create a linked {@link Account} with a discord user and the linking ID of a minecraft Player. + * @param user The discord {@link User} to link the minecraft {@link Player} to. + * @param linkID The ID used for finding the correct minecraft {@link Player}. + * @return If the ID is a valid token, it returns the newly {@link Account}, which was added to the list of accounts. + * If the ID is invalid, it returns an empty account. + */ + public static Optional linkToDiscord(User user, int linkID) { + // Iterates over all the discord accounts + for (Player p: linkableAccounts.keySet()) { + // if the linkID passed in matches the id generated in the DiscordLinker#getLinkToken function, + // it would create a new Account with the player and the User and adds this account. + if (linkableAccounts.get(p) == linkID) { + Account account = new Account(user, p); + + AccountManager.addAccount(account); + + //removes the current linkID from the map, so the link id would be freed again. + linkableAccounts.remove(p); + + return Optional.of(account); + } + } + + return Optional.empty(); + } +} diff --git a/src/main/java/com/github/mafelp/discord/ChannelAdmin.java b/src/main/java/com/github/mafelp/discord/ChannelAdmin.java index 2585654..79b4901 100644 --- a/src/main/java/com/github/mafelp/discord/ChannelAdmin.java +++ b/src/main/java/com/github/mafelp/discord/ChannelAdmin.java @@ -2,17 +2,15 @@ import com.github.mafelp.utils.Logging; import com.github.mafelp.utils.Settings; -import com.github.mafelp.minecraft.skins.Skin; import org.bukkit.ChatColor; import org.bukkit.entity.Player; import org.javacord.api.entity.channel.Channel; import org.javacord.api.entity.channel.ServerTextChannel; import org.javacord.api.entity.channel.ServerTextChannelBuilder; -import org.javacord.api.entity.channel.TextChannel; import org.javacord.api.entity.message.embed.EmbedBuilder; import org.javacord.api.entity.server.Server; +import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder; -import java.awt.*; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletionException; @@ -52,12 +50,12 @@ protected static List getMessageChannels() { * @param server server on which to create the channel on * @param topic the topic the channel should have * @param successEmbed the embed to be sent into successChannel after completion - * @param successChannel the channel successEmbed is sent to + * @param responder The slash command responder to send the success message to. * @param welcomeEmbed the embed to sent to the newly created channel * @return the newly created channel */ public static ServerTextChannel createChannel(String name, Server server, String topic, - EmbedBuilder successEmbed, TextChannel successChannel, + EmbedBuilder successEmbed, InteractionImmediateResponseBuilder responder, EmbedBuilder welcomeEmbed) throws CompletionException { // Create the Channel ServerTextChannel serverTextChannel = new ServerTextChannelBuilder(server) @@ -74,7 +72,7 @@ public static ServerTextChannel createChannel(String name, Server server, String // Add a field containing a link to the new channel and send the embed successEmbed.addField("New Channel", "The new channel is: <#" + serverTextChannel.getIdAsString() + ">"); - successChannel.sendMessage(successEmbed); + responder.addEmbed(successEmbed).respond(); // Also send the welcome embed into the newly created channel serverTextChannel.sendMessage(welcomeEmbed); diff --git a/src/main/java/com/github/mafelp/discord/DiscordListener.java b/src/main/java/com/github/mafelp/discord/DiscordListener.java index 2104886..0a5fc92 100644 --- a/src/main/java/com/github/mafelp/discord/DiscordListener.java +++ b/src/main/java/com/github/mafelp/discord/DiscordListener.java @@ -42,12 +42,16 @@ public void onMessageCreate(MessageCreateEvent event) { if (event.getReadableMessageContent().startsWith(Settings.discordCommandPrefix)) return; + /* + * If the message is empty and only houses an embed. + * + * This embed can not be displayed in the in-game chat and would therefore be an empty message. + */ + if (event.getReadableMessageContent().equalsIgnoreCase("")) + return; + // Send the readable content of the message into the minecraft chat // for everyone to read. - // TODO broadcast version of message WITHOUT line break to the console and messages with line breaks to the players if Settings.shortMsg == true - // Settings.minecraftServer.broadcastMessage( - // msgPrefix(event) + event.getReadableMessageContent() - //); for (Player p : Settings.minecraftServer.getOnlinePlayers()) { p.sendMessage(msgPrefix(event) + event.getReadableMessageContent()); diff --git a/src/main/java/com/github/mafelp/discord/DiscordMain.java b/src/main/java/com/github/mafelp/discord/DiscordMain.java index 8f3270d..d2d175c 100644 --- a/src/main/java/com/github/mafelp/discord/DiscordMain.java +++ b/src/main/java/com/github/mafelp/discord/DiscordMain.java @@ -1,16 +1,20 @@ package com.github.mafelp.discord; -import com.github.mafelp.discord.commands.CreateRoleListener; -import com.github.mafelp.discord.commands.SetupListener; +import com.github.mafelp.accounts.AccountManager; +import com.github.mafelp.discord.commands.MainSlashCommandListener; import com.github.mafelp.utils.Logging; import com.github.mafelp.utils.Settings; -import com.github.mafelp.discord.commands.CreateChannelListener; import org.bukkit.ChatColor; import org.javacord.api.DiscordApiBuilder; +import org.javacord.api.entity.activity.ActivityType; import org.javacord.api.entity.permission.PermissionType; import org.javacord.api.entity.permission.Permissions; import org.javacord.api.entity.permission.PermissionsBuilder; +import org.javacord.api.entity.server.Server; +import org.javacord.api.interaction.*; +import java.io.IOException; +import java.util.*; import java.util.concurrent.CompletionException; import static com.github.mafelp.utils.Settings.discordApi; @@ -20,6 +24,26 @@ * The class that handles initiation and destruction of the discord bot instance(s) */ public class DiscordMain extends Thread { + /** + * The switch that decides, if the Accounts should be loaded after the login of the Bot instance. + */ + private final boolean loadAccounts; + + /** + * Constructor to set the {@link DiscordMain#loadAccounts} switch. + * @param loadAccounts if the accounts should be loaded after bot startup. + */ + public DiscordMain(boolean loadAccounts) { + this.loadAccounts = loadAccounts; + } + + /** + * Constructor for normal discord instance startup. Does not load the Accounts in. + */ + public DiscordMain() { + this.loadAccounts = false; + } + /** * Method used to create the bot instance and log it in */ @@ -49,40 +73,267 @@ public void run() { .setAllowed(PermissionType.MENTION_EVERYONE) .setAllowed(PermissionType.USE_EXTERNAL_EMOJIS) .setAllowed(PermissionType.ADD_REACTIONS) + .setAllowed(PermissionType.MANAGE_MESSAGES) .build(); - // TODO Change: make this function a thread / use bukkit scheduler // Log that the instance is being started Logging.info(ChatColor.DARK_GRAY + "Starting Discord Instance..."); // try to log the instance in and set it in the settings try { // Create the API Settings.discordApi = new DiscordApiBuilder() + .setWaitForServersOnStartup(true) // set the token, specified in the config.yml or with command "/token " .setToken(Settings.getApiToken()) // register listeners .addListener(DiscordListener::new) - .addListener(CreateChannelListener::new) - .addListener(CreateRoleListener::new) - .addListener(SetupListener::new) + .addListener(MainSlashCommandListener::new) + .addListener(MessageComponentCreationListener::new) // log the bot in and join the servers .login().join(); - // TODO Add: activity + + registerSlashCommands(); Logging.info(ChatColor.GREEN + "Successfully started the discord instance!"); Logging.info(ChatColor.RESET + "The bot invitation token is: " + discordApi.createBotInvite(botPermissions)); + Logging.info(ChatColor.YELLOW + "If you do not see any slash commands in your server, click this link and authorise this bot for your server: https://discord.com/api/oauth2/authorize?client_id=" + discordApi.getClientId() + "&scope=applications.commands"); } catch (IllegalStateException | CompletionException exception) { // If the API creation fails, // log an error to the console. - Logging.logException(exception, ChatColor.RED + + Logging.logException(exception, ChatColor.RED + "An error occurred whilst trying to create the discord instance! Error: " + exception.getMessage()); + return; + } + + // Checks the configuration and sets the according activity. + if (Settings.getConfiguration().getBoolean("activity.enabled", true)) { + String activityMessage = Objects.requireNonNull(Settings.getConfiguration().getString("activity.message", "to your messages 👀")); + String activityType = Objects.requireNonNull(Settings.getConfiguration().getString("activity.type", "listening")).toUpperCase(Locale.ROOT); + + discordApi.updateActivity(ActivityType.valueOf(activityType), activityMessage); + Logging.info("Set the activity to type " + ChatColor.GRAY + activityType + ChatColor.RESET + " and the text to " + ChatColor.GRAY + activityMessage + ChatColor.RESET + "."); + } + + if (this.loadAccounts) { + // Loads all the Accounts to memory + try { + AccountManager.loadAccounts(); + } catch (IOException e) { + Logging.logIOException(e, "Could not load the Accounts in. The Account file is not present and it could not be created."); + } + } + + // Send an Event message that this player has joined + if (Settings.events.contains("ServerStartupEvent")) { + DiscordMessageBroadcast discordMessageBroadcast = new DiscordMessageBroadcast( + "Server Startup", + "The server has been started and is now ready to connect!", + null); + discordMessageBroadcast.setName("StartupEventBroadcaster"); + discordMessageBroadcast.start(); + } + } + + + /** + * Method to register all slash commands (in bulk). + */ + private void registerSlashCommands() { + List accountSlashCommands = new ArrayList<>(); + List adminSlashCommands = new ArrayList<>(); + + // global help command. Is always enabled, but needs to be added to the account slash commands list, + // as the list will be the list of global commands and overwrite any others created before. + accountSlashCommands.add(SlashCommand.with("help", "A command to give help about this bot and its commands") + .setDefaultPermission(true) + ); + + // Account command + accountSlashCommands.add(SlashCommand.with("account", "A command for account management", + Arrays.asList( + SlashCommandOption.createWithOptions(SlashCommandOptionType.SUB_COMMAND, "link", "Link your discord and minecraft accounts", + Collections.singletonList( + SlashCommandOption.create(SlashCommandOptionType.LONG, "token", "The OPTIONAL token to link your accounts", false) + )), + SlashCommandOption.createWithOptions(SlashCommandOptionType.SUB_COMMAND, "name", "Change or get your username for the accounts", + Collections.singletonList( + SlashCommandOption.create(SlashCommandOptionType.STRING, "change-to", "The string to change your name to. Leave blank to get current name.", false) + )), + SlashCommandOption.createWithOptions(SlashCommandOptionType.SUB_COMMAND, "username", "Change or get your username for the accounts", + Collections.singletonList( + SlashCommandOption.create(SlashCommandOptionType.STRING, "change-to", "The string to change your name to. Leave blank to get current name.", false) + )), + SlashCommandOption.createWithOptions(SlashCommandOptionType.SUB_COMMAND, "get", "Gets information about an account", + Collections.singletonList( + SlashCommandOption.create(SlashCommandOptionType.USER, "account", "Get information about this user. Leave blank to get yourself.", false) + )), + SlashCommandOption.create(SlashCommandOptionType.SUB_COMMAND, "list", "List all currently linked accounts"), + SlashCommandOption.create(SlashCommandOptionType.SUB_COMMAND, "unlink", "Unlink your discord account from your minecraft account.") + ) + ).setDefaultPermission(true)); + // Link command + accountSlashCommands.add(SlashCommand.with("link", "A command to link your discord and minecraft accounts", + Collections.singletonList( + SlashCommandOption.create(SlashCommandOptionType.LONG, "token", "The token used to link your accounts", false) + ) + ).setDefaultPermission(true)); + + // Unlink command + accountSlashCommands.add(SlashCommand.with("unlink", "Unlink your discord account from your minecraft account")); + + // Whisper and mcmsg commands + accountSlashCommands.add(SlashCommand.with("whisper", "Whisper to your friends on the minecraft server!", + Arrays.asList( + SlashCommandOption.create(SlashCommandOptionType.USER, "user", "The user to whisper your message to", true), + SlashCommandOption.create(SlashCommandOptionType.STRING, "message", "What you want to whisper", true) + )) + .setDefaultPermission(true) + ); + accountSlashCommands.add(SlashCommand.with("mcmsg", "Whisper to your friends on the minecraft server!", + Arrays.asList( + SlashCommandOption.create(SlashCommandOptionType.USER, "user", "The user to whisper your message to", true), + SlashCommandOption.create(SlashCommandOptionType.STRING, "message", "What you want to whisper", true) + )).setDefaultPermission(true)); + + // Create role and create channel commands + adminSlashCommands.add(SlashCommand.with("create", "Create a channel/role for syncing minecraft and discord messages", + Arrays.asList( + SlashCommandOption.createWithOptions(SlashCommandOptionType.SUB_COMMAND, "channel", "Create a channel to sync minecraft messages to", + Collections.singletonList( + SlashCommandOption.create(SlashCommandOptionType.STRING, "name", "The name the channel should have", true) + )), + SlashCommandOption.createWithOptions(SlashCommandOptionType.SUB_COMMAND, "role", "Create a role which you can give the permission to read/write to channels", + Collections.singletonList( + SlashCommandOption.create(SlashCommandOptionType.STRING, "name", "The name the channel should have", true) + )) + )) + .setDefaultPermission(false) + ); + + // Setup command + adminSlashCommands.add(SlashCommand.with("setup","Creates and channel and a role for syncing minecraft and discord messages", + Collections.singletonList( + SlashCommandOption.create(SlashCommandOptionType.STRING, "name", "The name of the role and the channel", true) + )) + .setDefaultPermission(false) + ); + + // The account command for administrators + adminSlashCommands.add(SlashCommand.with("account", "A command for account management", + Arrays.asList( + SlashCommandOption.create(SlashCommandOptionType.SUB_COMMAND, "save", "Saves the current memory state to the accounts file."), + SlashCommandOption.create(SlashCommandOptionType.SUB_COMMAND, "reload", "Reloads the accounts from the save file and overrides the memory accounts. USE WITH CAUTION"), + SlashCommandOption.createWithOptions(SlashCommandOptionType.SUB_COMMAND, "remove", "Removed the account of given discord user.", + Collections.singletonList( + SlashCommandOption.create(SlashCommandOptionType.USER, "user", "The user to delete the account from", true) + )) + ) + ).setDefaultPermission(true)); + + // The config command for the bot & plugin configuration + adminSlashCommands.add(SlashCommand.with("config", "Manage the bot's and plugin's configuration", + Arrays.asList( + // default + SlashCommandOption.create(SlashCommandOptionType.SUB_COMMAND, "default", "Restores the configuration to its defaults."), + // save + SlashCommandOption.create(SlashCommandOptionType.SUB_COMMAND, "save", "Saves the configuration in its current state to the config file, overriding changes to the file."), + //reload + SlashCommandOption.create(SlashCommandOptionType.SUB_COMMAND, "reload", "Loads the configuration from the configuration file, overriding current changes."), + // set + SlashCommandOption.createWithOptions(SlashCommandOptionType.SUB_COMMAND, "set", "Sets a value in the configuration.", + Arrays.asList( + SlashCommandOption.create(SlashCommandOptionType.STRING, "path", "The configuration path of the element", true), + SlashCommandOption.create(SlashCommandOptionType.STRING, "value", "The value that will be set to in the configuration", true) + )), + // get + SlashCommandOption.createWithOptions(SlashCommandOptionType.SUB_COMMAND, "get", "Gets a value/list from the configuration", + Collections.singletonList( + SlashCommandOption.create(SlashCommandOptionType.STRING, "path", "The configuration path to get the value of", true) + )), + // add + SlashCommandOption.createWithOptions(SlashCommandOptionType.SUB_COMMAND, "add", "Appends a value to a list", + Arrays.asList( + SlashCommandOption.create(SlashCommandOptionType.STRING, "path", "The configuration path of the list", true), + SlashCommandOption.create(SlashCommandOptionType.STRING, "value", "The value to add to the list", true) + )), + // remove + SlashCommandOption.createWithOptions(SlashCommandOptionType.SUB_COMMAND, "remove", "Removed a value from a list", + Arrays.asList( + SlashCommandOption.create(SlashCommandOptionType.STRING, "path", "The configuration path of the list", true), + SlashCommandOption.create(SlashCommandOptionType.STRING, "value", "The value to remove from the list", true) + )) + ) + ).setDefaultPermission(true)); + + // If linking is NOT enabled, set the default permission for all the slash commands to false. No one can use them then + if (!Settings.getConfiguration().getBoolean("enableLinking", true)) { + Logging.info("Linking is not enabled. Setting permission for all slash commands to false."); + int i = 0; + for (SlashCommandBuilder slashCommandBuilder : accountSlashCommands) { + if ( i != 0) + slashCommandBuilder.setDefaultPermission(false); + i++; + } } + var _slashCommands = discordApi.bulkOverwriteGlobalApplicationCommands(accountSlashCommands).join(); + _slashCommands.forEach(slashCommand -> { + Logging.info("Added global slash command \"/" + slashCommand.getName() + "\""); + Logging.debug("Default-Permission for \"/" + slashCommand.getName() + "\": " + slashCommand.getDefaultPermission()); + }); + for (Server server : discordApi.getServers()) { + _slashCommands = discordApi.bulkOverwriteServerApplicationCommands(server, adminSlashCommands).join(); + // Setup a list with all allowed Users, configured in the config file and the bot owner + var allowedUserIDs = Settings.getConfiguration().getLongList("permission.discordServerAdmin.allowedUserIDs"); + allowedUserIDs = Settings.getConfiguration().getLongList("permission.discordBotAdmin.allowedUserIDs"); + allowedUserIDs.remove(1234L); + if (!allowedUserIDs.contains(discordApi.getOwnerId())) + allowedUserIDs.add(discordApi.getOwnerId()); + + // Register the slash commands for each server + List updatedSlashCommands = new ArrayList<>(); + List permissions = new ArrayList<>(); + Logging.debug("Updating admin slash command permission for server " + server.getName()); + // Check if the server owner of this server is in the allowed lists. + // If not, add them only for this server and remove them afterwards. + boolean serverOwnerIsAllowed = allowedUserIDs.contains(server.getOwnerId()); + if (!serverOwnerIsAllowed) + allowedUserIDs.add(server.getOwnerId()); + + // Create permission to use this slash command for each allowed user + allowedUserIDs.forEach(userID -> permissions.add(ApplicationCommandPermissions.create(userID, ApplicationCommandPermissionType.USER, true))); + // Prepare the commands to have the new permissions: Allow all allowed users to use this slash command. + _slashCommands.forEach(slashCommand -> updatedSlashCommands.add(new ServerApplicationCommandPermissionsBuilder(slashCommand.getId(), permissions))); + + // Do the actual updates + discordApi.batchUpdateApplicationCommandPermissions(server, updatedSlashCommands).thenAccept(serverSlashCommandPermissions -> + Logging.info("Updated admin slash command permissions for server " + server.getName())); + + if (!serverOwnerIsAllowed) + allowedUserIDs.remove(server.getOwnerId()); + + permissions.clear(); + updatedSlashCommands.clear(); + } + Logging.info(ChatColor.GREEN + "Registered slash Commands."); + Logging.info(ChatColor.YELLOW + "Information: Due to caching, it can take " + ChatColor.BOLD + "UP TO" + ChatColor.RESET + ChatColor.YELLOW + " an hour"); + Logging.info(ChatColor.YELLOW + "Information: until the slash commands can be used (until they appear or the message"); + Logging.info(ChatColor.YELLOW + "Information: \"InvalidInteractionID\" disappears when trying to use a slash command)"); } /** * Shutdown method to disconnect the bot instance */ public static void shutdown() { + // Send an Event message that this player has joined + if (Settings.events.contains("ServerShutdownEvent")) { + DiscordMessageBroadcast discordMessageBroadcast = new DiscordMessageBroadcast( + "Server Stopped", + "The server has been stopped.", + null); + discordMessageBroadcast.setName("ShutdownEventBroadcaster"); + discordMessageBroadcast.start(); + } + // check if the bot is already logged out if (Settings.discordApi == null) { // if so, log a message and return @@ -93,7 +344,9 @@ public static void shutdown() { Logging.info(ChatColor.DARK_GRAY + "Shutting down Discord Instance..."); // Disconnect the bot / shut the bot down - Settings.discordApi.disconnect(); + try { + Settings.discordApi.disconnect(); + } catch (IllegalStateException ignored) {} Settings.discordApi = null; } } diff --git a/src/main/java/com/github/mafelp/discord/DiscordMessageBroadcast.java b/src/main/java/com/github/mafelp/discord/DiscordMessageBroadcast.java index 7fce737..126ad60 100644 --- a/src/main/java/com/github/mafelp/discord/DiscordMessageBroadcast.java +++ b/src/main/java/com/github/mafelp/discord/DiscordMessageBroadcast.java @@ -1,10 +1,12 @@ package com.github.mafelp.discord; +import com.github.mafelp.accounts.Account; import com.github.mafelp.minecraft.skins.Skin; import com.github.mafelp.utils.Settings; import org.bukkit.entity.Player; import org.javacord.api.entity.channel.Channel; import org.javacord.api.entity.message.embed.EmbedBuilder; +import org.jetbrains.annotations.Nullable; import java.awt.*; import java.util.List; @@ -18,19 +20,73 @@ public class DiscordMessageBroadcast extends Thread { */ private final Player messageAuthor; + /** + * The command that is being executed. + */ + private final String command; + /** * What the player has sent. */ private final String message; /** - * Constructor to give the function all needed information. + * The type of the broadcast. + */ + private final BroadcastType broadcastType; + + /** + * Constructor to define this thread as a message broadcast. * @param messageAuthor The person, who sent the message. * @param message The message, the messageAuthor has sent. */ public DiscordMessageBroadcast(Player messageAuthor, String message) { this.messageAuthor = messageAuthor; this.message = message; + + this.broadcastType = BroadcastType.chatMessageBroadcast; + + this.command = null; + } + + /** + * Constructor to define this thread as a command-info broadcast by the server. + * @param command The command that was executed. + */ + public DiscordMessageBroadcast(String command) { + this.command = command; + + this.broadcastType = BroadcastType.serverCommandBroadcast; + + this.messageAuthor = null; + this.message = null; + } + + /** + * The constructor to define this thread as a command-info broadcast by a player. + * @param command The command the player executed. + * @param player The player who executed the command. + */ + public DiscordMessageBroadcast(String command, Player player) { + this.command = command; + this.messageAuthor = player; + + this.broadcastType = BroadcastType.playerCommandBroadcast; + + this.message = null; + } + + /** + * The constructor to define this thread as an event broadcast for discord channel. + * @param event The event that has happened + * @param message The message to accompany this event + * @param messageAuthor The player that might be referenced in this event + */ + public DiscordMessageBroadcast(String event, String message, @Nullable Player messageAuthor) { + this.command = event; + this.message = message; + this.messageAuthor = messageAuthor; + this.broadcastType = BroadcastType.eventBroadcast; } /** @@ -38,6 +94,19 @@ public DiscordMessageBroadcast(Player messageAuthor, String message) { */ @Override public void run() { + switch (this.broadcastType) { + case chatMessageBroadcast -> messageBroadcast(); + case playerCommandBroadcast -> playerCommandBroadcast(); + case serverCommandBroadcast -> serverCommandBroadcast(); + case eventBroadcast -> eventBroadcast(); + } + } + + /** + * Sends the give embed to all configured channels. + * @param embed The embed to send to the different channels. + */ + private void sendMessages(EmbedBuilder embed) { // get all channels, where the message should be send to List channels = ChannelAdmin.getMessageChannels(); @@ -47,21 +116,117 @@ public void run() { return; } + // Send the message to each channel, if it is a text channel. + channels.forEach(channel -> + channel.asTextChannel().ifPresent(textChannel -> + textChannel.sendMessage(embed) + ) + ); + } + + /** + * The method to send a chat message. + */ + private void messageBroadcast() { // create an embed for the message - //TODO Add: show head of player as author picture EmbedBuilder embed = new EmbedBuilder() - // .setAuthor(messageAuthor.getDisplayName()) - .setAuthor(messageAuthor.getDisplayName(), "", new Skin(messageAuthor, false).getHead(), ".png") .setColor(Color.YELLOW) - .setFooter("On " + Settings.serverName) - .addInlineField("Message:", message); - - // send the embed to all channels in the list - for (Channel channel : channels) { - // only send the embed, if the channel is present, to avoid exceptions - if (channel.asServerTextChannel().isPresent()) { - channel.asServerTextChannel().get().sendMessage(embed); - } + .setDescription(message); + + if (Account.getByPlayer(messageAuthor).isPresent()) + embed.setAuthor(Account.getByPlayer(messageAuthor).get().getUser()); + else { + assert messageAuthor != null; + embed.setAuthor(messageAuthor.getDisplayName(), "https://namemc.com/profile/" + messageAuthor.getName(), new Skin(messageAuthor, false).getHead(), ".png"); } + if (Settings.getConfiguration().getBoolean("showFooterInMessages", true)) + embed.setFooter("On " + Settings.serverName); + + sendMessages(embed); + } + + /** + * The method to send a server command embed. + */ + private void serverCommandBroadcast() { + // create an embed for the message + EmbedBuilder embed = new EmbedBuilder() + .setColor(new Color(0xA245F3)) + .setAuthor(Settings.discordApi.getYourself()) + .setTitle("Server Command was executed!") + .setDescription(command); + + if (Settings.getConfiguration().getBoolean("showFooterInMessages", true)) + embed.setFooter("On " + Settings.serverName); + + sendMessages(embed); + } + + /** + * The method to send a player command embed. + */ + private void playerCommandBroadcast() { + assert messageAuthor != null; + + // create an embed for the message + EmbedBuilder embed = new EmbedBuilder() + .setColor(new Color(0x5645F3)) + .setAuthor(Settings.discordApi.getYourself()) + .setTitle("Player " + messageAuthor.getName() + " executed a command!") + .setDescription(command); + + if (Account.getByPlayer(messageAuthor).isPresent()) + embed.setAuthor(Account.getByPlayer(messageAuthor).get().getUser()); + else + embed.setAuthor(messageAuthor.getDisplayName(), "https://namemc.com/profile/" + messageAuthor.getName(), new Skin(messageAuthor, false).getHead(), ".png"); + + if (Settings.getConfiguration().getBoolean("showFooterInMessages", true)) + embed.setFooter("On " + Settings.serverName); + + sendMessages(embed); + } + + private void eventBroadcast() { + // create an embed for the message + EmbedBuilder embed = new EmbedBuilder() + .setColor(new Color(0x17A288)) + .setAuthor(Settings.discordApi.getYourself()) + .setTitle(command) + .setDescription(message); + + if (messageAuthor != null) { + if (Account.getByPlayer(messageAuthor).isPresent()) + embed.setAuthor(Account.getByPlayer(messageAuthor).get().getUser()); + else + embed.setAuthor(messageAuthor.getDisplayName(), "https://namemc.com/profile/" + messageAuthor.getName(), new Skin(messageAuthor, false).getHead(), ".png"); + } + + if (Settings.getConfiguration().getBoolean("showFooterInMessages", true)) + embed.setFooter("On " + Settings.serverName); + + sendMessages(embed); } } + +/** + * The type of the broadcast. + */ +enum BroadcastType { + /** + * A player executed a command. + */ + playerCommandBroadcast, + /** + * The server executed a command. + */ + serverCommandBroadcast, + /** + * A player sent a normal message. + */ + chatMessageBroadcast, + + /** + * If an event was emitted + */ + eventBroadcast +} \ No newline at end of file diff --git a/src/main/java/com/github/mafelp/discord/MessageComponentCreationListener.java b/src/main/java/com/github/mafelp/discord/MessageComponentCreationListener.java new file mode 100644 index 0000000..d2e9a8c --- /dev/null +++ b/src/main/java/com/github/mafelp/discord/MessageComponentCreationListener.java @@ -0,0 +1,253 @@ +package com.github.mafelp.discord; + +import com.github.mafelp.utils.CheckPermission; +import com.github.mafelp.utils.Logging; +import com.github.mafelp.utils.Permissions; +import org.bukkit.ChatColor; +import org.bukkit.configuration.InvalidConfigurationException; +import org.javacord.api.entity.message.component.ActionRow; +import org.javacord.api.entity.message.component.Button; +import org.javacord.api.entity.message.embed.EmbedBuilder; +import org.javacord.api.event.interaction.MessageComponentCreateEvent; +import org.javacord.api.interaction.MessageComponentInteraction; +import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder; +import org.javacord.api.listener.interaction.MessageComponentCreateListener; + +import java.awt.*; + +import static com.github.mafelp.utils.Settings.createDefaultConfig; +import static com.github.mafelp.utils.Settings.getConfiguration; + +public class MessageComponentCreationListener implements MessageComponentCreateListener { + @Override + public void onComponentCreate(MessageComponentCreateEvent event) { + MessageComponentInteraction interaction = event.getMessageComponentInteraction(); + + switch (interaction.getCustomId()) { + case "helpSelectMenu" -> helpSelectMenu(interaction); + case "configResetConfirm" -> configResetConfirm(interaction); + case "configResetCancel" -> configResetAbort(interaction); + default -> Logging.info(ChatColor.RED + "Wait what? This interaction should not have been caught! Invalid Interaction: " + interaction.getCustomId()); + } + } + + private static void helpSelectMenu(MessageComponentInteraction interaction) { + if (interaction.asSelectMenuInteraction().isEmpty()) + return; + if (interaction.getMessage().canYouDelete()) { + interaction.getMessage().delete("Message no longer needed, as the help command continues.").thenAccept(none -> + Logging.debug("Removed original help message.")); + } + + InteractionImmediateResponseBuilder message = interaction.createImmediateResponder() + .append("Your requested help page(s):") + .addComponents(ActionRow.of(Button.link("https://mafelp.github.io/MCDC/", "View on the web"))); + EmbedBuilder embed = new EmbedBuilder() + .setAuthor(interaction.getUser()) + .setColor(new Color(0xDE9A4C)) + .setTimestampToNow() + ; + interaction.asSelectMenuInteraction().get().getChosenOptions().forEach(selectMenuOption -> { + switch (selectMenuOption.getLabel()) { + case "Account" -> { + message.addEmbed(embed + .setTitle("Account Help") + .setDescription("Here is the information about the custom accounts\n\nFor more account controls, please log in to the Minecraft Server and use tab completion on the `/account` command") + .addField("/link", "Links your discord account to your Minecraft Account on this server.") + .addField("/unlink", "Removes the discord - Minecraft account link") + ); + Logging.debug("Added Account embed to the help message."); + } + case "Config" -> { + message.addEmbed(embed + .setTitle("Config Help") + .setDescription(""" + Administrators can change the configuration of this bot. + + This feature is currently only available as a server command! + As an administrator, you can also use the minecraft server console. + + To see available options, please use the `Tab` Key when typing `/config `. + """) + ); + Logging.debug("Added Config embed to the help message."); + } + case "Create Channel" -> { + message.addEmbed(embed + .setTitle("Create Channel Help") + .setDescription(""" + Takes one argument: + - The channel name + + What does this command do? It creates a channel with the specified name and adds it to its configuration to send all the minecraft messages to. + + It is advised to not use this command, but use the `/setup` command instead! + + This command can only be used by Server/Bot Admins!""") + ); + Logging.debug("Added createChannel embed to the help message."); + } + case "Create Role" -> { + message.addEmbed(embed + .setTitle("Create Role Help") + .setDescription(""" + Takes one argument: + - The role name + + What does this command do? It creates a new role with the specified name, so you can grant this role access to the minecraft related channels. + + It is advised to not use this command, but use the `/setup` command instead! + + This command can only be used by Server/Bot Admins!""") + ); + Logging.debug("Added createRole embed to the help message."); + } + case "Help" -> { + message.addEmbed(embed + .setTitle("Help") + .setDescription(""" + Gets you a menu, with which you can get further information on all the commands. + + If you still need further help, please write the developers an email (mafelp@protonmail.ch)!""") + ); + Logging.debug("Added Help embed to the help message."); + } + case "Link" -> { + message.addEmbed(embed + .setTitle("Link Help") + .setDescription(""" + This command either takes one optional argument: A link token. + You can obtain a link token, by running `/link` in minecraft. + If you leave the token field blank, this command will generate you a link token, which you can then use as an argument in minecraft to link your accounts. + + Linking of your account enables you for whisper messages between discord users and minecraft users (and the other way round as well), as well as getting clickable mentions if mentioned with `@` in the minecraft chat.""") + ); + Logging.debug("Added Link embed to the help message."); + } + case "mcmsg" -> { + message.addEmbed(embed + .setTitle("mcmsg Help") + .setDescription(""" + Send a private message to a person on discord, that just they can see! + + This command takes two arguments: + - The first argument is the user(name) that you want to send the message to. + - The second argument is the message you want to send them.""") + ); + Logging.debug("Added mcmsg embed to the help message."); + } + case "Setup" -> { + message.addEmbed(embed + .setTitle("Setup Help") + .setDescription(""" + Setup a new channel for incoming messages and a new role to manage permissions with. + + This command takes one argument: The name of the new channel and role (it will be the same name)""") + ); + Logging.debug("Added Setup embed to the help message."); + } + case "Unlink" -> { + message.addEmbed(embed + .setTitle("Unlink Help") + .setDescription("Unlinks your discord and minecraft accounts. If you want to link your accounts, run `/link` either in discord or in minecraft.") + ); + Logging.debug("Added Unlink embed to the help message."); + } + case "Whisper" -> { + message.addEmbed(embed + .setTitle("Whisper Help") + .setDescription(""" + Send a private message to a person on discord, that just they can see! + + This command takes two arguments: + - The first argument is the user(name) that you want to send the message to. + - The second argument is the message you want to send them.""") + ); + Logging.debug("Added Whisper embed to the help message."); + } + default -> { + message.addEmbed(new EmbedBuilder() + .setColor(new Color(0xFF0048)) + .setTitle("Error!") + .setAuthor(interaction.getApi().getYourself()) + .setDescription(""" + An internal server error occurred! + + Please create an issue here: https://github.com/MaFeLP/MCDC/issues/new + and reference the following error message: + + MessageComponentCreationListener.java: Unrecognised selectMenuLabel \"""" + selectMenuOption.getLabel() + "\"") + ); + Logging.debug("Added Account embed to the help message."); + } + } + }); + message.respond(); + } + + private static void configResetConfirm(MessageComponentInteraction interaction) { + if (incidentReport(interaction)) return; + + EmbedBuilder replyEmbed = new EmbedBuilder().setAuthor(interaction.getUser()); + try { + // tries to set the configuration to the defaults. + getConfiguration().loadFromString(createDefaultConfig().saveToString()); + replyEmbed.setTitle("Success!") + .setColor(Color.GREEN) + .setDescription("Successfully reloaded the config file from its defaults!"); + Logging.info(ChatColor.GRAY + interaction.getUser().getName() + ChatColor.RESET + " successfully reset the config file to its defaults!"); + } catch (InvalidConfigurationException e) { + Logging.logInvalidConfigurationException(e, "Error whilst resetting the config to the defaults."); + replyEmbed.setTitle("Error!") + .setColor(Color.RED) + .setDescription(""" + The Configuration is invalid! + + This should not happen! + + Please open an issue at https://github.com/MaFeLP/MCDC/issues/new"""); + } finally { + interaction.getMessage().createUpdater().removeAllEmbeds().addEmbed(replyEmbed).replaceMessage().join(); + Logging.debug("Updated the original message."); + } + } + + private static void configResetAbort(MessageComponentInteraction interaction) { + if (incidentReport(interaction)) return; + + Logging.info(ChatColor.GRAY + interaction.getUser().getName() + ChatColor.RESET + " successfully aborted the reset of the config file!"); + interaction.getMessage().createUpdater().removeAllEmbeds().addEmbed(new EmbedBuilder() + .setAuthor(interaction.getUser()) + .setTitle("Aborted!") + .setColor(Color.ORANGE) + .setDescription("Aborted resetting the configuration to its defaults.") + ).replaceMessage().join(); + Logging.debug("Updated the original message."); + } + + /** + * The method used to check the {@link com.github.mafelp.utils.Permissions}: {@code accountEdit} of a command Sender. + * @param event The command that will be shown into the console, if the permission is denied. + * @return If the permission was granted. + */ + private static boolean incidentReport(MessageComponentInteraction event) { + EmbedBuilder errorEmbed = new EmbedBuilder() + .setAuthor(event.getUser()) + .setTitle("Error") + .setColor(Color.RED) + .setFooter("Use \"/help\" for help!"); + + if (!CheckPermission.checkPermission(Permissions.discordBotAdmin, event.getUser().getId()) + && !CheckPermission.checkPermission(Permissions.discordServerAdmin, event.getUser().getId()) + ) { + event.createImmediateResponder().addEmbed(errorEmbed.setDescription(""" + Sorry, you don't have the required permissions, to execute this command! + + This incident will be reported!""" + )).respond().join(); + Logging.info("DC User " + ChatColor.DARK_GRAY + event.getUser().getName() + ChatColor.RESET + " tried to execute the command " + ChatColor.DARK_GRAY + "config" + ChatColor.RESET + "! This action was denied due to missing permission!"); + return true; + } + return false; + } +} diff --git a/src/main/java/com/github/mafelp/discord/RoleAdmin.java b/src/main/java/com/github/mafelp/discord/RoleAdmin.java index 8fba134..2447d69 100644 --- a/src/main/java/com/github/mafelp/discord/RoleAdmin.java +++ b/src/main/java/com/github/mafelp/discord/RoleAdmin.java @@ -1,10 +1,12 @@ package com.github.mafelp.discord; +import com.github.mafelp.utils.Settings; import org.bukkit.ChatColor; -import org.javacord.api.entity.channel.ServerTextChannel; import org.javacord.api.entity.message.embed.EmbedBuilder; import org.javacord.api.entity.permission.*; import org.javacord.api.entity.server.Server; +import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder; +import org.jetbrains.annotations.Nullable; import java.awt.*; import java.util.concurrent.CompletionException; @@ -21,12 +23,14 @@ public class RoleAdmin { * Method creates a new Role on a server with the specified name. * @param server The server to create the new role on. * @param name The name of the new role. - * @param successEmbed The embed to sent the user on success. - * @param successChannel The channel to sent the successEmbed to. + * @param successEmbed The Embed to send on a success. + * @param successBuilder The immediate responseBuilder to answer to the slash commands. * @return The newly created role */ public static Role createNewRole(Server server, String name, - EmbedBuilder successEmbed, ServerTextChannel successChannel) throws CompletionException { + @Nullable EmbedBuilder successEmbed, + @Nullable InteractionImmediateResponseBuilder successBuilder) throws CompletionException { + // Set the permissions the new role should have. Permissions permissions = new PermissionsBuilder() .setAllowed(PermissionType.ADD_REACTIONS) .setDenied(PermissionType.ADMINISTRATOR) @@ -61,6 +65,7 @@ public static Role createNewRole(Server server, String name, .build() ; + // Build the Role. This throws the CompletionException Role role = new RoleBuilder(server) .setColor(new Color(194, 98, 94)) .setAuditLogReason("MCDC: Minecraft Server Role creation") @@ -72,13 +77,24 @@ public static Role createNewRole(Server server, String name, info("Created new Role " + ChatColor.GRAY + role.getName() + ChatColor.RESET + " on server " + ChatColor.RESET + server.getName() + "!"); - if (successEmbed != null) - successChannel.sendMessage(successEmbed.addField("New Role", "The new role is: " + role.getMentionTag() + "!") - .addField("Usage:", "Give the role to any members that should be allowed to view and write to the minecraft channel. Later this will get added automatically with linking!")); + // Send the success embed, if one exists. + if (successEmbed != null && successBuilder != null) + successBuilder.addEmbed(successEmbed.addField("New Role", "The new role is: " + role.getMentionTag() + "!") + .addField("Usage:", "Give the role to any members that should be allowed to view and write to the minecraft channel. Later this will get added automatically with linking!") + ).respond(); discordApi.getYourself().addRole(role, "MCDC needs to see the channel as well!"); info("Added role \"" + role.getName() + "\" to the discord API."); + var roleIDs = Settings.getConfiguration().getLongList("roleIDs"); + if (roleIDs.get(0) == 1234L) { + roleIDs.set(0, role.getId()); + } else { + roleIDs.add(role.getId()); + } + Settings.getConfiguration().set("roleIDs", roleIDs); + Settings.saveConfiguration(); + return role; } } diff --git a/src/main/java/com/github/mafelp/discord/commands/AccountListener.java b/src/main/java/com/github/mafelp/discord/commands/AccountListener.java new file mode 100644 index 0000000..94cb134 --- /dev/null +++ b/src/main/java/com/github/mafelp/discord/commands/AccountListener.java @@ -0,0 +1,386 @@ +package com.github.mafelp.discord.commands; + +import com.github.mafelp.accounts.Account; +import com.github.mafelp.accounts.AccountManager; +import com.github.mafelp.utils.CheckPermission; +import com.github.mafelp.utils.Logging; +import com.github.mafelp.utils.Permissions; +import com.github.mafelp.utils.Settings; +import org.bukkit.ChatColor; +import org.bukkit.OfflinePlayer; +import org.javacord.api.entity.message.embed.EmbedBuilder; +import org.javacord.api.entity.user.User; +import org.javacord.api.interaction.SlashCommandInteraction; +import org.javacord.api.interaction.SlashCommandInteractionOption; +import org.javacord.api.interaction.callback.InteractionOriginalResponseUpdater; +import org.jetbrains.annotations.NotNull; + +import java.awt.*; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; +import java.util.*; + +/** + * The class that listens to the discord chats, if the channel creation command is executed. - + * As discord announced just today, there will be an update to the bot API, that'll be adding + * slash command support. This class will be moved, if the update is available in this API. + */ +public class AccountListener { + /** + * The method called by the discord API, for every chat message. - + * This method will filter them and execute commands accordingly. + * @param event The event containing information about the message. + */ + public static void onSlashCommand(SlashCommandInteraction event) { + User author = event.getUser(); + // help message for wrong usage + EmbedBuilder helpMessage = new EmbedBuilder() + .setAuthor(author) + .setTitle("Error") + .addField("Usage", "/account (