feat: submit

This commit is contained in:
2025-12-08 23:09:57 -08:00
parent 4dd36d9141
commit 6caa6b261f
8 changed files with 372 additions and 31 deletions

View File

@@ -8,6 +8,7 @@
plugins {
`java-library`
id("com.gradleup.shadow") version "9.3.0"
id("xyz.jpenilla.run-velocity") version "3.0.2"
}
@@ -38,6 +39,9 @@ java {
}
tasks {
shadowJar {
archiveFileName.set("mineroo-velocity.jar")
}
runVelocity {
// Configure the Velocity version for our task.
// This is the only required configuration besides applying the plugin.

View File

@@ -19,16 +19,24 @@ public class Config {
@ConfigSerializable
public static class ServerSection {
@Setting("hostname")
@Comment("Server Hostname")
private String hostname = "";
@Setting("address")
@Comment("Server Address")
private String address = "";
@Setting("port")
@Comment("Server Port")
private Integer port = null;
@Setting("token")
@Comment("Server Bind Token")
private String bindToken = "get token from `mineroo.online/resources/servers` page!";
public String getHostname() {
return hostname;
public String getAddress() {
return address;
}
public Integer getPort() {
return port;
}
public String getBindToken() {

View File

@@ -4,12 +4,13 @@ import com.google.inject.Inject;
import com.velocitypowered.api.command.CommandManager;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.ProxyServer;
import online.mineroo.velocity.commands.MainCommand;
import online.mineroo.velocity.listeners.BindListener;
import online.mineroo.velocity.utils.MessageManager;
import java.io.IOException;
import java.nio.file.Files;
@@ -31,6 +32,10 @@ public class MinerooPlugin {
private final Path dataDirectory;
private Config config;
private BindListener bindListener;
private MessageManager messageManager;
@Inject
public MinerooPlugin(ProxyServer server, Logger logger, @DataDirectory Path dataDirectory) {
this.server = server;
@@ -40,50 +45,68 @@ public class MinerooPlugin {
@Subscribe
public void onProxyInitialization(ProxyInitializeEvent event) {
loadConfig();
this.bindListener = new BindListener();
server.getEventManager().register(this, bindListener);
reloadConfig();
this.messageManager = new MessageManager(config);
CommandManager commandManager = server.getCommandManager();
commandManager.register(commandManager.metaBuilder("mineroo").build(), new MainCommand());
}
@Subscribe
public void onProxyPing(ProxyPingEvent event) {
String token = "1";
commandManager.register(commandManager.metaBuilder("mineroo").build(),
new MainCommand(this));
}
// load & create config
private void loadConfig() {
public boolean reloadConfig() {
try {
// create dir if not exists
if (Files.notExists(dataDirectory)) {
Files.createDirectories(dataDirectory);
}
Path configPath = dataDirectory.resolve("config.yaml");
// Inital Loader
ConfigurationLoader<CommentedConfigurationNode> loader = YamlConfigurationLoader.builder()
.path(configPath)
.nodeStyle(NodeStyle.BLOCK)
.build();
// Create & load config file
if (Files.notExists(configPath)) {
logger.warn("Configuration file not found, creating default config.toml...");
logger.warn("Please change `config.toml` before bind server.");
logger.warn("Configuration file not found, creating default config.yaml...");
this.config = new Config();
CommentedConfigurationNode node = loader.createNode();
node.set(Config.class, this.config);
loader.save(node);
} else {
logger.info("Loading configuration...");
logger.info("Reloading configuration...");
CommentedConfigurationNode node = loader.load();
this.config = node.get(Config.class);
}
return true;
} catch (IOException e) {
logger.error("Failed to load or create config.yaml!", e);
logger.error("Failed to reload config!", e);
return false;
}
}
public ProxyServer getServer() {
return this.server;
}
public Logger getLogger() {
return logger;
}
public Config getConfig() {
return config;
}
public BindListener getBindListener() {
return bindListener;
}
public MessageManager getMessageManager() {
return messageManager;
}
}

View File

@@ -1,18 +1,151 @@
package online.mineroo.velocity.commands;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.command.SimpleCommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import online.mineroo.velocity.Config;
import online.mineroo.velocity.MinerooPlugin;
import online.mineroo.velocity.utils.BindRequest;
import online.mineroo.velocity.utils.MessageManager;
public class MainCommand implements SimpleCommand {
private final MinerooPlugin plugin;
public MainCommand(MinerooPlugin plugin) {
this.plugin = plugin;
}
@Override
public void execute(Invocation invocation) {
String[] args = invocation.arguments();
if (args.length == 0) {
if (args.length < 1) {
sendHelp(invocation.source());
return;
}
String subCommand = args[0];
if (subCommand.equals("bind")) {
bindServer(invocation.source());
}
if (subCommand.equalsIgnoreCase("reload")) {
reload(invocation.source());
}
}
@Override
public List<String> suggest(Invocation invocation) {
String[] args = invocation.arguments();
if (args.length == 0) {
return List.of("bind", "reload");
}
if (args.length == 1) {
String input = args[0].toLowerCase();
return Stream.of("bind", "reload")
.filter(cmd -> cmd.startsWith(input))
.collect(Collectors.toList());
}
return List.of();
}
public void reload(CommandSource source) {
MessageManager msg = plugin.getMessageManager();
long start = System.currentTimeMillis();
boolean success = plugin.reloadConfig();
if (success) {
long time = System.currentTimeMillis() - start;
source.sendMessage(msg.get("command.reload.success", "time", String.valueOf(time)));
} else {
source.sendMessage(msg.get("command.reload.failed"));
}
return;
}
public void bindServer(CommandSource source) {
Config config = plugin.getConfig();
MessageManager msg = plugin.getMessageManager();
String bind_name = config.getServerName();
String bind_description = config.getDescription();
String bind_token = config.getServer().getBindToken();
String bind_address = config.getServer().getAddress();
Integer bind_port = config.getServer().getPort();
source.sendMessage(msg.get("command.bind.server.start"));
Logger logger = plugin.getLogger();
plugin.getServer().getScheduler().buildTask(plugin, () -> {
BindRequest request = new BindRequest(logger, bind_token);
try {
boolean inital_bind = request.initalMotdVerifyRequest(bind_address, bind_port);
if (inital_bind) {
String motdToken = request.getMotdToken();
plugin.getBindListener().setVerificationToken(motdToken);
source.sendMessage(msg.get("command.bind.server.wait"));
Thread.sleep(2 * 60 * 1000 + 5000);
// Final Bind
boolean success = false;
int maxRetries = 3;
for (int i = 1; i <= maxRetries; i++) {
if (request.finalizeServerBinding(bind_name, bind_description)) {
success = true;
break;
}
if (i < maxRetries) {
source.sendMessage(msg.get("command.bind.server.retry",
"current", String.valueOf(i),
"max", String.valueOf(maxRetries)));
Thread.sleep(10000);
}
}
if (success) {
source.sendMessage(msg.get("command.bind.server.success"));
} else {
source.sendMessage(msg.get("command.bind.server.failed"));
}
} else {
source.sendMessage(
Component.text("Initial handshake failed. Check your token or network.", NamedTextColor.RED));
}
} catch (Exception e) {
logger.error("Mineroo Bind: " + e.toString());
} finally {
plugin.getBindListener().clearVerificationToken();
}
}).schedule();
}
public void sendHelp(CommandSource source) {

View File

@@ -0,0 +1,36 @@
package online.mineroo.velocity.listeners;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
import com.velocitypowered.api.proxy.server.ServerPing;
import net.kyori.adventure.text.Component;
import java.util.concurrent.atomic.AtomicReference;
public class BindListener {
private final AtomicReference<String> verificationToken = new AtomicReference<>(null);
public void setVerificationToken(String token) {
this.verificationToken.set(token);
}
public void clearVerificationToken() {
this.verificationToken.set(null);
}
@Subscribe
public void onPing(ProxyPingEvent event) {
String token = verificationToken.get();
if (token == null) {
return;
}
ServerPing.Builder builder = event.getPing().asBuilder();
builder.description(Component.text(token));
event.setPing(builder.build());
}
}

View File

@@ -8,9 +8,12 @@ import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
public class BindRequest {
@@ -19,23 +22,31 @@ public class BindRequest {
private final Logger logger;
private final HttpClient httpClient;
public BindRequest(Logger logger, HttpClient httpClient) {
private final String bindToken;
private String motdToken = "";
private String sessionToken = "";
public BindRequest(Logger logger, String bindToken) {
this.logger = logger;
this.httpClient = httpClient;
this.bindToken = bindToken;
this.httpClient = HttpClient.newHttpClient();
}
public void InitalMotdVerifyRequest(String token, String hostname, int port)
throws IOException, InterruptedException {
public boolean initalMotdVerifyRequest(String hostname, Integer port)
throws IOException, InterruptedException, JsonParseException {
// generate uri
String uri = endpoint + "/server/motd-verify";
// build jsonBody
JsonObject json = new JsonObject();
json.addProperty("server_adress", hostname);
json.addProperty("server_address", hostname);
json.addProperty("server_port", port);
String basicAuth = "mbind:" + token;
String basicAuth = "mbind:" + bindToken;
String encodedAuth = Base64.getEncoder().encodeToString(basicAuth.getBytes(StandardCharsets.UTF_8));
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri))
@@ -46,8 +57,56 @@ public class BindRequest {
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
logger.debug("test");
String body = response.body();
JsonObject responseData = JsonParser.parseString(body).getAsJsonObject();
if (responseData.has("motd_token")) {
motdToken = responseData.get("motd_token").getAsString();
} else {
logger.error("Mineroo Bind: MOTD Token not received.");
return false;
}
if (responseData.has("session_token")) {
sessionToken = responseData.get("session_token").getAsString();
} else {
logger.error("Mineroo Bind: Session Token not received.");
return false;
}
return true;
} else {
logger.error("Mineroo Bind: " + response.body().toString());
}
return false;
}
public boolean finalizeServerBinding(@Nullable String name, @Nullable String description)
throws IOException, InterruptedException, JsonParseException {
String uri = endpoint + "/server/server-bind";
JsonObject json = new JsonObject();
json.addProperty("session_token", sessionToken);
json.addProperty("name", name);
json.addProperty("description", description);
String basicAuth = "mbind:" + bindToken;
String encodedAuth = Base64.getEncoder().encodeToString(basicAuth.getBytes(StandardCharsets.UTF_8));
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri))
.header("Content-Type", "application/json")
.header("Authorization", "Basic " + encodedAuth)
.POST(HttpRequest.BodyPublishers.ofString(json.toString())).build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
return response.statusCode() == 200;
}
public String getMotdToken() {
return motdToken;
}
}

View File

@@ -0,0 +1,67 @@
package online.mineroo.velocity.utils;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import online.mineroo.velocity.Config;
import java.util.Locale;
import java.util.ResourceBundle;
public class MessageManager {
private final Config config;
private ResourceBundle bundle;
private final MiniMessage miniMessage;
public MessageManager(Config config) {
this.config = config;
this.miniMessage = MiniMessage.miniMessage();
reload();
}
public void reload() {
String lang = "zh-CN";
Locale locale;
if (lang.contains("-")) {
String[] parts = lang.split("-");
locale = Locale.of(parts[0], parts[1]);
} else {
locale = Locale.of(lang);
}
try {
this.bundle = ResourceBundle.getBundle("mineroo.messages", locale);
} catch (Exception e) {
this.bundle = ResourceBundle.getBundle("mineroo.messages", Locale.ROOT);
}
}
public Component get(String key) {
String raw = getString(key);
return miniMessage.deserialize(raw);
}
public Component get(String key, String... placeholders) {
String raw = getString(key);
TagResolver.Builder builder = TagResolver.builder();
for (int i = 0; i < placeholders.length; i += 2) {
if (i + 1 < placeholders.length) {
builder.resolver(Placeholder.parsed(placeholders[i], placeholders[i + 1]));
}
}
return miniMessage.deserialize(raw, builder.build());
}
private String getString(String key) {
try {
return bundle.getString(key);
} catch (Exception e) {
return "<red>Missing key: " + key;
}
}
}

View File

@@ -0,0 +1,11 @@
# Command Bind
command.bind.server.start=<gray>开始服务器绑定流程,请稍等...</gray>
command.bind.server.inital.failed=<red>初次握手失败。检查您的令牌或网络。</red>
command.bind.server.wait=<aqua>正在验证服务器所有权... 数据同步可能需要 2 分钟,请稍候。</aqua>
command.bind.server.success=<green>绑定成功!<green>
command.bind.server.failed=<red>绑定失败。</red>
command.bind.server.retry=<yellow>验证未通过10 秒后重试 (<current>/<max>) ...</yellow>
# Command Reload
command.reload.success=<green>配置文件重载成功!耗时 <time>ms</green>
command.reload.failed=<red>配置文件重载失败,请查看控制台日志。</red>