1
0
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:
2024-11-27 21:37:21 +03:00
parent fb7020354a
commit 0d56c339d1
10 changed files with 287 additions and 31 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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