[FEATURE] Implement TextureUploadExtension

This commit is contained in:
Gravita 2025-06-13 13:59:30 +07:00
parent 2470780591
commit a03de56dde
5 changed files with 124 additions and 5 deletions

View file

@ -8,6 +8,7 @@
import pro.gravit.launcher.core.api.LauncherAPIHolder; import pro.gravit.launcher.core.api.LauncherAPIHolder;
import pro.gravit.launcher.core.api.features.AuthFeatureAPI; import pro.gravit.launcher.core.api.features.AuthFeatureAPI;
import pro.gravit.launcher.core.api.features.ProfileFeatureAPI; 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.api.features.UserFeatureAPI;
import pro.gravit.launcher.core.backend.LauncherBackendAPIHolder; import pro.gravit.launcher.core.backend.LauncherBackendAPIHolder;
import pro.gravit.launcher.runtime.backend.LauncherBackendImpl; import pro.gravit.launcher.runtime.backend.LauncherBackendImpl;
@ -256,7 +257,8 @@ public void start(String... args) throws Throwable {
return new LauncherAPI(Map.of( return new LauncherAPI(Map.of(
AuthFeatureAPI.class, impl, AuthFeatureAPI.class, impl,
UserFeatureAPI.class, impl, UserFeatureAPI.class, impl,
ProfileFeatureAPI.class, impl)); ProfileFeatureAPI.class, impl,
TextureUploadFeatureAPI.class, impl));
}); });
LauncherBackendAPIHolder.setApi(new LauncherBackendImpl()); LauncherBackendAPIHolder.setApi(new LauncherBackendImpl());
// //

View file

@ -6,6 +6,7 @@
import pro.gravit.launcher.core.api.features.AuthFeatureAPI; import pro.gravit.launcher.core.api.features.AuthFeatureAPI;
import pro.gravit.launcher.core.api.features.CoreFeatureAPI; import pro.gravit.launcher.core.api.features.CoreFeatureAPI;
import pro.gravit.launcher.core.api.features.ProfileFeatureAPI; 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.AuthMethod;
import pro.gravit.launcher.core.api.method.AuthMethodPassword; import pro.gravit.launcher.core.api.method.AuthMethodPassword;
import pro.gravit.launcher.core.api.model.SelfUser; 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.UserSettings;
import pro.gravit.launcher.core.backend.exceptions.LauncherBackendException; import pro.gravit.launcher.core.backend.exceptions.LauncherBackendException;
import pro.gravit.launcher.core.backend.extensions.Extension; 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.NewLauncherSettings;
import pro.gravit.launcher.runtime.client.DirBridge; import pro.gravit.launcher.runtime.client.DirBridge;
import pro.gravit.launcher.runtime.client.ServerPinger; import pro.gravit.launcher.runtime.client.ServerPinger;
@ -39,7 +41,7 @@
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.function.Function; import java.util.function.Function;
public class LauncherBackendImpl implements LauncherBackendAPI { public class LauncherBackendImpl implements LauncherBackendAPI, TextureUploadExtension {
private final ClientDownloadImpl clientDownloadImpl = new ClientDownloadImpl(this); private final ClientDownloadImpl clientDownloadImpl = new ClientDownloadImpl(this);
private volatile MainCallback callback; private volatile MainCallback callback;
ExecutorService executorService; ExecutorService executorService;
@ -285,8 +287,14 @@ public boolean isTestMode() {
} }
} }
@SuppressWarnings("unchecked")
@Override @Override
public <T extends Extension> T getExtension(Class<T> clazz) { public <T extends Extension> T getExtension(Class<T> clazz) {
if(clazz == TextureUploadExtension.class) {
if(authMethod != null && authMethod.getFeatures().contains(TextureUploadFeatureAPI.FEATURE_NAME)) {
return (T) this;
}
}
return null; return null;
} }
@ -303,4 +311,14 @@ public void shutdown() {
} }
} }
} }
@Override
public CompletableFuture<TextureUploadFeatureAPI.TextureUploadInfo> fetchTextureUploadInfo() {
return LauncherAPIHolder.get().get(TextureUploadFeatureAPI.class).fetchInfo();
}
@Override
public CompletableFuture<Texture> uploadTexture(String name, byte[] bytes, TextureUploadFeatureAPI.UploadSettings settings) {
return LauncherAPIHolder.get().get(TextureUploadFeatureAPI.class).upload(name, bytes, settings);
}
} }

View file

@ -1,10 +1,11 @@
package pro.gravit.launcher.base.events.request; package pro.gravit.launcher.base.events.request;
import pro.gravit.launcher.base.events.RequestEvent; import pro.gravit.launcher.base.events.RequestEvent;
import pro.gravit.launcher.core.api.features.TextureUploadFeatureAPI;
import java.util.Set; import java.util.Set;
public class AssetUploadInfoRequestEvent extends RequestEvent { public class AssetUploadInfoRequestEvent extends RequestEvent implements TextureUploadFeatureAPI.TextureUploadInfo {
public Set<String> available; public Set<String> available;
public SlimSupportConf slimSupportConf; public SlimSupportConf slimSupportConf;
@ -18,6 +19,16 @@ public String getType() {
return "assetUploadInfo"; return "assetUploadInfo";
} }
@Override
public Set<String> getAvailable() {
return available;
}
@Override
public boolean isRequireManualSlimSkinSelect() {
return slimSupportConf == SlimSupportConf.USER;
}
public enum SlimSupportConf { public enum SlimSupportConf {
UNSUPPORTED, USER, SERVER UNSUPPORTED, USER, SERVER
} }

View file

@ -4,11 +4,15 @@
import pro.gravit.launcher.base.profiles.ClientProfile; import pro.gravit.launcher.base.profiles.ClientProfile;
import pro.gravit.launcher.base.request.auth.*; import pro.gravit.launcher.base.request.auth.*;
import pro.gravit.launcher.base.request.auth.password.*; 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.ProfilesRequest;
import pro.gravit.launcher.base.request.update.UpdateRequest; import pro.gravit.launcher.base.request.update.UpdateRequest;
import pro.gravit.launcher.base.request.uuid.ProfileByUUIDRequest; import pro.gravit.launcher.base.request.uuid.ProfileByUUIDRequest;
import pro.gravit.launcher.base.request.uuid.ProfileByUsernameRequest; 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.AuthFeatureAPI;
import pro.gravit.launcher.core.api.features.TextureUploadFeatureAPI;
import pro.gravit.launcher.core.api.features.UserFeatureAPI; import pro.gravit.launcher.core.api.features.UserFeatureAPI;
import pro.gravit.launcher.core.api.features.ProfileFeatureAPI; import pro.gravit.launcher.core.api.features.ProfileFeatureAPI;
import pro.gravit.launcher.core.api.method.AuthMethodPassword; 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.AuthPlainPassword;
import pro.gravit.launcher.core.api.method.password.AuthTotpPassword; import pro.gravit.launcher.core.api.method.password.AuthTotpPassword;
import pro.gravit.launcher.core.api.model.SelfUser; 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.api.model.User;
import pro.gravit.launcher.core.hasher.HashedDir; import pro.gravit.launcher.core.hasher.HashedDir;
import pro.gravit.utils.helper.SecurityHelper; 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.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; 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 RequestService request;
private final String authId; private final String authId;
private final HttpClient client = HttpClient.newBuilder().build();
public RequestFeatureAPIImpl(RequestService request, String authId) { public RequestFeatureAPIImpl(RequestService request, String authId) {
this.request = request; this.request = request;
@ -177,6 +189,66 @@ public CompletableFuture<UpdateInfo> fetchUpdateInfo(String dirName) {
return request.request(new UpdateRequest(dirName)).thenApply(response -> new UpdateInfoData(response.hdir, response.url)); return request.request(new UpdateRequest(dirName)).thenApply(response -> new UpdateInfoData(response.hdir, response.url));
} }
@Override
public CompletableFuture<TextureUploadInfo> fetchInfo() {
return request.request(new AssetUploadInfoRequest()).thenApply(response -> response);
}
@Override
public CompletableFuture<Texture> 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 { public record UpdateInfoData(HashedDir hdir, String url) implements ProfileFeatureAPI.UpdateInfo {
@Override @Override
public HashedDir getHashedDir() { public HashedDir getHashedDir() {
@ -188,4 +260,19 @@ public String getUrl() {
return url; return url;
} }
} }
public record TextureUploadOptions(boolean modelSlim) {
}
public record UserTexture(@LauncherNetworkAPI String url, @LauncherNetworkAPI String digest, @LauncherNetworkAPI Map<String, String> metadata) {
Texture toLauncherTexture() {
return new pro.gravit.launcher.base.profiles.Texture(url, SecurityHelper.fromHex(digest), metadata);
}
}
public record UploadError(@LauncherNetworkAPI String error) {
}
} }

View file

@ -5,7 +5,8 @@
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
public interface TextureUploadFeatureAPI { public interface TextureUploadFeatureAPI extends FeatureAPI {
String FEATURE_NAME = "assetupload";
CompletableFuture<TextureUploadInfo> fetchInfo(); CompletableFuture<TextureUploadInfo> fetchInfo();
CompletableFuture<Texture> upload(String name, byte[] bytes, UploadSettings settings); CompletableFuture<Texture> upload(String name, byte[] bytes, UploadSettings settings);