diff --git a/src/main/java/de/sudoplugin/SudoCommand.java b/src/main/java/de/sudoplugin/SudoCommand.java new file mode 100644 index 0000000..4b79f0b --- /dev/null +++ b/src/main/java/de/sudoplugin/SudoCommand.java @@ -0,0 +1,214 @@ +package de.sudoplugin; + +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; + +import java.io.File; +import java.util.*; +import java.util.stream.Collectors; + +public class SudoCommand implements CommandExecutor, TabCompleter { + + private final SudoPlugin plugin; + private final Map commandMap = new LinkedHashMap<>(); + + // Cooldown: UUID des Ausführers -> Zeitpunkt letzter Nutzung (ms) + private final Map> cooldownPerTarget = new HashMap<>(); + private final Map cooldownGlobal = new HashMap<>(); + + public SudoCommand(SudoPlugin plugin) { + this.plugin = plugin; + loadCommands(); + } + + public void loadCommands() { + commandMap.clear(); + File file = new File(plugin.getDataFolder(), "commands.yml"); + if (!file.exists()) { + plugin.getLogger().warning("commands.yml nicht gefunden!"); + return; + } + YamlConfiguration cfg = YamlConfiguration.loadConfiguration(file); + for (Map entry : cfg.getMapList("commands")) { + String name = (String) entry.get("name"); + String args = entry.containsKey("args") ? (String) entry.get("args") : ""; + if (name != null && !name.isBlank()) + commandMap.put(name.toLowerCase(), args == null ? "" : args.toLowerCase()); + } + plugin.getLogger().info(commandMap.size() + " Befehle aus commands.yml geladen."); + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + + // /sudo reload + if (args.length == 1 && args[0].equalsIgnoreCase("reload")) { + if (!sender.hasPermission("sudoplugin.use")) { + sender.sendMessage(plugin.msg("no-permission")); + return true; + } + plugin.reloadConfig(); + plugin.openLogFile(); + loadCommands(); + sender.sendMessage(plugin.msg("reload-success")); + return true; + } + + if (args.length < 2) { + sender.sendMessage(plugin.msg("usage")); + return true; + } + + // Konsolen-Check + boolean isConsole = !(sender instanceof Player); + if (isConsole && !plugin.getConfig().getBoolean("security.allow-console", true)) { + sender.sendMessage(plugin.msg("no-permission")); + return true; + } + + if (!isConsole && !sender.hasPermission("sudoplugin.use")) { + sender.sendMessage(plugin.msg("no-permission")); + return true; + } + + Player target = Bukkit.getPlayer(args[0]); + if (target == null) { + sender.sendMessage(plugin.msg("player-not-found", "{player}", args[0])); + return true; + } + + // Self-sudo Check + if (!isConsole && !plugin.getConfig().getBoolean("security.allow-self-sudo", false)) { + if (((Player) sender).getUniqueId().equals(target.getUniqueId())) { + sender.sendMessage(plugin.msg("self-sudo-denied")); + return true; + } + } + + // OP-Check + if (!isConsole && !plugin.getConfig().getBoolean("security.allow-sudo-on-op", false)) { + if (target.isOp() && !sender.isOp()) { + sender.sendMessage(plugin.msg("op-sudo-denied")); + return true; + } + } + + // Blocked-Commands Check + List blocked = plugin.getConfig().getStringList("security.blocked-commands"); + if (blocked.stream().anyMatch(b -> b.equalsIgnoreCase(args[1]))) { + sender.sendMessage(plugin.msg("command-blocked")); + return true; + } + + // Cooldown Check + if (!isConsole) { + Player senderPlayer = (Player) sender; + String bypassPerm = plugin.getConfig().getString("cooldown.bypass-permission", "sudoplugin.cooldown.bypass"); + int cooldownSecs = plugin.getConfig().getInt("cooldown.seconds", 0); + boolean perTarget = plugin.getConfig().getBoolean("cooldown.per-target", false); + + if (cooldownSecs > 0 && !senderPlayer.hasPermission(bypassPerm)) { + long now = System.currentTimeMillis(); + long last = 0; + + if (perTarget) { + last = cooldownPerTarget + .computeIfAbsent(senderPlayer.getUniqueId(), k -> new HashMap<>()) + .getOrDefault(target.getUniqueId(), 0L); + } else { + last = cooldownGlobal.getOrDefault(senderPlayer.getUniqueId(), 0L); + } + + long diff = (now - last) / 1000; + if (diff < cooldownSecs) { + long remaining = cooldownSecs - diff; + sender.sendMessage(plugin.msg("cooldown-active", "{seconds}", String.valueOf(remaining))); + return true; + } + + // Cooldown setzen + if (perTarget) { + cooldownPerTarget.computeIfAbsent(senderPlayer.getUniqueId(), k -> new HashMap<>()) + .put(target.getUniqueId(), now); + } else { + cooldownGlobal.put(senderPlayer.getUniqueId(), now); + } + } + } + + // Befehl zusammenbauen & ausführen + StringBuilder fullCommand = new StringBuilder(args[1]); + for (int i = 2; i < args.length; i++) fullCommand.append(" ").append(args[i]); + String commandToRun = fullCommand.toString(); + + target.performCommand(commandToRun); + + // Sender benachrichtigen + if (plugin.getConfig().getBoolean("notifications.notify-sender", true)) { + sender.sendMessage(plugin.msg("success", "{cmd}", commandToRun, "{player}", target.getName())); + } + + // Zielspieler benachrichtigen + if (plugin.getConfig().getBoolean("notifications.notify-target", false)) { + String notifyMsg = SudoPlugin.colorize( + plugin.getConfig().getString("notifications.notify-message", "&eEin Admin hat einen Befehl als du ausgeführt.")); + target.sendMessage(notifyMsg); + } + + // Konsolen-Log + String logEntry = sender.getName() + " -> /" + commandToRun + " als " + target.getName(); + if (plugin.getConfig().getBoolean("notifications.log-to-console", true)) { + plugin.getLogger().info(logEntry); + } + + // Datei-Log + if (plugin.getConfig().getBoolean("notifications.log-to-file", false)) { + plugin.writeLog(logEntry); + } + + return true; + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + boolean isConsole = !(sender instanceof Player); + if (!isConsole && !sender.hasPermission("sudoplugin.use")) return new ArrayList<>(); + + if (args.length == 1) { + return Bukkit.getOnlinePlayers().stream() + .map(Player::getName) + .filter(n -> n.toLowerCase().startsWith(args[0].toLowerCase())) + .collect(Collectors.toList()); + } + + if (args.length == 2) { + List blocked = plugin.getConfig().getStringList("security.blocked-commands"); + return commandMap.keySet().stream() + .filter(c -> c.startsWith(args[1].toLowerCase())) + .filter(c -> blocked.stream().noneMatch(b -> b.equalsIgnoreCase(c))) + .collect(Collectors.toList()); + } + + if (args.length == 3) { + String argType = commandMap.getOrDefault(args[1].toLowerCase(), ""); + if (argType.equals("player")) { + return Bukkit.getOnlinePlayers().stream() + .map(Player::getName) + .filter(n -> n.toLowerCase().startsWith(args[2].toLowerCase())) + .collect(Collectors.toList()); + } + if (argType.equals("gamemode")) { + return Arrays.asList("survival", "creative", "adventure", "spectator").stream() + .filter(m -> m.startsWith(args[2].toLowerCase())) + .collect(Collectors.toList()); + } + } + + return new ArrayList<>(); + } +} diff --git a/src/main/java/de/sudoplugin/SudoPlugin.java b/src/main/java/de/sudoplugin/SudoPlugin.java new file mode 100644 index 0000000..3e719c5 --- /dev/null +++ b/src/main/java/de/sudoplugin/SudoPlugin.java @@ -0,0 +1,72 @@ +package de.sudoplugin; + +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class SudoPlugin extends JavaPlugin { + + private PrintWriter logWriter; + private static final DateTimeFormatter LOG_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + @Override + public void onEnable() { + saveDefaultConfig(); + if (!new File(getDataFolder(), "commands.yml").exists()) { + saveResource("commands.yml", false); + } + openLogFile(); + + SudoCommand sudoCommand = new SudoCommand(this); + getCommand("sudo").setExecutor(sudoCommand); + getCommand("sudo").setTabCompleter(sudoCommand); + getLogger().info("SudoPlugin aktiviert!"); + } + + @Override + public void onDisable() { + if (logWriter != null) logWriter.close(); + getLogger().info("SudoPlugin deaktiviert!"); + } + + public void openLogFile() { + if (logWriter != null) { logWriter.close(); logWriter = null; } + if (!getConfig().getBoolean("notifications.log-to-file", false)) return; + try { + File logFile = new File(getDataFolder(), "sudo.log"); + logWriter = new PrintWriter(new FileWriter(logFile, true), true); + } catch (IOException e) { + getLogger().warning("Konnte sudo.log nicht öffnen: " + e.getMessage()); + } + } + + public void writeLog(String entry) { + if (logWriter == null) return; + logWriter.println("[" + LocalDateTime.now().format(LOG_FORMAT) + "] " + entry); + } + + /** Gibt eine formatierte Nachricht aus der config zurück. */ + public String msg(String key) { + String prefix = colorize(getConfig().getString("messages.prefix", "&8[&cSudo&8] &r")); + String message = colorize(getConfig().getString("messages." + key, "&c[" + key + " fehlt in config.yml]")); + return prefix + message; + } + + /** Wie msg(), aber mit Platzhalter-Ersetzung. */ + public String msg(String key, String... replacements) { + String m = msg(key); + for (int i = 0; i + 1 < replacements.length; i += 2) { + m = m.replace(replacements[i], replacements[i + 1]); + } + return m; + } + + public static String colorize(String s) { + return org.bukkit.ChatColor.translateAlternateColorCodes('&', s); + } +} diff --git a/src/main/resources/commands.yml b/src/main/resources/commands.yml new file mode 100644 index 0000000..e34eebe --- /dev/null +++ b/src/main/resources/commands.yml @@ -0,0 +1,63 @@ +# SudoPlugin - Verfügbare Befehle für Tab-Completion +# Hier kannst du Befehle hinzufügen oder entfernen. +# +# Format: +# - name: Befehlsname (ohne /) +# args: "player" # optionaler Typ für arg3-Completion +# # Mögliche Werte: player, gamemode, leer lassen wenn nichts +# +# Beispiel eigener Befehl: +# - name: meinbefehl +# args: "" + +commands: + - name: msg + args: player + - name: tell + args: player + - name: w + args: player + - name: whisper + args: player + - name: me + args: "" + - name: say + args: "" + - name: tp + args: player + - name: tpa + args: player + - name: tpaccept + args: "" + - name: tpdeny + args: "" + - name: gamemode + args: gamemode + - name: give + args: "" + - name: fly + args: "" + - name: speed + args: "" + - name: heal + args: "" + - name: feed + args: "" + - name: spawn + args: "" + - name: home + args: "" + - name: sethome + args: "" + - name: warp + args: "" + - name: kit + args: "" + - name: pay + args: player + - name: balance + args: "" + - name: seen + args: player + - name: ignore + args: player diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..d86b374 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,69 @@ +# ----------------------------------------------- +# SudoPlugin - Konfiguration +# ----------------------------------------------- + +# Sprache der Plugin-Nachrichten +# Verfügbare Werte: de, en +language: de + +# ----------------------------------------------- +# BENACHRICHTIGUNGEN +# ----------------------------------------------- +notifications: + # Soll der Zielspieler informiert werden wenn ein Befehl als er ausgeführt wird? + notify-target: false + notify-message: "&eEin Admin hat einen Befehl als du ausgeführt." + + # Soll der ausführende Spieler eine Bestätigungsnachricht sehen? + notify-sender: true + + # Soll in der Konsole geloggt werden? + log-to-console: true + + # Soll ein Eintrag in eine Log-Datei geschrieben werden? (plugins/SudoPlugin/sudo.log) + log-to-file: false + +# ----------------------------------------------- +# BERECHTIGUNGEN & SICHERHEIT +# ----------------------------------------------- +security: + # Kann ein Spieler /sudo auf sich selbst ausführen? + allow-self-sudo: false + + # Können Spieler mit sudoplugin.use auch andere OPs mit sudo ansprechen? + allow-sudo-on-op: false + + # Kann /sudo auch von der Konsole aus genutzt werden? + allow-console: true + + # Bestimmte Befehle komplett sperren (werden nie ausgeführt, egal was in commands.yml steht) + # Beispiel: [op, deop, stop] + blocked-commands: [] + +# ----------------------------------------------- +# COOLDOWN +# ----------------------------------------------- +cooldown: + # Cooldown zwischen sudo-Befehlen in Sekunden (0 = deaktiviert) + seconds: 0 + + # Soll der Cooldown pro Zielspieler gelten (true) oder global pro Ausführer (false)? + per-target: false + + # Spieler mit dieser Permission sind vom Cooldown ausgenommen + bypass-permission: sudoplugin.cooldown.bypass + +# ----------------------------------------------- +# NACHRICHTEN-FORMAT +# ----------------------------------------------- +messages: + prefix: "&8[&cSudo&8] &r" + no-permission: "&cDu hast keine Berechtigung." + player-not-found: "&cSpieler '{player}' ist nicht online." + usage: "&cVerwendung: /sudo [argumente...]" + success: "&aBefehl &f/{cmd} &awurde als &f{player} &aausgeführt." + self-sudo-denied: "&cDu kannst /sudo nicht auf dich selbst anwenden." + op-sudo-denied: "&cDu kannst /sudo nicht auf einen OP anwenden." + command-blocked: "&cDieser Befehl ist in der Konfiguration gesperrt." + cooldown-active: "&cBitte warte noch &f{seconds}s &cbevor du /sudo erneut nutzt." + reload-success: "&aKonfiguration erfolgreich neu geladen." diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..526db0a --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,17 @@ +name: SudoPlugin +version: 1.0.0 +main: de.sudoplugin.SudoPlugin +api-version: 1.20 +author: M_Viper +description: Lets admins run commands as other players + +commands: + sudo: + description: Execute a command as another player + usage: /sudo [args...] + permission: sudoplugin.use + +permissions: + sudoplugin.use: + description: Allows use of the /sudo command + default: op