mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-18 08:16:58 -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.Metadata;
|
||||||
import org.jackhuang.hmcl.auth.Account;
|
import org.jackhuang.hmcl.auth.Account;
|
||||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
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.Texture;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
|
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||||
@ -181,6 +183,30 @@ public final class TexturesLoader {
|
|||||||
}
|
}
|
||||||
}, uuidFallback);
|
}, 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 ====
|
// ==== Avatar ====
|
||||||
@ -209,8 +235,10 @@ public final class TexturesLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static ObjectBinding<Image> fxAvatarBinding(Account account, int size) {
|
public static ObjectBinding<Image> fxAvatarBinding(Account account, int size) {
|
||||||
if (account instanceof YggdrasilAccount) {
|
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) {
|
||||||
return fxAvatarBinding(((YggdrasilAccount) account).getYggdrasilService(), account.getUUID(), size);
|
return BindingMapping.of(skinBinding(account))
|
||||||
|
.map(it -> toAvatar(it.image, size))
|
||||||
|
.map(img -> SwingFXUtils.toFXImage(img, null));
|
||||||
} else {
|
} else {
|
||||||
return Bindings.createObjectBinding(
|
return Bindings.createObjectBinding(
|
||||||
() -> SwingFXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size), null));
|
() -> SwingFXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size), null));
|
||||||
|
@ -258,7 +258,7 @@ public class AddAccountPane extends StackPane {
|
|||||||
Controllers.dialog(new AddAuthlibInjectorServerPane());
|
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 AdvancedListBox listBox = new AdvancedListBox();
|
||||||
private final JFXButton cancel = new JFXButton();
|
private final JFXButton cancel = new JFXButton();
|
||||||
|
@ -21,6 +21,10 @@ import javafx.application.Platform;
|
|||||||
import javafx.beans.InvalidationListener;
|
import javafx.beans.InvalidationListener;
|
||||||
import javafx.beans.Observable;
|
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.ToStringBuilder;
|
||||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||||
|
|
||||||
@ -92,6 +96,10 @@ public abstract class Account implements Observable {
|
|||||||
Platform.runLater(helper::invalidate);
|
Platform.runLater(helper::invalidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ObjectBinding<Optional<Map<TextureType, Texture>>> getTextures() {
|
||||||
|
return Bindings.createObjectBinding(Optional::empty);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return new ToStringBuilder(this)
|
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
|
* 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 {
|
public interface CharacterSelector {
|
||||||
|
|
||||||
|
@ -17,7 +17,11 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.auth.microsoft;
|
package org.jackhuang.hmcl.auth.microsoft;
|
||||||
|
|
||||||
|
import javafx.beans.binding.ObjectBinding;
|
||||||
import org.jackhuang.hmcl.auth.*;
|
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.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@ -109,6 +113,13 @@ public class MicrosoftAccount extends Account {
|
|||||||
return service;
|
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
|
@Override
|
||||||
public void clearCache() {
|
public void clearCache() {
|
||||||
authenticated = false;
|
authenticated = false;
|
||||||
|
@ -23,33 +23,53 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
|
|||||||
import org.jackhuang.hmcl.auth.NoCharacterException;
|
import org.jackhuang.hmcl.auth.NoCharacterException;
|
||||||
import org.jackhuang.hmcl.auth.ServerDisconnectException;
|
import org.jackhuang.hmcl.auth.ServerDisconnectException;
|
||||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
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.JsonUtils;
|
||||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||||
import org.jackhuang.hmcl.util.gson.Validation;
|
import org.jackhuang.hmcl.util.gson.Validation;
|
||||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||||
|
import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
import java.util.logging.Level;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
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;
|
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||||
|
|
||||||
public class MicrosoftService {
|
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 static final Pattern OAUTH_URL_PATTERN = Pattern.compile("^https://login\\.live\\.com/oauth20_desktop\\.srf\\?code=(.*?)&lc=(.*?)$");
|
||||||
|
|
||||||
private final WebViewCallback callback;
|
private final WebViewCallback callback;
|
||||||
|
|
||||||
|
private final ObservableOptionalCache<String, MinecraftProfileResponse, AuthenticationException> profileRepository;
|
||||||
|
|
||||||
public MicrosoftService(WebViewCallback callback) {
|
public MicrosoftService(WebViewCallback callback) {
|
||||||
this.callback = 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 {
|
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 {
|
public boolean validate(String tokenType, String accessToken) throws AuthenticationException {
|
||||||
requireNonNull(tokenType);
|
requireNonNull(tokenType);
|
||||||
requireNonNull(accessToken);
|
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 {
|
private static class LiveAuthorizationResponse {
|
||||||
@SerializedName("token_type")
|
@SerializedName("token_type")
|
||||||
String tokenType;
|
String tokenType;
|
||||||
@ -242,11 +289,11 @@ public class MicrosoftService {
|
|||||||
String keyId;
|
String keyId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class MinecraftProfileResponseSkin implements Validation {
|
public static class MinecraftProfileResponseSkin implements Validation {
|
||||||
public String id;
|
public String id;
|
||||||
public String state;
|
public String state;
|
||||||
public String url;
|
public String url;
|
||||||
public String variant;
|
public String variant; // CLASSIC, SLIM
|
||||||
public String alias;
|
public String alias;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -255,15 +302,14 @@ public class MicrosoftService {
|
|||||||
Validation.requireNonNull(state, "state cannot be null");
|
Validation.requireNonNull(state, "state cannot be null");
|
||||||
Validation.requireNonNull(url, "url cannot be null");
|
Validation.requireNonNull(url, "url cannot be null");
|
||||||
Validation.requireNonNull(variant, "variant 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")
|
@SerializedName("id")
|
||||||
UUID id;
|
UUID id;
|
||||||
@SerializedName("name")
|
@SerializedName("name")
|
||||||
|
@ -49,6 +49,10 @@ public class MicrosoftSession {
|
|||||||
return accessToken;
|
return accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getAuthorization() {
|
||||||
|
return String.format("%s %s", getTokenType(), getAccessToken());
|
||||||
|
}
|
||||||
|
|
||||||
public User getUser() {
|
public User getUser() {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
@ -27,11 +27,14 @@ import org.jackhuang.hmcl.auth.CredentialExpiredException;
|
|||||||
import org.jackhuang.hmcl.auth.NoCharacterException;
|
import org.jackhuang.hmcl.auth.NoCharacterException;
|
||||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||||
|
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
|
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||||
|
|
||||||
public class YggdrasilAccount extends Account {
|
public class YggdrasilAccount extends Account {
|
||||||
|
|
||||||
@ -189,6 +192,20 @@ public class YggdrasilAccount extends Account {
|
|||||||
service.getProfileRepository().invalidate(characterUUID);
|
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 {
|
public void uploadSkin(String model, Path file) throws AuthenticationException, UnsupportedOperationException {
|
||||||
service.uploadSkin(characterUUID, session.getAccessToken(), model, file);
|
service.uploadSkin(characterUUID, session.getAccessToken(), model, file);
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ import static org.jackhuang.hmcl.util.Pair.pair;
|
|||||||
|
|
||||||
public class YggdrasilService {
|
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());
|
public static final YggdrasilService MOJANG = new YggdrasilService(new MojangYggdrasilProvider());
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user