diff --git a/.editorconfig b/.editorconfig index df07143..155c26f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,6 +14,10 @@ ij_java_lambda_brace_style = end_of_line ij_java_method_brace_style = end_of_line ij_java_names_count_to_use_import_on_demand = 999 +[*.kt] +ij_kotlin_name_count_to_use_star_import = 999 +ij_kotlin_name_count_to_use_star_import_for_members = 999 + [{*.gant,*.groovy,*.gy,*.gradle}] ij_groovy_block_brace_style = end_of_line ij_groovy_class_brace_style = end_of_line diff --git a/botfest/src/main/kotlin/net/modfest/botfest/Platform.kt b/botfest/src/main/kotlin/net/modfest/botfest/Platform.kt index ee62d60..931296b 100644 --- a/botfest/src/main/kotlin/net/modfest/botfest/Platform.kt +++ b/botfest/src/main/kotlin/net/modfest/botfest/Platform.kt @@ -172,6 +172,18 @@ class PlatformAuthenticated(var client: HttpClient, var discordUser: Snowflake) }.unwrapErrors(); } + suspend fun addMinecraft(username: String) { + client.put("/user/@me/minecraft/$username") { + addAuth() + }.unwrapErrors(); + } + + suspend fun removeMinecraft(username: String) { + client.delete("/user/@me/minecraft/$username") { + addAuth() + }.unwrapErrors(); + } + suspend fun submitModrinth(eventId: String, mrId: String): SubmissionResponseData { return client.post("/event/$eventId/submissions?type=modrinth") { addAuth() diff --git a/botfest/src/main/kotlin/net/modfest/botfest/extensions/UserCommands.kt b/botfest/src/main/kotlin/net/modfest/botfest/extensions/UserCommands.kt index 24b6bcc..72e7e01 100644 --- a/botfest/src/main/kotlin/net/modfest/botfest/extensions/UserCommands.kt +++ b/botfest/src/main/kotlin/net/modfest/botfest/extensions/UserCommands.kt @@ -1,22 +1,16 @@ package net.modfest.botfest.extensions import dev.kordex.core.commands.Arguments -import dev.kordex.core.commands.application.slash.converters.ChoiceEnum -import dev.kordex.core.commands.application.slash.converters.impl.enumChoice -import dev.kordex.core.commands.application.slash.converters.impl.stringChoice import dev.kordex.core.commands.application.slash.ephemeralSubCommand import dev.kordex.core.commands.converters.impl.optionalString import dev.kordex.core.commands.converters.impl.string import dev.kordex.core.extensions.Extension import dev.kordex.core.extensions.ephemeralSlashCommand -import dev.kordex.core.i18n.types.Key import dev.kordex.core.i18n.withContext import dev.kordex.core.koin.KordExKoinComponent import net.modfest.botfest.MAIN_GUILD_ID import net.modfest.botfest.Platform import net.modfest.botfest.i18n.Translations -import net.modfest.platform.pojo.CurrentEventData -import net.modfest.platform.pojo.UserData import net.modfest.platform.pojo.UserPatchData import org.koin.core.component.inject @@ -54,6 +48,38 @@ class UserCommands : Extension(), KordExKoinComponent { } } } + + // Allows the user to add a minecraft username + ephemeralSubCommand(::MinecraftUsernameArgs) { + name = Translations.Commands.User.Minecraft.Add.name + description = Translations.Commands.User.Minecraft.Add.description + + action { + platform.withAuth(this.user).addMinecraft(this.arguments.username) + + respond { + content = Translations.Commands.User.Minecraft.Add.response + .withContext(this@action) + .translateNamed() + } + } + } + + // Allows the user to remove a minecraft username + ephemeralSubCommand(::MinecraftUsernameArgs) { + name = Translations.Commands.User.Minecraft.Remove.name + description = Translations.Commands.User.Minecraft.Remove.description + + action { + platform.withAuth(this.user).removeMinecraft(this.arguments.username) + + respond { + content = Translations.Commands.User.Minecraft.Remove.response + .withContext(this@action) + .translateNamed() + } + } + } } } @@ -71,4 +97,11 @@ class UserCommands : Extension(), KordExKoinComponent { description = Translations.Arguments.Setuser.Bio.description } } + + inner class MinecraftUsernameArgs : Arguments() { + val username by string { + name = Translations.Arguments.Minecraft.Username.name + description = Translations.Arguments.Minecraft.Username.description + } + } } diff --git a/botfest/src/main/resources/translations/botfest/strings.properties b/botfest/src/main/resources/translations/botfest/strings.properties index cebc2a9..98a898b 100644 --- a/botfest/src/main/resources/translations/botfest/strings.properties +++ b/botfest/src/main/resources/translations/botfest/strings.properties @@ -48,6 +48,12 @@ commands.whoami.response=\ commands.user.set.name=set commands.user.set.description=Change information on your profile commands.user.set.response=Successfully changed data +commands.user.minecraft.add.name=minecraft add +commands.user.minecraft.add.description=Associate a minecraft username with your profile +commands.user.minecraft.add.response=Successfully added {username} ({uuid}) to your user. +commands.user.minecraft.remove.name=minecraft remove +commands.user.minecraft.remove.description=Remove a minecraft username from your profile +commands.user.minecraft.remove.response=Successfully removed {username} ({uuid}) from your user. commands.register.name=Register commands.register.description=Register for the current ModFest event commands.register.response.noevent=ModFest registrations are not currently open. \ @@ -110,6 +116,8 @@ arguments.submission.edit.name=submission arguments.submission.edit.description=The submission you want to edit arguments.submission.edit_image.name=image arguments.submission.edit_image.description=Your image +arguments.minecraft.username.name=username +arguments.minecraft.username.description=Minecraft username modal.register.title=Register for ModFest modal.register.name.label=What name would you like on your profile? diff --git a/common/src/main/java/net/modfest/platform/pojo/MinecraftProfile.java b/common/src/main/java/net/modfest/platform/pojo/MinecraftProfile.java new file mode 100644 index 0000000..1c19713 --- /dev/null +++ b/common/src/main/java/net/modfest/platform/pojo/MinecraftProfile.java @@ -0,0 +1,4 @@ +package net.modfest.platform.pojo; + +public record MinecraftProfile(String name, String id) { +} diff --git a/common/src/main/java/net/modfest/platform/pojo/UserData.java b/common/src/main/java/net/modfest/platform/pojo/UserData.java index ee9be2f..c2dbe0c 100644 --- a/common/src/main/java/net/modfest/platform/pojo/UserData.java +++ b/common/src/main/java/net/modfest/platform/pojo/UserData.java @@ -22,6 +22,7 @@ public record UserData( @Nullable String icon, Set badges, Set registered, + Set minecraftAccounts, @NonNull UserRole role ) implements Data { public UserData withRegistration(EventData event, boolean registered) { diff --git a/platform_api/src/main/java/net/modfest/platform/controller/UserController.java b/platform_api/src/main/java/net/modfest/platform/controller/UserController.java index 59d26ea..5a3be2b 100644 --- a/platform_api/src/main/java/net/modfest/platform/controller/UserController.java +++ b/platform_api/src/main/java/net/modfest/platform/controller/UserController.java @@ -23,6 +23,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Objects; @@ -170,6 +171,61 @@ public void editUserData(@PathVariable String id, @RequestBody UserPatchData dat service.save(newUser); } + @PutMapping("/user/{id}/minecraft/{username}") + public void addUserMinecraft(@PathVariable String id, @PathVariable String username) { + var user = getSingleUser(id); + + // Check permissions + // In order for the request to be allowed, the person making the request needs + // to either be editing their own data, or they need to have the EDIT_OTHERS permission + var subject = SecurityUtils.getSubject(); + var edit_others = subject.isPermitted(Permissions.Users.EDIT_OTHERS); + var owns = PermissionUtils.owns(subject, user); + + if (!owns && !edit_others) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You may not edit this user"); + } + + String uuid = service.getMinecraftId(username); + if (uuid == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "A minecraft profile with that username does not exist"); + } + + // Perform operation + var accounts = new HashSet<>(user.minecraftAccounts()); + accounts.add(uuid); + service.save(user.withMinecraftAccounts(accounts)); + } + + @DeleteMapping("/user/{id}/minecraft/{username}") + public void deleteUserMinecraft(@PathVariable String id, @PathVariable String username) { + var user = getSingleUser(id); + + // Check permissions + // In order for the request to be allowed, the person making the request needs + // to either be editing their own data, or they need to have the EDIT_OTHERS permission + var subject = SecurityUtils.getSubject(); + var edit_others = subject.isPermitted(Permissions.Users.EDIT_OTHERS); + var owns = PermissionUtils.owns(subject, user); + + if (!owns && !edit_others) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You may not edit this user"); + } + + String uuid = service.getMinecraftId(username); + if (uuid == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "A minecraft profile with that username does not exist"); + } + + // Perform operation + var accounts = new HashSet<>(user.minecraftAccounts()); + if (!accounts.contains(uuid)) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "That minecraft account isn't associated with this user"); + } + accounts.remove(uuid); + service.save(user.withMinecraftAccounts(accounts)); + } + @PostMapping("/admin/update_user") @RequiresPermissions(Permissions.Users.FORCE_EDIT) public void forceUpdateUser(@RequestBody UserData data) { diff --git a/platform_api/src/main/java/net/modfest/platform/migrations/Migrator.java b/platform_api/src/main/java/net/modfest/platform/migrations/Migrator.java index 4adab83..e8863cc 100644 --- a/platform_api/src/main/java/net/modfest/platform/migrations/Migrator.java +++ b/platform_api/src/main/java/net/modfest/platform/migrations/Migrator.java @@ -19,7 +19,7 @@ * Contains ad-hoc migrations to our json format */ public record Migrator(JsonUtil json, Path root) { - static final int CURRENT_VERSION = 7; + static final int CURRENT_VERSION = 8; static final Map MIGRATIONS = new HashMap<>(); static { @@ -30,6 +30,7 @@ public record Migrator(JsonUtil json, Path root) { MIGRATIONS.put(5, Migrator::migrateTo5); MIGRATIONS.put(6, Migrator::migrateTo6); MIGRATIONS.put(7, Migrator::migrateTo7); + MIGRATIONS.put(8, Migrator::migrateTo8); } @@ -306,4 +307,22 @@ public void migrateTo7() { }); }); } + + /** + * V6 + * The "minecraft_accounts" field inside user data has been added + */ + public void migrateTo8() { + var userPath = root.resolve("users"); + MigratorUtils.executeForAllFiles(userPath, path -> { + var userJson = json.readJson(path, JsonObject.class); + var minecraftAccounts = userJson.get("minecraft_accounts"); + if (minecraftAccounts == null || !minecraftAccounts.isJsonArray()) { + // Default to the empty list + userJson.add("minecraft_accounts", new JsonArray()); + // Write the new json + json.writeJson(path, userJson); + } + }); + } } diff --git a/platform_api/src/main/java/net/modfest/platform/service/UserService.java b/platform_api/src/main/java/net/modfest/platform/service/UserService.java index 9a576d5..23a6e4d 100644 --- a/platform_api/src/main/java/net/modfest/platform/service/UserService.java +++ b/platform_api/src/main/java/net/modfest/platform/service/UserService.java @@ -1,17 +1,24 @@ package net.modfest.platform.service; +import com.google.gson.Gson; import net.modfest.platform.misc.EventSource; import net.modfest.platform.misc.MfUserId; import net.modfest.platform.misc.PlatformStandardException; +import net.modfest.platform.pojo.MinecraftProfile; import net.modfest.platform.pojo.PlatformErrorResponse; import net.modfest.platform.pojo.UserCreateData; import net.modfest.platform.pojo.UserData; import net.modfest.platform.pojo.UserRole; import net.modfest.platform.repository.UserRepository; import nl.theepicblock.dukerinth.ModrinthApi; +import nl.theepicblock.dukerinth.internal.GsonBodyHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; import java.util.Collection; import java.util.Set; @@ -80,6 +87,7 @@ public String create(UserCreateData data) throws InvalidModrinthIdException, Pla mrUser.avatarUrl, Set.of(), Set.of(), + Set.of(), UserRole.NONE )); @@ -99,6 +107,14 @@ private String generateUserId() { } } + public String getMinecraftId(String username) { + try(HttpClient client = HttpClient.newHttpClient()) { + return client.send(HttpRequest.newBuilder(URI.create("https://api.minecraftservices.com/minecraft/profile/lookup/name/%s".formatted(username))).build(), new GsonBodyHandler<>(MinecraftProfile.class, new Gson())).body().id(); + } catch (IOException | InterruptedException e) { + return null; + } + } + public static class InvalidModrinthIdException extends Exception { } diff --git a/platform_api/src/test/java/net/modfest/platform/JsonTests.java b/platform_api/src/test/java/net/modfest/platform/JsonTests.java index 78bcb8d..6aece9f 100644 --- a/platform_api/src/test/java/net/modfest/platform/JsonTests.java +++ b/platform_api/src/test/java/net/modfest/platform/JsonTests.java @@ -41,6 +41,7 @@ public static List testObjects() { "https://i.imgflip.com/4awf2i.jpg", Set.of("Badge"), Set.of("bc2+5i"), + Set.of("abcd"), UserRole.TEAM_MEMBER ), new EventData( diff --git a/platform_api/src/test/java/net/modfest/platform/UserDbTests.java b/platform_api/src/test/java/net/modfest/platform/UserDbTests.java index 154e125..241b248 100644 --- a/platform_api/src/test/java/net/modfest/platform/UserDbTests.java +++ b/platform_api/src/test/java/net/modfest/platform/UserDbTests.java @@ -37,6 +37,7 @@ public class UserDbTests { "https://upload.wikimedia.org/wikipedia/commons/thumb/8/87/GOODSMILE_Racing_Komatti-Mirai_EV_TT_Zero.jpg/1920px-GOODSMILE_Racing_Komatti-Mirai_EV_TT_Zero.jpg", Set.of(), Set.of(), + Set.of(), UserRole.TEAM_MEMBER );