├── .github ├── CONTRIBUTING.md └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── announce.json ├── colors.json ├── config.json ├── data.json ├── flags.json ├── items.json ├── pom.xml ├── recipes.json ├── src ├── br │ └── com │ │ └── azalim │ │ └── mcserverping │ │ ├── MCPing.java │ │ ├── MCPingOptions.java │ │ ├── MCPingResponse.java │ │ └── MCPingUtil.java └── com │ └── tisawesomeness │ └── minecord │ ├── Announcement.java │ ├── Bot.java │ ├── Config.java │ ├── DiscordLogger.java │ ├── GuildListener.java │ ├── Loader.java │ ├── Main.java │ ├── StatusListener.java │ ├── command │ ├── Command.java │ ├── CommandListener.java │ ├── ExtraHelpPage.java │ ├── LegacyCommand.java │ ├── Module.java │ ├── Registry.java │ ├── SlashCommand.java │ ├── admin │ │ ├── BanCommand.java │ │ ├── DebugCommand.java │ │ ├── DemoteCommand.java │ │ ├── DeployCommand.java │ │ ├── EvalCommand.java │ │ ├── MsgCommand.java │ │ ├── NameCommand.java │ │ ├── PermsCommand.java │ │ ├── PromoteCommand.java │ │ ├── ReloadCommand.java │ │ ├── SayCommand.java │ │ ├── ShutdownCommand.java │ │ ├── TestCommand.java │ │ └── UsageCommand.java │ ├── core │ │ ├── CreditsCommand.java │ │ ├── CreditsCommandLegacy.java │ │ ├── HelpCommand.java │ │ ├── HelpCommandLegacy.java │ │ ├── InfoCommand.java │ │ ├── InfoCommandAdmin.java │ │ ├── InfoCommandLegacy.java │ │ ├── InviteCommand.java │ │ ├── PingCommand.java │ │ ├── PingCommandLegacy.java │ │ ├── PrefixCommand.java │ │ ├── SettingsCommand.java │ │ ├── SettingsCommandAdmin.java │ │ └── VoteCommand.java │ ├── discord │ │ ├── GuildCommand.java │ │ ├── GuildCommandAdmin.java │ │ ├── IdCommand.java │ │ ├── RoleCommand.java │ │ ├── RoleCommandAdmin.java │ │ ├── RolesCommand.java │ │ ├── UserCommand.java │ │ └── UserCommandAdmin.java │ ├── player │ │ ├── AbstractPlayerCommand.java │ │ ├── AnsiCommand.java │ │ ├── BasePlayerCommand.java │ │ ├── BaseRenderCommand.java │ │ ├── CapeCommand.java │ │ ├── HistoryCommand.java │ │ ├── ProfileCommand.java │ │ ├── RenderCommand.java │ │ ├── SkinCommand.java │ │ └── UuidCommand.java │ └── utility │ │ ├── CodesCommand.java │ │ ├── ColorCommand.java │ │ ├── CoordsCommand.java │ │ ├── IngredientCommand.java │ │ ├── ItemCommand.java │ │ ├── RandomCommand.java │ │ ├── RecipeCommand.java │ │ ├── SeedCommand.java │ │ ├── ServerCommand.java │ │ ├── Sha1Command.java │ │ ├── ShadowCommand.java │ │ ├── StackCommand.java │ │ └── StatusCommand.java │ ├── database │ ├── Database.java │ ├── DbGuild.java │ └── DbUser.java │ ├── debug │ ├── ClientDebugOption.java │ ├── DebugOption.java │ ├── ItemDebugOption.java │ ├── JDADebugOption.java │ ├── ThreadDebugOption.java │ └── cache │ │ ├── CacheDebugOption.java │ │ ├── CooldownCacheDebugOption.java │ │ ├── PlayerCacheDebugOption.java │ │ ├── StatusCacheDebugOption.java │ │ └── UuidCacheDebugOption.java │ ├── interaction │ ├── InteractionListener.java │ ├── InteractionTracker.java │ ├── RecipeMenu.java │ └── UpdatingMessage.java │ ├── listing │ ├── TopGGClient.java │ └── VoteHandler.java │ ├── mc │ ├── Favicon.java │ ├── FeatureFlag.java │ ├── FeatureFlagRegistry.java │ ├── MCLibrary.java │ ├── StandardMCLibrary.java │ ├── Version.java │ ├── VersionRegistry.java │ ├── external │ │ ├── DualPlayerProvider.java │ │ ├── ElectroidAPI.java │ │ ├── ElectroidAPIImpl.java │ │ ├── GappleAPI.java │ │ ├── GappleAPIImpl.java │ │ ├── MojangAPI.java │ │ ├── MojangAPIImpl.java │ │ └── PlayerProvider.java │ ├── item │ │ ├── Container.java │ │ ├── ItemCount.java │ │ └── ItemRegistry.java │ ├── player │ │ ├── AccountStatus.java │ │ ├── DefaultSkin.java │ │ ├── DefaultSkinType.java │ │ ├── NameChange.java │ │ ├── Player.java │ │ ├── Profile.java │ │ ├── ProfileAction.java │ │ ├── Render.java │ │ ├── RenderType.java │ │ ├── SkinModel.java │ │ └── Username.java │ ├── pos │ │ ├── BlockPos.java │ │ ├── RegionPos.java │ │ ├── SectionPos.java │ │ ├── Vec.java │ │ ├── Vec2.java │ │ ├── Vec2i.java │ │ ├── Vec3.java │ │ └── Vec3i.java │ └── recipe │ │ ├── BrewingRecipe.java │ │ ├── CraftResult.java │ │ ├── CraftingRecipe.java │ │ ├── Ingredient.java │ │ ├── LegacySmithingRecipe.java │ │ ├── Recipe.java │ │ ├── RecipeRegistry.java │ │ ├── ShapedRecipe.java │ │ ├── ShapelessRecipe.java │ │ ├── SmeltingRecipe.java │ │ ├── SmithingRecipe.java │ │ ├── StonecuttingRecipe.java │ │ └── TransmuteRecipe.java │ ├── network │ ├── APIClient.java │ ├── NetUtil.java │ ├── OkAPIClient.java │ └── StatusCodes.java │ └── util │ ├── ArrayUtils.java │ ├── ColorUtils.java │ ├── DateUtils.java │ ├── DiscordUtils.java │ ├── MathUtils.java │ ├── MessageUtils.java │ ├── RequestUtils.java │ ├── StringUtils.java │ ├── UrlUtils.java │ ├── Utils.java │ ├── UuidUtils.java │ ├── dice │ ├── DiceCombination.java │ ├── DiceError.java │ └── DiceGroup.java │ └── type │ ├── DelayedCountDownLatch.java │ ├── Dimensions.java │ ├── Either.java │ ├── FutureCallback.java │ ├── HumanDecimalFormat.java │ ├── Switch.java │ └── ThrowingFunction.java ├── tags.json └── versions.json /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First, join the [support server](https://minecord.github.io/support) and request the "Developer" role to get up-to-date on what is currently being added. 4 | 5 | ## Setting Up the Environment 6 | 7 | To get started, clone the repository with `git clone https://github.com/Tisawesomeness/Minecord.git` and open in your favorite IDE 8 | ([IntelliJ](https://www.jetbrains.com/idea/) is recommended, though any proper IDE will work). 9 | 10 | To run the bot from your IDE, run the `Main` class. To build the executable JAR files, use `mvn package`. 11 | 12 | ## Conventions 13 | 14 | Minecord is meant to be a beginner-friendly project. Follow conventions if you can, but don't worry too much if you are a beginner. We will guide you through any stylistic changes when you make a pull request. 15 | 16 | ### Formatting 17 | 18 | This project 4 spaces for indentation and the One True Brace Style (1TBS), meaning that every control statement should have braces as shown below. 19 | 20 | ```java 21 | if (condition) { 22 | statement; 23 | } 24 | statement; 25 | ``` 26 | 27 | ### Annotations 28 | 29 | You are **highly encouraged** to use the `@lombok.NonNull` and `@javax.annotation.Nullable` on class fields and method parameters. 30 | This ensures that code elsewhere in the codebase knows exactly when a null check is necessary and prevents unexpected NullPointerExceptions. 31 | 32 | ### Documentation 33 | 34 | Public methods should usually have Javadocs, which must have all assumptions, parameters, thrown exceptions, and return values documented. 35 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: tis_awesomeness 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .factorypath 4 | /.settings/ 5 | /.vscode/ 6 | /.idea/ 7 | *.iml 8 | /target/ 9 | 10 | minecord.db 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minecord 2 | 3 | A Discord bot with many Minecraft-related features, such as name history, skin renders, color codes, and recipe lookup! 4 | 5 | - **Official Bot Invite: https://minecord.github.io/invite** 6 | - Bot User: `Minecord#1216` 7 | - Support Server: https://minecord.github.io/support 8 | 9 | ### How to Install 10 | 11 | To use the bot on your Discord server, **invite the bot at https://minecord.github.io/invite**. 12 | 13 | ### Self Hosting 14 | 15 | - If you only want to use commands, simply [invite the public bot](https://minecord.github.io/invite). 16 | - If you want to host it locally, download the [latest release](releases/latest), unzip the archive, and follow the [self-hosting instructions](wiki/Self-Hosting). 17 | - Want to contribute? See the [contributing guide](blob/master/CONTRIBUTING.md)! 18 | -------------------------------------------------------------------------------- /announce.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "text": "Minecord {version} | Self-hosted by {author}", 4 | "weight": 16 5 | }, 6 | { 7 | "text": "Join the help server at {help_server}", 8 | "weight": 2 9 | }, 10 | { 11 | "text": "Invite me to your server! {invite}", 12 | "weight": 2 13 | } 14 | ] -------------------------------------------------------------------------------- /colors.json: -------------------------------------------------------------------------------- 1 | { 2 | "en_US": { 3 | "black": 0, 4 | "0": 0, 5 | "dark_blue": 1, 6 | "1": 1, 7 | "dark_green": 2, 8 | "2": 2, 9 | "dark_aqua": 3, 10 | "dark_cyan": 3, 11 | "3": 3, 12 | "dark_red": 4, 13 | "4": 4, 14 | "dark_purple": 5, 15 | "purple": 5, 16 | "thanos": 5, 17 | "the_man_behind_the_slaughter": 5, 18 | "behind_the_slaughter": 5, 19 | "purple_guy": 5, 20 | "5": 5, 21 | "gold": 6, 22 | "orange": 6, 23 | "6": 6, 24 | "light_gray": 7, 25 | "light_grey": 7, 26 | "silver": 7, 27 | "7": 7, 28 | "dark_gray": 8, 29 | "dark_grey": 8, 30 | "gray": 8, 31 | "grey": 8, 32 | "8": 8, 33 | "blue": 9, 34 | "9": 9, 35 | "green": 10, 36 | "lime": 10, 37 | "light_green": 10, 38 | "10": 10, 39 | "a": 10, 40 | "light_blue": 11, 41 | "aqua": 11, 42 | "cyan": 11, 43 | "11": 11, 44 | "b": 11, 45 | "red": 12, 46 | "12": 12, 47 | "c": 12, 48 | "light_purple": 13, 49 | "pink": 13, 50 | "magenta": 13, 51 | "13": 13, 52 | "d": 13, 53 | "yellow": 14, 54 | "14": 14, 55 | "e": 14, 56 | "white": 15, 57 | "blank": 15, 58 | "15": 15, 59 | "f": 15 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "Check the wiki for more information: https://github.com/Tisawesomeness/Minecord/wiki/Config", 3 | "clientToken": "your token here", 4 | "shardCount": 1, 5 | "autoDeploy": true, 6 | "owner": "0", 7 | "testServers": [ 8 | ], 9 | "settings": { 10 | "logChannel": "0", 11 | "joinLogChannel": "0", 12 | "logWebhook": "", 13 | "statusWebhook": "", 14 | "includeSpamStatuses": true, 15 | "author": "Tis_awesomeness", 16 | "authorTag": "@tis_awesomeness", 17 | "invite": "https://minecord.github.io/invite", 18 | "helpServer": "https://minecord.github.io/support", 19 | "website": "https://minecord.github.io", 20 | "github": "https://github.com/Tisawesomeness/Minecord", 21 | "prefix": "&", 22 | "game": "/help | {guilds} guilds", 23 | "devMode": false, 24 | "debugMode": false, 25 | "deleteCommands": false, 26 | "useMenus": true, 27 | "showMemory": false, 28 | "elevatedSkipCooldown": true, 29 | "serverTimeout": 5000, 30 | "serverReadTimeout": 5000, 31 | "warnOnLocalPing": true, 32 | "useElectroidAPI": false, 33 | "useGappleAPI": true, 34 | "recordCacheStats": false, 35 | "itemImageHost": "https://minecord.github.io/item/", 36 | "recipeImageHost": "https://minecord.github.io/recipe/", 37 | "crafatarHost": "https://crafatar.com/", 38 | "reuploadCrafatarImages": false 39 | }, 40 | "database": { 41 | "type": "sqlite", 42 | "host": "./minecord.db", 43 | "port": "3306", 44 | "name": "minecord", 45 | "user": "minecord", 46 | "pass": "password here" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /data.json: -------------------------------------------------------------------------------- 1 | { 2 | "en_US": { 3 | "white": 0, 4 | "blank": 0, 5 | "bone_meal": 0, 6 | "oak": 0, 7 | "orange": 1, 8 | "spruce": 1, 9 | "magenta": 2, 10 | "birch": 2, 11 | "light_blue": 3, 12 | "aqua": 3, 13 | "jungle": 3, 14 | "yellow": 4, 15 | "dandelion": 4, 16 | "dandelion_yellow": 4, 17 | "acacia": 4, 18 | "lime": 5, 19 | "light_green": 5, 20 | "dark_oak": 5, 21 | "dark": 6, 22 | "pink": 6, 23 | "light_purple": 6, 24 | "cherry": 6, 25 | "sakura": 6, 26 | "gray": 7, 27 | "grey": 7, 28 | "light_gray": 8, 29 | "light_grey": 8, 30 | "silver": 8, 31 | "cyan": 9, 32 | "purple": 10, 33 | "thanos": 10, 34 | "behind_the_slaughter": 10, 35 | "blue": 11, 36 | "lapis": 11, 37 | "lapis_lazuli": 11, 38 | "brown": 12, 39 | "coco": 12, 40 | "cocoa": 12, 41 | "green": 13, 42 | "cactus": 13, 43 | "cactus_green": 13, 44 | "red": 14, 45 | "rose": 14, 46 | "rose_red": 14, 47 | "black": 15, 48 | "ink": 15, 49 | "inc": 15, 50 | "inc_sac": 15, 51 | "ink_sac": 15, 52 | "inc_sack": 15, 53 | "ink_sack": 15 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /flags.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "1.20" 4 | }, 5 | { 6 | "id": "1.21" 7 | }, 8 | { 9 | "id": "bundle", 10 | "name": "Bundle", 11 | "release": "1.21.2" 12 | }, 13 | { 14 | "id": "winter_drop", 15 | "name": "The Garden Awakens", 16 | "release": "1.21.4" 17 | }, 18 | { 19 | "id": "vanilla" 20 | } 21 | ] 22 | -------------------------------------------------------------------------------- /src/br/com/azalim/mcserverping/MCPingOptions.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 jamietech. All rights reserved. 3 | * https://github.com/jamietech/MinecraftServerPing 4 | * 5 | * Redistribution and use in source and binary forms, with or without modification, are 6 | * permitted provided that the following conditions are met: 7 | * 8 | * 1. Redistributions of source code must retain the above copyright notice, this list of 9 | * conditions and the following disclaimer. 10 | * 11 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list 12 | * of conditions and the following disclaimer in the documentation and/or other materials 13 | * provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY EXPRESS OR IMPLIED 16 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 17 | * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR 18 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 23 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | * 25 | * The views and conclusions contained in the software and documentation are those of the 26 | * authors and contributors and should not be interpreted as representing official policies, 27 | * either expressed or implied, of anybody else. 28 | */ 29 | package br.com.azalim.mcserverping; 30 | 31 | import lombok.Builder; 32 | import lombok.Getter; 33 | 34 | import java.nio.charset.StandardCharsets; 35 | 36 | /** 37 | * Storage class for {@link MCPing} options. 38 | */ 39 | @Builder 40 | public class MCPingOptions { 41 | 42 | @Getter 43 | private String hostname; 44 | 45 | // Modification from tis 46 | // remove guava 47 | @Getter 48 | @Builder.Default 49 | private String charset = StandardCharsets.UTF_8.name(); 50 | 51 | @Getter 52 | @Builder.Default 53 | private int port = 25565; 54 | 55 | @Getter 56 | @Builder.Default 57 | private int timeout = 5000; 58 | 59 | // Modification from tis 60 | @Getter 61 | @Builder.Default 62 | private int readTimeout = 5000; 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/Announcement.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord; 2 | 3 | import com.tisawesomeness.minecord.util.DiscordUtils; 4 | import com.tisawesomeness.minecord.util.RequestUtils; 5 | 6 | import org.json.JSONArray; 7 | import org.json.JSONObject; 8 | 9 | import java.io.IOException; 10 | import java.util.ArrayList; 11 | 12 | public class Announcement { 13 | 14 | private static ArrayList announcements = new ArrayList<>(); 15 | private static int totalWeight; 16 | private final String text; 17 | private final int weight; 18 | 19 | private Announcement(String text, int weight) { 20 | this.text = DiscordUtils.parseConstants(text); 21 | this.weight = weight; 22 | } 23 | 24 | /** 25 | * Reads announcements from file and parses their {constants} 26 | * @param path The path to the announce.json file 27 | * @throws IOException If announce.json is not found 28 | */ 29 | public static void init(String path) throws IOException { 30 | announcements = new ArrayList<>(); 31 | totalWeight = 0; 32 | JSONArray announceArr = RequestUtils.loadJSONArray(path + "/announce.json"); 33 | for (int i = 0; i < announceArr.length(); i++) { 34 | JSONObject announceObj = announceArr.getJSONObject(i); 35 | int weight = announceObj.getInt("weight"); 36 | announcements.add(new Announcement(announceObj.getString("text"), weight)); 37 | totalWeight += weight; 38 | } 39 | } 40 | 41 | /** 42 | * Randomly selects an announcement based on their weights and parses their {variables} 43 | * @return The selected announcement string 44 | */ 45 | public static String rollAnnouncement() { 46 | int rand = (int) (Math.random() * totalWeight); 47 | int i = -1; 48 | while (rand >= 0) { 49 | i++; 50 | rand -= announcements.get(i).weight; 51 | } 52 | return DiscordUtils.parseVariables(announcements.get(i).text); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/GuildListener.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord; 2 | 3 | import com.tisawesomeness.minecord.listing.TopGGClient; 4 | import com.tisawesomeness.minecord.util.DiscordUtils; 5 | import lombok.RequiredArgsConstructor; 6 | import net.dv8tion.jda.api.EmbedBuilder; 7 | import net.dv8tion.jda.api.entities.Guild; 8 | import net.dv8tion.jda.api.entities.Member; 9 | import net.dv8tion.jda.api.events.guild.GenericGuildEvent; 10 | import net.dv8tion.jda.api.events.guild.GuildJoinEvent; 11 | import net.dv8tion.jda.api.events.guild.GuildLeaveEvent; 12 | import net.dv8tion.jda.api.hooks.ListenerAdapter; 13 | import net.dv8tion.jda.api.utils.messages.MessageCreateData; 14 | 15 | import java.util.concurrent.CompletableFuture; 16 | 17 | @RequiredArgsConstructor 18 | public class GuildListener extends ListenerAdapter { 19 | 20 | private final TopGGClient topGGClient; 21 | 22 | @Override 23 | public void onGenericGuild(GenericGuildEvent e) { 24 | 25 | //Get guild info 26 | EmbedBuilder eb = new EmbedBuilder(); 27 | Guild guild = e.getGuild(); 28 | Member owner = guild.getOwner(); 29 | 30 | //Create embed 31 | if (e instanceof GuildJoinEvent) { 32 | 33 | String avatarUrl = owner == null ? null : owner.getUser().getAvatarUrl(); 34 | eb.setAuthor("Joined guild!", null, avatarUrl); 35 | eb.addField("Name", guild.getName(), true); 36 | eb.addField("Guild ID", guild.getId(), true); 37 | if (owner != null) { 38 | eb.addField("Owner", owner.getEffectiveName(), true); 39 | eb.addField("Owner ID", owner.getUser().getId(), true); 40 | } 41 | 42 | } else if (e instanceof GuildLeaveEvent) { 43 | if (owner != null) { 44 | eb.setAuthor(owner.getEffectiveName() + " (" + owner.getUser().getId() + ")", 45 | null, owner.getUser().getAvatarUrl()); 46 | } 47 | eb.setDescription("Left guild `" + guild.getName() + "` (" + guild.getId() + ")"); 48 | } else { 49 | return; 50 | } 51 | 52 | eb.setThumbnail(guild.getIconUrl()); 53 | Bot.logger.joinLog(MessageCreateData.fromEmbeds(eb.build())); 54 | CompletableFuture.runAsync(topGGClient::sendGuilds); 55 | DiscordUtils.update(); //Update guild, channel, and user count 56 | 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/Loader.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord; 2 | 3 | import java.io.DataInputStream; 4 | import java.io.File; 5 | import java.io.FileInputStream; 6 | import java.io.IOException; 7 | import java.net.URL; 8 | 9 | public class Loader implements Runnable { 10 | 11 | private final static boolean propagate = false; 12 | private final static String botClass = "com.tisawesomeness.minecord.Bot"; 13 | private final String[] args; 14 | 15 | public Loader(String[] args) { 16 | this.args = args; 17 | } 18 | 19 | public void run() { 20 | 21 | //Clear references 22 | DynamicLoader dl; 23 | 24 | //Dynamically start a new bot 25 | dl = new DynamicLoader(Main.cl); 26 | if (propagate) Thread.currentThread().setContextClassLoader(dl); 27 | Class clazz = dl.loadClass(botClass); 28 | try { 29 | clazz.getMethods()[1].invoke(null, args, (Object) true); 30 | } catch (ClassCastException ex) { 31 | //Do nothing 32 | } catch (Exception ex) { 33 | ex.printStackTrace(); 34 | } 35 | 36 | } 37 | 38 | // Adapted from https://github.com/evacchi/class-reloader 39 | static class DynamicLoader extends ClassLoader { 40 | 41 | private final ClassLoader orig; 42 | DynamicLoader(ClassLoader orig) { 43 | this.orig = orig; 44 | } 45 | 46 | @Override 47 | public Class loadClass(String s) { 48 | return findClass(s); 49 | } 50 | 51 | @Override 52 | public Class findClass(String s) { 53 | try { 54 | byte[] bytes = loadClassData(s); 55 | return defineClass(s, bytes, 0, bytes.length); 56 | } catch (IOException ioe) { 57 | try { 58 | return super.loadClass(s); 59 | } catch (ClassNotFoundException ex) { 60 | ex.printStackTrace(System.out); 61 | } 62 | ioe.printStackTrace(System.out); 63 | return null; 64 | } 65 | } 66 | 67 | private byte[] loadClassData(String className) throws IOException { 68 | try { 69 | Class clazz = orig.loadClass(className); 70 | String name = clazz.getName(); 71 | name = name.substring(name.lastIndexOf('.') + 1); 72 | URL url = clazz.getResource(name + ".class"); 73 | File f = new File(url.toURI()); 74 | int size = (int) f.length(); 75 | byte[] buff = new byte[size]; 76 | try (DataInputStream dis = new DataInputStream(new FileInputStream(f))) { 77 | dis.readFully(buff); 78 | } 79 | return buff; 80 | } catch (Exception ex) { 81 | throw new IOException(ex); 82 | } 83 | } 84 | 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/Main.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord; 2 | 3 | import net.dv8tion.jda.api.entities.Message; 4 | import net.dv8tion.jda.api.entities.User; 5 | import net.dv8tion.jda.api.sharding.ShardManager; 6 | 7 | @SuppressWarnings("unused") 8 | public class Main { 9 | 10 | protected static ClassLoader cl; 11 | 12 | //Store data between reloads 13 | private static ShardManager shards; 14 | private static User user; 15 | private static Message message; 16 | private static long birth; 17 | 18 | public static void main(String[] args) { 19 | 20 | if (!Bot.setup(args, false)) { 21 | cl = Thread.currentThread().getContextClassLoader(); 22 | load(args); 23 | } 24 | 25 | } 26 | 27 | //Start loader 28 | public static void load(String[] args) { 29 | new Thread(new Loader(args)).start(); 30 | } 31 | 32 | //Getters and setters 33 | public static Message getMessage(String ignore) { 34 | return message; 35 | } 36 | public static void setMessage(Message m) { 37 | message = m; 38 | } 39 | public static User getUser(String ignore) { 40 | return user; 41 | } 42 | public static void setUser(User u) { 43 | user = u; 44 | } 45 | public static ShardManager getShards(String ignore) { 46 | return shards; 47 | } 48 | public static void setShards(ShardManager s) { 49 | shards = s; 50 | } 51 | public static long getBirth(String ignore) { 52 | return birth; 53 | } 54 | public static void setBirth(long b) { 55 | birth = b; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/StatusListener.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord; 2 | 3 | import net.dv8tion.jda.api.events.session.*; 4 | import net.dv8tion.jda.api.hooks.ListenerAdapter; 5 | 6 | import java.time.Instant; 7 | import java.time.ZoneId; 8 | import java.time.format.DateTimeFormatter; 9 | 10 | public class StatusListener extends ListenerAdapter { 11 | 12 | private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss:SSS") 13 | .withZone(ZoneId.of("UTC")); 14 | 15 | @Override 16 | public void onReady(ReadyEvent e) { 17 | processStatus(e); 18 | } 19 | @Override 20 | public void onSessionInvalidate(SessionInvalidateEvent e) { 21 | processStatus(e); 22 | } 23 | @Override 24 | public void onSessionDisconnect(SessionDisconnectEvent e) { 25 | processStatus(e); 26 | } 27 | @Override 28 | public void onSessionResume(SessionResumeEvent e) { 29 | processStatus(e); 30 | } 31 | @Override 32 | public void onSessionRecreate(SessionRecreateEvent e) { 33 | processStatus(e); 34 | } 35 | @Override 36 | public void onShutdown(ShutdownEvent e) { 37 | processStatus(e); 38 | } 39 | 40 | private static void processStatus(GenericSessionEvent e) { 41 | String emote; 42 | String status; 43 | switch (e.getState()) { 44 | case READY: 45 | Bot.readyShard(); 46 | emote = ":ballot_box_with_check:"; 47 | status = "Ready"; 48 | break; 49 | case INVALIDATED: 50 | emote = ":no_entry_sign:"; 51 | status = "Invalidated"; 52 | break; 53 | case DISCONNECTED: 54 | if (!Config.getIncludeSpamStatuses()) { 55 | return; 56 | } 57 | emote = ":no_mobile_phones:"; 58 | status = "Disconnected"; 59 | break; 60 | case RESUMED: 61 | if (!Config.getIncludeSpamStatuses()) { 62 | return; 63 | } 64 | emote = ":arrow_forward:"; 65 | status = "Resumed"; 66 | break; 67 | case RECREATED: 68 | emote = ":recycle:"; 69 | status = "Recreated"; 70 | break; 71 | case SHUTDOWN: 72 | emote = ":octagonal_sign:"; 73 | status = "Shutdown"; 74 | break; 75 | default: 76 | emote = ":interrobang:"; 77 | status = "Unknown"; 78 | } 79 | 80 | String time = FORMATTER.format(Instant.now()); 81 | int shardId = e.getJDA().getShardInfo().getShardId(); 82 | int shardCount = e.getJDA().getShardInfo().getShardTotal(); 83 | System.out.printf("%s Shard %03d/%03d %s\n", time, shardId, shardCount, status); 84 | String logMsg = String.format("`%s` %s Shard `%03d/%03d` %s", time, emote, shardId, shardCount, status); 85 | Bot.logger.statusLog(logMsg); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/ExtraHelpPage.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | import java.util.Arrays; 7 | 8 | @RequiredArgsConstructor 9 | public enum ExtraHelpPage { 10 | USERNAME_INPUT( 11 | "usernameInput", 12 | "Username Input Help", 13 | "Invalid/duplicate names and names with spaces", 14 | new String[]{"nameInput"}, 15 | "Valid usernames are 3-16 characters long and only contain letters, numbers, and underscores.\n" + 16 | "However, invalid usernames such as `8`, `Cool.J`, and `Will Wall` have existed.\n" + 17 | "If a username contains spaces, surround the name in quotes (\"\") or backticks (\\`\\`).\n" + 18 | "\n" + 19 | "The Mojang API supports usernames with up to 25 ASCII letters, numbers, spaces, and the characters `_!@$-.?` (Sorry, `Seng\u00E5ngaren`).\n" + 20 | "If two or more accounts share the same username, use the UUID instead.\n" + 21 | "\n" + 22 | "Examples:\n" + 23 | "- `{&}profile Cool.J 5` --> `Cool.J` is the username\n" + 24 | "- `{&}profile Will Wall 5` --> `Will` is the username\n" + 25 | "- `{&}body \"Will Wall\" 5` --> `Will Wall` is the username" 26 | ), 27 | UUID_INPUT( 28 | "uuidInput", 29 | "UUID Input Help", 30 | "NBT formats for UUIDs", 31 | new String[0], 32 | "A [UUID](https://minecraft.wiki/w/Universally_unique_identifier) (Universally Unique IDentifier) is a player's unique ID.\n" + 33 | "UUIDs can be in any format shown in `{&}uuid`.\n" + 34 | "\n" + 35 | "**Short**: `f6489b797a9f49e2980e265a05dbc3af`\n" + 36 | "**Long**: `f6489b79-7a9f-49e2-980e-265a05dbc3af`\n" + 37 | "**1.16+ NBT**: `[I;-163013767,2057259490,-1743903142,98288559]`\n" + 38 | "**Pre-1.16 NBT**: `UUIDMost:-700138796005504542,UUIDLeast:-7490006962183355473`" 39 | ), 40 | PHD( 41 | "phd", 42 | "Pseudo Hard-Deletion", 43 | "What are pseudo hard-deleted accounts?", 44 | new String[0], 45 | "A **pseudo hard-deleted** account (or **PHD** for short) is an account that has been partially deleted from Mojang's account database.\n" + 46 | "PHD accounts can be looked up by UUID, but not by name.\n" + 47 | "All player commands except `{&}uuid` will check if an account is PHD if you enter a UUID." 48 | ); 49 | 50 | /** 51 | * The internal ID of the help page 52 | */ 53 | @Getter private final String name; 54 | @Getter private final String title; 55 | @Getter private final String description; 56 | private final String[] aliases; 57 | private final String help; 58 | 59 | public String getHelp() { 60 | return help.replace("{&}", "/"); 61 | } 62 | 63 | public static ExtraHelpPage from(String name) { 64 | return Arrays.stream(values()) 65 | .filter(page -> page.matches(name)) 66 | .findFirst() 67 | .orElse(null); 68 | } 69 | private boolean matches(String name) { 70 | return name.equalsIgnoreCase(this.name) || Arrays.stream(aliases) 71 | .anyMatch(alias -> alias.equalsIgnoreCase(name)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/LegacyCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 5 | import net.dv8tion.jda.api.interactions.IntegrationType; 6 | import net.dv8tion.jda.api.interactions.InteractionContextType; 7 | import net.dv8tion.jda.api.utils.MarkdownUtil; 8 | import net.dv8tion.jda.api.utils.messages.MessageCreateData; 9 | 10 | import java.util.EnumSet; 11 | import java.util.Set; 12 | 13 | public abstract class LegacyCommand implements Command { 14 | 15 | public static final EnumSet CONTEXTS = EnumSet.of( 16 | InteractionContextType.GUILD, InteractionContextType.BOT_DM 17 | ); 18 | 19 | public String[] getAliases() { 20 | return new String[0]; 21 | } 22 | 23 | @Override 24 | public final void sendSuccess(MessageReceivedEvent e, MessageCreateData message) { 25 | e.getChannel().sendMessage(message).queue(); 26 | } 27 | @Override 28 | public final void sendFailure(MessageReceivedEvent e, MessageCreateData message) { 29 | e.getChannel().sendMessage(message).queue(); 30 | } 31 | 32 | @Override 33 | public boolean supportsContext(Set install, InteractionContextType context) { 34 | return CONTEXTS.contains(context); 35 | } 36 | 37 | @Override 38 | public final String getMention() { 39 | String username = Bot.getSelfUser().getName(); 40 | return MarkdownUtil.monospace(String.format("@%s %s", username, getInfo().name)); 41 | } 42 | 43 | @Override 44 | public final String debugRunCommand(MessageReceivedEvent e) { 45 | return e.getMessage().getContentRaw(); 46 | } 47 | 48 | public abstract Result run(String[] args, MessageReceivedEvent e) throws Exception; 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/Module.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | 5 | /** 6 | * A category of commands grouped together. 7 | */ 8 | public class Module { 9 | private final String name; 10 | private final boolean hidden; 11 | private final String moduleHelp; 12 | private final Command[] commands; 13 | 14 | /** 15 | * Creates a new user-facing module. 16 | * @param name The display name of the module 17 | * @param commands The list of commands it contains 18 | */ 19 | public Module(String name, Command... commands) { 20 | this(name, false, null, commands); 21 | } 22 | /** 23 | * Creates a new module. 24 | * @param name The display name of the module 25 | * @param hidden Whether the module is hidden to users 26 | * @param moduleHelp Extra info displayed when using &help . May be null. Use {&} to substitute in the current prefix. 27 | * @param commands The list of commands it contains 28 | */ 29 | public Module(String name, boolean hidden, String moduleHelp, Command... commands) { 30 | this.name = name; 31 | this.hidden = hidden; 32 | this.moduleHelp = moduleHelp; 33 | this.commands = commands; 34 | } 35 | 36 | public String getName() { 37 | return name; 38 | } 39 | public boolean isHidden() { 40 | return hidden; 41 | } 42 | public String getHelp() { 43 | String username = Bot.getSelfUser().getName(); 44 | return moduleHelp == null ? null : moduleHelp.replace("{&}", "@" + username + " "); 45 | } 46 | public Command[] getCommands() { 47 | return commands; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/SlashCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; 5 | import net.dv8tion.jda.api.interactions.IntegrationType; 6 | import net.dv8tion.jda.api.interactions.InteractionContextType; 7 | import net.dv8tion.jda.api.interactions.commands.ICommandReference; 8 | import net.dv8tion.jda.api.interactions.commands.build.Commands; 9 | import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; 10 | import net.dv8tion.jda.api.utils.MarkdownUtil; 11 | import net.dv8tion.jda.api.utils.messages.MessageCreateData; 12 | 13 | import java.util.Optional; 14 | import java.util.Set; 15 | import java.util.stream.Collectors; 16 | 17 | public abstract class SlashCommand implements Command { 18 | 19 | public final SlashCommandData getCommandSyntax() { 20 | CommandInfo info = getInfo(); 21 | SlashCommandData builder = Commands.slash(info.name, info.description) 22 | .setContexts(InteractionContextType.ALL) 23 | .setIntegrationTypes(IntegrationType.ALL); 24 | return addCommandSyntax(builder); 25 | } 26 | public SlashCommandData addCommandSyntax(SlashCommandData builder) { 27 | return builder; 28 | } 29 | 30 | /** 31 | * @return list of aliases that existed before this command moved to slash commands 32 | */ 33 | public String[] getLegacyAliases() { 34 | return new String[0]; 35 | } 36 | 37 | @Override 38 | public final void sendSuccess(SlashCommandInteractionEvent e, MessageCreateData message) { 39 | if (e.isAcknowledged()) { 40 | e.getHook().sendMessage(message).queue(); 41 | } else { 42 | e.reply(message).queue(); 43 | } 44 | } 45 | @Override 46 | public final void sendFailure(SlashCommandInteractionEvent e, MessageCreateData message) { 47 | if (e.isAcknowledged()) { 48 | e.getHook().sendMessage(message).setEphemeral(true).queue(); 49 | } else { 50 | e.reply(message).setEphemeral(true).queue(); 51 | } 52 | } 53 | 54 | @Override 55 | public boolean supportsContext(Set install, InteractionContextType context) { 56 | Optional commandOpt = getDiscordCommand(); 57 | if (!commandOpt.isPresent()) { 58 | return LegacyCommand.CONTEXTS.contains(context); 59 | } 60 | net.dv8tion.jda.api.interactions.commands.Command command = commandOpt.get(); 61 | return install.stream().anyMatch(i -> command.getIntegrationTypes().contains(i)) 62 | && command.getContexts().contains(context); 63 | } 64 | 65 | @Override 66 | public final String getMention() { 67 | return getDiscordCommand() 68 | .map(ICommandReference::getAsMention) 69 | .orElse(MarkdownUtil.monospace("/" + getInfo().name)); 70 | } 71 | 72 | private Optional getDiscordCommand() { 73 | return Bot.getSlashCommands().stream() 74 | .filter(c -> c.getName().equals(getInfo().name)) 75 | .findFirst(); 76 | } 77 | 78 | @Override 79 | public String debugRunCommand(SlashCommandInteractionEvent e) { 80 | return debugRunSlashCommand(e); 81 | } 82 | public static String debugRunSlashCommand(SlashCommandInteractionEvent e) { 83 | return e.getName() + ": " + e.getOptions().stream() 84 | .map(o -> o.getName() + "=" + o.getAsString()) 85 | .collect(Collectors.joining("\n")); 86 | } 87 | 88 | public abstract Result run(SlashCommandInteractionEvent e) throws Exception; 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/admin/DemoteCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.admin; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.Config; 5 | import com.tisawesomeness.minecord.command.LegacyCommand; 6 | import com.tisawesomeness.minecord.database.Database; 7 | import com.tisawesomeness.minecord.util.DiscordUtils; 8 | 9 | import net.dv8tion.jda.api.entities.User; 10 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 11 | 12 | public class DemoteCommand extends LegacyCommand { 13 | 14 | public CommandInfo getInfo() { 15 | return new CommandInfo( 16 | "demote", 17 | "De-elevate a user.", 18 | "", 19 | 5000, 20 | true, 21 | true 22 | ); 23 | } 24 | 25 | public String[] getAliases() { 26 | return new String[]{"delevate", "normie", "badboi"}; 27 | } 28 | 29 | public Result run(String[] args, MessageReceivedEvent e) throws Exception { 30 | 31 | if (args.length == 0) { 32 | return new Result(Outcome.WARNING, ":warning: You must specify a user!"); 33 | } 34 | 35 | //Extract user 36 | User user = DiscordUtils.findUser(args[0]); 37 | if (user == null) { 38 | return new Result(Outcome.ERROR, ":x: Not a valid user!"); 39 | } 40 | long id = user.getIdLong(); 41 | 42 | //Don't demote a normal user 43 | if (!Database.isElevated(id)) { 44 | return new Result(Outcome.WARNING, ":warning: User is not elevated!"); 45 | } 46 | 47 | //Can't demote the owner 48 | if (id == Long.parseLong(Config.getOwner())) { 49 | return new Result(Outcome.WARNING, ":warning: You can't demote the owner!"); 50 | } 51 | 52 | //Demote user 53 | Database.changeElevated(id, false); 54 | String msg = "Demoted " + DiscordUtils.tagAndId(user); 55 | System.out.println(msg); 56 | Bot.logger.log(":arrow_down: " + msg); 57 | return new Result(Outcome.SUCCESS, ":arrow_down: " + msg); 58 | 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/admin/DeployCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.admin; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.command.LegacyCommand; 5 | import com.tisawesomeness.minecord.util.DiscordUtils; 6 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 7 | import net.dv8tion.jda.api.utils.MarkdownUtil; 8 | 9 | public class DeployCommand extends LegacyCommand { 10 | 11 | @Override 12 | public CommandInfo getInfo() { 13 | return new CommandInfo( 14 | "deploy", 15 | "Deploys global slash commands", 16 | null, 17 | 0, 18 | true, 19 | true 20 | ); 21 | } 22 | 23 | @Override 24 | public String getHelp() { 25 | return "Pray."; 26 | } 27 | 28 | @Override 29 | public Result run(String[] args, MessageReceivedEvent e) throws Exception { 30 | Bot.deployCommands(); 31 | String msg = DiscordUtils.tagAndId(e.getAuthor()) + " deployed global slash commands"; 32 | Bot.logger.log(":rotating_light: " + MarkdownUtil.bold(msg)); 33 | return new Result(Outcome.SUCCESS, "May the ratelimit have mercy on your soul."); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/admin/MsgCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.admin; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.command.LegacyCommand; 5 | import com.tisawesomeness.minecord.util.ArrayUtils; 6 | import com.tisawesomeness.minecord.util.DiscordUtils; 7 | 8 | import net.dv8tion.jda.api.EmbedBuilder; 9 | import net.dv8tion.jda.api.entities.User; 10 | import net.dv8tion.jda.api.entities.channel.concrete.PrivateChannel; 11 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 12 | import net.dv8tion.jda.api.utils.MarkdownUtil; 13 | import net.dv8tion.jda.api.utils.messages.MessageCreateData; 14 | 15 | import java.util.concurrent.ExecutionException; 16 | 17 | public class MsgCommand extends LegacyCommand { 18 | 19 | public CommandInfo getInfo() { 20 | return new CommandInfo( 21 | "msg", 22 | "Open the DMs.", 23 | " ", 24 | 0, 25 | true, 26 | true 27 | ); 28 | } 29 | 30 | public String[] getAliases() { 31 | return new String[]{"dm", "tell", "pm"}; 32 | } 33 | 34 | public Result run(String[] args, MessageReceivedEvent e) { 35 | 36 | //Check for proper argument length 37 | if (args.length < 2) { 38 | return new Result(Outcome.WARNING, ":warning: Please specify a message."); 39 | } 40 | 41 | //Extract user 42 | User user = DiscordUtils.findUser(args[0]); 43 | if (user == null) return new Result(Outcome.ERROR, ":x: Not a valid user!"); 44 | 45 | //Send the message 46 | String msg; 47 | try { 48 | PrivateChannel channel = user.openPrivateChannel().submit().get(); 49 | msg = String.join(" ", ArrayUtils.remove(args, 0)); 50 | channel.sendMessage(msg).queue(); 51 | } catch (InterruptedException | ExecutionException ex) { 52 | ex.printStackTrace(); 53 | return new Result(Outcome.ERROR, ":x: An exception occured."); 54 | } 55 | 56 | EmbedBuilder eb = new EmbedBuilder(); 57 | String authorLogMsg = DiscordUtils.tagAndId(e.getAuthor()); 58 | eb.setAuthor(authorLogMsg, null, e.getAuthor().getAvatarUrl()); 59 | String sentLogMsg = "Sent a message to " + DiscordUtils.tagAndId(user); 60 | System.out.println(authorLogMsg + " " + sentLogMsg + ":\n" + msg); 61 | eb.setDescription(MarkdownUtil.bold(sentLogMsg) + ":\n" + msg); 62 | eb.setThumbnail(user.getAvatarUrl()); 63 | Bot.logger.log(MessageCreateData.fromEmbeds(eb.build())); 64 | 65 | return new Result(Outcome.SUCCESS); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/admin/NameCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.admin; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.command.LegacyCommand; 5 | import com.tisawesomeness.minecord.util.ArrayUtils; 6 | import com.tisawesomeness.minecord.util.DiscordUtils; 7 | 8 | import net.dv8tion.jda.api.EmbedBuilder; 9 | import net.dv8tion.jda.api.Permission; 10 | import net.dv8tion.jda.api.entities.Guild; 11 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 12 | import net.dv8tion.jda.api.utils.MarkdownUtil; 13 | import net.dv8tion.jda.api.utils.messages.MessageCreateData; 14 | 15 | public class NameCommand extends LegacyCommand { 16 | 17 | public CommandInfo getInfo() { 18 | return new CommandInfo( 19 | "name", 20 | "Changes the bot's nickname per-guild, enter nothing to reset.", 21 | " ", 22 | 0, 23 | true, 24 | true 25 | ); 26 | } 27 | 28 | public String[] getAliases() { 29 | return new String[]{"nick", "nickname"}; 30 | } 31 | 32 | public String getHelp() { 33 | return "`{&}name ` - Resets the bot's nickname for the guild.\n" + 34 | "`{&}name ` - Sets the bot's nickname for the guild. Requires *Change Nickname* permissions.\n"; 35 | } 36 | 37 | public Result run(String[] args, MessageReceivedEvent e) { 38 | 39 | //Check for proper argument length 40 | if (args.length < 1) { 41 | return new Result(Outcome.WARNING, ":warning: Please specify a guild."); 42 | } 43 | 44 | //Get guild 45 | Guild guild = Bot.shardManager.getGuildById(args[0]); 46 | if (guild == null) return new Result(Outcome.ERROR, ":x: Not a valid guild!"); 47 | 48 | //Check for permissions 49 | if (!guild.getSelfMember().hasPermission(Permission.NICKNAME_CHANGE)) { 50 | return new Result(Outcome.WARNING, ":warning: No permissions!"); 51 | } 52 | 53 | //Set the nickname 54 | String name = args.length > 1 ? String.join(" ", ArrayUtils.remove(args, 0)) : e.getJDA().getSelfUser().getName(); 55 | guild.modifyNickname(guild.getSelfMember(), name).queue(); 56 | 57 | //Log it 58 | EmbedBuilder eb = new EmbedBuilder(); 59 | eb.setAuthor(e.getAuthor().getName() + " (" + e.getAuthor().getId() + ")", 60 | null, e.getAuthor().getAvatarUrl()); 61 | String author = DiscordUtils.tagAndId(e.getAuthor()); 62 | String action = args.length == 1 ? "reset" : "changed"; 63 | String descName = args.length == 1 ? "\n" + name : ""; 64 | String desc = author + " " + action + " nickname on `" + guild.getName() + "` (" + guild.getId() + "):"; 65 | System.out.println(desc + "\n" + descName); 66 | eb.setDescription(MarkdownUtil.bold(desc) + "\n" + descName); 67 | eb.setThumbnail(guild.getIconUrl()); 68 | Bot.logger.log(MessageCreateData.fromEmbeds(eb.build())); 69 | 70 | return new Result(Outcome.SUCCESS); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/admin/PermsCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.admin; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.command.LegacyCommand; 5 | import com.tisawesomeness.minecord.util.DiscordUtils; 6 | import net.dv8tion.jda.api.Permission; 7 | import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; 8 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 9 | 10 | import java.util.EnumSet; 11 | 12 | public class PermsCommand extends LegacyCommand { 13 | 14 | public CommandInfo getInfo() { 15 | return new CommandInfo( 16 | "perms", 17 | "Test the bot's permissions in a channel.", 18 | "[]", 19 | 0, 20 | true, 21 | true 22 | ); 23 | } 24 | 25 | @Override 26 | public String getHelp() { 27 | return "`{&}perms ` - Test the bot's permissions for any channel.\n" + 28 | "\n" + 29 | "Examples:\n" + 30 | "- `{&}perms 347909541264097281`\n"; 31 | } 32 | 33 | public Result run(String[] args, MessageReceivedEvent e) { 34 | if (args.length == 0) { 35 | return run(e.getGuildChannel()); 36 | } 37 | if (!DiscordUtils.isDiscordId(args[0])) { 38 | return new Result(Outcome.WARNING, ":warning: Not a valid ID!"); 39 | } 40 | GuildChannel c = Bot.shardManager.getTextChannelById(args[0]); 41 | if (c == null) { 42 | return new Result(Outcome.WARNING, ":warning: That channel does not exist."); 43 | } 44 | return run(c); 45 | } 46 | 47 | private static Result run(GuildChannel c) { 48 | EnumSet perms = c.getGuild().getSelfMember().getPermissions(c); 49 | String m = String.format("**Bot Permissions for %s:**", c.getAsMention()) + 50 | "\nView channels: " + DiscordUtils.getBoolEmote(perms.contains(Permission.VIEW_CHANNEL)) + 51 | "\nSend messages: " + DiscordUtils.getBoolEmote(perms.contains(Permission.MESSAGE_SEND)) + 52 | "\nSend messages in threads: " + DiscordUtils.getBoolEmote(perms.contains(Permission.MESSAGE_SEND_IN_THREADS)) + 53 | "\nEmbed links: " + DiscordUtils.getBoolEmote(perms.contains(Permission.MESSAGE_EMBED_LINKS)) + 54 | "\nAttach files: " + DiscordUtils.getBoolEmote(perms.contains(Permission.MESSAGE_ATTACH_FILES)) + 55 | "\nManage messages (optional): " + DiscordUtils.getBoolEmote(perms.contains(Permission.MESSAGE_MANAGE)); 56 | return new Result(Outcome.SUCCESS, m); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/admin/PromoteCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.admin; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.command.LegacyCommand; 5 | import com.tisawesomeness.minecord.database.Database; 6 | import com.tisawesomeness.minecord.util.DiscordUtils; 7 | 8 | import net.dv8tion.jda.api.entities.User; 9 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 10 | 11 | public class PromoteCommand extends LegacyCommand { 12 | 13 | public CommandInfo getInfo() { 14 | return new CommandInfo( 15 | "promote", 16 | "Elevate a user.", 17 | "", 18 | 5000, 19 | true, 20 | true 21 | ); 22 | } 23 | 24 | public String[] getAliases() { 25 | return new String[]{"elevate", "rankup"}; 26 | } 27 | 28 | public Result run(String[] args, MessageReceivedEvent e) throws Exception { 29 | 30 | if (args.length == 0) { 31 | return new Result(Outcome.WARNING, ":warning: You must specify a user!"); 32 | } 33 | 34 | //Extract user 35 | User user = DiscordUtils.findUser(args[0]); 36 | if (user == null) return new Result(Outcome.ERROR, ":x: Not a valid user!"); 37 | 38 | //Don't elevate a normal user 39 | if (Database.isElevated(user.getIdLong())) { 40 | return new Result(Outcome.WARNING, ":warning: User is already elevated!"); 41 | } 42 | 43 | //Elevate user 44 | Database.changeElevated(user.getIdLong(), true); 45 | String msg = "Elevated " + DiscordUtils.tagAndId(user); 46 | System.out.println(msg); 47 | Bot.logger.log(":arrow_up: " + msg); 48 | return new Result(Outcome.SUCCESS, ":arrow_up: " + msg); 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/admin/ReloadCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.admin; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.Config; 5 | import com.tisawesomeness.minecord.command.LegacyCommand; 6 | import net.dv8tion.jda.api.entities.Message; 7 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 8 | 9 | public class ReloadCommand extends LegacyCommand { 10 | 11 | public CommandInfo getInfo() { 12 | return new CommandInfo( 13 | "reload", 14 | "Reloads the bot.", 15 | "[]", 16 | 0, 17 | true, 18 | true 19 | ); 20 | } 21 | 22 | public String[] getAliases() { 23 | return new String[]{"restart", "reboot", "refresh"}; 24 | } 25 | 26 | public String getHelp() { 27 | if (Config.getDevMode()) { 28 | return "Reloads all non-reflection code, keeping the JDA instance.\n"; 29 | } 30 | return "Reloads the config, announcement, and item/recipe files, and restarts the database and vote server."; 31 | } 32 | 33 | public Result run(String[] args, MessageReceivedEvent e) { 34 | 35 | String reason; 36 | if (args.length > 0) { 37 | reason = String.join(" ", args); 38 | } else { 39 | reason = null; 40 | } 41 | 42 | Message m = e.getChannel().sendMessage(":arrows_counterclockwise: Reloading...").complete(); 43 | if (Config.getDevMode()) { 44 | Bot.shutdown(m, e.getAuthor()); 45 | } else { 46 | if (Bot.reload(e.getAuthor(), reason)) { 47 | m.editMessage(":white_check_mark: Reloaded!").queue(); 48 | } else { 49 | m.editMessage(":x: An Error occurred while reloading, check logs").queue(); 50 | } 51 | } 52 | 53 | return new Result(Outcome.SUCCESS); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/admin/SayCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.admin; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.command.LegacyCommand; 5 | import com.tisawesomeness.minecord.util.ArrayUtils; 6 | import com.tisawesomeness.minecord.util.DiscordUtils; 7 | 8 | import net.dv8tion.jda.api.EmbedBuilder; 9 | import net.dv8tion.jda.api.entities.Guild; 10 | import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; 11 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 12 | import net.dv8tion.jda.api.utils.MarkdownUtil; 13 | import net.dv8tion.jda.api.utils.messages.MessageCreateData; 14 | 15 | public class SayCommand extends LegacyCommand { 16 | 17 | public CommandInfo getInfo() { 18 | return new CommandInfo( 19 | "say", 20 | "Send a message.", 21 | " ", 22 | 0, 23 | true, 24 | true 25 | ); 26 | } 27 | 28 | public String[] getAliases() { 29 | return new String[]{"talk", "announce"}; 30 | } 31 | 32 | public String getHelp() { 33 | return "`{&}say ` - Make the bot send a message.\n" + 34 | "`` is either a # channel name or a channel ID.\n"; 35 | } 36 | 37 | public Result run(String[] args, MessageReceivedEvent e) { 38 | 39 | //Check for proper argument length 40 | if (args.length < 2) { 41 | return new Result(Outcome.WARNING, ":warning: Please specify a message."); 42 | } 43 | 44 | //Extract channel 45 | TextChannel channel = DiscordUtils.findChannel(args[0]); 46 | if (channel == null) return new Result(Outcome.ERROR, ":x: Not a valid channel!"); 47 | 48 | //Send the message 49 | String msg = String.join(" ", ArrayUtils.remove(args, 0)); 50 | channel.sendMessage(msg).queue(); 51 | 52 | //Log it 53 | EmbedBuilder eb = new EmbedBuilder(); 54 | Guild guild = channel.getGuild(); 55 | String authorLogMsg = DiscordUtils.tagAndId(e.getAuthor()); 56 | String channelLogMsg = "sent a message to `#" + channel.getName() + "` (" + channel.getId() + ")"; 57 | String guildLogMsg = "on `" + guild.getName() + "` (" + guild.getId() + ")"; 58 | eb.setAuthor(authorLogMsg, null, e.getAuthor().getAvatarUrl()); 59 | eb.setDescription(MarkdownUtil.bold(channelLogMsg) + "\n" + guildLogMsg + ":\n" + msg); 60 | System.out.println(authorLogMsg + " " + channelLogMsg + "\n" + guildLogMsg + ":\n" + msg); 61 | eb.setThumbnail(guild.getIconUrl()); 62 | Bot.logger.log(MessageCreateData.fromEmbeds(eb.build())); 63 | 64 | return new Result(Outcome.SUCCESS); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/admin/ShutdownCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.admin; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.command.LegacyCommand; 5 | import com.tisawesomeness.minecord.util.DiscordUtils; 6 | 7 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 8 | import net.dv8tion.jda.api.utils.MarkdownUtil; 9 | 10 | public class ShutdownCommand extends LegacyCommand { 11 | 12 | public CommandInfo getInfo() { 13 | return new CommandInfo( 14 | "shutdown", 15 | "Shuts down the bot.", 16 | null, 17 | 0, 18 | true, 19 | true 20 | ); 21 | } 22 | 23 | public String getHelp() { 24 | return "Shuts down the bot. Note that the bot may reboot if it is run by a restart script.\n"; 25 | } 26 | 27 | public Result run(String[] args, MessageReceivedEvent e) { 28 | String msg = "Bot shut down by " + DiscordUtils.tagAndId(e.getAuthor()); 29 | Bot.logger.log(":x: " + MarkdownUtil.bold(msg)); 30 | e.getChannel().sendMessage(":wave: Goodbye!").complete(); 31 | e.getJDA().shutdown(); 32 | System.exit(0); 33 | return new Result(Outcome.SUCCESS); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/admin/TestCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.admin; 2 | 3 | import com.tisawesomeness.minecord.command.LegacyCommand; 4 | 5 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 6 | 7 | public class TestCommand extends LegacyCommand { 8 | 9 | public CommandInfo getInfo() { 10 | return new CommandInfo( 11 | "test", 12 | "Test command.", 13 | null, 14 | 5000, 15 | false, 16 | false 17 | ); 18 | } 19 | 20 | public Result run(String[] args, MessageReceivedEvent e) { 21 | return new Result(Outcome.SUCCESS, "Test"); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/admin/UsageCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.admin; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.command.Command; 5 | import com.tisawesomeness.minecord.command.LegacyCommand; 6 | import com.tisawesomeness.minecord.command.Module; 7 | import com.tisawesomeness.minecord.command.Registry; 8 | import com.tisawesomeness.minecord.util.DateUtils; 9 | import com.tisawesomeness.minecord.util.MessageUtils; 10 | import net.dv8tion.jda.api.EmbedBuilder; 11 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 12 | 13 | import java.util.Arrays; 14 | import java.util.stream.Collectors; 15 | 16 | public class UsageCommand extends LegacyCommand { 17 | 18 | public CommandInfo getInfo() { 19 | return new CommandInfo( 20 | "usage", 21 | "Shows how often commands are used.", 22 | null, 23 | 0, 24 | true, 25 | true 26 | ); 27 | } 28 | 29 | public Result run(String[] args, MessageReceivedEvent e) { 30 | // Build usage message 31 | EmbedBuilder eb = new EmbedBuilder() 32 | .setTitle("Command usage for " + DateUtils.getUptime()) 33 | .setColor(Bot.color); 34 | for (Module m : Registry.modules) { 35 | String field = Arrays.stream(m.getCommands()) 36 | .filter(c -> !c.getInfo().name.isEmpty()) 37 | .filter(c -> !isLegacyCommandWithSlashVariant(c)) 38 | .map(c -> String.format("`%s%s` **-** %d", getPrefix(c, e), c.getInfo().name, Registry.getUses(c))) 39 | .collect(Collectors.joining("\n")); 40 | eb.addField(String.format("**%s**", m.getName()), field, true); 41 | } 42 | 43 | return new Result(Outcome.SUCCESS, MessageUtils.addFooter(eb).build()); 44 | } 45 | 46 | private static boolean isLegacyCommandWithSlashVariant(Command c) { 47 | return c instanceof LegacyCommand && Registry.getSlashCommand(c.getInfo().name).isPresent(); 48 | } 49 | 50 | private static String getPrefix(Command c, MessageReceivedEvent e) { 51 | if (c instanceof LegacyCommand) { 52 | return MessageUtils.getPrefix(e); 53 | } 54 | return "/"; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/core/CreditsCommandLegacy.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.core; 2 | 3 | import com.tisawesomeness.minecord.command.LegacyCommand; 4 | 5 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 6 | 7 | public class CreditsCommandLegacy extends LegacyCommand { 8 | 9 | @Override 10 | public CommandInfo getInfo() { 11 | return new CommandInfo( 12 | "credits", 13 | "See who made the bot possible.", 14 | null, 15 | 0, 16 | true, 17 | false 18 | ); 19 | } 20 | @Override 21 | public String[] getAliases() { 22 | return CreditsCommand.legacyAliases(); 23 | } 24 | 25 | @Override 26 | public Result run(String[] args, MessageReceivedEvent e) { 27 | return CreditsCommand.run(e.getAuthor()); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/core/HelpCommandLegacy.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.core; 2 | 3 | import com.tisawesomeness.minecord.command.LegacyCommand; 4 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 5 | import net.dv8tion.jda.api.interactions.IntegrationType; 6 | import net.dv8tion.jda.api.interactions.InteractionContextType; 7 | 8 | import java.util.EnumSet; 9 | import java.util.Set; 10 | 11 | public class HelpCommandLegacy extends LegacyCommand { 12 | 13 | public CommandInfo getInfo() { 14 | return new CommandInfo( 15 | "help", 16 | "Displays help for the bot, a command, or a module.", 17 | "[||extra]", 18 | 0, 19 | true, 20 | false 21 | ); 22 | } 23 | @Override 24 | public String[] getAliases() { 25 | return HelpCommand.legacyAliases(); 26 | } 27 | @Override 28 | public String getHelp() { 29 | return HelpCommand.help; 30 | } 31 | 32 | public Result run(String[] args, MessageReceivedEvent e) { 33 | String page = args.length == 0 ? null : String.join(" ", args); 34 | Set install = e.isFromGuild() ? EnumSet.of(IntegrationType.GUILD_INSTALL) : EnumSet.noneOf(IntegrationType.class); 35 | InteractionContextType context = e.isFromGuild() ? InteractionContextType.GUILD : InteractionContextType.BOT_DM; 36 | return HelpCommand.run(page, e.getAuthor(), e.getJDA(), install, context); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/core/InfoCommandAdmin.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.core; 2 | 3 | import com.tisawesomeness.minecord.command.LegacyCommand; 4 | 5 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 6 | 7 | public class InfoCommandAdmin extends LegacyCommand { 8 | 9 | public CommandInfo getInfo() { 10 | return new CommandInfo( 11 | "infoadmin", 12 | "Shows the bot info.", 13 | null, 14 | 0, 15 | true, 16 | true 17 | ); 18 | } 19 | 20 | @Override 21 | public String getHelp() { 22 | return "`{&}infoadmin` - Shows the bot info, including memory usage and boot time.\n"; 23 | } 24 | 25 | public Result run(String[] args, MessageReceivedEvent e) throws Exception { 26 | return InfoCommand.run(true, e.getJDA()); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/core/InfoCommandLegacy.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.core; 2 | 3 | import com.tisawesomeness.minecord.command.LegacyCommand; 4 | 5 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 6 | 7 | public class InfoCommandLegacy extends LegacyCommand { 8 | 9 | @Override 10 | public CommandInfo getInfo() { 11 | return new CommandInfo( 12 | "info", 13 | "Shows the bot info.", 14 | null, 15 | 0, 16 | true, 17 | false 18 | ); 19 | } 20 | @Override 21 | public String[] getAliases() { 22 | return InfoCommand.legacyAliases(); 23 | } 24 | 25 | @Override 26 | public Result run(String[] args, MessageReceivedEvent e) { 27 | return InfoCommand.run(false, e.getJDA()); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/core/InviteCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.core; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.Config; 5 | import com.tisawesomeness.minecord.command.SlashCommand; 6 | import com.tisawesomeness.minecord.util.MessageUtils; 7 | import net.dv8tion.jda.api.EmbedBuilder; 8 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; 9 | import net.dv8tion.jda.api.utils.MarkdownUtil; 10 | 11 | public class InviteCommand extends SlashCommand { 12 | 13 | public CommandInfo getInfo() { 14 | return new CommandInfo( 15 | "invite", 16 | "Invite the bot!", 17 | null, 18 | 0, 19 | false, 20 | false 21 | ); 22 | } 23 | 24 | public Result run(SlashCommandInteractionEvent e) { 25 | EmbedBuilder eb = new EmbedBuilder(); 26 | eb.setTitle("Invite Minecord"); 27 | String links = MarkdownUtil.maskedLink("INVITE", Config.getInvite()) + 28 | " | " + MarkdownUtil.maskedLink("SUPPORT", Config.getHelpServer()) + 29 | " | " + MarkdownUtil.maskedLink("WEBSITE", Config.getWebsite()); 30 | eb.setDescription(links); 31 | eb.setColor(Bot.color); 32 | eb = MessageUtils.addFooter(eb); 33 | return new Result(Outcome.SUCCESS, eb.build()); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/core/PingCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.core; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.command.SlashCommand; 5 | import com.tisawesomeness.minecord.util.type.HumanDecimalFormat; 6 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; 7 | 8 | import java.math.RoundingMode; 9 | 10 | public class PingCommand extends SlashCommand { 11 | 12 | private static final HumanDecimalFormat FORMAT = HumanDecimalFormat.builder() 13 | .minimumFractionDigits(0) 14 | .maximumFractionDigits(3) 15 | .roundingMode(RoundingMode.UP) 16 | .build(); 17 | 18 | public CommandInfo getInfo() { 19 | return new CommandInfo( 20 | "ping", 21 | "Pings the bot.", 22 | null, 23 | 0, 24 | true, 25 | false 26 | ); 27 | } 28 | 29 | public static final String help = "Pings the bot.\nUse `/server` to ping a server.\n"; 30 | @Override 31 | public String getHelp() { 32 | return help; 33 | } 34 | 35 | public Result run(SlashCommandInteractionEvent e) { 36 | return run(); 37 | } 38 | public static Result run() { 39 | String ping = FORMAT.format(Bot.getPing()); 40 | String msg = String.format(":ping_pong: **Pong!** `%s ms`\nUse `/server` to ping a server.", ping); 41 | return new Result(Outcome.SUCCESS, msg); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/core/PingCommandLegacy.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.core; 2 | 3 | import com.tisawesomeness.minecord.command.LegacyCommand; 4 | 5 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 6 | 7 | public class PingCommandLegacy extends LegacyCommand { 8 | 9 | public CommandInfo getInfo() { 10 | return new CommandInfo( 11 | "ping", 12 | "Pings the bot.", 13 | null, 14 | 0, 15 | true, 16 | false 17 | ); 18 | } 19 | @Override 20 | public String getHelp() { 21 | return PingCommand.help; 22 | } 23 | 24 | public Result run(String[] args, MessageReceivedEvent e) { 25 | return PingCommand.run(); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/core/PrefixCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.core; 2 | 3 | import com.tisawesomeness.minecord.command.LegacyCommand; 4 | import com.tisawesomeness.minecord.database.Database; 5 | import net.dv8tion.jda.api.Permission; 6 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 7 | import net.dv8tion.jda.api.utils.MarkdownUtil; 8 | 9 | public class PrefixCommand extends LegacyCommand { 10 | 11 | public CommandInfo getInfo() { 12 | return new CommandInfo( 13 | "prefix", 14 | "Change the prefix.", 15 | "[]", 16 | 1000, 17 | true, 18 | false 19 | ); 20 | } 21 | 22 | @Override 23 | public String[] getAliases() { 24 | return new String[]{"resetprefix", "changeprefix"}; 25 | } 26 | 27 | @Override 28 | public String getHelp() { 29 | return "`{&}prefix` - Show the current prefix.\n" + 30 | "`{&}prefix ` - Change the prefix. The user must have **Manage Server** permissions.\n" + 31 | "The prefix can be any text between 1-16 characters.\n" + 32 | "\n" + 33 | "Examples:\n" + 34 | "- `{&}prefix mc!`\n" + 35 | "- {@}` prefix &`\n"; 36 | } 37 | 38 | public Result run(String[] args, MessageReceivedEvent e) throws Exception { 39 | 40 | if (!e.isFromGuild()) { 41 | String username = e.getJDA().getSelfUser().getName(); 42 | return new Result(Outcome.SUCCESS, "The current prefix is " + MarkdownUtil.monospace(username)); 43 | } 44 | 45 | if (args.length == 0) { 46 | 47 | //Print current prefix 48 | return new Result(Outcome.SUCCESS, 49 | "The current prefix is `" + Database.getPrefix(e.getGuild().getIdLong()) + "`" 50 | ); 51 | 52 | } else { 53 | 54 | //Check if user is elevated or has the manage messages permission 55 | if (!Database.isElevated(e.getAuthor().getIdLong()) 56 | && !e.getMember().hasPermission(e.getGuildChannel(), Permission.MANAGE_SERVER)) { 57 | return new Result(Outcome.WARNING, ":warning: You must have manage server permissions!"); 58 | } 59 | 60 | //No prefixes longer than 16 characters 61 | if (args[0].length() > 16) { 62 | return new Result(Outcome.WARNING, ":warning: The prefix you specified is too long!"); 63 | } 64 | //Easter egg for those naughty bois 65 | if (args.length == 3 && args[0].equals("'") && args[1].equals("OR") && args[2].equals("1=1")) { 66 | return new Result(Outcome.SUCCESS, "Nice try."); 67 | } 68 | //Set new prefix 69 | Database.changePrefix(e.getGuild().getIdLong(), args[0]); 70 | return new Result(Outcome.SUCCESS, ":white_check_mark: Prefix changed to `" + args[0] + "`"); 71 | 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/core/SettingsCommandAdmin.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.core; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.command.LegacyCommand; 5 | import com.tisawesomeness.minecord.database.Database; 6 | import com.tisawesomeness.minecord.util.DiscordUtils; 7 | import com.tisawesomeness.minecord.util.MessageUtils; 8 | 9 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 10 | 11 | import java.util.Arrays; 12 | 13 | public class SettingsCommandAdmin extends LegacyCommand { 14 | 15 | @Override 16 | public CommandInfo getInfo() { 17 | return new CommandInfo( 18 | "settingsadmin", 19 | "Change the bot's settings for another guild.", 20 | " [ ]", 21 | 0, 22 | true, 23 | true 24 | ); 25 | } 26 | 27 | @Override 28 | public String getHelp() { 29 | return "`{&}settings ` - View settings for another guild.\n" + 30 | "`{&}settings ` - Changes settings in another guild.\n" + 31 | "\n" + 32 | "Examples:\n" + 33 | "- `{&}settings 347765748577468416`\n" + 34 | "- `{&}settings 347765748577468416 prefix mc!`\n"; 35 | } 36 | 37 | @Override 38 | public Result run(String[] args, MessageReceivedEvent e) throws Exception { 39 | // If the author used the admin keyword and is an elevated user 40 | String sourcePrefix = MessageUtils.getPrefix(e); 41 | if (!DiscordUtils.isDiscordId(args[0])) { 42 | return new Result(Outcome.WARNING, ":warning: Not a valid ID!"); 43 | } 44 | if (Bot.shardManager.getGuildById(args[0]) == null) { 45 | return new Result(Outcome.WARNING, ":warning: Minecord does not know that guild ID!"); 46 | } 47 | long gid = Long.parseLong(args[0]); 48 | args = Arrays.copyOfRange(args, 1, args.length); 49 | String targetPrefix = Database.getPrefix(gid); 50 | return SettingsCommand.run(args, e, sourcePrefix, targetPrefix, gid, true); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/core/VoteCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.core; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.Config; 5 | import com.tisawesomeness.minecord.command.SlashCommand; 6 | import com.tisawesomeness.minecord.util.MessageUtils; 7 | 8 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; 9 | import net.dv8tion.jda.api.utils.MarkdownUtil; 10 | 11 | public class VoteCommand extends SlashCommand { 12 | 13 | public CommandInfo getInfo() { 14 | return new CommandInfo( 15 | "vote", 16 | "Vote for the bot!", 17 | null, 18 | 0, 19 | false, 20 | false 21 | ); 22 | } 23 | 24 | @Override 25 | public String[] getLegacyAliases() { 26 | return new String[]{"v", "upvote", "updoot", "rep"}; 27 | } 28 | 29 | public Result run(SlashCommandInteractionEvent e) { 30 | String m = "Top.gg: " + MarkdownUtil.maskedLink("VOTE", "https://top.gg/bot/292279711034245130/vote"); 31 | String title = Config.isSelfHosted() ? "Vote for the main bot!" : "Vote for Minecord!"; 32 | return new Result(Outcome.SUCCESS, MessageUtils.embedMessage(title, null, m, Bot.color)); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/discord/GuildCommandAdmin.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.discord; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.command.LegacyCommand; 5 | import com.tisawesomeness.minecord.database.Database; 6 | import com.tisawesomeness.minecord.util.DiscordUtils; 7 | 8 | import net.dv8tion.jda.api.entities.Guild; 9 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 10 | import net.dv8tion.jda.api.utils.messages.MessageCreateData; 11 | 12 | public class GuildCommandAdmin extends LegacyCommand { 13 | 14 | public CommandInfo getInfo() { 15 | return new CommandInfo( 16 | "guildadmin", 17 | "Shows guild info.", 18 | "", 19 | 0, 20 | true, 21 | true 22 | ); 23 | } 24 | 25 | @Override 26 | public String getHelp() { 27 | return "`{&}guildadmin ` - Shows the info of another guild.\n" + 28 | "\n" + 29 | "Examples:\n" + 30 | "- `{&}guild 347765748577468416`\n"; 31 | } 32 | 33 | public Result run(String[] args, MessageReceivedEvent e) throws Exception { 34 | if (args.length == 0) { 35 | return new Result(Outcome.WARNING, ":warning: You need to specify a guild id."); 36 | } 37 | if (!DiscordUtils.isDiscordId(args[0])) { 38 | return new Result(Outcome.WARNING, ":warning: Not a valid ID!"); 39 | } 40 | Guild g = Bot.shardManager.getGuildById(args[0]); 41 | if (g == null) { 42 | long gid = Long.parseLong(args[0]); 43 | if (Database.isBanned(gid)) { 44 | return new Result(Outcome.SUCCESS, "__**GUILD BANNED FROM MINECORD**__\n" + GuildCommand.getSettingsStr(gid)); 45 | } 46 | return new Result(Outcome.SUCCESS, GuildCommand.getSettingsStr(gid)); 47 | } 48 | GuildCommand.buildReply(g, true) 49 | .thenAccept(eb -> sendSuccess(e, MessageCreateData.fromEmbeds(eb.build()))); 50 | return new Result(Outcome.SUCCESS); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/discord/IdCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.discord; 2 | 3 | import com.tisawesomeness.minecord.command.SlashCommand; 4 | 5 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; 6 | import net.dv8tion.jda.api.interactions.commands.OptionType; 7 | import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; 8 | import net.dv8tion.jda.api.utils.TimeFormat; 9 | import net.dv8tion.jda.api.utils.TimeUtil; 10 | 11 | import java.time.OffsetDateTime; 12 | 13 | public class IdCommand extends SlashCommand { 14 | 15 | public CommandInfo getInfo() { 16 | return new CommandInfo( 17 | "id", 18 | "Gets the creation time of a Discord ID.", 19 | "", 20 | 0, 21 | false, 22 | false 23 | ); 24 | } 25 | 26 | @Override 27 | public SlashCommandData addCommandSyntax(SlashCommandData builder) { 28 | return builder.addOption(OptionType.INTEGER, "id", "The Discord ID", true); 29 | } 30 | 31 | @Override 32 | public String[] getLegacyAliases() { 33 | return new String[]{"snowflake"}; 34 | } 35 | 36 | @Override 37 | public String getHelp() { 38 | return "`{&}id ` - Gets the creation time of a Discord ID.\n" + 39 | "This command does not check if an ID exists.\n" + 40 | "To get Discord IDs, turn on User Settings > Advanced > Developer Mode, then right click and select \"Copy ID\"\n" + 41 | "The `{&}user`/`{&}role`/`{&}guild` commands also show IDs.\n" + 42 | "\n" + 43 | "Examples:\n" + 44 | "- `{&}id 211261249386708992`\n" + 45 | "- `{&}id 292279711034245130`\n"; 46 | } 47 | 48 | @Override 49 | public Result run(SlashCommandInteractionEvent e) throws Exception { 50 | long id = e.getOption("id").getAsLong(); 51 | OffsetDateTime time = TimeUtil.getTimeCreated(id); 52 | return new Result(Outcome.SUCCESS, "Created " + TimeFormat.RELATIVE.format(time)); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/discord/RoleCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.discord; 2 | 3 | import com.tisawesomeness.minecord.command.SlashCommand; 4 | import com.tisawesomeness.minecord.util.ColorUtils; 5 | import com.tisawesomeness.minecord.util.MessageUtils; 6 | import net.dv8tion.jda.api.EmbedBuilder; 7 | import net.dv8tion.jda.api.entities.Guild; 8 | import net.dv8tion.jda.api.entities.MessageEmbed; 9 | import net.dv8tion.jda.api.entities.Role; 10 | import net.dv8tion.jda.api.entities.RoleIcon; 11 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; 12 | import net.dv8tion.jda.api.interactions.InteractionContextType; 13 | import net.dv8tion.jda.api.interactions.commands.OptionType; 14 | import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; 15 | import net.dv8tion.jda.api.utils.TimeFormat; 16 | 17 | import javax.annotation.Nullable; 18 | import java.util.List; 19 | 20 | public class RoleCommand extends SlashCommand { 21 | 22 | public CommandInfo getInfo() { 23 | return new CommandInfo( 24 | "role", 25 | "Shows role info.", 26 | "", 27 | 0, 28 | false, 29 | false 30 | ); 31 | } 32 | 33 | @Override 34 | public SlashCommandData addCommandSyntax(SlashCommandData builder) { 35 | return builder.setContexts(InteractionContextType.GUILD) 36 | .addOption(OptionType.ROLE, "role", "The role to look up", true); 37 | } 38 | 39 | @Override 40 | public String[] getLegacyAliases() { 41 | return new String[]{"roleinfo"}; 42 | } 43 | 44 | @Override 45 | public String getHelp() { 46 | return "Shows the info of a role in the current guild.\n" + 47 | "\n" + 48 | "Examples:\n" + 49 | "- `{&}role Moderator`\n"; 50 | } 51 | 52 | public Result run(SlashCommandInteractionEvent e) { 53 | Role role = e.getOption("role").getAsRole(); 54 | return run(role, e.getGuild()); 55 | } 56 | 57 | public static Result run(Role role, @Nullable Guild g) { 58 | boolean isNotDetached = g != null && !g.isDetached(); 59 | EmbedBuilder eb = new EmbedBuilder() 60 | .setTitle(role.getName().substring(0, Math.min(MessageEmbed.TITLE_MAX_LENGTH, role.getName().length()))) 61 | .setColor(role.getColorRaw()) 62 | .addField("ID", role.getId(), true) 63 | .addField("Color", ColorUtils.getHexCode(role.getColorRaw()), true); // Mask gets RGB of color 64 | if (isNotDetached) { 65 | List roles = g.getRoles(); 66 | // Position corrected so @everyone is pos 1 67 | eb.addField("Position", (role.getPosition() + 2) + "/" + roles.size(), true); 68 | } else { 69 | eb.addField("Role Created", TimeFormat.RELATIVE.format(role.getTimeCreated()), true); 70 | } 71 | eb.addField("Mentionable?", role.isMentionable() ? "Yes" : "No", true) 72 | .addField("Hoisted?", role.isHoisted() ? "Yes" : "No", true) 73 | .addField("Managed?", role.isManaged() ? "Yes" : "No", true); 74 | 75 | RoleIcon icon = role.getIcon(); 76 | if (icon != null) { 77 | if (icon.isEmoji()) { 78 | eb.addField("Role Icon Emoji", icon.getEmoji(), true); 79 | } else { 80 | eb.setThumbnail(icon.getIconUrl()); 81 | } 82 | } 83 | 84 | if (isNotDetached) { 85 | eb.addField("Role Created", TimeFormat.RELATIVE.format(role.getTimeCreated()), true); 86 | } 87 | 88 | return new Result(Outcome.SUCCESS, MessageUtils.addFooter(eb).build()); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/discord/RoleCommandAdmin.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.discord; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.command.LegacyCommand; 5 | import net.dv8tion.jda.api.entities.Role; 6 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 7 | 8 | public class RoleCommandAdmin extends LegacyCommand { 9 | 10 | @Override 11 | public CommandInfo getInfo() { 12 | return new CommandInfo( 13 | "roleadmin", 14 | "Shows role info.", 15 | "", 16 | 0, 17 | true, 18 | true 19 | ); 20 | } 21 | 22 | @Override 23 | public String getHelp() { 24 | return "`{&}roleadmin ` - Shows the info of any role.\n" + 25 | "\n" + 26 | "Examples:\n" + 27 | "- `{&}role 347797250266628108`\n"; 28 | } 29 | 30 | @Override 31 | public Result run(String[] args, MessageReceivedEvent e) throws Exception { 32 | if (args.length == 0) { 33 | return new Result(Outcome.WARNING, ":warning: You must specify a role id!"); 34 | } 35 | Role role = Bot.shardManager.getRoleById(args[0]); 36 | if (role == null) { 37 | return new Result(Outcome.WARNING, ":warning: That role does not exist."); 38 | } 39 | return RoleCommand.run(role, null); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/discord/RolesCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.discord; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.command.SlashCommand; 5 | import com.tisawesomeness.minecord.util.MessageUtils; 6 | import com.tisawesomeness.minecord.util.StringUtils; 7 | import net.dv8tion.jda.api.EmbedBuilder; 8 | import net.dv8tion.jda.api.entities.IMentionable; 9 | import net.dv8tion.jda.api.entities.Member; 10 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; 11 | import net.dv8tion.jda.api.interactions.IntegrationType; 12 | import net.dv8tion.jda.api.interactions.InteractionContextType; 13 | import net.dv8tion.jda.api.interactions.commands.OptionType; 14 | import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; 15 | 16 | import java.util.ArrayList; 17 | import java.util.stream.Collectors; 18 | 19 | public class RolesCommand extends SlashCommand { 20 | 21 | public CommandInfo getInfo() { 22 | return new CommandInfo( 23 | "roles", 24 | "List a user's roles.", 25 | "", 26 | 0, 27 | false, 28 | false 29 | ); 30 | } 31 | 32 | @Override 33 | public SlashCommandData addCommandSyntax(SlashCommandData builder) { 34 | return builder.setIntegrationTypes(IntegrationType.GUILD_INSTALL) 35 | .setContexts(InteractionContextType.GUILD) 36 | .addOption(OptionType.USER, "user", "The user to list roles for", true); 37 | } 38 | 39 | @Override 40 | public String getHelp() { 41 | return "List the roles of a user in the current guild.\n" + 42 | "\n" + 43 | "Examples:\n" + 44 | "- `{&}roles @Tis_awesomeness`\n"; 45 | } 46 | 47 | public Result run(SlashCommandInteractionEvent e) { 48 | // Find user 49 | Member mem = e.getOption("user").getAsMember(); 50 | if (mem == null) { 51 | return new Result(Outcome.WARNING, ":warning: That user is not in this guild."); 52 | } 53 | 54 | EmbedBuilder eb = new EmbedBuilder() 55 | .setTitle("Roles for " + mem.getUser().getEffectiveName()) 56 | .setColor(Bot.color); 57 | 58 | // Truncate role list until 6000 chars reached 59 | ArrayList lines = mem.getRoles().stream() 60 | .map(IMentionable::getAsMention) 61 | .collect(Collectors.toCollection(ArrayList::new)); 62 | int chars = StringUtils.getTotalChars(lines); 63 | boolean truncated = false; 64 | while (chars > 6000 - 4) { 65 | truncated = true; 66 | lines.remove(lines.size() - 1); 67 | chars = StringUtils.getTotalChars(lines); 68 | } 69 | if (truncated) { 70 | lines.add("..."); 71 | } 72 | 73 | // If over 2048, use fields, otherwise use description 74 | if (chars > 2048) { 75 | // Split into fields, avoiding 1024 field char limit 76 | for (String field : StringUtils.splitLinesByLength(lines, 1024)) { 77 | eb.addField("Roles", field, true); 78 | } 79 | } else { 80 | eb.setDescription(String.join("\n", lines)); 81 | } 82 | 83 | return new Result(Outcome.SUCCESS, MessageUtils.addFooter(eb).build()); 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/player/CapeCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.player; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.mc.player.Player; 5 | import com.tisawesomeness.minecord.mc.player.RenderType; 6 | import com.tisawesomeness.minecord.util.ColorUtils; 7 | import net.dv8tion.jda.api.EmbedBuilder; 8 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; 9 | 10 | import java.awt.*; 11 | import java.io.IOException; 12 | import java.net.URL; 13 | import java.util.Optional; 14 | 15 | public class CapeCommand extends BasePlayerCommand { 16 | 17 | public CommandInfo getInfo() { 18 | return new CommandInfo( 19 | "cape", 20 | "Shows a player's capes.", 21 | "", 22 | 1000, 23 | false, 24 | false 25 | ); 26 | } 27 | 28 | @Override 29 | public String getHelp() { 30 | return "`{&}cape ` - Shows an image of the player's Minecraft and Optifine capes.\n" + 31 | "- `` can be a username or UUID.\n" + 32 | "Use `{&}help usernameInput|uuidInput|phd` for more help.\n" + 33 | "\n" + 34 | "Examples:\n" + 35 | "- `{&}cape Tis_awesomeness`\n" + 36 | "- `{&}cape LadyAgnes`\n" + 37 | "- `{&}cape f6489b797a9f49e2980e265a05dbc3af`\n" + 38 | "- `{&}cape 069a79f4-44e9-4726-a5be-fca90e38aaf5`\n"; 39 | } 40 | 41 | protected void onSuccessfulPlayer(SlashCommandInteractionEvent e, Player player) { 42 | boolean hasMojangCape = false; 43 | Optional capeUrlOpt = player.getProfile().getCapeUrl(); 44 | if (capeUrlOpt.isPresent()) { 45 | URL capeUrl = capeUrlOpt.get(); 46 | sendCape(e, player, capeUrl, "Minecraft"); 47 | hasMojangCape = true; 48 | } 49 | 50 | URL optifineCapeUrl = player.getOptifineCapeUrl(); 51 | boolean hasOptifineCape = false; 52 | try { 53 | hasOptifineCape = Bot.mcLibrary.getClient().exists(optifineCapeUrl); 54 | } catch (IOException ex) { 55 | System.err.println("IOE getting optifine cape for " + player); 56 | ex.printStackTrace(); 57 | e.getHook().sendMessage("There was an error requesting the Optifine cape.").setEphemeral(true).queue(); 58 | } 59 | if (hasOptifineCape) { 60 | sendCape(e, player, optifineCapeUrl, "Optifine"); 61 | } 62 | 63 | if (!hasMojangCape && !hasOptifineCape) { 64 | e.getHook().sendMessage(player.getUsername() + " does not have a cape.").queue(); 65 | } 66 | } 67 | 68 | private void sendCape(SlashCommandInteractionEvent e, Player player, URL capeUrl, String capeType) { 69 | String nameMcUrl = player.getNameMCUrl().toString(); 70 | String avatarUrl = player.createRender(RenderType.AVATAR, true).render().toString(); 71 | String title = capeType + " Cape for " + player.getUsername(); 72 | Color color = player.isRainbow() ? ColorUtils.randomColor() : Bot.color; 73 | EmbedBuilder eb = new EmbedBuilder() 74 | .setAuthor(title, nameMcUrl, avatarUrl) 75 | .setColor(color) 76 | .setImage(capeUrl.toString()); 77 | uploadOrEmbedImages(e, eb.build()); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/player/HistoryCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.player; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.mc.player.Player; 5 | import com.tisawesomeness.minecord.mc.player.RenderType; 6 | import com.tisawesomeness.minecord.util.ColorUtils; 7 | import com.tisawesomeness.minecord.util.MessageUtils; 8 | 9 | import net.dv8tion.jda.api.EmbedBuilder; 10 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; 11 | 12 | import java.awt.Color; 13 | 14 | public class HistoryCommand extends BasePlayerCommand { 15 | 16 | public CommandInfo getInfo() { 17 | return new CommandInfo( 18 | "history", 19 | "Shows a player's name history.", 20 | "", 21 | 1000, 22 | false, 23 | false 24 | ); 25 | } 26 | 27 | @Override 28 | public String[] getLegacyAliases() { 29 | return new String[]{"h", "hist", "namehist", "namehistory"}; 30 | } 31 | 32 | @Override 33 | public String getHelp() { 34 | return "`{&}history ` - Shows a player''s name history.\n" + 35 | "- `` can be a username or UUID.\n" + 36 | "Use `{&}help usernameInput|uuidInput|phd` for more help.\n" + 37 | "\n" + 38 | "Examples:\n" + 39 | "- `{&}history Tis_awesomeness`\n" + 40 | "- `{&}history LadyAgnes`\n" + 41 | "- `{&}history f6489b797a9f49e2980e265a05dbc3af`\n" + 42 | "- `{&}history 069a79f4-44e9-4726-a5be-fca90e38aaf5`\n"; 43 | } 44 | 45 | protected void onSuccessfulPlayer(SlashCommandInteractionEvent e, Player player) { 46 | String title = "Name History for " + player.getUsername(); 47 | String nameMCUrl = player.getNameMCUrl().toString(); 48 | String avatarUrl = player.createRender(RenderType.AVATAR, true).render().toString(); 49 | Color color = player.isRainbow() ? ColorUtils.randomColor() : Bot.color; 50 | EmbedBuilder eb = MessageUtils.addFooter(new EmbedBuilder()) 51 | .setAuthor(title, nameMCUrl, avatarUrl) 52 | .setColor(color) 53 | .setDescription("Mojang has removed the name history API, [read more here](https://help.minecraft.net/hc/en-us/articles/8969841895693). We are working on a different way to retrieve name history, stay tuned.\nUse `/profile` to look up a player's other information"); 54 | e.getHook().sendMessageEmbeds(eb.build()).queue(); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/player/SkinCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.player; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.mc.player.Player; 5 | import com.tisawesomeness.minecord.mc.player.ProfileAction; 6 | import com.tisawesomeness.minecord.mc.player.RenderType; 7 | import com.tisawesomeness.minecord.util.ColorUtils; 8 | import com.tisawesomeness.minecord.util.MessageUtils; 9 | import lombok.NonNull; 10 | import net.dv8tion.jda.api.EmbedBuilder; 11 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; 12 | 13 | import java.awt.*; 14 | 15 | public class SkinCommand extends BasePlayerCommand { 16 | 17 | public CommandInfo getInfo() { 18 | return new CommandInfo( 19 | "skin", 20 | "Shows an image of a player's skin.", 21 | "", 22 | 1000, 23 | false, 24 | false 25 | ); 26 | } 27 | 28 | public String getHelp() { 29 | return "`{&}skin ` - Shows an image of the player's skin.\n" + 30 | "- `` can be a username or UUID.\n" + 31 | "Use `{&}help usernameInput|uuidInput|phd` for more help.\n" + 32 | "\n" + 33 | "Examples:\n" + 34 | "- `{&}skin Tis_awesomeness`\n" + 35 | "- `{&}skin LadyAgnes`\n" + 36 | "- `{&}skin f6489b797a9f49e2980e265a05dbc3af`\n" + 37 | "- `{&}skin 069a79f4-44e9-4726-a5be-fca90e38aaf5`\n"; 38 | } 39 | 40 | protected void onSuccessfulPlayer(SlashCommandInteractionEvent e, Player player) { 41 | String title = "Skin for " + player.getUsername(); 42 | String skinHistoryUrl = player.getMCSkinHistoryUrl().toString(); 43 | String avatarUrl = player.createRender(RenderType.AVATAR, true).render().toString(); 44 | String skinUrl = player.getSkinUrl().toString(); 45 | String description = constructDescription(player); 46 | 47 | Color color = player.isRainbow() ? ColorUtils.randomColor() : Bot.color; 48 | EmbedBuilder eb = MessageUtils.addFooter(new EmbedBuilder()) 49 | .setAuthor(title, skinHistoryUrl, avatarUrl) 50 | .setColor(color) 51 | .setDescription(description) 52 | .setImage(skinUrl); 53 | uploadOrEmbedImages(e, eb.build()); 54 | } 55 | private static @NonNull String constructDescription(Player player) { 56 | String custom = "**Custom**: " + (player.hasCustomSkin() ? "True" : "False"); 57 | String skinModel = "**Skin Model**: " + player.getSkinModel().getDescription(); 58 | String defaultModel = "**Default Skin Model**: " + player.getDefaultSkinModel().getDescription(); 59 | String newDefaultModel = "**1.19.3+ Default Skin**: " + player.getNewDefaultSkin(); 60 | 61 | String description = custom + "\n" + skinModel + "\n" + defaultModel + "\n" + newDefaultModel; 62 | if (player.getProfile().getProfileActions().contains(ProfileAction.USING_BANNED_SKIN)) { 63 | String banNotice = player.hasCustomSkin() ? 64 | "__Cannot play multiplayer due to banned skin.__" : 65 | "__Cannot play multiplayer due to banned skin__ (skin has been reset)."; 66 | return banNotice + "\n\n" + description; 67 | } 68 | return description; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/utility/CodesCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.utility; 2 | 3 | import com.tisawesomeness.minecord.command.SlashCommand; 4 | import com.tisawesomeness.minecord.util.ColorUtils; 5 | import com.tisawesomeness.minecord.util.MessageUtils; 6 | 7 | import net.dv8tion.jda.api.EmbedBuilder; 8 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; 9 | 10 | public class CodesCommand extends SlashCommand { 11 | 12 | public CommandInfo getInfo() { 13 | return new CommandInfo( 14 | "codes", 15 | "Lists the available chat codes.", 16 | null, 17 | 1000, 18 | false, 19 | false 20 | ); 21 | } 22 | 23 | @Override 24 | public String[] getLegacyAliases() { 25 | return new String[]{"code", "chat"}; 26 | } 27 | 28 | private static final String img = "https://minecraft.wiki/images/Minecraft_Formatting.gif?2311f"; 29 | 30 | public Result run(SlashCommandInteractionEvent e) { 31 | 32 | String desc = "Symbol copy-paste: `\u00A7`, `\\u00A7`\nUse `/color` to get info on a color."; 33 | EmbedBuilder eb = MessageUtils.addFooter(new EmbedBuilder()) 34 | .setTitle("Minecraft Chat Codes") 35 | .setColor(ColorUtils.randomColor()) 36 | .setDescription(desc) 37 | .setImage(img); 38 | return new Result(Outcome.SUCCESS, MessageUtils.addFooter(eb).build()); 39 | 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/utility/ColorCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.utility; 2 | 3 | import com.tisawesomeness.minecord.command.SlashCommand; 4 | import com.tisawesomeness.minecord.util.ColorUtils; 5 | import com.tisawesomeness.minecord.util.MessageUtils; 6 | import net.dv8tion.jda.api.EmbedBuilder; 7 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; 8 | import net.dv8tion.jda.api.interactions.commands.OptionType; 9 | import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; 10 | 11 | import java.awt.*; 12 | 13 | public class ColorCommand extends SlashCommand { 14 | 15 | public CommandInfo getInfo() { 16 | return new CommandInfo( 17 | "color", 18 | "Look up a color or Minecraft color code.", 19 | "", 20 | 0, 21 | false, 22 | false 23 | ); 24 | } 25 | 26 | @Override 27 | public SlashCommandData addCommandSyntax(SlashCommandData builder) { 28 | return builder.addOption(OptionType.STRING, "color", "A color", true); 29 | } 30 | 31 | @Override 32 | public String[] getLegacyAliases() { 33 | return new String[]{"colour", "colorcode", "colourcode"}; 34 | } 35 | 36 | @Override 37 | public String getHelp() { 38 | return "`{&}color ` - Look up a color.\n" + 39 | "Shows extra info if the color is one of the 16 Minecraft color codes.\n" + 40 | "\n" + 41 | "`` can be:\n" + 42 | "- A Minecraft color name: `red`, `dark blue`\n" + 43 | "- A color code: `&b`, `\u00A7b`, `b`, `8`\n" + 44 | "- A hex code: `#55ffff`, `0x55ffff`, `#8855ffff`\n" + 45 | "- RGB format: `85 85 255`, `rgb(85,85,255)`, `rgb(50%,50%,100%)`\n" + 46 | "- RGBA format: `85 85 255 0.5`, `rgba(85,85,255,0.5)`\n" + 47 | "- Other formats: `hsv(120,100%,50%)`, `hsl(120 100% 25%)`, `hsla(120 100% 25% 0.5)`, `cmyk(100%,0%,100%,50%)`\n" + 48 | "- An RGB int: `5635925`, `i8`\n"; 49 | } 50 | 51 | public Result run(SlashCommandInteractionEvent e) { 52 | Color c = ColorUtils.parseColor(e.getOption("color").getAsString(), "en_US"); 53 | if (c == null) { 54 | return new Result(Outcome.WARNING, ":warning: Not a valid color!"); 55 | } 56 | EmbedBuilder eb = buildEmbed(c); 57 | return new Result(Outcome.SUCCESS, MessageUtils.addFooter(eb).build()); 58 | } 59 | 60 | public static EmbedBuilder buildEmbed(Color c) { 61 | String formats = ColorUtils.getRGB(c) + "\n" + 62 | ColorUtils.getRGBA(c) + "\n" + 63 | ColorUtils.getHSL(c) + "\n" + 64 | ColorUtils.getHSLA(c) + "\n" + 65 | ColorUtils.getHSV(c) + "\n" + 66 | ColorUtils.getCMYK(c); 67 | String hexCode = String.format("%s (w/o alpha)\n%s (w/ alpha)", ColorUtils.getHexCode(c), ColorUtils.getHexCodeWithAlpha(c)); 68 | String integer = String.format("%d (w/o alpha)\n%d (w/ alpha)", ColorUtils.getInt(c), ColorUtils.getIntWithAlpha(c)); 69 | EmbedBuilder eb = new EmbedBuilder() 70 | .setTitle("Color Info") 71 | .setColor(c) 72 | .addField("Other formats", formats, true) 73 | .addField("Hex Code", hexCode, true) 74 | .addField("Integer", integer, true); 75 | // Test for Minecraft color 76 | int colorID = ColorUtils.getMCIndex(c); 77 | if (colorID >= 0) { 78 | eb.addField("Name", ColorUtils.getName(colorID), true) 79 | .addField("Chat Code", ColorUtils.getColorCode(colorID), true) 80 | .addField("Background Color", ColorUtils.getBackgroundHex(colorID), true); 81 | } 82 | return eb; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/utility/ItemCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.utility; 2 | 3 | import com.tisawesomeness.minecord.command.SlashCommand; 4 | import com.tisawesomeness.minecord.mc.VersionRegistry; 5 | import com.tisawesomeness.minecord.mc.item.ItemRegistry; 6 | import com.tisawesomeness.minecord.util.MessageUtils; 7 | import net.dv8tion.jda.api.EmbedBuilder; 8 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; 9 | import net.dv8tion.jda.api.interactions.commands.OptionType; 10 | import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; 11 | 12 | public class ItemCommand extends SlashCommand { 13 | 14 | public CommandInfo getInfo() { 15 | return new CommandInfo( 16 | "item", 17 | "Looks up an item.", 18 | "", 19 | 1000, 20 | false, 21 | false 22 | ); 23 | } 24 | 25 | @Override 26 | public SlashCommandData addCommandSyntax(SlashCommandData builder) { 27 | return builder.addOption(OptionType.STRING, "item", "The Minecraft item to look up", true); 28 | } 29 | 30 | @Override 31 | public String[] getLegacyAliases() { 32 | return new String[]{"i"}; 33 | } 34 | 35 | @Override 36 | public String getHelp() { 37 | return "Searches for a Minecraft item.\n" + 38 | "Items are from Java Edition 1.7 to " + VersionRegistry.getLatestVersion() + ".\n" + 39 | "\n" + 40 | ItemRegistry.help + "\n"; 41 | } 42 | 43 | public Result run(SlashCommandInteractionEvent e) { 44 | // Search through the item database 45 | String search = e.getOption("item").getAsString(); 46 | String item = ItemRegistry.search(search); 47 | 48 | // If nothing is found 49 | if (item == null) { 50 | return new Result(Outcome.WARNING, 51 | ":warning: That item does not exist! " + 52 | "\n" + "Did you spell it correctly?"); 53 | } 54 | 55 | // Build message 56 | EmbedBuilder eb = ItemRegistry.display(item, "/"); 57 | eb = MessageUtils.addFooter(eb); 58 | 59 | return new Result(Outcome.SUCCESS, eb.build()); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/utility/SeedCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.utility; 2 | 3 | import com.tisawesomeness.minecord.command.SlashCommand; 4 | 5 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; 6 | import net.dv8tion.jda.api.interactions.commands.OptionType; 7 | import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; 8 | 9 | public class SeedCommand extends SlashCommand { 10 | 11 | public CommandInfo getInfo() { 12 | return new CommandInfo( 13 | "seed", 14 | "Converts some text to a seed number.", 15 | "", 16 | 0, 17 | false, 18 | false 19 | ); 20 | } 21 | 22 | @Override 23 | public SlashCommandData addCommandSyntax(SlashCommandData builder) { 24 | return builder.addOption(OptionType.STRING, "text", "The text to convert to a seed number", true); 25 | } 26 | 27 | @Override 28 | public String getHelp() { 29 | return "`{&}seed ` - Converts some text to a seed number.\n" + 30 | "Spaces at the start and end are removed, and numbers are treated as strings.\n" + 31 | "\n" + 32 | "Examples:\n" + 33 | "- `{&}seed Glacier`\n" + 34 | "- `{&}seed zsjpxah` - numeric seed 0\n" + 35 | "- `{&}seed 0` - treated as a string\n"; 36 | } 37 | 38 | @Override 39 | public Result run(SlashCommandInteractionEvent e) throws Exception { 40 | long seed = e.getOption("text").getAsString().hashCode(); 41 | return new Result(Outcome.SUCCESS, String.format("Seed: `%d`", seed)); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/utility/Sha1Command.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.utility; 2 | 3 | import com.tisawesomeness.minecord.command.SlashCommand; 4 | import com.tisawesomeness.minecord.util.MathUtils; 5 | 6 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; 7 | import net.dv8tion.jda.api.interactions.commands.OptionType; 8 | import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; 9 | 10 | public class Sha1Command extends SlashCommand { 11 | 12 | public CommandInfo getInfo() { 13 | return new CommandInfo( 14 | "sha1", 15 | "Computes the sha1 hash of some text.", 16 | "", 17 | 0, 18 | false, 19 | false 20 | ); 21 | } 22 | 23 | @Override 24 | public SlashCommandData addCommandSyntax(SlashCommandData builder) { 25 | return builder.addOption(OptionType.STRING, "text", "The text to hash", true); 26 | } 27 | 28 | @Override 29 | public String[] getLegacyAliases() { 30 | return new String[]{"sha", "hash"}; 31 | } 32 | 33 | @Override 34 | public String getHelp() { 35 | return "`{&}sha1 ` - Computes the sha1 hash of some text.\n" + 36 | "Useful for comparing a server against Mojang's blocked server list.\n" + 37 | "\n" + 38 | "Examples:\n" + 39 | "- `{&}sha1 any string here`\n" + 40 | "- `{&}sha1 mc.hypixel.net`\n"; 41 | } 42 | 43 | public Result run(SlashCommandInteractionEvent e) { 44 | return new Result(Outcome.SUCCESS, MathUtils.sha1(e.getOption("text").getAsString())); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/command/utility/ShadowCommand.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.command.utility; 2 | 3 | import com.tisawesomeness.minecord.command.SlashCommand; 4 | 5 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; 6 | import net.dv8tion.jda.api.interactions.commands.OptionType; 7 | import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; 8 | 9 | public class ShadowCommand extends SlashCommand { 10 | 11 | private static final long SUM = -7379792620528906219L; 12 | 13 | public CommandInfo getInfo() { 14 | return new CommandInfo( 15 | "shadow", 16 | "Gets the shadow of a seed.", 17 | "", 18 | 0, 19 | false, 20 | false 21 | ); 22 | } 23 | 24 | @Override 25 | public SlashCommandData addCommandSyntax(SlashCommandData builder) { 26 | return builder.addOption(OptionType.STRING, "seed", "The seed to get the shadow of", true); 27 | } 28 | 29 | @Override 30 | public String getHelp() { 31 | return "`{&}shadow ` - Generates a seed's \"shadow\", where the biome maps are the same but everything else is different.\n" + 32 | "Spaces at the start and end are removed, and numbers are treated as raw numbers (same as MC 1.18.2).\n" + 33 | "\n" + 34 | "Examples:\n" + 35 | "- `{&}shadow Glacier`\n" + 36 | "- `{&}shadow zsjpxah` - converted to numeric 0\n" + 37 | "- `{&}shadow 0` - numeric seed 0\n"; 38 | } 39 | 40 | public Result run(SlashCommandInteractionEvent e) { 41 | String input = e.getOption("seed").getAsString(); 42 | long shadow = shadow(stringToSeed(input)); 43 | return new Result(Outcome.SUCCESS, String.format("Shadow Seed: `%s`", shadow)); 44 | } 45 | 46 | private static long shadow(long seed) { 47 | return SUM - seed; 48 | } 49 | private static long stringToSeed(String input) { 50 | String seed = input.trim(); 51 | try { 52 | return Long.parseLong(seed); 53 | } catch (NumberFormatException ignored) { 54 | return seed.hashCode(); 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/database/DbGuild.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.database; 2 | 3 | public class DbGuild { 4 | 5 | public long id; 6 | public String prefix; 7 | public String lang; 8 | public boolean banned; 9 | public boolean noCooldown; 10 | public Boolean deleteCommands; 11 | public Boolean noMenu; 12 | 13 | public DbGuild(long id, String prefix, String lang, boolean banned, boolean noCooldown, Boolean deleteCommands, Boolean noMenu) { 14 | this.id = id; 15 | this.prefix = prefix; 16 | this.lang = lang; 17 | this.banned = banned; 18 | this.noCooldown = noCooldown; 19 | this.deleteCommands = deleteCommands; 20 | this.noMenu = noMenu; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/database/DbUser.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.database; 2 | 3 | public class DbUser { 4 | 5 | public long id; 6 | public boolean elevated; 7 | public boolean banned; 8 | 9 | public DbUser(long id, boolean elevated, boolean banned) { 10 | this.id = id; 11 | this.elevated = elevated; 12 | this.banned = banned; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/debug/ClientDebugOption.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.debug; 2 | 3 | import com.tisawesomeness.minecord.network.APIClient; 4 | 5 | import lombok.NonNull; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | @RequiredArgsConstructor 9 | public class ClientDebugOption implements DebugOption { 10 | private final @NonNull APIClient client; 11 | public @NonNull String getName() { 12 | return "client"; 13 | } 14 | public @NonNull String debug(@NonNull String extra) { 15 | return String.format("Calls: %d queued, %d running\nConnections: %d idle, %d total", 16 | client.getQueuedCallsCount(), client.getRunningCallsCount(), 17 | client.getIdleConnectionCount(), client.getConnectionCount()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/debug/DebugOption.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.debug; 2 | 3 | import lombok.NonNull; 4 | 5 | /** 6 | * Provides debug information to the {@code &debug} command. 7 | */ 8 | public interface DebugOption { 9 | /** 10 | * @return The name of this debug option, used for user input 11 | */ 12 | @NonNull String getName(); 13 | /** 14 | * Gets useful debug information this object is responsible for. 15 | * @param extra A possibly-empty string, used to select sub-options 16 | * @return The debug information formatted as a multiline string 17 | */ 18 | @NonNull String debug(@NonNull String extra); 19 | } 20 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/debug/ItemDebugOption.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.debug; 2 | 3 | import com.tisawesomeness.minecord.mc.item.ItemRegistry; 4 | import lombok.NonNull; 5 | 6 | public class ItemDebugOption implements DebugOption { 7 | public @NonNull String getName() { 8 | return "item"; 9 | } 10 | public @NonNull String debug(@NonNull String extra) { 11 | int hits = ItemRegistry.getHits(); 12 | int misses = ItemRegistry.getMisses(); 13 | int total = hits + misses; 14 | double rate = total == 0 ? 100.0 : 100.0 * hits / total; 15 | return String.format("Item search hit rate: `%d/%d %.2f%%`", hits, total, rate); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/debug/JDADebugOption.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.debug; 2 | 3 | import lombok.NonNull; 4 | import lombok.RequiredArgsConstructor; 5 | import net.dv8tion.jda.api.JDA; 6 | import net.dv8tion.jda.api.sharding.ShardManager; 7 | 8 | import java.util.Comparator; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.concurrent.CompletableFuture; 13 | import java.util.concurrent.ExecutionException; 14 | import java.util.stream.Collectors; 15 | 16 | /** 17 | * Debugs JDA shard gateway and rest ping times. 18 | */ 19 | @RequiredArgsConstructor 20 | public class JDADebugOption implements DebugOption { 21 | 22 | private final @NonNull ShardManager shardManager; 23 | public @NonNull String getName() { 24 | return "JDA"; 25 | } 26 | 27 | public @NonNull String debug(@NonNull String extra) { 28 | List shards = shardManager.getShards(); // Not guaranteed to be sorted by shard id 29 | // Submitting all ping requests all at once 30 | // Instead of waiting for one to finish to submit the next 31 | List> shardPings = shards.stream() 32 | .map(jda -> jda.getRestPing().submit()) 33 | .collect(Collectors.toList()); 34 | 35 | Map shardStrings = new HashMap<>(); 36 | for (int i = 0; i < shardPings.size(); i++) { 37 | shardStrings.put(i, getShardLine(shardPings.get(i), shards.get(i))); 38 | } 39 | return shardStrings.entrySet().stream() 40 | .sorted(Map.Entry.comparingByKey(Comparator.reverseOrder())) // Ascending order by shard id 41 | .map(Map.Entry::getValue) 42 | .collect(Collectors.joining("\n")); 43 | } 44 | 45 | private static String getShardLine(CompletableFuture shardPing, JDA shard) { 46 | int id = shard.getShardInfo().getShardId() + 1; 47 | long gatewayPing = shard.getGatewayPing(); 48 | String str = String.format("**Shard %s:** Gateway Ping `%sms`", id, gatewayPing); 49 | try { 50 | long restPing = shardPing.get(); 51 | return str + String.format(" | Rest Ping `%sms`", restPing); 52 | } catch (InterruptedException | ExecutionException ex) { 53 | ex.printStackTrace(); 54 | } 55 | return str + " | Error getting rest ping."; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/debug/ThreadDebugOption.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.debug; 2 | 3 | import lombok.NonNull; 4 | import net.dv8tion.jda.api.utils.MarkdownUtil; 5 | 6 | import java.util.Comparator; 7 | import java.util.Set; 8 | import java.util.stream.Collectors; 9 | 10 | /** 11 | * Debugs all current threads in the JVM 12 | */ 13 | public class ThreadDebugOption implements DebugOption { 14 | public @NonNull String getName() { 15 | return "threads"; 16 | } 17 | public @NonNull String debug(@NonNull String extra) { 18 | // Although getting all stack traces is expensive 19 | // this is called from an admin-only command 20 | // so performance is not necessary 21 | Set threads = Thread.getAllStackTraces().keySet(); 22 | return threads.stream() 23 | .sorted(Comparator.comparing(Thread::getState).thenComparing(Thread::getId)) 24 | .map(ThreadDebugOption::getThreadInfo) 25 | .collect(Collectors.joining("\n")); 26 | } 27 | 28 | private static String getThreadInfo(Thread t) { 29 | String threadInfo = String.format("%s %s: ID `%s` | Priority `%s`", 30 | t.getState(), MarkdownUtil.bold(t.getName()), t.getId(), t.getPriority()); 31 | if (t.isDaemon()) { 32 | threadInfo += " | DAEMON"; 33 | } 34 | if (Thread.currentThread().equals(t)) { 35 | return MarkdownUtil.italics(threadInfo); 36 | } 37 | return threadInfo; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/debug/cache/CacheDebugOption.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.debug.cache; 2 | 3 | import com.tisawesomeness.minecord.debug.DebugOption; 4 | 5 | import com.github.benmanes.caffeine.cache.stats.CacheStats; 6 | import lombok.NonNull; 7 | 8 | import java.util.Optional; 9 | 10 | /** 11 | * Debugs a Caffeine {@link com.github.benmanes.caffeine.cache.Cache}. 12 | */ 13 | public abstract class CacheDebugOption implements DebugOption { 14 | 15 | public static final int MILLION = 1_000_000; 16 | 17 | public @NonNull String debug(@NonNull String extra) { 18 | Optional statsOpt = getCacheStats(extra); 19 | if (!statsOpt.isPresent()) { 20 | return "N/A"; 21 | } 22 | CacheStats stats = statsOpt.get(); 23 | return String.format("**%s Stats**\n", getName()) + 24 | String.format("Hits: `%s/%s %.2f%%`\n", stats.hitCount(), stats.requestCount(), 100*stats.hitRate()) + 25 | String.format("Load Failures: `%s/%s %.2f%%`\n", stats.loadFailureCount(), stats.loadCount(), 100*stats.loadFailureRate()) + 26 | String.format("Eviction Count: `%s`\n", stats.evictionCount()) + 27 | String.format("Average Load Penalty: `%.3fms`\n", stats.averageLoadPenalty() / MILLION) + 28 | String.format("Total Load Time: `%sms`", stats.totalLoadTime() / MILLION); 29 | } 30 | /** 31 | * @return The cache stats to be used in {@link #debug(String)}. 32 | */ 33 | public abstract Optional getCacheStats(@NonNull String extra); 34 | } 35 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/debug/cache/CooldownCacheDebugOption.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.debug.cache; 2 | 3 | import com.tisawesomeness.minecord.command.Registry; 4 | 5 | import com.github.benmanes.caffeine.cache.stats.CacheStats; 6 | import lombok.NonNull; 7 | import lombok.RequiredArgsConstructor; 8 | 9 | import java.util.Optional; 10 | 11 | @RequiredArgsConstructor 12 | public class CooldownCacheDebugOption extends CacheDebugOption { 13 | public @NonNull String getName() { 14 | return "cooldownCache"; 15 | } 16 | public Optional getCacheStats(@NonNull String extra) { 17 | if (extra.isEmpty()) { 18 | return Optional.of(Registry.cooldownStats()); 19 | } 20 | return Registry.cooldownStats(extra); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/debug/cache/PlayerCacheDebugOption.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.debug.cache; 2 | 3 | import com.tisawesomeness.minecord.mc.external.DualPlayerProvider; 4 | import com.tisawesomeness.minecord.mc.external.PlayerProvider; 5 | 6 | import com.github.benmanes.caffeine.cache.stats.CacheStats; 7 | import lombok.NonNull; 8 | 9 | import javax.annotation.Nullable; 10 | import java.util.Optional; 11 | 12 | public class PlayerCacheDebugOption extends CacheDebugOption { 13 | 14 | private final @Nullable DualPlayerProvider playerProvider; 15 | public PlayerCacheDebugOption(PlayerProvider playerProvider) { 16 | if (playerProvider instanceof DualPlayerProvider) { 17 | this.playerProvider = (DualPlayerProvider) playerProvider; 18 | } else { 19 | this.playerProvider = null; 20 | } 21 | } 22 | 23 | public Optional getCacheStats(@NonNull String extra) { 24 | return Optional.ofNullable(playerProvider).map(DualPlayerProvider::getPlayerCacheStats); 25 | } 26 | public @NonNull String getName() { 27 | return "playerCache"; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/debug/cache/StatusCacheDebugOption.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.debug.cache; 2 | 3 | import com.tisawesomeness.minecord.mc.external.DualPlayerProvider; 4 | import com.tisawesomeness.minecord.mc.external.PlayerProvider; 5 | 6 | import com.github.benmanes.caffeine.cache.stats.CacheStats; 7 | import lombok.NonNull; 8 | 9 | import javax.annotation.Nullable; 10 | import java.util.Optional; 11 | 12 | public class StatusCacheDebugOption extends CacheDebugOption { 13 | 14 | private final @Nullable DualPlayerProvider playerProvider; 15 | public StatusCacheDebugOption(PlayerProvider playerProvider) { 16 | if (playerProvider instanceof DualPlayerProvider) { 17 | this.playerProvider = (DualPlayerProvider) playerProvider; 18 | } else { 19 | this.playerProvider = null; 20 | } 21 | } 22 | 23 | public Optional getCacheStats(@NonNull String extra) { 24 | return Optional.ofNullable(playerProvider).map(DualPlayerProvider::getStatusCacheStats); 25 | } 26 | public @NonNull String getName() { 27 | return "statusCache"; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/debug/cache/UuidCacheDebugOption.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.debug.cache; 2 | 3 | import com.tisawesomeness.minecord.mc.external.DualPlayerProvider; 4 | import com.tisawesomeness.minecord.mc.external.PlayerProvider; 5 | 6 | import com.github.benmanes.caffeine.cache.stats.CacheStats; 7 | import lombok.NonNull; 8 | 9 | import javax.annotation.Nullable; 10 | import java.util.Optional; 11 | 12 | public class UuidCacheDebugOption extends CacheDebugOption { 13 | 14 | private final @Nullable DualPlayerProvider playerProvider; 15 | public UuidCacheDebugOption(PlayerProvider playerProvider) { 16 | if (playerProvider instanceof DualPlayerProvider) { 17 | this.playerProvider = (DualPlayerProvider) playerProvider; 18 | } else { 19 | this.playerProvider = null; 20 | } 21 | } 22 | 23 | public Optional getCacheStats(@NonNull String extra) { 24 | return Optional.ofNullable(playerProvider).map(DualPlayerProvider::getUuidCacheStats); 25 | } 26 | public @NonNull String getName() { 27 | return "uuidCache"; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/interaction/InteractionListener.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.interaction; 2 | 3 | import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; 4 | import net.dv8tion.jda.api.events.interaction.component.GenericComponentInteractionCreateEvent; 5 | import net.dv8tion.jda.api.hooks.ListenerAdapter; 6 | 7 | public class InteractionListener extends ListenerAdapter { 8 | 9 | @Override 10 | public void onGenericComponentInteractionCreate(GenericComponentInteractionCreateEvent e) { 11 | InteractionTracker.onInteract(e); 12 | } 13 | @Override 14 | public void onModalInteraction(ModalInteractionEvent e) { 15 | InteractionTracker.onSubmit(e); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/interaction/UpdatingMessage.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.interaction; 2 | 3 | import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; 4 | import net.dv8tion.jda.api.events.interaction.component.GenericComponentInteractionCreateEvent; 5 | import net.dv8tion.jda.api.utils.messages.MessageCreateData; 6 | 7 | /** 8 | * Represents a message with state that can be updated with interactions. 9 | */ 10 | public interface UpdatingMessage { 11 | 12 | /** 13 | * Run when this message is interacted with. 14 | * @param e the event 15 | * @return true if the message was modified and should be edited 16 | */ 17 | boolean onInteract(GenericComponentInteractionCreateEvent e); 18 | 19 | /** 20 | * Run when a modal attached to this message is submitted. 21 | * @param e the event 22 | * @return true if the message was modified and should be edited 23 | */ 24 | default boolean onSubmit(ModalInteractionEvent e) { 25 | return false; 26 | } 27 | 28 | /** 29 | * Renders the message into a format that can be sent. 30 | * This method will be called both for message creation and editing. 31 | * @param supportsInteractions whether the current context supports interactions 32 | * @return the message 33 | */ 34 | MessageCreateData render(boolean supportsInteractions); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/listing/TopGGClient.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.listing; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | import com.tisawesomeness.minecord.Config; 5 | import com.tisawesomeness.minecord.command.Registry; 6 | import com.tisawesomeness.minecord.network.APIClient; 7 | import com.tisawesomeness.minecord.network.NetUtil; 8 | import lombok.Cleanup; 9 | import lombok.RequiredArgsConstructor; 10 | import net.dv8tion.jda.api.interactions.commands.build.CommandData; 11 | import okhttp3.Response; 12 | import org.json.JSONArray; 13 | import org.json.JSONObject; 14 | 15 | import java.io.IOException; 16 | import java.net.URL; 17 | import java.nio.charset.StandardCharsets; 18 | import java.util.Collection; 19 | import java.util.stream.Collectors; 20 | 21 | @RequiredArgsConstructor 22 | public class TopGGClient { 23 | 24 | private final APIClient apiClient; 25 | 26 | public void sendGuilds() { 27 | if (!Config.getSendServerCount() || Config.getOrgToken() == null) { 28 | return; 29 | } 30 | 31 | try { 32 | String id = Bot.getSelfUser().getId(); 33 | URL url = new URL(String.format("https://top.gg/api/bots/%s/stats", id)); 34 | 35 | JSONObject payload = new JSONObject(); 36 | payload.put("server_count", Bot.getGuildCount()); 37 | 38 | @Cleanup Response response = apiClient.post(url, payload, "Bearer " + Config.getOrgToken()); 39 | NetUtil.throwIfError(response, "top.gg"); 40 | } catch (IOException ex) { 41 | throw new RuntimeException("Sending guilds to top.gg failed", ex); 42 | } 43 | } 44 | 45 | public void sendSlashCommands() { 46 | if (!Config.getSendSlashCommands() || Config.getOrgToken() == null) { 47 | return; 48 | } 49 | 50 | try { 51 | URL url = new URL("https://top.gg/api/v1/projects/@me/commands"); 52 | @Cleanup Response response = apiClient.post(url, commandsJson(), "Bearer " + Config.getOrgToken()); 53 | NetUtil.throwIfError(response, "top.gg"); 54 | } catch (IOException ex) { 55 | throw new RuntimeException("Sending guilds to top.gg failed", ex); 56 | } 57 | } 58 | 59 | private JSONArray commandsJson() { 60 | Collection jsonObjects = Registry.getSlashCommands().stream() 61 | .map(this::convertToJson) 62 | .collect(Collectors.toList()); 63 | return new JSONArray(jsonObjects); 64 | } 65 | private JSONObject convertToJson(CommandData cmd) { 66 | return new JSONObject(new String(cmd.toData().toJson(), StandardCharsets.UTF_8)); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/listing/VoteHandler.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.listing; 2 | 3 | import com.sun.net.httpserver.HttpExchange; 4 | import com.sun.net.httpserver.HttpHandler; 5 | import com.sun.net.httpserver.HttpServer; 6 | import com.tisawesomeness.minecord.Bot; 7 | import com.tisawesomeness.minecord.Config; 8 | import com.tisawesomeness.minecord.network.StatusCodes; 9 | import com.tisawesomeness.minecord.util.DiscordUtils; 10 | import lombok.Cleanup; 11 | import org.json.JSONObject; 12 | 13 | import java.io.IOException; 14 | import java.io.OutputStream; 15 | import java.net.InetSocketAddress; 16 | import java.util.Collections; 17 | import java.util.Scanner; 18 | 19 | public class VoteHandler implements HttpHandler { 20 | 21 | private static HttpServer server; 22 | 23 | public static void init() throws IOException { 24 | server = HttpServer.create(new InetSocketAddress(Config.getWebhookPort()), 0); 25 | server.createContext("/" + Config.getWebhookURL(), new VoteHandler()); 26 | server.start(); 27 | System.out.println("Web server started."); 28 | } 29 | 30 | public static void close() { 31 | if (server != null) { 32 | server.stop(0); 33 | } 34 | } 35 | 36 | @Override 37 | public void handle(HttpExchange t) throws IOException { 38 | if (!"POST".equals(t.getRequestMethod())) { 39 | respond(t, StatusCodes.METHOD_NOT_ALLOWED, "Method Not Allowed"); 40 | } 41 | 42 | String auth = t.getRequestHeaders().getOrDefault("Authorization", Collections.singletonList("N/A")).get(0); 43 | if (!auth.equals(Config.getWebhookAuth())) { 44 | respond(t, StatusCodes.FORBIDDEN, "Forbidden"); 45 | } 46 | 47 | try { 48 | @Cleanup Scanner scanner = new Scanner(t.getRequestBody()); 49 | String body = scanner.useDelimiter("\\A").next(); 50 | JSONObject bodyJson = new JSONObject(body); 51 | 52 | boolean upvote = "upvote".equals(bodyJson.getString("type")); 53 | Bot.shardManager.retrieveUserById(bodyJson.getString("user")).queue(user -> { 54 | 55 | String logMsg = upvote ? "upvoted!" : "downvoted ;("; 56 | logMsg = DiscordUtils.tagAndId(user) + " " + logMsg; 57 | Bot.logger.joinLog(logMsg); 58 | System.out.println(logMsg); 59 | 60 | if (upvote) { 61 | user.openPrivateChannel().queue(c -> c.sendMessage("Thanks for voting!").queue()); 62 | } 63 | 64 | }); 65 | } finally { 66 | respond(t, StatusCodes.OK, "OK"); 67 | } 68 | } 69 | 70 | private static void respond(HttpExchange t, int statusCode, String message) throws IOException { 71 | t.sendResponseHeaders(statusCode, message.length()); 72 | @Cleanup OutputStream os = t.getResponseBody(); 73 | os.write(message.getBytes()); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/FeatureFlag.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | import javax.annotation.Nullable; 7 | import java.util.Optional; 8 | 9 | @Getter 10 | @AllArgsConstructor 11 | public class FeatureFlag { 12 | 13 | private final String id; 14 | @Getter private final String displayName; 15 | private final @Nullable Version releaseVersion; 16 | 17 | FeatureFlag(Version version) { 18 | this(version.toString(), version.toString(), version); 19 | } 20 | 21 | public Optional getReleaseVersion() { 22 | return Optional.ofNullable(releaseVersion); 23 | } 24 | public boolean isReleased() { 25 | return releaseVersion != null; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/FeatureFlagRegistry.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc; 2 | 3 | import com.tisawesomeness.minecord.util.RequestUtils; 4 | import org.json.JSONArray; 5 | import org.json.JSONObject; 6 | 7 | import java.io.IOException; 8 | import java.util.*; 9 | 10 | public class FeatureFlagRegistry { 11 | 12 | private static List flags; 13 | 14 | public static void init(String path) throws IOException { 15 | parseFlags(RequestUtils.loadJSONArray(path + "/flags.json")); 16 | System.out.println("Loaded " + flags.size() + " feature flags"); 17 | } 18 | private static void parseFlags(JSONArray flagsArr) { 19 | flags = new ArrayList<>(); 20 | for (int i = 0; i < flagsArr.length(); i++) { 21 | JSONObject flagObj = flagsArr.getJSONObject(i); 22 | flags.add(parseFlag(flagObj)); 23 | } 24 | } 25 | private static FeatureFlag parseFlag(JSONObject flagObj) { 26 | String id = flagObj.getString("id"); 27 | if (id.equals("vanilla")) { 28 | return null; 29 | } 30 | Version version = Version.parse(id); 31 | if (version != null) { 32 | return new FeatureFlag(version); 33 | } else { 34 | String name = flagObj.optString("name", id); 35 | Version release = Version.parse(flagObj.optString("release")); 36 | return new FeatureFlag(id, name, release); 37 | } 38 | } 39 | 40 | public static final Comparator RELEASE_ORDER_COMPARATOR = Comparator.comparingInt(f -> flags.indexOf(f)); 41 | 42 | /** 43 | * @return all feature flags in release order, with `null` separating released and unreleased feature flags 44 | */ 45 | public static List getFlags() { 46 | return Collections.unmodifiableList(flags); 47 | } 48 | 49 | public static Optional get(String id) { 50 | return flags.stream() 51 | .filter(Objects::nonNull) 52 | .filter(f -> f.getId().equals(id)) 53 | .findFirst(); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/MCLibrary.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc; 2 | 3 | import com.tisawesomeness.minecord.mc.external.PlayerProvider; 4 | import com.tisawesomeness.minecord.network.APIClient; 5 | 6 | import lombok.NonNull; 7 | 8 | /** 9 | * A library for getting information about anything Minecraft-related. 10 | */ 11 | public interface MCLibrary { 12 | @NonNull APIClient getClient(); 13 | @NonNull PlayerProvider getPlayerProvider(); 14 | } 15 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/StandardMCLibrary.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc; 2 | 3 | import com.tisawesomeness.minecord.mc.external.DualPlayerProvider; 4 | import com.tisawesomeness.minecord.mc.external.PlayerProvider; 5 | import com.tisawesomeness.minecord.network.APIClient; 6 | 7 | import lombok.Getter; 8 | import lombok.NonNull; 9 | 10 | /** 11 | * Implements a MC Library using the standard implementations. 12 | */ 13 | public class StandardMCLibrary implements MCLibrary { 14 | 15 | @Getter private final @NonNull APIClient client; 16 | @Getter private final @NonNull PlayerProvider playerProvider; 17 | public StandardMCLibrary(@NonNull APIClient client) { 18 | this.client = client; 19 | playerProvider = new DualPlayerProvider(client); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/Version.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc; 2 | 3 | import lombok.Value; 4 | 5 | import javax.annotation.Nullable; 6 | import java.util.Comparator; 7 | 8 | @Value 9 | public class Version implements Comparable { 10 | 11 | public static final Comparator NULLS_FIRST_COMPARATOR = Comparator.nullsFirst(Comparator.naturalOrder()); 12 | 13 | int major; 14 | int minor; 15 | int patch; 16 | 17 | public Version(int major, int minor, int patch) { 18 | if (major < 0 || minor < 0 || patch < 0) { 19 | throw new IllegalArgumentException("All parts of the version must be nonnegative"); 20 | } 21 | this.major = major; 22 | this.minor = minor; 23 | this.patch = patch; 24 | } 25 | 26 | public static @Nullable Version parse(@Nullable String version) { 27 | if (version == null) { 28 | return null; 29 | } 30 | String[] parts = version.split("\\."); 31 | if (parts.length != 2 && parts.length != 3) { 32 | return null; 33 | } 34 | try { 35 | int major = Integer.parseInt(parts[0]); 36 | int minor = Integer.parseInt(parts[1]); 37 | int patch = parts.length == 3 ? Integer.parseInt(parts[2]) : 0; 38 | return new Version(major, minor, patch); 39 | } catch (IllegalArgumentException ignored) { 40 | return null; 41 | } 42 | } 43 | 44 | @Override 45 | public int compareTo(Version o) { 46 | if (major != o.major) { 47 | return Integer.compare(major, o.major); 48 | } 49 | if (minor != o.minor) { 50 | return Integer.compare(minor, o.minor); 51 | } 52 | return Integer.compare(patch, o.patch); 53 | } 54 | 55 | @Override 56 | public String toString() { 57 | if (patch == 0) { 58 | return major + "." + minor; 59 | } 60 | return major + "." + minor + "." + patch; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/VersionRegistry.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc; 2 | 3 | import com.tisawesomeness.minecord.util.RequestUtils; 4 | import org.json.JSONArray; 5 | 6 | import java.io.IOException; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Optional; 10 | 11 | public class VersionRegistry { 12 | 13 | // List of latest minor versions: 1.7.10, 1.8.9, 1.9.4... 14 | private static List latestMinorVersions; 15 | 16 | public static void init(String path) throws IOException { 17 | parseVersions(path); 18 | if (latestMinorVersions.isEmpty()) { 19 | System.out.println("No versions found in versions.json"); 20 | } 21 | System.out.println("Latest known version: " + latestMinorVersions.get(latestMinorVersions.size() - 1)); 22 | } 23 | 24 | private static void parseVersions(String path) throws IOException { 25 | latestMinorVersions = new ArrayList<>(); 26 | JSONArray versions = RequestUtils.loadJSONArray(path + "/versions.json"); 27 | for (int i = 0; i < versions.length(); i++) { 28 | Version version = Version.parse(versions.getString(i)); 29 | if (version != null) { 30 | latestMinorVersions.add(version); 31 | } 32 | } 33 | } 34 | 35 | public static Optional getLatestVersion() { 36 | if (latestMinorVersions.isEmpty()) { 37 | return Optional.empty(); 38 | } 39 | return Optional.of(latestMinorVersions.get(latestMinorVersions.size() - 1)); 40 | } 41 | 42 | /** 43 | * Computes the previous Minecraft version of the given version. 44 | * Ex: 1.8.9 -> 1.8.8, 1.8.0 -> 1.7.10 45 | * @param version the version to compute the previous version of 46 | * @return the previous version, or empty if the version is the first known (1.7.0) 47 | */ 48 | public static Optional getPreviousVersion(Version version) { 49 | if (version.getPatch() > 0) { 50 | return Optional.of(new Version(version.getMajor(), version.getMinor(), version.getPatch() - 1)); 51 | } else { 52 | for (int i = latestMinorVersions.size() - 1; i >= 0; i--) { 53 | Version v = latestMinorVersions.get(i); 54 | if (v.compareTo(version) < 0) { 55 | return Optional.of(v); 56 | } 57 | } 58 | } 59 | return Optional.empty(); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/external/ElectroidAPIImpl.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.external; 2 | 3 | import com.tisawesomeness.minecord.mc.player.Username; 4 | import com.tisawesomeness.minecord.network.APIClient; 5 | import com.tisawesomeness.minecord.network.StatusCodes; 6 | import com.tisawesomeness.minecord.util.UrlUtils; 7 | 8 | import lombok.Cleanup; 9 | import lombok.NonNull; 10 | import lombok.RequiredArgsConstructor; 11 | import okhttp3.Response; 12 | 13 | import java.io.IOException; 14 | import java.net.URL; 15 | import java.util.Objects; 16 | import java.util.Optional; 17 | import java.util.UUID; 18 | 19 | /** 20 | * Implements the Electroid API wrapper. 21 | */ 22 | @RequiredArgsConstructor 23 | public class ElectroidAPIImpl extends ElectroidAPI { 24 | 25 | private static final String BASE = "https://api.ashcon.app/mojang/v2/user/"; 26 | private final @NonNull APIClient client; 27 | 28 | protected Optional requestPlayer(@NonNull Username username) throws IOException { 29 | String encodedName = UrlUtils.encode(username.toString()); 30 | URL url = UrlUtils.createUrl(BASE + encodedName); 31 | @Cleanup Response response = client.get(url); 32 | return processResponse(response); 33 | } 34 | 35 | protected Optional requestPlayer(@NonNull UUID uuid) throws IOException { 36 | URL url = UrlUtils.createUrl(BASE + uuid); 37 | @Cleanup Response response = client.get(url); 38 | return processResponse(response); 39 | } 40 | 41 | private static Optional processResponse(@NonNull Response response) throws IOException { 42 | if (response.code() == StatusCodes.NOT_FOUND) { 43 | return Optional.empty(); 44 | } 45 | String msg = Objects.requireNonNull(response.body()).string(); 46 | if (!response.isSuccessful()) { 47 | System.err.println("Electroid API error: " + msg); 48 | throw new IOException(response.code() + " error from Electroid API: " + response.message()); 49 | } 50 | return Optional.of(msg); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/external/GappleAPI.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.external; 2 | 3 | import com.tisawesomeness.minecord.mc.player.AccountStatus; 4 | import com.tisawesomeness.minecord.util.UuidUtils; 5 | 6 | import lombok.NonNull; 7 | import org.json.JSONObject; 8 | 9 | import java.io.IOException; 10 | import java.util.Optional; 11 | import java.util.UUID; 12 | 13 | /** 14 | * A wrapper for the Gapple API. See the docs 15 | */ 16 | public abstract class GappleAPI { 17 | 18 | /** 19 | * Requests the account status of a UUID. 20 | * @param uuid a valid UUID 21 | * @return the raw JSON response, or empty if the uuid doesn't currently exist 22 | * @throws IOException if an I/O error occurs 23 | */ 24 | protected abstract Optional requestAccountStatus(@NonNull UUID uuid) throws IOException; 25 | /** 26 | * Gets the status of the account associated with the given UUID. 27 | * The API will do its best, but there are no accuracy guarantees. 28 | * @param uuid a valid UUID 29 | * @return the account status, or empty if the uuid doesn't currently exist 30 | * @throws IOException if an I/O error occurs 31 | * @throws IllegalArgumentException if the uuid is invalid 32 | */ 33 | public Optional getAccountStatus(@NonNull UUID uuid) throws IOException { 34 | if (!UuidUtils.isValid(uuid)) { 35 | throw new IllegalArgumentException(String.format("UUID %s is not a valid UUID", uuid)); 36 | } 37 | Optional responseOpt = requestAccountStatus(uuid); 38 | if (!responseOpt.isPresent()) { 39 | return Optional.empty(); 40 | } 41 | JSONObject json = new JSONObject(responseOpt.get()); 42 | String status = json.getString("status"); 43 | // NOTE: new microsoft accounts show up as "msa" instead of "new_msa" as of mar 12 2022 44 | return AccountStatus.from(status); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/external/GappleAPIImpl.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.external; 2 | 3 | import com.tisawesomeness.minecord.network.APIClient; 4 | import com.tisawesomeness.minecord.network.NetUtil; 5 | import com.tisawesomeness.minecord.network.StatusCodes; 6 | import com.tisawesomeness.minecord.util.UrlUtils; 7 | 8 | import lombok.Cleanup; 9 | import lombok.NonNull; 10 | import lombok.RequiredArgsConstructor; 11 | import okhttp3.Response; 12 | 13 | import java.io.IOException; 14 | import java.net.URL; 15 | import java.util.Objects; 16 | import java.util.Optional; 17 | import java.util.UUID; 18 | 19 | /** 20 | * Implements the Gapple API. 21 | */ 22 | @RequiredArgsConstructor 23 | public class GappleAPIImpl extends GappleAPI { 24 | 25 | private static final URL BASE_URL = UrlUtils.createUrl("https://api.gapple.pw/status/"); 26 | 27 | private final @NonNull APIClient client; 28 | 29 | protected Optional requestAccountStatus(@NonNull UUID uuid) throws IOException { 30 | URL url = new URL(BASE_URL, uuid.toString()); 31 | @Cleanup Response response = client.get(url); 32 | if (response.code() == StatusCodes.NOT_FOUND) { 33 | return Optional.empty(); 34 | } 35 | NetUtil.throwIfError(response, "Gapple API"); 36 | String content = Objects.requireNonNull(response.body()).string(); 37 | return Optional.of(content); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/external/MojangAPIImpl.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.external; 2 | 3 | import com.tisawesomeness.minecord.mc.player.Username; 4 | import com.tisawesomeness.minecord.network.APIClient; 5 | import com.tisawesomeness.minecord.network.NetUtil; 6 | import com.tisawesomeness.minecord.network.StatusCodes; 7 | import com.tisawesomeness.minecord.util.UrlUtils; 8 | import com.tisawesomeness.minecord.util.UuidUtils; 9 | import lombok.Cleanup; 10 | import lombok.NonNull; 11 | import lombok.RequiredArgsConstructor; 12 | import okhttp3.Response; 13 | 14 | import java.io.IOException; 15 | import java.net.MalformedURLException; 16 | import java.net.URL; 17 | import java.util.Objects; 18 | import java.util.Optional; 19 | import java.util.UUID; 20 | import java.util.regex.Pattern; 21 | 22 | /** 23 | * Implements the Mojang API. 24 | */ 25 | @RequiredArgsConstructor 26 | public class MojangAPIImpl extends MojangAPI { 27 | 28 | private static final Pattern EMAIL_CASE_PATTERN = Pattern.compile("^[0-9A-Za-z_\\-.*@]+$"); 29 | private static final URL BASE_URL = UrlUtils.createUrl("https://api.mojang.com/users/profiles/minecraft/"); 30 | private final APIClient client; 31 | 32 | protected Optional requestUUID(@NonNull Username username) throws IOException { 33 | // While technically possible usernames, these two cannot be queried since they mess up URLs 34 | String name = username.toString(); 35 | if (".".equals(name) || "..".equals(name)) { 36 | return Optional.empty(); 37 | } 38 | @Cleanup Response response = client.get(getUuidUrl(username)); 39 | return getContentIfPresent(response); 40 | } 41 | 42 | private static URL getUuidUrl(@NonNull Username username) { 43 | try { 44 | // Email usernames ("sample@email.com") only work when the @ is unescaped 45 | // This special case skips URL encoding if all characters (excluding @) are the same after encoding 46 | if (username.contains("@") && EMAIL_CASE_PATTERN.matcher(username).matches()) { 47 | return new URL(BASE_URL, username.toString()); 48 | } 49 | // Otherwise, encoding is necessary to clean out naughty characters 50 | return new URL(BASE_URL, UrlUtils.encode(username.toString())); 51 | } catch (MalformedURLException ex) { 52 | throw new AssertionError(ex); 53 | } 54 | } 55 | 56 | protected Optional requestProfile(@NonNull UUID uuid) throws IOException { 57 | // UUID must have hyphens stripped 58 | String link = "https://sessionserver.mojang.com/session/minecraft/profile/" + UuidUtils.toShortString(uuid); 59 | @Cleanup Response response = client.get(UrlUtils.createUrl(link)); 60 | return getContentIfPresent(response); 61 | } 62 | 63 | private static Optional getContentIfPresent(@NonNull Response response) throws IOException { 64 | if (response.code() == StatusCodes.NOT_FOUND) { 65 | return Optional.empty(); 66 | } 67 | NetUtil.throwIfError(response, "Mojang API"); 68 | if (response.code() == StatusCodes.NO_CONTENT) { 69 | return Optional.empty(); 70 | } 71 | String content = Objects.requireNonNull(response.body()).string(); 72 | return Optional.of(content); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/external/PlayerProvider.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.external; 2 | 3 | import com.tisawesomeness.minecord.mc.player.AccountStatus; 4 | import com.tisawesomeness.minecord.mc.player.Player; 5 | import com.tisawesomeness.minecord.mc.player.Username; 6 | 7 | import lombok.NonNull; 8 | 9 | import java.io.IOException; 10 | import java.util.Optional; 11 | import java.util.UUID; 12 | import java.util.concurrent.CompletableFuture; 13 | 14 | /** 15 | * Requests player data from a UUID or username. 16 | */ 17 | public interface PlayerProvider { 18 | 19 | /** 20 | * Requests the UUID currently associated with the given username. 21 | *
The future throws {@link IOException} If an I/O error occurs 22 | * @param username The input username 23 | * @return The associated UUID, or empty if the username doesn't currently exist 24 | */ 25 | CompletableFuture> getUUID(@NonNull Username username); 26 | 27 | /** 28 | * Requests the player with the given username. 29 | *
The future throws {@link IOException} If an I/O error occurs 30 | * @param username The input username 31 | * @return The player, or empty if the username doesn't currently exist 32 | */ 33 | CompletableFuture> getPlayer(@NonNull Username username); 34 | 35 | /** 36 | * Requests the player with the given UUID. 37 | *
The future throws {@link IOException} If an I/O error occurs 38 | * @param uuid The input UUID 39 | * @return The player, or empty if the UUID doesn't currently exist 40 | */ 41 | CompletableFuture> getPlayer(@NonNull UUID uuid); 42 | 43 | /** 44 | * @return true if getAccountStatus() is enabled 45 | */ 46 | boolean isStatusAPIEnabled(); 47 | 48 | /** 49 | * Requests the status of the account associated with the given UUID. 50 | *
The future throws {@link IOException} If an I/O error occurs 51 | * @param uuid a valid UUID 52 | * @return the account status, or empty if the uuid doesn't currently exist 53 | * @throws IllegalStateException if status APIs are disabled 54 | */ 55 | CompletableFuture> getAccountStatus(@NonNull UUID uuid); 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/item/Container.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.item; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | /** 7 | * A container that can hold one or more stacks of Minecraft items. 8 | */ 9 | @RequiredArgsConstructor 10 | public enum Container { 11 | STACK(1, "stacks"), 12 | CHEST(27, "chests"), 13 | DOUBLE_CHEST(2 * 27, "double chests"), 14 | CHEST_SHULKER(27 * 27, "chests full of shulkers"), 15 | DOUBLE_CHEST_SHULKER(2 * 27 * 27, "double chests full of shulkers"); 16 | 17 | @Getter private final int slots; 18 | private final String description; 19 | 20 | /** 21 | * Gets the description of this container, taking into account whether the number of containers is plural. 22 | * @param count number of containers 23 | * @return description 24 | */ 25 | public String getDescription(double count) { 26 | return count == 1.0 ? description.substring(0, description.length() - 1) : description; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/item/ItemCount.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.item; 2 | 3 | import lombok.Getter; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collection; 7 | import java.util.List; 8 | 9 | /** 10 | * A specific number of Minecraft items. Item count may be negative. 11 | */ 12 | public class ItemCount { 13 | 14 | public static final int MAX_STACK_SIZE = 99; 15 | 16 | private final long itemCount; 17 | @Getter private final int stackSize; 18 | 19 | /** 20 | * Creates a new item count. 21 | * @param itemCount number of starting items 22 | * @param stackSize stack size of the item 23 | * @throws IllegalArgumentException if the stack size is not within 1-64 24 | */ 25 | public ItemCount(long itemCount, int stackSize) { 26 | this.itemCount = itemCount; 27 | if (stackSize < 1 || MAX_STACK_SIZE < stackSize) { 28 | throw new IllegalArgumentException("stackSize must be between 1 and " + MAX_STACK_SIZE + " but was " + stackSize); 29 | } 30 | this.stackSize = stackSize; 31 | 32 | } 33 | 34 | /** 35 | * Creates a new item count with the added items and same stack size. 36 | * @param items number of items to add 37 | * @return new item count 38 | */ 39 | public ItemCount addItems(long items) { 40 | return new ItemCount(itemCount + items, stackSize); 41 | } 42 | /** 43 | * Creates a new item count with the added stacks and same stack size. 44 | * @param stacks number of stacks to add 45 | * @return new item count 46 | */ 47 | public ItemCount addStacks(long stacks) { 48 | return addItems(stackSize * stacks); 49 | } 50 | /** 51 | * Creates a new item count, adding the given number of containers, keeping the same stack size. 52 | * @param container container holding the items 53 | * @param count number of containers to add 54 | * @return new item count 55 | */ 56 | public ItemCount addContainers(Container container, long count) { 57 | return addStacks(container.getSlots() * count); 58 | } 59 | 60 | /** 61 | * @return item count 62 | */ 63 | public long getCount() { 64 | return itemCount; 65 | } 66 | 67 | /** 68 | * Gets the exact number of containers needed to hold this item count. 69 | * @param container container holding the items 70 | * @return number of containers, possibly fractional 71 | */ 72 | public double getExact(Container container) { 73 | return (double) itemCount / (stackSize * container.getSlots()); 74 | } 75 | 76 | /** 77 | * Computes a combination of containers that holds this item count. 78 | * Containers with higher capacities are prioritized. 79 | *
Example: 1863 items is equal to 1 chest, 2 stacks, 7 items. 80 | * @param containers containers allowed to be used 81 | * @return list of length {@code containers.size() + 1}, each element is the amount of containers necessary in 82 | * descending order, and one extra element at the end for leftover items (above example would return 83 | * {@code [1, 2, 7]}) 84 | */ 85 | public List distribute(Collection containers) { 86 | long stacks = itemCount / stackSize; 87 | long items = itemCount % stackSize; 88 | 89 | List list = new ArrayList<>(); 90 | for (int i = Container.values().length - 1; i >= 0; i--) { 91 | Container container = Container.values()[i]; 92 | if (containers.contains(container)) { 93 | 94 | list.add(stacks / container.getSlots()); 95 | stacks %= container.getSlots(); 96 | 97 | } 98 | } 99 | 100 | list.add(items); 101 | return list; 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/player/AccountStatus.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.player; 2 | 3 | import lombok.Getter; 4 | import lombok.NonNull; 5 | import lombok.RequiredArgsConstructor; 6 | 7 | import java.util.Arrays; 8 | import java.util.Optional; 9 | 10 | /** 11 | * Represents a Minecraft account type, whether it's Microsoft, Minecraft, or legacy and how it was migrated. 12 | */ 13 | @RequiredArgsConstructor 14 | public enum AccountStatus { 15 | NEW_MICROSOFT("new_msa", "New Microsoft Account"), 16 | MIGRATED_MICROSOFT("migrated_msa", "Migrated Microsoft Account"), 17 | MIGRATED_MICROSOFT_FROM_LEGACY("migrated_msa_from_legacy", "Microsoft from Legacy Account"), 18 | UNKNOWN_MICROSOFT("msa", "Microsoft Account"), 19 | MINECRAFT("mojang", "Minecraft Account"), 20 | LEGACY("legacy", "Legacy Account"); 21 | 22 | /** 23 | * The key that identifies the account status type according to the 24 | * Gapple API 25 | * */ 26 | @Getter private final String key; 27 | @Getter private final String name; 28 | 29 | /** 30 | * Parses the account status from the key returned from the Gapple API. 31 | * @param key string key 32 | * @return the account status, or empty if not found 33 | */ 34 | public static Optional from(@NonNull String key) { 35 | return Arrays.stream(values()) 36 | .filter(status -> status.key.equalsIgnoreCase(key)) 37 | .findFirst(); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/player/DefaultSkin.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.player; 2 | 3 | import lombok.Value; 4 | 5 | import java.net.URL; 6 | import java.util.Optional; 7 | import java.util.UUID; 8 | 9 | /** 10 | * A default skin for 1.19.3+. 11 | */ 12 | @Value 13 | public class DefaultSkin { 14 | SkinModel model; 15 | DefaultSkinType type; 16 | 17 | /** 18 | * Gets the default skin type for a given UUID, for versions 1.19.3+ 19 | * @param uuid the UUID of the player 20 | * @return the default skin 21 | */ 22 | public static DefaultSkin defaultFor(UUID uuid) { 23 | int n = Math.floorMod(uuid.hashCode(), 18); 24 | return new DefaultSkin(SkinModel.values()[n / 9], DefaultSkinType.values()[n % 9]); 25 | } 26 | /** 27 | * Gets the default skin hosted by the given URL, for versions 1.19.3+ 28 | * @param url the skin URL 29 | * @return the default skin, or empty if the URL is for a custom skin 30 | */ 31 | public static Optional fromUrl(URL url) { 32 | for (DefaultSkinType type : DefaultSkinType.values()) { 33 | if (type.getUrl(SkinModel.WIDE).sameFile(url)) { 34 | return Optional.of(new DefaultSkin(SkinModel.WIDE, type)); 35 | } 36 | if (type.getUrl(SkinModel.SLIM).sameFile(url)) { 37 | return Optional.of(new DefaultSkin(SkinModel.SLIM, type)); 38 | } 39 | } 40 | return Optional.empty(); 41 | } 42 | 43 | /** 44 | * @return the URL of the default skin 45 | */ 46 | public URL getURL() { 47 | return type.getUrl(model); 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | return String.format("%s (%s)", type, model); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/player/DefaultSkinType.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.player; 2 | 3 | import com.tisawesomeness.minecord.util.UrlUtils; 4 | 5 | import java.net.URL; 6 | 7 | /** 8 | * An enum with every possible default skin types: Steve, Alex, and the 7 new skins. 9 | */ 10 | public enum DefaultSkinType { 11 | ALEX("Alex", "1abc803022d8300ab7578b189294cce39622d9a404cdc00d3feacfdf45be6981", "46acd06e8483b176e8ea39fc12fe105eb3a2a4970f5100057e9d84d4b60bdfa7"), 12 | ARI("Ari", "4c05ab9e07b3505dc3ec11370c3bdce5570ad2fb2b562e9b9dd9cf271f81aa44", "6ac6ca262d67bcfb3dbc924ba8215a18195497c780058a5749de674217721892"), 13 | EFE("Efe", "daf3d88ccb38f11f74814e92053d92f7728ddb1a7955652a60e30cb27ae6659f", "fece7017b1bb13926d1158864b283b8b930271f80a90482f174cca6a17e88236"), 14 | KAI("Kai", "e5cdc3243b2153ab28a159861be643a4fc1e3c17d291cdd3e57a7f370ad676f3", "226c617fde5b1ba569aa08bd2cb6fd84c93337532a872b3eb7bf66bdd5b395f8"), 15 | MAKENA("Makena", "dc0fcfaf2aa040a83dc0de4e56058d1bbb2ea40157501f3e7d15dc245e493095", "7cb3ba52ddd5cc82c0b050c3f920f87da36add80165846f479079663805433db"), 16 | NOOR("Noor", "90e75cd429ba6331cd210b9bd19399527ee3bab467b5a9f61cb8a27b177f6789", "6c160fbd16adbc4bff2409e70180d911002aebcfa811eb6ec3d1040761aea6dd"), 17 | STEVE("Steve", "31f477eb1a7beee631c2ca64d06f8f68fa93a3386d04452ab27f43acdf1b60cb", "d5c4ee5ce20aed9e33e866c66caa37178606234b3721084bf01d13320fb2eb3f"), 18 | SUNNY("Sunny", "a3bd16079f764cd541e072e888fe43885e711f98658323db0f9a6045da91ee7a", "b66bc80f002b10371e2fa23de6f230dd5e2f3affc2e15786f65bc9be4c6eb71a"), 19 | ZURI("Zuri", "f5dddb41dcafef616e959c2817808e0be741c89ffbfed39134a13e75b811863d", "eee522611005acf256dbd152e992c60c0bb7978cb0f3127807700e478ad97664"); 20 | 21 | private static final String BASE_URL = "https://textures.minecraft.net/texture/"; 22 | 23 | private final String label; 24 | private final URL wideUrl; 25 | private final URL slimUrl; 26 | 27 | DefaultSkinType(String label, String wide, String slim) { 28 | this.label = label; 29 | wideUrl = UrlUtils.createUrl(BASE_URL + wide); 30 | slimUrl = UrlUtils.createUrl(BASE_URL + slim); 31 | } 32 | 33 | /** 34 | * @param model whether the skin is slim or wide 35 | * @return The URL of the skin 36 | */ 37 | public URL getUrl(SkinModel model) { 38 | return model == SkinModel.SLIM ? slimUrl : wideUrl; 39 | } 40 | 41 | @Override 42 | public String toString() { 43 | return label; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/player/NameChange.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.player; 2 | 3 | import lombok.NonNull; 4 | import lombok.Value; 5 | 6 | import javax.annotation.Nullable; 7 | import java.time.Instant; 8 | import java.util.Comparator; 9 | import java.util.Optional; 10 | 11 | /** 12 | * Represents a point in a player's name change history. Either the name change is the original name 13 | * ({@link #isOriginal()}) is {@code true} and {@link #getTime()} is empty, or the name was changed at a specific time 14 | * ({@link #isOriginal()}) is {@code false} and {@link #getTime()} is present. 15 | *

16 | * The natural order (see {@link Comparator}) for name changes is original names first, then earliest time to 17 | * latest time. 18 | *

19 | * @see #withTimestamp(Username, long) 20 | * @see #original(Username) 21 | */ 22 | @Value 23 | public class NameChange implements Comparable { 24 | private static final Comparator COMPARATOR = initComparator(); 25 | 26 | /** 27 | * The username the player was changed to. 28 | */ 29 | @NonNull Username username; 30 | @Nullable Instant time; 31 | 32 | private NameChange(@NonNull Username username, @Nullable Instant time) { 33 | this.username = username; 34 | this.time = time; 35 | } 36 | 37 | /** 38 | * Creates a name change with the given username and timestamp. 39 | * @param username The username the player was changed to 40 | * @param timestamp The timestamp the username was changed 41 | */ 42 | public static @NonNull NameChange withTimestamp(@NonNull Username username, long timestamp) { 43 | return new NameChange(username, Instant.ofEpochMilli(timestamp)); 44 | } 45 | /** 46 | * Creates a name change with the given username and time. 47 | * @param username The username the player was changed to 48 | * @param time The time the username was changed 49 | */ 50 | public static @NonNull NameChange withTime(@NonNull Username username, @NonNull Instant time) { 51 | return new NameChange(username, time); 52 | } 53 | /** 54 | * Creates a name change with the original username. 55 | * @param username The username the player had when the account was created 56 | */ 57 | public static @NonNull NameChange original(@NonNull Username username) { 58 | return new NameChange(username, null); 59 | } 60 | 61 | /** 62 | * @return The time the username was changed, or empty if it was the original username 63 | */ 64 | public Optional getTime() { 65 | return Optional.ofNullable(time); 66 | } 67 | /** 68 | * @return True if the username in this name change was the original username 69 | */ 70 | public boolean isOriginal() { 71 | return time == null; 72 | } 73 | 74 | /** 75 | * Compares this name change to another, see the class documentation for the ordering. 76 | * @param other The other name change 77 | * @return -1, 0, or 1 if this name change is less than, equal to, 78 | * or greater than the other name change respectively 79 | */ 80 | public int compareTo(@NonNull NameChange other) { 81 | return COMPARATOR.compare(this, other); 82 | } 83 | 84 | @Override 85 | public @NonNull String toString() { 86 | if (time == null) { 87 | return String.format("NameChange(%s original)", username); 88 | } 89 | return String.format("NameChange(%s at %s)", username, time); 90 | } 91 | 92 | private static Comparator initComparator() { 93 | Comparator nullsFirst = Comparator.nullsFirst(Comparator.naturalOrder()); 94 | Comparator timeComparator = Comparator.comparing(nc -> nc.time, nullsFirst); 95 | return timeComparator.thenComparing(NameChange::getUsername); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/player/Profile.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.player; 2 | 3 | import com.tisawesomeness.minecord.util.UrlUtils; 4 | import lombok.AllArgsConstructor; 5 | import lombok.NonNull; 6 | import lombok.Value; 7 | 8 | import javax.annotation.Nullable; 9 | import java.net.URL; 10 | import java.util.Collections; 11 | import java.util.Optional; 12 | import java.util.Set; 13 | 14 | /** 15 | * Provides additional account information about a player. 16 | */ 17 | @Value 18 | @AllArgsConstructor 19 | public class Profile { 20 | 21 | private static final String MOJANG_STUDIOS_CAPE_URL = "https://minecraft.wiki/images/Mojang_Studios_Cape_%28Texture%29.png?7450c"; 22 | private static final URL FIXED_MOJANG_CAPE_URL = UrlUtils.createUrl("https://static.wikia.nocookie.net/minecraft_gamepedia/images/5/59/Mojang_Cape_(texture).png"); 23 | 24 | /** 25 | * The player's current username 26 | */ 27 | @NonNull Username username; 28 | /** 29 | * Whether the player has not migrated to a Minecraft account (using email to log in) 30 | */ 31 | boolean legacy; 32 | /** 33 | * Whether the player is a demo account 34 | */ 35 | boolean demo; 36 | /** 37 | * The skin model of the skin URL (only valid if the skin url exists) 38 | */ 39 | SkinModel skinModel; 40 | @Nullable URL skinUrl; 41 | @Nullable URL capeUrl; 42 | Set profileActions; 43 | 44 | public Profile(@NonNull Username username, boolean legacy, boolean demo, SkinModel skinModel, @Nullable URL skinUrl, 45 | @Nullable URL capeUrl) { 46 | this(username, legacy, demo, skinModel, skinUrl, capeUrl, Collections.emptySet()); 47 | } 48 | 49 | /** 50 | * @return The skin URL, or empty if the player has no custom skin 51 | */ 52 | public Optional getSkinUrl() { 53 | return Optional.ofNullable(skinUrl); 54 | } 55 | /** 56 | * @return The skin URL, or empty if the player has no custom cape 57 | */ 58 | public Optional getCapeUrl() { 59 | if (capeUrl != null && capeUrl.toString().equals(MOJANG_STUDIOS_CAPE_URL)) { 60 | return Optional.of(FIXED_MOJANG_CAPE_URL); 61 | } 62 | return Optional.ofNullable(capeUrl); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/player/ProfileAction.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.player; 2 | 3 | import lombok.NonNull; 4 | import org.json.JSONArray; 5 | 6 | import java.util.*; 7 | 8 | /** A moderation action taken against a profile */ 9 | public enum ProfileAction { 10 | FORCED_NAME_CHANGE, 11 | USING_BANNED_SKIN; 12 | 13 | public static Optional from(@NonNull String str) { 14 | for (ProfileAction action : values()) { 15 | if (action.toString().equals(str)) { 16 | return Optional.of(action); 17 | } 18 | } 19 | return Optional.empty(); 20 | } 21 | 22 | public static Set parseProfileActions(JSONArray arr) { 23 | if (arr == null) { 24 | return Collections.emptySet(); 25 | } 26 | Set profileActions = EnumSet.noneOf(ProfileAction.class); 27 | for (int i = 0; i < arr.length(); i++) { 28 | String actionStr = arr.getString(i); 29 | ProfileAction.from(actionStr.toUpperCase(Locale.ROOT)).ifPresent(profileActions::add); 30 | } 31 | return profileActions; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/player/Render.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.player; 2 | 3 | import com.tisawesomeness.minecord.Config; 4 | import com.tisawesomeness.minecord.util.UrlUtils; 5 | 6 | import lombok.NonNull; 7 | import lombok.Value; 8 | 9 | import java.net.URL; 10 | import java.util.UUID; 11 | 12 | /** 13 | * Represents a Crafatar player render. 14 | */ 15 | @Value 16 | public class Render { 17 | @NonNull UUID player; 18 | RenderType type; 19 | boolean overlay; 20 | int scale; 21 | int providedScale; 22 | 23 | /** 24 | * Creates a render. 25 | * @param player The UUID of the player to render 26 | * @param type The type of render 27 | * @param overlay Whether to show the second skin layer, or overlay 28 | */ 29 | public Render(@NonNull UUID player, RenderType type, boolean overlay) { 30 | this(player, type, overlay, type.getDefaultScale()); 31 | } 32 | /** 33 | * Creates a render. 34 | * @param player The UUID of the player to render 35 | * @param type The type of render 36 | * @param overlay Whether to show the second skin layer, or overlay 37 | * @param scale The scale of the render, capped at {@link RenderType#getMaxScale()} 38 | * @throws IllegalArgumentException If the scale is zero or negative 39 | */ 40 | public Render(@NonNull UUID player, RenderType type, boolean overlay, int scale) { 41 | if (scale < 1) { 42 | throw new IllegalArgumentException("The render scale must be positive but was " + scale); 43 | } 44 | this.player = player; 45 | this.type = type; 46 | this.overlay = overlay; 47 | this.scale = Math.min(scale, type.getMaxScale()); 48 | providedScale = scale; 49 | } 50 | 51 | /** 52 | * Generates a URL linking to the render image. 53 | * @return The render's URL 54 | */ 55 | public @NonNull URL render() { 56 | String query = type.isRender() ? "scale" : "size"; 57 | String overlayStr = overlay ? "&overlay" : ""; 58 | return UrlUtils.createUrl(String.format("%s%s/%s?%s=%d%s", 59 | Config.getCrafatarHost(), type.getBasePath(), player, query, scale, overlayStr)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/player/RenderType.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.player; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | /** 7 | * An enum of renders supported by {@link Render}. 8 | * See https://crafatar.com/ 9 | */ 10 | @RequiredArgsConstructor 11 | public enum RenderType { 12 | AVATAR("avatar", "Avatar", "avatars", false), 13 | HEAD("head", "Head", "renders/head", true), 14 | BODY("body", "Body", "renders/body", true); 15 | 16 | public static final int MAX_SIZE = 512; 17 | public static final int DEFAULT_SIZE = 160; 18 | public static final int MAX_SCALE = 10; 19 | public static final int DEFAULT_SCALE = 6; 20 | 21 | /** 22 | * The name of the render type 23 | */ 24 | @Getter private final String id; 25 | private final String name; 26 | /** 27 | * The path to the API endpoint 28 | */ 29 | @Getter private final String basePath; 30 | /** 31 | * Whether Crafatar recognizes this type as a render, and scale should be used instead of size 32 | */ 33 | @Getter private final boolean isRender; 34 | 35 | /** 36 | * @return The maximum scale of this render type 37 | */ 38 | public int getMaxScale() { 39 | return isRender ? MAX_SCALE : MAX_SIZE; 40 | } 41 | /** 42 | * @return The default scale of this render type 43 | */ 44 | public int getDefaultScale() { 45 | return isRender ? DEFAULT_SCALE : DEFAULT_SIZE; 46 | } 47 | 48 | @Override 49 | public String toString() { 50 | return name; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/player/SkinModel.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.player; 2 | 3 | import lombok.Getter; 4 | import lombok.NonNull; 5 | import lombok.RequiredArgsConstructor; 6 | 7 | import java.util.UUID; 8 | 9 | /** 10 | * An enum with every possible skin model type. 11 | */ 12 | @RequiredArgsConstructor 13 | public enum SkinModel { 14 | /** 15 | * The skin model with slim arms 16 | */ 17 | SLIM("Alex (slim arms)", "slim"), 18 | /** 19 | * The skin model with square arms 20 | */ 21 | WIDE("Steve (square arms)", "wide"); 22 | 23 | @Getter private final @NonNull String description; 24 | private final @NonNull String label; 25 | 26 | /** 27 | * @return The default skin model according to the UUID 28 | */ 29 | public static SkinModel defaultFor(UUID uuid) { 30 | return uuid.hashCode() % 2 == 0 ? WIDE : SLIM; 31 | } 32 | 33 | @Override 34 | public String toString() { 35 | return label; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/pos/BlockPos.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.pos; 2 | 3 | import com.tisawesomeness.minecord.util.MathUtils; 4 | 5 | /** 6 | * A block position within a Minecraft world. 7 | */ 8 | public class BlockPos extends Vec3i { 9 | 10 | /** Max world border distance from origin, 1 chunk size from 30 mil limit */ 11 | public static final int MAX_BORDER_DISTANCE = 29_999_984; 12 | /** Max distance from origin nether portals can generate, 128 blocks from max border */ 13 | public static final int MAX_PORTAL_DISTANCE = 29_999_872; 14 | 15 | public BlockPos(int x, int y, int z) { 16 | super(x, y, z); 17 | } 18 | public BlockPos(Vec3i vec) { 19 | super(vec.getX(), vec.getY(), vec.getZ()); 20 | } 21 | 22 | /** 23 | * Converts overworld to nether coordinates. Y values are clamped to nether height. 24 | * @return nether block pos 25 | */ 26 | public BlockPos overworldToNether() { 27 | int netherY = MathUtils.clamp(y, 0, 127); 28 | return new BlockPos(horizontal().floorDiv(8).withY(netherY)); 29 | } 30 | /** 31 | * Converts nether to overworld coordinates, clamped to world border/height. 32 | * @return overworld block pos 33 | */ 34 | public BlockPos netherToOverworld() { 35 | int overworldX = MathUtils.clamp(x * 8, -MAX_PORTAL_DISTANCE, MAX_PORTAL_DISTANCE); 36 | int overworldY = MathUtils.clamp(y, -64, 319); 37 | int overworldZ = MathUtils.clamp(z * 8, -MAX_PORTAL_DISTANCE, MAX_PORTAL_DISTANCE); 38 | return new BlockPos(overworldX, overworldY, overworldZ); 39 | } 40 | 41 | public SectionPos getSection() { 42 | return new SectionPos(floorDiv(16)); 43 | } 44 | public Vec3i getPosWithinSection() { 45 | return floorMod(16); 46 | } 47 | 48 | /** 49 | * @return true if the X/Z (not Y!) coordinates are within the world border 50 | * @see #MAX_BORDER_DISTANCE 51 | */ 52 | public boolean isInBounds() { 53 | return -MAX_BORDER_DISTANCE <= x && x <= MAX_BORDER_DISTANCE && 54 | -MAX_BORDER_DISTANCE <= z && z <= MAX_BORDER_DISTANCE; 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/pos/RegionPos.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.pos; 2 | 3 | public class RegionPos extends Vec2i { 4 | 5 | public RegionPos(Vec2i vec) { 6 | super(vec.getX(), vec.getZ()); 7 | } 8 | 9 | public SectionPos getSectionPos() { 10 | return new SectionPos(scale(32).withY(0)); 11 | } 12 | public String getFileName() { 13 | return String.format("r.%d.%d.mca", x, z); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/pos/SectionPos.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.pos; 2 | 3 | /** 4 | * A 16x16x16 section of a Minecraft world. {@link #horizontal()} gives the chunk coordinates. 5 | */ 6 | public class SectionPos extends Vec3i { 7 | 8 | public SectionPos(int x, int y, int z) { 9 | super(x, y, z); 10 | } 11 | public SectionPos(Vec3i vec) { 12 | super(vec.getX(), vec.getY(), vec.getZ()); 13 | } 14 | 15 | public BlockPos getBlockPos() { 16 | return new BlockPos(scale(16)); 17 | } 18 | public RegionPos getRegionPos() { 19 | return new RegionPos(horizontal().floorDiv(32)); 20 | } 21 | public Vec2i getPosWithinRegion() { 22 | return horizontal().floorMod(32); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/pos/Vec2.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.pos; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | @AllArgsConstructor 9 | @EqualsAndHashCode 10 | public class Vec2 extends Vec { 11 | 12 | private final double x; 13 | private final double z; 14 | 15 | public Vec2i round() { 16 | return new Vec2i((int) Math.round(x), (int) Math.round(z)); 17 | } 18 | 19 | @Override 20 | public String toString() { 21 | return x + ", " + z; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/pos/Vec2i.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.pos; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | @AllArgsConstructor 9 | @EqualsAndHashCode 10 | public class Vec2i { 11 | 12 | protected final int x; 13 | protected final int z; 14 | 15 | public Vec2i scale(int n) { 16 | return new Vec2i(x * n, z * n); 17 | } 18 | public Vec2i floorDiv(int n) { 19 | return new Vec2i(Math.floorDiv(x, n), Math.floorDiv(z, n)); 20 | } 21 | public Vec2i floorMod(int n) { 22 | return new Vec2i(Math.floorMod(x, n), Math.floorMod(z, n)); 23 | } 24 | 25 | public Vec3i withY(int y) { 26 | return new Vec3i(x, y, z); 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return x + ", " + z; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/pos/Vec3.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.pos; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | @AllArgsConstructor 9 | @EqualsAndHashCode 10 | public class Vec3 extends Vec { 11 | 12 | private final double x; 13 | private final double y; 14 | private final double z; 15 | 16 | public Vec3i round() { 17 | return new Vec3i((int) Math.round(x), (int) Math.round(y), (int) Math.round(z)); 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return x + ", " + y + ", " + z; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/pos/Vec3i.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.pos; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | @AllArgsConstructor 9 | @EqualsAndHashCode 10 | public class Vec3i { 11 | 12 | protected final int x; 13 | protected final int y; 14 | protected final int z; 15 | 16 | public Vec3i scale(int n) { 17 | return new Vec3i(x * n, y * n, z * n); 18 | } 19 | public Vec3i floorDiv(int n) { 20 | return new Vec3i(Math.floorDiv(x, n), Math.floorDiv(y, n), Math.floorDiv(z, n)); 21 | } 22 | public Vec3i floorMod(int n) { 23 | return new Vec3i(Math.floorMod(x, n), Math.floorMod(y, n), Math.floorMod(z, n)); 24 | } 25 | 26 | public Vec2i horizontal() { 27 | return new Vec2i(x, z); 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | return x + ", " + y + ", " + z; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/recipe/BrewingRecipe.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.recipe; 2 | 3 | import org.apache.commons.collections4.ListUtils; 4 | import org.json.JSONObject; 5 | 6 | import java.util.List; 7 | 8 | public class BrewingRecipe extends Recipe { 9 | 10 | protected BrewingRecipe(String key, JSONObject recipe) { 11 | super(key, recipe); 12 | } 13 | 14 | @Override 15 | public List getIngredients() { 16 | return ListUtils.union(getReagent(), getBase()); 17 | } 18 | public List getReagent() { 19 | return parseIngredients(recipe.get("reagent")); 20 | } 21 | public List getBase() { 22 | return parseIngredients(recipe.get("base")); 23 | } 24 | 25 | @Override 26 | public String getTableItem() { 27 | return "minecraft:brewing_stand"; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/recipe/CraftResult.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.recipe; 2 | 3 | import lombok.Value; 4 | 5 | @Value 6 | public class CraftResult { 7 | String item; 8 | int count; 9 | } 10 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/recipe/CraftingRecipe.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.recipe; 2 | 3 | import org.json.JSONObject; 4 | 5 | public abstract class CraftingRecipe extends Recipe { 6 | 7 | protected CraftingRecipe(String key, JSONObject recipe) { 8 | super(key, recipe); 9 | } 10 | 11 | @Override 12 | public String getTableItem() { 13 | return "minecraft:crafting_table"; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/recipe/Ingredient.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.recipe; 2 | 3 | import lombok.Value; 4 | 5 | public interface Ingredient { 6 | @Value 7 | class Item implements Ingredient { 8 | String item; 9 | } 10 | @Value 11 | class Tag implements Ingredient { 12 | String tag; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/recipe/LegacySmithingRecipe.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.recipe; 2 | 3 | import org.apache.commons.collections4.ListUtils; 4 | import org.json.JSONObject; 5 | 6 | import java.util.List; 7 | 8 | public class LegacySmithingRecipe extends Recipe { 9 | 10 | protected LegacySmithingRecipe(String key, JSONObject recipe) { 11 | super(key, recipe); 12 | } 13 | 14 | @Override 15 | public List getIngredients() { 16 | return ListUtils.union(getBase(), getAddition()); 17 | } 18 | public List getBase() { 19 | return parseIngredients(recipe.get("base")); 20 | } 21 | public List getAddition() { 22 | return parseIngredients(recipe.get("addition")); 23 | } 24 | 25 | @Override 26 | public String getTableItem() { 27 | return "minecraft:smithing_table"; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/recipe/ShapedRecipe.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.recipe; 2 | 3 | import com.tisawesomeness.minecord.util.Utils; 4 | import org.apache.commons.collections4.OrderedMap; 5 | import org.apache.commons.collections4.map.LinkedMap; 6 | import org.json.JSONArray; 7 | import org.json.JSONObject; 8 | 9 | import java.util.List; 10 | 11 | public class ShapedRecipe extends CraftingRecipe { 12 | 13 | protected ShapedRecipe(String key, JSONObject recipe) { 14 | super(key, recipe); 15 | } 16 | 17 | @Override 18 | public List getIngredients() { 19 | return Utils.flatten(getIngredientKey().values()); 20 | } 21 | 22 | public String[] getPattern() { 23 | String[] pattern = new String[3]; 24 | JSONArray givenPattern = recipe.getJSONArray("pattern"); 25 | for (int i = 0; i < 3; i++) { 26 | StringBuilder row = new StringBuilder(givenPattern.optString(i, " ")); 27 | while (row.length() < 3) { 28 | row.append(" "); 29 | } 30 | pattern[i] = row.toString(); 31 | } 32 | return pattern; 33 | } 34 | 35 | public OrderedMap> getIngredientKey() { 36 | OrderedMap> map = new LinkedMap<>(); 37 | JSONObject keyObj = recipe.getJSONObject("key"); 38 | keyObj.keys().forEachRemaining(k -> map.put(k.charAt(0), parseIngredients(keyObj.get(k)))); 39 | return map; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/recipe/ShapelessRecipe.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.recipe; 2 | 3 | import com.tisawesomeness.minecord.util.Utils; 4 | import org.json.JSONArray; 5 | import org.json.JSONObject; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class ShapelessRecipe extends CraftingRecipe { 11 | 12 | protected ShapelessRecipe(String key, JSONObject recipe) { 13 | super(key, recipe); 14 | } 15 | 16 | @Override 17 | public List getIngredients() { 18 | return Utils.flatten(getIngredientsPerSlot()); 19 | } 20 | public List> getIngredientsPerSlot() { 21 | List> slots = new ArrayList<>(); 22 | JSONArray ingredients = recipe.getJSONArray("ingredients"); 23 | for (int i = 0; i < ingredients.length(); i++) { 24 | slots.add(parseIngredients(ingredients.get(i))); 25 | } 26 | return slots; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/recipe/SmeltingRecipe.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.recipe; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.json.JSONObject; 5 | 6 | import java.util.List; 7 | 8 | public class SmeltingRecipe extends Recipe { 9 | 10 | protected SmeltingRecipe(String key, JSONObject recipe) { 11 | super(key, recipe); 12 | } 13 | 14 | @Override 15 | public List getIngredients() { 16 | return parseIngredients(recipe.get("ingredient")); 17 | } 18 | 19 | @Override 20 | public String getTableItem() { 21 | return "minecraft:furnace"; 22 | } 23 | 24 | public Type getType() { 25 | return Type.of(recipe.getString("type").substring("minecraft:".length())); 26 | } 27 | 28 | @RequiredArgsConstructor 29 | public enum Type { 30 | SMELTING("smelting"), 31 | BLASTING("blasting"), 32 | SMOKING("smoking"), 33 | CAMPFIRE_COOKING("campfire_cooking"); 34 | 35 | private final String id; 36 | 37 | public static Type of(String id) { 38 | for (Type type : values()) { 39 | if (type.id.equals(id)) { 40 | return type; 41 | } 42 | } 43 | throw new IllegalArgumentException("invalid id " + id); 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/recipe/SmithingRecipe.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.recipe; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.apache.commons.collections4.ListUtils; 5 | import org.json.JSONObject; 6 | 7 | import java.util.List; 8 | 9 | public class SmithingRecipe extends Recipe { 10 | 11 | protected SmithingRecipe(String key, JSONObject recipe) { 12 | super(key, recipe); 13 | } 14 | 15 | @Override 16 | public List getIngredients() { 17 | return ListUtils.union(getBase(), ListUtils.union(getTemplate(), getAddition())); 18 | } 19 | public List getTemplate() { 20 | return parseIngredients(recipe.get("template")); 21 | } 22 | public List getBase() { 23 | return parseIngredients(recipe.get("base")); 24 | } 25 | public List getAddition() { 26 | return parseIngredients(recipe.get("addition")); 27 | } 28 | 29 | @Override 30 | public String getTableItem() { 31 | return "minecraft:smithing_table"; 32 | } 33 | 34 | public Type getType() { 35 | return Type.of(recipe.getString("type").substring("minecraft:".length())); 36 | } 37 | 38 | @RequiredArgsConstructor 39 | public enum Type { 40 | TRANSFORM("smithing_transform"), 41 | TRIM("smithing_trim"); 42 | 43 | private final String id; 44 | 45 | public static Type of(String id) { 46 | for (Type type : values()) { 47 | if (type.id.equals(id)) { 48 | return type; 49 | } 50 | } 51 | throw new IllegalArgumentException("invalid id " + id); 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/recipe/StonecuttingRecipe.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.recipe; 2 | 3 | import org.json.JSONObject; 4 | 5 | import java.util.List; 6 | 7 | public class StonecuttingRecipe extends Recipe { 8 | 9 | protected StonecuttingRecipe(String key, JSONObject recipe) { 10 | super(key, recipe); 11 | } 12 | 13 | @Override 14 | public List getIngredients() { 15 | return parseIngredients(recipe.get("ingredient")); 16 | } 17 | 18 | @Override 19 | public String getTableItem() { 20 | return "minecraft:stonecutter"; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/mc/recipe/TransmuteRecipe.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.mc.recipe; 2 | 3 | import com.tisawesomeness.minecord.util.Utils; 4 | import org.apache.commons.collections4.ListUtils; 5 | import org.json.JSONObject; 6 | 7 | import java.util.List; 8 | 9 | public class TransmuteRecipe extends CraftingRecipe { 10 | 11 | protected TransmuteRecipe(String key, JSONObject recipe) { 12 | super(key, recipe); 13 | } 14 | 15 | @Override 16 | public List getIngredients() { 17 | return ListUtils.union(getMaterial(), getInput()); 18 | } 19 | public List getInput() { 20 | return parseIngredients(recipe.get("input")); 21 | } 22 | public List getMaterial() { 23 | return parseIngredients(recipe.get("material")); 24 | } 25 | 26 | /** 27 | * Whether it is okay for this recipe's ingredients to include the result. 28 | * If false, the result of this recipe should be manually removed from the list of ingredients. 29 | * Note that the result item can be hidden in a tag. 30 | * @return true or false 31 | */ 32 | public boolean shouldIngredientsIncludeResult() { 33 | Boolean includeResult = Utils.mapNullable(recipe.optJSONObject("properties"), 34 | prop -> prop.optBoolean("include_result", false)); 35 | return Boolean.TRUE.equals(includeResult); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/network/APIClient.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.network; 2 | 3 | import lombok.NonNull; 4 | import okhttp3.OkHttpClient; 5 | import okhttp3.Response; 6 | import org.json.JSONArray; 7 | import org.json.JSONObject; 8 | 9 | import java.io.IOException; 10 | import java.net.URL; 11 | 12 | public interface APIClient { 13 | 14 | OkHttpClient.Builder getHttpClientBuilder(); 15 | 16 | /** 17 | * Performs a HEAD request. 18 | * 19 | * @param url The URL to send a HEAD request to 20 | * @return The response of the request, which may be successful or unsuccessful 21 | * @throws IOException If an I/O error occurs 22 | */ 23 | @NonNull Response head(@NonNull URL url) throws IOException; 24 | 25 | /** 26 | * Performs a GET request. 27 | * 28 | * @param url The URL to send a GET request to 29 | * @return The response of the request, which may be successful or unsuccessful 30 | * @throws IOException If an I/O error occurs 31 | */ 32 | @NonNull Response get(@NonNull URL url) throws IOException; 33 | 34 | /** 35 | * Performs a POST request. 36 | * 37 | * @param url The URL to send a POST request to 38 | * @param payload The payload of the request 39 | * @param auth The authorization header 40 | * @return The response of the request, which may be successful or unsuccessful 41 | * @throws IOException If an I/O error occurs 42 | */ 43 | @NonNull Response post(@NonNull URL url, @NonNull JSONObject payload, @NonNull String auth) throws IOException; 44 | 45 | /** 46 | * Performs a POST request. 47 | * 48 | * @param url The URL to send a POST request to 49 | * @param payload The payload of the request 50 | * @param auth The authorization header 51 | * @return The response of the request, which may be successful or unsuccessful 52 | * @throws IOException If an I/O error occurs 53 | */ 54 | @NonNull Response post(@NonNull URL url, @NonNull JSONArray payload, @NonNull String auth) throws IOException; 55 | 56 | /** 57 | * Checks if a URL exists and is responsive. 58 | * 59 | * @param url The URL to request 60 | * @return Whether the URL is responsive 61 | * @throws IOException If an I/O error occurs 62 | */ 63 | boolean exists(@NonNull URL url) throws IOException; 64 | 65 | int getQueuedCallsCount(); 66 | int getRunningCallsCount(); 67 | int getIdleConnectionCount(); 68 | int getConnectionCount(); 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/network/NetUtil.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.network; 2 | 3 | import lombok.NonNull; 4 | import okhttp3.Response; 5 | import okhttp3.ResponseBody; 6 | 7 | import java.io.IOException; 8 | import java.net.Inet4Address; 9 | import java.net.InetAddress; 10 | import java.net.UnknownHostException; 11 | import java.util.Optional; 12 | 13 | public final class NetUtil { 14 | private NetUtil() {} 15 | 16 | private static final int LONGEST_DEBUGGABLE_ERROR = 256; 17 | 18 | /** 19 | * Attempts to parse an IPv4 address from a string. 20 | * @param ip The string to parse, in the form "1.2.3.4" 21 | * @return The parsed address, or empty if invalid 22 | */ 23 | public static Optional getAddress(String ip) { 24 | String[] parts = ip.split("\\."); 25 | if (parts.length != 4) { 26 | return Optional.empty(); 27 | } 28 | try { 29 | // get using the byte array since it doesn't resolve 30 | return Optional.of((Inet4Address) InetAddress.getByAddress(new byte[]{ 31 | (byte) Integer.parseInt(parts[0]), 32 | (byte) Integer.parseInt(parts[1]), 33 | (byte) Integer.parseInt(parts[2]), 34 | (byte) Integer.parseInt(parts[3]) 35 | })); 36 | } catch (NumberFormatException ex) { 37 | return Optional.empty(); 38 | } catch (UnknownHostException ex) { 39 | throw new AssertionError("impossible, array is always length 4"); 40 | } 41 | } 42 | 43 | /** 44 | * Throws a formatted IOE if the response is not successful. 45 | * The message contains the response code and the body error message if it exists. 46 | * @param response the response 47 | * @param apiName the API name to use in the error message 48 | * @throws IOException if the response is not successful 49 | */ 50 | public static void throwIfError(@NonNull Response response, @NonNull String apiName) throws IOException { 51 | if (!response.isSuccessful()) { 52 | ResponseBody body = response.body(); 53 | String error = String.format("%s error from %s: %s", response.code(), apiName, response.message()); 54 | if (body == null) { 55 | throw new IOException(error); 56 | } 57 | if (body.contentLength() > LONGEST_DEBUGGABLE_ERROR) { 58 | throw new IOException(error + " | " + body.string().substring(0, LONGEST_DEBUGGABLE_ERROR) + "..."); 59 | } 60 | throw new IOException(error + " | " + body.string()); 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/network/OkAPIClient.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.network; 2 | 3 | import lombok.Getter; 4 | import lombok.NonNull; 5 | import okhttp3.*; 6 | import org.json.JSONArray; 7 | import org.json.JSONObject; 8 | 9 | import java.io.IOException; 10 | import java.net.URL; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | /** 14 | * A simplified HTTP client that can be used to communicate with web JSON APIs. 15 | */ 16 | public class OkAPIClient implements APIClient { 17 | 18 | /** 19 | * The builder used to construct a {@link OkHttpClient} instance 20 | */ 21 | @Getter private final OkHttpClient.Builder httpClientBuilder; 22 | 23 | private final Dispatcher dispatcher; 24 | private final ConnectionPool connectionPool; 25 | 26 | /** 27 | * Creates a new API client with the default settings. 28 | */ 29 | public OkAPIClient() { 30 | // client is set to JDA defaults 31 | dispatcher = new Dispatcher(); 32 | dispatcher.setMaxRequestsPerHost(25); 33 | connectionPool = new ConnectionPool(5, 10000, TimeUnit.MILLISECONDS); 34 | httpClientBuilder = new OkHttpClient.Builder() 35 | .connectionPool(connectionPool) 36 | .dispatcher(dispatcher); 37 | } 38 | 39 | @Override 40 | public @NonNull Response head(@NonNull URL url) throws IOException { 41 | Request request = new Request.Builder() 42 | .url(url) 43 | .head() 44 | .build(); 45 | return dispatch(request); 46 | } 47 | 48 | @Override 49 | public @NonNull Response get(@NonNull URL url) throws IOException { 50 | Request request = new Request.Builder() 51 | .url(url) 52 | .header("Content-Type", "application/json") 53 | .build(); 54 | return dispatch(request); 55 | } 56 | 57 | @Override 58 | public @NonNull Response post(@NonNull URL url, @NonNull JSONObject payload, @NonNull String auth) throws IOException { 59 | return post(url, payload.toString(), auth); 60 | } 61 | 62 | @Override 63 | public @NonNull Response post(@NonNull URL url, @NonNull JSONArray payload, @NonNull String auth) throws IOException { 64 | return post(url, payload.toString(), auth); 65 | } 66 | 67 | private @NonNull Response post(@NonNull URL url, @NonNull String json, @NonNull String auth) throws IOException { 68 | Request request = new Request.Builder() 69 | .url(url) 70 | .post(RequestBody.create(json, MediaType.get("application/json"))) 71 | .header("Authorization", auth) 72 | .build(); 73 | return dispatch(request); 74 | } 75 | 76 | private @NonNull Response dispatch(@NonNull Request request) throws IOException { 77 | OkHttpClient client = httpClientBuilder.build(); 78 | return client.newCall(request).execute(); 79 | } 80 | 81 | @Override 82 | public boolean exists(@NonNull URL url) throws IOException { 83 | return head(url).code() == StatusCodes.OK; 84 | } 85 | 86 | @Override 87 | public int getQueuedCallsCount() { 88 | return dispatcher.queuedCallsCount(); 89 | } 90 | @Override 91 | public int getRunningCallsCount() { 92 | return dispatcher.runningCallsCount(); 93 | } 94 | @Override 95 | public int getIdleConnectionCount() { 96 | return connectionPool.idleConnectionCount(); 97 | } 98 | @Override 99 | public int getConnectionCount() { 100 | return connectionPool.connectionCount(); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/network/StatusCodes.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.network; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | 5 | /** 6 | * A constants class for standard HTTP status codes. This is not comprehensive, status codes will only be added as needed. 7 | */ 8 | @RequiredArgsConstructor 9 | public final class StatusCodes { 10 | public static final int OK = 200; 11 | public static final int NO_CONTENT = 204; 12 | public static final int FORBIDDEN = 203; 13 | public static final int NOT_FOUND = 404; 14 | public static final int METHOD_NOT_ALLOWED = 405; 15 | } 16 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/util/ArrayUtils.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.util; 2 | 3 | import java.lang.reflect.Array; 4 | import java.util.Objects; 5 | 6 | // Adapted from apache lang3 7 | public class ArrayUtils { 8 | 9 | /** 10 | * Adds all elements from two arrays into a new array. 11 | * @param arr first array 12 | * @param arr2 second array 13 | * @param array type 14 | * @return new array with the elements of the first array, then the elements of the second 15 | */ 16 | public static T[] addAll(T[] arr, T[] arr2) { 17 | @SuppressWarnings("unchecked") 18 | T[] newArr = (T[]) Array.newInstance(arr.getClass().getComponentType(), arr.length + arr2.length); 19 | System.arraycopy(arr, 0, newArr, 0, arr.length); 20 | System.arraycopy(arr2, 0, newArr, arr.length, arr2.length); 21 | return newArr; 22 | } 23 | 24 | /** 25 | * Creates a new array with the element at the given index removed. 26 | * @param arr array 27 | * @param index index 28 | * @param array type 29 | * @return a new array 30 | * @throws IndexOutOfBoundsException if the index is out of bounds ({@code (index < 0 || arr.length <= index}) 31 | */ 32 | public static T[] remove(T[] arr, int index) { 33 | int length = arr.length; 34 | if (index < 0 || length <= index) { 35 | throw new IndexOutOfBoundsException("Index must be between 0 and " + length + " but was " + index); 36 | } 37 | 38 | @SuppressWarnings("unchecked") 39 | T[] newArr = (T[]) Array.newInstance(arr.getClass().getComponentType(), length - 1); 40 | System.arraycopy(arr, 0, newArr, 0, index); 41 | if (index < length - 1) { 42 | System.arraycopy(arr, index + 1, newArr, index, length - index - 1); 43 | } 44 | return newArr; 45 | } 46 | 47 | /** 48 | * Checks if the array contains the given value. 49 | * @param arr array 50 | * @param val value to find 51 | * @param array type 52 | * @return whether the array contains the value 53 | */ 54 | public static boolean contains(T[] arr, T val) { 55 | return indexOf(arr, val) != -1; 56 | } 57 | 58 | /** 59 | * Finds the index of the given value in the array. 60 | * @param arr array 61 | * @param val value to find 62 | * @param array type 63 | * @return the index of the value in the array, or -1 if not found 64 | */ 65 | public static int indexOf(T[] arr, T val) { 66 | for (int i = 0; i < arr.length; i++) { 67 | if (Objects.equals(arr[i], val)) { 68 | return i; 69 | } 70 | } 71 | return -1; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/util/DateUtils.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.util; 2 | 3 | import com.tisawesomeness.minecord.Bot; 4 | 5 | public class DateUtils { 6 | 7 | public static String getUptime() { 8 | long uptimeRaw = System.currentTimeMillis() - Bot.birth; 9 | uptimeRaw = Math.floorDiv(uptimeRaw, 1000L); 10 | String uptime = ""; 11 | 12 | if (uptimeRaw >= 86400) { 13 | long days = Math.floorDiv(uptimeRaw, 86400L); 14 | uptime = days + "d"; 15 | uptimeRaw = uptimeRaw - days * 86400; 16 | } 17 | if (uptimeRaw >= 3600) { 18 | long hours = Math.floorDiv(uptimeRaw, 3600L); 19 | uptime = uptime + hours + "h"; 20 | uptimeRaw = uptimeRaw - hours * 3600; 21 | } 22 | if (uptimeRaw >= 60) { 23 | long minutes = Math.floorDiv(uptimeRaw, 60L); 24 | uptime = uptime + minutes + "m"; 25 | uptimeRaw = uptimeRaw - minutes * 60; 26 | } 27 | if (uptimeRaw > 0) { 28 | uptime = uptime + uptimeRaw + "s"; 29 | } 30 | if (uptime.isEmpty()) { 31 | uptime = "0s"; 32 | } 33 | 34 | return uptime; 35 | } 36 | 37 | /** 38 | * Returns a string with the time the bot took to boot up, in seconds, to 3 decimal places 39 | */ 40 | public static String getBootTime() { 41 | return (double) Bot.bootTime / 1000 + "s"; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/util/MessageUtils.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.util; 2 | 3 | import com.tisawesomeness.minecord.Announcement; 4 | import com.tisawesomeness.minecord.Bot; 5 | import com.tisawesomeness.minecord.Config; 6 | import com.tisawesomeness.minecord.database.Database; 7 | 8 | import net.dv8tion.jda.api.EmbedBuilder; 9 | import net.dv8tion.jda.api.entities.Message; 10 | import net.dv8tion.jda.api.entities.MessageEmbed; 11 | import net.dv8tion.jda.api.entities.SelfUser; 12 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 13 | 14 | import java.awt.Color; 15 | import java.util.regex.Pattern; 16 | 17 | public class MessageUtils { 18 | 19 | /** 20 | * If the message is over the content length limit, trim it down by ending it with an ellipsis. 21 | * @param msg the message in a code block (ending in ```) 22 | * @return the trimmed message 23 | */ 24 | public static String trimCodeblock(String msg) { 25 | if (msg.length() <= Message.MAX_CONTENT_LENGTH) { 26 | return msg; 27 | } 28 | return msg.substring(0, Message.MAX_CONTENT_LENGTH - 6) + "...```"; 29 | } 30 | 31 | /** 32 | * Formats a message to look more fancy using an embed. Pass null in any argument (except color) to remove that aspect of the message. 33 | * @param title The title or header of the message. 34 | * @param url A URL that the title goes to when clicked. Only works if title is not null. 35 | * @param body The main body of the message. 36 | * @param color The color of the embed. Discord markdown formatting and newline are supported. 37 | * @return A MessageEmbed representing the message. You can add additional info (e.g. fields) by passing this variable into a new EmbedBuilder. 38 | */ 39 | public static MessageEmbed embedMessage(String title, String url, String body, Color color) { 40 | EmbedBuilder eb = new EmbedBuilder(); 41 | if (title != null) eb.setTitle(title, url); 42 | eb.setDescription(body); 43 | eb.setColor(color); 44 | eb = addFooter(eb); 45 | return eb.build(); 46 | } 47 | 48 | public static EmbedBuilder addFooter(EmbedBuilder eb) { 49 | String announcement = Announcement.rollAnnouncement(); 50 | if (Config.getOwner().equals("0")) { 51 | return eb.setFooter(announcement); 52 | } 53 | return eb.setFooter(announcement, Bot.ownerAvatarUrl); 54 | } 55 | 56 | /** 57 | * Gets the command-useful content of a message, keeping the name and arguments and purging the prefix and mention. 58 | */ 59 | public static String[] getContent(Message m, String prefix, SelfUser su) { 60 | String content = m.getContentRaw(); 61 | if (m.getContentRaw().startsWith(prefix)) { 62 | return content.replaceFirst(Pattern.quote(prefix), "").split(" "); 63 | } else if (content.replace("@!", "@").startsWith(su.getAsMention())) { 64 | String[] args = content.split(" "); 65 | return ArrayUtils.remove(args, 0); 66 | } else { 67 | return null; 68 | } 69 | } 70 | 71 | /** 72 | * Gets the prefix the bot should use in a text or private channel 73 | * @param e The event corresponding to a command 74 | * @return The configured prefix if e is for a text channel, or the default otherwise 75 | */ 76 | public static String getPrefix(MessageReceivedEvent e) { 77 | return e.isFromGuild() ? Database.getPrefix(e.getGuild().getIdLong()) : Config.getPrefix(); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/util/UrlUtils.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.util; 2 | 3 | import lombok.NonNull; 4 | import lombok.SneakyThrows; 5 | 6 | import java.io.UnsupportedEncodingException; 7 | import java.net.MalformedURLException; 8 | import java.net.URL; 9 | import java.net.URLEncoder; 10 | import java.nio.charset.StandardCharsets; 11 | 12 | /** 13 | * Utility class for working with URLs 14 | */ 15 | public final class UrlUtils { 16 | private UrlUtils() {} 17 | 18 | /** 19 | * Creates a URL from a string without throwing a checked exception. Verify that all strings passed to this 20 | * method are valid URLs. 21 | * @param str The URL in string form 22 | * @return A URL 23 | * @throws MalformedURLException sneaky 24 | */ 25 | @SneakyThrows(MalformedURLException.class) 26 | public static @NonNull URL createUrl(@NonNull String str) { 27 | return new URL(str); 28 | } 29 | 30 | /** 31 | * Changes a string URL from HTTP to HTTPS if it begins with "http:" 32 | * @param link A URL as a string (though any string will work) 33 | * @return The string changed to HTTPS, or the same string unmodified 34 | */ 35 | public static @NonNull String httpToHttps(@NonNull String link) { 36 | if (link.startsWith("http:")) { 37 | return "https" + link.substring(4); 38 | } 39 | return link; 40 | } 41 | 42 | @SneakyThrows(UnsupportedEncodingException.class) // Not possible 43 | public static @NonNull String encode(@NonNull String str) { 44 | return URLEncoder.encode(str, StandardCharsets.UTF_8.toString()); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/util/Utils.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.util; 2 | 3 | import javax.annotation.Nullable; 4 | import java.util.Collection; 5 | import java.util.List; 6 | import java.util.function.Function; 7 | import java.util.stream.Collectors; 8 | 9 | public final class Utils { 10 | private Utils() {} 11 | 12 | public static @Nullable R mapNullable(@Nullable T obj, Function mapper) { 13 | return obj == null ? null : mapper.apply(obj); 14 | } 15 | public static @Nullable R mapNullable(@Nullable T1 obj, Function mapper1, 16 | Function mapper2) { 17 | return mapNullable(mapNullable(obj, mapper1), mapper2); 18 | } 19 | 20 | public static List flatten(Collection> list) { 21 | return list.stream() 22 | .flatMap(Collection::stream) 23 | .collect(Collectors.toList()); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/util/dice/DiceError.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.util.dice; 2 | 3 | public enum DiceError { 4 | NO_DELIMITER, 5 | DICE_INVALID, 6 | DICE_ZERO, 7 | FACES_INVALID, 8 | FACES_NOT_POSITIVE, 9 | MAX_TOO_HIGH, 10 | MIN_TOO_LOW 11 | } 12 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/util/type/DelayedCountDownLatch.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.util.type; 2 | 3 | import java.util.concurrent.CountDownLatch; 4 | import java.util.concurrent.locks.Condition; 5 | import java.util.concurrent.locks.ReentrantLock; 6 | 7 | /** 8 | * Similar to a {@link CountDownLatch}, but the count does not have to be known in advance. 9 | * Threads can {@link #countDown()} before or after the starting count is set in {@link #startCountDown(int)}. 10 | */ 11 | public class DelayedCountDownLatch { 12 | 13 | private boolean started = false; 14 | private int count = 0; 15 | private final ReentrantLock lock = new ReentrantLock(); 16 | private final Condition condition = lock.newCondition(); 17 | 18 | /** 19 | * Starts the countdown with the specified count. 20 | * @param count The count to start with 21 | * @throws IllegalArgumentException If the count is negative 22 | * @throws IllegalStateException If the countdown already started 23 | */ 24 | public void startCountDown(int count) { 25 | if (count < 0) { 26 | throw new IllegalArgumentException("Count cannot be negative"); 27 | } 28 | lock.lock(); 29 | try { 30 | if (started) { 31 | throw new IllegalStateException("Countdown already started"); 32 | } 33 | started = true; 34 | this.count += count; 35 | if (this.count <= 0) { 36 | condition.signalAll(); 37 | } 38 | } finally { 39 | lock.unlock(); 40 | } 41 | } 42 | 43 | /** 44 | * Decrements the count by 1. If the countdown has not started, then this countdown will be saved for later. 45 | */ 46 | public void countDown() { 47 | lock.lock(); 48 | try { 49 | count--; 50 | if (started && count <= 0) { 51 | condition.signalAll(); 52 | } 53 | } finally { 54 | lock.unlock(); 55 | } 56 | } 57 | 58 | /** 59 | * Waits for the countdown to start and complete. 60 | * @throws InterruptedException If the thread is interrupted while waiting 61 | */ 62 | public void await() throws InterruptedException { 63 | lock.lock(); 64 | try { 65 | while (!started || count > 0) { 66 | condition.await(); 67 | } 68 | } finally { 69 | lock.unlock(); 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/util/type/Dimensions.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.util.type; 2 | 3 | import lombok.Value; 4 | 5 | @Value 6 | public class Dimensions { 7 | int width; 8 | int height; 9 | 10 | /** 11 | * Creates a new Dimensions object 12 | * @param width width 13 | * @param height height 14 | * @throws IllegalArgumentException if width or height are negative 15 | */ 16 | public Dimensions(int width, int height) { 17 | if (width < 0) { 18 | throw new IllegalArgumentException("width cannot be negative but was " + width); 19 | } 20 | if (height < 0) { 21 | throw new IllegalArgumentException("height cannot be negative but was " + height); 22 | } 23 | this.width = width; 24 | this.height = height; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return width + "x" + height; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/util/type/Switch.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.util.type; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | import java.util.concurrent.locks.Condition; 5 | import java.util.concurrent.locks.ReentrantLock; 6 | 7 | /** 8 | * A switch that can be turned on and off. 9 | * Threads can use {@link #waitForEnable(long, TimeUnit)} to wait for the switch to be enabled. 10 | */ 11 | public final class Switch { 12 | 13 | private boolean value = false; 14 | private final ReentrantLock lock = new ReentrantLock(); 15 | private final Condition condition = lock.newCondition(); 16 | 17 | /** 18 | * Enables the switch, notifying all threads waiting in {@link #waitForEnable(long, TimeUnit)}. 19 | */ 20 | public void enable() { 21 | lock.lock(); 22 | try { 23 | value = true; 24 | condition.signalAll(); 25 | } finally { 26 | lock.unlock(); 27 | } 28 | } 29 | 30 | /** 31 | * Disables the switch. 32 | */ 33 | public void disable() { 34 | lock.lock(); 35 | try { 36 | value = false; 37 | } finally { 38 | lock.unlock(); 39 | } 40 | } 41 | 42 | /** 43 | * Waits for the switch to be enabled, or returns immediately if the switch is already enabled. 44 | * @param l The time to wait 45 | * @param timeUnit The time unit of the time to wait 46 | * @return True if the switch was enabled, false if the timeout was reached 47 | * @throws InterruptedException If the thread is interrupted while waiting 48 | */ 49 | public boolean waitForEnable(long l, TimeUnit timeUnit) throws InterruptedException { 50 | lock.lock(); 51 | try { 52 | return value || condition.await(l, timeUnit); 53 | } finally { 54 | lock.unlock(); 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/com/tisawesomeness/minecord/util/type/ThrowingFunction.java: -------------------------------------------------------------------------------- 1 | package com.tisawesomeness.minecord.util.type; 2 | 3 | import lombok.SneakyThrows; 4 | 5 | import java.util.function.Function; 6 | 7 | /** 8 | * A version of {@link Function} that can throw an 9 | * unchecked exception with the help of {@link SneakyThrows}. 10 | *
Use only to override or implement methods that may throw exceptions. 11 | * @param the type of the input to the function 12 | * @param the type of the result of the function 13 | * @param the type of exception the function can throw 14 | */ 15 | @FunctionalInterface 16 | public interface ThrowingFunction extends Function { 17 | @SneakyThrows 18 | default R apply(T t) { 19 | return applyThrows(t); 20 | } 21 | R applyThrows(T t) throws E; 22 | } 23 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | [ 2 | "1.7.10", 3 | "1.8.9", 4 | "1.9.4", 5 | "1.10.2", 6 | "1.11.2", 7 | "1.12.2", 8 | "1.13.2", 9 | "1.14.4", 10 | "1.15.2", 11 | "1.16.5", 12 | "1.17.1", 13 | "1.18.2", 14 | "1.19.4", 15 | "1.20.6", 16 | "1.21.9" 17 | ] 18 | --------------------------------------------------------------------------------