fix: Microsoft Account login

This commit is contained in:
yuhuihuang 2020-12-24 20:32:47 +08:00
parent 8bc5d2112f
commit dd4683e693
9 changed files with 125 additions and 11 deletions

View File

@ -24,6 +24,8 @@ import javafx.scene.image.Image;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
@ -181,6 +183,30 @@ public final class TexturesLoader {
}
}, uuidFallback);
}
public static ObjectBinding<LoadedTexture> skinBinding(Account account) {
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(account.getUUID()));
return BindingMapping.of(account.getTextures())
.map(textures -> textures
.flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN)))
.filter(it -> StringUtils.isNotBlank(it.getUrl())))
.asyncMap(it -> {
if (it.isPresent()) {
Texture texture = it.get();
return CompletableFuture.supplyAsync(() -> {
try {
return loadTexture(texture);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return uuidFallback;
}
}, POOL);
} else {
return CompletableFuture.completedFuture(uuidFallback);
}
}, uuidFallback);
}
// ====
// ==== Avatar ====
@ -209,8 +235,10 @@ public final class TexturesLoader {
}
public static ObjectBinding<Image> fxAvatarBinding(Account account, int size) {
if (account instanceof YggdrasilAccount) {
return fxAvatarBinding(((YggdrasilAccount) account).getYggdrasilService(), account.getUUID(), size);
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) {
return BindingMapping.of(skinBinding(account))
.map(it -> toAvatar(it.image, size))
.map(img -> SwingFXUtils.toFXImage(img, null));
} else {
return Bindings.createObjectBinding(
() -> SwingFXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size), null));

View File

@ -258,7 +258,7 @@ public class AddAccountPane extends StackPane {
Controllers.dialog(new AddAuthlibInjectorServerPane());
}
private class Selector extends BorderPane implements CharacterSelector {
private static class Selector extends BorderPane implements CharacterSelector {
private final AdvancedListBox listBox = new AdvancedListBox();
private final JFXButton cancel = new JFXButton();

View File

@ -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)

View File

@ -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 {

View File

@ -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;

View File

@ -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")

View File

@ -49,6 +49,10 @@ public class MicrosoftSession {
return accessToken;
}
public String getAuthorization() {
return String.format("%s %s", getTokenType(), getAccessToken());
}
public User getUser() {
return user;
}

View File

@ -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);
}

View 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());