支持在启动器内上传微软账号皮肤 (#3375)

* update

* Fix clearCache
This commit is contained in:
Glavo
2024-10-24 20:03:21 +08:00
committed by GitHub
parent e857f19b18
commit eae2670855
6 changed files with 68 additions and 30 deletions

View File

@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import java.nio.file.Path;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -69,6 +70,14 @@ public abstract class Account implements Observable {
*/
public abstract AuthInfo playOffline() throws AuthenticationException;
public boolean canUploadSkin() {
return false;
}
public void uploadSkin(boolean isSlim, Path file) throws AuthenticationException, UnsupportedOperationException {
throw new UnsupportedOperationException("Unsupported Operation");
}
public abstract Map<Object, Object> toStorage();
public void clearCache() {

View File

@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.nio.file.Path;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -32,7 +33,7 @@ import java.util.UUID;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class MicrosoftAccount extends OAuthAccount {
public final class MicrosoftAccount extends OAuthAccount {
protected final MicrosoftService service;
protected UUID characterUUID;
@@ -125,6 +126,16 @@ public class MicrosoftAccount extends OAuthAccount {
return session.toAuthInfo();
}
@Override
public boolean canUploadSkin() {
return true;
}
@Override
public void uploadSkin(boolean isSlim, Path file) throws AuthenticationException, UnsupportedOperationException {
service.uploadSkin(session.getAccessToken(), isSlim, file);
}
@Override
public Map<Object, Object> toStorage() {
return session.toStorage();
@@ -150,6 +161,7 @@ public class MicrosoftAccount extends OAuthAccount {
@Override
public void clearCache() {
authenticated = false;
service.getProfileRepository().invalidate(characterUUID);
}
@Override

View File

@@ -25,19 +25,18 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.OAuth;
import org.jackhuang.hmcl.auth.ServerDisconnectException;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.auth.yggdrasil.*;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.*;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jackhuang.hmcl.util.io.*;
import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -261,6 +260,33 @@ public class MicrosoftService {
return Optional.ofNullable(GSON.fromJson(request(NetworkUtils.toURL("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid)), null), CompleteGameProfile.class));
}
public void uploadSkin(String accessToken, boolean isSlim, Path file) throws AuthenticationException, UnsupportedOperationException {
try {
HttpURLConnection con = NetworkUtils.createHttpConnection(NetworkUtils.toURL("https://api.minecraftservices.com/minecraft/profile/skins"));
con.setRequestMethod("POST");
con.setRequestProperty("Authorization", "Bearer " + accessToken);
con.setDoOutput(true);
try (HttpMultipartRequest request = new HttpMultipartRequest(con)) {
request.param("variant", isSlim ? "slim" : "classic");
try (InputStream fis = Files.newInputStream(file)) {
request.file("file", FileUtils.getName(file), "image/" + FileUtils.getExtension(file), fis);
}
}
String response = NetworkUtils.readData(con);
if (StringUtils.isBlank(response)) {
if (con.getResponseCode() / 100 != 2)
throw new ResponseCodeException(con.getURL(), con.getResponseCode());
} else {
MinecraftErrorResponse profileResponse = GSON.fromJson(response, MinecraftErrorResponse.class);
if (StringUtils.isNotBlank(profileResponse.errorMessage) || con.getResponseCode() / 100 != 2)
throw new AuthenticationException("Failed to upload skin, response code: " + con.getResponseCode() + ", response: " + response);
}
} catch (IOException | JsonParseException e) {
throw new AuthenticationException(e);
}
}
private static String request(URL url, Object payload) throws AuthenticationException {
try {
if (payload == null)
@@ -272,14 +298,6 @@ public class MicrosoftService {
}
}
private static <T> T fromJson(String text, Class<T> typeOfT) throws ServerResponseMalformedException {
try {
return GSON.fromJson(text, typeOfT);
} catch (JsonParseException e) {
throw new ServerResponseMalformedException(text, e);
}
}
public static class XboxAuthorizationException extends AuthenticationException {
private final long errorCode;
private final String redirect;

View File

@@ -203,8 +203,14 @@ public abstract class YggdrasilAccount extends ClassicAccount {
}
public void uploadSkin(String model, Path file) throws AuthenticationException, UnsupportedOperationException {
service.uploadSkin(characterUUID, session.getAccessToken(), model, file);
@Override
public boolean canUploadSkin() {
return true;
}
@Override
public void uploadSkin(boolean isSlim, Path file) throws AuthenticationException, UnsupportedOperationException {
service.uploadSkin(characterUUID, session.getAccessToken(), isSlim, file);
}
private static String randomClientToken() {

View File

@@ -148,14 +148,14 @@ public class YggdrasilService {
requireEmpty(request(provider.getInvalidationURL(), createRequestWithCredentials(accessToken, clientToken)));
}
public void uploadSkin(UUID uuid, String accessToken, String model, Path file) throws AuthenticationException, UnsupportedOperationException {
public void uploadSkin(UUID uuid, String accessToken, boolean isSlim, Path file) throws AuthenticationException, UnsupportedOperationException {
try {
HttpURLConnection con = NetworkUtils.createHttpConnection(provider.getSkinUploadURL(uuid));
con.setRequestMethod("PUT");
con.setRequestProperty("Authorization", "Bearer " + accessToken);
con.setDoOutput(true);
try (HttpMultipartRequest request = new HttpMultipartRequest(con)) {
request.param("model", model);
request.param("model", isSlim ? "slim" : "");
try (InputStream fis = Files.newInputStream(file)) {
request.file("file", FileUtils.getName(file), "image/" + FileUtils.getExtension(file), fis);
}