mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-17 07:47:57 -04:00
fix: Microsoft Account login
This commit is contained in:
parent
8bc5d2112f
commit
dd4683e693
@ -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));
|
||||
|
@ -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();
|
||||
|
@ -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());
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user