diff --git a/LICENSE b/LICENSE index b62c9918..0a041280 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,165 @@ -MIT License - -Copyright (c) 2017-2020 NamelessMC - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/README.md b/README.md index af909a6a..8de14d31 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,35 @@ # Nameless-Java-API -Java library for interacting with a NamelessMC website. It is used by for example the NamelessMC Spigot plugin and Nameless-Link Discord bot. + +Java library for interacting with a NamelessMC website. It is used by for example the NamelessMC Minecraft server plugin and Nameless-Link Discord bot. + +## Install to local maven repository + +We don't publish builds to maven central (yet). You will need to build and install this project locally. + +OpenJDK 11, git and maven should be installed. Run in a terminal: + +``` +git clone https://github.com/NamelessMC/Nameless-Java-API +cd Nameless-Java-API +mvn install +``` + +## Include as dependency + +```xml + + com.namelessmc + java-api + canary + +``` + +## Usage + +Create an API instance: + +```java +String apiUrl = ""; +String apiKey = ""; +NamelessAPI api = NamelessAPI.builder(apiUrl, apiKey).build(); +``` diff --git a/pom.xml b/pom.xml index c61578d5..4ecbe73c 100644 --- a/pom.xml +++ b/pom.xml @@ -11,30 +11,51 @@ UTF-8 + 3.53.1 - src - org.apache.maven.plugins maven-compiler-plugin - 3.10.1 + 3.15.0 11 11 + true + true + + + org.checkerframework + checker + ${checkerFrameworkVersion} + + + + org.checkerframework.checker.nullness.NullnessChecker + org.checkerframework.checker.optional.OptionalChecker + org.checkerframework.checker.regex.RegexChecker + org.checkerframework.checker.formatter.FormatterChecker + + + -Awarns + -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED + -J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED + -J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED + - - org.apache.maven.plugins - maven-shade-plugin - 3.2.4 - - false - true - + maven-surefire-plugin + 3.5.4 @@ -44,26 +65,39 @@ com.google.code.gson gson - 2.9.0 + 2.13.2 com.google.guava guava - 21.0 + 23.0 org.slf4j slf4j-api - 2.0.0-alpha7 + 2.0.17 provided - org.jetbrains - annotations - 23.0.0 + org.checkerframework + checker-qual + ${checkerFrameworkVersion} + + + + com.github.mizosoft.methanol + methanol + 1.9.0 + + + + org.junit.jupiter + junit-jupiter-engine + 6.0.3 + test diff --git a/src/com/namelessmc/java_api/ApiError.java b/src/com/namelessmc/java_api/ApiError.java deleted file mode 100644 index 815416d3..00000000 --- a/src/com/namelessmc/java_api/ApiError.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.namelessmc.java_api; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Optional; - -public class ApiError extends NamelessException { - - public static final int UNKNOWN_ERROR = 0; - public static final int INVALID_API_KEY = 1; - // 2 intentionally missing - public static final int INVALID_API_METHOD = 3; - public static final int NO_UNIQUE_SITE_ID_AVAILABLE = 4; - // 5 intentionally missing - public static final int INVALID_GET_POST_CONTENTS = 6; - public static final int INVALID_EMAIL_ADDRESS = 7; - public static final int INVALID_USERNAME = 8; - public static final int INVALID_UUID = 9; - public static final int EMAIL_ALREADY_EXISTS = 10; - public static final int USERNAME_ALREADY_EXISTS = 11; - public static final int UUID_ALREADY_EXISTS = 12; - public static final int UNABLE_TO_CREATE_ACCOUNT = 13; - public static final int UNABLE_TO_SEND_REGISTRATION_EMAIL = 14; - // 15 intentionally missing - public static final int UNABLE_TO_FIND_USER = 16; - public static final int UNABLE_TO_FIND_GROUP = 17; - // 18 intentionally missing - public static final int REPORT_CONTENT_TOO_LARGE = 19; - // 20 intentionally missing - public static final int USER_CREATING_REPORT_BANNED = 21; - public static final int USER_ALREADY_HAS_OPEN_REPORT = 22; - // 23 intentionally missing - public static final int UNABLE_TO_UPDATE_USERNAME = 24; - public static final int UNABLE_TO_UPDATE_SERVER_INFO = 25; - public static final int CANNOT_REPORT_YOURSELF = 26; - public static final int INVALID_SERVER_ID = 27; - public static final int INVALID_VALIDATE_CODE = 28; - public static final int UNABLE_TO_SET_USER_DISCORD_ID = 29; - public static final int UNABLE_TO_SET_DISCORD_BOT_URL = 30; - // 31 intentionally missing - public static final int ACCOUNT_ALREADY_ACTIVATED = 32; - public static final int UNABLE_TO_SET_DISCORD_GUILD_ID = 33; - public static final int DISCORD_INTEGRATION_DISABLED = 34; - // 35 intentionally missing - public static final int REQUEST_NOT_AUTHORIZED = 36; - public static final int INVALID_INTEGRATION = 37; - // 38 intentionally missing - - private static final long serialVersionUID = 3093028909912281912L; - - private final int code; - private final @Nullable String meta; - - public ApiError(final int code, @Nullable String meta) { - super("An unexpected API error occurred with error code " + code + " and " + (meta == null ? "no meta" : "meta " + meta)); - this.code = code; - this.meta = meta; - } - - public int getError() { - return this.code; - } - - public @NotNull Optional<@NotNull String> getMeta() { - return Optional.ofNullable(meta); - } - -} diff --git a/src/com/namelessmc/java_api/CustomProfileFieldValue.java b/src/com/namelessmc/java_api/CustomProfileFieldValue.java deleted file mode 100644 index ee25851b..00000000 --- a/src/com/namelessmc/java_api/CustomProfileFieldValue.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.namelessmc.java_api; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class CustomProfileFieldValue { - - private final @NotNull CustomProfileField field; - private final @Nullable String value; - - CustomProfileFieldValue(@NotNull CustomProfileField field, @Nullable String value) { - this.field = field; - this.value = value; - } - - public @NotNull CustomProfileField getField() { - return this.field; - } - - public @Nullable String getValue() { - return value; - } - -} diff --git a/src/com/namelessmc/java_api/FilteredUserListBuilder.java b/src/com/namelessmc/java_api/FilteredUserListBuilder.java deleted file mode 100644 index 8ea3160d..00000000 --- a/src/com/namelessmc/java_api/FilteredUserListBuilder.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.namelessmc.java_api; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -public class FilteredUserListBuilder { - - private final @NotNull NamelessAPI api; - private @Nullable Map, Object> filters; - private @NotNull String operator = "AND"; - - FilteredUserListBuilder(@NotNull NamelessAPI api) { - this.api = api; - } - - public FilteredUserListBuilder withFilter(UserFilter filter, T value) { - if (filters == null) { - filters = new HashMap<>(); - } - - filters.put(filter, value); - return this; - } - - public FilteredUserListBuilder all() { - this.operator = "AND"; - return this; - } - - public FilteredUserListBuilder any() { - this.operator = "OR"; - return this; - } - - public List makeRequest() throws NamelessException { - final Object[] parameters; - if (filters != null) { - int filterCount = filters.size(); - parameters = new Object[filterCount * 2 + 2]; - parameters[0] = "operator"; - parameters[1] = operator; - Iterator, Object>> iterator = filters.entrySet().iterator(); - for (int i = 1; i < filterCount; i++) { - Map.Entry, Object> entry = iterator.next(); - parameters[i*2] = entry.getKey().getName(); - parameters[i*2+1] = entry.getValue(); - } - } else { - parameters = new Object[0]; - } - - final JsonObject response = this.api.getRequestHandler().get("users", parameters); - final JsonArray array = response.getAsJsonArray("users"); - final List users = new ArrayList<>(array.size()); - for (final JsonElement e : array) { - final JsonObject o = e.getAsJsonObject(); - final int id = o.get("id").getAsInt(); - final String username = o.get("username").getAsString(); - final UUID uuid; - if (o.has("uuid")) { - final String uuidString = o.get("uuid").getAsString(); - if (uuidString == null || uuidString.equals("none") || uuidString.equals("")) { - uuid = null; - } else { - uuid = NamelessAPI.websiteUuidToJavaUuid(uuidString); - } - } else { - uuid = null; - } - users.add(new NamelessUser(this.api, id, username, true, uuid, false, -1L)); - } - - return Collections.unmodifiableList(users); - } - -} diff --git a/src/com/namelessmc/java_api/IntegrationType.java b/src/com/namelessmc/java_api/IntegrationType.java deleted file mode 100644 index 40e541ed..00000000 --- a/src/com/namelessmc/java_api/IntegrationType.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.namelessmc.java_api; - -import org.jetbrains.annotations.NotNull; - -public enum IntegrationType { - - MINECRAFT("Minecraft"), - DISCORD("Discord"), - ; - - private final @NotNull String apiValue; - - IntegrationType(final @NotNull String apiValue) { - this.apiValue = apiValue; - } - - public @NotNull String apiValue() { - return apiValue; - } - -} diff --git a/src/com/namelessmc/java_api/LanguageCodeMap.java b/src/com/namelessmc/java_api/LanguageCodeMap.java deleted file mode 100644 index 459fd911..00000000 --- a/src/com/namelessmc/java_api/LanguageCodeMap.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.namelessmc.java_api; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.HashMap; -import java.util.Map; - -public class LanguageCodeMap { - - private static final Map NAMELESS_TO_POSIX = new HashMap<>(); - - static { - NAMELESS_TO_POSIX.put("Czech", "cs_CZ"); - NAMELESS_TO_POSIX.put("German", "de_DE"); - NAMELESS_TO_POSIX.put("Greek", "el_GR"); - NAMELESS_TO_POSIX.put("EnglishUK", "en_UK"); - NAMELESS_TO_POSIX.put("EnglishUS", "en_US"); - NAMELESS_TO_POSIX.put("Spanish", "es_419"); - NAMELESS_TO_POSIX.put("SpanishES", "es_ES"); - NAMELESS_TO_POSIX.put("French", "fr_FR"); - NAMELESS_TO_POSIX.put("Hungarian", "hu_HU"); - NAMELESS_TO_POSIX.put("Italian", "it_IT"); - NAMELESS_TO_POSIX.put("Lithuanian", "lt_LT"); - NAMELESS_TO_POSIX.put("Norwegian", "nb_NO"); - NAMELESS_TO_POSIX.put("Dutch", "nl_NL"); - NAMELESS_TO_POSIX.put("Polish", "pl_PL"); - NAMELESS_TO_POSIX.put("Portuguese", "pt_BR"); - NAMELESS_TO_POSIX.put("Romanian", "ro_RO"); - NAMELESS_TO_POSIX.put("Russian", "ru_RU"); - NAMELESS_TO_POSIX.put("Slovak", "sk_SK"); - NAMELESS_TO_POSIX.put("SwedishSE", "sv_SE"); - NAMELESS_TO_POSIX.put("Turkish", "tr_TR"); - NAMELESS_TO_POSIX.put("Chinese(Simplified)", "zh_CN"); - } - - static @Nullable String getLanguagePosix(final @NotNull String language) { - return NAMELESS_TO_POSIX.get(language); - } - -} diff --git a/src/com/namelessmc/java_api/LanguageEntity.java b/src/com/namelessmc/java_api/LanguageEntity.java deleted file mode 100644 index 36943e84..00000000 --- a/src/com/namelessmc/java_api/LanguageEntity.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.namelessmc.java_api; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public interface LanguageEntity { - - @NotNull String getLanguage() throws NamelessException; - - @Nullable String getLanguagePosix() throws NamelessException; - -} diff --git a/src/com/namelessmc/java_api/NamelessAPI.java b/src/com/namelessmc/java_api/NamelessAPI.java deleted file mode 100755 index e3872394..00000000 --- a/src/com/namelessmc/java_api/NamelessAPI.java +++ /dev/null @@ -1,510 +0,0 @@ -package com.namelessmc.java_api; - -import com.google.common.base.Preconditions; -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.namelessmc.java_api.exception.*; -import com.namelessmc.java_api.modules.websend.WebsendAPI; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.math.BigInteger; -import java.net.URL; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -public final class NamelessAPI { - - static final Gson GSON = new Gson(); - - @NotNull - private final RequestHandler requests; - - NamelessAPI(@NotNull final RequestHandler requests) { - this.requests = Objects.requireNonNull(requests, "Request handler is null"); - } - - @NotNull - RequestHandler getRequestHandler() { - return this.requests; - } - - @NotNull - public URL getApiUrl() { - return this.getRequestHandler().getApiUrl(); - } - - @NotNull - public String getApiKey() { - return getApiKey(this.getApiUrl().toString()); - } - - @NotNull - static String getApiKey(@NotNull final String url) { - if (url.endsWith("/")) { - return getApiKey(url.substring(0, url.length() - 1)); - } - - return url.substring(url.lastIndexOf('/') + 1); - } - - /** - * Get announcements visible to guests. Use {@link NamelessUser#getAnnouncements()} for non-guest announcements. - * @return List of announcements - */ - @NotNull - public List<@NotNull Announcement> getAnnouncements() throws NamelessException { - final JsonObject response = this.requests.get("announcements"); - return getAnnouncements(response); - } - - /** - * Get announcements visible to a {@link NamelessUser} - * @param user User to get visible announcements for - * @return List of announcements visible to the user - * @deprecated Use {@link NamelessUser#getAnnouncements()} - */ - @NotNull - @Deprecated - public List<@NotNull Announcement> getAnnouncements(@NotNull final NamelessUser user) throws NamelessException { - final JsonObject response = this.requests.get("users/" + user.getUserTransformer() + "/announcements"); - - return getAnnouncements(response); - } - - /** - * Convert announcement json to objects - * @param response Announcements json API response - * @return List of {@link Announcement} objects - */ - @NotNull - static List<@NotNull Announcement> getAnnouncements(@NotNull final JsonObject response) { - return StreamSupport.stream(response.getAsJsonArray("announcements").spliterator(), false) - .map(JsonElement::getAsJsonObject) - .map(Announcement::new) - .collect(Collectors.toList()); - } - - /** - * Send Minecraft server information to the website. Currently, the exact JSON contents are undocumented. - * @param jsonData Json data to submit - */ - public void submitServerInfo(final @NotNull JsonObject jsonData) throws NamelessException { - this.requests.post("minecraft/server-info", jsonData); - } - - /** - * Get website information - * @return {@link Website} object containing website information - */ - public Website getWebsite() throws NamelessException { - final JsonObject json = this.requests.get("info"); - return new Website(json); - } - - public FilteredUserListBuilder getRegisteredUsers() { - return new FilteredUserListBuilder(this); - } - - public @NotNull Optional getUser(final int id) throws NamelessException { - final NamelessUser user = getUserLazy(id); - if (user.exists()) { - return Optional.of(user); - } else { - return Optional.empty(); - } - } - - public @NotNull Optional getUser(@NotNull final String username) throws NamelessException { - final NamelessUser user = getUserLazy(username); - if (user.exists()) { - return Optional.of(user); - } else { - return Optional.empty(); - } - } - - public @NotNull Optional getUser(@NotNull final UUID uuid) throws NamelessException { - final NamelessUser user = getUserLazy(uuid); - if (user.exists()) { - return Optional.of(user); - } else { - return Optional.empty(); - } - } - - public @NotNull Optional getUserByDiscordId(final long discordId) throws NamelessException { - final NamelessUser user = getUserLazyDiscord(discordId); - if (user.exists()) { - return Optional.of(user); - } else { - return Optional.empty(); - } - } - - /** - * Construct a NamelessUser object without making API requests (so without checking if the user exists) - * @param id NamelessMC user id - * @return Nameless user object, never null - */ - public @NotNull NamelessUser getUserLazy(final int id) { - return new NamelessUser(this, id, null, false, null, false, -1L); - } - - /** - * Construct a NamelessUser object without making API requests (so without checking if the user exists) - * @param username NamelessMC user - * @return Nameless user object, never null - */ - public @NotNull NamelessUser getUserLazy(final @NotNull String username) { - return new NamelessUser(this, -1, username, false, null, false, -1L); - } - - /** - * Construct a NamelessUser object without making API requests (so without checking if the user exists) - * @param uuid Minecraft UUID - * @return Nameless user object, never null - */ - public @NotNull NamelessUser getUserLazy(@NotNull final UUID uuid) { - return new NamelessUser(this, -1, null, true, uuid, false, -1L); - } - - /** - * Construct a NamelessUser object without making API requests (so without checking if the user exists) - * @param username The user's username - * @param uuid The user's Mojang UUID - * @return Nameless user object, never null - */ - public NamelessUser getUserLazy(@NotNull final String username, @NotNull final UUID uuid) { - return new NamelessUser(this, -1, username, true, uuid, false,-1L); - } - - /** - * Construct a NamelessUser object without making API requests (so without checking if the user exists) - * @param id NamelessMC user id - * @return Nameless user object, never null - */ - public NamelessUser getUserLazy(final int id, final @NotNull String username, final @NotNull UUID uuid) { - return new NamelessUser(this, id, username, true, uuid, false, -1L); - } - - /** - * Construct a NamelessUser object without making API requests (so without checking if the user exists) - * @param discordId Discord user id - * @return Nameless user object, never null - */ - public NamelessUser getUserLazyDiscord(final long discordId) { - Preconditions.checkArgument(discordId > 0, "Discord id must be a positive long"); - return new NamelessUser(this, -1, null, false, null, true, discordId); - } - - /** - * Get NamelessMC group by ID - * @param id Group id - * @return Optional with a group if the group exists, empty optional if it doesn't - */ - @NotNull - public Optional<@NotNull Group> getGroup(final int id) throws NamelessException { - final JsonObject response = this.requests.get("groups", "id", id); - final JsonArray jsonArray = response.getAsJsonArray("groups"); - if (jsonArray.size() != 1) { - return Optional.empty(); - } else { - return Optional.of(new Group(jsonArray.get(0).getAsJsonObject())); - } - } - - /** - * Get NamelessMC groups by name - * @param name NamelessMC groups name - * @return List of groups with this name, empty if there are no groups with this name. - */ - @NotNull - public List<@NotNull Group> getGroup(@NotNull final String name) throws NamelessException { - Objects.requireNonNull(name, "Group name is null"); - final JsonObject response = this.requests.get("groups", "name", name); - return groupListFromJsonArray(response.getAsJsonArray("groups")); - } - - /** - * Get a list of all groups on the website - * @return list of groups - */ - public @NotNull List getAllGroups() throws NamelessException { - final JsonObject response = this.requests.get("groups"); - return groupListFromJsonArray(response.getAsJsonArray("groups")); - - } - - public int @NotNull[] getAllGroupIds() throws NamelessException { - final JsonObject response = this.requests.get("groups"); - return StreamSupport.stream(response.getAsJsonArray("groups").spliterator(), false) - .map(JsonElement::getAsJsonObject) - .mapToInt(o -> o.get("id").getAsInt()) - .toArray(); - } - - private @NotNull List groupListFromJsonArray(@NotNull final JsonArray array) { - return StreamSupport.stream(array.spliterator(), false) - .map(JsonElement::getAsJsonObject) - .map(Group::new) - .collect(Collectors.toList()); - } - - /** - * Registers a new account. The user will be emailed to set a password. - * - * @param username Username (this should match the user's in-game username when specifying a UUID) - * @param email Email address - * @param uuid Mojang UUID, if you wish to use the Minecraft integration. Nullable. - * @return Email verification disabled: A link which the user needs to click to complete registration - *
Email verification enabled: An empty string (the user needs to check their email to complete registration) - * @see #registerUser(String, String) - */ - public @NotNull Optional registerUser(@NotNull final String username, - @NotNull final String email, - @Nullable final UUID uuid) - throws NamelessException, InvalidUsernameException, UsernameAlreadyExistsException, CannotSendEmailException, UuidAlreadyExistsException { - Objects.requireNonNull(username, "Username is null"); - Objects.requireNonNull(email, "Email address is null"); - - final JsonObject post = new JsonObject(); - post.addProperty("username", username); - post.addProperty("email", email); - if (uuid != null) { - post.addProperty("uuid", uuid.toString()); - } - - try { - final JsonObject response = this.requests.post("users/register", post); - - if (response.has("link")) { - return Optional.of(response.get("link").getAsString()); - } else { - return Optional.empty(); - } - } catch (final ApiError e) { - if (e.getError() == ApiError.INVALID_USERNAME) { - throw new InvalidUsernameException(); - } else if (e.getError() == ApiError.USERNAME_ALREADY_EXISTS) { - throw new UsernameAlreadyExistsException(); - } else if (e.getError() == ApiError.UNABLE_TO_SEND_REGISTRATION_EMAIL) { - throw new CannotSendEmailException(); - } else if (e.getError() == ApiError.UUID_ALREADY_EXISTS) { - throw new UuidAlreadyExistsException(); - } else { - throw e; - } - } - } - - /** - * Register user without UUID {@link #registerUser(String, String, UUID)} - * WARNING: This will fail if the website has Minecraft integration enabled! - * @param username New username for this user - * @param email New email address for this user - * @return Verification URL if email verification is disabled. - */ - public @NotNull Optional registerUser(@NotNull final String username, - @NotNull final String email) - throws NamelessException, InvalidUsernameException, UsernameAlreadyExistsException, CannotSendEmailException { - try { - return registerUser(username, email, null); - } catch (final UuidAlreadyExistsException e) { - throw new IllegalStateException("Website said duplicate uuid but we haven't specified a uuid?", e); - } - } - - /** - * Set Discord bot URL (Nameless-Link internal webserver) - * @param url Discord bot URL - */ - public void setDiscordBotUrl(@NotNull final URL url) throws NamelessException { - Objects.requireNonNull(url, "Bot url is null"); - - final JsonObject json = new JsonObject(); - json.addProperty("url", url.toString()); - this.requests.post("discord/update-bot-settings", json); - } - - /** - * Set Discord guild (server) id - * @param guildId Discord guild (server) id - */ - public void setDiscordGuildId(final long guildId) throws NamelessException { - final JsonObject json = new JsonObject(); - json.addProperty("guild_id", guildId + ""); - this.requests.post("discord/update-bot-settings", json); - } - - /** - * Set discord bot username and user id - * @param username Bot username#tag - * @param userId Bot user id - * @see #setDiscordBotSettings(URL, long, String, long) - */ - public void setDiscordBotUser(@NotNull final String username, final long userId) throws NamelessException { - Objects.requireNonNull(username, "Bot username is null"); - - final JsonObject json = new JsonObject(); - json.addProperty("bot_username", username); - json.addProperty("bot_user_id", userId + ""); - this.requests.post("discord/update-bot-settings", json); - } - - /** - * Update all Discord bot settings. - * @param url Discord bot URL - * @param guildId Discord guild (server) id - * @param username Discord bot username#tag - * @param userId Discord bot user id - * @see #setDiscordBotUrl(URL) - * @see #setDiscordGuildId(long) - * @see #setDiscordBotUser(String, long) - */ - public void setDiscordBotSettings(@NotNull final URL url, final long guildId, @NotNull final String username, final long userId) throws NamelessException { - Objects.requireNonNull(url, "Bot url is null"); - Objects.requireNonNull(username, "Bot username is null"); - - final JsonObject json = new JsonObject(); - json.addProperty("url", url.toString()); - json.addProperty("guild_id", guildId + ""); - json.addProperty("bot_username", username); - json.addProperty("bot_user_id", userId + ""); - this.requests.post("discord/update-bot-settings", json); - } - - /** - * Send list of Discord roles to the website for populating the dropdown in StaffCP > API > Group sync - * @param discordRoles Map of Discord roles, key is role id, value is role name - */ - public void submitDiscordRoleList(@NotNull final Map discordRoles) throws NamelessException { - final JsonArray roles = new JsonArray(); - discordRoles.forEach((id, name) -> { - final JsonObject role = new JsonObject(); - role.addProperty("id", id); - role.addProperty("name", name); - roles.add(role); - }); - final JsonObject json = new JsonObject(); - json.add("roles", roles); - this.requests.post("discord/submit-role-list", json); - } - - /** - * Update Discord username for a NamelessMC user associated with the provided Discord user id - * @param discordUserId Discord user id - * @param discordUsername New Discord [username#tag]s - * @see #updateDiscordUsernames(long[], String[]) - */ - public void updateDiscordUsername(final long discordUserId, - final @NotNull String discordUsername) - throws NamelessException { - Objects.requireNonNull(discordUsername, "Discord username is null"); - - final JsonObject user = new JsonObject(); - user.addProperty("id", discordUserId); - user.addProperty("name", discordUsername); - final JsonArray users = new JsonArray(); - users.add(user); - final JsonObject json = new JsonObject(); - json.add("users", users); - this.requests.post("discord/update-usernames", json); - } - - /** - * Update Discord usernames in bulk - * @param discordUserIds Discord user ids - * @param discordUsernames New Discord [username#tag]s - * @see #updateDiscordUsername(long, String) - */ - public void updateDiscordUsernames(final long@NotNull[] discordUserIds, - final @NotNull String@NotNull[] discordUsernames) - throws NamelessException { - Objects.requireNonNull(discordUserIds, "User ids array is null"); - Objects.requireNonNull(discordUsernames, "Usernames array is null"); - Preconditions.checkArgument(discordUserIds.length == discordUsernames.length, - "discord user ids and discord usernames must be of same length"); - - if (discordUserIds.length == 0) { - return; - } - - final JsonArray users = new JsonArray(); - - for (int i = 0; i < discordUserIds.length; i++) { - final JsonObject user = new JsonObject(); - user.addProperty("id", discordUserIds[i]); - user.addProperty("name", discordUsernames[i]); - users.add(user); - } - - final JsonObject json = new JsonObject(); - json.add("users", users); - this.requests.post("discord/update-usernames", json); - } - - private void verifyIntegration(final @NotNull IntegrationType type, - final @NotNull String verificationCode, - final @NotNull String identifier, - final @NotNull String username) throws NamelessException, InvalidValidateCodeException { - JsonObject data = new JsonObject(); - data.addProperty("integration", type.apiValue()); - data.addProperty("code", Objects.requireNonNull(verificationCode, "Verification code is null")); - data.addProperty("identifier", Objects.requireNonNull(identifier, "Identifier is null")); - data.addProperty("username", Objects.requireNonNull(username, "Username is null")); - try { - this.requests.post("integration/verify", data); - } catch (ApiError e) { - if (e.getError() == ApiError.INVALID_VALIDATE_CODE) { - throw new InvalidValidateCodeException(); - } else { - throw e; - } - } - } - - public void verifyMinecraft(final @NotNull String verificationCode, - final @NotNull UUID uuid, - final @NotNull String username) throws NamelessException, InvalidValidateCodeException { - this.verifyIntegration(IntegrationType.MINECRAFT, verificationCode, uuid.toString(), username); - } - - public void verifyDiscord(final @NotNull String verificationCode, - final long id, - final String username) throws NamelessException, InvalidValidateCodeException { - this.verifyIntegration(IntegrationType.DISCORD, verificationCode, String.valueOf(id), username); - } - - public @NotNull WebsendAPI websend() { - return new WebsendAPI(this.requests); - } - - - /** - * Adds back dashes to a UUID string and converts it to a Java UUID object - * @param uuid UUID without dashes - * @return UUID with dashes - */ - static @NotNull UUID websiteUuidToJavaUuid(@NotNull final String uuid) { - Objects.requireNonNull(uuid, "UUID string is null"); - // Website sends UUIDs without dashes, so we can't use UUID#fromString - // https://stackoverflow.com/a/30760478 - try { - final BigInteger a = new BigInteger(uuid.substring(0, 16), 16); - final BigInteger b = new BigInteger(uuid.substring(16, 32), 16); - return new UUID(a.longValue(), b.longValue()); - } catch (final IndexOutOfBoundsException e) { - throw new IllegalArgumentException("Invalid uuid: '" + uuid + "'", e); - } - } - - @NotNull - public static NamelessApiBuilder builder(@NotNull URL apiUrl, @NotNull String apiKey) { - return new NamelessApiBuilder(apiUrl, apiKey); - } - -} diff --git a/src/com/namelessmc/java_api/NamelessApiBuilder.java b/src/com/namelessmc/java_api/NamelessApiBuilder.java deleted file mode 100644 index 8ef49b4d..00000000 --- a/src/com/namelessmc/java_api/NamelessApiBuilder.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.namelessmc.java_api; - -import com.google.gson.GsonBuilder; -import com.namelessmc.java_api.logger.ApiLogger; -import com.namelessmc.java_api.logger.PrintStreamLogger; -import com.namelessmc.java_api.logger.Slf4jLogger; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.net.Authenticator; -import java.net.ProxySelector; -import java.net.URL; -import java.net.http.HttpClient; -import java.time.Duration; -import java.util.Objects; - -public class NamelessApiBuilder { - - private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(3); - private static final String DEFAULT_USER_AGENT = "Nameless-Java-API"; - - private final @NotNull URL apiUrl; - private final @NotNull String apiKey; - private final @NotNull HttpClient.Builder httpClientBuilder; - private final @NotNull GsonBuilder gsonBuilder; - private @NotNull String userAgent = DEFAULT_USER_AGENT; - private @Nullable ApiLogger debugLogger = null; - private Duration timeout = DEFAULT_TIMEOUT; - - NamelessApiBuilder(@NotNull URL apiUrl, @NotNull String apiKey) { - this.apiUrl = apiUrl; - this.apiKey = apiKey; - this.httpClientBuilder = HttpClient.newBuilder(); - this.gsonBuilder = new GsonBuilder(); - this.gsonBuilder.disableHtmlEscaping(); - } - - public @NotNull NamelessApiBuilder userAgent(@NotNull final String userAgent) { - this.userAgent = Objects.requireNonNull(userAgent, "User agent is null"); - return this; - } - - public @NotNull NamelessApiBuilder debug(final boolean debug) { - if (debug) { - return this.withStdErrDebugLogging(); - } else { - this.debugLogger = null; - return this; - } - } - - public @NotNull NamelessApiBuilder withStdErrDebugLogging() { - this.debugLogger = PrintStreamLogger.DEFAULT_INSTANCE; - return this; - } - - public @NotNull NamelessApiBuilder withSlf4jDebugLogging() { - this.debugLogger = Slf4jLogger.DEFAULT_INSTANCE; - return this; - } - - public @NotNull NamelessApiBuilder withCustomDebugLogger(final @Nullable ApiLogger debugLogger) { - this.debugLogger = debugLogger; - return this; - } - - @Deprecated - public @NotNull NamelessApiBuilder withTimeoutMillis(final int timeout) { - this.timeout = Duration.ofMillis(timeout); - return this; - } - - public @NotNull NamelessApiBuilder withTimeout(final @Nullable Duration timeout) { - this.timeout = timeout; - return this; - } - - public @NotNull NamelessApiBuilder withProxy(ProxySelector proxy) { - this.httpClientBuilder.proxy(proxy); - return this; - } - - public @NotNull NamelessApiBuilder withAuthenticator(Authenticator authenticator) { - this.httpClientBuilder.authenticator(authenticator); - return this; - } - - public @NotNull NamelessApiBuilder withPrettyJson() { - gsonBuilder.setPrettyPrinting(); - return this; - } - - public @NotNull NamelessAPI build() { - return new NamelessAPI(new RequestHandler(this.apiUrl, this.apiKey, this.httpClientBuilder.build(), this.gsonBuilder.create(), this.userAgent, this.debugLogger, this.timeout)); - } - -} diff --git a/src/com/namelessmc/java_api/NamelessException.java b/src/com/namelessmc/java_api/NamelessException.java deleted file mode 100644 index 549ee5e5..00000000 --- a/src/com/namelessmc/java_api/NamelessException.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.namelessmc.java_api; - -import org.jetbrains.annotations.NotNull; - -/** - * Generic exception thrown by many methods in the Nameless API - */ -public class NamelessException extends Exception { - - private static final long serialVersionUID = -3698433855091611529L; - - public NamelessException(@NotNull final String message) { - super(message); - } - - public NamelessException(@NotNull final String message, @NotNull final Throwable cause) { - super(message, cause); - } - - public NamelessException(@NotNull final Throwable cause) { - super(cause); - } - - public NamelessException() { - super(); - } - -} diff --git a/src/com/namelessmc/java_api/NamelessUser.java b/src/com/namelessmc/java_api/NamelessUser.java deleted file mode 100644 index 6f0fa940..00000000 --- a/src/com/namelessmc/java_api/NamelessUser.java +++ /dev/null @@ -1,456 +0,0 @@ -package com.namelessmc.java_api; - -import com.google.common.base.Preconditions; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.namelessmc.java_api.Notification.NotificationType; -import com.namelessmc.java_api.exception.AlreadyHasOpenReportException; -import com.namelessmc.java_api.exception.CannotReportSelfException; -import com.namelessmc.java_api.exception.ReportUserBannedException; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -public final class NamelessUser implements LanguageEntity { - - @NotNull - private final NamelessAPI api; - @NotNull - private final RequestHandler requests; - - private int id; // -1 if not known - private @Nullable String username; // null if not known - private boolean uuidKnown; - private @Nullable UUID uuid; // null if not known or not present - private boolean discordIdKnown; - private long discordId; // -1 if not known or not present - - @Nullable - private JsonObject _userInfo; // Do not use directly, instead use getUserInfo() - - /** - * Create a Nameless user. Only one of 'id', 'uuid', 'discordId' has to be provided. - * @param api Nameless API - * @param id The user's id, or -1 if not known - * @param username The user's username, or null if not known - * @param uuidKnown True if it is known whether this user has a UUID or not - * @param uuid The user's uuid, or null if the user doesn't have a UUID, or it is not known whether the user has a UUID - * @param discordIdKnown True if it is known whether this user has a linked Discord id or not - * @param discordId The user's discord id, or -1 if the user doesn't have a linked Discord id, or it is not known whether the user has a Discord id - */ - NamelessUser(@NotNull final NamelessAPI api, - final int id, - @Nullable final String username, - boolean uuidKnown, - @Nullable UUID uuid, - boolean discordIdKnown, - long discordId - ) { - this.api = api; - this.requests = api.getRequestHandler(); - - if (id == -1 && username == null && !uuidKnown && !discordIdKnown) { - throw new IllegalArgumentException("You must specify at least one of ID, uuid, username, discordId"); - } - - this.id = id; - this.username = username; - this.uuidKnown = uuidKnown; - this.uuid = uuid; - this.discordIdKnown = discordIdKnown; - this.discordId = discordId; - } - - private JsonObject getUserInfo() throws NamelessException { - if (this._userInfo == null) { - final JsonObject response = this.requests.get("users/" + this.getUserTransformer()); - - if (!response.get("exists").getAsBoolean()) { - throw new UserNotExistException(); - } - - this._userInfo = response; - } - - return this._userInfo; - } - - public String getUserTransformer() { - if (id != -1) { - return "id:" + this.id; - } else if (this.uuidKnown && this.uuid != null) { - return "integration_id:minecraft:" + this.uuid; - } else if (this.discordIdKnown && this.discordId != -1) { - return "integration_id:discord:" + this.discordId; - } else if (this.username != null) { - return "username:" + username; - } else { - throw new IllegalStateException("ID, uuid, and username not known for this player. " + - "This should be impossible, the constructor checks for this."); - } - } - - @NotNull - public NamelessAPI getApi() { - return this.api; - } - - /** - * The API method `userInfo` is only called once to improve performance. - * This means that if something changes on the website, methods that use - * data from the `userInfo` API method will keep returning the old data. - * Calling this method will invalidate the cache and require making a new - * API request. It will not make a new API request immediately. Calling - * this method multiple times while the cache is already cleared has no - * effect. - */ - public void invalidateCache() { - this._userInfo = null; - } - - public int getId() throws NamelessException { - if (this.id == -1) { - this.id = this.getUserInfo().get("id").getAsInt(); - } - - return this.id; - } - - public @NotNull String getUsername() throws NamelessException { - if (this.username == null) { - this.username = this.getUserInfo().get("username").getAsString(); - } - - return this.username; - } - - public void updateUsername(final @NotNull String username) throws NamelessException { - JsonObject post = new JsonObject(); - post.addProperty("username", username); - this.requests.post("users/" + this.getUserTransformer() + "/update-username", post); - } - - public @NotNull Optional<@NotNull UUID> getUniqueId() throws NamelessException { - if (!this.uuidKnown) { - JsonObject userInfo = this.getUserInfo(); - if (userInfo.has("uuid")) { - final String uuidString = userInfo.get("uuid").getAsString(); - if (uuidString == null || - uuidString.equals("none") || - uuidString.equals("")) { - this.uuid = null; - } else { - this.uuid = NamelessAPI.websiteUuidToJavaUuid(uuidString); - } - } else { - this.uuid = null; - } - this.uuidKnown = true; - } - - return Optional.ofNullable(this.uuid); - } - - public @NotNull Optional<@NotNull Long> getDiscordId() throws NamelessException { - if (!this.discordIdKnown) { - JsonObject userInfo = this.getUserInfo(); - //noinspection ConstantConditions - if (userInfo.has("discord_id")) { - this.discordId = userInfo.get("discord_id").getAsLong(); - } else { - this.discordId = -1; - } - this.discordIdKnown = true; - } - - return this.discordId > 0 ? Optional.of(this.discordId) : Optional.empty(); - } - - public boolean exists() throws NamelessException { - try { - this.getUserInfo(); - return true; - } catch (final UserNotExistException e) { - return false; - } - } - - public @NotNull String getDisplayName() throws NamelessException { - return this.getUserInfo().get("displayname").getAsString(); - } - - /** - * @return The date the user registered on the website. - */ - public @NotNull Date getRegisteredDate() throws NamelessException { - return new Date(this.getUserInfo().get("registered_timestamp").getAsLong() * 1000); - } - - public @NotNull Date getLastOnline() throws NamelessException { - return new Date(this.getUserInfo().get("last_online_timestamp").getAsLong() * 1000); - } - - /** - * @return Whether this account is banned from the website. - */ - public boolean isBanned() throws NamelessException { - return this.getUserInfo().get("banned").getAsBoolean(); - } - - public boolean isVerified() throws NamelessException { - return this.getUserInfo().get("validated").getAsBoolean(); - } - - @Override - public @NotNull String getLanguage() throws NamelessException { - return this.getUserInfo().get("language").getAsString(); - } - - /** - * Get POSIX code for user language (uses lookup table) - * @return Language code or null if the user's language does not exist in our lookup table - */ - @Override - public @Nullable String getLanguagePosix() throws NamelessException { - return LanguageCodeMap.getLanguagePosix(this.getLanguage()); - } - - public @NotNull VerificationInfo getVerificationInfo() throws NamelessException { - final boolean verified = isVerified(); - final JsonObject verification = this.getUserInfo().getAsJsonObject("verification"); - return new VerificationInfo(verified, verification); - } - - /** - * @return True if the user is member of at least one staff group, otherwise false - */ - public boolean isStaff() throws NamelessException { - JsonArray groups = this.getUserInfo().getAsJsonArray("groups"); - for (JsonElement elem : groups) { - JsonObject group = elem.getAsJsonObject(); - if (group.has("staff") && - group.get("staff").getAsBoolean()) { - return true; - } - } - return false; - } - - /** - * @return Set of user's groups - * @see #getSortedGroups() - */ - public @NotNull Set<@NotNull Group> getGroups() throws NamelessException { - return Collections.unmodifiableSet( - StreamSupport.stream(this.getUserInfo().getAsJsonArray("groups").spliterator(), false) - .map(JsonElement::getAsJsonObject) - .map(Group::new) - .collect(Collectors.toSet())); - } - - /** - * @return List of the user's groups, sorted from low order to high order. - * @see #getGroups() - */ - public @NotNull List<@NotNull Group> getSortedGroups() throws NamelessException { - return Collections.unmodifiableList( - StreamSupport.stream(this.getUserInfo().getAsJsonArray("groups").spliterator(), false) - .map(JsonElement::getAsJsonObject) - .map(Group::new) - .sorted() - .collect(Collectors.toList())); - } - - /** - * Same as doing {@link #getGroups()}.get(0), but with better performance - * since it doesn't need to create and sort a list of group objects. - * Empty if the user is not in any groups. - * - * @return Player's group with the lowest order - */ - public @NotNull Optional<@NotNull Group> getPrimaryGroup() throws NamelessException { - final JsonArray groups = this.getUserInfo().getAsJsonArray("groups"); - if (groups.size() > 0) { - return Optional.of(new Group(groups.get(0).getAsJsonObject())); - } else { - return Optional.empty(); - } - } - - public void addGroups(@NotNull final Group@NotNull ... groups) throws NamelessException { - final JsonObject post = new JsonObject(); - post.add("groups", groupsToJsonArray(groups)); - this.requests.post("users/" + this.getUserTransformer() + "/groups/add", post); - invalidateCache(); // Groups modified, invalidate cache - } - - public void removeGroups(@NotNull final Group@NotNull... groups) throws NamelessException { - final JsonObject post = new JsonObject(); - post.add("groups", groupsToJsonArray(groups)); - this.requests.post("users/" + this.getUserTransformer() + "/groups/add", post); - invalidateCache(); // Groups modified, invalidate cache - } - - private JsonArray groupsToJsonArray(@NotNull final Group@NotNull [] groups) { - final JsonArray array = new JsonArray(); - for (final Group group : groups) { - array.add(group.getId()); - } - return array; - } - - public int getNotificationCount() throws NamelessException { - final JsonObject response = this.requests.get("users/" + this.getUserTransformer() + "/notifications"); - return response.getAsJsonArray("notifications").size(); - } - - public @NotNull List getNotifications() throws NamelessException { - final JsonObject response = this.requests.get("users/" + this.getUserTransformer() + "/notifications"); - - final List notifications = new ArrayList<>(); - response.getAsJsonArray("notifications").forEach((element) -> { - final String message = element.getAsJsonObject().get("message").getAsString(); - final String url = element.getAsJsonObject().get("url").getAsString(); - final NotificationType type = NotificationType.fromString(element.getAsJsonObject().get("type").getAsString()); - notifications.add(new Notification(message, url, type)); - }); - - return notifications; - } - - /** - * Creates a report for a website user - * @param user User to report. Lazy loading possible, only the ID is used. - * @param reason Reason why this player has been reported - * @throws IllegalArgumentException Report reason is too long (>255 characters) - * @throws IllegalArgumentException Report reason is too long (>255 characters) - * @throws NamelessException Unexpected http or api error - * @throws ReportUserBannedException If the user creating this report is banned - * @throws AlreadyHasOpenReportException If the user creating this report already has an open report for this user - * @throws CannotReportSelfException If the user tries to report themselves - */ - public void createReport(@NotNull final NamelessUser user, @NotNull final String reason) - throws NamelessException, ReportUserBannedException, AlreadyHasOpenReportException, CannotReportSelfException { - Objects.requireNonNull(user, "User to report is null"); - Objects.requireNonNull(reason, "Report reason is null"); - Preconditions.checkArgument(reason.length() < 255, - "Report reason too long, it's %s characters but must be less than 255", reason.length()); - final JsonObject post = new JsonObject(); - post.addProperty("reporter", this.getId()); - post.addProperty("reported", user.getId()); - post.addProperty("content", reason); - try { - this.requests.post("reports/create", post); - } catch (final ApiError e) { - if (e.getError() == ApiError.USER_CREATING_REPORT_BANNED) { - throw new ReportUserBannedException(); - } else if (e.getError() == ApiError.REPORT_CONTENT_TOO_LARGE) { - throw new IllegalStateException("Website said report reason is too long, but we have client-side validation for this"); - } else if (e.getError() == ApiError.USER_ALREADY_HAS_OPEN_REPORT) { - throw new AlreadyHasOpenReportException(); - } else if (e.getError() == ApiError.CANNOT_REPORT_YOURSELF) { - throw new CannotReportSelfException(); - } else { - throw e; - } - } - } - - /** - * Create a report for a user who may or may not have a website account - * @param reportedUuid The Mojang UUID of the Minecraft player to report - * @param reportedName The Minecraft username of this player - * @param reason Report reason - * @throws IllegalArgumentException Report reason is too long (>255 characters) - * @throws NamelessException Unexpected http or api error - * @throws ReportUserBannedException If the user creating this report is banned - * @throws AlreadyHasOpenReportException If the user creating this report already has an open report for this user - * @throws CannotReportSelfException If the user tries to report themselves - */ - public void createReport(final @NotNull UUID reportedUuid, - final @NotNull String reportedName, - final @NotNull String reason) - throws NamelessException, ReportUserBannedException, AlreadyHasOpenReportException, CannotReportSelfException { - Objects.requireNonNull(reportedUuid, "Reported uuid is null"); - Objects.requireNonNull(reportedName, "Reported name is null"); - Objects.requireNonNull(reason, "Report reason is null"); - Preconditions.checkArgument(reason.length() < 255, - "Report reason too long, it's %s characters but must be less than 255", reason.length()); - final JsonObject post = new JsonObject(); - post.addProperty("reporter", this.getId()); - post.addProperty("reported_uid", reportedUuid.toString()); - post.addProperty("reported_username", reportedName); - post.addProperty("content", reason); - try { - this.requests.post("reports/create", post); - } catch (final ApiError e) { - if (e.getError() == ApiError.USER_CREATING_REPORT_BANNED) { - throw new ReportUserBannedException(); - } else if (e.getError() == ApiError.REPORT_CONTENT_TOO_LARGE) { - throw new IllegalStateException("Website said report reason is too long, but we have client-side validation for this"); - } else if (e.getError() == ApiError.USER_ALREADY_HAS_OPEN_REPORT) { - throw new AlreadyHasOpenReportException(); - } else if (e.getError() == ApiError.CANNOT_REPORT_YOURSELF) { - throw new CannotReportSelfException(); - } else { - throw e; - } - } - } - - public void setDiscordRoles(final long@NotNull[] roleIds) throws NamelessException { - final JsonObject post = new JsonObject(); - post.addProperty("user", this.getId()); - post.add("roles", NamelessAPI.GSON.toJsonTree(roleIds)); - this.requests.post("discord/set-roles", post); - } - - /** - * Get announcements visible to this user - * @return List of announcements visible to this user - */ - @NotNull - public List<@NotNull Announcement> getAnnouncements() throws NamelessException { - final JsonObject response = this.requests.get("users/" + this.getUserTransformer() + "/announcements"); - return NamelessAPI.getAnnouncements(response); - } - - /** - * Ban this user - * @since 2021-10-24 commit cce8d262b0be3f70818c188725cd7e7fc4fdbb9a - */ - public void banUser() throws NamelessException { - this.requests.post("users/" + this.getUserTransformer() + "/ban", null); - } - - public @NotNull Collection<@NotNull CustomProfileFieldValue> getProfileFields() throws NamelessException { - if (!this.getUserInfo().has("profile_fields")) { - return Collections.emptyList(); - } - - final JsonObject fieldsJson = this.getUserInfo().getAsJsonObject("profile_fields"); - final List fieldValues = new ArrayList<>(fieldsJson.size()); - for (final Map.Entry e : fieldsJson.entrySet()) { - int id = Integer.parseInt(e.getKey()); - final JsonObject values = e.getValue().getAsJsonObject(); - fieldValues.add(new CustomProfileFieldValue( - new CustomProfileField( - id, - values.get("name").getAsString(), - CustomProfileFieldType.fromNamelessTypeInt(values.get("type").getAsInt()), - values.get("public").getAsBoolean(), - values.get("required").getAsBoolean(), - values.get("description").getAsString() - ), - values.get("value").getAsString() - )); - } - - return fieldValues; - } - -} diff --git a/src/com/namelessmc/java_api/NamelessVersion.java b/src/com/namelessmc/java_api/NamelessVersion.java deleted file mode 100644 index d4b56553..00000000 --- a/src/com/namelessmc/java_api/NamelessVersion.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.namelessmc.java_api; - -import com.namelessmc.java_api.exception.UnknownNamelessVersionException; -import org.jetbrains.annotations.NotNull; - -import java.util.*; - -public enum NamelessVersion { - - V2_0_0_PR_7("2.0.0-pr7", "2.0.0 pre-release 7", 2, 0, true), - V2_0_0_PR_8("2.0.0-pr8", "2.0.0 pre-release 8", 2, 0, true), - V2_0_0_PR_9("2.0.0-pr9", "2.0.0 pre-release 9", 2, 0, true), - V2_0_0_PR_10("2.0.0-pr10", "2.0.0 pre-release 10", 2, 0, true), - V2_0_0_PR_11("2.0.0-pr11", "2.0.0 pre-release 11", 2, 0, true), - V2_0_0_PR_12("2.0.0-pr12", "2.0.0 pre-release 12", 2, 0, true), - V2_0_0_PR_13("2.0.0-pr13", "2.0.0 pre-release 13", 2, 0, true), - - ; - - private static final Set SUPPORTED_VERSIONS = EnumSet.of( - // Actually, only pr13 is supported but pr13 development releases still report as pr12 - V2_0_0_PR_12, - V2_0_0_PR_13 - ); - - private final @NotNull String name; - private final @NotNull String friendlyName; - private final int major; - private final int minor; - private final boolean isBeta; - - @SuppressWarnings("SameParameterValue") - NamelessVersion(@NotNull final String name, @NotNull String friendlyName, final int major, final int minor, final boolean isBeta) { - this.name = name; - this.friendlyName = friendlyName; - this.major = major; - this.minor = minor; - this.isBeta = isBeta; - } - - public @NotNull String getName() { - return this.name; - } - - public @NotNull String getFriendlyName() { - return this.friendlyName; - } - - public int getMajor() { - return this.major; - } - - public int getMinor() { - return this.minor; - } - - /** - * @return True if this version is a release candidate, pre-release, beta, alpha. - */ - public boolean isBeta() { - return this.isBeta; - } - - @Override - public String toString() { - return this.friendlyName; - } - - private static final Map BY_NAME = new HashMap<>(); - - static { - for (final NamelessVersion version : values()) { - BY_NAME.put(version.getName(), version); - } - } - - public static @NotNull NamelessVersion parse(@NotNull final String versionName) throws UnknownNamelessVersionException { - Objects.requireNonNull(versionName, "Version name is null"); - final NamelessVersion version = BY_NAME.get(versionName); - if (version == null) { - throw new UnknownNamelessVersionException(versionName); - } - return version; - } - - /** - * @return List of NamelessMC versions supported by the Java API - */ - public static Set getSupportedVersions() { - return SUPPORTED_VERSIONS; - } - - /** - * @param version A version to check - * @return Whether the provided NamelessMC version is supported by this Java API library. - */ - public static boolean isSupportedByJavaApi(final NamelessVersion version) { - return SUPPORTED_VERSIONS.contains(version); - } - -} diff --git a/src/com/namelessmc/java_api/Notification.java b/src/com/namelessmc/java_api/Notification.java deleted file mode 100644 index 83c39a2d..00000000 --- a/src/com/namelessmc/java_api/Notification.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.namelessmc.java_api; - -public class Notification { - - private final String message; - private final String url; - private final NotificationType type; - - public Notification(final String message, final String url, final NotificationType type) { - this.message = message; - this.url = url; - this.type = type; - } - - public String getMessage() { - return this.message; - } - - public String getUrl() { - return this.url; - } - - public NotificationType getType() { - return this.type; - } - - public enum NotificationType { - - TAG, - MESSAGE, - LIKE, - PROFILE_COMMENT, - COMMENT_REPLY, - THREAD_REPLY, - FOLLOW, - - UNKNOWN; - - public static NotificationType fromString(final String string) { - try { - return NotificationType.valueOf(string.replace('-', '_').toUpperCase()); - } catch (final IllegalArgumentException e) { - return NotificationType.UNKNOWN; - } - } - - } - -} diff --git a/src/com/namelessmc/java_api/RequestHandler.java b/src/com/namelessmc/java_api/RequestHandler.java deleted file mode 100644 index 871546a8..00000000 --- a/src/com/namelessmc/java_api/RequestHandler.java +++ /dev/null @@ -1,225 +0,0 @@ -package com.namelessmc.java_api; - -import com.google.common.base.Ascii; -import com.google.common.base.Preconditions; -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.JsonSyntaxException; -import com.namelessmc.java_api.exception.ApiDisabledException; -import com.namelessmc.java_api.logger.ApiLogger; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URL; -import java.net.URLEncoder; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.Arrays; -import java.util.Objects; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -public class RequestHandler { - - private final @NotNull URL baseUrl; - private final @NotNull String apiKey; - private final @NotNull HttpClient httpClient; - private final @NotNull String userAgent; - private final @Nullable ApiLogger debugLogger; - private final @Nullable Duration timeout; - private final @NotNull Gson gson; - - RequestHandler(final @NotNull URL baseUrl, - final @NotNull String apiKey, - final @NotNull HttpClient httpClient, - final @NotNull Gson gson, - final @NotNull String userAgent, - final @Nullable ApiLogger debugLogger, - final @Nullable Duration timeout) { - this.baseUrl = Objects.requireNonNull(baseUrl, "Base URL is null"); - this.apiKey = Objects.requireNonNull(apiKey, "Api key is null"); - this.httpClient = Objects.requireNonNull(httpClient, "http client is null"); - this.gson = gson; - this.userAgent = Objects.requireNonNull(userAgent, "User agent is null"); - this.debugLogger = debugLogger; - this.timeout = timeout; - } - - public @NotNull URL getApiUrl() { - return this.baseUrl; - } - - public @NotNull String getApiKey() { - return this.apiKey; - } - - public @NotNull JsonObject post(final @NotNull String route, final @Nullable JsonObject postData) throws NamelessException { - Preconditions.checkArgument(!route.startsWith("/"), "Route must not start with a slash"); - URI uri = URI.create(this.baseUrl + "/" + route); - return makeConnection(uri, postData); - } - - public @NotNull JsonObject get(final @NotNull String route, final @NotNull Object @NotNull... parameters) throws NamelessException { - Preconditions.checkArgument(!route.startsWith("/"), "Route must not start with a slash"); - - final StringBuilder urlBuilder = new StringBuilder(this.baseUrl.toString()); - urlBuilder.append("/"); - urlBuilder.append(route); - - if (parameters.length > 0) { - if (parameters.length % 2 != 0) { - final String paramString = Arrays.stream(parameters).map(Object::toString).collect(Collectors.joining("|")); - throw new IllegalArgumentException(String.format("Parameter string varargs array length must be even (length is %s - %s)", parameters.length, paramString)); - } - - for (int i = 0; i < parameters.length; i++) { - if (i % 2 == 0) { - urlBuilder.append("&"); - urlBuilder.append(parameters[i]); - } else { - urlBuilder.append("="); - try { - urlBuilder.append(URLEncoder.encode(parameters[i].toString(), StandardCharsets.UTF_8.toString())); - } catch (final UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - } - } - } - - final @NotNull URI uri = URI.create(urlBuilder.toString()); - return makeConnection(uri, null); - } - - private void debug(final @NotNull String message, @NotNull Supplier argsSupplier) { - if (this.debugLogger != null) { - this.debugLogger.log(String.format(message, argsSupplier.get())); - } - } - - private @NotNull JsonObject makeConnection(final URI uri, final @Nullable JsonObject postBody) throws NamelessException { - HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(uri); - if (timeout != null) { - reqBuilder.timeout(timeout); - } - - debug("Making connection %s to url %s", () -> new Object[]{ postBody != null ? "POST" : "GET", uri}); - - if (postBody != null) { - byte[] postBytes = gson.toJson(postBody).getBytes(StandardCharsets.UTF_8); - reqBuilder.POST(HttpRequest.BodyPublishers.ofByteArray(postBytes)); - reqBuilder.header("Content-Type", "application/json"); - - debug("Post body below\n-----------------\n%s\n-----------------", - () -> new Object[] { new String(postBytes, StandardCharsets.UTF_8) }); - } else { - reqBuilder.GET(); - } - - reqBuilder.header("User-Agent", this.userAgent); - reqBuilder.header("X-API-Key", this.apiKey); - - HttpRequest httpRequest = reqBuilder.build(); - - int statusCode; - String responseBody; - try { - HttpResponse httpResponse = httpClient.send(httpRequest, - HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); - statusCode = httpResponse.statusCode(); - responseBody = httpResponse.body(); - } catch (final IOException e) { - final StringBuilder message = new StringBuilder("Network connection error (not a Nameless issue)."); - if (e.getMessage().contains("unable to find valid certification path to requested target")) { - message.append("\n HINT: Your certificate is invalid or incomplete. Ensure your website uses a valid *full chain* SSL/TLS certificate."); - } - message.append(" IOException: "); - message.append(e.getMessage()); - throw new NamelessException(message.toString(), e); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - debug("Website response below\n-----------------\n%s\n-----------------", - () -> new Object[] { regularAsciiOnly(responseBody) }); - - if (responseBody.length() == 0) { - throw new NamelessException("Website sent empty response with status code " + statusCode); - } - - if (responseBody.equals("API is disabled")) { - throw new ApiDisabledException(); - } - - JsonObject json; - - try { - json = JsonParser.parseString(responseBody).getAsJsonObject(); - } catch (final JsonSyntaxException | IllegalStateException e) { - StringBuilder message = new StringBuilder(); - message.append("Website returned invalid response with code "); - message.append(statusCode); - message.append(".\n"); - if (statusCode >= 301 && statusCode <= 303) { - message.append("HINT: The URL results in a redirect. If your URL uses http://, change to https://. If your website forces www., make sure to add www. to the url.\n"); - } else if (statusCode == 520 || statusCode == 521) { - message.append("HINT: Status code 520/521 is sent by CloudFlare when the backend webserver is down or having issues.\n"); - } else if (responseBody.contains("/aes.js")) { - message.append("HINT: It looks like requests are being blocked by your web server or a proxy. "); - message.append("This is a common occurrence with free web hosting services; they usually don't allow API access.\n"); - } else if (responseBody.contains("Please Wait... | Cloudflare")) { - message.append("HINT: CloudFlare is blocking our request. Please see https://docs.namelessmc.com/cloudflare-apis\n"); - } else if (responseBody.startsWith("\ufeff")) { - message.append("HINT: The website response contains invisible unicode characters.\n"); - } - - message.append("Website response:\n"); - message.append("-----------------\n"); - int totalLengthLimit = 1950; // fit in a Discord message with safety margin - String printableResponse = regularAsciiOnly(responseBody); - message.append(Ascii.truncate(printableResponse, totalLengthLimit - printableResponse.length(), "[truncated]\n")); - if (message.charAt(message.length()) != '\n') { - message.append('\n'); - } - - throw new NamelessException(message.toString(), e); - } - - if (!json.has("error")) { - throw new NamelessException("Unexpected response from website (missing json key 'error')"); - } - - if (json.get("error").getAsBoolean()) { - @Nullable String meta = null; - if (json.has("meta") && !json.get("meta").isJsonNull()) { - meta = json.get("meta").toString(); - } - throw new ApiError(json.get("code").getAsInt(), meta); - } - - return json; - } - - private static @NotNull String regularAsciiOnly(@NotNull String message) { - char[] chars = message.toCharArray(); - for (int i = 0; i < chars.length; i++) { - char c = chars[i]; - // only allow standard symbols, letters, numbers - // look up an ascii table if you don't understand this if statement - if (c >= ' ' && c <= '~' || c == '\n') { - chars[i] = c; - } else { - chars[i] = '.'; - } - } - return new String(chars); - } - -} diff --git a/src/com/namelessmc/java_api/UserNotExistException.java b/src/com/namelessmc/java_api/UserNotExistException.java deleted file mode 100644 index 88ce6ed5..00000000 --- a/src/com/namelessmc/java_api/UserNotExistException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.namelessmc.java_api; - -public class UserNotExistException extends NamelessException { - - private static final long serialVersionUID = 1L; - -} diff --git a/src/com/namelessmc/java_api/Website.java b/src/com/namelessmc/java_api/Website.java deleted file mode 100644 index 57d324ef..00000000 --- a/src/com/namelessmc/java_api/Website.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.namelessmc.java_api; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.namelessmc.java_api.exception.UnknownNamelessVersionException; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Objects; -import java.util.Optional; -import java.util.stream.StreamSupport; - -public class Website implements LanguageEntity { - - - private final @NotNull String version; - private final @Nullable Update update; - private final @NotNull String@NotNull[] modules; - private final @NotNull String language; - - Website(@NotNull final JsonObject json) { - Objects.requireNonNull(json, "Provided json object is null"); - - this.version = json.get("nameless_version").getAsString(); - - this.modules = StreamSupport.stream(json.get("modules").getAsJsonArray().spliterator(), false) - .map(JsonElement::getAsString) - .toArray(String[]::new); - - if (json.has("version_update")) { - final JsonObject updateJson = json.get("version_update").getAsJsonObject(); - final boolean updateAvailable = updateJson.get("update").getAsBoolean(); - if (updateAvailable) { - final String updateVersion = updateJson.get("version").getAsString(); - final boolean isUrgent = updateJson.get("urgent").getAsBoolean(); - this.update = new Update(isUrgent, updateVersion); - } else { - this.update = null; - } - } else { - this.update = null; - } - - this.language = json.get("language").getAsString(); - } - - @NotNull - public String getVersion() { - return this.version; - } - - @NotNull - public NamelessVersion getParsedVersion() throws UnknownNamelessVersionException { - return NamelessVersion.parse(this.version); - } - - /** - * @return Information about an update, or empty if no update is available. - */ - public @NotNull Optional<@NotNull Update> getUpdate() { - return Optional.ofNullable(this.update); - } - - public @NotNull String@NotNull [] getModules() { - return this.modules; - } - - @Override - public @NotNull String getLanguage() { - return this.language; - } - - /** - * Get POSIX code for website language (uses lookup table) - * @return Language code or null if the website's language does not exist in our lookup table - */ - @Override - public @Nullable String getLanguagePosix() { - return LanguageCodeMap.getLanguagePosix(this.language); - } - - public static class Update { - - private final boolean isUrgent; - private final @NotNull String version; - - Update(final boolean isUrgent, @NotNull final String version) { - this.isUrgent = isUrgent; - this.version = version; - } - - public boolean isUrgent() { - return this.isUrgent; - } - - @NotNull - public String getVersion() { - return this.version; - } - - public NamelessVersion getParsedVersion() throws UnknownNamelessVersionException { - return NamelessVersion.parse(this.version); - } - - } - -} diff --git a/src/com/namelessmc/java_api/exception/AccountAlreadyActivatedException.java b/src/com/namelessmc/java_api/exception/AccountAlreadyActivatedException.java deleted file mode 100644 index fe2bf576..00000000 --- a/src/com/namelessmc/java_api/exception/AccountAlreadyActivatedException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.namelessmc.java_api.exception; - -import com.namelessmc.java_api.ApiError; - -public class AccountAlreadyActivatedException extends ApiErrorException { - - private static final long serialVersionUID = 1L; - - public AccountAlreadyActivatedException() { - super(ApiError.ACCOUNT_ALREADY_ACTIVATED); - } - -} diff --git a/src/com/namelessmc/java_api/exception/AlreadyHasOpenReportException.java b/src/com/namelessmc/java_api/exception/AlreadyHasOpenReportException.java deleted file mode 100644 index 96b7130b..00000000 --- a/src/com/namelessmc/java_api/exception/AlreadyHasOpenReportException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.namelessmc.java_api.exception; - -import com.namelessmc.java_api.ApiError; - -public class AlreadyHasOpenReportException extends ApiErrorException { - - private static final long serialVersionUID = 1L; - - public AlreadyHasOpenReportException() { - super(ApiError.USER_ALREADY_HAS_OPEN_REPORT); - } - -} diff --git a/src/com/namelessmc/java_api/exception/ApiDisabledException.java b/src/com/namelessmc/java_api/exception/ApiDisabledException.java deleted file mode 100644 index 85c1f293..00000000 --- a/src/com/namelessmc/java_api/exception/ApiDisabledException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.namelessmc.java_api.exception; - -import com.namelessmc.java_api.NamelessException; - -public class ApiDisabledException extends NamelessException { - - public ApiDisabledException() { - super("API is disabled, please enable it in StaffCP > Configuration > API"); - } - -} diff --git a/src/com/namelessmc/java_api/exception/ApiErrorException.java b/src/com/namelessmc/java_api/exception/ApiErrorException.java deleted file mode 100644 index 7cec8361..00000000 --- a/src/com/namelessmc/java_api/exception/ApiErrorException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.namelessmc.java_api.exception; - -public class ApiErrorException extends Exception { - - private static final long serialVersionUID = 1L; - - public ApiErrorException(final int code) { - super("API error code " + code); - } - -} diff --git a/src/com/namelessmc/java_api/exception/CannotReportSelfException.java b/src/com/namelessmc/java_api/exception/CannotReportSelfException.java deleted file mode 100644 index 6abd3dc7..00000000 --- a/src/com/namelessmc/java_api/exception/CannotReportSelfException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.namelessmc.java_api.exception; - -import com.namelessmc.java_api.ApiError; - -public class CannotReportSelfException extends ApiErrorException { - - private static final long serialVersionUID = 1L; - - public CannotReportSelfException() { - super(ApiError.CANNOT_REPORT_YOURSELF); - } - -} diff --git a/src/com/namelessmc/java_api/exception/CannotSendEmailException.java b/src/com/namelessmc/java_api/exception/CannotSendEmailException.java deleted file mode 100644 index e24b003b..00000000 --- a/src/com/namelessmc/java_api/exception/CannotSendEmailException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.namelessmc.java_api.exception; - -import com.namelessmc.java_api.ApiError; - -public class CannotSendEmailException extends ApiErrorException { - - private static final long serialVersionUID = 1L; - - public CannotSendEmailException() { - super(ApiError.UNABLE_TO_SEND_REGISTRATION_EMAIL); - } - -} diff --git a/src/com/namelessmc/java_api/exception/InvalidUsernameException.java b/src/com/namelessmc/java_api/exception/InvalidUsernameException.java deleted file mode 100644 index 1fdcf121..00000000 --- a/src/com/namelessmc/java_api/exception/InvalidUsernameException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.namelessmc.java_api.exception; - -import com.namelessmc.java_api.ApiError; - -public class InvalidUsernameException extends ApiErrorException { - - private static final long serialVersionUID = 1L; - - public InvalidUsernameException() { - super(ApiError.INVALID_USERNAME); - } - -} diff --git a/src/com/namelessmc/java_api/exception/InvalidValidateCodeException.java b/src/com/namelessmc/java_api/exception/InvalidValidateCodeException.java deleted file mode 100644 index 20389f9f..00000000 --- a/src/com/namelessmc/java_api/exception/InvalidValidateCodeException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.namelessmc.java_api.exception; - -import com.namelessmc.java_api.ApiError; - -public class InvalidValidateCodeException extends ApiErrorException { - - private static final long serialVersionUID = 1L; - - public InvalidValidateCodeException() { - super(ApiError.INVALID_VALIDATE_CODE); - } - -} diff --git a/src/com/namelessmc/java_api/exception/ReportUserBannedException.java b/src/com/namelessmc/java_api/exception/ReportUserBannedException.java deleted file mode 100644 index dbf8996a..00000000 --- a/src/com/namelessmc/java_api/exception/ReportUserBannedException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.namelessmc.java_api.exception; - -import com.namelessmc.java_api.ApiError; - -public class ReportUserBannedException extends ApiErrorException { - - private static final long serialVersionUID = 1L; - - public ReportUserBannedException() { - super(ApiError.USER_CREATING_REPORT_BANNED); - } - -} diff --git a/src/com/namelessmc/java_api/exception/UnknownNamelessVersionException.java b/src/com/namelessmc/java_api/exception/UnknownNamelessVersionException.java deleted file mode 100644 index 0ec6844c..00000000 --- a/src/com/namelessmc/java_api/exception/UnknownNamelessVersionException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.namelessmc.java_api.exception; - -public class UnknownNamelessVersionException extends Exception { - - private static final long serialVersionUID = 1L; - - public UnknownNamelessVersionException(final String versionString) { - super("Cannot parse version string '" + versionString + "'. Try updating the API or the software using it."); - } - -} diff --git a/src/com/namelessmc/java_api/exception/UsernameAlreadyExistsException.java b/src/com/namelessmc/java_api/exception/UsernameAlreadyExistsException.java deleted file mode 100644 index 47559717..00000000 --- a/src/com/namelessmc/java_api/exception/UsernameAlreadyExistsException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.namelessmc.java_api.exception; - -import com.namelessmc.java_api.ApiError; - -public class UsernameAlreadyExistsException extends ApiErrorException { - - private static final long serialVersionUID = 1L; - - public UsernameAlreadyExistsException() { - super(ApiError.USERNAME_ALREADY_EXISTS); - } - -} diff --git a/src/com/namelessmc/java_api/exception/UuidAlreadyExistsException.java b/src/com/namelessmc/java_api/exception/UuidAlreadyExistsException.java deleted file mode 100644 index 27459c70..00000000 --- a/src/com/namelessmc/java_api/exception/UuidAlreadyExistsException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.namelessmc.java_api.exception; - -import com.namelessmc.java_api.ApiError; - -public class UuidAlreadyExistsException extends ApiErrorException { - - private static final long serialVersionUID = 1L; - - public UuidAlreadyExistsException() { - super(ApiError.UUID_ALREADY_EXISTS); - } - -} diff --git a/src/com/namelessmc/java_api/modules/websend/WebsendCommand.java b/src/com/namelessmc/java_api/modules/websend/WebsendCommand.java deleted file mode 100644 index 05e3c21b..00000000 --- a/src/com/namelessmc/java_api/modules/websend/WebsendCommand.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.namelessmc.java_api.modules.websend; - -import org.jetbrains.annotations.NotNull; - -public class WebsendCommand { - - private final int id; - private final @NotNull String commandLine; - - public WebsendCommand(final int id, - final @NotNull String commandLine) { - this.id = id; - this.commandLine = commandLine; - } - - public int getId() { - return id; - } - - public @NotNull String getCommandLine() { - return this.commandLine; - } - -} diff --git a/src/com/namelessmc/java_api/Announcement.java b/src/main/java/com/namelessmc/java_api/Announcement.java similarity index 63% rename from src/com/namelessmc/java_api/Announcement.java rename to src/main/java/com/namelessmc/java_api/Announcement.java index 69e197fb..e2286454 100644 --- a/src/com/namelessmc/java_api/Announcement.java +++ b/src/main/java/com/namelessmc/java_api/Announcement.java @@ -2,7 +2,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import org.jetbrains.annotations.NotNull; +import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Collections; import java.util.Set; @@ -12,12 +12,12 @@ public class Announcement { private final int id; - private final @NotNull String header; - private final @NotNull String message; - private final @NotNull Set<@NotNull String> displayPages; - private final int @NotNull[] displayGroups; + private final @NonNull String header; + private final @NonNull String message; + private final @NonNull Set<@NonNull String> displayPages; + private final int @NonNull[] displayGroups; - Announcement(@NotNull JsonObject announcementJson) { + Announcement(@NonNull JsonObject announcementJson) { this.id = announcementJson.get("id").getAsInt(); this.header = announcementJson.get("header").getAsString(); this.message = announcementJson.get("message").getAsString(); @@ -31,28 +31,23 @@ public class Announcement { .toArray(); } - public int getId() { + public int id() { return id; } - public @NotNull String getHeader() { + public @NonNull String header() { return this.header; } - public @NotNull String getMessage() { + public @NonNull String message() { return this.message; } - @Deprecated - public @NotNull String getContent() { - return this.message; - } - - public @NotNull Set<@NotNull String> getDisplayPages() { + public @NonNull Set<@NonNull String> displayedPages() { return this.displayPages; } - public int @NotNull[] getDisplayGroupIds() { + public int @NonNull[] displayedGroupIds() { return this.displayGroups; } diff --git a/src/com/namelessmc/java_api/CustomProfileField.java b/src/main/java/com/namelessmc/java_api/CustomProfileField.java similarity index 58% rename from src/com/namelessmc/java_api/CustomProfileField.java rename to src/main/java/com/namelessmc/java_api/CustomProfileField.java index acac403d..5b989448 100644 --- a/src/com/namelessmc/java_api/CustomProfileField.java +++ b/src/main/java/com/namelessmc/java_api/CustomProfileField.java @@ -1,24 +1,25 @@ package com.namelessmc.java_api; -import org.jetbrains.annotations.NotNull; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Objects; public class CustomProfileField { private final int id; - private final @NotNull String name; - private final @NotNull CustomProfileFieldType type; + private final @NonNull String name; + private final @NonNull CustomProfileFieldType type; private final boolean isPublic; private final boolean isRequired; - private final @NotNull String description; + private final @NonNull String description; CustomProfileField(final int id, - final @NotNull String name, - final @NotNull CustomProfileFieldType type, + final @NonNull String name, + final @NonNull CustomProfileFieldType type, final boolean isPublic, final boolean isRequired, - final @NotNull String description) { + final @NonNull String description) { this.id = id; this.name = name; this.type = type; @@ -27,15 +28,15 @@ public class CustomProfileField { this.description = description; } - public int getId() { + public int id() { return id; } - public @NotNull String getName() { + public @NonNull String name() { return name; } - public @NotNull CustomProfileFieldType getType() { + public @NonNull CustomProfileFieldType type() { return type; } @@ -47,12 +48,12 @@ public boolean isRequired() { return isRequired; } - public @NotNull String getDescription() { + public @NonNull String description() { return description; } @Override - public boolean equals(Object other) { + public boolean equals(final @Nullable Object other) { return other instanceof CustomProfileField && ((CustomProfileField) other).id == this.id; } diff --git a/src/com/namelessmc/java_api/CustomProfileFieldType.java b/src/main/java/com/namelessmc/java_api/CustomProfileFieldType.java similarity index 100% rename from src/com/namelessmc/java_api/CustomProfileFieldType.java rename to src/main/java/com/namelessmc/java_api/CustomProfileFieldType.java diff --git a/src/main/java/com/namelessmc/java_api/CustomProfileFieldValue.java b/src/main/java/com/namelessmc/java_api/CustomProfileFieldValue.java new file mode 100644 index 00000000..91360aae --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/CustomProfileFieldValue.java @@ -0,0 +1,24 @@ +package com.namelessmc.java_api; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class CustomProfileFieldValue { + + private final @NonNull CustomProfileField field; + private final @Nullable String value; + + CustomProfileFieldValue(@NonNull CustomProfileField field, @Nullable String value) { + this.field = field; + this.value = value; + } + + public @NonNull CustomProfileField field() { + return this.field; + } + + public @Nullable String value() { + return value; + } + +} diff --git a/src/main/java/com/namelessmc/java_api/FilteredUserListBuilder.java b/src/main/java/com/namelessmc/java_api/FilteredUserListBuilder.java new file mode 100644 index 00000000..5ab83250 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/FilteredUserListBuilder.java @@ -0,0 +1,78 @@ +package com.namelessmc.java_api; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.namelessmc.java_api.exception.NamelessException; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.*; + +public class FilteredUserListBuilder { + + private final @NonNull NamelessAPI api; + private @Nullable Map, Object> filters; + private @NonNull String operator = "AND"; + + FilteredUserListBuilder(@NonNull NamelessAPI api) { + this.api = api; + } + + public @NonNull FilteredUserListBuilder withFilter(final @NonNull UserFilter filter, + final @NonNull T value) { + if (filters == null) { + filters = new HashMap<>(); + } + + filters.put( + Objects.requireNonNull(filter, "Filter is null"), + Objects.requireNonNull(value, "Value for filter " + filter.name() + " is null") + ); + return this; + } + + public @NonNull FilteredUserListBuilder all() { + this.operator = "AND"; + return this; + } + + public @NonNull FilteredUserListBuilder any() { + this.operator = "OR"; + return this; + } + + public JsonObject makeRawRequest() throws NamelessException { + final Object[] parameters; + if (filters != null) { + int filterCount = filters.size(); + parameters = new Object[2 + 4 + filterCount * 2]; + int i = 2; + parameters[i++] = "operator"; + parameters[i++] = operator; + parameters[i++] = "limit"; + parameters[i++] = 0; + for (Map.Entry, Object> filter : this.filters.entrySet()) { + parameters[i++] = filter.getKey().name(); + parameters[i++] = filter.getValue(); + } + } else { + parameters = new Object[2]; + } + + parameters[0] = "groups"; // Request NamelessMC to include groups in response + + return this.api.requests().get("users", parameters); + } + + public @NonNull List<@NonNull NamelessUser> makeRequest() throws NamelessException { + final JsonObject response = this.makeRawRequest(); + final JsonArray array = response.getAsJsonArray("users"); + final List users = new ArrayList<>(array.size()); + for (final JsonElement e : array) { + users.add(new NamelessUser(this.api, e.getAsJsonObject())); + } + return Collections.unmodifiableList(users); + } + +} diff --git a/src/com/namelessmc/java_api/Group.java b/src/main/java/com/namelessmc/java_api/Group.java similarity index 74% rename from src/com/namelessmc/java_api/Group.java rename to src/main/java/com/namelessmc/java_api/Group.java index e7631fcb..8451a60e 100644 --- a/src/com/namelessmc/java_api/Group.java +++ b/src/main/java/com/namelessmc/java_api/Group.java @@ -1,20 +1,19 @@ package com.namelessmc.java_api; -import org.jetbrains.annotations.NotNull; - import com.google.gson.JsonObject; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Objects; public class Group implements Comparable { private final int id; - @NotNull - private final String name; + private final @NonNull String name; private final int order; private final boolean staff; - Group(@NotNull final JsonObject group) { + Group(final @NonNull JsonObject group) { this.id = group.get("id").getAsInt(); this.name = group.get("name").getAsString(); this.order = group.get("order").getAsInt(); @@ -25,8 +24,7 @@ public int getId() { return this.id; } - @NotNull - public String getName() { + public @NonNull String getName() { return this.name; } @@ -44,7 +42,7 @@ public int compareTo(final Group other) { } @Override - public boolean equals(Object other) { + public boolean equals(final @Nullable Object other) { return other instanceof Group && ((Group) other).id == this.id; } @@ -55,7 +53,7 @@ public int hashCode() { } @Override - public String toString() { + public @NonNull String toString() { return "Group[id=" + id + ",name=" + name + "]"; } diff --git a/src/main/java/com/namelessmc/java_api/LanguageEntity.java b/src/main/java/com/namelessmc/java_api/LanguageEntity.java new file mode 100644 index 00000000..a946d4f6 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/LanguageEntity.java @@ -0,0 +1,21 @@ +package com.namelessmc.java_api; + +import com.namelessmc.java_api.exception.NamelessException; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Locale; + +public interface LanguageEntity { + + String rawLocale() throws NamelessException; + + default @NonNull Locale locale() throws NamelessException { + final String language = this.rawLocale(); + final String[] langSplit = language.split("_"); + if (langSplit.length != 2) { + throw new IllegalArgumentException("Invalid language: " + language); + } + return new Locale(langSplit[0], langSplit[1]); + } + +} diff --git a/src/main/java/com/namelessmc/java_api/NamelessAPI.java b/src/main/java/com/namelessmc/java_api/NamelessAPI.java new file mode 100755 index 00000000..1b5d4337 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/NamelessAPI.java @@ -0,0 +1,379 @@ +package com.namelessmc.java_api; + +import java.math.BigInteger; +import java.net.URL; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.namelessmc.java_api.exception.ApiError; +import com.namelessmc.java_api.exception.ApiException; +import com.namelessmc.java_api.exception.MissingModuleException; +import com.namelessmc.java_api.exception.NamelessException; +import com.namelessmc.java_api.integrations.IntegrationData; +import com.namelessmc.java_api.integrations.StandardIntegrationTypes; +import com.namelessmc.java_api.modules.NamelessModule; +import com.namelessmc.java_api.modules.discord.DiscordAPI; +import com.namelessmc.java_api.modules.store.StoreAPI; +import com.namelessmc.java_api.modules.suggestions.SuggestionsAPI; +import com.namelessmc.java_api.modules.websend.WebsendAPI; + +public final class NamelessAPI { + + private final @NonNull RequestHandler requests; + + // Not actually used by the Nameless Java API, but could be useful to applications using it. + private final @NonNull URL apiUrl; + private final @NonNull String apiKey; + + private static final long CACHED_WEBSITE_INFO_VALIDITY = 60_000; + private @Nullable Website cachedWebsiteInfo = null; + private long cachedWebsiteInfoTime = 0; + + NamelessAPI(final @NonNull RequestHandler requests, + final @NonNull URL apiUrl, + final @NonNull String apiKey) { + this.requests = Objects.requireNonNull(requests, "Request handler is null"); + this.apiUrl = apiUrl; + this.apiKey = apiKey; + } + + public @NonNull RequestHandler requests() { + return this.requests; + } + + public @NonNull URL apiUrl() { + return this.apiUrl; + } + + public @NonNull String apiKey() { + return this.apiKey; + } + + /** + * Get announcements visible to guests. Use {@link NamelessUser#announcements()} for non-guest announcements. + * @return List of announcements + */ + + public @NonNull List<@NonNull Announcement> announcements() throws NamelessException { + final JsonObject response = this.requests.get("announcements"); + return announcements(response); + } + + /** + * Convert announcement json to objects + * @param response Announcements json API response + * @return List of {@link Announcement} objects + */ + static @NonNull List<@NonNull Announcement> announcements(final @NonNull JsonObject response) { + return StreamSupport.stream(response.getAsJsonArray("announcements").spliterator(), false) + .map(JsonElement::getAsJsonObject) + .map(Announcement::new) + .collect(Collectors.toList()); + } + + /** + * Send Minecraft server information to the website. Currently, the exact JSON contents are undocumented. + * @param jsonData Json data to submit + */ + public void submitServerInfo(final @NonNull JsonObject jsonData) throws NamelessException { + this.requests.post("minecraft/server-info", jsonData); + } + + /** + * Send Minecraft groups to website. Only available in Nameless 2.1.0+ + * @param groups + * @throws NamelessException + * @deprecated Should use {@link com.namelessmc.java_api.NamelessUser#updateMinecraftGroups} for Nameless 2.2.0+ + */ + @Deprecated + public void sendMinecraftGroups(final int serverId, final Map> groups) throws NamelessException { + final JsonObject groupsJson = new JsonObject(); + final Gson gson = this.requests().gson(); + groups.forEach((uuid, playerGroups) -> { + final JsonObject playerGroupsObject = new JsonObject(); + playerGroupsObject.add("groups", gson.toJsonTree(playerGroups)); + groupsJson.add(javaUuidToWebsiteUuid(uuid), playerGroupsObject); + }); + + final JsonObject body = new JsonObject(); + body.addProperty("server_id", serverId); + body.add("player_groups", groupsJson); + + this.requests.post("minecraft/update-groups", body); + } + + /** + * Get website information + * @return {@link Website} object containing website information + */ + public Website website() throws NamelessException { + if (this.cachedWebsiteInfoTime + CACHED_WEBSITE_INFO_VALIDITY > System.currentTimeMillis() && + this.cachedWebsiteInfo != null) { + return this.cachedWebsiteInfo; + } + + final JsonObject json = this.requests.get("info"); + final Website website = new Website(json); + this.cachedWebsiteInfo = website; + this.cachedWebsiteInfoTime = System.currentTimeMillis(); + return website; + } + + public @Nullable Website websiteIfCached() { + return this.cachedWebsiteInfo; + } + + public FilteredUserListBuilder users() { + return new FilteredUserListBuilder(this); + } + + public @Nullable NamelessUser userAsNullable(NamelessUser user) throws NamelessException { + try { + user.userInfo(); + return user; + } catch (final ApiException e) { + if (e.apiError() == ApiError.NAMELESS_CANNOT_FIND_USER) { + return null; + } + throw e; + } + } + + public @Nullable NamelessUser user(final int id) throws NamelessException { + return this.userAsNullable(this.userLazy(id)); + } + + public @Nullable NamelessUser userByUsername(final @NonNull String username) throws NamelessException { + return this.userAsNullable(this.userByUsernameLazy(username)); + } + + public @Nullable NamelessUser userByMinecraftUuid(final @NonNull UUID uuid) throws NamelessException { + return this.userAsNullable(this.userByMinecraftUuidLazy(uuid)); + } + + public @Nullable NamelessUser userByMinecraftUsername(final @NonNull String username) throws NamelessException { + return this.userAsNullable(this.userByMinecraftUsernameLazy(username)); + } + + public @Nullable NamelessUser userByDiscordId(final long id) throws NamelessException { + return this.userAsNullable(this.userByDiscordIdLazy(id)); + } + + public @Nullable NamelessUser userByDiscordUsername(final @NonNull String username) throws NamelessException { + return this.userAsNullable(this.userByDiscordUsernameLazy(username)); + } + + /** + * Construct a NamelessUser object without making API requests (so without checking if the user exists) + * @param id NamelessMC user id + * @return Nameless user object, never null + */ + public @NonNull NamelessUser userLazy(final int id) { + return new NamelessUser(this, id); + } + + public @NonNull NamelessUser userLazy(final @NonNull String userTransformer) { + return new NamelessUser(this, userTransformer); + } + + public @NonNull NamelessUser userByUsernameLazy(final @NonNull String username) { + return this.userLazy("username:" + username); + } + + public @NonNull NamelessUser userByMinecraftUuidLazy(final @NonNull UUID uuid) { + return this.byIntegrationIdentifierLazy(StandardIntegrationTypes.MINECRAFT, javaUuidToWebsiteUuid(uuid)); + } + + public @NonNull NamelessUser userByMinecraftUsernameLazy(final @NonNull String username) { + return this.byIntegrationUsernameLazy(StandardIntegrationTypes.MINECRAFT, username); + } + + public @NonNull NamelessUser userByDiscordIdLazy(final long id) { + return this.byIntegrationIdentifierLazy(StandardIntegrationTypes.DISCORD, String.valueOf(id)); + } + + public @NonNull NamelessUser userByDiscordUsernameLazy(final @NonNull String username) { + return this.byIntegrationUsernameLazy(StandardIntegrationTypes.DISCORD, username); + } + + public NamelessUser byIntegrationIdentifierLazy(String integrationName, String identifier) { + return this.userLazy("integration_id:" + integrationName + ":" + identifier); + } + + public NamelessUser byIntegrationUsernameLazy(String integrationName, String username) { + return this.userLazy("integration_name:" + integrationName + ":" + username); + } + + /** + * Get NamelessMC group by ID + * @param id Group id + * @return Group or null if it doesn't exist + */ + public @Nullable Group group(final int id) throws NamelessException { + final JsonObject response = this.requests.get("groups", "id", id); + final JsonArray jsonArray = response.getAsJsonArray("groups"); + if (jsonArray.size() == 1) { + return new Group(jsonArray.get(0).getAsJsonObject()); + } else if (jsonArray.isEmpty()) { + return null; + } else { + throw new IllegalStateException("Website returned multiple groups for one id"); + } + } + + /** + * Get NamelessMC groups by name + * @param name NamelessMC groups name + * @return List of groups with this name, empty if there are no groups with this name. + */ + public List group(final @NonNull String name) throws NamelessException { + Objects.requireNonNull(name, "Group name is null"); + final JsonObject response = this.requests.get("groups", "name", name); + return this.groupListFromJsonArray(response.getAsJsonArray("groups")); + } + + /** + * Get a list of all groups on the website + * @return list of groups + */ + public List getAllGroups() throws NamelessException { + final JsonObject response = this.requests.get("groups"); + return this.groupListFromJsonArray(response.getAsJsonArray("groups")); + + } + + public int[] getAllGroupIds() throws NamelessException { + final JsonObject response = this.requests.get("groups"); + return StreamSupport.stream(response.getAsJsonArray("groups").spliterator(), false) + .map(JsonElement::getAsJsonObject) + .mapToInt(o -> o.get("id").getAsInt()) + .toArray(); + } + + private @NonNull List groupListFromJsonArray(final @NonNull JsonArray array) { + return StreamSupport.stream(array.spliterator(), false) + .map(JsonElement::getAsJsonObject) + .map(Group::new) + .collect(Collectors.toList()); + } + + /** + * Registers a new account. The user will be emailed to set a password. + * + * @param username Username (this should match the user's in-game username when specifying a UUID) + * @param email Email address + * @param integrationData Integration data objects. By supplying account information here, the user will + * an account connection will automatically be created without the user needing to + * verify. + * @return Email verification disabled: A link which the user needs to click to complete registration + *
Email verification enabled: An empty string (the user needs to check their email to complete registration) + */ + public Optional registerUser(final @NonNull String username, + final @NonNull String email, + final @NonNull IntegrationData@Nullable ... integrationData) + throws NamelessException { + + Objects.requireNonNull(username, "Username is null"); + Objects.requireNonNull(email, "Email address is null"); + + final JsonObject post = new JsonObject(); + post.addProperty("username", username); + post.addProperty("email", email); + if (integrationData != null && integrationData.length > 0) { + final JsonObject integrationsJson = new JsonObject(); + for (final IntegrationData integration : integrationData) { + final JsonObject integrationJson = new JsonObject(); + integrationJson.addProperty("identifier", integration.identifier()); + integrationJson.addProperty("username", integration.username()); + integrationsJson.add(integration.type().toString(), integrationJson); + } + post.add("integrations", integrationsJson); + } + + final JsonObject response = this.requests.post("users/register", post); + + if (response.has("link")) { + return Optional.of(response.get("link").getAsString()); + } else { + return Optional.empty(); + } + } + + public void verifyIntegration(final @NonNull IntegrationData integrationData, + final @NonNull String verificationCode) throws NamelessException { + final JsonObject data = new JsonObject(); + data.addProperty("integration", integrationData.type()); + data.addProperty("identifier", integrationData.identifier()); + data.addProperty("username", integrationData.username()); + data.addProperty("code", Objects.requireNonNull(verificationCode, "Verification code is null")); + this.requests.post("integration/verify", data); + } + + /** + * Ensures the given module is installed, throwing {@link MissingModuleException} if missing. + * @param module Module to check + * @see NamelessModule + */ + public void ensureModuleInstalled(NamelessModule module) throws NamelessException { + if (!this.website().modules().contains(module)) { + throw new MissingModuleException(module); + } + } + + public DiscordAPI discord() throws NamelessException { + return new DiscordAPI(this); + } + + public StoreAPI store() throws NamelessException { + return new StoreAPI(this); + } + + public SuggestionsAPI suggestions() throws NamelessException { + return new SuggestionsAPI(this); + } + + public WebsendAPI websend() throws NamelessException { + return new WebsendAPI(this); + } + + /** + * Adds back dashes to a UUID string and converts it to a Java UUID object + * @param uuid UUID without dashes + * @return UUID with dashes + */ + public static @NonNull UUID websiteUuidToJavaUuid(final @NonNull String uuid) { + Objects.requireNonNull(uuid, "UUID string is null"); + // Website sends UUIDs without dashes, so we can't use UUID#fromString + // https://stackoverflow.com/a/30760478 + try { + final BigInteger a = new BigInteger(uuid.substring(0, 16), 16); + final BigInteger b = new BigInteger(uuid.substring(16, 32), 16); + return new UUID(a.longValue(), b.longValue()); + } catch (final IndexOutOfBoundsException e) { + throw new IllegalArgumentException("Invalid uuid: '" + uuid + "'", e); + } + } + + public static @NonNull String javaUuidToWebsiteUuid(final @NonNull UUID uuid) { + return uuid.toString().replace("-", ""); + } + + public static @NonNull NamelessApiBuilder builder(final @NonNull URL apiUrl, + final @NonNull String apiKey) { + return new NamelessApiBuilder(apiUrl, apiKey); + } + +} diff --git a/src/main/java/com/namelessmc/java_api/NamelessApiBuilder.java b/src/main/java/com/namelessmc/java_api/NamelessApiBuilder.java new file mode 100644 index 00000000..f2e59727 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/NamelessApiBuilder.java @@ -0,0 +1,146 @@ +package com.namelessmc.java_api; + +import com.github.mizosoft.methanol.Methanol; +import com.google.gson.GsonBuilder; +import com.namelessmc.java_api.logger.ApiLogger; +import com.namelessmc.java_api.logger.PrintStreamLogger; +import com.namelessmc.java_api.logger.Slf4jLogger; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import javax.net.ssl.SSLParameters; +import java.net.Authenticator; +import java.net.MalformedURLException; +import java.net.ProxySelector; +import java.net.URI; +import java.net.URL; +import java.net.http.HttpClient; +import java.time.Duration; +import java.util.Objects; + +public class NamelessApiBuilder { + + private static final SSLParameters SSL_PARAMETERS = new SSLParameters(); + + static { + SSL_PARAMETERS.setProtocols(new String[]{"TLSv1.3", "TLSv1.2"}); + } + + private final @NonNull URL apiUrl; + private final @NonNull String apiKey; + + private Duration timeout = Duration.ofSeconds(10); + private int responseSizeLimit = 32*1024*1024; + private String userAgent = "Nameless-Java-API"; + private @Nullable ApiLogger debugLogger = null; + private @Nullable ProxySelector proxy = null; + private @Nullable Authenticator authenticator = null; + private HttpClient.@Nullable Version httpVersion = null; + + private boolean pettyJsonRequests = false; + + NamelessApiBuilder(final @NonNull URL apiUrl, + final @NonNull String apiKey) { + try { + this.apiUrl = apiUrl.toString().endsWith("/") ? apiUrl : (URI.create(apiUrl + "/").toURL()); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + this.apiKey = apiKey; + } + + public NamelessApiBuilder userAgent(final String userAgent) { + this.userAgent = userAgent; + return this; + } + + public NamelessApiBuilder stdErrDebugLogger() { + this.debugLogger = PrintStreamLogger.DEFAULT_INSTANCE; + return this; + } + + public NamelessApiBuilder slf4jDebugLogger() { + this.debugLogger = Slf4jLogger.DEFAULT_INSTANCE; + return this; + } + + public NamelessApiBuilder customDebugLogger(final @Nullable ApiLogger debugLogger) { + this.debugLogger = debugLogger; + return this; + } + + public NamelessApiBuilder timeout(final Duration timeout) { + this.timeout = Objects.requireNonNull(timeout); + return this; + } + + public NamelessApiBuilder withProxy(final @Nullable ProxySelector proxy) { + this.proxy = proxy; + return this; + } + + public NamelessApiBuilder authenticator(final @Nullable Authenticator authenticator) { + this.authenticator = authenticator; + return this; + } + + public NamelessApiBuilder pettyJsonRequests() { + this.pettyJsonRequests = true; + return this; + } + + public NamelessApiBuilder responseSizeLimit(int responseSizeLimitBytes) { + this.responseSizeLimit = responseSizeLimitBytes; + return this; + } + + public NamelessApiBuilder httpVersion(final HttpClient. @Nullable Version httpVersion) { + this.httpVersion = httpVersion; + return this; + } + + public NamelessAPI build() { + final Methanol.Builder methanolBuilder = Methanol.newBuilder() + .defaultHeaders( + "Authorization", "Bearer " + this.apiKey, + "X-API-Key", this.apiKey + ) + .userAgent(this.userAgent) + .autoAcceptEncoding(true) + .readTimeout(this.timeout) + .requestTimeout(this.timeout) + .connectTimeout(this.timeout) + .headersTimeout(this.timeout); + if (this.proxy != null) { + methanolBuilder.proxy(this.proxy); + } + if (this.authenticator != null) { + methanolBuilder.authenticator(this.authenticator); + } + if (this.httpVersion != null) { + methanolBuilder.version(this.httpVersion); + } + + methanolBuilder.sslParameters(SSL_PARAMETERS); + + GsonBuilder gsonBuilder = new GsonBuilder() + .disableHtmlEscaping(); + + if (this.pettyJsonRequests) { + gsonBuilder.setPrettyPrinting(); + } + + return new NamelessAPI( + new RequestHandler( + this.apiUrl, + methanolBuilder.build(), + gsonBuilder.create(), + this.debugLogger, + this.responseSizeLimit + ), + this.apiUrl, + this.apiKey + ); + } + +} diff --git a/src/main/java/com/namelessmc/java_api/NamelessUser.java b/src/main/java/com/namelessmc/java_api/NamelessUser.java new file mode 100644 index 00000000..51736e31 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/NamelessUser.java @@ -0,0 +1,445 @@ +package com.namelessmc.java_api; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +import org.checkerframework.checker.index.qual.Positive; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import com.google.common.base.Preconditions; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.namelessmc.java_api.exception.ApiError; +import com.namelessmc.java_api.exception.ApiException; +import com.namelessmc.java_api.exception.NamelessException; +import com.namelessmc.java_api.integrations.DetailedDiscordIntegrationData; +import com.namelessmc.java_api.integrations.DetailedIntegrationData; +import com.namelessmc.java_api.integrations.DetailedMinecraftIntegrationData; +import com.namelessmc.java_api.integrations.IDiscordIntegrationData; +import com.namelessmc.java_api.integrations.IMinecraftIntegrationData; +import com.namelessmc.java_api.integrations.IntegrationData; +import com.namelessmc.java_api.integrations.StandardIntegrationTypes; +import com.namelessmc.java_api.modules.discord.DiscordUser; +import com.namelessmc.java_api.modules.store.StoreUser; +import com.namelessmc.java_api.modules.suggestions.SuggestionsUser; +import com.namelessmc.java_api.util.GsonHelper; + +public final class NamelessUser implements LanguageEntity { + + private final @NonNull NamelessAPI api; + private final @NonNull RequestHandler requests; + + private int id; // -1 if not known + private String userTransformer; + + // Do not use directly, instead use userInfo() and integrations() + private @Nullable JsonObject _cachedUserInfo; + private @Nullable Map _cachedIntegrationData; + + + NamelessUser(final @NonNull NamelessAPI api, final @Positive int id) { + this.api = api; + this.requests = api.requests(); + + this.id = id; + this.userTransformer = "id:" + id; + } + + NamelessUser(final @NonNull NamelessAPI api, final @NonNull String userTransformer) { + this.api = api; + this.requests = api.requests(); + + this.id = -1; + this.userTransformer = URLEncoder.encode(userTransformer, StandardCharsets.UTF_8); + } + + NamelessUser(final NamelessAPI api, final JsonObject userInfo) { + this(api, userInfo.get("id").getAsInt()); + this._cachedUserInfo = userInfo; + } + + @NonNull JsonObject userInfo() throws NamelessException { + if (this._cachedUserInfo != null) { + return this._cachedUserInfo; + } + + final JsonObject response = this.requests.get("users/" + this.userTransformer); + + if (!response.get("exists").getAsBoolean()) { + throw new IllegalStateException("User was returned by the API without an error code so it should exist"); + } + + this._cachedUserInfo = response; + + if (this.id < 0) { + // The id was unknown before (we were using some other identifier to find the user) + // Now that we do know the id, use the id to identify the user instead + this.id = response.get("id").getAsInt(); + this.userTransformer = "id:" + this.id; + } + + return response; + } + + public @NonNull NamelessAPI api() { + return this.api; + } + + /** + * The API method `userInfo` is only called once to improve performance. + * This means that if something changes on the website, methods that use + * data from the `userInfo` API method will keep returning the old data. + * Calling this method will invalidate the cache and require making a new + * API request. It will not make a new API request immediately. Calling + * this method multiple times while the cache is already cleared has no + * effect. + */ + public void invalidateCache() { + this._cachedUserInfo = null; + this._cachedIntegrationData = null; + } + + public String userTransformer() { + return this.userTransformer; + } + + public int id() throws NamelessException { + if (this.id == -1) { + this.id = this.userInfo().get("id").getAsInt(); + } + + return this.id; + } + + public @NonNull String username() throws NamelessException { + return this.userInfo().get("username").getAsString(); + } + + public void updateUsername(final @NonNull String username) throws NamelessException { + final JsonObject post = new JsonObject(); + post.addProperty("username", username); + this.requests.post("users/" + this.userTransformer + "/update-username", post); + } + + public @NonNull String displayName() throws NamelessException { + return this.userInfo().get("displayname").getAsString(); + } + + /** + * @return The date the user registered on the website. + */ + public @NonNull Date registeredDate() throws NamelessException { + return new Date(this.userInfo().get("registered_timestamp").getAsLong() * 1000); + } + + public @NonNull Date lastOnline() throws NamelessException { + return new Date(this.userInfo().get("last_online_timestamp").getAsLong() * 1000); + } + + /** + * @return Whether this account is banned from the website. + */ + public boolean isBanned() throws NamelessException { + return this.userInfo().get("banned").getAsBoolean(); + } + + public boolean isVerified() throws NamelessException { + return this.userInfo().get("validated").getAsBoolean(); + } + + @Override + public @NonNull String rawLocale() throws NamelessException { + return this.userInfo().get("locale").getAsString(); + } + + public @NonNull VerificationInfo verificationInfo() throws NamelessException { + final boolean verified = this.isVerified(); + final JsonObject verification = this.userInfo().getAsJsonObject("verification"); + return new VerificationInfo(verified, verification); + } + + /** + * @return True if the user is member of at least one staff group, otherwise false + */ + public boolean isStaff() throws NamelessException { + if (!this.userInfo().has("groups")) { + throw new IllegalStateException("Groups array missing: https://github.com/NamelessMC/Nameless/issues/3052"); + } + + final JsonArray groups = this.userInfo().getAsJsonArray("groups"); + for (final JsonElement elem : groups) { + final JsonObject group = elem.getAsJsonObject(); + if (group.has("staff") && + group.get("staff").getAsBoolean()) { + return true; + } + } + return false; + } + + /** + * @return List of the user's groups, sorted from low order to high order. + */ + public @NonNull List<@NonNull Group> groups() throws NamelessException { + if (!this.userInfo().has("groups")) { + throw new IllegalStateException("Groups array missing: https://github.com/NamelessMC/Nameless/issues/3052"); + } + return GsonHelper.toObjectList(this.userInfo().getAsJsonArray("groups"), Group::new); + } + + /** + * Same as doing {@link #groups()}.get(0), but with better performance + * since it doesn't need to create and sort a list of group objects. + * Empty if the user is not in any groups. + * + * @return Player's group with the lowest order + */ + public @Nullable Group primaryGroup() throws NamelessException { + if (!this.userInfo().has("groups")) { + throw new IllegalStateException("Groups array missing: https://github.com/NamelessMC/Nameless/issues/3052"); + } + final JsonArray groups = this.userInfo().getAsJsonArray("groups"); + if (groups.size() > 0) { + // Website group response is ordered, first group is primary group. + return new Group(groups.get(0).getAsJsonObject()); + } else { + return null; + } + } + + public void addGroups(final @NonNull Group@NonNull ... groups) throws NamelessException { + final JsonObject post = new JsonObject(); + post.add("groups", this.groupsToJsonArray(groups)); + this.requests.post("users/" + this.userTransformer + "/groups/add", post); + this.invalidateCache(); // Groups modified, invalidate cache + } + + public void removeGroups(final @NonNull Group@NonNull... groups) throws NamelessException { + final JsonObject post = new JsonObject(); + post.add("groups", this.groupsToJsonArray(groups)); + this.requests.post("users/" + this.userTransformer + "/groups/remove", post); + this.invalidateCache(); // Groups modified, invalidate cache + } + + public void updateMinecraftGroups(final String[] addedGroups, final String[] removedGroups) throws NamelessException { + final JsonObject post = new JsonObject(); + post.add("add", this.requests.gson().toJsonTree(addedGroups)); + post.add("remove", this.requests.gson().toJsonTree(removedGroups)); + this.requests.post("minecraft/" + this.userTransformer + "/sync-groups", post); + } + + private JsonArray groupsToJsonArray(final @NonNull Group@NonNull [] groups) { + final JsonArray array = new JsonArray(); + for (final Group group : groups) { + array.add(group.getId()); + } + return array; + } + + public int notificationCount() throws NamelessException { + final JsonObject response = this.requests.get("users/" + this.userTransformer + "/notifications"); + return response.getAsJsonArray("notifications").size(); + } + + public List notifications() throws NamelessException { + final JsonObject response = this.requests.get("users/" + this.userTransformer + "/notifications"); + return GsonHelper.toObjectList(response.getAsJsonArray("notifications"), Notification::new); + } + + /** + * Creates a report for a website user + * @param user User to report. Lazy loading possible, only the ID is used. + * @param reason Reason why this player has been reported + * @throws IllegalArgumentException Report reason is too long (>255 characters) + * @throws IllegalArgumentException Report reason is too long (>255 characters) + */ + public void createReport(final @NonNull NamelessUser user, final @NonNull String reason) throws NamelessException { + Objects.requireNonNull(user, "User to report is null"); + Objects.requireNonNull(reason, "Report reason is null"); + Preconditions.checkArgument(reason.length() < 255, + "Report reason too long, it's %s characters but must be less than 255", reason.length()); + final JsonObject post = new JsonObject(); + post.addProperty("reporter", this.id()); + post.addProperty("reported", user.id()); + post.addProperty("content", reason); + try { + this.requests.post("reports/create", post); + } catch (final ApiException e) { + if (e.apiError() == ApiError.CORE_REPORT_CONTENT_TOO_LONG) { + throw new IllegalStateException("Website said report reason is too long, but we have " + + "client-side validation for this so it should be impossible"); + } + throw e; + } + } + + /** + * Create a report for a user who may or may not have a website account + * @param reportedUuid The Mojang UUID of the Minecraft player to report + * @param reportedName The Minecraft username of this player + * @param reason Report reason + * @throws IllegalArgumentException Report reason is too long (>255 characters) + */ + public void createReport(final @NonNull UUID reportedUuid, + final @NonNull String reportedName, + final @NonNull String reason) throws NamelessException { + this.createReport(reportedUuid, reportedName, reason, 0); + } + + /** + * Create a report for a user who may or may not have a website account + * @param reportedUuid The Mojang UUID of the Minecraft player to report + * @param reportedName The Minecraft username of this player + * @param reason Report reason + * @param serverId Minecraft server id + * @throws IllegalArgumentException Report reason is too long (>255 characters) + */ + public void createReport(final @NonNull UUID reportedUuid, + final @NonNull String reportedName, + final @NonNull String reason, + final int serverId) throws NamelessException { + Objects.requireNonNull(reportedUuid, "Reported uuid is null"); + Objects.requireNonNull(reportedName, "Reported name is null"); + Objects.requireNonNull(reason, "Report reason is null"); + Preconditions.checkArgument(reason.length() < 255, + "Report reason too long, it's %s characters but must be less than 255", reason.length()); + final JsonObject post = new JsonObject(); + post.addProperty("reporter", this.id()); + post.addProperty("reported_uid", reportedUuid.toString()); + post.addProperty("reported_username", reportedName); + post.addProperty("content", reason); + if (serverId != 0) { + post.addProperty("server_id", serverId); + } + try { + this.requests.post("reports/create", post); + } catch (final ApiException e) { + if (e.apiError() == ApiError.CORE_REPORT_CONTENT_TOO_LONG) { + throw new IllegalStateException("Website said report reason is too long, but we have " + + "client-side validation for this so it should be impossible"); + } + throw e; + } + } + + /** + * Get announcements visible to this user + * @return List of announcements visible to this user + */ + public @NonNull List<@NonNull Announcement> announcements() throws NamelessException { + final JsonObject response = this.requests.get("users/" + this.userTransformer + "/announcements"); + return NamelessAPI.announcements(response); + } + + /** + * Ban this user + * @since 2021-10-24 commit cce8d262b0be3f70818c188725cd7e7fc4fdbb9a + */ + public void banUser() throws NamelessException { + this.requests.post("users/" + this.userTransformer + "/ban", new JsonObject()); + } + + public Collection profileFields() throws NamelessException { + if (!this.userInfo().has("profile_fields")) { + return Collections.emptyList(); + } + + final JsonObject fieldsJson = this.userInfo().getAsJsonObject("profile_fields"); + final List fieldValues = new ArrayList<>(fieldsJson.size()); + for (final Map.Entry e : fieldsJson.entrySet()) { + final int id = Integer.parseInt(e.getKey()); + final JsonObject values = e.getValue().getAsJsonObject(); + fieldValues.add(new CustomProfileFieldValue( + new CustomProfileField( + id, + values.get("name").getAsString(), + CustomProfileFieldType.fromNamelessTypeInt(values.get("type").getAsInt()), + values.get("public").getAsBoolean(), + values.get("required").getAsBoolean(), + values.get("description").getAsString() + ), + GsonHelper.getNullableString(values, "value") + )); + } + + return fieldValues; + } + + public Map integrations() throws NamelessException { + if (this._cachedIntegrationData != null) { + return this._cachedIntegrationData; + } + + final JsonObject userInfo = this.userInfo(); + final JsonArray integrationsJsonArray = userInfo.getAsJsonArray("integrations"); + final Map integrationDataMap = new HashMap<>(integrationsJsonArray.size()); + for (final JsonElement integrationElement : integrationsJsonArray) { + final JsonObject integrationJson = integrationElement.getAsJsonObject(); + final String integrationName = integrationJson.get("integration").getAsString(); + DetailedIntegrationData integrationData; + switch(integrationName) { + case StandardIntegrationTypes.MINECRAFT: + integrationData = new DetailedMinecraftIntegrationData(integrationJson); + break; + case StandardIntegrationTypes.DISCORD: + integrationData = new DetailedDiscordIntegrationData(integrationJson); + break; + default: + integrationData = new DetailedIntegrationData(integrationJson); + } + integrationDataMap.put(integrationName, integrationData); + } + this._cachedIntegrationData = integrationDataMap; + return integrationDataMap; + } + + public @Nullable UUID minecraftUuid() throws NamelessException { + final IntegrationData integration = this.integrations().get(StandardIntegrationTypes.MINECRAFT); + return integration == null ? null : ((IMinecraftIntegrationData) integration).uuid(); + } + + public @Nullable String minecraftUsername() throws NamelessException { + final IntegrationData integration = this.integrations().get(StandardIntegrationTypes.MINECRAFT); + return integration == null ? null : integration.username(); + } + + public @Nullable Long discordId() throws NamelessException { + final IntegrationData integration = this.integrations().get(StandardIntegrationTypes.DISCORD); + return integration == null ? null : ((IDiscordIntegrationData) integration).idLong(); + } + + public @Nullable String discordUsername() throws NamelessException { + final IntegrationData integration = this.integrations().get(StandardIntegrationTypes.DISCORD); + return integration == null ? null : integration.username(); + } + + public void verify(final @NonNull String verificationCode) throws NamelessException { + final JsonObject body = new JsonObject(); + body.addProperty("code", verificationCode); + this.requests.post("users/" + this.userTransformer + "/verify", body); + } + + public DiscordUser discord() throws NamelessException { + return new DiscordUser(this); + } + + public StoreUser store() throws NamelessException { + return new StoreUser(this); + } + + public SuggestionsUser suggestions() throws NamelessException { + return new SuggestionsUser(this); + } + +} diff --git a/src/main/java/com/namelessmc/java_api/NamelessVersion.java b/src/main/java/com/namelessmc/java_api/NamelessVersion.java new file mode 100644 index 00000000..0e16e209 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/NamelessVersion.java @@ -0,0 +1,141 @@ +package com.namelessmc.java_api; + +import com.namelessmc.java_api.exception.UnknownNamelessVersionException; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.*; + +public enum NamelessVersion { + + V2_0_0_PR_7("2.0.0-pr7", "2.0.0 pre-release 7", 2, 0, true), + V2_0_0_PR_8("2.0.0-pr8", "2.0.0 pre-release 8", 2, 0, true), + V2_0_0_PR_9("2.0.0-pr9", "2.0.0 pre-release 9", 2, 0, true), + V2_0_0_PR_10("2.0.0-pr10", "2.0.0 pre-release 10", 2, 0, true), + V2_0_0_PR_11("2.0.0-pr11", "2.0.0 pre-release 11", 2, 0, true), + V2_0_0_PR_12("2.0.0-pr12", "2.0.0 pre-release 12", 2, 0, true), + V2_0_0_PR_13("2.0.0-pr13", "2.0.0 pre-release 13", 2, 0, true), + V2_0(null, "2.0.*", 2, 0, false), + V2_1(null, "2.1.*", 2, 1, false), + V2_2(null, "2.2.*", 2, 2, false) + + ; + + private static final Set SUPPORTED_VERSIONS = EnumSet.of( + V2_0_0_PR_13, + V2_0, + V2_1, + V2_2 + ); + + private final @Nullable String exactMatchName; // Only for pre-releases + private final @NonNull String friendlyName; + private final int major; + private final int minor; + private final boolean preRelease; + + @SuppressWarnings("SameParameterValue") + NamelessVersion(final @Nullable String exactMatchName, + final @NonNull String friendlyName, + final int major, + final int minor, + final boolean preRelease) { + this.exactMatchName = exactMatchName; + this.friendlyName = friendlyName; + this.major = major; + this.minor = minor; + this.preRelease = preRelease; + } + + public @NonNull String friendlyName() { + return this.friendlyName; + } + + public int major() { + return this.major; + } + + public int minor() { + return this.minor; + } + + /** + * @return True if this version is a pre-release + */ + public boolean isPreRelease() { + return this.preRelease; + } + + @Override + public String toString() { + return this.friendlyName; + } + + private static final Map BY_NAME = new HashMap<>(); + + private static final NamelessVersion[] CACHED_VALUES = NamelessVersion.values(); + + static { + for (final NamelessVersion version : allVersions()) { + if (version.exactMatchName != null) { + BY_NAME.put(version.exactMatchName, version); + } + } + } + + public static NamelessVersion parse(final @NonNull String versionName) throws UnknownNamelessVersionException { + Objects.requireNonNull(versionName, "Version name is null"); + if (versionName.contains("-pr")) { + // Pre-release version should match exactly + NamelessVersion version = BY_NAME.get(versionName); + if (version == null) { + throw new UnknownNamelessVersionException(versionName, "no pre-release matches exactly"); + } + return version; + } + String[] split = versionName.split("\\."); + if (split.length != 3) { + throw new UnknownNamelessVersionException(versionName, "version doesn't split to 3 components"); + } + int[] splitInts = new int[3]; + for (int i = 0; i < 3; i++) { + try { + splitInts[i] = Integer.parseInt(split[i]); + } catch (NumberFormatException e) { + throw new UnknownNamelessVersionException(versionName, "split component " + i + " is not an integer"); + } + } + + int major = splitInts[0]; + int minor = splitInts[1]; + + for (NamelessVersion version : allVersions()) { + if (version.major == major && version.minor == minor && !version.isPreRelease()) { + return version; + } + } + + throw new UnknownNamelessVersionException(versionName, "no match for major=" + major + " minor=" + minor); + + } + + public static NamelessVersion[] allVersions() { + return CACHED_VALUES; + } + + /** + * @return List of NamelessMC versions supported by the Java API + */ + public static Set supportedVersions() { + return SUPPORTED_VERSIONS; + } + + /** + * @param version A version to check + * @return Whether the provided NamelessMC version is supported by this Java API library. + */ + public static boolean isSupportedByJavaApi(final NamelessVersion version) { + return SUPPORTED_VERSIONS.contains(version); + } + +} diff --git a/src/main/java/com/namelessmc/java_api/Notification.java b/src/main/java/com/namelessmc/java_api/Notification.java new file mode 100644 index 00000000..dcab79fa --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/Notification.java @@ -0,0 +1,51 @@ +package com.namelessmc.java_api; + +import com.google.gson.JsonObject; + +public class Notification { + + private final String message; + private final String url; + private final Type type; + + public Notification(JsonObject json) { + this.message = json.get("message").getAsString(); + this.url = json.get("url").getAsString(); + this.type = Type.fromString(json.get("type").getAsString()); + } + + public String message() { + return this.message; + } + + public String url() { + return this.url; + } + + public Type type() { + return this.type; + } + + public enum Type { + + TAG, + MESSAGE, + LIKE, + PROFILE_COMMENT, + COMMENT_REPLY, + THREAD_REPLY, + FOLLOW, + + UNKNOWN; + + public static Type fromString(final String string) { + try { + return Type.valueOf(string.replace('-', '_').toUpperCase()); + } catch (final IllegalArgumentException e) { + return Type.UNKNOWN; + } + } + + } + +} diff --git a/src/main/java/com/namelessmc/java_api/RequestHandler.java b/src/main/java/com/namelessmc/java_api/RequestHandler.java new file mode 100644 index 00000000..066970ae --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/RequestHandler.java @@ -0,0 +1,264 @@ +package com.namelessmc.java_api; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.net.URLEncoder; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import com.github.mizosoft.methanol.Methanol; +import com.github.mizosoft.methanol.MutableRequest; +import com.google.common.base.Ascii; +import com.google.common.base.Preconditions; +import com.google.common.io.ByteStreams; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import com.namelessmc.java_api.exception.ApiError; +import com.namelessmc.java_api.exception.ApiException; +import com.namelessmc.java_api.exception.NamelessException; +import com.namelessmc.java_api.logger.ApiLogger; + +public class RequestHandler { + + private static final int RETRIES = 2; + + private final @NonNull URL apiUrl; + private final @NonNull Methanol httpClient; + private final @Nullable ApiLogger debugLogger; + private final @NonNull Gson gson; + private final int responseLengthLimit; + + RequestHandler(final @NonNull URL apiUrl, + final @NonNull Methanol httpClient, + final @NonNull Gson gson, + final @Nullable ApiLogger debugLogger, + final int responseLengthLimit) { + this.apiUrl = Objects.requireNonNull(apiUrl, "API URL is null"); + this.httpClient = Objects.requireNonNull(httpClient, "http client is null"); + this.gson = gson; + this.debugLogger = debugLogger; + this.responseLengthLimit = responseLengthLimit; + } + + public Gson gson() { + return this.gson; + } + + public JsonObject post(final String route, + final JsonObject postData) throws NamelessException { + return this.makeConnection(route, postData, RETRIES); + } + + public JsonObject get(final String route, + final @Nullable Object... parameters) throws NamelessException { + final StringBuilder urlBuilder = new StringBuilder(route); + + if (parameters.length > 0) { + if (parameters.length % 2 != 0) { + final String paramString = Arrays.stream(parameters).map(Objects::toString).collect(Collectors.joining("|")); + throw new IllegalArgumentException(String.format("Parameter string varargs array length must be even (length is %s - %s)", parameters.length, paramString)); + } + + for (int i = 0; i < parameters.length; i++) { + final Object param = parameters[i]; + if (i % 2 == 0) { + if (param == null) { + throw new IllegalArgumentException("Parameter keys must never be null, only values may be null"); + } + urlBuilder.append("&"); + urlBuilder.append(param); + } else if (param != null) { + urlBuilder.append("="); + urlBuilder.append(URLEncoder.encode(param.toString(), StandardCharsets.UTF_8)); + } + } + } + + return this.makeConnection(urlBuilder.toString(), null, RETRIES); + } + + private void debug(final @NonNull Supplier messageSupplier) { + if (this.debugLogger != null) { + this.debugLogger.log(messageSupplier.get()); + } + } + + private @NonNull JsonObject makeConnection(final @NonNull String route, + final @Nullable JsonObject postBody, + final int retries) throws NamelessException { + Preconditions.checkArgument(!route.startsWith("/"), "Route must not start with a slash"); + final URI uri = URI.create(this.apiUrl + route); + if (uri.getHost() == null) { + throw new NamelessException("URI has empty host, does it contain invalid characters? Please note that although underscores are " + + "legal in domain names, the Java URI class (and the Java HttpClient) does not accept them, because it uses the specification " + + "for 'host names' not 'domain names'."); + } + final MutableRequest request = MutableRequest.create(uri); + + this.debug(() -> "Making connection " + (postBody != null ? "POST" : "GET") + " to " + request.uri()); + + final long requestStartTime = System.currentTimeMillis(); + + if (postBody != null) { + final byte[] postBytes = this.gson.toJson(postBody).getBytes(StandardCharsets.UTF_8); + request.POST(HttpRequest.BodyPublishers.ofByteArray(postBytes)); + request.header("Content-Type", "application/json"); + + this.debug(() -> "POST request body:\n" + new String(postBytes, StandardCharsets.UTF_8)); + } else { + request.GET(); + } + + request.header("Accept", "application/json"); + + final int statusCode; + final String responseBody; + try { + final HttpResponse httpResponse = this.httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); + statusCode = httpResponse.statusCode(); + responseBody = this.getBodyAsString(httpResponse); + } catch (final IOException e) { + final @Nullable String exceptionMessage = e.getMessage(); + final StringBuilder message = new StringBuilder(); + message.append("Network connection error (not a Nameless issue). "); + message.append(e.getClass().getSimpleName()); + message.append(": "); + message.append(exceptionMessage); + if (exceptionMessage != null) { + if (e.getMessage() != null && e.getMessage().contains("GOAWAY received")) { + // Receiving a GOAWAY means the connection should be retried. For some reason, the Java + // HTTP client doesn't seem to. See also: https://stackoverflow.com/a/55092354 + if (retries > 0) { + this.debug(() -> "Retrying after received GOAWAY"); + return this.makeConnection(route, postBody, retries - 1); + } else { + message.append("Already retried after GOAWAY multiple times, your web server is probably down."); + } + } else if (exceptionMessage.contains("unable to find valid certification path to requested target")) { + message.append("\nHINT: Your HTTPS certificate is probably valid, but is it complete? Ensure your website uses a valid *full chain* SSL/TLS certificate."); + } else if (exceptionMessage.contains("No subject alternative DNS name matching")) { + message.append("\nHINT: Is your HTTPS certificate valid? Is it for the correct domain?"); + } else if (exceptionMessage.contains("Connect timed out")) { + message.append("\nHINT: Is a webserver running at the provided domain? Are we blocked by a firewall? Is your webserver fast enough?"); + } else if (exceptionMessage.contains("Connection refused")) { + message.append("\nHINT: Is the domain correct? Is your webserver running? Are we blocked by a firewall?"); + } else if (exceptionMessage.contains("timed out")) { + message.append("\nHINT: The website responded too slow, no response after waiting for "); + message.append((System.currentTimeMillis() - requestStartTime) / 1000); + message.append(" seconds."); + } + } + + throw new NamelessException(message.toString(), e); + } catch (final InterruptedException e) { + throw new NamelessException("In-progress request was aborted", e); + } + + this.debug(() -> "Website response body, after " + (System.currentTimeMillis() - requestStartTime) + "ms:\n" + regularAsciiOnly(responseBody)); + + if (responseBody.length() == 0) { + if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307 || statusCode == 308) { + throw new NamelessException("Website returned a redirect. Please ensure your URL is correct, paying attention to whether it should use HTTP or HTTPS, or whether it should or should not contain 'www.'."); + } + throw new NamelessException("Website returned empty response with status code " + statusCode); + } + + JsonObject json; + + try { + json = JsonParser.parseString(responseBody).getAsJsonObject(); + } catch (final JsonSyntaxException | IllegalStateException e) { + final StringBuilder message = new StringBuilder(); + message.append("Website returned invalid response with code "); + message.append(statusCode); + message.append(".\n"); + if (statusCode >= 301 && statusCode <= 303) { + message.append("HINT: The web server returned a redirect. If your URL uses http://, change to https://. If your website forces www., make sure to add www. to the url.\n"); + } else if (statusCode == 520 || statusCode == 521) { + message.append("HINT: Status code 520/521 is sent by CloudFlare when the backend webserver is down or having issues. Check your webserver and CloudFlare configuration.\n"); + } else if (responseBody.contains("/aes.js")) { + message.append("HINT: It looks like requests are being blocked by your web server or a proxy. "); + message.append("This is a common occurrence with free web hosting services; they usually don't allow API access.\n"); + } else if (responseBody.contains("Please Wait... | Cloudflare") || + responseBody.contains("#cf-bubbles") || + responseBody.contains("_cf_chl_opt")) { + message.append("HINT: CloudFlare is blocking our request. Please see https://docs.namelessmc.com/cloudflare-api\n"); + } else if (responseBody.startsWith("\ufeff")) { + message.append("HINT: The website response contains invisible unicode characters. This seems to be caused by Partydragen's Store module, we have no idea why.\n"); + } + + message.append("Website response, after "); + message.append(System.currentTimeMillis() - requestStartTime); + message.append("ms:\n"); + message.append("-----------------\n"); + final int totalLengthLimit = 1500; // fit in a Discord message + final String printableResponse = regularAsciiOnly(responseBody); + message.append(Ascii.truncate(printableResponse, totalLengthLimit, "[truncated]\n")); + if (message.charAt(message.length() - 1) != '\n') { + message.append('\n'); + } + + throw new NamelessException(message.toString(), e); + } + + if (json.has("error")) { + final String errorString = json.get("error").getAsString(); + if (errorString.equals("true")) { + throw new NamelessException("Error string is 'true', are you using an older NamelessMC version?"); + } + final ApiError apiError = ApiError.fromString(errorString); + if (apiError == null) { + throw new NamelessException("Unknown API error: " + errorString); + } + + final String meta; + if (json.has("meta") && !json.get("meta").isJsonNull()) { + meta = json.get("meta").toString(); + } else { + meta = null; + } + throw new ApiException(apiError, meta); + } + + return json; + } + + private String getBodyAsString(HttpResponse response) throws IOException { + try (InputStream in = response.body(); + InputStream limited = ByteStreams.limit(in, this.responseLengthLimit)) { + final byte[] bytes = limited.readAllBytes(); + if (bytes.length == this.responseLengthLimit) { + throw new IOException("Response larger than limit of " + this.responseLengthLimit + " bytes."); + } + return new String(bytes, StandardCharsets.UTF_8); + } + } + + private static @NonNull String regularAsciiOnly(@NonNull String message) { + final char[] chars = message.toCharArray(); + for (int i = 0; i < chars.length; i++) { + final char c = chars[i]; + // only allow standard symbols, letters, numbers + // look up an ascii table if you don't understand this if statement + if (c >= ' ' && c <= '~' || c == '\n') { + chars[i] = c; + } else { + chars[i] = '.'; + } + } + return new String(chars); + } + +} diff --git a/src/com/namelessmc/java_api/UserFilter.java b/src/main/java/com/namelessmc/java_api/UserFilter.java similarity index 57% rename from src/com/namelessmc/java_api/UserFilter.java rename to src/main/java/com/namelessmc/java_api/UserFilter.java index 204f8fbb..3a797fd1 100644 --- a/src/com/namelessmc/java_api/UserFilter.java +++ b/src/main/java/com/namelessmc/java_api/UserFilter.java @@ -1,21 +1,21 @@ package com.namelessmc.java_api; -import org.jetbrains.annotations.NotNull; +import org.checkerframework.checker.nullness.qual.NonNull; public class UserFilter { public static UserFilter BANNED = new UserFilter<>("banned"); public static UserFilter VERIFIED = new UserFilter<>("verified"); - public static UserFilter DISCORD_LINKED = new UserFilter<>("discord_linked"); public static UserFilter GROUP_ID = new UserFilter<>("group_id"); + public static UserFilter INTEGRATION = new UserFilter<>("integration"); - private final @NotNull String filterName; + private final @NonNull String filterName; - public UserFilter(final @NotNull String filterName) { + public UserFilter(final @NonNull String filterName) { this.filterName = filterName; } - public @NotNull String getName() { + public @NonNull String name() { return this.filterName; } diff --git a/src/com/namelessmc/java_api/VerificationInfo.java b/src/main/java/com/namelessmc/java_api/VerificationInfo.java similarity index 71% rename from src/com/namelessmc/java_api/VerificationInfo.java rename to src/main/java/com/namelessmc/java_api/VerificationInfo.java index a1ab7975..0909664c 100644 --- a/src/com/namelessmc/java_api/VerificationInfo.java +++ b/src/main/java/com/namelessmc/java_api/VerificationInfo.java @@ -2,14 +2,14 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import org.jetbrains.annotations.NotNull; +import org.checkerframework.checker.nullness.qual.NonNull; public class VerificationInfo { private final boolean verified; - private final @NotNull JsonObject json; + private final @NonNull JsonObject json; - VerificationInfo(final boolean verified, @NotNull final JsonObject json) { + VerificationInfo(final boolean verified, final @NonNull JsonObject json) { this.verified = verified; this.json = json; } @@ -18,7 +18,7 @@ public boolean isVerified() { return this.verified; } - public boolean isVerifiedCustom(@NotNull final String name) { + public boolean isVerifiedCustom(final @NonNull String name) { final JsonElement e = this.json.get(name); if (e == null) { throw new UnsupportedOperationException("The API did not return verification for '" + name + "'"); @@ -31,6 +31,7 @@ public boolean isVerifiedEmail() { return isVerifiedCustom("email"); } + // TODO are these still relevant with the new integration system? public boolean isVerifiedMinecraft() { return isVerifiedCustom("minecraft"); } diff --git a/src/main/java/com/namelessmc/java_api/Website.java b/src/main/java/com/namelessmc/java_api/Website.java new file mode 100644 index 00000000..b1a278ec --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/Website.java @@ -0,0 +1,82 @@ +package com.namelessmc.java_api; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.namelessmc.java_api.exception.NamelessException; +import com.namelessmc.java_api.exception.UnknownNamelessVersionException; +import com.namelessmc.java_api.modules.NamelessModule; + +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +public class Website implements LanguageEntity { + + private final String version; + private final Set modules; + private final String rawLanguage; + + Website(final JsonObject json) throws NamelessException { + if (!json.has("nameless_version")) { + // This is usually the point where people run into issues if the response is not from NamelessMC + // but from something else like a proxy or denial of service protection system, so we throw a useful + // exception. + throw new NamelessException("Website didn't include namelessmc_version in the info response. Is the response from NamelessMC?"); + } + + this.version = json.get("nameless_version").getAsString(); + + this.modules = StreamSupport.stream(json.get("modules").getAsJsonArray().spliterator(), false) + .map(JsonElement::getAsString) + .map(NamelessModule::byName) + .collect(Collectors.toUnmodifiableSet()); + + if (json.get("locale").isJsonNull()) { + throw new NamelessException("Website returned null locale. This can happen if you upgraded from v2-pr12 to v2-pr13, please try switching the site's language to something else and back."); + } + + this.rawLanguage = json.get("locale").getAsString(); + } + + public String rawVersion() { + return this.version; + } + + public NamelessVersion parsedVersion() throws UnknownNamelessVersionException { + return NamelessVersion.parse(this.version); + } + + public Set modules() { + return this.modules; + } + + @Override + public String rawLocale() { + return this.rawLanguage; + } + + public static class Update { + + private final boolean isUrgent; + private final String version; + + Update(final boolean isUrgent, final String version) { + this.isUrgent = isUrgent; + this.version = version; + } + + public boolean isUrgent() { + return this.isUrgent; + } + + public String rawVersion() { + return this.version; + } + + public NamelessVersion parsedVersion() throws UnknownNamelessVersionException { + return NamelessVersion.parse(this.version); + } + + } + +} diff --git a/src/main/java/com/namelessmc/java_api/exception/ApiError.java b/src/main/java/com/namelessmc/java_api/exception/ApiError.java new file mode 100644 index 00000000..8c64999f --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/exception/ApiError.java @@ -0,0 +1,95 @@ +package com.namelessmc.java_api.exception; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public enum ApiError { + + // https://github.com/NamelessMC/Nameless/blob/v2/modules/Core/classes/Misc/Nameless2API.php + NAMELESS_API_IS_DISABLED("nameless", "api_is_disabled"), + NAMELESS_UNKNOWN_ERROR("nameless", "unknown_error"), + NAMELESS_NOT_AUTHORIZED("nameless", "not_authorized"), + NAMELESS_INVALID_API_KEY("nameless", "invalid_api_key"), + NAMELESS_MISSING_API_KEY("nameless", "missing_api_key"), + NAMELESS_INVALID_API_METHOD("nameless", "invalid_api_method"), + NAMELESS_CANNOT_FIND_USER("nameless", "cannot_find_user"), + NAMELESS_INVALID_POST_CONTENTS("nameless", "invalid_post_contents"), + NAMELESS_INVALID_GET_CONTENTS("nameless", "invalid_get_contents"), + NAMELESS_NO_SITE_UID("nameless", "no_site_uid"), + + // https://github.com/NamelessMC/Nameless/blob/v2/modules/Core/classes/Misc/CoreApiErrors.php + CORE_UNABLE_TO_FIND_GROUP("core", "unable_to_find_group"), + CORE_BANNED_FROM_WEBSITE("core", "banned_from_website"), + CORE_REPORT_CONTENT_TOO_LONG("core", "report_content_too_long"), + CORE_CANNOT_REPORT_YOURSELF("core", "cannot_report_yourself"), + CORE_OPEN_REPORT_ALREADY("core", "open_report_already"), + CORE_UNABLE_TO_UPDATE_SERVER_INFO("core", "unable_to_update_server_info"), + CORE_INVALID_SERVER_ID("core", "invalid_server_id"), + CORE_EMAIL_ALREADY_EXISTS("core", "email_already_exists"), + CORE_USERNAME_ALREADY_EXISTS("core", "username_already_exists"), + CORE_INVALID_EMAIL_ADDRESS("core", "invalid_email_address"), + CORE_INVALID_USERNAME("core", "invalid_username"), + CORE_UNABLE_TO_CREATE_ACCOUNT("core", "unable_to_create_account"), + CORE_UNABLE_TO_SEND_REGISTRATION_EMAIL("core", "unable_to_send_registration_email"), + CORE_INVALID_INTEGRATION("core", "invalid_integration"), + CORE_INVALID_CODE("core", "invalid_code"), + CORE_USER_ALREADY_ACTIVE("core", "user_already_active"), + CORE_INTEGRATION_ALREADY_VERIFIED("core", "integration_already_verified"), + CORE_UNABLE_TO_UPDATE_USERNAME("core", "unable_to_update_username"), + CORE_INTEGRATION_IDENTIFIER_ERROR("core", "integration_identifier_errors"), + CORE_INTEGRATION_USERNAME_ERROR("core", "integration_username_errors"), + + // https://github.com/NamelessMC/Nameless/blob/v2/modules/Discord%20Integration/classes/DiscordApiErrors.php + DISCORD_DISCORD_INTEGRATION_DISABLED("discord_integration", "discord_integration_disabled"), + DISCORD_UNABLE_TO_UPDATE_DISCORD_ROLES("discord_integration", "unable_to_update_discord_roles"), + DISCORD_UNABLE_TO_SET_DISCORD_BOT_URL("discord_integration", "unable_to_set_discord_bot_url"), + DISCORD_UNABLE_TO_SET_DISCORD_GUILD_ID("discord_integration", "unable_to_set_discord_guild_id"), + DISCORD_UNABLE_TO_SET_DISCORD_BOT_USERNAME("discord_integration", "unable_to_set_discord_bot_username"), + + // https://github.com/partydragen/Nameless-Store/blob/master/upload/modules/Store/classes/StoreApiErrors.php + STORE_PAYMENT_NOT_FOUND("store", "payment_not_found"), + STORE_CONNECTION_NOT_FOUND("store", "connection_not_found"), + @Deprecated + ERROR_INVALID_CREDITS_AMOUNT("store", "invalid_credits_amount"), + STORE_INVALID_CREDITS_AMOUNT("store", "invalid_credits_amount"), + + ; + + private final String key; + private final String value; + private final String string; + + ApiError(final String namespaceKey, final String namespaceValue) { + this.key = namespaceKey; + this.value = namespaceValue; + this.string = this.key + ":" + this.value; + } + + public String key() { + return this.key; + } + + public String value() { + return this.value; + } + + @Override + public String toString() { + return this.string; + } + + private static final Map FROM_STRING = new HashMap<>(); + + static { + for (ApiError apiError : ApiError.values()) { + FROM_STRING.put(apiError.string, apiError); + } + } + + public static @Nullable ApiError fromString(final String string) { + return FROM_STRING.get(string); + } + +} diff --git a/src/main/java/com/namelessmc/java_api/exception/ApiException.java b/src/main/java/com/namelessmc/java_api/exception/ApiException.java new file mode 100644 index 00000000..c33514c5 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/exception/ApiException.java @@ -0,0 +1,20 @@ +package com.namelessmc.java_api.exception; + +import org.checkerframework.checker.nullness.qual.Nullable; + +public class ApiException extends NamelessException { + + private static final long serialVersionUID = 1L; + + private final ApiError apiError; + + public ApiException(final ApiError apiError, final @Nullable String meta) { + super("API error " + apiError + (meta == null ? "" : " (meta: " + meta + ")")); + this.apiError = apiError; + } + + public ApiError apiError() { + return this.apiError; + } + +} diff --git a/src/main/java/com/namelessmc/java_api/exception/MissingModuleException.java b/src/main/java/com/namelessmc/java_api/exception/MissingModuleException.java new file mode 100644 index 00000000..4f968e78 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/exception/MissingModuleException.java @@ -0,0 +1,33 @@ +package com.namelessmc.java_api.exception; + +import com.namelessmc.java_api.modules.NamelessModule; + +public class MissingModuleException extends NamelessException { + + private static final long serialVersionUID = 1L; + + public MissingModuleException(final NamelessModule module) { + super(getExceptionMessage(module)); + } + + private static String getExceptionMessage(final NamelessModule module) { + StringBuilder builder = new StringBuilder("Required module not installed: "); + builder.append(module.name()); + builder.append("."); + + if (module.isIncluded()) { + builder.append(" This module is an official module, included with NamelessMC. Please enable it."); + } else { + builder.append(" This module is a third-party module."); + if (module.downloadLink() != null) { + builder.append(" It can be downloaded here: "); + builder.append(module.downloadLink()); + } else { + builder.append(" It can be downloaded externally."); + } + } + + return builder.toString(); + } + +} diff --git a/src/main/java/com/namelessmc/java_api/exception/NamelessException.java b/src/main/java/com/namelessmc/java_api/exception/NamelessException.java new file mode 100644 index 00000000..bc588a71 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/exception/NamelessException.java @@ -0,0 +1,28 @@ +package com.namelessmc.java_api.exception; + +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Generic exception thrown by many methods in the Nameless API + */ +public class NamelessException extends Exception { + + private static final long serialVersionUID = 1L; + + public NamelessException(final @NonNull String message) { + super(message); + } + + public NamelessException(final @NonNull String message, final @NonNull Throwable cause) { + super(message, cause); + } + + public NamelessException(final @NonNull Throwable cause) { + super(cause); + } + + public NamelessException() { + super(); + } + +} diff --git a/src/main/java/com/namelessmc/java_api/exception/UnknownNamelessVersionException.java b/src/main/java/com/namelessmc/java_api/exception/UnknownNamelessVersionException.java new file mode 100644 index 00000000..a643179a --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/exception/UnknownNamelessVersionException.java @@ -0,0 +1,9 @@ +package com.namelessmc.java_api.exception; + +public class UnknownNamelessVersionException extends NamelessException { + + public UnknownNamelessVersionException(String versionName, String reason) { + super("Unknown Nameless version:" + versionName + ": " + reason); + } + +} diff --git a/src/main/java/com/namelessmc/java_api/integrations/DetailedDiscordIntegrationData.java b/src/main/java/com/namelessmc/java_api/integrations/DetailedDiscordIntegrationData.java new file mode 100644 index 00000000..32923ecb --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/integrations/DetailedDiscordIntegrationData.java @@ -0,0 +1,20 @@ +package com.namelessmc.java_api.integrations; + +import com.google.gson.JsonObject; +import org.checkerframework.checker.nullness.qual.NonNull; + +public class DetailedDiscordIntegrationData extends DetailedIntegrationData implements IDiscordIntegrationData { + + private final long idLong; + + public DetailedDiscordIntegrationData(final @NonNull JsonObject json) { + super(json); + this.idLong = Long.parseLong(this.identifier()); + } + + @Override + public final long idLong() { + return this.idLong; + } + +} diff --git a/src/main/java/com/namelessmc/java_api/integrations/DetailedIntegrationData.java b/src/main/java/com/namelessmc/java_api/integrations/DetailedIntegrationData.java new file mode 100644 index 00000000..708453b1 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/integrations/DetailedIntegrationData.java @@ -0,0 +1,49 @@ +package com.namelessmc.java_api.integrations; + +import com.google.gson.JsonObject; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Date; + +public class DetailedIntegrationData extends IntegrationData { + + private final boolean verified; + private final @NonNull Date linkedDate; + private final boolean shownPublicly; + + public DetailedIntegrationData(final @NonNull String integrationType, + final @NonNull String identifier, + final @NonNull String username, + final boolean verified, + final @NonNull Date linkedDate, + final boolean shownPublicly) { + super(integrationType, identifier, username); + this.verified = verified; + this.linkedDate = linkedDate; + this.shownPublicly = shownPublicly; + } + + public DetailedIntegrationData(final @NonNull JsonObject json) { + this( + json.get("integration").getAsString(), + json.get("identifier").getAsString(), + json.get("username").getAsString(), + json.get("verified").getAsBoolean(), + new Date(json.get("linked_date").getAsLong() * 1000), + json.get("show_publicly").getAsBoolean() + ); + } + + public final boolean isVerified() { + return this.verified; + } + + public final @NonNull Date linkedDate() { + return this.linkedDate; + } + + public final boolean isShownPublicly() { + return this.shownPublicly; + } + +} diff --git a/src/main/java/com/namelessmc/java_api/integrations/DetailedMinecraftIntegrationData.java b/src/main/java/com/namelessmc/java_api/integrations/DetailedMinecraftIntegrationData.java new file mode 100644 index 00000000..75afbedf --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/integrations/DetailedMinecraftIntegrationData.java @@ -0,0 +1,22 @@ +package com.namelessmc.java_api.integrations; + +import com.google.gson.JsonObject; +import com.namelessmc.java_api.NamelessAPI; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.UUID; + +public class DetailedMinecraftIntegrationData extends DetailedIntegrationData implements IMinecraftIntegrationData { + + private final @NonNull UUID uuid; + + public DetailedMinecraftIntegrationData(final @NonNull JsonObject json) { + super(json); + this.uuid = NamelessAPI.websiteUuidToJavaUuid(this.identifier()); + } + + @Override + public final @NonNull UUID uuid() { + return this.uuid; + } +} diff --git a/src/main/java/com/namelessmc/java_api/integrations/DiscordIntegrationData.java b/src/main/java/com/namelessmc/java_api/integrations/DiscordIntegrationData.java new file mode 100644 index 00000000..b7ded0cf --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/integrations/DiscordIntegrationData.java @@ -0,0 +1,19 @@ +package com.namelessmc.java_api.integrations; + +import org.checkerframework.checker.nullness.qual.NonNull; + +public class DiscordIntegrationData extends IntegrationData { + + private final long id; + + public DiscordIntegrationData(final long id, + final @NonNull String username) { + super(StandardIntegrationTypes.DISCORD, String.valueOf(id), username); + this.id = id; + } + + public final long idLong() { + return this.id; + } + +} diff --git a/src/main/java/com/namelessmc/java_api/integrations/IDiscordIntegrationData.java b/src/main/java/com/namelessmc/java_api/integrations/IDiscordIntegrationData.java new file mode 100644 index 00000000..a86a7676 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/integrations/IDiscordIntegrationData.java @@ -0,0 +1,7 @@ +package com.namelessmc.java_api.integrations; + +public interface IDiscordIntegrationData { + + long idLong(); + +} diff --git a/src/main/java/com/namelessmc/java_api/integrations/IMinecraftIntegrationData.java b/src/main/java/com/namelessmc/java_api/integrations/IMinecraftIntegrationData.java new file mode 100644 index 00000000..6b639f42 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/integrations/IMinecraftIntegrationData.java @@ -0,0 +1,11 @@ +package com.namelessmc.java_api.integrations; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.UUID; + +public interface IMinecraftIntegrationData { + + @NonNull UUID uuid(); + +} diff --git a/src/main/java/com/namelessmc/java_api/integrations/IntegrationData.java b/src/main/java/com/namelessmc/java_api/integrations/IntegrationData.java new file mode 100644 index 00000000..43a1f77d --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/integrations/IntegrationData.java @@ -0,0 +1,32 @@ +package com.namelessmc.java_api.integrations; + +import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.nullness.qual.NonNull; + +public class IntegrationData { + + private final @NonNull String integrationType; + private final @NonNull String identifier; + private final @NonNull String username; + + public IntegrationData(final @NonNull String integrationType, + final @NonNull String identifier, + final @NonNull String username) { + this.integrationType = integrationType; + this.identifier = identifier; + this.username = username; + } + + public final @NonNull String type(@UnknownInitialization(IntegrationData.class) IntegrationData this) { + return this.integrationType; + } + + public final @NonNull String identifier(@UnknownInitialization(IntegrationData.class) IntegrationData this) { + return this.identifier; + } + + public final @NonNull String username(@UnknownInitialization(IntegrationData.class) IntegrationData this) { + return this.username; + } + +} diff --git a/src/main/java/com/namelessmc/java_api/integrations/MinecraftIntegrationData.java b/src/main/java/com/namelessmc/java_api/integrations/MinecraftIntegrationData.java new file mode 100644 index 00000000..e2834f63 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/integrations/MinecraftIntegrationData.java @@ -0,0 +1,22 @@ +package com.namelessmc.java_api.integrations; + +import com.namelessmc.java_api.NamelessAPI; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.UUID; + +public class MinecraftIntegrationData extends IntegrationData implements IMinecraftIntegrationData { + + private final @NonNull UUID uuid; + + public MinecraftIntegrationData(final @NonNull UUID uuid, + final @NonNull String username) { + super(StandardIntegrationTypes.MINECRAFT, NamelessAPI.javaUuidToWebsiteUuid(uuid), username); + this.uuid = uuid; + } + + public final @NonNull UUID uuid() { + return this.uuid; + } + +} diff --git a/src/main/java/com/namelessmc/java_api/integrations/StandardIntegrationTypes.java b/src/main/java/com/namelessmc/java_api/integrations/StandardIntegrationTypes.java new file mode 100644 index 00000000..f94d0b74 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/integrations/StandardIntegrationTypes.java @@ -0,0 +1,8 @@ +package com.namelessmc.java_api.integrations; + +public class StandardIntegrationTypes { + + public static final String MINECRAFT = "Minecraft"; + public static final String DISCORD = "Discord"; + +} diff --git a/src/com/namelessmc/java_api/logger/ApiLogger.java b/src/main/java/com/namelessmc/java_api/logger/ApiLogger.java similarity index 100% rename from src/com/namelessmc/java_api/logger/ApiLogger.java rename to src/main/java/com/namelessmc/java_api/logger/ApiLogger.java diff --git a/src/com/namelessmc/java_api/logger/JavaLoggerLogger.java b/src/main/java/com/namelessmc/java_api/logger/JavaLoggerLogger.java similarity index 100% rename from src/com/namelessmc/java_api/logger/JavaLoggerLogger.java rename to src/main/java/com/namelessmc/java_api/logger/JavaLoggerLogger.java diff --git a/src/com/namelessmc/java_api/logger/PrintStreamLogger.java b/src/main/java/com/namelessmc/java_api/logger/PrintStreamLogger.java similarity index 100% rename from src/com/namelessmc/java_api/logger/PrintStreamLogger.java rename to src/main/java/com/namelessmc/java_api/logger/PrintStreamLogger.java diff --git a/src/com/namelessmc/java_api/logger/Slf4jLogger.java b/src/main/java/com/namelessmc/java_api/logger/Slf4jLogger.java similarity index 100% rename from src/com/namelessmc/java_api/logger/Slf4jLogger.java rename to src/main/java/com/namelessmc/java_api/logger/Slf4jLogger.java diff --git a/src/main/java/com/namelessmc/java_api/modules/NamelessModule.java b/src/main/java/com/namelessmc/java_api/modules/NamelessModule.java new file mode 100644 index 00000000..d09bbaf8 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/NamelessModule.java @@ -0,0 +1,84 @@ +package com.namelessmc.java_api.modules; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class NamelessModule { + + public static final NamelessModule CORE = new NamelessModule("Core", true, null); + public static final NamelessModule FORUM = new NamelessModule("Forum", true, null); + public static final NamelessModule DISCORD_INTEGRATION = new NamelessModule("Discord Integration", true, null); + public static final NamelessModule COOKIE_CONSENT = new NamelessModule("Cookie Consent", true, null); + + public static final NamelessModule STORE = new NamelessModule("Store", false, "https://namelessmc.com/resources/resource/139"); + public static final NamelessModule WEBSEND = new NamelessModule("Websend", false, "https://github.com/supercrafter100/Nameless-Websend"); + public static final NamelessModule SUGGESTIONS = new NamelessModule("Suggestions", false, "https://namelessmc.com/resources/resource/129"); + + private final String name; + private final boolean included; + private final @Nullable String downloadLink; + + private NamelessModule(String name, boolean included, @Nullable String downloadLink) { + this.name = name; + this.included = included; + this.downloadLink = downloadLink; + } + + public String name() { + return this.name; + } + + public boolean isIncluded() { + return this.included; + } + + public @Nullable String downloadLink() { + return downloadLink; + } + + private static final List MODULES = List.of( + CORE, + FORUM, + DISCORD_INTEGRATION, + COOKIE_CONSENT, + STORE, + WEBSEND, + SUGGESTIONS + ); + + private static final Map BY_NAME = new HashMap<>(); + + static { + for (NamelessModule module : MODULES) { + BY_NAME.put(module.name(), module); + } + } + + public static NamelessModule custom(String name) { + return new NamelessModule(Objects.requireNonNull(name), false, null); + } + + public static NamelessModule byName(String name) { + if (BY_NAME.containsKey(name)) { + return BY_NAME.get(name); + } else { + return custom(name); + } + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(final @Nullable Object obj) { + return obj instanceof NamelessModule && + ((NamelessModule) obj).name().equals(this.name); + } + +} diff --git a/src/main/java/com/namelessmc/java_api/modules/discord/DiscordAPI.java b/src/main/java/com/namelessmc/java_api/modules/discord/DiscordAPI.java new file mode 100644 index 00000000..18946771 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/discord/DiscordAPI.java @@ -0,0 +1,159 @@ +package com.namelessmc.java_api.modules.discord; + +import com.google.common.base.Preconditions; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.namelessmc.java_api.NamelessAPI; +import com.namelessmc.java_api.RequestHandler; +import com.namelessmc.java_api.exception.NamelessException; +import com.namelessmc.java_api.modules.NamelessModule; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.net.URL; +import java.util.Map; +import java.util.Objects; + +public class DiscordAPI { + + private final RequestHandler requests; + + public DiscordAPI(NamelessAPI api) throws NamelessException { + this.requests = api.requests(); + api.ensureModuleInstalled(NamelessModule.DISCORD_INTEGRATION); + } + + /** + * Set Discord bot URL (Nameless-Link internal webserver) + * @param url Discord bot URL + * @see #updateBotSettings(URL, long, String, long) + */ + public void updateBotUrl(final @NonNull URL url) throws NamelessException { + Objects.requireNonNull(url, "Bot url is null"); + + final JsonObject json = new JsonObject(); + json.addProperty("url", url.toString()); + this.requests.post("discord/update-bot-settings", json); + } + + /** + * Set discord bot username and user id + * @param username Bot username#tag + * @param userId Bot user id + * @see #updateBotSettings(URL, long, String, long) + */ + public void updateBotUser(final @NonNull String username, final long userId) throws NamelessException { + Objects.requireNonNull(username, "Bot username is null"); + + final JsonObject json = new JsonObject(); + json.addProperty("bot_username", username); + json.addProperty("bot_user_id", userId + ""); + this.requests.post("discord/update-bot-settings", json); + } + + /** + * Set Discord guild (server) id + * @param guildId Discord guild (server) id + * @see #updateBotSettings(URL, long, String, long) + */ + public void updateGuildId(final long guildId) throws NamelessException { + final JsonObject json = new JsonObject(); + json.addProperty("guild_id", guildId + ""); + this.requests.post("discord/update-bot-settings", json); + } + + /** + * Update all Discord bot settings. + * @param url Discord bot URL + * @param guildId Discord guild (server) id + * @param username Discord bot username#tag + * @param userId Discord bot user id + * @see #updateBotUrl(URL) + * @see #updateGuildId(long) + * @see #updateBotUser(String, long) + */ + public void updateBotSettings(final @NonNull URL url, + final long guildId, + final @NonNull String username, + final long userId) throws NamelessException { + Objects.requireNonNull(url, "Bot url is null"); + Objects.requireNonNull(username, "Bot username is null"); + + final JsonObject json = new JsonObject(); + json.addProperty("url", url.toString()); + json.addProperty("guild_id", guildId + ""); + json.addProperty("bot_username", username); + json.addProperty("bot_user_id", userId + ""); + this.requests.post("discord/update-bot-settings", json); + } + + /** + * Update Discord username for a NamelessMC user associated with the provided Discord user id + * @param discordUserId Discord user id + * @param discordUsername New Discord [username#tag]s + * @see #updateDiscordUsernames(long[], String[]) + */ + public void updateDiscordUsername(final long discordUserId, + final @NonNull String discordUsername) + throws NamelessException { + Objects.requireNonNull(discordUsername, "Discord username is null"); + + final JsonObject user = new JsonObject(); + user.addProperty("id", discordUserId); + user.addProperty("name", discordUsername); + final JsonArray users = new JsonArray(); + users.add(user); + final JsonObject json = new JsonObject(); + json.add("users", users); + this.requests.post("discord/update-usernames", json); + } + + /** + * Update Discord usernames in bulk + * @param discordUserIds Discord user ids + * @param discordUsernames New Discord [username#tag]s + * @see #updateDiscordUsername(long, String) + */ + public void updateDiscordUsernames(final long@NonNull[] discordUserIds, + final @NonNull String@NonNull[] discordUsernames) + throws NamelessException { + Objects.requireNonNull(discordUserIds, "User ids array is null"); + Objects.requireNonNull(discordUsernames, "Usernames array is null"); + Preconditions.checkArgument(discordUserIds.length == discordUsernames.length, + "discord user ids and discord usernames must be of same length"); + + if (discordUserIds.length == 0) { + return; + } + + final JsonArray users = new JsonArray(); + + for (int i = 0; i < discordUserIds.length; i++) { + final JsonObject user = new JsonObject(); + user.addProperty("id", discordUserIds[i]); + user.addProperty("name", discordUsernames[i]); + users.add(user); + } + + final JsonObject json = new JsonObject(); + json.add("users", users); + this.requests.post("discord/update-usernames", json); + } + + /** + * Send list of Discord roles to the website for populating the dropdown in StaffCP > API > Group sync + * @param discordRoles Map of Discord roles, key is role id, value is role name + */ + public void updateRoleList(final @NonNull Map discordRoles) throws NamelessException { + final JsonArray roles = new JsonArray(); + discordRoles.forEach((id, name) -> { + final JsonObject role = new JsonObject(); + role.addProperty("id", id); + role.addProperty("name", name); + roles.add(role); + }); + final JsonObject json = new JsonObject(); + json.add("roles", roles); + this.requests.post("discord/submit-role-list", json); + } + +} diff --git a/src/main/java/com/namelessmc/java_api/modules/discord/DiscordUser.java b/src/main/java/com/namelessmc/java_api/modules/discord/DiscordUser.java new file mode 100644 index 00000000..1cb846bb --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/discord/DiscordUser.java @@ -0,0 +1,43 @@ +package com.namelessmc.java_api.modules.discord; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import com.google.gson.JsonObject; +import com.namelessmc.java_api.NamelessUser; +import com.namelessmc.java_api.RequestHandler; +import com.namelessmc.java_api.exception.NamelessException; +import com.namelessmc.java_api.modules.NamelessModule; + +public class DiscordUser { + + private final NamelessUser user; + private final RequestHandler requests; + + public DiscordUser(NamelessUser user) throws NamelessException { + this.user = user; + this.requests = user.api().requests(); + user.api().ensureModuleInstalled(NamelessModule.DISCORD_INTEGRATION); + } + + /** + * @deprecated Replaced by {@link #syncGroups(long[], long[])} + */ + @Deprecated + public void updateDiscordRoles(final long@NonNull [] roleIds) throws NamelessException { + final JsonObject post = new JsonObject(); + post.addProperty("user", this.user.id()); + post.add("roles", this.requests.gson().toJsonTree(roleIds)); + this.requests.post("discord/set-roles", post); + } + + /** + * Available from NamelessMC 2.2.0+. Use with a fallback to {@link #syncRoles(long[], long[])}. + * @throws NamelessException + */ + public void syncRoles(final long[] addedRolesIds, final long[] removedRoleIds) throws NamelessException { + final JsonObject post = new JsonObject(); + post.add("add", this.requests.gson().toJsonTree(addedRolesIds)); + post.add("remove", this.requests.gson().toJsonTree(removedRoleIds)); + this.requests.post("discord/" + this.user.userTransformer() + "/sync-roles", post); + } +} diff --git a/src/main/java/com/namelessmc/java_api/modules/store/PaymentStatus.java b/src/main/java/com/namelessmc/java_api/modules/store/PaymentStatus.java new file mode 100644 index 00000000..eac20841 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/store/PaymentStatus.java @@ -0,0 +1,15 @@ +package com.namelessmc.java_api.modules.store; + +public enum PaymentStatus { + + PAYMENT_PENDING, // 0 + PAYMENT_COMPLETE, // 1 + PAYMENT_REFUNDED, // 2 + PAYMENT_CHARGED_BACK, // 3 + PAYMENT_DENIED, // 4 + + ; + + static final PaymentStatus[] BY_ID = PaymentStatus.values(); + +} diff --git a/src/main/java/com/namelessmc/java_api/modules/store/PaymentsFilter.java b/src/main/java/com/namelessmc/java_api/modules/store/PaymentsFilter.java new file mode 100644 index 00000000..9d7de754 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/store/PaymentsFilter.java @@ -0,0 +1,55 @@ +package com.namelessmc.java_api.modules.store; + +import java.util.Objects; + +public class PaymentsFilter { + + private final String name; + private final String value; + + private PaymentsFilter(String name, String value) { + this.name = Objects.requireNonNull(name); + this.value = Objects.requireNonNull(value); + } + + public String name() { + return this.name; + } + + public String value() { + return this.value; + } + + public static PaymentsFilter order(int orderId) { + return new PaymentsFilter("order", String.valueOf(orderId)); + } + + public static PaymentsFilter gateway(int gatewayId) { + return new PaymentsFilter("gateway", String.valueOf(gatewayId)); + } + + public static PaymentsFilter status(PaymentStatus status) { + return new PaymentsFilter("status", String.valueOf(status.ordinal())); + } + + public static PaymentsFilter payingCustomer(int customerId) { + return new PaymentsFilter("customer", String.valueOf(customerId)); + } + + public static PaymentsFilter payingCustomer(StoreCustomer customer) { + return payingCustomer(customer.id()); + } + + public static PaymentsFilter receivingCustomer(int customerId) { + return new PaymentsFilter("recipient", String.valueOf(customerId)); + } + + public static PaymentsFilter receivingCustomer(StoreCustomer customer) { + return payingCustomer(customer.id()); + } + + public static PaymentsFilter limit(int limit) { + return new PaymentsFilter("limit", String.valueOf(limit)); + } + +} diff --git a/src/main/java/com/namelessmc/java_api/modules/store/PendingCommandsResponse.java b/src/main/java/com/namelessmc/java_api/modules/store/PendingCommandsResponse.java new file mode 100644 index 00000000..7a001ac5 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/store/PendingCommandsResponse.java @@ -0,0 +1,95 @@ +package com.namelessmc.java_api.modules.store; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.namelessmc.java_api.NamelessAPI; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.ArrayList; +import java.util.List; + +public class PendingCommandsResponse { + + private final boolean useUuids; + private final List customers; + + PendingCommandsResponse(NamelessAPI api, JsonObject json) { + this.useUuids = json.get("online_mode").getAsBoolean(); + JsonArray customers = json.getAsJsonArray("customers"); + this.customers = new ArrayList<>(customers.size()); + for (JsonElement element : customers) { + this.customers.add(new PendingCommandsCustomer(api, element.getAsJsonObject())); + } + } + + public boolean shouldUseUuids() { + return this.useUuids; + } + + public List customers() { + return this.customers; + } + + public static class PendingCommandsCustomer extends StoreCustomer { + + private final List pendingCommands; + + private PendingCommandsCustomer(NamelessAPI api, JsonObject json) { + super(api, json); + + JsonArray commands = json.getAsJsonArray("commands"); + this.pendingCommands = new ArrayList<>(commands.size()); + for (JsonElement element : commands) { + this.pendingCommands.add(new PendingCommand(element.getAsJsonObject())); + } + } + + @Override + public @NonNull String username() { + String username = super.username(); + if (username == null) { + throw new IllegalStateException("Pending commands response cannot contain null username"); + } + return username; + } + + public List pendingCommands() { + return this.pendingCommands; + } + + } + + public static class PendingCommand { + + private final int id; + private final String command; + private final int orderId; + private final boolean requireOnline; + + private PendingCommand(JsonObject json) { + this.id = json.get("id").getAsInt(); + this.command = json.get("command").getAsString(); + this.orderId = json.get("order_id").getAsInt(); + this.requireOnline = json.get("require_online").getAsBoolean(); + } + + public int id() { + return id; + } + + public String command() { + return command; + } + + public int orderId() { + return orderId; + } + + public boolean isOnlineRequired() { + return requireOnline; + } + + } + +} diff --git a/src/main/java/com/namelessmc/java_api/modules/store/StoreAPI.java b/src/main/java/com/namelessmc/java_api/modules/store/StoreAPI.java new file mode 100644 index 00000000..883f3a77 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/store/StoreAPI.java @@ -0,0 +1,77 @@ +package com.namelessmc.java_api.modules.store; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.namelessmc.java_api.NamelessAPI; +import com.namelessmc.java_api.RequestHandler; +import com.namelessmc.java_api.exception.NamelessException; +import com.namelessmc.java_api.modules.NamelessModule; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class StoreAPI { + + private final NamelessAPI api; + private final RequestHandler requests; + + public StoreAPI(final NamelessAPI api) throws NamelessException { + this.api = api; + this.requests = api.requests(); + this.api.ensureModuleInstalled(NamelessModule.STORE); + } + + public List products() throws NamelessException { + JsonObject response = this.requests.get("store/products"); + JsonArray productsJson = response.getAsJsonArray("products"); + List products = new ArrayList<>(productsJson.size()); + for (JsonElement productElement : productsJson) { + products.add(new StoreProduct(productElement.getAsJsonObject())); + } + return Collections.unmodifiableList(products); + } + + public List payments(PaymentsFilter... filters) throws NamelessException { + Object[] params = new Object[filters.length * 2]; + for (int i = 0; i < filters.length; i++) { + params[i*2] = filters[i].name(); + params[i*2+1] = filters[i].value(); + } + JsonObject response = this.requests.get("store/payments", params); + JsonArray paymentsJson = response.getAsJsonArray("payments"); + List payments = new ArrayList<>(paymentsJson.size()); + for (JsonElement productElement : paymentsJson) { + payments.add(new StorePayment(this.api, productElement.getAsJsonObject())); + } + return Collections.unmodifiableList(payments); + } + + public List categories() throws NamelessException { + JsonObject response = this.requests.get("store/products"); + JsonArray array = response.getAsJsonArray("categories"); + List categories = new ArrayList<>(array.size()); + for (JsonElement element : array) { + categories.add(new StoreCategory(element.getAsJsonObject())); + } + return Collections.unmodifiableList(categories); + } + + public PendingCommandsResponse pendingCommands(int connectionId) throws NamelessException { + JsonObject response = this.requests.get("store/pending-commands", "connection_id", connectionId); + return new PendingCommandsResponse(this.api, response); + } + + public void markCommandsExecuted(Collection commands) throws NamelessException { + JsonArray array = new JsonArray(commands.size()); + for (PendingCommandsResponse.PendingCommand command : commands) { + array.add(command.id()); + } + JsonObject body = new JsonObject(); + body.add("commands", array); + this.requests.post("store/commands-executed", body); + } + +} diff --git a/src/main/java/com/namelessmc/java_api/modules/store/StoreCategory.java b/src/main/java/com/namelessmc/java_api/modules/store/StoreCategory.java new file mode 100644 index 00000000..62401a33 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/store/StoreCategory.java @@ -0,0 +1,35 @@ +package com.namelessmc.java_api.modules.store; + +import com.google.gson.JsonObject; + +public class StoreCategory { + + private final int id; + private final String name; + private final boolean hidden; + private final boolean disabled; + + StoreCategory(JsonObject json) { + this.id = json.get("id").getAsInt(); + this.name = json.get("name").getAsString(); + this.hidden = json.get("hidden").getAsBoolean(); + this.disabled = json.get("disabled").getAsBoolean(); + } + + public int id() { + return this.id; + } + + public String name() { + return this.name; + } + + public boolean isHidden() { + return this.hidden; + } + + public boolean isDisabled() { + return this.disabled; + } + +} diff --git a/src/main/java/com/namelessmc/java_api/modules/store/StoreCustomer.java b/src/main/java/com/namelessmc/java_api/modules/store/StoreCustomer.java new file mode 100644 index 00000000..6aa9474c --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/store/StoreCustomer.java @@ -0,0 +1,54 @@ +package com.namelessmc.java_api.modules.store; + +import com.google.gson.JsonObject; +import com.namelessmc.java_api.NamelessAPI; +import com.namelessmc.java_api.exception.NamelessException; +import com.namelessmc.java_api.NamelessUser; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.UUID; + +public class StoreCustomer { + + private final NamelessAPI api; + private final int id; + private final @Nullable Integer userId; + private final @Nullable String username; + private final @Nullable String identifier; + + StoreCustomer(NamelessAPI api, JsonObject json) { + this.api = api; + this.id = json.get("customer_id").getAsInt(); + this.userId = json.has("user_id") ? json.get("user_id").getAsInt() : null; + this.username = json.has("username") && !json.get("username").isJsonNull() + ? json.get("username").getAsString() : null; + this.identifier = json.has("identifier") && !json.get("identifier").isJsonNull() + ? json.get("identifier").getAsString() : null; + + if (this.username == null && this.identifier == null) { + throw new IllegalStateException("Username and identifier cannot be null at the same time"); + } + } + + public int id() { + return this.id; + } + + public @Nullable NamelessUser user() throws NamelessException { + return this.userId != null ? this.api.user(this.userId) : null; + } + + public @Nullable String username() { + return this.username; + } + + public @Nullable String identifier() { + return this.identifier; + } + + public @Nullable UUID identifierAsUuid() { + // Unlike NamelessMC, the store module sends UUIDs with dashes + return this.identifier != null ? UUID.fromString(this.identifier) : null; + } + +} diff --git a/src/main/java/com/namelessmc/java_api/modules/store/StorePayment.java b/src/main/java/com/namelessmc/java_api/modules/store/StorePayment.java new file mode 100644 index 00000000..f8f53f7e --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/store/StorePayment.java @@ -0,0 +1,103 @@ +package com.namelessmc.java_api.modules.store; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.namelessmc.java_api.NamelessAPI; +import com.namelessmc.java_api.util.GsonHelper; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +public class StorePayment { + + private final int id; + private final int orderId; + private final int gatewayId; + private final @Nullable String transaction; + private final String amount; + private final String currency; + private final String fee; + private final PaymentStatus status; + private final Date creationDate; + private final Date lastUpdateDate; + private final StoreCustomer payingCustomer; + private final StoreCustomer receivingCustomer; + private final List products; + + StorePayment(NamelessAPI api, JsonObject json) { + this.id = json.get("id").getAsInt(); + this.orderId = json.get("order_id").getAsInt(); + this.gatewayId = json.get("gateway_id").getAsInt(); + this.transaction = GsonHelper.getNullableString(json, "transaction"); + this.amount = json.get("amount").getAsString(); + this.currency = json.get("currency").getAsString(); + this.fee = json.get("fee").getAsString(); + this.status = PaymentStatus.BY_ID[json.get("status_id").getAsInt()]; + this.creationDate = new Date(json.get("created").getAsLong() * 1000); + this.lastUpdateDate = new Date(json.get("last_updated").getAsLong() * 1000); + this.payingCustomer = new StoreCustomer(api, json.getAsJsonObject("customer")); + this.receivingCustomer = new StoreCustomer(api, json.getAsJsonObject("recipient")); + + JsonArray productsJson = json.getAsJsonArray("products"); + this.products = new ArrayList<>(productsJson.size()); + for (JsonElement element : productsJson) { + this.products.add(new StorePaymentProduct(element.getAsJsonObject())); + } + } + + public int id() { + return id; + } + + public int orderId() { + return orderId; + } + + public int gatewayId() { + return gatewayId; + } + + public @Nullable String transaction() { + return transaction; + } + + public String amount() { + return amount; + } + + public String currency() { + return currency; + } + + public String fee() { + return fee; + } + + public PaymentStatus status() { + return status; + } + + public Date creationDate() { + return creationDate; + } + + public Date lastUpdatedDate() { + return lastUpdateDate; + } + + public StoreCustomer payingCustomer() { + return payingCustomer; + } + + public StoreCustomer receivingCustomer() { + return receivingCustomer; + } + + public List products() { + return products; + } + +} diff --git a/src/main/java/com/namelessmc/java_api/modules/store/StorePaymentProduct.java b/src/main/java/com/namelessmc/java_api/modules/store/StorePaymentProduct.java new file mode 100644 index 00000000..05ba7cb9 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/store/StorePaymentProduct.java @@ -0,0 +1,23 @@ +package com.namelessmc.java_api.modules.store; + +import com.google.gson.JsonObject; + +public class StorePaymentProduct { + + private final int id; + private final String name; + + StorePaymentProduct(JsonObject json) { + this.id = json.get("id").getAsInt(); + this.name = json.get("name").getAsString(); + } + + public int id() { + return id; + } + + public String name() { + return name; + } + +} diff --git a/src/main/java/com/namelessmc/java_api/modules/store/StoreProduct.java b/src/main/java/com/namelessmc/java_api/modules/store/StoreProduct.java new file mode 100644 index 00000000..5920239f --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/store/StoreProduct.java @@ -0,0 +1,89 @@ +package com.namelessmc.java_api.modules.store; + +import com.google.gson.JsonObject; +import com.namelessmc.java_api.util.GsonHelper; + +import java.util.List; + +public class StoreProduct { + + private final int id; + private final int categoryId; + private final String name; + private final int priceCents; + private final boolean hidden; + private final boolean disabled; + private final int[] requiredProductsIds; + private final int[] requiredGroupsIds; + private final int[] requiredIntegrationsIds; + private final String descriptionHtml; + private final List fields; + private final List actions; + + StoreProduct(JsonObject json) { + if (!json.has("price_cents")) { + throw new IllegalArgumentException("Missing price_cents, are you using an old store module version?"); + } + this.id = json.get("id").getAsInt(); + this.categoryId = json.get("category_id").getAsInt(); + this.name = json.get("name").getAsString(); + this.priceCents = json.get("price_cents").getAsInt(); + this.hidden = json.get("hidden").getAsBoolean(); + this.disabled = json.get("disabled").getAsBoolean(); + this.requiredProductsIds = GsonHelper.toIntArray(json.getAsJsonArray("required_products")); + this.requiredGroupsIds = GsonHelper.toIntArray(json.getAsJsonArray("required_groups")); + this.requiredIntegrationsIds = GsonHelper.toIntArray(json.getAsJsonArray("required_integrations")); + this.descriptionHtml = json.get("description").getAsString(); + this.fields = GsonHelper.toObjectList(json.getAsJsonArray("fields"), StoreProductField::new); + this.actions = GsonHelper.toObjectList(json.getAsJsonArray("actions"), StoreProductAction::new); + } + + public int id() { + return this.id; + } + + public int categoryId() { + return this.categoryId; + } + + public String name() { + return this.name; + } + + public int priceCents() { + return this.priceCents; + } + + public boolean isHidden() { + return this.hidden; + } + + public boolean isDisabled() { + return this.disabled; + } + + public int[] requiredProductsIds() { + return this.requiredProductsIds; + } + + public int[] requiredGroupsIds() { + return this.requiredGroupsIds; + } + + public int[] requiredIntegrationsIds() { + return this.requiredIntegrationsIds; + } + + public String descriptionHtml() { + return this.descriptionHtml; + } + + public List fields() { + return this.fields; + } + + public List actions() { + return this.actions; + } + +} diff --git a/src/main/java/com/namelessmc/java_api/modules/store/StoreProductAction.java b/src/main/java/com/namelessmc/java_api/modules/store/StoreProductAction.java new file mode 100644 index 00000000..1f4dc467 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/store/StoreProductAction.java @@ -0,0 +1,48 @@ +package com.namelessmc.java_api.modules.store; + +import com.google.gson.JsonObject; + +public class StoreProductAction { + + private final int id; + private final int typeId; // TODO enum + private final int serviceId; + private final String command; + private final boolean requireOnline; + private final boolean ownConnections; + + StoreProductAction(JsonObject json) { + this.id = json.get("id").getAsInt(); + this.typeId = json.get("type").getAsInt(); + this.serviceId = json.get("service_id").getAsInt(); + this.command = json.get("command").getAsString(); + this.requireOnline = json.get("require_online").getAsBoolean(); + this.ownConnections = json.get("own_connections").getAsBoolean(); + } + + public int id() { + return this.id; + } + + @Deprecated + public int typeId() { + return this.typeId; + } + + public int serviceId() { + return this.serviceId; + } + + public String command() { + return this.command; + } + + public boolean requireOnline() { + return this.requireOnline; + } + + public boolean ownConnections() { + return this.ownConnections; + } + +} diff --git a/src/main/java/com/namelessmc/java_api/modules/store/StoreProductField.java b/src/main/java/com/namelessmc/java_api/modules/store/StoreProductField.java new file mode 100644 index 00000000..982d7671 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/store/StoreProductField.java @@ -0,0 +1,56 @@ +package com.namelessmc.java_api.modules.store; + +import com.google.gson.JsonObject; +import com.namelessmc.java_api.util.GsonHelper; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class StoreProductField { + + private final int id; + private final String identifier; + private final int typeId; // TODO enum + private final boolean required; + private final int min; + private final @Nullable String regex; + private final String defaultValue; + + public StoreProductField(JsonObject json) { + this.id = json.get("id").getAsInt(); + this.identifier = json.get("identifier").getAsString(); + this.typeId = json.get("type").getAsInt(); + this.required = json.get("required").getAsBoolean(); + this.min = json.get("min").getAsInt(); + this.regex = GsonHelper.getNullableString(json, "regex"); + this.defaultValue = json.get("default_value").getAsString(); + } + + public int id() { + return this.id; + } + + public String identifier() { + return this.identifier; + } + + @Deprecated + public int typeId() { + return this.typeId; + } + + public boolean required() { + return this.required; + } + + public int min() { + return this.min; + } + + public @Nullable String regex() { + return this.regex; + } + + public String defaultValue() { + return this.defaultValue; + } + +} diff --git a/src/main/java/com/namelessmc/java_api/modules/store/StoreUser.java b/src/main/java/com/namelessmc/java_api/modules/store/StoreUser.java new file mode 100644 index 00000000..ad3334ed --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/store/StoreUser.java @@ -0,0 +1,87 @@ +package com.namelessmc.java_api.modules.store; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.namelessmc.java_api.NamelessUser; +import com.namelessmc.java_api.RequestHandler; +import com.namelessmc.java_api.exception.NamelessException; +import com.namelessmc.java_api.modules.NamelessModule; + +import java.util.List; + +public class StoreUser { + + private final NamelessUser user; + private final RequestHandler requests; + + public StoreUser(NamelessUser user) throws NamelessException { + this.user = user; + this.requests = user.api().requests(); + user.api().ensureModuleInstalled(NamelessModule.STORE); + } + + @Deprecated + public void addCredits(float creditsToAdd) throws NamelessException { + JsonObject body = new JsonObject(); + body.addProperty("credits", creditsToAdd); + this.requests.post("users/" + this.user.userTransformer() + "/add-credits", body); + } + + public void addCredits(int cents) throws NamelessException { + // Module does not support adding cents yet + this.addCredits(cents / 100f); + } + + @Deprecated + public void removeCredits(float creditsToRemove) throws NamelessException { + JsonObject body = new JsonObject(); + body.addProperty("credits", creditsToRemove); + this.requests.post("users/" + this.user.userTransformer() + "/remove-credits", body); + } + + public void removeCredits(int cents) throws NamelessException { + // Module does not support removing cents yet + this.removeCredits(cents / 100f); + } + + @Deprecated + public float credits() throws NamelessException { + JsonObject response = this.requests.get("users/" + this.user.userTransformer() + "/credits"); + return response.get("credits").getAsFloat(); + } + + public int creditsCents() throws NamelessException { + JsonObject response = this.requests.get("users/" + this.user.userTransformer() + "/credits"); + return response.get("cents").getAsInt(); + } + + /** + * Credits, formatted as a float string with 2 decimals + */ + public String creditsDisplay() throws NamelessException { + return String.format("%.2f", this.creditsCents() / 100f); + } + + public int customerId() throws NamelessException { + JsonObject response = this.requests.get("users/" + this.user.userTransformer() + "/credits"); + return response.get("customer_id").getAsInt(); + } + + public void createOrder(StoreCustomer purchaser, StoreCustomer recipient, List products) throws NamelessException { + JsonArray productIds = new JsonArray(products.size()); + for (int i = 0; i < products.size(); i++) { + productIds.add(products.get(i).id()); + } + this.createOrder(purchaser.id(), recipient.id(), productIds); + } + + public void createOrder(int purchaserCustomerId, int recipientCustomerId, JsonArray productIds) throws NamelessException { + JsonObject body = new JsonObject(); + body.addProperty("user", this.user.userTransformer()); + body.addProperty("customer", purchaserCustomerId); + body.addProperty("recipient", recipientCustomerId); + body.add("products", productIds); + this.requests.post("store/order/create", body); + } + +} diff --git a/src/main/java/com/namelessmc/java_api/modules/suggestions/Suggestion.java b/src/main/java/com/namelessmc/java_api/modules/suggestions/Suggestion.java new file mode 100644 index 00000000..b3f4eee4 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/suggestions/Suggestion.java @@ -0,0 +1,102 @@ +package com.namelessmc.java_api.modules.suggestions; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Date; + +import com.google.gson.JsonObject; +import com.namelessmc.java_api.NamelessAPI; +import com.namelessmc.java_api.exception.NamelessException; + +public class Suggestion { + + private final int id; + private final URL url; + private final SuggestionUser author; + private final SuggestionUser updatedBy; + private final SuggestionStatus status; + private final SuggestionCategory category; + private final String title; + private final String content; + private final int views; + private final Date createdTime; + private final Date lastUpdatedTime; + private final int likeCount; + private final int dislikeCount; + + Suggestion(final NamelessAPI api, final JsonObject json) throws NamelessException { + this.id = json.get("id").getAsInt(); + final String urlString = json.get("link").getAsString(); + try { + this.url = new URI(urlString).toURL(); + } catch (MalformedURLException | URISyntaxException e) { + throw new NamelessException("Website provided invalid suggestion URL: " + urlString, e); + } + this.author = new SuggestionUser(api, json.getAsJsonObject("author")); + this.updatedBy = new SuggestionUser(api, json.getAsJsonObject("updated_by")); + this.status = new SuggestionStatus(json.getAsJsonObject("status")); + this.category = new SuggestionCategory(json.getAsJsonObject("category")); + this.title = json.get("title").getAsString(); + this.content = json.get("content").getAsString(); + this.views = json.get("views").getAsInt(); + this.createdTime = new Date(json.get("created").getAsLong() * 1000); + this.lastUpdatedTime = new Date(json.get("last_updated").getAsLong() * 1000); + this.likeCount = json.get("likes_count").getAsInt(); + this.dislikeCount = json.get("dislikes_count").getAsInt(); + } + + public int id() { + return this.id; + } + + public URL url() { + return this.url; + } + + public SuggestionUser author() { + return this.author; + } + + public SuggestionUser updatedBy() { + return this.updatedBy; + } + + public SuggestionStatus status() { + return this.status; + } + + public SuggestionCategory category() { + return this.category; + } + + public String title() { + return this.title; + } + + public String content() { + return this.content; + } + + public int views() { + return this.views; + } + + public Date createdTime() { + return this.createdTime; + } + + public Date lastUpdatedTime() { + return this.lastUpdatedTime; + } + + public int likeCount() { + return this.likeCount; + } + + public int dislikeCount() { + return this.dislikeCount; + } + +} diff --git a/src/main/java/com/namelessmc/java_api/modules/suggestions/SuggestionCategory.java b/src/main/java/com/namelessmc/java_api/modules/suggestions/SuggestionCategory.java new file mode 100644 index 00000000..b62269fc --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/suggestions/SuggestionCategory.java @@ -0,0 +1,23 @@ +package com.namelessmc.java_api.modules.suggestions; + +import com.google.gson.JsonObject; + +public class SuggestionCategory { + + private final int id; + private final String name; + + SuggestionCategory(final JsonObject json) { + this.id = json.get("id").getAsInt(); + this.name = json.get("name").getAsString(); + } + + public int id() { + return this.id; + } + + public String name() { + return this.name; + } + +} diff --git a/src/main/java/com/namelessmc/java_api/modules/suggestions/SuggestionStatus.java b/src/main/java/com/namelessmc/java_api/modules/suggestions/SuggestionStatus.java new file mode 100644 index 00000000..7d0c5231 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/suggestions/SuggestionStatus.java @@ -0,0 +1,29 @@ +package com.namelessmc.java_api.modules.suggestions; + +import com.google.gson.JsonObject; + +public class SuggestionStatus { + + private final int id; + private final String name; + private final boolean open; + + SuggestionStatus(JsonObject json) { + this.id = json.get("id").getAsInt(); + this.name = json.get("name").getAsString(); + this.open = json.get("open").getAsBoolean(); + } + + public int id() { + return this.id; + } + + public String name() { + return this.name; + } + + public boolean isOpen() { + return this.open; + } + +} diff --git a/src/main/java/com/namelessmc/java_api/modules/suggestions/SuggestionUser.java b/src/main/java/com/namelessmc/java_api/modules/suggestions/SuggestionUser.java new file mode 100644 index 00000000..a22ef15c --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/suggestions/SuggestionUser.java @@ -0,0 +1,38 @@ +package com.namelessmc.java_api.modules.suggestions; + +import com.google.gson.JsonObject; +import com.namelessmc.java_api.NamelessAPI; +import com.namelessmc.java_api.NamelessUser; +import com.namelessmc.java_api.exception.NamelessException; + +public class SuggestionUser { + + private final NamelessAPI api; + + private final int id; + private final String username; + + SuggestionUser(final NamelessAPI api, final JsonObject json) { + this.api = api; + + this.id = json.get("id").getAsInt(); + this.username = json.get("username").getAsString(); + } + + public int userId() { + return this.id; + } + + public NamelessUser user() throws NamelessException { + NamelessUser user = this.api.user(this.id); + if (user == null) { + throw new IllegalStateException("Suggestions module returned a user id that doesn't exist"); + } + return user; + } + + public String username() { + return username; + } + +} diff --git a/src/main/java/com/namelessmc/java_api/modules/suggestions/SuggestionsAPI.java b/src/main/java/com/namelessmc/java_api/modules/suggestions/SuggestionsAPI.java new file mode 100644 index 00000000..8542c6d2 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/suggestions/SuggestionsAPI.java @@ -0,0 +1,25 @@ +package com.namelessmc.java_api.modules.suggestions; + +import com.google.gson.JsonObject; +import com.namelessmc.java_api.NamelessAPI; +import com.namelessmc.java_api.RequestHandler; +import com.namelessmc.java_api.exception.NamelessException; +import com.namelessmc.java_api.modules.NamelessModule; + +public class SuggestionsAPI { + + private final NamelessAPI api; + private final RequestHandler requests; + + public SuggestionsAPI(final NamelessAPI api) throws NamelessException { + this.api = api; + this.requests = api.requests(); + api.ensureModuleInstalled(NamelessModule.SUGGESTIONS); + } + + public Suggestion suggestion(int suggestionId) throws NamelessException { + final JsonObject response = this.requests.get("suggestions/" + suggestionId); + return new Suggestion(this.api, response); + } + +} diff --git a/src/main/java/com/namelessmc/java_api/modules/suggestions/SuggestionsUser.java b/src/main/java/com/namelessmc/java_api/modules/suggestions/SuggestionsUser.java new file mode 100644 index 00000000..c98cd566 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/suggestions/SuggestionsUser.java @@ -0,0 +1,66 @@ +package com.namelessmc.java_api.modules.suggestions; + +import com.google.gson.JsonObject; +import com.namelessmc.java_api.NamelessAPI; +import com.namelessmc.java_api.NamelessUser; +import com.namelessmc.java_api.RequestHandler; +import com.namelessmc.java_api.exception.NamelessException; +import com.namelessmc.java_api.modules.NamelessModule; + +import java.util.Objects; + +public class SuggestionsUser { + + private final NamelessUser user; + private final NamelessAPI api; + private final RequestHandler requests; + + public SuggestionsUser(final NamelessUser user) throws NamelessException { + this.user = user; + this.api = this.user.api(); + this.requests = this.api.requests(); + + this.api.ensureModuleInstalled(NamelessModule.SUGGESTIONS); + } + + public void like(final int suggestionId) throws NamelessException { + JsonObject body = new JsonObject(); + body.addProperty("user", this.user.userTransformer()); + this.requests.post("suggestions/like", body); + } + + public void like(final Suggestion suggestion) throws NamelessException { + this.like(suggestion.id()); + } + + public void dislike(final int suggestionId) throws NamelessException { + final JsonObject body = new JsonObject(); + body.addProperty("user", this.user.userTransformer()); + this.requests.post("suggestions/dislike", body); + } + + public void dislike(final Suggestion suggestion) throws NamelessException { + this.dislike(suggestion.id()); + } + + public Suggestion createSuggestion(final String title, final String content, final int categoryId) throws NamelessException { + final JsonObject body = new JsonObject(); + body.addProperty("user", this.user.userTransformer()); + body.addProperty("title", Objects.requireNonNull(title, "title is null")); + body.addProperty("content", Objects.requireNonNull(content, "content is null")); + if (categoryId > 0) { + body.addProperty("category", categoryId); + } + final JsonObject response = this.requests.post("suggestions/create", body); + return new Suggestion(this.api, response); + } + + public Suggestion createSuggestion(final String title, final String content) throws NamelessException { + return this.createSuggestion(title, content, -1); + } + + public Suggestion createSuggestion(final String title, final String content, final SuggestionCategory category) throws NamelessException { + return this.createSuggestion(title, content, category.id()); + } + +} diff --git a/src/com/namelessmc/java_api/modules/websend/WebsendAPI.java b/src/main/java/com/namelessmc/java_api/modules/websend/WebsendAPI.java similarity index 58% rename from src/com/namelessmc/java_api/modules/websend/WebsendAPI.java rename to src/main/java/com/namelessmc/java_api/modules/websend/WebsendAPI.java index b95e443d..4eb5cf84 100644 --- a/src/com/namelessmc/java_api/modules/websend/WebsendAPI.java +++ b/src/main/java/com/namelessmc/java_api/modules/websend/WebsendAPI.java @@ -3,25 +3,27 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import com.namelessmc.java_api.NamelessException; +import com.namelessmc.java_api.NamelessAPI; import com.namelessmc.java_api.RequestHandler; -import org.jetbrains.annotations.NotNull; +import com.namelessmc.java_api.exception.NamelessException; +import com.namelessmc.java_api.modules.NamelessModule; +import org.checkerframework.checker.nullness.qual.NonNull; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Objects; public class WebsendAPI { - private final @NotNull RequestHandler requests; + private final RequestHandler requests; - public WebsendAPI(@NotNull RequestHandler requests) { - this.requests = Objects.requireNonNull(requests, "Request handler is null"); + public WebsendAPI(final NamelessAPI api) throws NamelessException { + this.requests = api.requests(); + api.ensureModuleInstalled(NamelessModule.WEBSEND); } - public @NotNull List getCommands(int serverId) throws NamelessException { + public @NonNull List commands(int serverId) throws NamelessException { JsonObject response = this.requests.get("websend/commands","server_id", serverId); JsonArray commandsJson = response.getAsJsonArray("commands"); List commands = new ArrayList<>(commandsJson.size()); @@ -34,9 +36,14 @@ public WebsendAPI(@NotNull RequestHandler requests) { return Collections.unmodifiableList(commands); } - public void sendConsoleLog(int serverId, @NotNull Collection lines) throws NamelessException { + public void sendConsoleLog(int serverId, Collection lines) throws NamelessException { + sendConsoleLog(serverId, lines, false); + } + + public void sendConsoleLog(int serverId, Collection lines, boolean clearPrevious) throws NamelessException { JsonObject body = new JsonObject(); body.addProperty("server_id", serverId); + body.addProperty("clear_previous", clearPrevious); JsonArray content = new JsonArray(); for (String line : lines) { content.add(line); diff --git a/src/main/java/com/namelessmc/java_api/modules/websend/WebsendCommand.java b/src/main/java/com/namelessmc/java_api/modules/websend/WebsendCommand.java new file mode 100644 index 00000000..180afce0 --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/modules/websend/WebsendCommand.java @@ -0,0 +1,25 @@ +package com.namelessmc.java_api.modules.websend; + +import org.checkerframework.checker.index.qual.Positive; +import org.checkerframework.checker.nullness.qual.NonNull; + +public class WebsendCommand { + + private final @Positive int id; + private final @NonNull String commandLine; + + public WebsendCommand(final @Positive int id, + final @NonNull String commandLine) { + this.id = id; + this.commandLine = commandLine; + } + + public @Positive int id() { + return id; + } + + public @NonNull String command() { + return this.commandLine; + } + +} diff --git a/src/main/java/com/namelessmc/java_api/util/GsonHelper.java b/src/main/java/com/namelessmc/java_api/util/GsonHelper.java new file mode 100644 index 00000000..07be020a --- /dev/null +++ b/src/main/java/com/namelessmc/java_api/util/GsonHelper.java @@ -0,0 +1,55 @@ +package com.namelessmc.java_api.util; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.*; +import java.util.function.Function; + +public class GsonHelper { + + public static int[] toIntArray(JsonArray jsonArray) { + int[] array = new int[jsonArray.size()]; + for (int i = 0; i < jsonArray.size(); i++) { + array[i] = jsonArray.get(i).getAsInt(); + } + return array; + } + + public static long[] toLongArray(JsonArray jsonArray) { + long[] array = new long[jsonArray.size()]; + for (int i = 0; i < jsonArray.size(); i++) { + array[i] = jsonArray.get(i).getAsLong(); + } + return array; + } + + public static Set toIntegerSet(JsonArray jsonArray) { + Set set = new HashSet<>(); + for (JsonElement elem : jsonArray) { + set.add(elem.getAsInt()); + } + return Collections.unmodifiableSet(set); + } + + public static List toObjectList(JsonArray array, Function constructor) { + List list = new ArrayList<>(array.size()); + for (JsonElement e : array) { + list.add(constructor.apply(e.getAsJsonObject())); + } + return Collections.unmodifiableList(list); + } + + public static @Nullable String getNullableString(JsonObject object, String key) { + if (object.has(key)) { + JsonElement e = object.get(key); + if (!e.isJsonNull()) { + return e.getAsString(); + } + } + return null; + } + +} diff --git a/src/test/java/TestTls.java b/src/test/java/TestTls.java new file mode 100644 index 00000000..bc447761 --- /dev/null +++ b/src/test/java/TestTls.java @@ -0,0 +1,22 @@ +import java.net.MalformedURLException; +import java.net.URI; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.google.gson.JsonObject; +import com.namelessmc.java_api.NamelessAPI; +import com.namelessmc.java_api.exception.NamelessException; + +public class TestTls { + + @Test + void checkTlsVersion() throws MalformedURLException, NamelessException { + final NamelessAPI api = NamelessAPI.builder(URI.create("https://check-tls.akamai.io/").toURL(), "").build(); + final JsonObject response = api.requests().get("v1/tlsinfo.json"); + Assertions.assertEquals(response.get("tls_sni_status").getAsString(), "present"); + Assertions.assertEquals(response.get("tls_version").getAsString(), "tls1.3"); + Assertions.assertEquals(response.get("tls_sni_value").getAsString(), "check-tls.akamai.io"); + } + +} diff --git a/src/test/java/TestUuid.java b/src/test/java/TestUuid.java new file mode 100644 index 00000000..dbce2077 --- /dev/null +++ b/src/test/java/TestUuid.java @@ -0,0 +1,17 @@ +import com.namelessmc.java_api.NamelessAPI; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +public class TestUuid { + + @Test + void testUuidConversion() { + UUID java = UUID.fromString("09948878-fe20-44e3-a072-42c39869dd1f"); + String website = "09948878fe2044e3a07242c39869dd1f"; + Assertions.assertEquals(NamelessAPI.javaUuidToWebsiteUuid(java), website); + Assertions.assertEquals(NamelessAPI.websiteUuidToJavaUuid(website), java); + } + +}