@@ -32,17 +32,14 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
|
|||||||
import org.jackhuang.hmcl.auth.CredentialExpiredException;
|
import org.jackhuang.hmcl.auth.CredentialExpiredException;
|
||||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
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.offline.OfflineAccount;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
|
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
|
||||||
import org.jackhuang.hmcl.setting.Accounts;
|
import org.jackhuang.hmcl.setting.Accounts;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
import org.jackhuang.hmcl.ui.Controllers;
|
import org.jackhuang.hmcl.ui.Controllers;
|
||||||
import org.jackhuang.hmcl.ui.DialogController;
|
import org.jackhuang.hmcl.ui.DialogController;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
|
||||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||||
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
|
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
|
||||||
@@ -128,7 +125,7 @@ public class AccountListItem extends RadioButton {
|
|||||||
.orElse(emptySet());
|
.orElse(emptySet());
|
||||||
return uploadableTextures.contains(TextureType.SKIN);
|
return uploadableTextures.contains(TextureType.SKIN);
|
||||||
}, profile);
|
}, profile);
|
||||||
} else if (account instanceof OfflineAccount || account instanceof MicrosoftAccount) {
|
} else if (account instanceof OfflineAccount || account.canUploadSkin()) {
|
||||||
return createBooleanBinding(() -> true);
|
return createBooleanBinding(() -> true);
|
||||||
} else {
|
} else {
|
||||||
return createBooleanBinding(() -> false);
|
return createBooleanBinding(() -> false);
|
||||||
@@ -144,11 +141,7 @@ public class AccountListItem extends RadioButton {
|
|||||||
Controllers.dialog(new OfflineAccountSkinPane((OfflineAccount) account));
|
Controllers.dialog(new OfflineAccountSkinPane((OfflineAccount) account));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (account instanceof MicrosoftAccount) {
|
if (!account.canUploadSkin()) {
|
||||||
FXUtils.openLink("https://www.minecraft.net/msaprofile/mygames/editskin");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!(account instanceof YggdrasilAccount)) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +167,7 @@ public class AccountListItem extends RadioButton {
|
|||||||
NormalizedSkin skin = new NormalizedSkin(skinImg);
|
NormalizedSkin skin = new NormalizedSkin(skinImg);
|
||||||
String model = skin.isSlim() ? "slim" : "";
|
String model = skin.isSlim() ? "slim" : "";
|
||||||
LOG.info("Uploading skin [" + selectedFile + "], model [" + model + "]");
|
LOG.info("Uploading skin [" + selectedFile + "], model [" + model + "]");
|
||||||
((YggdrasilAccount) account).uploadSkin(model, selectedFile.toPath());
|
account.uploadSkin(skin.isSlim(), selectedFile.toPath());
|
||||||
})
|
})
|
||||||
.thenComposeAsync(refreshAsync())
|
.thenComposeAsync(refreshAsync())
|
||||||
.whenComplete(Schedulers.javafx(), e -> {
|
.whenComplete(Schedulers.javafx(), e -> {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ 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;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -69,6 +70,14 @@ public abstract class Account implements Observable {
|
|||||||
*/
|
*/
|
||||||
public abstract AuthInfo playOffline() throws AuthenticationException;
|
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 abstract Map<Object, Object> toStorage();
|
||||||
|
|
||||||
public void clearCache() {
|
public void clearCache() {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
|||||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -32,7 +33,7 @@ import java.util.UUID;
|
|||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
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 final MicrosoftService service;
|
||||||
protected UUID characterUUID;
|
protected UUID characterUUID;
|
||||||
@@ -125,6 +126,16 @@ public class MicrosoftAccount extends OAuthAccount {
|
|||||||
return session.toAuthInfo();
|
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
|
@Override
|
||||||
public Map<Object, Object> toStorage() {
|
public Map<Object, Object> toStorage() {
|
||||||
return session.toStorage();
|
return session.toStorage();
|
||||||
@@ -150,6 +161,7 @@ public class MicrosoftAccount extends OAuthAccount {
|
|||||||
@Override
|
@Override
|
||||||
public void clearCache() {
|
public void clearCache() {
|
||||||
authenticated = false;
|
authenticated = false;
|
||||||
|
service.getProfileRepository().invalidate(characterUUID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -25,19 +25,18 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
|
|||||||
import org.jackhuang.hmcl.auth.OAuth;
|
import org.jackhuang.hmcl.auth.OAuth;
|
||||||
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.CompleteGameProfile;
|
import org.jackhuang.hmcl.auth.yggdrasil.*;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
|
||||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
|
||||||
import org.jackhuang.hmcl.util.gson.*;
|
import org.jackhuang.hmcl.util.gson.*;
|
||||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
import org.jackhuang.hmcl.util.io.*;
|
||||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
|
||||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
|
||||||
import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
|
import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ThreadPoolExecutor;
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -261,6 +260,33 @@ public class MicrosoftService {
|
|||||||
return Optional.ofNullable(GSON.fromJson(request(NetworkUtils.toURL("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid)), null), CompleteGameProfile.class));
|
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 {
|
private static String request(URL url, Object payload) throws AuthenticationException {
|
||||||
try {
|
try {
|
||||||
if (payload == null)
|
if (payload == null)
|
||||||
@@ -272,14 +298,6 @@ public class MicrosoftService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
public static class XboxAuthorizationException extends AuthenticationException {
|
||||||
private final long errorCode;
|
private final long errorCode;
|
||||||
private final String redirect;
|
private final String redirect;
|
||||||
|
|||||||
@@ -203,8 +203,14 @@ public abstract class YggdrasilAccount extends ClassicAccount {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void uploadSkin(String model, Path file) throws AuthenticationException, UnsupportedOperationException {
|
@Override
|
||||||
service.uploadSkin(characterUUID, session.getAccessToken(), model, file);
|
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() {
|
private static String randomClientToken() {
|
||||||
|
|||||||
@@ -148,14 +148,14 @@ public class YggdrasilService {
|
|||||||
requireEmpty(request(provider.getInvalidationURL(), createRequestWithCredentials(accessToken, clientToken)));
|
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 {
|
try {
|
||||||
HttpURLConnection con = NetworkUtils.createHttpConnection(provider.getSkinUploadURL(uuid));
|
HttpURLConnection con = NetworkUtils.createHttpConnection(provider.getSkinUploadURL(uuid));
|
||||||
con.setRequestMethod("PUT");
|
con.setRequestMethod("PUT");
|
||||||
con.setRequestProperty("Authorization", "Bearer " + accessToken);
|
con.setRequestProperty("Authorization", "Bearer " + accessToken);
|
||||||
con.setDoOutput(true);
|
con.setDoOutput(true);
|
||||||
try (HttpMultipartRequest request = new HttpMultipartRequest(con)) {
|
try (HttpMultipartRequest request = new HttpMultipartRequest(con)) {
|
||||||
request.param("model", model);
|
request.param("model", isSlim ? "slim" : "");
|
||||||
try (InputStream fis = Files.newInputStream(file)) {
|
try (InputStream fis = Files.newInputStream(file)) {
|
||||||
request.file("file", FileUtils.getName(file), "image/" + FileUtils.getExtension(file), fis);
|
request.file("file", FileUtils.getName(file), "image/" + FileUtils.getExtension(file), fis);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user