Mirror von
https://github.com/Moulberry/AxiomPaperPlugin.git
synchronisiert 2024-11-17 05:40:06 +01:00
CoreProtect Integration
Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>
Dieser Commit ist enthalten in:
Ursprung
f879e8b038
Commit
84d2153c38
3
.gitignore
vendored
3
.gitignore
vendored
@ -15,6 +15,9 @@ out/
|
|||||||
plugin/bin/
|
plugin/bin/
|
||||||
api/bin/
|
api/bin/
|
||||||
|
|
||||||
|
# VSCode
|
||||||
|
.vscode/
|
||||||
|
|
||||||
# Compiled class file
|
# Compiled class file
|
||||||
*.class
|
*.class
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
plugins {
|
plugins {
|
||||||
`java-library`
|
`java-library`
|
||||||
id("io.papermc.paperweight.userdev") version "1.5.11"
|
alias(libs.plugins.paperweight.userdev)
|
||||||
id("xyz.jpenilla.run-paper") version "2.2.2" // Adds runServer and runMojangMappedServer tasks for testing
|
alias(libs.plugins.run.paper) // Adds runServer and runMojangMappedServer tasks for testing
|
||||||
|
|
||||||
// Shades and relocates dependencies into our plugin jar. See https://imperceptiblethoughts.com/shadow/introduction/
|
// Shades and relocates dependencies into our plugin jar. See https://imperceptiblethoughts.com/shadow/introduction/
|
||||||
id("com.github.johnrengelman.shadow") version "8.1.1"
|
alias(libs.plugins.shadow)
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "com.moulberry.axiom"
|
group = "com.moulberry.axiom"
|
||||||
@ -23,26 +23,30 @@ repositories {
|
|||||||
maven("https://jitpack.io")
|
maven("https://jitpack.io")
|
||||||
maven("https://repo.papermc.io/repository/maven-public/")
|
maven("https://repo.papermc.io/repository/maven-public/")
|
||||||
maven("https://maven.enginehub.org/repo/")
|
maven("https://maven.enginehub.org/repo/")
|
||||||
|
maven("https://maven.playpro.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
paperweight.paperDevBundle("1.20.4-R0.1-SNAPSHOT")
|
paperweight.paperDevBundle(libs.versions.paper)
|
||||||
implementation("xyz.jpenilla:reflection-remapper:0.1.0-SNAPSHOT")
|
implementation(libs.reflection.remapper)
|
||||||
implementation("org.incendo:cloud-paper:2.0.0-beta.2")
|
implementation(libs.cloud.paper)
|
||||||
|
|
||||||
// Zstd Compression Library
|
// Zstd Compression Library
|
||||||
implementation("com.github.luben:zstd-jni:1.5.5-4")
|
implementation(libs.zstd.jni)
|
||||||
|
|
||||||
// ViaVersion support
|
// ViaVersion support
|
||||||
compileOnly("com.viaversion:viaversion-api:4.10.1-SNAPSHOT")
|
compileOnly(libs.viaversion.api)
|
||||||
|
|
||||||
// WorldGuard support
|
// WorldGuard support
|
||||||
compileOnly("com.sk89q.worldguard:worldguard-bukkit:7.1.0-SNAPSHOT")
|
compileOnly(libs.worldguard.bukkit)
|
||||||
|
|
||||||
// PlotSquared support
|
// PlotSquared support
|
||||||
implementation(platform("com.intellectualsites.bom:bom-newest:1.37"))
|
implementation(platform(libs.bom.newest))
|
||||||
compileOnly("com.intellectualsites.plotsquared:plotsquared-core")
|
compileOnly(libs.plotsquared.core)
|
||||||
compileOnly("com.intellectualsites.plotsquared:plotsquared-bukkit") { isTransitive = false }
|
compileOnly(libs.plotsquared.bukkit) { isTransitive = false }
|
||||||
|
|
||||||
|
// CoreProtect support
|
||||||
|
compileOnly(libs.coreprotect)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
|
32
gradle/libs.versions.toml
Normale Datei
32
gradle/libs.versions.toml
Normale Datei
@ -0,0 +1,32 @@
|
|||||||
|
[versions]
|
||||||
|
# Dependencies
|
||||||
|
bom-newest = "1.37"
|
||||||
|
cloud-paper = "2.0.0-20240516.054251-69"
|
||||||
|
coreprotect = "22.4"
|
||||||
|
paper = "1.20.4-R0.1-SNAPSHOT"
|
||||||
|
plotsquared = "7.3.9-20240513.192211-13"
|
||||||
|
reflection-remapper = "0.1.2-20240315.033304-2"
|
||||||
|
viaversion-api = "4.10.1-20240505.124211-22"
|
||||||
|
worldguard-bukkit = "7.1.0-20240503.180049-12"
|
||||||
|
zstd-jni = "1.5.5-4"
|
||||||
|
|
||||||
|
# Plugins
|
||||||
|
paperweight-userdev = "1.5.11"
|
||||||
|
run-paper = "2.2.2"
|
||||||
|
shadow = "8.1.1"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
bom-newest = { group = "com.intellectualsites.bom", name = "bom-newest", version.ref = "bom-newest" }
|
||||||
|
cloud-paper = { group = "org.incendo", name = "cloud-paper", version.ref = "cloud-paper" }
|
||||||
|
coreprotect = { group = "net.coreprotect", name = "coreprotect", version.ref = "coreprotect" }
|
||||||
|
plotsquared-bukkit = { group = "com.intellectualsites.plotsquared", name = "plotsquared-bukkit", version.ref = "plotsquared" }
|
||||||
|
plotsquared-core = { group = "com.intellectualsites.plotsquared", name = "plotsquared-core", version.ref = "plotsquared" }
|
||||||
|
reflection-remapper = { group = "xyz.jpenilla", name = "reflection-remapper", version.ref = "reflection-remapper" }
|
||||||
|
viaversion-api = { group = "com.viaversion", name = "viaversion-api", version.ref = "viaversion-api" }
|
||||||
|
worldguard-bukkit = { group = "com.sk89q.worldguard", name = "worldguard-bukkit", version.ref = "worldguard-bukkit" }
|
||||||
|
zstd-jni = { group = "com.github.luben", name = "zstd-jni", version = "1.5.5-4" }
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
paperweight-userdev = { id = "io.papermc.paperweight.userdev", version.ref = "paperweight-userdev" }
|
||||||
|
run-paper = { id = "xyz.jpenilla.run-paper", version.ref = "run-paper" }
|
||||||
|
shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" }
|
@ -6,6 +6,7 @@ import com.moulberry.axiom.buffer.CompressedBlockEntity;
|
|||||||
import com.moulberry.axiom.commands.AxiomDebugCommand;
|
import com.moulberry.axiom.commands.AxiomDebugCommand;
|
||||||
import com.moulberry.axiom.event.AxiomCreateWorldPropertiesEvent;
|
import com.moulberry.axiom.event.AxiomCreateWorldPropertiesEvent;
|
||||||
import com.moulberry.axiom.event.AxiomModifyWorldEvent;
|
import com.moulberry.axiom.event.AxiomModifyWorldEvent;
|
||||||
|
import com.moulberry.axiom.integration.coreprotect.CoreProtectIntegration;
|
||||||
import com.moulberry.axiom.integration.plotsquared.PlotSquaredIntegration;
|
import com.moulberry.axiom.integration.plotsquared.PlotSquaredIntegration;
|
||||||
import com.moulberry.axiom.packet.*;
|
import com.moulberry.axiom.packet.*;
|
||||||
import com.moulberry.axiom.world_properties.server.ServerWorldPropertiesRegistry;
|
import com.moulberry.axiom.world_properties.server.ServerWorldPropertiesRegistry;
|
||||||
@ -38,7 +39,7 @@ import org.bukkit.plugin.messaging.Messenger;
|
|||||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||||
import org.incendo.cloud.bukkit.CloudBukkitCapabilities;
|
import org.incendo.cloud.bukkit.CloudBukkitCapabilities;
|
||||||
import org.incendo.cloud.execution.ExecutionCoordinator;
|
import org.incendo.cloud.execution.ExecutionCoordinator;
|
||||||
import org.incendo.cloud.paper.PaperCommandManager;
|
import org.incendo.cloud.paper.LegacyPaperCommandManager;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.FileReader;
|
import java.io.FileReader;
|
||||||
@ -285,16 +286,22 @@ public class AxiomPaper extends JavaPlugin implements Listener {
|
|||||||
WorldExtension.tick(MinecraftServer.getServer(), sendMarkers, maxChunkRelightsPerTick, maxChunkSendsPerTick);
|
WorldExtension.tick(MinecraftServer.getServer(), sendMarkers, maxChunkRelightsPerTick, maxChunkSendsPerTick);
|
||||||
}, 1, 1);
|
}, 1, 1);
|
||||||
|
|
||||||
PaperCommandManager<CommandSender> manager = PaperCommandManager.createNative(
|
try {
|
||||||
|
LegacyPaperCommandManager<CommandSender> manager = LegacyPaperCommandManager.createNative(
|
||||||
this,
|
this,
|
||||||
ExecutionCoordinator.simpleCoordinator()
|
ExecutionCoordinator.simpleCoordinator()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (manager.hasCapability(CloudBukkitCapabilities.NATIVE_BRIGADIER)) {
|
if (manager.hasCapability(CloudBukkitCapabilities.NATIVE_BRIGADIER)) {
|
||||||
manager.registerBrigadier();
|
manager.registerBrigadier();
|
||||||
}
|
}
|
||||||
|
|
||||||
AxiomDebugCommand.register(this, manager);
|
AxiomDebugCommand.register(this, manager);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CoreProtectIntegration.isEnabled()) {
|
||||||
|
this.getLogger().info("CoreProtect integration enabled");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean logLargeBlockBufferChanges() {
|
public boolean logLargeBlockBufferChanges() {
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
package com.moulberry.axiom.integration.coreprotect;
|
||||||
|
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import org.bukkit.craftbukkit.CraftWorld;
|
||||||
|
|
||||||
|
public class CoreProtectIntegration {
|
||||||
|
public static boolean isEnabled() {
|
||||||
|
return CoreProtectIntegrationImpl.isEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean logPlacement(String name, BlockState blockState, CraftWorld world, BlockPos pos) {
|
||||||
|
if (!CoreProtectIntegrationImpl.isEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CoreProtectIntegrationImpl.logPlacement(name, blockState, world, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean logPlacement(String name, BlockState blockState, CraftWorld world, int x, int y, int z) {
|
||||||
|
if (!CoreProtectIntegrationImpl.isEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CoreProtectIntegrationImpl.logPlacement(name, blockState, world, x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean logPlacement(String name, Level level, CraftWorld world, BlockPos pos) {
|
||||||
|
if (!CoreProtectIntegrationImpl.isEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CoreProtectIntegrationImpl.logPlacement(name, level, world, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean logRemoval(String name, BlockState blockState, CraftWorld world, BlockPos pos) {
|
||||||
|
if (!CoreProtectIntegrationImpl.isEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CoreProtectIntegrationImpl.logRemoval(name, blockState, world, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean logRemoval(String name, BlockState blockState, CraftWorld world, int x, int y, int z) {
|
||||||
|
if (!CoreProtectIntegrationImpl.isEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CoreProtectIntegrationImpl.logRemoval(name, blockState, world, x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean logRemoval(String name, Level level, CraftWorld world, BlockPos pos) {
|
||||||
|
if (!CoreProtectIntegrationImpl.isEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CoreProtectIntegrationImpl.logRemoval(name, level, world, pos);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
package com.moulberry.axiom.integration.coreprotect;
|
||||||
|
|
||||||
|
import com.moulberry.axiom.AxiomPaper;
|
||||||
|
import net.coreprotect.CoreProtect;
|
||||||
|
import net.coreprotect.CoreProtectAPI;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.craftbukkit.CraftWorld;
|
||||||
|
import org.bukkit.craftbukkit.block.CraftBlockState;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
|
||||||
|
public class CoreProtectIntegrationImpl {
|
||||||
|
private static final CoreProtectAPI COREPROTECT_API;
|
||||||
|
private static final boolean COREPROTECT_ENABLED;
|
||||||
|
private static final Constructor<CraftBlockState> CRAFT_BLOCK_STATE_CONSTRUCTOR;
|
||||||
|
|
||||||
|
static {
|
||||||
|
COREPROTECT_API = getCoreProtect();
|
||||||
|
Constructor<CraftBlockState> constructor = null;
|
||||||
|
|
||||||
|
if (COREPROTECT_API != null) {
|
||||||
|
try {
|
||||||
|
constructor = CraftBlockState.class.getDeclaredConstructor(World.class, BlockPos.class, BlockState.class);
|
||||||
|
constructor.setAccessible(true);
|
||||||
|
} catch (NoSuchMethodException | SecurityException e) {
|
||||||
|
AxiomPaper.PLUGIN.getLogger().warning("Failed to get CraftBlockState constructor for CoreProtect: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CRAFT_BLOCK_STATE_CONSTRUCTOR = constructor;
|
||||||
|
COREPROTECT_ENABLED = COREPROTECT_API != null && CRAFT_BLOCK_STATE_CONSTRUCTOR != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CoreProtectAPI getCoreProtect() {
|
||||||
|
Plugin plugin = Bukkit.getPluginManager().getPlugin("CoreProtect");
|
||||||
|
|
||||||
|
if (plugin == null || !(plugin instanceof CoreProtect)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreProtectAPI coreProtect = ((CoreProtect) plugin).getAPI();
|
||||||
|
if (coreProtect.isEnabled() == false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coreProtect.APIVersion() < 10) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return coreProtect;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CraftBlockState createCraftBlockState(World world, BlockPos pos, BlockState blockState) {
|
||||||
|
try {
|
||||||
|
return (CraftBlockState) CRAFT_BLOCK_STATE_CONSTRUCTOR.newInstance(world, pos, blockState);
|
||||||
|
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
||||||
|
AxiomPaper.PLUGIN.getLogger().warning("Failed to create CraftBlockState for CoreProtect: " + e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isEnabled() {
|
||||||
|
return COREPROTECT_ENABLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean logPlacement(String name, BlockState blockState, CraftWorld world, BlockPos pos) {
|
||||||
|
if (blockState.isAir()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return COREPROTECT_API.logPlacement(name, createCraftBlockState(world, pos, blockState));
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean logPlacement(String name, BlockState blockState, CraftWorld world, int x, int y, int z) {
|
||||||
|
return logPlacement(name, blockState, world, new BlockPos(x, y, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean logPlacement(String name, Level level, CraftWorld world, BlockPos pos) {
|
||||||
|
return logPlacement(name, level.getBlockState(pos), world, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean logRemoval(String name, BlockState blockState, CraftWorld world, BlockPos pos) {
|
||||||
|
return COREPROTECT_API.logRemoval(name, createCraftBlockState(world, pos, blockState));
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean logRemoval(String name, BlockState blockState, CraftWorld world, int x, int y, int z) {
|
||||||
|
return logRemoval(name, blockState, world, new BlockPos(x, y, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean logRemoval(String name, Level level, CraftWorld world, BlockPos pos) {
|
||||||
|
return logRemoval(name, level.getBlockState(pos), world, pos);
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ import com.moulberry.axiom.buffer.BlockBuffer;
|
|||||||
import com.moulberry.axiom.buffer.CompressedBlockEntity;
|
import com.moulberry.axiom.buffer.CompressedBlockEntity;
|
||||||
import com.moulberry.axiom.integration.Integration;
|
import com.moulberry.axiom.integration.Integration;
|
||||||
import com.moulberry.axiom.integration.SectionPermissionChecker;
|
import com.moulberry.axiom.integration.SectionPermissionChecker;
|
||||||
|
import com.moulberry.axiom.integration.coreprotect.CoreProtectIntegration;
|
||||||
import com.moulberry.axiom.viaversion.UnknownVersionHelper;
|
import com.moulberry.axiom.viaversion.UnknownVersionHelper;
|
||||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
||||||
import it.unimi.dsi.fastutil.shorts.Short2ObjectMap;
|
import it.unimi.dsi.fastutil.shorts.Short2ObjectMap;
|
||||||
@ -220,6 +221,9 @@ public class SetBlockBufferPacketListener {
|
|||||||
|
|
||||||
BlockState old = section.setBlockState(x, y, z, blockState, true);
|
BlockState old = section.setBlockState(x, y, z, blockState, true);
|
||||||
if (blockState != old) {
|
if (blockState != old) {
|
||||||
|
CoreProtectIntegration.logRemoval(player.getBukkitEntity().getName(), old, world.getWorld(), bx, by, bz);
|
||||||
|
CoreProtectIntegration.logPlacement(player.getBukkitEntity().getName(), blockState, world.getWorld(), bx, by, bz);
|
||||||
|
|
||||||
sectionChanged = true;
|
sectionChanged = true;
|
||||||
blockPos.set(bx, by, bz);
|
blockPos.set(bx, by, bz);
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package com.moulberry.axiom.packet;
|
|||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import com.moulberry.axiom.AxiomPaper;
|
import com.moulberry.axiom.AxiomPaper;
|
||||||
import com.moulberry.axiom.integration.Integration;
|
import com.moulberry.axiom.integration.Integration;
|
||||||
|
import com.moulberry.axiom.integration.coreprotect.CoreProtectIntegration;
|
||||||
import com.moulberry.axiom.integration.plotsquared.PlotSquaredIntegration;
|
import com.moulberry.axiom.integration.plotsquared.PlotSquaredIntegration;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
@ -158,8 +159,11 @@ public class SetBlockPacketListener implements PluginMessageListener {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CoreProtectIntegration.logRemoval(bukkitPlayer.getName(), player.level(), world, blockPos);
|
||||||
|
|
||||||
// Place block
|
// Place block
|
||||||
player.level().setBlock(blockPos, blockState, 3);
|
player.level().setBlock(blockPos, blockState, 3);
|
||||||
|
CoreProtectIntegration.logPlacement(bukkitPlayer.getName(), blockState, world, blockPos);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
@ -221,6 +225,9 @@ public class SetBlockPacketListener implements PluginMessageListener {
|
|||||||
|
|
||||||
BlockState old = section.setBlockState(x, y, z, blockState, true);
|
BlockState old = section.setBlockState(x, y, z, blockState, true);
|
||||||
if (blockState != old) {
|
if (blockState != old) {
|
||||||
|
CoreProtectIntegration.logRemoval(bukkitPlayer.getName(), old, world, blockPos);
|
||||||
|
CoreProtectIntegration.logPlacement(bukkitPlayer.getName(), blockState, world, blockPos);
|
||||||
|
|
||||||
Block block = blockState.getBlock();
|
Block block = blockState.getBlock();
|
||||||
motionBlocking.update(x, by, z, blockState);
|
motionBlocking.update(x, by, z, blockState);
|
||||||
motionBlockingNoLeaves.update(x, by, z, blockState);
|
motionBlockingNoLeaves.update(x, by, z, blockState);
|
||||||
|
@ -5,6 +5,7 @@ description: $description
|
|||||||
authors:
|
authors:
|
||||||
- Moulberry
|
- Moulberry
|
||||||
api-version: "$apiVersion"
|
api-version: "$apiVersion"
|
||||||
|
softdepend: ["CoreProtect"]
|
||||||
permissions:
|
permissions:
|
||||||
axiom.*:
|
axiom.*:
|
||||||
description: Allows use of all default Axiom features
|
description: Allows use of all default Axiom features
|
||||||
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren