1
mirror of https://github.com/Shiewk/SModeration.git synced 2026-04-29 06:34:17 +02:00

6 Commits

21 changed files with 376 additions and 70 deletions
@@ -10,11 +10,12 @@ import de.shiewk.smoderation.paper.punishments.Kick;
import de.shiewk.smoderation.paper.punishments.Mute;
import de.shiewk.smoderation.paper.punishments.PunishmentManager;
import de.shiewk.smoderation.paper.translation.TranslatorManager;
import de.shiewk.smoderation.paper.util.ColorPalette;
import de.shiewk.smoderation.paper.util.SModLegacy;
import de.shiewk.smoderation.paper.util.SchedulerUtil;
import io.papermc.paper.command.brigadier.Commands;
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
import net.kyori.adventure.text.minimessage.MiniMessage;
@@ -41,14 +42,11 @@ import static org.bukkit.Bukkit.getPluginManager;
@SuppressWarnings("UnstableApiUsage") // Paper Brigadier API
public final class SModerationPaper extends JavaPlugin {
public static final TextColor PRIMARY_COLOR = TextColor.color(212, 0, 255);
public static final TextColor SECONDARY_COLOR = TextColor.color(52, 143, 255);
public static final TextColor INACTIVE_COLOR = NamedTextColor.GRAY;
public static final Gson gson = new Gson();
public static ComponentLogger LOGGER = null;
public static SModerationPaper PLUGIN = null;
private static SkinTextureProvider textureProvider = null;
private ColorPalette colors;
private final TranslatorManager translatorManager = new TranslatorManager(
Key.key("smoderation", "translations"),
@@ -66,19 +64,50 @@ public final class SModerationPaper extends JavaPlugin {
return PLUGIN.getConfig();
}
public static ColorPalette colors() {
return PLUGIN.colors;
}
@Override
public void onLoad() {
LOGGER = getComponentLogger();
LOGGER.info("Folia: {}", SchedulerUtil.isFolia ? "yes" : "no");
PLUGIN = this;
LOGGER.info("Loading translations");
translatorManager.load();
if (config().getBoolean("custom-messages", false)) {
translatorManager.loadCustomMessages(getDataPath().resolve("messages.json"));
} else {
translatorManager.load();
}
updateConfig();
this.punishmentManager = new PunishmentManager(getDataPath().resolve("punishments.v2"));
this.punishmentManager.registerType("mute", new Mute.Factory());
this.punishmentManager.registerType("ban", new Ban.Factory());
this.punishmentManager.registerType("kick", new Kick.Factory());
this.colors = new ColorPalette(
parseColor(config().getString("colors.primary")),
parseColor(config().getString("colors.secondary")),
parseColor(config().getString("colors.detail"))
);
SModLegacy.migrateV1PunishmentsFile(
this.punishmentManager,
getDataPath().resolve("container.gz"),
getDataPath().resolve("v1-backup.gz")
);
}
private TextColor parseColor(String string) {
if (string == null || string.length() != 7) {
throw new IllegalArgumentException("Color not formatted correctly: " + string);
}
TextColor color = TextColor.fromHexString(string);
if (color == null) {
throw new IllegalArgumentException("Color not formatted correctly: " + string);
}
return color;
}
public boolean isFeatureEnabled(String feature){
@@ -88,6 +117,7 @@ public final class SModerationPaper extends JavaPlugin {
@Override
public void onEnable() {
if (isFeatureEnabled("punishments")) listen(new PunishmentListener(punishmentManager));
if (isFeatureEnabled("punishments")) listen(new CacheListener(punishmentManager));
if (isFeatureEnabled("invsee")) listen(new InvSeeListener());
if (isFeatureEnabled("enderchestsee")) listen(new EnderchestSeeListener());
if (isFeatureEnabled("socialspy")) listen(new SocialSpyListener());
@@ -162,8 +192,9 @@ public final class SModerationPaper extends JavaPlugin {
private MiniMessage createMiniMessage() {
return MiniMessage.builder()
.tags(TagResolver.builder()
.resolver(TagResolver.resolver("primary", Tag.styling(style -> style.color(PRIMARY_COLOR))))
.resolver(TagResolver.resolver("secondary", Tag.styling(style -> style.color(SECONDARY_COLOR))))
.resolver(TagResolver.resolver("primary", Tag.styling(style -> style.color(colors().primary()))))
.resolver(TagResolver.resolver("secondary", Tag.styling(style -> style.color(colors().secondary()))))
.resolver(TagResolver.resolver("detail", Tag.styling(style -> style.color(colors().detail()))))
.resolver(TagResolver.standard())
.build()
)
@@ -54,7 +54,7 @@ public final class BanCommand implements CommandProvider {
UUID sender = CommandUtil.getSenderUUID(context.getSource());
UUID target = context.getArgument("player", UUID.class);
long duration = context.getArgument("duration", Long.class);
executeBan(punishmentManager, sender, target, duration, Punishment.DEFAULT_REASON);
executeBan(punishmentManager, sender, target, duration, SModerationPaper.config().getString("default-reason", "No reason provided."));
return Command.SINGLE_SUCCESS;
}
@@ -57,7 +57,7 @@ public final class KickCommand implements CommandProvider {
}
UUID sender = CommandUtil.getSenderUUID(context.getSource());
Player target = CommandUtil.getPlayerSingle(context, "player");
executeKick(punishmentManager, sender, target, Punishment.DEFAULT_REASON);
executeKick(punishmentManager, sender, target, SModerationPaper.config().getString("default-reason", "No reason provided."));
return Command.SINGLE_SUCCESS;
}
@@ -49,11 +49,15 @@ public final class ModLogsCommand implements CommandProvider {
List<Punishment> punishments = punishmentManager.byTargetUUID(uuid);
for (Punishment punishment : punishments) {
if (punishment instanceof TimedPunishment timed && timed.isActive()){
sender.sendMessage(translatable("smod.command.modlogs." + punishment.getType(),
TimeUtil.calendarTimestamp(timed.getExpiry()),
TimeUtil.formatTimeLong(timed.getExpiry() - System.currentTimeMillis()),
text(punishment.getReason())
));
if (timed.isPermanent()){
sender.sendMessage(translatable("smod.command.modlogs." + punishment.getType() + ".permanent", text(punishment.getReason())));
} else {
sender.sendMessage(translatable("smod.command.modlogs." + punishment.getType(),
TimeUtil.calendarTimestamp(timed.getExpiry()),
TimeUtil.formatTimeLong(timed.getExpiry() - System.currentTimeMillis()),
text(punishment.getReason())
));
}
}
}
if (punishments.isEmpty()){
@@ -54,7 +54,7 @@ public final class MuteCommand implements CommandProvider {
UUID sender = CommandUtil.getSenderUUID(context.getSource());
UUID target = context.getArgument("player", UUID.class);
long duration = context.getArgument("duration", Long.class);
executeMute(punishmentManager, sender, target, duration, Punishment.DEFAULT_REASON);
executeMute(punishmentManager, sender, target, duration, SModerationPaper.config().getString("default-reason", "No reason provided."));
return Command.SINGLE_SUCCESS;
}
@@ -4,6 +4,7 @@ import com.mojang.brigadier.Command;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.tree.LiteralCommandNode;
import de.shiewk.smoderation.paper.SModerationPaper;
import de.shiewk.smoderation.paper.event.VanishToggleEvent;
import de.shiewk.smoderation.paper.punishments.Punishment;
import de.shiewk.smoderation.paper.util.CommandUtil;
@@ -150,11 +151,11 @@ public final class VanishCommand implements CommandProvider {
for (ObjectListIterator<Player> iterator = vanishedPlayers.iterator(); iterator.hasNext(); ) {
Player vanishedPlayer = iterator.next();
vanishList = vanishList.append(
vanishedPlayer.teamDisplayName().colorIfAbsent(SECONDARY_COLOR)
vanishedPlayer.teamDisplayName().colorIfAbsent(SModerationPaper.colors().secondary())
);
if (iterator.hasNext()){
vanishList = vanishList.append(
text().content(", ").color(PRIMARY_COLOR)
text().content(", ").color(SModerationPaper.colors().primary())
);
}
}
@@ -21,9 +21,13 @@ public final class DurationArgument implements CustomArgumentType.Converted<Long
public static final Pattern DURATION_PATTERN = Pattern.compile("([0-9]{1,9})(ms|s|min|h|d|w|mo|y)");
public static final Pattern VALIDATION_PATTERN = Pattern.compile("(([0-9]{1,9})(ms|s|min|h|d|w|mo|y))+");
public static final long INFINITE_DURATION = -1L;
@Override
public @NotNull Long convert(@NotNull String nativeType) throws CommandSyntaxException {
if (nativeType.startsWith("perm") || nativeType.startsWith("inf")) {
return INFINITE_DURATION;
}
if (!VALIDATION_PATTERN.matcher(nativeType).matches()){
CommandUtil.errorTranslatable("smod.argument.duration.fail.pattern");
}
@@ -58,6 +62,8 @@ public final class DurationArgument implements CustomArgumentType.Converted<Long
public @NotNull <S> CompletableFuture<Suggestions> listSuggestions(@NotNull CommandContext<S> context, @NotNull SuggestionsBuilder builder) {
if (builder.getRemaining().isBlank()){
List.of(
"infinite", "inf",
"permanent", "perm",
"100ms",
"15s",
"2min",
@@ -1,5 +1,6 @@
package de.shiewk.smoderation.paper.input;
import de.shiewk.smoderation.paper.SModerationPaper;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.title.Title;
import org.bukkit.entity.Player;
@@ -10,7 +11,6 @@ import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import static de.shiewk.smoderation.paper.SModerationPaper.PRIMARY_COLOR;
import static net.kyori.adventure.text.Component.text;
import static net.kyori.adventure.text.Component.translatable;
@@ -53,7 +53,7 @@ public class ChatInput {
public static void prompt(Player player, Consumer<Component> consumer, Component prompt, int timeSeconds){
runningInputs.put(player, new ChatInput(player, prompt, consumer, timeSeconds));
player.sendMessage(prompt.colorIfAbsent(PRIMARY_COLOR));
player.sendMessage(prompt.colorIfAbsent(SModerationPaper.colors().primary()));
}
public Component getPrompt() {
@@ -2,6 +2,7 @@ package de.shiewk.smoderation.paper.inventory;
import com.destroystokyo.paper.profile.PlayerProfile;
import com.destroystokyo.paper.profile.ProfileProperty;
import de.shiewk.smoderation.paper.SModerationPaper;
import de.shiewk.smoderation.paper.SkinTextureProvider;
import de.shiewk.smoderation.paper.input.ChatInput;
import de.shiewk.smoderation.paper.punishments.Punishment;
@@ -58,7 +59,7 @@ public class SModMenu extends PageableCustomInventory {
}
public enum Sort {
EXPIRY(translatable("smod.menu.sort.expiry"), Comparator.comparingLong(p -> p instanceof TimedPunishment timed ? p.getTimestamp() + timed.getDuration() : p.getTimestamp())),
EXPIRY(translatable("smod.menu.sort.expiry"), Comparator.comparingLong(p -> p instanceof TimedPunishment timed ? timed.getExpiry() : p.getTimestamp())),
TIME(translatable("smod.menu.sort.time"), Comparator.comparingLong(Punishment::getTimestamp)),
PLAYER_NAME(translatable("smod.menu.sort.playerName"), (p1, p2) -> String.CASE_INSENSITIVE_ORDER.compare(PlayerUtil.offlinePlayerName(p1.getTargetID()), PlayerUtil.offlinePlayerName(p2.getTargetID()))),
MODERATOR_NAME(translatable("smod.menu.sort.moderatorName"), (p1, p2) -> String.CASE_INSENSITIVE_ORDER.compare(PlayerUtil.offlinePlayerName(p1.getIssuerID()), PlayerUtil.offlinePlayerName(p2.getIssuerID())));
@@ -126,7 +127,7 @@ public class SModMenu extends PageableCustomInventory {
// chat event is async
SchedulerUtil.scheduleForEntity(PLUGIN, player, this::open);
}
}, translatable("smod.menu.search.query").color(SECONDARY_COLOR), 30);
}, translatable("smod.menu.search.query").color(SModerationPaper.colors().secondary()), 30);
}
@Override
@@ -199,13 +200,13 @@ public class SModMenu extends PageableCustomInventory {
private ItemStack createFilterItem(){
final Filter filter = getFilter();
final ItemStack stack = new ItemStack(Filter.ICON);
stack.setData(DataComponentTypes.ITEM_NAME, renderComponent(player, translatable("smod.menu.filter", filter.name).color(PRIMARY_COLOR)));
stack.setData(DataComponentTypes.ITEM_NAME, renderComponent(player, translatable("smod.menu.filter", filter.name).color(SModerationPaper.colors().primary())));
ItemLore.Builder loreBuilder = ItemLore.lore();
loreBuilder.addLine(empty());
for (Filter value : Filter.values()) {
final boolean selected = filter == value;
Component filterText = renderComponent(player, applyFormatting(text((selected ? "\u00BB " : ""), selected ? SECONDARY_COLOR : INACTIVE_COLOR).append(value.name)));
Component filterText = renderComponent(player, applyFormatting(text((selected ? "\u00BB " : ""), selected ? SModerationPaper.colors().secondary() : SModerationPaper.colors().detail()).append(value.name)));
loreBuilder.addLine(filterText);
}
loreBuilder.addLine(empty());
@@ -218,13 +219,13 @@ public class SModMenu extends PageableCustomInventory {
private ItemStack createTypeItem(){
final String type = getType();
final ItemStack stack = new ItemStack(Material.CHEST);
stack.setData(DataComponentTypes.ITEM_NAME, renderComponent(player, translatable("smod.menu.type", (type == null ? translatable("smod.menu.type.all") : translatable("smod.punishment.name." + type)))).color(PRIMARY_COLOR));
stack.setData(DataComponentTypes.ITEM_NAME, renderComponent(player, translatable("smod.menu.type", (type == null ? translatable("smod.menu.type.all") : translatable("smod.punishment.name." + type)))).color(SModerationPaper.colors().primary()));
ItemLore.Builder loreBuilder = ItemLore.lore();
loreBuilder.addLine(empty());
final Consumer<String> addToLore = value -> {
final boolean selected = Objects.equals(type, value);
Component typeText = renderComponent(player, applyFormatting(text((selected ? "\u00BB " : ""), selected ? SECONDARY_COLOR : INACTIVE_COLOR).append(value == null ? translatable("smod.menu.type.all") : translatable("smod.punishment.name." + value))));
Component typeText = renderComponent(player, applyFormatting(text((selected ? "\u00BB " : ""), selected ? SModerationPaper.colors().secondary() : SModerationPaper.colors().detail()).append(value == null ? translatable("smod.menu.type.all") : translatable("smod.punishment.name." + value))));
loreBuilder.addLine(typeText);
};
addToLore.accept(null);
@@ -242,14 +243,14 @@ public class SModMenu extends PageableCustomInventory {
private ItemStack createSortItem(){
final Sort sort = getSort();
final ItemStack stack = new ItemStack(Sort.ICON);
stack.setData(DataComponentTypes.ITEM_NAME, renderComponent(player, translatable("smod.menu.sort", sort.name).color(PRIMARY_COLOR)));
stack.setData(DataComponentTypes.ITEM_NAME, renderComponent(player, translatable("smod.menu.sort", sort.name).color(SModerationPaper.colors().primary())));
ItemLore.Builder loreBuilder = ItemLore.lore();
loreBuilder.addLine(empty());
for (Sort value : Sort.values()) {
final boolean selected = sort == value;
Component sortText = renderComponent(player, applyFormatting(text((selected ? "\u00BB " : ""), selected ? SECONDARY_COLOR : INACTIVE_COLOR).append(value.name)));
Component sortText = renderComponent(player, applyFormatting(text((selected ? "\u00BB " : ""), selected ? SModerationPaper.colors().secondary() : SModerationPaper.colors().detail()).append(value.name)));
loreBuilder.addLine(sortText);
}
@@ -270,12 +271,12 @@ public class SModMenu extends PageableCustomInventory {
// we just create the stack without it instead of throwing
}
stack.setData(DataComponentTypes.ITEM_NAME, renderComponent(player, translatable("smod.menu.search", PRIMARY_COLOR)));
stack.setData(DataComponentTypes.ITEM_NAME, renderComponent(player, translatable("smod.menu.search", SModerationPaper.colors().primary())));
ItemLore.Builder loreBuilder = ItemLore.lore();
loreBuilder.addLines(List.of(
empty(),
renderComponent(player, applyFormatting(translatable("smod.menu.search.current", searchQuery == null ? translatable("smod.menu.search.none") : text('"' + searchQuery + '"'))).color(SECONDARY_COLOR)),
renderComponent(player, applyFormatting(translatable("smod.menu.search.current", searchQuery == null ? translatable("smod.menu.search.none") : text('"' + searchQuery + '"'))).color(SModerationPaper.colors().secondary())),
empty(),
renderComponent(player, applyFormatting(translatable("smod.menu.search.new", NamedTextColor.GOLD)))
));
@@ -330,12 +331,17 @@ public class SModMenu extends PageableCustomInventory {
lore.addLine(renderComponent(player, applyFormatting(translatable("smod.menu.info.timestamp", TimeUtil.calendarTimestamp(punishment.getTimestamp())))));
if (punishment instanceof TimedPunishment timed){
lore.addLine(renderComponent(player, applyFormatting(translatable("smod.menu.info.duration", TimeUtil.formatTimeLong(timed.getExpiry() - punishment.getTimestamp())))));
long remainingTime = timed.getExpiry() - System.currentTimeMillis();
if (remainingTime > 0){
lore.addLine(renderComponent(player, applyFormatting(translatable("smod.menu.info.expiry.future", TimeUtil.formatTimeLong(remainingTime)))));
if (timed.isPermanent()){
lore.addLine(renderComponent(player, applyFormatting(translatable("smod.menu.info.duration", translatable("smod.time.permanent")))));
lore.addLine(renderComponent(player, applyFormatting(translatable("smod.menu.info.expiry.never"))));
} else {
lore.addLine(renderComponent(player, applyFormatting(translatable("smod.menu.info.expiry.past", TimeUtil.formatTimeLong(-remainingTime)))));
lore.addLine(renderComponent(player, applyFormatting(translatable("smod.menu.info.duration", TimeUtil.formatTimeLong(timed.getExpiry() - punishment.getTimestamp())))));
long remainingTime = timed.getExpiry() - System.currentTimeMillis();
if (remainingTime > 0){
lore.addLine(renderComponent(player, applyFormatting(translatable("smod.menu.info.expiry.future", TimeUtil.formatTimeLong(remainingTime)))));
} else {
lore.addLine(renderComponent(player, applyFormatting(translatable("smod.menu.info.expiry.past", TimeUtil.formatTimeLong(-remainingTime)))));
}
}
}
@@ -0,0 +1,35 @@
package de.shiewk.smoderation.paper.listener;
import de.shiewk.smoderation.paper.punishments.PunishmentManager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import static de.shiewk.smoderation.paper.SModerationPaper.LOGGER;
import static net.kyori.adventure.text.Component.translatable;
public class CacheListener implements Listener {
private final PunishmentManager punishmentManager;
public CacheListener(PunishmentManager punishmentManager) {
this.punishmentManager = punishmentManager;
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event){
try {
punishmentManager.loadToCache(event.getPlayer().getUniqueId());
} catch (Exception e) {
LOGGER.error("Failed to load punishments", e);
event.getPlayer().kick(translatable("mco.errorMessage.connectionFailure"));
}
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event){
punishmentManager.removeFromCache(event.getPlayer().getUniqueId());
}
}
@@ -16,7 +16,6 @@ import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import java.util.List;
import static de.shiewk.smoderation.paper.SModerationPaper.PRIMARY_COLOR;
import static net.kyori.adventure.text.Component.translatable;
public class PunishmentListener implements Listener {
@@ -41,7 +40,7 @@ public class PunishmentListener implements Listener {
List<Punishment> list = punishmentManager.byTargetUUID(player.getUniqueId(), p -> p instanceof Mute mute && mute.isActive());
if (!list.isEmpty()) {
event.setCancelled(true);
player.sendMessage(list.getFirst().infoMessage().colorIfAbsent(PRIMARY_COLOR));
player.sendMessage(list.getFirst().infoMessage().colorIfAbsent(SModerationPaper.colors().primary()));
}
}
@@ -57,7 +56,7 @@ public class PunishmentListener implements Listener {
|| message.toLowerCase().startsWith(str.toLowerCase()+" ")
)){
Bukkit.getConsoleSender().sendMessage(player.getName() + " tried to run forbidden command while muted");
player.sendMessage(translatable("smod.punishment.playerMessage.mute.chat", PRIMARY_COLOR));
player.sendMessage(translatable("smod.punishment.playerMessage.mute.chat", SModerationPaper.colors().primary()));
event.setCancelled(true);
}
}
@@ -19,13 +19,12 @@ import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.persistence.PersistentDataType;
import static de.shiewk.smoderation.paper.SModerationPaper.SECONDARY_COLOR;
import static net.kyori.adventure.text.Component.text;
import static net.kyori.adventure.text.Component.translatable;
public class VanishListener implements Listener {
public static final Component PREFIX = text("[VANISH] ").color(SECONDARY_COLOR);
public static final Component PREFIX = text("[VANISH] ").color(SModerationPaper.colors().secondary());
@EventHandler(priority = EventPriority.HIGH) public void onPlayerQuit(PlayerQuitEvent event){
final Player player = event.getPlayer();
@@ -17,8 +17,6 @@ import static net.kyori.adventure.text.Component.translatable;
public abstract class Punishment {
public static final String DEFAULT_REASON = "No reason provided.";
protected final UUID id;
protected final String type;
protected final long timestamp;
@@ -22,6 +22,7 @@ import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import java.util.stream.Stream;
@@ -31,6 +32,7 @@ public final class PunishmentManager {
private static final Logger log = LoggerFactory.getLogger(PunishmentManager.class);
private final Object2ObjectArrayMap<String, PunishmentFactory<?>> typeRegistry = new Object2ObjectArrayMap<>(1);
private final ConcurrentHashMap<UUID, List<Punishment>> cache = new ConcurrentHashMap<>(1);
private final Object ioLock = new Object();
private final Path dataDir;
@@ -58,8 +60,13 @@ public final class PunishmentManager {
}
public List<Punishment> byTargetUUID(UUID target) {
List<Punishment> cached = cache.get(target);
if (cached != null) {
return cached;
}
synchronized (ioLock) {
Path file = getTargetFile(target);
if (!Files.exists(file)) {
return List.of();
}
@@ -115,7 +122,7 @@ public final class PunishmentManager {
return List.copyOf(typeRegistry.keySet());
}
private void appendToSave(Punishment punishment) throws IOException {
public void appendToSave(Punishment punishment) throws IOException {
synchronized (ioLock) {
Path file = getTargetFile(punishment.getTargetID());
if (!Files.exists(file)) {
@@ -129,6 +136,7 @@ public final class PunishmentManager {
writer.append('\n');
}
}
addToCachedList(punishment);
}
public @NotNull List<Punishment> getAll() throws IOException {
@@ -165,4 +173,21 @@ public final class PunishmentManager {
}
}
public void loadToCache(UUID uuid) {
removeFromCache(uuid);
cache.put(uuid, byTargetUUID(uuid));
}
public void removeFromCache(UUID uuid) {
cache.remove(uuid);
}
private void addToCachedList(Punishment punishment) {
cache.computeIfPresent(punishment.getTargetID(), (k, v) -> {
ObjectArrayList<Punishment> newList = new ObjectArrayList<>(v);
newList.add(punishment);
return List.copyOf(newList);
});
}
}
@@ -1,5 +1,6 @@
package de.shiewk.smoderation.paper.punishments;
import de.shiewk.smoderation.paper.command.argument.DurationArgument;
import de.shiewk.smoderation.paper.util.PlayerUtil;
import de.shiewk.smoderation.paper.util.SerializationHelper;
import de.shiewk.smoderation.paper.util.TimeUtil;
@@ -35,7 +36,7 @@ public abstract class TimedPunishment extends Punishment {
}
public boolean isActive(){
return !wasCancelled() && System.currentTimeMillis() < timestamp + duration;
return !wasCancelled() && (isPermanent() || System.currentTimeMillis() < getExpiry());
}
@Override
@@ -55,23 +56,40 @@ public abstract class TimedPunishment extends Punishment {
@Override
public Component infoMessage(){
return translatable(
"smod.punishment.playerMessage." + type,
text(PlayerUtil.offlinePlayerName(this.issuer)),
text(reason),
TimeUtil.formatTimeLong(this.timestamp + this.duration - System.currentTimeMillis())
);
if (isPermanent()){
return translatable(
"smod.punishment.playerMessage." + type + ".permanent",
text(PlayerUtil.offlinePlayerName(this.issuer)),
text(reason)
);
} else {
return translatable(
"smod.punishment.playerMessage." + type,
text(PlayerUtil.offlinePlayerName(this.issuer)),
text(reason),
TimeUtil.formatTimeLong(this.timestamp + this.duration - System.currentTimeMillis())
);
}
}
@Override
public Component adminMessage(){
return translatable(
"smod.punishment.broadcast." + type,
text(PlayerUtil.offlinePlayerName(target)),
text(PlayerUtil.offlinePlayerName(issuer)),
text(reason),
TimeUtil.formatTimeLong(this.duration)
);
if (isPermanent()){
return translatable(
"smod.punishment.broadcast." + type + ".permanent",
text(PlayerUtil.offlinePlayerName(target)),
text(PlayerUtil.offlinePlayerName(issuer)),
text(reason)
);
} else {
return translatable(
"smod.punishment.broadcast." + type,
text(PlayerUtil.offlinePlayerName(target)),
text(PlayerUtil.offlinePlayerName(issuer)),
text(reason),
TimeUtil.formatTimeLong(this.duration)
);
}
}
public Component cancelMessage(){
@@ -83,7 +101,11 @@ public abstract class TimedPunishment extends Punishment {
}
public long getExpiry() {
return getTimestamp() + getDuration();
return isPermanent() ? Long.MAX_VALUE : getTimestamp() + getDuration();
}
public boolean isPermanent() {
return getDuration() == DurationArgument.INFINITE_DURATION;
}
protected void cancel(UUID canceller) {
@@ -1,17 +1,20 @@
package de.shiewk.smoderation.paper.translation;
import com.google.gson.FormattingStyle;
import com.google.gson.JsonIOException;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonWriter;
import de.shiewk.smoderation.paper.SModerationPaper;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.translation.MiniMessageTranslationStore;
import net.kyori.adventure.translation.GlobalTranslator;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Locale;
import java.util.Map;
@@ -45,4 +48,54 @@ public class TranslatorManager {
}
GlobalTranslator.translator().addSource(translationStore);
}
public void loadCustomMessages(Path customPath) {
try {
if (Files.notExists(customPath)) {
Files.createDirectories(customPath.getParent());
Files.write(customPath, "{}".getBytes(), StandardOpenOption.CREATE);
}
Map<String, String> predefinedMap;
try (InputStream stream = SModerationPaper.class.getClassLoader().getResourceAsStream(resourcePath + "en_us.json")) {
if (stream == null) {
SModerationPaper.LOGGER.warn("English (US) predefined translations not found or not accessible");
predefinedMap = Map.of();
} else {
predefinedMap = SModerationPaper.gson.fromJson(new InputStreamReader(stream), new TypeToken<>(){});
}
}
Map<String, String> translationMap;
try (InputStream stream = Files.newInputStream(customPath)) {
translationMap = SModerationPaper.gson.fromJson(new InputStreamReader(stream), new TypeToken<>(){});
}
boolean updated = false;
for (Map.Entry<String, String> entry : predefinedMap.entrySet()) {
if (!translationMap.containsKey(entry.getKey())) {
translationMap.put(entry.getKey(), entry.getValue());
updated = true;
}
}
if (updated) {
SModerationPaper.LOGGER.warn("Updating {} custom translations", translationMap.size());
try (OutputStream stream = Files.newOutputStream(customPath, StandardOpenOption.TRUNCATE_EXISTING);
OutputStreamWriter writer = new OutputStreamWriter(stream);
JsonWriter jsonWriter = new JsonWriter(writer)) {
jsonWriter.setFormattingStyle(FormattingStyle.PRETTY);
SModerationPaper.gson.toJson(translationMap, translationMap.getClass(), jsonWriter);
}
SModerationPaper.LOGGER.info("Update successful");
}
translationStore.registerAll(Locale.forLanguageTag("en-US"), translationMap);
SModerationPaper.LOGGER.info("Registered {} custom translations", translationMap.size());
} catch (IOException e) {
SModerationPaper.LOGGER.warn("Failed to load custom translations", e);
}
GlobalTranslator.translator().addSource(translationStore);
}
}
@@ -0,0 +1,7 @@
package de.shiewk.smoderation.paper.util;
import net.kyori.adventure.text.format.TextColor;
public record ColorPalette(TextColor primary, TextColor secondary, TextColor detail) {
}
@@ -1,7 +1,18 @@
package de.shiewk.smoderation.paper.util;
import de.shiewk.smoderation.paper.punishments.*;
import java.io.EOFException;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
import java.util.zip.GZIPInputStream;
import static de.shiewk.smoderation.paper.SModerationPaper.LOGGER;
public final class SModLegacy {
private SModLegacy() {}
@@ -54,4 +65,76 @@ public final class SModLegacy {
return buffer.array();
}
private static byte[] readStreamInternal(InputStream stream, int len) throws IOException {
final byte[] bytes = stream.readNBytes(len);
if (bytes.length != len){
throw new EOFException("Stream has ended before enough bytes were read");
}
return bytes;
}
public static void migrateV1PunishmentsFile(PunishmentManager manager, Path path, Path copy) {
int count = 0;
try {
if (Files.isRegularFile(path)) {
LOGGER.info("Migrating V1 punishment file: {}", path);
try (InputStream in = new FileInputStream(path.toFile());
GZIPInputStream gzin = new GZIPInputStream(in)){
while (gzin.available() > 0){
int type = bytesToInt(readStreamInternal(gzin, 4));
long time = bytesToLong(readStreamInternal(gzin, 8));
long until = bytesToLong(readStreamInternal(gzin, 8));
UUID by = bytesToUuid(readStreamInternal(gzin, 16));
UUID to = bytesToUuid(readStreamInternal(gzin, 16));
int reasonLen = bytesToInt(readStreamInternal(gzin, 4));
String reason = new String(readStreamInternal(gzin, reasonLen));
UUID canceller = null;
boolean cancelled = gzin.read() == 1;
if (cancelled){
canceller = bytesToUuid(readStreamInternal(gzin, 16));
}
// Type 0: mute; 1: kick; 2: ban
Punishment p = switch (type){
case 0 -> new Mute(
Punishment.generateUUID(),
time,
by,
to,
reason,
until - time,
canceller
);
case 1 -> new Kick(
Punishment.generateUUID(),
time,
by,
to,
reason
);
case 2 -> new Ban(
Punishment.generateUUID(),
time,
by,
to,
reason,
until - time,
canceller
);
default -> throw new IllegalArgumentException("Invalid legacy type for punishment: " + type);
};
count++;
manager.appendToSave(p);
LOGGER.info("Migrated: {}", p);
}
}
LOGGER.info("Successfully loaded {} items.", count);
Files.move(path, copy);
}
} catch (EOFException e) {
LOGGER.error("The file was not correctly saved, {} items could be recovered!", count);
} catch (IOException e){
LOGGER.error("An error occurred while loading", e);
throw new RuntimeException(e);
}
}
}
+20 -1
View File
@@ -1,11 +1,15 @@
# If enabled, punishments can no longer be issued without providing a reason.
force-reason: false
# The string that should be used when no reason is provided.
# This has no effect if 'force-reason' is set to true.
default-reason: No reason provided.
# Allows you to toggle specific plugin features.
features:
# Should punishments be tracked and executed?
# Note that disabling this will also disable the SMod menu and
# /modlogs command since their only use is viewing punishments
# /modlogs command since their only use is viewing punishments.
punishments: true
# Should commands for opening the SMod menu be registered?
smodmenu: true
@@ -20,6 +24,21 @@ features:
# Should players be able to enable vanish mode?
vanish: true
# Allows you to customize plugin colors.
# Colors have to be formatted #rrggbb.
colors:
primary: '#e26c04'
secondary: '#fc9e07'
detail: '#ababab'
# Allows you to customize translation files.
# - At 'false', the plugin's predefined localization is used
# and available for multiple languages:
# https://github.com/Shiewk/SModeration/tree/main/src/main/resources/smoderation/translations
# - At 'true', a JSON localization file will be created in the plugin directory.
# Only the contained strings will be used and localization will be disabled.
custom-messages: false
# A list of commands which will be captured and broadcast
# to players which have SocialSpy enabled (/socialspy).
socialspy-commands:
@@ -3,7 +3,7 @@
"smod.argument.duration.fail.pattern": "Bitte gib eine gültige Zeit an, z.B. '1d6h30min'",
"smod.argument.offlinePlayer.fail.notCached": "Die Daten des Spielers sind nicht gespeichert.",
"smod.argument.uuid.fail.notCached": "Die Daten des Spielers sind nicht gespeichert. Versuche, stattdessen seine UUID anzugeben.",
"smod.chatInput.remainingTime": "<gray><arg:0> Sekunden",
"smod.chatInput.remainingTime": "<detail><arg:0> Sekunden",
"smod.command.ban.fail.alreadyBanned": "Dieser Spieler ist schon gebannt.",
"smod.command.ban.fail.forceReason": "Bitte gib einen Grund an.",
"smod.command.ban.fail.protect": "Dieser Spieler kann nicht gebannt werden.",
@@ -18,9 +18,11 @@
"smod.command.kick.fail.forceReason": "Bitte gib einen Grund an.",
"smod.command.kick.fail.protect": "Dieser Spieler kann nicht gekickt werden.",
"smod.command.kick.fail.self": "Du kannst dich nicht selbst kicken.",
"smod.command.modlogs.ban": "<primary>- ist bis <secondary><arg:0></secondary> <gray>(in <arg:1>)</gray> gebannt. Grund: <secondary><arg:2>",
"smod.command.modlogs.heading": "<primary>Spieler <secondary><arg:0> <gray>(<arg:1>)",
"smod.command.modlogs.mute": "<primary>- ist bis <secondary><arg:0></secondary> <gray>(in <arg:1>)</gray> stummgeschaltet. Grund: <secondary><arg:2>",
"smod.command.modlogs.ban": "<primary>- ist bis <secondary><arg:0></secondary> <detail>(in <arg:1>)</detail> gebannt. Grund: <secondary><arg:2>",
"smod.command.modlogs.ban.permanent": "<primary>- ist <secondary>permanent</secondary> gebannt. Grund: <secondary><arg:0>",
"smod.command.modlogs.heading": "<primary>Spieler <secondary><arg:0> <detail>(<arg:1>)",
"smod.command.modlogs.mute": "<primary>- ist bis <secondary><arg:0></secondary> <detail>(in <arg:1>)</detail> stummgeschaltet. Grund: <secondary><arg:2>",
"smod.command.modlogs.mute.permanent": "<primary>- ist <secondary>permanent</secondary> stummgeschaltet. Grund: <secondary><arg:0>",
"smod.command.modlogs.none": "<primary>- ist momentan nicht gebannt oder stummgeschaltet.",
"smod.command.mute.fail.alreadyMuted": "Dieser Spieler ist schon stummgeschaltet.",
"smod.command.mute.fail.forceReason": "Bitte gib einen Grund an.",
@@ -56,6 +58,7 @@
"smod.menu.info.click": "\u00BB Klicke, um die Strafe aufzuheben",
"smod.menu.info.duration": "<secondary>Dauer: <primary><arg:0>",
"smod.menu.info.expiry.future": "<secondary>Läuft ab: <primary>In <arg:0>",
"smod.menu.info.expiry.never": "<secondary>Läuft ab: <primary>Nie",
"smod.menu.info.expiry.past": "<secondary>Ist abgelaufen: <primary><arg:0> ago",
"smod.menu.info.player": "<secondary>Spieler: <primary><arg:0>",
"smod.menu.info.punishedBy": "<secondary>Bestraft von: <primary><arg:0>",
@@ -77,16 +80,20 @@
"smod.menu.type.all": "Alle",
"smod.menu.type.switch": "\u00BB Klicke, um den Typ zu ändern",
"smod.punishment.broadcast.ban": "<primary><secondary><arg:0></secondary> wurde von <secondary><arg:1></secondary> für <secondary><arg:3></secondary> gebannt.<newline>Grund: <secondary><arg:2>",
"smod.punishment.broadcast.ban.permanent": "<primary><secondary><arg:0></secondary> wurde von <secondary><arg:1></secondary> <secondary>permanent</secondary> gebannt.<newline>Grund: <secondary><arg:2>",
"smod.punishment.broadcast.kick": "<primary><secondary><arg:0></secondary> wurde von <secondary><arg:1></secondary> gekickt.<newline>Grund: <secondary><arg:2>",
"smod.punishment.broadcast.mute": "<primary><secondary><arg:0></secondary> wurde von <secondary><arg:1></secondary> für <secondary><arg:3></secondary> stummgeschaltet.<newline>Grund: <secondary><arg:2>",
"smod.punishment.broadcast.mute.permanent": "<primary><secondary><arg:0></secondary> wurde von <secondary><arg:1></secondary> <secondary>permanent</secondary> stummgeschaltet.<newline>Grund: <secondary><arg:2>",
"smod.punishment.cancel.ban": "<primary><secondary><arg:0></secondary> wurde von <secondary><arg:1></secondary> entbannt.",
"smod.punishment.cancel.mute": "<primary><secondary><arg:0></secondary>s Stummschaltung wurde von <secondary><arg:1></secondary> aufgehoben.",
"smod.punishment.name.ban": "Bann",
"smod.punishment.name.kick": "Kick",
"smod.punishment.name.mute": "Stummschaltung",
"smod.punishment.playerMessage.ban": "<primary>Du wurdest von <secondary><arg:0></secondary> vom Server gebannt.<newline>Grund: <secondary><arg:1></secondary><newline>Dein Bann läuft in <secondary><arg:2></secondary> ab.",
"smod.punishment.playerMessage.ban.permanent": "<primary>Du wurdest von <secondary><arg:0></secondary> vom Server gebannt.<newline>Grund: <secondary><arg:1></secondary><newline>Dein Bann läuft <secondary>nicht</secondary> ab.",
"smod.punishment.playerMessage.kick": "<primary>Du wurdest von <secondary><arg:0></secondary> vom Server gekickt.<newline>Grund: <secondary><arg:1>",
"smod.punishment.playerMessage.mute": "<primary>Du wurdest von <secondary><arg:0></secondary> stummgeschaltet.<newline>Grund: <secondary><arg:1></secondary><newline>Du kannst in <secondary><arg:2></secondary> wieder schreiben.",
"smod.punishment.playerMessage.mute.permanent": "<primary>Du wurdest von <secondary><arg:0></secondary> stummgeschaltet.<newline>Grund: <secondary><arg:1></secondary><newline>Du kannst <secondary>nie</secondary> wieder schreiben.",
"smod.punishment.playerMessage.mute.chat": "<primary>Du kannst diesen Befehl nicht ausführen, während du stummgeschaltet bist.",
"smod.socialspy.command": "<primary>[<secondary>SocialSpy</secondary>] <arg:0>: <secondary><arg:1>",
"smod.time.days": "<arg:0> Tage",
@@ -106,6 +113,8 @@
"smod.time.month.8": "September",
"smod.time.month.9": "Oktober",
"smod.time.months": "<arg:0> Monate",
"smod.time.never": "Nie",
"smod.time.permanent": "Permanent",
"smod.time.seconds": "<arg:0> Sekunden",
"smod.time.timestamp": "<arg:2>. <arg:1> <arg:0> <arg:3>:<arg:4>:<arg:5> <arg:6>",
"smod.time.weeks": "<arg:0> Wochen",
@@ -3,7 +3,7 @@
"smod.argument.duration.fail.pattern": "Please provide a valid duration, e.g. '1d6h30min'",
"smod.argument.offlinePlayer.fail.notCached": "That player's data is not saved.",
"smod.argument.uuid.fail.notCached": "That player's data is not saved. Try providing an UUID instead.",
"smod.chatInput.remainingTime": "<gray><arg:0> seconds",
"smod.chatInput.remainingTime": "<detail><arg:0> seconds",
"smod.command.ban.fail.alreadyBanned": "This player is already banned.",
"smod.command.ban.fail.forceReason": "Please provide a reason.",
"smod.command.ban.fail.protect": "This player can't be banned.",
@@ -18,9 +18,11 @@
"smod.command.kick.fail.forceReason": "Please provide a reason.",
"smod.command.kick.fail.protect": "This player can't be kicked.",
"smod.command.kick.fail.self": "You can't kick yourself.",
"smod.command.modlogs.ban": "<primary>- is banned until <secondary><arg:0></secondary> <gray>(in <arg:1>)</gray>. Reason: <secondary><arg:2>",
"smod.command.modlogs.heading": "<primary>Player <secondary><arg:0> <gray>(<arg:1>)",
"smod.command.modlogs.mute": "<primary>- is muted until <secondary><arg:0></secondary> <gray>(in <arg:1>)</gray>. Reason: <secondary><arg:2>",
"smod.command.modlogs.ban": "<primary>- is banned until <secondary><arg:0></secondary> <detail>(in <arg:1>)</detail>. Reason: <secondary><arg:2>",
"smod.command.modlogs.ban.permanent": "<primary>- is banned <secondary>permanently</secondary>. Reason: <secondary><arg:0>",
"smod.command.modlogs.heading": "<primary>Player <secondary><arg:0> <detail>(<arg:1>)",
"smod.command.modlogs.mute": "<primary>- is muted until <secondary><arg:0></secondary> <detail>(in <arg:1>)</detail>. Reason: <secondary><arg:2>",
"smod.command.modlogs.mute.permanent": "<primary>- is muted <secondary>permanently</secondary>. Reason: <secondary><arg:0>",
"smod.command.modlogs.none": "<primary>- is not currently muted or banned.",
"smod.command.mute.fail.alreadyMuted": "This player is already muted.",
"smod.command.mute.fail.forceReason": "Please provide a reason.",
@@ -56,6 +58,7 @@
"smod.menu.info.click": "\u00BB Click to cancel punishment",
"smod.menu.info.duration": "<secondary>Duration: <primary><arg:0>",
"smod.menu.info.expiry.future": "<secondary>Expires: <primary>In <arg:0>",
"smod.menu.info.expiry.never": "<secondary>Expires: <primary>Never",
"smod.menu.info.expiry.past": "<secondary>Expired: <primary><arg:0> ago",
"smod.menu.info.player": "<secondary>Player: <primary><arg:0>",
"smod.menu.info.punishedBy": "<secondary>Punished by: <primary><arg:0>",
@@ -77,16 +80,20 @@
"smod.menu.type.all": "All",
"smod.menu.type.switch": "» Click to switch type",
"smod.punishment.broadcast.ban": "<primary><secondary><arg:0></secondary> was banned by <secondary><arg:1></secondary> for <secondary><arg:3></secondary>.<newline>Reason: <secondary><arg:2>",
"smod.punishment.broadcast.ban.permanent": "<primary><secondary><arg:0></secondary> was banned <secondary>permanently</secondary> by <secondary><arg:1></secondary>.<newline>Reason: <secondary><arg:2>",
"smod.punishment.broadcast.kick": "<primary><secondary><arg:0></secondary> was kicked by <secondary><arg:1></secondary>.<newline>Reason: <secondary><arg:2>",
"smod.punishment.broadcast.mute": "<primary><secondary><arg:0></secondary> was muted by <secondary><arg:1></secondary> for <secondary><arg:3></secondary>.<newline>Reason: <secondary><arg:2>",
"smod.punishment.broadcast.mute.permanent": "<primary><secondary><arg:0></secondary> was muted <secondary>permanently</secondary> by <secondary><arg:1></secondary>.<newline>Reason: <secondary><arg:2>",
"smod.punishment.cancel.ban": "<primary><secondary><arg:0></secondary> was unbanned by <secondary><arg:1></secondary>.",
"smod.punishment.cancel.mute": "<primary><secondary><arg:0></secondary> was unmuted by <secondary><arg:1></secondary>.",
"smod.punishment.name.ban": "Ban",
"smod.punishment.name.kick": "Kick",
"smod.punishment.name.mute": "Mute",
"smod.punishment.playerMessage.ban": "<primary>You have been banned from this server by <secondary><arg:0></secondary>.<newline>Reason: <secondary><arg:1></secondary><newline>Your ban expires in <secondary><arg:2></secondary>.",
"smod.punishment.playerMessage.ban.permanent": "<primary>You have been banned from this server by <secondary><arg:0></secondary>.<newline>Reason: <secondary><arg:1></secondary><newline>Your ban <secondary>does not</secondary> expire.",
"smod.punishment.playerMessage.kick": "<primary>You have been kicked by <secondary><arg:0></secondary>.<newline>Reason: <secondary><arg:1>",
"smod.punishment.playerMessage.mute": "<primary>You have been muted by <secondary><arg:0></secondary>.<newline>Reason: <secondary><arg:1></secondary><newline>Your mute expires in <secondary><arg:2></secondary>.",
"smod.punishment.playerMessage.mute.permanent": "<primary>You have been muted by <secondary><arg:0></secondary>.<newline>Reason: <secondary><arg:1></secondary><newline>Your mute <secondary>does not</secondary> expire.",
"smod.punishment.playerMessage.mute.chat": "<primary>You can't run this command while you are muted.",
"smod.socialspy.command": "<primary>[<secondary>SocialSpy</secondary>] <arg:0>: <secondary><arg:1>",
"smod.time.days": "<arg:0> days",
@@ -106,6 +113,8 @@
"smod.time.month.8": "September",
"smod.time.month.9": "October",
"smod.time.months": "<arg:0> months",
"smod.time.never": "Never",
"smod.time.permanent": "Permanent",
"smod.time.seconds": "<arg:0> seconds",
"smod.time.timestamp": "<arg:2> <arg:1> <arg:0> <arg:3>:<arg:4>:<arg:5> <arg:6>",
"smod.time.weeks": "<arg:0> weeks",