fix: Microsoft Account login
This commit is contained in:
@@ -21,6 +21,10 @@ import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
|
||||
@@ -92,6 +96,10 @@ public abstract class Account implements Observable {
|
||||
Platform.runLater(helper::invalidate);
|
||||
}
|
||||
|
||||
public ObjectBinding<Optional<Map<TextureType, Texture>>> getTextures() {
|
||||
return Bindings.createObjectBinding(Optional::empty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this)
|
||||
|
||||
@@ -24,7 +24,7 @@ import java.util.List;
|
||||
|
||||
/**
|
||||
* This interface is for your application to open a GUI for user to choose the character
|
||||
* when a having-multi-character yggdrasil account is being logging in..
|
||||
* when a having-multi-character yggdrasil account is being logging in.
|
||||
*/
|
||||
public interface CharacterSelector {
|
||||
|
||||
|
||||
@@ -17,7 +17,11 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.microsoft;
|
||||
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -109,6 +113,13 @@ public class MicrosoftAccount extends Account {
|
||||
return service;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectBinding<Optional<Map<TextureType, Texture>>> getTextures() {
|
||||
return BindingMapping.of(service.getProfileRepository().binding(session.getAuthorization()))
|
||||
.map(profile -> profile.flatMap(MicrosoftService::getTextures));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCache() {
|
||||
authenticated = false;
|
||||
|
||||
@@ -23,33 +23,53 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.NoCharacterException;
|
||||
import org.jackhuang.hmcl.auth.ServerDisconnectException;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.*;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
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.javafx.ObservableOptionalCache;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.threadPool;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class MicrosoftService {
|
||||
private static final ThreadPoolExecutor POOL = threadPool("MicrosoftProfileProperties", true, 2, 10, TimeUnit.SECONDS);
|
||||
private static final Pattern OAUTH_URL_PATTERN = Pattern.compile("^https://login\\.live\\.com/oauth20_desktop\\.srf\\?code=(.*?)&lc=(.*?)$");
|
||||
|
||||
private final WebViewCallback callback;
|
||||
|
||||
private final ObservableOptionalCache<String, MinecraftProfileResponse, AuthenticationException> profileRepository;
|
||||
|
||||
public MicrosoftService(WebViewCallback callback) {
|
||||
this.callback = callback;
|
||||
this.profileRepository = new ObservableOptionalCache<>(
|
||||
authorization -> {
|
||||
LOG.info("Fetching properties");
|
||||
return getCompleteProfile(authorization);
|
||||
},
|
||||
(uuid, e) -> LOG.log(Level.WARNING, "Failed to fetch properties of " + uuid, e),
|
||||
POOL);
|
||||
}
|
||||
|
||||
public ObservableOptionalCache<String, MinecraftProfileResponse, AuthenticationException> getProfileRepository() {
|
||||
return profileRepository;
|
||||
}
|
||||
|
||||
public MicrosoftSession authenticate() throws AuthenticationException {
|
||||
@@ -139,6 +159,18 @@ public class MicrosoftService {
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<MinecraftProfileResponse> getCompleteProfile(String authorization) throws AuthenticationException {
|
||||
try {
|
||||
return Optional.ofNullable(HttpRequest.GET(NetworkUtils.toURL("https://api.minecraftservices.com/minecraft/profile"))
|
||||
.authorization(authorization)
|
||||
.getJson(MinecraftProfileResponse.class));
|
||||
} catch (IOException e) {
|
||||
throw new ServerDisconnectException(e);
|
||||
} catch (JsonParseException e) {
|
||||
throw new ServerResponseMalformedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean validate(String tokenType, String accessToken) throws AuthenticationException {
|
||||
requireNonNull(tokenType);
|
||||
requireNonNull(accessToken);
|
||||
@@ -166,6 +198,21 @@ public class MicrosoftService {
|
||||
}
|
||||
}
|
||||
|
||||
public static Optional<Map<TextureType, Texture>> getTextures(MinecraftProfileResponse profile) {
|
||||
Objects.requireNonNull(profile);
|
||||
|
||||
Map<TextureType, Texture> textures = new EnumMap<>(TextureType.class);
|
||||
|
||||
if (!profile.skins.isEmpty()) {
|
||||
textures.put(TextureType.SKIN, new Texture(profile.skins.get(0).url, null));
|
||||
}
|
||||
// if (!profile.capes.isEmpty()) {
|
||||
// textures.put(TextureType.CAPE, new Texture(profile.capes.get(0).url, null);
|
||||
// }
|
||||
|
||||
return Optional.of(textures);
|
||||
}
|
||||
|
||||
private static class LiveAuthorizationResponse {
|
||||
@SerializedName("token_type")
|
||||
String tokenType;
|
||||
@@ -242,11 +289,11 @@ public class MicrosoftService {
|
||||
String keyId;
|
||||
}
|
||||
|
||||
private static class MinecraftProfileResponseSkin implements Validation {
|
||||
public static class MinecraftProfileResponseSkin implements Validation {
|
||||
public String id;
|
||||
public String state;
|
||||
public String url;
|
||||
public String variant;
|
||||
public String variant; // CLASSIC, SLIM
|
||||
public String alias;
|
||||
|
||||
@Override
|
||||
@@ -255,15 +302,14 @@ public class MicrosoftService {
|
||||
Validation.requireNonNull(state, "state cannot be null");
|
||||
Validation.requireNonNull(url, "url cannot be null");
|
||||
Validation.requireNonNull(variant, "variant cannot be null");
|
||||
Validation.requireNonNull(alias, "alias cannot be null");
|
||||
}
|
||||
}
|
||||
|
||||
private static class MinecraftProfileResponseCape {
|
||||
public static class MinecraftProfileResponseCape {
|
||||
|
||||
}
|
||||
|
||||
private static class MinecraftProfileResponse extends MinecraftErrorResponse implements Validation {
|
||||
public static class MinecraftProfileResponse extends MinecraftErrorResponse implements Validation {
|
||||
@SerializedName("id")
|
||||
UUID id;
|
||||
@SerializedName("name")
|
||||
|
||||
@@ -49,6 +49,10 @@ public class MicrosoftSession {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public String getAuthorization() {
|
||||
return String.format("%s %s", getTokenType(), getAccessToken());
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -27,11 +27,14 @@ import org.jackhuang.hmcl.auth.CredentialExpiredException;
|
||||
import org.jackhuang.hmcl.auth.NoCharacterException;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public class YggdrasilAccount extends Account {
|
||||
|
||||
@@ -189,6 +192,20 @@ public class YggdrasilAccount extends Account {
|
||||
service.getProfileRepository().invalidate(characterUUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectBinding<Optional<Map<TextureType, Texture>>> getTextures() {
|
||||
return BindingMapping.of(service.getProfileRepository().binding(getUUID()))
|
||||
.map(profile -> profile.flatMap(it -> {
|
||||
try {
|
||||
return YggdrasilService.getTextures(it);
|
||||
} catch (ServerResponseMalformedException e) {
|
||||
LOG.log(Level.WARNING, "Failed to parse texture payload", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
public void uploadSkin(String model, Path file) throws AuthenticationException, UnsupportedOperationException {
|
||||
service.uploadSkin(characterUUID, session.getAccessToken(), model, file);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class YggdrasilService {
|
||||
|
||||
private static final ThreadPoolExecutor POOL = threadPool("ProfileProperties", true, 2, 10, TimeUnit.SECONDS);
|
||||
private static final ThreadPoolExecutor POOL = threadPool("YggdrasilProfileProperties", true, 2, 10, TimeUnit.SECONDS);
|
||||
|
||||
public static final YggdrasilService MOJANG = new YggdrasilService(new MojangYggdrasilProvider());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user