Skip to content

Commit

Permalink
支持在启动器内上传微软账号皮肤 (HMCL-dev#3375)
Browse files Browse the repository at this point in the history
* update

* Fix clearCache
  • Loading branch information
Glavo authored Oct 24, 2024
1 parent e857f19 commit eae2670
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,14 @@
import org.jackhuang.hmcl.auth.CredentialExpiredException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
Expand Down Expand Up @@ -128,7 +125,7 @@ public ObservableBooleanValue canUploadSkin() {
.orElse(emptySet());
return uploadableTextures.contains(TextureType.SKIN);
}, profile);
} else if (account instanceof OfflineAccount || account instanceof MicrosoftAccount) {
} else if (account instanceof OfflineAccount || account.canUploadSkin()) {
return createBooleanBinding(() -> true);
} else {
return createBooleanBinding(() -> false);
Expand All @@ -144,11 +141,7 @@ public Task<?> uploadSkin() {
Controllers.dialog(new OfflineAccountSkinPane((OfflineAccount) account));
return null;
}
if (account instanceof MicrosoftAccount) {
FXUtils.openLink("https://www.minecraft.net/msaprofile/mygames/editskin");
return null;
}
if (!(account instanceof YggdrasilAccount)) {
if (!account.canUploadSkin()) {
return null;
}

Expand All @@ -174,7 +167,7 @@ public Task<?> uploadSkin() {
NormalizedSkin skin = new NormalizedSkin(skinImg);
String model = skin.isSlim() ? "slim" : "";
LOG.info("Uploading skin [" + selectedFile + "], model [" + model + "]");
((YggdrasilAccount) account).uploadSkin(model, selectedFile.toPath());
account.uploadSkin(skin.isSlim(), selectedFile.toPath());
})
.thenComposeAsync(refreshAsync())
.whenComplete(Schedulers.javafx(), e -> {
Expand Down
9 changes: 9 additions & 0 deletions HMCLCore/src/main/java/org/jackhuang/hmcl/auth/Account.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
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;
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
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;
Expand All @@ -32,7 +33,7 @@
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;
Expand Down Expand Up @@ -125,6 +126,16 @@ public AuthInfo playOffline() {
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();
Expand All @@ -150,6 +161,7 @@ public ObjectBinding<Optional<Map<TextureType, Texture>>> getTextures() {
@Override
public void clearCache() {
authenticated = false;
service.getProfileRepository().invalidate(characterUUID);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,18 @@
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;
Expand Down Expand Up @@ -261,6 +260,33 @@ public Optional<CompleteGameProfile> getCompleteGameProfile(UUID uuid) throws Au
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)
Expand All @@ -272,14 +298,6 @@ private static String request(URL url, Object payload) throws AuthenticationExce
}
}

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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,14 @@ public ObjectBinding<Optional<Map<TextureType, Texture>>> getTextures() {

}

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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,14 +148,14 @@ public void invalidate(String accessToken, String clientToken) throws Authentica
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);
}
Expand Down

0 comments on commit eae2670

Please sign in to comment.