Schematic Share system (#1591)

* Very WIP in-game schematic sharing system

* Add support for paste meta, and send that data when possible

* Add ability to specify the name of the shared schematic
This commit is contained in:
Matthew Miller 2021-01-05 16:48:33 +10:00 committed by GitHub
parent df71f3ae7d
commit 303f5a76b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 167 additions and 18 deletions

View File

@ -54,6 +54,8 @@ import com.sk89q.worldedit.util.formatting.text.format.TextColor;
import com.sk89q.worldedit.util.io.Closer;
import com.sk89q.worldedit.util.io.file.FilenameException;
import com.sk89q.worldedit.util.io.file.MorePaths;
import com.sk89q.worldedit.util.paste.EngineHubPaste;
import com.sk89q.worldedit.util.paste.PasteMetadata;
import org.enginehub.piston.annotation.Command;
import org.enginehub.piston.annotation.CommandContainer;
import org.enginehub.piston.annotation.param.Arg;
@ -66,14 +68,19 @@ import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.Callable;
@ -146,7 +153,7 @@ public class SchematicCommands {
@Command(
name = "save",
desc = "Save a schematic into your clipboard"
desc = "Save your clipboard into a schematic file"
)
@CommandPermissions({ "worldedit.clipboard.save", "worldedit.schematic.save" })
public void save(Actor actor, LocalSession session,
@ -205,6 +212,39 @@ public class SchematicCommands {
.buildAndExec(worldEdit.getExecutorService());
}
@Command(
name = "share",
desc = "Share your clipboard as a schematic online"
)
@CommandPermissions({ "worldedit.clipboard.share", "worldedit.schematic.share" })
public void share(Actor actor, LocalSession session,
@Arg(desc = "Schematic name. Defaults to name-millis", def = "")
String schematicName,
@Arg(desc = "Format name.", def = "sponge")
String formatName) throws WorldEditException {
if (worldEdit.getPlatformManager().queryCapability(Capability.GAME_HOOKS).getDataVersion() == -1) {
actor.printError(TranslatableComponent.of("worldedit.schematic.unsupported-minecraft-version"));
return;
}
ClipboardFormat format = ClipboardFormats.findByAlias(formatName);
if (format == null) {
actor.printError(TranslatableComponent.of("worldedit.schematic.unknown-format", TextComponent.of(formatName)));
return;
}
ClipboardHolder holder = session.getClipboard();
SchematicShareTask task = new SchematicShareTask(actor, format, holder, schematicName);
AsyncCommandBuilder.wrap(task, actor)
.registerWithSupervisor(worldEdit.getSupervisor(), "Sharing schematic")
.setDelayMessage(TranslatableComponent.of("worldedit.schematic.save.saving"))
.setWorkingMessage(TranslatableComponent.of("worldedit.schematic.save.still-saving"))
.onSuccess("Shared", (url -> actor.printInfo(TextComponent.of(url.toExternalForm() + ".schem").clickEvent(ClickEvent.openUrl(url.toExternalForm() + ".schem")))))
.onFailure("Failed to share schematic", worldEdit.getPlatformManager().getPlatformCommandManager().getExceptionConverter())
.buildAndExec(worldEdit.getExecutorService());
}
@Command(
name = "delete",
aliases = {"d"},
@ -326,23 +366,18 @@ public class SchematicCommands {
}
}
private static class SchematicSaveTask implements Callable<Void> {
private final Actor actor;
private final File file;
private abstract static class SchematicOutputTask<T> implements Callable<T> {
protected final Actor actor;
private final ClipboardFormat format;
private final ClipboardHolder holder;
private final boolean overwrite;
SchematicSaveTask(Actor actor, File file, ClipboardFormat format, ClipboardHolder holder, boolean overwrite) {
SchematicOutputTask(Actor actor, ClipboardFormat format, ClipboardHolder holder) {
this.actor = actor;
this.file = file;
this.format = format;
this.holder = holder;
this.overwrite = overwrite;
}
@Override
public Void call() throws Exception {
protected void writeToOutputStream(OutputStream outputStream) throws Exception {
Clipboard clipboard = holder.getClipboard();
Transform transform = holder.getTransform();
Clipboard target;
@ -358,11 +393,28 @@ public class SchematicCommands {
}
try (Closer closer = Closer.create()) {
FileOutputStream fos = closer.register(new FileOutputStream(file));
BufferedOutputStream bos = closer.register(new BufferedOutputStream(fos));
OutputStream stream = closer.register(outputStream);
BufferedOutputStream bos = closer.register(new BufferedOutputStream(stream));
ClipboardWriter writer = closer.register(format.getWriter(bos));
writer.write(target);
}
}
}
private static class SchematicSaveTask extends SchematicOutputTask<Void> {
private final File file;
private final boolean overwrite;
SchematicSaveTask(Actor actor, File file, ClipboardFormat format, ClipboardHolder holder, boolean overwrite) {
super(actor, format, holder);
this.file = file;
this.overwrite = overwrite;
}
@Override
public Void call() throws Exception {
try {
writeToOutputStream(new FileOutputStream(file));
log.info(actor.getName() + " saved " + file.getCanonicalPath() + (overwrite ? " (overwriting previous file)" : ""));
} catch (IOException e) {
file.delete();
@ -372,6 +424,32 @@ public class SchematicCommands {
}
}
private static class SchematicShareTask extends SchematicOutputTask<URL> {
private final String name;
SchematicShareTask(Actor actor, ClipboardFormat format, ClipboardHolder holder, String name) {
super(actor, format, holder);
this.name = name;
}
@Override
public URL call() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
writeToOutputStream(baos);
} catch (Exception e) {
throw new CommandException(TextComponent.of(e.getMessage()), e, ImmutableList.of());
}
EngineHubPaste pasteService = new EngineHubPaste();
PasteMetadata metadata = new PasteMetadata();
metadata.author = this.actor.getName();
metadata.extension = "schem";
metadata.name = name == null ? actor.getName() + "-" + System.currentTimeMillis() : name;
return pasteService.paste(new String(Base64.getEncoder().encode(baos.toByteArray()), StandardCharsets.UTF_8), metadata).call();
}
}
private static class SchematicListTask implements Callable<Component> {
private final Comparator<Path> pathComparator;
private final int page;

View File

@ -40,6 +40,7 @@ import com.sk89q.worldedit.util.formatting.text.TranslatableComponent;
import com.sk89q.worldedit.util.formatting.text.event.HoverEvent;
import com.sk89q.worldedit.util.formatting.text.format.TextColor;
import com.sk89q.worldedit.util.paste.ActorCallbackPaste;
import com.sk89q.worldedit.util.paste.PasteMetadata;
import com.sk89q.worldedit.util.report.ConfigReport;
import com.sk89q.worldedit.util.report.ReportList;
import com.sk89q.worldedit.util.report.SystemInfoReport;
@ -137,7 +138,10 @@ public class WorldEditCommands {
if (pastebin) {
actor.checkPermission("worldedit.report.pastebin");
ActorCallbackPaste.pastebin(we.getSupervisor(), actor, result, TranslatableComponent.builder("worldedit.report.callback"));
PasteMetadata metadata = new PasteMetadata();
metadata.author = actor.getName();
metadata.extension = "report";
ActorCallbackPaste.pastebin(we.getSupervisor(), actor, result, metadata, TranslatableComponent.builder("worldedit.report.callback"));
}
}

View File

@ -77,4 +77,26 @@ public final class ActorCallbackPaste {
.buildAndExec(Pasters.getExecutor());
}
/**
* Submit data to a pastebin service and inform the sender of
* success or failure.
*
* @param supervisor The supervisor instance
* @param sender The sender
* @param content The content
* @param pasteMetadata The paste metadata
* @param successMessage The message builder, given the URL as an arg
*/
public static void pastebin(Supervisor supervisor, final Actor sender, String content, PasteMetadata pasteMetadata, final TranslatableComponent.Builder successMessage) {
Callable<URL> task = paster.paste(content, pasteMetadata);
AsyncCommandBuilder.wrap(task, sender)
.registerWithSupervisor(supervisor, "Submitting content to a pastebin service.")
.setDelayMessage(TranslatableComponent.of("worldedit.pastebin.uploading"))
.onSuccess((String) null, url -> sender.printInfo(successMessage.args(TextComponent.of(url.toString())).build()))
.onFailure("Failed to submit paste", null)
.buildAndExec(Pasters.getExecutor());
}
}

View File

@ -33,22 +33,37 @@ public class EngineHubPaste implements Paster {
private static final Gson GSON = new Gson();
@Override
public Callable<URL> paste(String content) {
return new PasteTask(content);
public Callable<URL> paste(String content, PasteMetadata metadata) {
return new PasteTask(content, metadata);
}
private static final class PasteTask implements Callable<URL> {
private final String content;
private final PasteMetadata metadata;
private PasteTask(String content) {
private PasteTask(String content, PasteMetadata metadata) {
this.content = content;
this.metadata = metadata;
}
@Override
public URL call() throws IOException, InterruptedException {
URL initialUrl = HttpRequest.url("https://paste.enginehub.org/signed_paste");
SignedPasteResponse response = GSON.fromJson(HttpRequest.get(initialUrl)
HttpRequest requestBuilder = HttpRequest.get(initialUrl);
requestBuilder.header("x-paste-meta-from", "EngineHub");
if (metadata.name != null) {
requestBuilder.header("x-paste-meta-name", metadata.name);
}
if (metadata.author != null) {
requestBuilder.header("x-paste-meta-author", metadata.author);
}
if (metadata.extension != null) {
requestBuilder.header("x-paste-meta-extension", metadata.extension);
}
SignedPasteResponse response = GSON.fromJson(requestBuilder
.execute()
.expectResponseCode(200)
.returnContent()

View File

@ -0,0 +1,26 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) WorldEdit team and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.sk89q.worldedit.util.paste;
public class PasteMetadata {
public String name;
public String extension;
public String author;
}

View File

@ -24,6 +24,10 @@ import java.util.concurrent.Callable;
public interface Paster {
Callable<URL> paste(String content);
default Callable<URL> paste(String content) {
return paste(content, new PasteMetadata());
}
Callable<URL> paste(String content, PasteMetadata metadata);
}