mirror of
https://github.com/Suiranoil/SkinRestorer.git
synced 2026-01-16 04:42:12 +00:00
use mineskin client to support mineskin api v2
This commit is contained in:
18
build.gradle
18
build.gradle
@@ -9,5 +9,21 @@ plugins {
|
||||
|
||||
id 'org.spongepowered.mixin' version '0.7-SNAPSHOT' apply false
|
||||
|
||||
id 'me.modmuss50.mod-publish-plugin' version '0.6.3' apply false
|
||||
id 'me.modmuss50.mod-publish-plugin' version '0.8.1' apply false
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
exclusiveContent {
|
||||
forRepository {
|
||||
maven {
|
||||
name = 'MineSkin'
|
||||
url = 'https://repo.inventivetalent.org/repository/public'
|
||||
}
|
||||
}
|
||||
filter {
|
||||
includeGroupAndSubgroups('org.mineskin')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ publishMods {
|
||||
if (project.name == 'fabric')
|
||||
file = remapJar.archiveFile
|
||||
else
|
||||
file = jar.archiveFile
|
||||
file = tasks.named('jarJar').get().archiveFile
|
||||
|
||||
modLoaders.add(project.name)
|
||||
type = STABLE
|
||||
|
||||
@@ -11,6 +11,8 @@ dependencies {
|
||||
parchment("org.parchmentmc.data:parchment-${parchment_minecraft}:${parchment_version}@zip")
|
||||
}
|
||||
compileOnly group: 'org.spongepowered', name: 'mixin', version: '0.8.5'
|
||||
|
||||
implementation("org.mineskin:java-client:${mineskin_client_version}")
|
||||
}
|
||||
|
||||
loom {
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
package net.lionarius.skinrestorer.mineskin;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.mineskin.MineSkinClientImpl;
|
||||
import org.mineskin.data.CodeAndMessage;
|
||||
import org.mineskin.exception.MineSkinRequestException;
|
||||
import org.mineskin.exception.MineskinException;
|
||||
import org.mineskin.request.RequestHandler;
|
||||
import org.mineskin.response.MineSkinResponse;
|
||||
import org.mineskin.response.ResponseConstructor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpRequest.BodyPublishers;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
// copy-pasted from https://github.com/InventivetalentDev/MineskinClient/blob/master/java11/src/main/java/org/mineskin/Java11RequestHandler.java
|
||||
// with some modifications to support proxy
|
||||
public class Java11RequestHandler extends RequestHandler {
|
||||
|
||||
private final Gson gson;
|
||||
private final HttpClient httpClient;
|
||||
|
||||
public Java11RequestHandler(String userAgent, String apiKey, int timeout, Gson gson, InetSocketAddress proxy) {
|
||||
super(userAgent, apiKey, timeout, gson);
|
||||
this.gson = gson;
|
||||
|
||||
HttpClient.Builder clientBuilder = HttpClient.newBuilder()
|
||||
.connectTimeout(java.time.Duration.ofMillis(timeout));
|
||||
|
||||
if (userAgent != null) {
|
||||
clientBuilder.followRedirects(HttpClient.Redirect.NORMAL);
|
||||
}
|
||||
|
||||
if (proxy != null) {
|
||||
clientBuilder.proxy(ProxySelector.of(proxy));
|
||||
}
|
||||
|
||||
this.httpClient = clientBuilder.build();
|
||||
}
|
||||
|
||||
private <T, R extends MineSkinResponse<T>> R wrapResponse(HttpResponse<String> response, Class<T> clazz, ResponseConstructor<T, R> constructor)
|
||||
throws IOException {
|
||||
String rawBody = response.body();
|
||||
try {
|
||||
JsonObject jsonBody = gson.fromJson(rawBody, JsonObject.class);
|
||||
R wrapped = constructor.construct(
|
||||
response.statusCode(),
|
||||
lowercaseHeaders(response.headers().map()),
|
||||
jsonBody,
|
||||
gson, clazz
|
||||
);
|
||||
if (!wrapped.isSuccess()) {
|
||||
throw new MineSkinRequestException(
|
||||
wrapped.getFirstError().map(CodeAndMessage::code).orElse("request_failed"),
|
||||
wrapped.getFirstError().map(CodeAndMessage::message).orElse("Request Failed"),
|
||||
wrapped
|
||||
);
|
||||
}
|
||||
return wrapped;
|
||||
} catch (JsonParseException e) {
|
||||
MineSkinClientImpl.LOGGER.log(Level.WARNING, "Failed to parse response body: " + rawBody, e);
|
||||
throw new MineskinException("Failed to parse response", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> lowercaseHeaders(Map<String, java.util.List<String>> headers) {
|
||||
return headers.entrySet().stream()
|
||||
.collect(Collectors.toMap(
|
||||
entry -> entry.getKey().toLowerCase(),
|
||||
entry -> String.join(", ", entry.getValue())
|
||||
));
|
||||
}
|
||||
|
||||
public <T, R extends MineSkinResponse<T>> R getJson(String url, Class<T> clazz, ResponseConstructor<T, R> constructor)
|
||||
throws IOException {
|
||||
MineSkinClientImpl.LOGGER.fine("GET " + url);
|
||||
|
||||
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.GET()
|
||||
.header("User-Agent", this.userAgent);
|
||||
HttpRequest request;
|
||||
if (apiKey != null) {
|
||||
request = requestBuilder
|
||||
.header("Authorization", "Bearer " + apiKey)
|
||||
.header("Accept", "application/json").build();
|
||||
} else {
|
||||
request = requestBuilder.build();
|
||||
}
|
||||
HttpResponse<String> response;
|
||||
try {
|
||||
response = this.httpClient.send(request, BodyHandlers.ofString());
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return wrapResponse(response, clazz, constructor);
|
||||
}
|
||||
|
||||
public <T, R extends MineSkinResponse<T>> R postJson(String url, JsonObject data, Class<T> clazz, ResponseConstructor<T, R> constructor)
|
||||
throws IOException {
|
||||
MineSkinClientImpl.LOGGER.fine("POST " + url);
|
||||
|
||||
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.POST(BodyPublishers.ofString(gson.toJson(data)))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("User-Agent", this.userAgent);
|
||||
HttpRequest request;
|
||||
if (apiKey != null) {
|
||||
request = requestBuilder
|
||||
.header("Authorization", "Bearer " + apiKey)
|
||||
.header("Accept", "application/json").build();
|
||||
} else {
|
||||
request = requestBuilder.build();
|
||||
}
|
||||
|
||||
HttpResponse<String> response;
|
||||
try {
|
||||
response = this.httpClient.send(request, BodyHandlers.ofString());
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return wrapResponse(response, clazz, constructor);
|
||||
}
|
||||
|
||||
public <T, R extends MineSkinResponse<T>> R postFormDataFile(String url, String key, String filename, InputStream in, Map<String, String> data, Class<T> clazz, ResponseConstructor<T, R> constructor)
|
||||
throws IOException {
|
||||
MineSkinClientImpl.LOGGER.fine("POST " + url);
|
||||
|
||||
String boundary = "mineskin-" + System.currentTimeMillis();
|
||||
StringBuilder bodyBuilder = new StringBuilder();
|
||||
|
||||
// add form fields
|
||||
for (Map.Entry<String, String> entry : data.entrySet()) {
|
||||
bodyBuilder.append("--").append(boundary).append("\r\n")
|
||||
.append("Content-Disposition: form-data; name=\"").append(entry.getKey()).append("\"\r\n\r\n")
|
||||
.append(entry.getValue()).append("\r\n");
|
||||
}
|
||||
|
||||
// add file
|
||||
byte[] fileContent = in.readAllBytes();
|
||||
bodyBuilder.append("--").append(boundary).append("\r\n")
|
||||
.append("Content-Disposition: form-data; name=\"").append(key)
|
||||
.append("\"; filename=\"").append(filename).append("\"\r\n")
|
||||
.append("Content-Type: image/png\r\n\r\n");
|
||||
byte[] bodyStart = bodyBuilder.toString().getBytes();
|
||||
byte[] boundaryEnd = ("\r\n--" + boundary + "--\r\n").getBytes();
|
||||
byte[] bodyString = new byte[bodyStart.length + fileContent.length + boundaryEnd.length];
|
||||
System.arraycopy(bodyStart, 0, bodyString, 0, bodyStart.length);
|
||||
System.arraycopy(fileContent, 0, bodyString, bodyStart.length, fileContent.length);
|
||||
System.arraycopy(boundaryEnd, 0, bodyString, bodyStart.length + fileContent.length, boundaryEnd.length);
|
||||
|
||||
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.POST(HttpRequest.BodyPublishers.ofByteArray(bodyString))
|
||||
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
|
||||
.header("User-Agent", this.userAgent);
|
||||
HttpRequest request;
|
||||
if (apiKey != null) {
|
||||
request = requestBuilder
|
||||
.header("Authorization", "Bearer " + apiKey)
|
||||
.header("Accept", "application/json").build();
|
||||
} else {
|
||||
request = requestBuilder.build();
|
||||
}
|
||||
|
||||
HttpResponse<String> response;
|
||||
try {
|
||||
response = this.httpClient.send(request, BodyHandlers.ofString());
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return wrapResponse(response, clazz, constructor);
|
||||
}
|
||||
}
|
||||
@@ -4,21 +4,26 @@ import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import com.google.common.util.concurrent.UncheckedExecutionException;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.mojang.authlib.properties.Property;
|
||||
import it.unimi.dsi.fastutil.Pair;
|
||||
import net.lionarius.skinrestorer.SkinRestorer;
|
||||
import net.lionarius.skinrestorer.mineskin.Java11RequestHandler;
|
||||
import net.lionarius.skinrestorer.skin.SkinVariant;
|
||||
import net.lionarius.skinrestorer.util.JsonUtils;
|
||||
import net.lionarius.skinrestorer.util.PlayerUtils;
|
||||
import net.lionarius.skinrestorer.util.Result;
|
||||
import net.lionarius.skinrestorer.util.WebUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.mineskin.MineSkinClient;
|
||||
import org.mineskin.data.Variant;
|
||||
import org.mineskin.data.Visibility;
|
||||
import org.mineskin.request.GenerateRequest;
|
||||
import org.mineskin.response.QueueResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -27,6 +32,7 @@ public final class MineskinSkinProvider implements SkinProvider {
|
||||
public static final String PROVIDER_NAME = "web";
|
||||
|
||||
private static final URI API_URI;
|
||||
private static MineSkinClient MINESKIN_CLIENT;
|
||||
|
||||
private static LoadingCache<Pair<URI, SkinVariant>, Optional<Property>> SKIN_CACHE;
|
||||
|
||||
@@ -39,6 +45,20 @@ public final class MineskinSkinProvider implements SkinProvider {
|
||||
}
|
||||
|
||||
public static void reload() {
|
||||
MINESKIN_CLIENT = MineSkinClient
|
||||
.builder()
|
||||
.userAgent(WebUtils.USER_AGENT)
|
||||
.gson(JsonUtils.GSON)
|
||||
.timeout((int) Duration.ofSeconds(SkinRestorer.getConfig().requestTimeout()).toMillis())
|
||||
.requestHandler((userAgent, apiKey, timeout, gson) -> new Java11RequestHandler(
|
||||
userAgent,
|
||||
apiKey,
|
||||
timeout,
|
||||
gson,
|
||||
SkinRestorer.getConfig().proxy().map(proxy -> new InetSocketAddress(proxy.host(), proxy.port())).orElse(null)
|
||||
))
|
||||
.build();
|
||||
|
||||
createCache();
|
||||
}
|
||||
|
||||
@@ -80,29 +100,26 @@ public final class MineskinSkinProvider implements SkinProvider {
|
||||
}
|
||||
|
||||
private static Optional<Property> loadSkin(URI uri, SkinVariant variant) throws Exception {
|
||||
var result = MineskinSkinProvider.uploadToMineskin(uri, variant);
|
||||
var texture = result.getAsJsonObject("data").getAsJsonObject("texture");
|
||||
var textures = new Property(PlayerUtils.TEXTURES_KEY, texture.get("value").getAsString(), texture.get("signature").getAsString());
|
||||
var mineskinVariant = switch (variant) {
|
||||
case CLASSIC -> Variant.CLASSIC;
|
||||
case SLIM -> Variant.SLIM;
|
||||
};
|
||||
|
||||
return Optional.of(textures);
|
||||
}
|
||||
|
||||
private static JsonObject uploadToMineskin(URI url, SkinVariant variant) throws IOException {
|
||||
var body = ("{\"variant\":\"%s\",\"name\":\"%s\",\"visibility\":%d,\"url\":\"%s\"}")
|
||||
.formatted(variant.toString(), "none", 0, url);
|
||||
var request = GenerateRequest.url(uri)
|
||||
.variant(mineskinVariant)
|
||||
.name("skinrestorer-skin")
|
||||
.visibility(Visibility.UNLISTED);
|
||||
|
||||
var request = HttpRequest.newBuilder()
|
||||
.uri(MineskinSkinProvider.API_URI.resolve("/generate/url"))
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body))
|
||||
.header("Content-Type", "application/json")
|
||||
.build();
|
||||
var skin = MINESKIN_CLIENT.queue().submit(request)
|
||||
.thenApply(QueueResponse::getJob)
|
||||
.thenCompose(jobInfo -> jobInfo.waitForCompletion(MINESKIN_CLIENT))
|
||||
.thenCompose(jobReference -> jobReference.getOrLoadSkin(MINESKIN_CLIENT))
|
||||
.join();
|
||||
|
||||
var response = WebUtils.executeRequest(request);
|
||||
WebUtils.throwOnClientErrors(response);
|
||||
|
||||
if (response.statusCode() != 200)
|
||||
throw new IllegalArgumentException("could not get mineskin skin");
|
||||
|
||||
return JsonUtils.parseJson(response.body());
|
||||
return Optional.of(new Property(
|
||||
PlayerUtils.TEXTURES_KEY,
|
||||
skin.texture().data().value(),
|
||||
skin.texture().data().signature()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.util.UUID;
|
||||
|
||||
public final class JsonUtils {
|
||||
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
public static final Gson GSON = new GsonBuilder()
|
||||
.registerTypeAdapter(UUID.class, new UUIDTypeAdapter())
|
||||
.registerTypeAdapter(PropertyMap.class, new PropertyMap.Serializer())
|
||||
.registerTypeAdapter(GameProfile.class, new GameProfile.Serializer())
|
||||
|
||||
@@ -5,6 +5,7 @@ plugins {
|
||||
|
||||
id 'multiloader-publish'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
minecraft "com.mojang:minecraft:${minecraft_version}"
|
||||
mappings loom.layered {
|
||||
@@ -14,6 +15,8 @@ dependencies {
|
||||
modImplementation "net.fabricmc:fabric-loader:${fabric_loader_version}"
|
||||
|
||||
modCompileOnly "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}"
|
||||
|
||||
include implementation("org.mineskin:java-client:${mineskin_client_version}")
|
||||
}
|
||||
|
||||
loom {
|
||||
|
||||
@@ -15,6 +15,14 @@ mixin {
|
||||
config "${mod_id}.mixins.json"
|
||||
}
|
||||
|
||||
jarJar.enable()
|
||||
|
||||
tasks.named('jarJar') {
|
||||
archiveClassifier = ''
|
||||
}
|
||||
|
||||
jar.finalizedBy('jarJar')
|
||||
|
||||
minecraft {
|
||||
mappings channel: 'parchment', version: "${parchment_version}-${parchment_minecraft}"
|
||||
|
||||
@@ -60,6 +68,11 @@ dependencies {
|
||||
|
||||
annotationProcessor('org.spongepowered:mixin:0.8.5-SNAPSHOT:processor')
|
||||
implementation('net.sf.jopt-simple:jopt-simple:5.0.4') { version { strictly '5.0.4' } }
|
||||
|
||||
// TODO: somehow fix forge gradle including dependencies of java-client
|
||||
minecraftLibrary(jarJar(group: 'org.mineskin', name: 'java-client', version: mineskin_client_version)) {
|
||||
jarJar.ranged(it, "[${mineskin_client_version},)")
|
||||
}
|
||||
}
|
||||
|
||||
// for some reason mixin plugin does not add 'MixinConfigs' to MANIFEST.MF so we do it manually
|
||||
|
||||
@@ -17,6 +17,8 @@ license=MIT
|
||||
credits=
|
||||
description=A server-side mod for managing skins.
|
||||
|
||||
mineskin_client_version=3.0.1-SNAPSHOT
|
||||
|
||||
# ParchmentMC mappings, see https://parchmentmc.org/docs/getting-started#choose-a-version for new versions
|
||||
parchment_minecraft=1.21
|
||||
parchment_version=2024.11.10
|
||||
|
||||
@@ -14,6 +14,26 @@ if (at.exists()) {
|
||||
minecraft.accessTransformers.file(at)
|
||||
}
|
||||
|
||||
jarJar.enable()
|
||||
|
||||
tasks.named('jar') {
|
||||
archiveClassifier = 'thin'
|
||||
}
|
||||
|
||||
tasks.named('jarJar') {
|
||||
archiveClassifier = ''
|
||||
}
|
||||
|
||||
jar.finalizedBy('jarJar')
|
||||
|
||||
dependencies {
|
||||
implementation "net.neoforged:neoforge:${neoforge_version}"
|
||||
|
||||
implementation(jarJar(group: 'org.mineskin', name: 'java-client', version: mineskin_client_version)) {
|
||||
jarJar.ranged(it, "[${mineskin_client_version},)")
|
||||
}
|
||||
}
|
||||
|
||||
subsystems {
|
||||
parchment {
|
||||
minecraftVersion = parchment_minecraft
|
||||
@@ -36,7 +56,3 @@ runs {
|
||||
workingDirectory = file('../run/server')
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "net.neoforged:neoforge:${neoforge_version}"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user