diff --git a/Launcher/src/main/java/pro/gravit/launcher/runtime/LauncherEngine.java b/Launcher/src/main/java/pro/gravit/launcher/runtime/LauncherEngine.java index a752b47b..b74b4ca0 100644 --- a/Launcher/src/main/java/pro/gravit/launcher/runtime/LauncherEngine.java +++ b/Launcher/src/main/java/pro/gravit/launcher/runtime/LauncherEngine.java @@ -8,6 +8,7 @@ import pro.gravit.launcher.core.api.LauncherAPIHolder; import pro.gravit.launcher.core.api.features.AuthFeatureAPI; import pro.gravit.launcher.core.api.features.ProfileFeatureAPI; +import pro.gravit.launcher.core.api.features.TextureUploadFeatureAPI; import pro.gravit.launcher.core.api.features.UserFeatureAPI; import pro.gravit.launcher.core.backend.LauncherBackendAPIHolder; import pro.gravit.launcher.runtime.backend.LauncherBackendImpl; @@ -256,7 +257,8 @@ public void start(String... args) throws Throwable { return new LauncherAPI(Map.of( AuthFeatureAPI.class, impl, UserFeatureAPI.class, impl, - ProfileFeatureAPI.class, impl)); + ProfileFeatureAPI.class, impl, + TextureUploadFeatureAPI.class, impl)); }); LauncherBackendAPIHolder.setApi(new LauncherBackendImpl()); // diff --git a/Launcher/src/main/java/pro/gravit/launcher/runtime/backend/LauncherBackendImpl.java b/Launcher/src/main/java/pro/gravit/launcher/runtime/backend/LauncherBackendImpl.java index 7485c53e..da4dbfa1 100644 --- a/Launcher/src/main/java/pro/gravit/launcher/runtime/backend/LauncherBackendImpl.java +++ b/Launcher/src/main/java/pro/gravit/launcher/runtime/backend/LauncherBackendImpl.java @@ -6,6 +6,7 @@ import pro.gravit.launcher.core.api.features.AuthFeatureAPI; import pro.gravit.launcher.core.api.features.CoreFeatureAPI; import pro.gravit.launcher.core.api.features.ProfileFeatureAPI; +import pro.gravit.launcher.core.api.features.TextureUploadFeatureAPI; import pro.gravit.launcher.core.api.method.AuthMethod; import pro.gravit.launcher.core.api.method.AuthMethodPassword; import pro.gravit.launcher.core.api.model.SelfUser; @@ -15,6 +16,7 @@ import pro.gravit.launcher.core.backend.UserSettings; import pro.gravit.launcher.core.backend.exceptions.LauncherBackendException; import pro.gravit.launcher.core.backend.extensions.Extension; +import pro.gravit.launcher.core.backend.extensions.TextureUploadExtension; import pro.gravit.launcher.runtime.NewLauncherSettings; import pro.gravit.launcher.runtime.client.DirBridge; import pro.gravit.launcher.runtime.client.ServerPinger; @@ -39,7 +41,7 @@ import java.util.concurrent.Executors; import java.util.function.Function; -public class LauncherBackendImpl implements LauncherBackendAPI { +public class LauncherBackendImpl implements LauncherBackendAPI, TextureUploadExtension { private final ClientDownloadImpl clientDownloadImpl = new ClientDownloadImpl(this); private volatile MainCallback callback; ExecutorService executorService; @@ -285,8 +287,14 @@ public boolean isTestMode() { } } + @SuppressWarnings("unchecked") @Override public T getExtension(Class clazz) { + if(clazz == TextureUploadExtension.class) { + if(authMethod != null && authMethod.getFeatures().contains(TextureUploadFeatureAPI.FEATURE_NAME)) { + return (T) this; + } + } return null; } @@ -303,4 +311,14 @@ public void shutdown() { } } } + + @Override + public CompletableFuture fetchTextureUploadInfo() { + return LauncherAPIHolder.get().get(TextureUploadFeatureAPI.class).fetchInfo(); + } + + @Override + public CompletableFuture uploadTexture(String name, byte[] bytes, TextureUploadFeatureAPI.UploadSettings settings) { + return LauncherAPIHolder.get().get(TextureUploadFeatureAPI.class).upload(name, bytes, settings); + } } diff --git a/LauncherAPI/src/main/java/pro/gravit/launcher/base/events/request/AssetUploadInfoRequestEvent.java b/LauncherAPI/src/main/java/pro/gravit/launcher/base/events/request/AssetUploadInfoRequestEvent.java index 1b2046cb..c4150e80 100644 --- a/LauncherAPI/src/main/java/pro/gravit/launcher/base/events/request/AssetUploadInfoRequestEvent.java +++ b/LauncherAPI/src/main/java/pro/gravit/launcher/base/events/request/AssetUploadInfoRequestEvent.java @@ -1,10 +1,11 @@ package pro.gravit.launcher.base.events.request; import pro.gravit.launcher.base.events.RequestEvent; +import pro.gravit.launcher.core.api.features.TextureUploadFeatureAPI; import java.util.Set; -public class AssetUploadInfoRequestEvent extends RequestEvent { +public class AssetUploadInfoRequestEvent extends RequestEvent implements TextureUploadFeatureAPI.TextureUploadInfo { public Set available; public SlimSupportConf slimSupportConf; @@ -18,6 +19,16 @@ public String getType() { return "assetUploadInfo"; } + @Override + public Set getAvailable() { + return available; + } + + @Override + public boolean isRequireManualSlimSkinSelect() { + return slimSupportConf == SlimSupportConf.USER; + } + public enum SlimSupportConf { UNSUPPORTED, USER, SERVER } diff --git a/LauncherAPI/src/main/java/pro/gravit/launcher/base/request/RequestFeatureAPIImpl.java b/LauncherAPI/src/main/java/pro/gravit/launcher/base/request/RequestFeatureAPIImpl.java index 39994d24..0b9fe6a7 100644 --- a/LauncherAPI/src/main/java/pro/gravit/launcher/base/request/RequestFeatureAPIImpl.java +++ b/LauncherAPI/src/main/java/pro/gravit/launcher/base/request/RequestFeatureAPIImpl.java @@ -4,11 +4,15 @@ import pro.gravit.launcher.base.profiles.ClientProfile; import pro.gravit.launcher.base.request.auth.*; import pro.gravit.launcher.base.request.auth.password.*; +import pro.gravit.launcher.base.request.cabinet.AssetUploadInfoRequest; +import pro.gravit.launcher.base.request.cabinet.GetAssetUploadUrl; import pro.gravit.launcher.base.request.update.ProfilesRequest; import pro.gravit.launcher.base.request.update.UpdateRequest; import pro.gravit.launcher.base.request.uuid.ProfileByUUIDRequest; import pro.gravit.launcher.base.request.uuid.ProfileByUsernameRequest; +import pro.gravit.launcher.core.LauncherNetworkAPI; import pro.gravit.launcher.core.api.features.AuthFeatureAPI; +import pro.gravit.launcher.core.api.features.TextureUploadFeatureAPI; import pro.gravit.launcher.core.api.features.UserFeatureAPI; import pro.gravit.launcher.core.api.features.ProfileFeatureAPI; import pro.gravit.launcher.core.api.method.AuthMethodPassword; @@ -17,19 +21,27 @@ import pro.gravit.launcher.core.api.method.password.AuthPlainPassword; import pro.gravit.launcher.core.api.method.password.AuthTotpPassword; import pro.gravit.launcher.core.api.model.SelfUser; +import pro.gravit.launcher.core.api.model.Texture; import pro.gravit.launcher.core.api.model.User; import pro.gravit.launcher.core.hasher.HashedDir; import pro.gravit.utils.helper.SecurityHelper; +import java.io.*; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; -public class RequestFeatureAPIImpl implements AuthFeatureAPI, UserFeatureAPI, ProfileFeatureAPI { +public class RequestFeatureAPIImpl implements AuthFeatureAPI, UserFeatureAPI, ProfileFeatureAPI, TextureUploadFeatureAPI { private final RequestService request; private final String authId; + private final HttpClient client = HttpClient.newBuilder().build(); public RequestFeatureAPIImpl(RequestService request, String authId) { this.request = request; @@ -177,6 +189,66 @@ public CompletableFuture fetchUpdateInfo(String dirName) { return request.request(new UpdateRequest(dirName)).thenApply(response -> new UpdateInfoData(response.hdir, response.url)); } + @Override + public CompletableFuture fetchInfo() { + return request.request(new AssetUploadInfoRequest()).thenApply(response -> response); + } + + @Override + public CompletableFuture upload(String name, byte[] bytes, UploadSettings settings) { + return request.request(new GetAssetUploadUrl(name)).thenCompose((response) -> { + String accessToken = response.token == null ? Request.getAccessToken() : response.token.accessToken; + String boundary = SecurityHelper.toHex(SecurityHelper.randomBytes(32)); + String jsonOptions = settings == null ? "{}" : Launcher.gsonManager.gson.toJson(new TextureUploadOptions(settings.slim())); + byte[] preFileData; + try(ByteArrayOutputStream output = new ByteArrayOutputStream(256)) { + output.write("--".getBytes(StandardCharsets.UTF_8)); + output.write(boundary.getBytes(StandardCharsets.UTF_8)); + output.write("\r\nContent-Disposition: form-data; name=\"options\"\r\nContent-Type: application/json\r\n\r\n".getBytes(StandardCharsets.UTF_8)); + output.write(jsonOptions.getBytes(StandardCharsets.UTF_8)); + output.write("\r\n--".getBytes(StandardCharsets.UTF_8)); + output.write(boundary.getBytes(StandardCharsets.UTF_8)); + output.write("\r\nContent-Disposition: form-data; name=\"file\"; filename=\"file\"\r\nContent-Type: image/png\r\n\r\n".getBytes(StandardCharsets.UTF_8)); + preFileData = output.toByteArray(); + } catch (IOException ex) { + return CompletableFuture.failedFuture(ex); + } + byte[] postFileData; + try(ByteArrayOutputStream output = new ByteArrayOutputStream(128)) { + output.write("\r\n--".getBytes(StandardCharsets.UTF_8)); + output.write(boundary.getBytes(StandardCharsets.UTF_8)); + output.write("--\r\n".getBytes(StandardCharsets.UTF_8)); + postFileData = output.toByteArray(); + } catch (IOException ex) { + return CompletableFuture.failedFuture(ex); + } + return client.sendAsync(HttpRequest.newBuilder() + .uri(URI.create(response.url)) + .POST(HttpRequest.BodyPublishers.concat(HttpRequest.BodyPublishers.ofByteArray(preFileData), + HttpRequest.BodyPublishers.ofByteArray(bytes), + HttpRequest.BodyPublishers.ofByteArray(postFileData))) + .header("Authorization", "Bearer "+accessToken) + .header("Content-Type", "multipart/form-data; boundary=\""+boundary+"\"") + .header("Accept", "application/json") + .build(), HttpResponse.BodyHandlers.ofByteArray()); + }).thenCompose((response) -> { + if(response.statusCode() >= 200 && response.statusCode() < 300) { + try (Reader reader = new InputStreamReader(new ByteArrayInputStream(response.body()))) { + return CompletableFuture.completedFuture(Launcher.gsonManager.gson.fromJson(reader, UserTexture.class).toLauncherTexture()); + } catch (Throwable e) { + return CompletableFuture.failedFuture(e); + } + } else { + try(Reader reader = new InputStreamReader(new ByteArrayInputStream(response.body()))) { + UploadError error = Launcher.gsonManager.gson.fromJson(reader, UploadError.class); + return CompletableFuture.failedFuture(new RequestException(error.error())); + } catch (Exception ex) { + return CompletableFuture.failedFuture(ex); + } + } + }); + } + public record UpdateInfoData(HashedDir hdir, String url) implements ProfileFeatureAPI.UpdateInfo { @Override public HashedDir getHashedDir() { @@ -188,4 +260,19 @@ public String getUrl() { return url; } } + + public record TextureUploadOptions(boolean modelSlim) { + + } + + public record UserTexture(@LauncherNetworkAPI String url, @LauncherNetworkAPI String digest, @LauncherNetworkAPI Map metadata) { + + Texture toLauncherTexture() { + return new pro.gravit.launcher.base.profiles.Texture(url, SecurityHelper.fromHex(digest), metadata); + } + } + + public record UploadError(@LauncherNetworkAPI String error) { + + } } diff --git a/LauncherCore/src/main/java/pro/gravit/launcher/core/api/features/TextureUploadFeatureAPI.java b/LauncherCore/src/main/java/pro/gravit/launcher/core/api/features/TextureUploadFeatureAPI.java index cba41c1a..4e515c01 100644 --- a/LauncherCore/src/main/java/pro/gravit/launcher/core/api/features/TextureUploadFeatureAPI.java +++ b/LauncherCore/src/main/java/pro/gravit/launcher/core/api/features/TextureUploadFeatureAPI.java @@ -5,7 +5,8 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; -public interface TextureUploadFeatureAPI { +public interface TextureUploadFeatureAPI extends FeatureAPI { + String FEATURE_NAME = "assetupload"; CompletableFuture fetchInfo(); CompletableFuture upload(String name, byte[] bytes, UploadSettings settings);