New database API

This commit is contained in:
2026-05-24 22:37:32 -04:00
parent 2b9f7c7de7
commit 3c72688e0e
53 changed files with 1398 additions and 265 deletions
+4 -2
View File
@@ -1,12 +1,14 @@
plugins {
java
`java-library`
`maven-publish`
}
dependencies {
api("com.j256.ormlite:ormlite-core:6.1")
api("com.google.code.gson:gson:2.13.2")
compileOnly("io.papermc.paper:paper-api:26.1.2.build.+")
compileOnly("org.apache.logging.log4j:log4j-api:2.26.0")
compileOnly("com.google.code.gson:gson:2.13.2")
compileOnly("org.jetbrains:annotations:26.1.0")
}
@@ -0,0 +1,78 @@
package dev.plex.api.player;
import com.google.gson.JsonElement;
import java.util.Optional;
/**
* Typed key-value JSON storage for a player's module data.
*/
public interface PlayerModuleData
{
/**
* Gets a raw JSON value.
*
* @param key data key
* @return stored JSON value, if present
*/
Optional<JsonElement> get(String key);
/**
* Gets and maps a JSON value to a Java type.
*
* @param key data key
* @param type target type
* @param <T> target type
* @return mapped value, if present
*/
<T> Optional<T> get(String key, Class<T> type);
/**
* Gets a string value.
*
* @param key data key
* @param fallback value returned when the key is absent or incompatible
* @return stored string or fallback
*/
String getString(String key, String fallback);
/**
* Gets a long value.
*
* @param key data key
* @param fallback value returned when the key is absent or incompatible
* @return stored long or fallback
*/
long getLong(String key, long fallback);
/**
* Gets a boolean value.
*
* @param key data key
* @param fallback value returned when the key is absent or incompatible
* @return stored boolean or fallback
*/
boolean getBoolean(String key, boolean fallback);
/**
* Stores a raw JSON value.
*
* @param key data key
* @param value JSON value to store
*/
void set(String key, JsonElement value);
/**
* Stores a Java value as JSON.
*
* @param key data key
* @param value value to serialize and store
*/
void set(String key, Object value);
/**
* Removes a stored value.
*
* @param key data key
*/
void remove(String key);
}
@@ -3,6 +3,7 @@ package dev.plex.api.player;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import dev.plex.module.PlexModule;
/**
* Looks up Plex players through read-only API views.
@@ -15,7 +16,7 @@ public interface PlayersApi
* @param uuid player UUID
* @return player view, if known
*/
Optional<? extends PlexPlayerView> byUuid(UUID uuid);
Optional<? extends PlexPlayerView> player(UUID uuid);
/**
* Looks up a player by name.
@@ -31,4 +32,13 @@ public interface PlayersApi
* @return names of currently online players
*/
List<String> onlineNames();
/**
* Returns module-scoped data storage for a player.
*
* @param module module requesting player data
* @param playerUuid player UUID
* @return module-scoped player data storage
*/
PlayerModuleData moduleData(PlexModule module, UUID playerUuid);
}
@@ -8,17 +8,18 @@ import java.util.UUID;
*
* @param punished UUID of the player being punished
* @param punisher UUID of the actor issuing the punishment
* @param punisherName display name of the actor issuing the punishment
* @param source source that issued the punishment
* @param punisherReference source-specific actor reference
* @param ip IP address associated with the punished player
* @param punishedUsername username of the punished player
* @param type punishment type to apply
* @param reason punishment reason
* @param customTime whether the punishment uses a custom duration
* @param active whether the punishment should start active
* @param endDate punishment end date, or {@code null} for punishments without an end date
*/
public record PunishmentRequest(UUID punished, UUID punisher, String punisherName, String ip,
String punishedUsername, PunishmentType type, String reason,
boolean customTime, boolean active, ZonedDateTime endDate)
public record PunishmentRequest(UUID punished, UUID punisher, PunishmentSource source,
String punisherReference, String ip, PunishmentType type,
String reason, boolean customTime, boolean active,
ZonedDateTime endDate)
{
}
@@ -0,0 +1,20 @@
package dev.plex.api.punishment;
/**
* Source that issued a punishment.
*/
public enum PunishmentSource
{
/**
* Punishment issued by an in-game player.
*/
PLAYER,
/**
* Punishment issued by the server console.
*/
CONSOLE,
/**
* Punishment issued by a web or external integration.
*/
WEB
}
@@ -23,11 +23,25 @@ public interface PunishmentView
UUID punisher();
/**
* Returns the display name of the actor who issued the punishment.
* Returns the source that issued the punishment.
*
* @return display name of the actor who issued the punishment
* @return punishment source
*/
String punisherName();
PunishmentSource source();
/**
* Returns the source-specific punisher reference.
*
* @return punisher reference, or {@code null} when not applicable
*/
String punisherReference();
/**
* Returns the resolved display name for the punishment actor.
*
* @return display name for the punishment actor
*/
String punisherDisplayName();
/**
* Returns the IP address associated with the punished player.
@@ -36,13 +50,6 @@ public interface PunishmentView
*/
String ip();
/**
* Returns the username of the punished player.
*
* @return username of the punished player
*/
String punishedUsername();
/**
* Returns the punishment type.
*
@@ -0,0 +1,27 @@
package dev.plex.api.storage;
import java.sql.SQLException;
import java.util.List;
/**
* Runs database migrations for a module storage namespace.
*/
public interface ModuleMigrations
{
/**
* Runs migrations from the default module migration resource root.
*
* @param versions migration versions to apply
* @throws SQLException if a migration cannot be applied
*/
void run(List<String> versions) throws SQLException;
/**
* Runs migrations from a custom module migration resource root.
*
* @param resourceRoot resource root containing dialect migration folders
* @param versions migration versions to apply
* @throws SQLException if a migration cannot be applied
*/
void run(String resourceRoot, List<String> versions) throws SQLException;
}
@@ -0,0 +1,22 @@
package dev.plex.api.storage;
import com.j256.ormlite.dao.Dao;
import java.sql.SQLException;
/**
* Creates ORMLite DAOs for module-scoped tables.
*/
public interface ModuleOrm
{
/**
* Creates or returns a cached DAO for a module-local table.
*
* @param entityClass ORMLite entity class
* @param localTableName module-local table name
* @param <T> entity type
* @param <ID> entity ID type
* @return ORMLite DAO using the module-prefixed physical table
* @throws SQLException if the DAO cannot be created
*/
<T, ID> Dao<T, ID> dao(Class<T> entityClass, String localTableName) throws SQLException;
}
@@ -0,0 +1,48 @@
package dev.plex.api.storage;
import java.sql.SQLException;
/**
* Module-scoped storage namespace.
*/
public interface ModuleStorage
{
/**
* Returns the validated module table prefix.
*
* @return module table prefix
*/
String prefix();
/**
* Resolves a local table name to the module's physical table name.
*
* @param localName module-local table name
* @return physical table name
*/
String table(String localName);
/**
* Returns module migration operations.
*
* @return module migration operations
*/
ModuleMigrations migrations();
/**
* Returns module ORMLite DAO operations.
*
* @return module ORMLite DAO operations
*/
ModuleOrm orm();
/**
* Runs work inside a storage transaction.
*
* @param callable work to run
* @param <T> callback result type
* @return callback result
* @throws SQLException if the transaction cannot complete
*/
<T> T transaction(SqlCallable<T> callable) throws SQLException;
}
@@ -0,0 +1,20 @@
package dev.plex.api.storage;
import java.sql.SQLException;
/**
* Callback used for SQL work that does not receive a connection directly.
*
* @param <T> callback result type
*/
@FunctionalInterface
public interface SqlCallable<T>
{
/**
* Runs SQL work.
*
* @return callback result
* @throws SQLException if the SQL work cannot complete
*/
T call() throws SQLException;
}
@@ -0,0 +1,37 @@
package dev.plex.api.storage;
/**
* SQL dialects supported by Plex storage.
*/
public enum SqlDialect
{
/**
* SQLite storage.
*/
SQLITE("sqlite"),
/**
* MariaDB or MySQL-compatible storage.
*/
MARIADB("mariadb"),
/**
* PostgreSQL storage.
*/
POSTGRES("postgres");
private final String migrationDirectory;
SqlDialect(String migrationDirectory)
{
this.migrationDirectory = migrationDirectory;
}
/**
* Returns the resource directory name used for dialect-specific migrations.
*
* @return migration resource directory name
*/
public String migrationDirectory()
{
return migrationDirectory;
}
}
@@ -2,6 +2,7 @@ package dev.plex.api.storage;
import java.sql.Connection;
import java.sql.SQLException;
import dev.plex.module.PlexModule;
/**
* Provides controlled access to Plex SQL storage.
@@ -18,6 +19,21 @@ public interface StorageApi
*/
<T> T withConnection(SqlFunction<T> function) throws SQLException;
/**
* Returns storage operations scoped to a module namespace.
*
* @param module module requesting storage
* @return module-scoped storage operations
*/
ModuleStorage forModule(PlexModule module);
/**
* Returns the configured SQL dialect.
*
* @return configured SQL dialect
*/
SqlDialect dialect();
/**
* SQL callback used by {@link #withConnection(SqlFunction)}.
*
@@ -45,11 +45,22 @@ public abstract class SimplePlexCommand implements PlexCommand
private PlexApi api;
private PlexModule module;
/**
* Creates a command using explicit command metadata.
*
* @param commandSpec command metadata
*/
protected SimplePlexCommand(CommandSpec commandSpec)
{
this.commandSpec = commandSpec;
}
/**
* Starts a command spec builder for the given command name.
*
* @param name primary command name
* @return command spec builder
*/
protected static CommandSpec.Builder command(String name)
{
return CommandSpec.builder(name);
@@ -82,6 +93,14 @@ public abstract class SimplePlexCommand implements PlexCommand
return command.build();
}
/**
* Configures the Brigadier command tree for this command.
*
* <p>The default tree accepts optional greedy string arguments and dispatches
* them to {@link #execute(CommandSender, Player, String[])}.</p>
*
* @param command root command literal builder
*/
protected void configureCommand(LiteralArgumentBuilder<CommandSourceStack> command)
{
command.executes(context -> dispatchCommand(context, new String[0]));
@@ -90,13 +109,36 @@ public abstract class SimplePlexCommand implements PlexCommand
.executes(context -> dispatchCommand(context, splitExecutionArgs(StringArgumentType.getString(context, "args")))));
}
/**
* Executes this command.
*
* @param sender command sender
* @param player player sender, or {@code null} when the sender is not a player
* @param args command arguments
* @return component to send to the sender, or {@code null} to send no response
*/
protected abstract Component execute(@NotNull CommandSender sender, @Nullable Player player, @NotNull String[] args);
/**
* Returns tab-completion suggestions for this command.
*
* @param sender command sender
* @param alias command alias used by the sender
* @param args current command arguments
* @return suggested completions
* @throws IllegalArgumentException when suggestions cannot be produced for the supplied arguments
*/
protected @NotNull List<String> suggestions(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) throws IllegalArgumentException
{
return List.of();
}
/**
* Returns the bound Plex API.
*
* @return bound Plex API
* @throws IllegalStateException when the command has not been bound to the API
*/
protected PlexApi api()
{
if (api == null)
@@ -106,26 +148,56 @@ public abstract class SimplePlexCommand implements PlexCommand
return api;
}
/**
* Sends an ampersand-colorized legacy message to an audience.
*
* @param audience message recipient
* @param message legacy message text
*/
protected void send(Audience audience, String message)
{
audience.sendMessage(componentFromString(message));
}
/**
* Sends a component to an audience.
*
* @param audience message recipient
* @param component component to send
*/
protected void send(Audience audience, Component component)
{
audience.sendMessage(component);
}
/**
* Broadcasts a MiniMessage-formatted message.
*
* @param miniMessage MiniMessage-formatted message
*/
protected void broadcast(String miniMessage)
{
api().messages().broadcast(miniMessage);
}
/**
* Broadcasts a component.
*
* @param component component to broadcast
*/
protected void broadcast(Component component)
{
api().messages().broadcast(component);
}
/**
* Checks whether a sender has a permission node.
*
* @param sender command sender
* @param permission permission node to check
* @return {@code true} when the sender can use the permission
* @throws CommandFailException when the sender lacks the permission
*/
protected boolean checkPermission(CommandSender sender, String permission)
{
if (permission.isEmpty() || isConsole(sender) || sender.hasPermission(permission))
@@ -135,31 +207,68 @@ public abstract class SimplePlexCommand implements PlexCommand
throw new CommandFailException(messageString("noPermissionNode", permission));
}
/**
* Checks whether a sender has a permission node without throwing an exception.
*
* @param sender command sender
* @param permission permission node to check
* @return {@code true} when the sender can use the permission
*/
protected boolean silentCheckPermission(CommandSender sender, String permission)
{
return permission.isEmpty() || isConsole(sender) || sender.hasPermission(permission);
}
/**
* Returns the standard no-permission message for this command's permission node.
*
* @return no-permission component
*/
protected Component permissionMessage()
{
return permissionMessage(getPermission());
}
/**
* Returns the standard no-permission message for a permission node.
*
* @param permission permission node
* @return no-permission component
*/
protected Component permissionMessage(String permission)
{
return messageComponent("noPermissionNode", permission);
}
/**
* Returns a sender's UUID when the sender is a player.
*
* @param sender command sender
* @return player UUID, or {@code null} for non-player senders
*/
protected @Nullable UUID getUUID(CommandSender sender)
{
return sender instanceof Player player ? player.getUniqueId() : null;
}
/**
* Returns whether a sender is not a player.
*
* @param sender command sender
* @return {@code true} when the sender is not a player
*/
protected boolean isConsole(CommandSender sender)
{
return !(sender instanceof Player);
}
/**
* Resolves a configured message as a component.
*
* @param key message key
* @param objects replacement values
* @return resolved message component
*/
protected Component messageComponent(String key, Object... objects)
{
if (module != null)
@@ -169,6 +278,13 @@ public abstract class SimplePlexCommand implements PlexCommand
return api().messages().messageComponent(key, objects);
}
/**
* Resolves a configured message as a component using component replacements.
*
* @param key message key
* @param objects replacement components
* @return resolved message component
*/
protected Component messageComponent(String key, Component... objects)
{
if (module != null)
@@ -178,6 +294,13 @@ public abstract class SimplePlexCommand implements PlexCommand
return api().messages().messageComponent(key, objects);
}
/**
* Resolves a configured message as plain text.
*
* @param key message key
* @param objects replacement values
* @return resolved message text
*/
protected String messageString(String key, Object... objects)
{
if (module != null)
@@ -187,16 +310,34 @@ public abstract class SimplePlexCommand implements PlexCommand
return api().messages().messageString(key, objects);
}
/**
* Returns this command's formatted usage component.
*
* @return formatted usage component
*/
protected Component usage()
{
return usage(getUsage());
}
/**
* Formats command usage text with the standard usage prefix.
*
* @param usage usage text
* @return formatted usage component
*/
protected Component usage(String usage)
{
return messageComponent("correctUsagePrefix").append(componentFromString(usage).color(NamedTextColor.GRAY));
}
/**
* Returns an online player by UUID string or name.
*
* @param name UUID string or player name
* @return matching online player
* @throws PlayerNotFoundException when no matching online player exists
*/
protected Player getNonNullPlayer(String name)
{
try
@@ -220,21 +361,44 @@ public abstract class SimplePlexCommand implements PlexCommand
return player;
}
/**
* Returns the names of currently online players.
*
* @return online player names
*/
protected List<String> onlinePlayerNames()
{
return api().players().onlineNames();
}
/**
* Converts ampersand-colorized legacy text to a gray-default component.
*
* @param value legacy text
* @return deserialized component
*/
protected Component componentFromString(String value)
{
return LegacyComponentSerializer.legacyAmpersand().deserialize(value).colorIfAbsent(NamedTextColor.GRAY);
}
/**
* Converts ampersand-colorized legacy text to a component without adding a default color.
*
* @param value legacy text
* @return deserialized component
*/
protected Component noColorComponentFromString(String value)
{
return LegacyComponentSerializer.legacyAmpersand().deserialize(value);
}
/**
* Converts MiniMessage-formatted text to a component.
*
* @param value MiniMessage-formatted text
* @return deserialized component
*/
protected Component mmString(String value)
{
return api().messages().miniMessage(value);
+7
View File
@@ -13,6 +13,7 @@ import dev.plex.hook.CoreProtectHook;
import dev.plex.hook.PrismHook;
import dev.plex.hook.RollbackManager;
import dev.plex.module.ModuleManager;
import dev.plex.player.PlayerNameResolver;
import dev.plex.player.PlayerService;
import dev.plex.player.PlexPlayer;
import dev.plex.punishment.PunishmentManager;
@@ -21,6 +22,8 @@ import dev.plex.storage.RedisConnection;
import dev.plex.storage.SQLConnection;
import dev.plex.storage.StorageType;
import dev.plex.storage.player.SQLPlayerData;
import dev.plex.storage.player.PlayerModuleDataRepository;
import dev.plex.storage.player.SQLPlayerModuleData;
import dev.plex.storage.punishment.SQLNotes;
import dev.plex.storage.punishment.SQLPunishment;
import dev.plex.storage.repository.NoteRepository;
@@ -65,7 +68,9 @@ public class Plex extends JavaPlugin
private PlayerCache playerCache;
private PlayerRepository playerRepository;
private PlayerModuleDataRepository playerModuleDataRepository;
private PlayerService playerService;
private PlayerNameResolver playerNameResolver;
private PunishmentRepository punishmentRepository;
private NoteRepository noteRepository;
@@ -217,8 +222,10 @@ public class Plex extends JavaPlugin
punishmentRepository = new SQLPunishment(sqlConnection.getConnectionSource(), api.scheduler().asyncExecutor());
playerRepository = new SQLPlayerData(sqlConnection.getConnectionSource(), punishmentRepository);
playerModuleDataRepository = new SQLPlayerModuleData(sqlConnection, storageType);
noteRepository = new SQLNotes(sqlConnection.getConnectionSource(), api.scheduler().asyncExecutor());
playerService = new PlayerService(playerCache, playerRepository);
playerNameResolver = new PlayerNameResolver(playerService);
new ListenerHandler(this);
commandHandler = new CommandHandler(this);
@@ -0,0 +1,100 @@
package dev.plex.api.impl;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import dev.plex.api.player.PlayerModuleData;
import dev.plex.storage.player.PlayerModuleDataRepository;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.regex.Pattern;
public class DefaultPlayerModuleData implements PlayerModuleData
{
private static final Gson GSON = new Gson();
private static final Pattern KEY_PATTERN = Pattern.compile("^[a-z][a-z0-9_]{0,63}$");
private final PlayerModuleDataRepository repository;
private final String modulePrefix;
private final UUID playerUuid;
public DefaultPlayerModuleData(PlayerModuleDataRepository repository, String modulePrefix, UUID playerUuid)
{
this.repository = repository;
this.modulePrefix = modulePrefix;
this.playerUuid = playerUuid;
}
@Override
public Optional<JsonElement> get(String key)
{
return repository.get(playerUuid, modulePrefix, validateKey(key));
}
@Override
public <T> Optional<T> get(String key, Class<T> type)
{
return get(key).map(element -> GSON.fromJson(element, type));
}
@Override
public String getString(String key, String fallback)
{
return get(key)
.filter(JsonElement::isJsonPrimitive)
.map(JsonElement::getAsJsonPrimitive)
.filter(primitive -> primitive.isString())
.map(primitive -> primitive.getAsString())
.orElse(fallback);
}
@Override
public long getLong(String key, long fallback)
{
return get(key)
.filter(JsonElement::isJsonPrimitive)
.map(JsonElement::getAsJsonPrimitive)
.filter(primitive -> primitive.isNumber())
.map(primitive -> primitive.getAsLong())
.orElse(fallback);
}
@Override
public boolean getBoolean(String key, boolean fallback)
{
return get(key)
.filter(JsonElement::isJsonPrimitive)
.map(JsonElement::getAsJsonPrimitive)
.filter(primitive -> primitive.isBoolean())
.map(primitive -> primitive.getAsBoolean())
.orElse(fallback);
}
@Override
public void set(String key, JsonElement value)
{
repository.set(playerUuid, modulePrefix, validateKey(key), Objects.requireNonNull(value, "value"));
}
@Override
public void set(String key, Object value)
{
set(key, GSON.toJsonTree(value));
}
@Override
public void remove(String key)
{
repository.remove(playerUuid, modulePrefix, validateKey(key));
}
private String validateKey(String key)
{
if (key == null || !KEY_PATTERN.matcher(key).matches())
{
throw new IllegalArgumentException("Invalid player module data key: " + key);
}
return key;
}
}
@@ -1,9 +1,12 @@
package dev.plex.api.impl;
import dev.plex.Plex;
import dev.plex.api.player.PlayerModuleData;
import dev.plex.api.player.PlayersApi;
import dev.plex.api.player.PlexPlayerView;
import dev.plex.module.PlexModule;
import dev.plex.player.PlexPlayer;
import dev.plex.storage.module.ModuleNames;
import dev.plex.util.PlexUtils;
import java.util.List;
import java.util.Optional;
@@ -15,9 +18,10 @@ final class DefaultPlayersApi implements PlayersApi
DefaultPlayersApi(Plex plugin) { this.plugin = plugin; }
@Override public Optional<? extends PlexPlayerView> byUuid(UUID uuid) { return Optional.ofNullable(plugin.getPlayerService().getPlayer(uuid)).map(DefaultPlexPlayerView::new); }
@Override public Optional<? extends PlexPlayerView> byName(String name) { return Optional.ofNullable(plugin.getPlayerService().getPlayer(name)).map(DefaultPlexPlayerView::new); }
@Override public Optional<? extends PlexPlayerView> player(UUID uuid) { return Optional.ofNullable(plugin.getPlayerService().getPlayer(uuid)).map(player -> new DefaultPlexPlayerView(player, plugin.getPlayerNameResolver())); }
@Override public Optional<? extends PlexPlayerView> byName(String name) { return Optional.ofNullable(plugin.getPlayerService().getPlayer(name)).map(player -> new DefaultPlexPlayerView(player, plugin.getPlayerNameResolver())); }
@Override public List<String> onlineNames() { return PlexUtils.getPlayerNameList(); }
@Override public PlayerModuleData moduleData(PlexModule module, UUID playerUuid) { return new DefaultPlayerModuleData(plugin.getPlayerModuleDataRepository(), ModuleNames.prefix(module), playerUuid); }
static PlexPlayer unwrap(PlexPlayerView view)
{
@@ -2,17 +2,18 @@ package dev.plex.api.impl;
import dev.plex.api.player.PlexPlayerView;
import dev.plex.api.punishment.PunishmentView;
import dev.plex.player.PlayerNameResolver;
import dev.plex.player.PlexPlayer;
import java.util.List;
import java.util.UUID;
import org.bukkit.entity.Player;
record DefaultPlexPlayerView(PlexPlayer player) implements PlexPlayerView
record DefaultPlexPlayerView(PlexPlayer player, PlayerNameResolver playerNameResolver) implements PlexPlayerView
{
@Override public UUID uuid() { return player.getUuid(); }
@Override public String name() { return player.getName(); }
@Override public List<String> ips() { return List.copyOf(player.getIps()); }
@Override public List<? extends PunishmentView> punishments() { return player.getPunishments().stream().map(DefaultPunishmentView::new).toList(); }
@Override public List<? extends PunishmentView> punishments() { return player.getPunishments().stream().map(punishment -> new DefaultPunishmentView(punishment, playerNameResolver)).toList(); }
@Override public boolean frozen() { return player.isFrozen(); }
@Override public boolean muted() { return player.isMuted(); }
@Override public boolean lockedUp() { return player.isLockedUp(); }
@@ -1,18 +1,21 @@
package dev.plex.api.impl;
import dev.plex.api.punishment.PunishmentSource;
import dev.plex.api.punishment.PunishmentType;
import dev.plex.api.punishment.PunishmentView;
import dev.plex.player.PlayerNameResolver;
import dev.plex.punishment.Punishment;
import java.time.ZonedDateTime;
import java.util.UUID;
record DefaultPunishmentView(Punishment punishment) implements PunishmentView
record DefaultPunishmentView(Punishment punishment, PlayerNameResolver playerNameResolver) implements PunishmentView
{
@Override public UUID punished() { return punishment.getPunished(); }
@Override public UUID punisher() { return punishment.getPunisher(); }
@Override public String punisherName() { return punishment.getPunisherName(); }
@Override public PunishmentSource source() { return punishment.getSource() == null ? (punishment.getPunisher() == null ? PunishmentSource.CONSOLE : PunishmentSource.PLAYER) : punishment.getSource(); }
@Override public String punisherReference() { return punishment.getPunisherReference(); }
@Override public String punisherDisplayName() { return Punishment.punisherDisplayName(punishment, playerNameResolver); }
@Override public String ip() { return punishment.getIp(); }
@Override public String punishedUsername() { return punishment.getPunishedUsername(); }
@Override public PunishmentType type() { return PunishmentType.valueOf(punishment.getType().name()); }
@Override public String reason() { return punishment.getReason(); }
@Override public boolean customTime() { return punishment.isCustomTime(); }
@@ -27,9 +27,9 @@ final class DefaultPunishmentsApi implements PunishmentsApi
PlexPlayer player = DefaultPlayersApi.unwrap(playerView);
if (player == null) player = plugin.getPlayerService().getPlayer(playerView.uuid());
Punishment punishment = new Punishment(request.punished(), request.punisher());
punishment.setPunisherName(request.punisherName());
punishment.setSource(request.source());
punishment.setPunisherReference(request.punisherReference());
punishment.setIp(request.ip());
punishment.setPunishedUsername(request.punishedUsername());
punishment.setType(PunishmentType.valueOf(request.type().name()));
punishment.setReason(request.reason());
punishment.setCustomTime(request.customTime());
@@ -1,7 +1,11 @@
package dev.plex.api.impl;
import dev.plex.Plex;
import dev.plex.api.storage.ModuleStorage;
import dev.plex.api.storage.SqlDialect;
import dev.plex.api.storage.StorageApi;
import dev.plex.module.PlexModule;
import dev.plex.storage.module.ServerModuleStorage;
import java.sql.Connection;
import java.sql.SQLException;
@@ -19,4 +23,16 @@ final class DefaultStorageApi implements StorageApi
return function.apply(connection);
}
}
@Override
public ModuleStorage forModule(PlexModule module)
{
return new ServerModuleStorage(plugin, module);
}
@Override
public SqlDialect dialect()
{
return plugin.getStorageType().dialect();
}
}
@@ -97,7 +97,6 @@ public class BanCMD extends ServerCommand
{
punishment.setReason(context.messageString("noReasonProvided"));
}
punishment.setPunishedUsername(plexPlayer.getName());
ZonedDateTime date = ZonedDateTime.now(ZoneId.of(TimeUtils.TIMEZONE));
punishment.setEndDate(date.plusDays(1));
punishment.setCustomTime(false);
@@ -107,7 +106,7 @@ public class BanCMD extends ServerCommand
PlexUtils.broadcast(context.messageComponent("banningPlayer", sender.getName(), plexPlayer.getName()));
if (player != null)
{
plugin.getApi().scheduler().runEntity(player, () -> BungeeUtil.kickPlayer(plugin, player, Punishment.generateBanMessage(punishment, plugin.config.getString("banning.ban_url"), plugin.getPlayerService())));
plugin.getApi().scheduler().runEntity(player, () -> BungeeUtil.kickPlayer(plugin, player, Punishment.generateBanMessage(punishment, plugin.config.getString("banning.ban_url"), plugin.getPlayerNameResolver())));
}
PlexLog.debug("(From /ban command) PunishedPlayer UUID: " + plexPlayer.getUuid());
@@ -3,9 +3,6 @@ package dev.plex.command.impl;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import dev.plex.command.ServerCommand;
import dev.plex.command.ServerCommandContext;
import dev.plex.punishment.Punishment;
import java.util.stream.Collectors;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import net.kyori.adventure.text.Component;
@@ -44,7 +41,7 @@ public class BanListCommand extends ServerCommand
{
plugin.getPunishmentManager().getActiveBans().whenComplete((punishments, throwable) ->
{
context.send(sender, context.messageComponent("activeBansList", punishments.size(), StringUtils.join(punishments.stream().map(Punishment::getPunishedUsername).collect(Collectors.toList()), ", ")));
context.send(sender, context.messageComponent("activeBansList", punishments.size(), StringUtils.join(punishments.stream().map(punishment -> plugin.getPlayerNameResolver().resolve(punishment.getPunished())).toList(), ", ")));
});
return null;
}
@@ -60,7 +60,6 @@ public class FreezeCMD extends ServerCommand
ZonedDateTime date = ZonedDateTime.now(ZoneId.of(TimeUtils.TIMEZONE));
punishment.setEndDate(date.plusSeconds(plugin.config.getInt("punishments.freeze-timer", 300)));
punishment.setType(PunishmentType.FREEZE);
punishment.setPunishedUsername(player.getName());
punishment.setIp(player.getAddress().getAddress().getHostAddress().trim());
punishment.setReason("");
punishment.setActive(true);
@@ -76,14 +76,13 @@ public class KickCMD extends ServerCommand
}
punishment.setReason(reason);
punishment.setPunishedUsername(plexPlayer.getName());
punishment.setEndDate(ZonedDateTime.now(ZoneId.of(TimeUtils.TIMEZONE)));
punishment.setCustomTime(false);
punishment.setActive(false);
punishment.setIp(player.getAddress().getAddress().getHostAddress().trim());
plugin.getPunishmentManager().punish(plexPlayer, punishment);
PlexUtils.broadcast(context.messageComponent("kickedPlayer", sender.getName(), plexPlayer.getName()));
BungeeUtil.kickPlayer(plugin, player, Punishment.generateKickMessage(punishment, plugin.getPlayerService()));
BungeeUtil.kickPlayer(plugin, player, Punishment.generateKickMessage(punishment, plugin.getPlayerNameResolver()));
return null;
}
@@ -66,7 +66,6 @@ public class MuteCMD extends ServerCommand
ZonedDateTime date = ZonedDateTime.now(ZoneId.of(TimeUtils.TIMEZONE));
punishment.setEndDate(date.plusSeconds(plugin.config.getInt("punishments.mute-timer", 300)));
punishment.setType(PunishmentType.MUTE);
punishment.setPunishedUsername(player.getName());
punishment.setIp(player.getAddress().getAddress().getHostAddress().trim());
punishment.setReason("");
punishment.setActive(true);
@@ -150,7 +150,8 @@ public class NotesCMD extends ServerCommand
AtomicReference<Component> noteList = new AtomicReference<>(context.messageComponent("notesHeader", plexPlayer.getName()));
for (Note note : notes)
{
Component noteLine = context.messageComponent("notePrefix", note.getId(), plugin.getPlayerService().getPlayer(note.getWrittenBy()).getName(), TimeUtils.useTimezone(note.getTimestamp()));
String author = plugin.getPlayerNameResolver().resolve(note.getWrittenBy());
Component noteLine = context.messageComponent("notePrefix", note.getId(), author, TimeUtils.useTimezone(note.getTimestamp()));
noteLine = noteLine.append(context.messageComponent("noteLine", note.getNote()));
noteList.set(noteList.get().append(Component.newline()));
noteList.set(noteList.get().append(noteLine));
@@ -128,7 +128,6 @@ public class SmiteCMD extends ServerCommand
punishment.setCustomTime(false);
punishment.setEndDate(ZonedDateTime.now(ZoneId.of(TimeUtils.TIMEZONE)));
punishment.setType(PunishmentType.SMITE);
punishment.setPunishedUsername(player.getName());
punishment.setIp(player.getAddress().getAddress().getHostAddress().trim());
if (reason != null)
@@ -83,7 +83,6 @@ public class TempbanCMD extends ServerCommand
{
punishment.setReason(context.messageString("noReasonProvided"));
}
punishment.setPunishedUsername(target.getName());
punishment.setEndDate(TimeUtils.createDate(args[1]));
punishment.setCustomTime(false);
punishment.setActive(true);
@@ -92,7 +91,7 @@ public class TempbanCMD extends ServerCommand
PlexUtils.broadcast(context.messageComponent("banningPlayer", sender.getName(), target.getName()));
if (player != null)
{
plugin.getApi().scheduler().runEntity(player, () -> BungeeUtil.kickPlayer(plugin, player, Punishment.generateBanMessage(punishment, plugin.config.getString("banning.ban_url"), plugin.getPlayerService())));
plugin.getApi().scheduler().runEntity(player, () -> BungeeUtil.kickPlayer(plugin, player, Punishment.generateBanMessage(punishment, plugin.config.getString("banning.ban_url"), plugin.getPlayerNameResolver())));
}
if (rollBack)
{
@@ -93,7 +93,6 @@ public class TempmuteCMD extends ServerCommand
punishment.setCustomTime(true);
punishment.setEndDate(endDate);
punishment.setType(PunishmentType.MUTE);
punishment.setPunishedUsername(player.getName());
punishment.setIp(player.getAddress().getAddress().getHostAddress().trim());
punishment.setReason(reason);
punishment.setActive(true);
@@ -54,7 +54,7 @@ public class BanListener extends ServerListenerBase
PlexPlayer player = plugin.getPlayerService().getPlayer(event.getUniqueId());
player.getPunishments().stream().filter(punishment -> (punishment.getType() == PunishmentType.BAN || punishment.getType() == PunishmentType.TEMPBAN) && punishment.isActive()).findFirst().ifPresent(punishment ->
event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_BANNED,
Punishment.generateBanMessage(punishment, plugin.config.getString("banning.ban_url"), plugin.getPlayerService())));
Punishment.generateBanMessage(punishment, plugin.config.getString("banning.ban_url"), plugin.getPlayerNameResolver())));
return;
}
Punishment ipBannedPunishment = plugin.getPunishmentManager().getBanByIP(event.getAddress().getHostAddress());
@@ -66,7 +66,7 @@ public class BanListener extends ServerListenerBase
return;
}
event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_BANNED,
Punishment.generateBanMessage(ipBannedPunishment, plugin.config.getString("banning.ban_url"), plugin.getPlayerService()));
Punishment.generateBanMessage(ipBannedPunishment, plugin.config.getString("banning.ban_url"), plugin.getPlayerNameResolver()));
}
}
}
@@ -54,7 +54,7 @@ public class PlayerListener extends ServerListenerBase
}
if (!plexPlayer.getName().equals(player.getName()))
{
PlexLog.log(plexPlayer.getName() + " has a new name. Changing it to " + player.getName());
PlexLog.log(plexPlayer.getName() + " has a new last known name. Changing it to " + player.getName());
plexPlayer.setName(player.getName());
plugin.getPlayerService().update(plexPlayer);
}
@@ -0,0 +1,96 @@
package dev.plex.player;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class PlayerNameResolver
{
private final PlayerService playerService;
private final HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3))
.build();
private final Map<UUID, String> profileCache = new ConcurrentHashMap<>();
public PlayerNameResolver(PlayerService playerService)
{
this.playerService = playerService;
}
public String resolve(UUID uuid)
{
if (uuid == null)
{
return "CONSOLE";
}
Player online = Bukkit.getPlayer(uuid);
if (online != null)
{
return online.getName();
}
String local = playerService.getNameByUUID(uuid);
if (local != null && !local.isBlank())
{
return local;
}
String cached = profileCache.get(uuid);
if (cached != null && !cached.isBlank())
{
return cached;
}
return lookupMojangName(uuid)
.map(name ->
{
profileCache.put(uuid, name);
return name;
})
.orElse(uuid.toString());
}
private Optional<String> lookupMojangName(UUID uuid)
{
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid.toString().replace("-", "")))
.timeout(Duration.ofSeconds(5))
.GET()
.build();
try
{
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200)
{
return Optional.empty();
}
JsonObject object = JsonParser.parseString(response.body()).getAsJsonObject();
if (!object.has("name") || !object.get("name").isJsonPrimitive())
{
return Optional.empty();
}
return Optional.ofNullable(object.get("name").getAsString()).filter(name -> !name.isBlank());
}
catch (IOException | InterruptedException | RuntimeException e)
{
if (e instanceof InterruptedException)
{
Thread.currentThread().interrupt();
}
return Optional.empty();
}
}
}
@@ -34,7 +34,6 @@ public class PlexPlayer
private String prefix;
private boolean staffChat;
private boolean vanished;
private boolean commandSpy;
// These fields are transient so MongoDB doesn't automatically drop them in.
@@ -42,8 +41,6 @@ public class PlexPlayer
private transient boolean muted;
private transient boolean lockedUp;
private long coins;
private List<String> ips = Lists.newArrayList();
private List<Punishment> punishments = Lists.newArrayList();
@@ -62,11 +59,8 @@ public class PlexPlayer
this.loginMessage = "";
this.prefix = "";
this.vanished = false;
this.commandSpy = false;
this.coins = 0;
if (loadPunishments)
{
this.checkMutesAndFreeze();
@@ -2,7 +2,8 @@ package dev.plex.punishment;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import dev.plex.player.PlayerService;
import dev.plex.api.punishment.PunishmentSource;
import dev.plex.player.PlayerNameResolver;
import dev.plex.util.PlexUtils;
import dev.plex.util.TimeUtils;
import dev.plex.util.adapter.ZonedDateTimeAdapter;
@@ -24,12 +25,9 @@ public class Punishment
@NotNull
private final UUID punished;
private final UUID punisher;
// Optional display attribution for punishers without a Minecraft UUID
// (e.g. web staff signed in via XenForo). When non-null, render this in
// place of the UUID-based name lookup.
private String punisherName;
private PunishmentSource source;
private String punisherReference;
private String ip;
private String punishedUsername;
private PunishmentType type;
private String reason;
private boolean customTime;
@@ -41,32 +39,33 @@ public class Punishment
{
this.punished = punished;
this.punisher = punisher;
this.source = punisher == null ? PunishmentSource.CONSOLE : PunishmentSource.PLAYER;
this.issueDate = ZonedDateTime.now(ZoneId.of(TimeUtils.TIMEZONE));
}
public static Component generateBanMessage(Punishment punishment, String banUrl, PlayerService playerService)
public static Component generateBanMessage(Punishment punishment, String banUrl, PlayerNameResolver playerNameResolver)
{
return PlexUtils.messageComponent("banMessage", banUrl, punishment.getReason(), TimeUtils.useTimezone(punishment.getEndDate()), punisherDisplayName(punishment, playerService));
return PlexUtils.messageComponent("banMessage", banUrl, punishment.getReason(), TimeUtils.useTimezone(punishment.getEndDate()), punisherDisplayName(punishment, playerNameResolver));
}
public static Component generateKickMessage(Punishment punishment, PlayerService playerService)
public static Component generateKickMessage(Punishment punishment, PlayerNameResolver playerNameResolver)
{
return PlexUtils.messageComponent("kickMessage", punishment.getReason(), punisherDisplayName(punishment, playerService));
return PlexUtils.messageComponent("kickMessage", punishment.getReason(), punisherDisplayName(punishment, playerNameResolver));
}
/**
* Resolves the human-readable punisher attribution for display.
* Prefers the explicit {@link #punisherName} (used for off-server
* sources such as XenForo staff acting via the web HTTPD), falling
* back to a UUID lookup, and finally "CONSOLE" when the punisher is
* truly unknown.
*/
public static String punisherDisplayName(Punishment punishment, PlayerService playerService)
public static String punisherDisplayName(Punishment punishment, PlayerNameResolver playerNameResolver)
{
String explicit = punishment.getPunisherName();
if (explicit != null && !explicit.isEmpty()) return explicit;
if (punishment.getPunisher() == null) return "CONSOLE";
return playerService.getNameByUUID(punishment.getPunisher());
PunishmentSource source = punishment.getSource();
if (source == null)
{
source = punishment.getPunisher() == null ? PunishmentSource.CONSOLE : PunishmentSource.PLAYER;
}
return switch (source)
{
case PLAYER -> punishment.getPunisher() == null ? "CONSOLE" : playerNameResolver.resolve(punishment.getPunisher());
case CONSOLE -> "CONSOLE";
case WEB -> punishment.getPunisherReference() == null || punishment.getPunisherReference().isBlank() ? "WEB" : punishment.getPunisherReference();
};
}
public static Component generateIndefBanMessageWithReason(String type, String banUrl, String reason)
@@ -55,7 +55,6 @@ public class TimingService extends AbstractService
punishment.setReason(PlexUtils.messageString("nukerTempbanReason"));
if (player != null)
{
punishment.setPunishedUsername(player.getName());
punishment.setIp(player.getAddress().getAddress().getHostAddress());
}
punishment.setEndDate(TimeUtils.createDate("5m"));
@@ -2,6 +2,7 @@ package dev.plex.storage;
import com.zaxxer.hikari.HikariConfig;
import dev.plex.Plex;
import dev.plex.api.storage.SqlDialect;
import java.io.File;
import java.util.Arrays;
@@ -24,7 +25,7 @@ public enum StorageType
@Override
public String migrationHistoryTableSql(String tableName)
{
return "CREATE TABLE IF NOT EXISTS " + tableName + " (version VARCHAR(100) NOT NULL PRIMARY KEY, installed_at INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000))";
return "CREATE TABLE IF NOT EXISTS " + quoteIdentifier(tableName) + " (scope VARCHAR(100) NOT NULL, version VARCHAR(100) NOT NULL, installed_at INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000), PRIMARY KEY (scope, version))";
}
},
@@ -42,7 +43,7 @@ public enum StorageType
@Override
public String migrationHistoryTableSql(String tableName)
{
return "CREATE TABLE IF NOT EXISTS `" + tableName + "` (`version` VARCHAR(100) NOT NULL PRIMARY KEY, `installed_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)";
return "CREATE TABLE IF NOT EXISTS " + quoteIdentifier(tableName) + " (`scope` VARCHAR(100) NOT NULL, `version` VARCHAR(100) NOT NULL, `installed_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`scope`, `version`))";
}
},
@@ -81,9 +82,56 @@ public enum StorageType
public abstract void configure(HikariConfig config, Plex plugin);
public SqlDialect dialect()
{
return switch (this)
{
case SQLITE -> SqlDialect.SQLITE;
case MARIADB -> SqlDialect.MARIADB;
case POSTGRES -> SqlDialect.POSTGRES;
};
}
public String quoteIdentifier(String identifier)
{
return switch (this)
{
case MARIADB -> "`" + identifier + "`";
case SQLITE, POSTGRES -> "\"" + identifier + "\"";
};
}
public String migrationHistoryTableSql(String tableName)
{
return "CREATE TABLE IF NOT EXISTS " + tableName + " (version VARCHAR(100) NOT NULL PRIMARY KEY, installed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)";
return "CREATE TABLE IF NOT EXISTS " + quoteIdentifier(tableName) + " (scope VARCHAR(100) NOT NULL, version VARCHAR(100) NOT NULL, installed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (scope, version))";
}
public String playerModuleDataUpsertSql()
{
return switch (this)
{
case SQLITE -> """
INSERT INTO player_module_data (player_uuid, module, data_key, value_json, updated_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(player_uuid, module, data_key) DO UPDATE SET
value_json = excluded.value_json,
updated_at = excluded.updated_at
""";
case MARIADB -> """
INSERT INTO `player_module_data` (`player_uuid`, `module`, `data_key`, `value_json`, `updated_at`)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
`value_json` = VALUES(`value_json`),
`updated_at` = VALUES(`updated_at`)
""";
case POSTGRES -> """
INSERT INTO player_module_data (player_uuid, module, data_key, value_json, updated_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(player_uuid, module, data_key) DO UPDATE SET
value_json = excluded.value_json,
updated_at = excluded.updated_at
""";
};
}
public String getDisplayName()
@@ -9,24 +9,16 @@ import dev.plex.storage.StorageType;
import dev.plex.util.PlexLog;
import lombok.Getter;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
@Getter
public class Database
{
private static final String MIGRATION_TABLE = "plex_schema_history";
protected final Plex plugin;
private final HikariDataSource dataSource;
private final ConnectionSource connectionSource;
private final StorageType storageType;
private final MigrationRunner migrationRunner;
public Database(Plex plugin)
{
@@ -48,7 +40,8 @@ public class Database
try
{
this.connectionSource = new DataSourceConnectionSource(dataSource, config.getJdbcUrl());
runMigrations();
this.migrationRunner = new MigrationRunner(storageType);
this.migrationRunner.runCore(dataSource, getClass().getClassLoader(), List.of("001_initial_schema"));
}
catch (Exception e)
{
@@ -57,105 +50,7 @@ public class Database
}
}
private void runMigrations() throws Exception
{
try (Connection connection = dataSource.getConnection())
{
ensureMigrationTable(connection);
for (String migration : List.of("001_initial_schema"))
{
if (hasMigration(connection, migration))
{
continue;
}
executeMigration(connection, migration);
try (Statement statement = connection.createStatement())
{
statement.executeUpdate("INSERT INTO " + MIGRATION_TABLE + " (version) VALUES ('" + migration + "')");
}
PlexLog.log("Applied database migration " + migration);
}
}
}
private void ensureMigrationTable(Connection connection) throws SQLException
{
try (Statement statement = connection.createStatement())
{
statement.execute(storageType.migrationHistoryTableSql(MIGRATION_TABLE));
}
}
private boolean hasMigration(Connection connection, String migration) throws SQLException
{
try (Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT version FROM " + MIGRATION_TABLE + " WHERE version = '" + migration + "'"))
{
return resultSet.next();
}
}
private void executeMigration(Connection connection, String migration) throws Exception
{
String resource = "db/migration/" + storageType.getMigrationDirectory() + "/" + migration + ".sql";
try (InputStream stream = getClass().getClassLoader().getResourceAsStream(resource))
{
if (stream == null)
{
throw new IllegalStateException("Missing database migration resource: " + resource);
}
for (String sql : splitStatements(new String(stream.readAllBytes(), StandardCharsets.UTF_8)))
{
try (Statement statement = connection.createStatement())
{
statement.execute(sql);
}
}
}
}
private List<String> splitStatements(String script)
{
List<String> statements = new ArrayList<>();
StringBuilder current = new StringBuilder();
boolean inSingleQuote = false;
boolean inDoubleQuote = false;
for (int i = 0; i < script.length(); i++)
{
char c = script.charAt(i);
if (c == '\'' && !inDoubleQuote)
{
inSingleQuote = !inSingleQuote;
}
else if (c == '"' && !inSingleQuote)
{
inDoubleQuote = !inDoubleQuote;
}
if (c == ';' && !inSingleQuote && !inDoubleQuote)
{
addStatement(statements, current);
current.setLength(0);
continue;
}
current.append(c);
}
addStatement(statements, current);
return statements;
}
private void addStatement(List<String> statements, StringBuilder statement)
{
String sql = statement.toString().replaceAll("(?m)^\\s*--.*$", "").trim();
if (!sql.isEmpty())
{
statements.add(sql);
}
}
public Connection getConnection() throws SQLException
public java.sql.Connection getConnection() throws java.sql.SQLException
{
return dataSource.getConnection();
}
@@ -0,0 +1,206 @@
package dev.plex.storage.database;
import dev.plex.module.PlexModule;
import dev.plex.storage.StorageType;
import dev.plex.util.PlexLog;
import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MigrationRunner
{
private static final String MIGRATION_TABLE = "plex_schema_history";
private static final Pattern VERSION_PATTERN = Pattern.compile("^[0-9]{3}_[a-z0-9_]+$");
private static final Pattern TABLE_TOKEN_PATTERN = Pattern.compile("\\{\\{table:([a-z0-9_]+)}}");
private final StorageType storageType;
public MigrationRunner(StorageType storageType)
{
this.storageType = storageType;
}
public void runCore(DataSource dataSource, ClassLoader classLoader, List<String> versions) throws SQLException
{
run(dataSource, "core", versions, version -> readCore(classLoader, version), Function.identity());
}
public void runModule(DataSource dataSource, PlexModule module, String scope, String resourceRoot, List<String> versions, Function<String, String> tableResolver) throws SQLException
{
run(dataSource, scope, versions, version -> readModule(module, resourceRoot, version), tableResolver);
}
private void run(DataSource dataSource, String scope, List<String> versions, ResourceReader reader, Function<String, String> tableResolver) throws SQLException
{
try (Connection connection = dataSource.getConnection())
{
ensureMigrationTable(connection);
for (String version : versions)
{
validateVersion(version);
if (hasMigration(connection, scope, version))
{
continue;
}
String script = replaceTableTokens(reader.read(version), tableResolver);
for (String sql : splitStatements(script))
{
try (Statement statement = connection.createStatement())
{
statement.execute(sql);
}
}
insertMigration(connection, scope, version);
PlexLog.log("Applied database migration " + scope + ":" + version);
}
}
}
private void ensureMigrationTable(Connection connection) throws SQLException
{
try (Statement statement = connection.createStatement())
{
statement.execute(storageType.migrationHistoryTableSql(MIGRATION_TABLE));
}
}
private boolean hasMigration(Connection connection, String scope, String version) throws SQLException
{
try (PreparedStatement statement = connection.prepareStatement("SELECT version FROM " + MIGRATION_TABLE + " WHERE scope = ? AND version = ?"))
{
statement.setString(1, scope);
statement.setString(2, version);
try (ResultSet resultSet = statement.executeQuery())
{
return resultSet.next();
}
}
}
private void insertMigration(Connection connection, String scope, String version) throws SQLException
{
try (PreparedStatement statement = connection.prepareStatement("INSERT INTO " + MIGRATION_TABLE + " (scope, version) VALUES (?, ?)"))
{
statement.setString(1, scope);
statement.setString(2, version);
statement.executeUpdate();
}
}
private void validateVersion(String version) throws SQLException
{
if (!VERSION_PATTERN.matcher(version).matches())
{
throw new SQLException("Invalid migration version: " + version);
}
}
private String readCore(ClassLoader classLoader, String version) throws SQLException
{
String resource = "db/migration/" + storageType.dialect().migrationDirectory() + "/" + version + ".sql";
try (InputStream stream = classLoader.getResourceAsStream(resource))
{
if (stream == null)
{
throw new SQLException("Missing database migration resource: " + resource);
}
return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
}
catch (IOException e)
{
throw new SQLException("Failed to read database migration resource: " + resource, e);
}
}
private String readModule(PlexModule module, String resourceRoot, String version) throws SQLException
{
String resource = resourceRoot + "/" + storageType.dialect().migrationDirectory() + "/" + version + ".sql";
try (InputStream stream = module.getResource(resource))
{
if (stream == null)
{
throw new SQLException("Missing module migration resource: " + resource);
}
return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
}
catch (IOException e)
{
throw new SQLException("Failed to read module migration resource: " + resource, e);
}
}
private String replaceTableTokens(String script, Function<String, String> tableResolver) throws SQLException
{
Matcher matcher = TABLE_TOKEN_PATTERN.matcher(script);
StringBuilder replaced = new StringBuilder();
while (matcher.find())
{
matcher.appendReplacement(replaced, Matcher.quoteReplacement(tableResolver.apply(matcher.group(1))));
}
matcher.appendTail(replaced);
if (replaced.toString().contains("{{table:"))
{
throw new SQLException("Unsupported table token in migration");
}
return replaced.toString();
}
private List<String> splitStatements(String script)
{
List<String> statements = new ArrayList<>();
StringBuilder current = new StringBuilder();
boolean inSingleQuote = false;
boolean inDoubleQuote = false;
for (int i = 0; i < script.length(); i++)
{
char c = script.charAt(i);
if (c == '\'' && !inDoubleQuote)
{
inSingleQuote = !inSingleQuote;
}
else if (c == '"' && !inSingleQuote)
{
inDoubleQuote = !inDoubleQuote;
}
if (c == ';' && !inSingleQuote && !inDoubleQuote)
{
addStatement(statements, current);
current.setLength(0);
continue;
}
current.append(c);
}
addStatement(statements, current);
return statements;
}
private void addStatement(List<String> statements, StringBuilder statement)
{
String sql = statement.toString().replaceAll("(?m)^\\s*--.*$", "").trim();
if (!sql.isEmpty())
{
statements.add(sql);
}
}
@FunctionalInterface
private interface ResourceReader
{
String read(String version) throws SQLException;
}
}
@@ -19,8 +19,8 @@ public class NoteEntity
@DatabaseField(columnName = "uuid", canBeNull = false, index = true, width = 46)
private String uuid;
@DatabaseField(columnName = "written_by", width = 46)
private String writtenBy;
@DatabaseField(columnName = "written_by_uuid", width = 46)
private String writtenByUuid;
@DatabaseField(columnName = "note", width = 2000)
private String note;
@@ -13,8 +13,8 @@ public class PlayerEntity
@DatabaseField(id = true, columnName = "uuid", width = 46)
private String uuid;
@DatabaseField(columnName = "name", width = 18)
private String name;
@DatabaseField(columnName = "last_known_name", width = 18, index = true)
private String lastKnownName;
@DatabaseField(columnName = "login_msg", width = 2000)
private String loginMessage;
@@ -28,12 +28,6 @@ public class PlayerEntity
@DatabaseField(columnName = "ips", width = 2000)
private String ips;
@DatabaseField(columnName = "coins")
private long coins;
@DatabaseField(columnName = "vanished")
private boolean vanished;
@DatabaseField(columnName = "commandspy")
private boolean commandSpy;
@@ -13,17 +13,17 @@ public class PunishmentEntity
@DatabaseField(generatedId = true, columnName = "id")
private long id;
@DatabaseField(columnName = "punished", canBeNull = false, index = true, width = 46)
private String punished;
@DatabaseField(columnName = "punished_uuid", canBeNull = false, index = true, width = 46)
private String punishedUuid;
@DatabaseField(columnName = "punisher", width = 46)
private String punisher;
@DatabaseField(columnName = "punisher_uuid", width = 46)
private String punisherUuid;
@DatabaseField(columnName = "punisherName", width = 64)
private String punisherName;
@DatabaseField(columnName = "source", width = 20)
private String source;
@DatabaseField(columnName = "punishedUsername", width = 16)
private String punishedUsername;
@DatabaseField(columnName = "punisher_reference", width = 200)
private String punisherReference;
@DatabaseField(columnName = "ip", width = 2000, index = true)
private String ip;
@@ -0,0 +1,47 @@
package dev.plex.storage.module;
import dev.plex.module.PlexModule;
import java.util.Locale;
public final class ModuleNames
{
private static final int MAX_PREFIX_LENGTH = 40;
private ModuleNames()
{
}
public static String prefix(PlexModule module)
{
String name = module.getPlexModuleFile().getName().toLowerCase(Locale.ROOT);
if (name.startsWith("module-"))
{
name = name.substring("module-".length());
}
name = name.replaceAll("[^a-z0-9]+", "_").replaceAll("^_+|_+$", "");
if (name.isBlank())
{
throw new IllegalArgumentException("Module name does not produce a valid storage prefix");
}
if (name.length() > MAX_PREFIX_LENGTH)
{
name = name.substring(0, MAX_PREFIX_LENGTH).replaceAll("_+$", "");
}
return name;
}
public static String table(String prefix, String localName)
{
return prefix + "_" + validateLocalName(localName);
}
public static String validateLocalName(String localName)
{
if (localName == null || !localName.matches("[a-z][a-z0-9_]{0,47}"))
{
throw new IllegalArgumentException("Invalid module table name: " + localName);
}
return localName;
}
}
@@ -0,0 +1,41 @@
package dev.plex.storage.module;
import dev.plex.Plex;
import dev.plex.api.storage.ModuleMigrations;
import dev.plex.module.PlexModule;
import java.sql.SQLException;
import java.util.List;
public class ServerModuleMigrations implements ModuleMigrations
{
private final Plex plugin;
private final PlexModule module;
private final ServerModuleStorage storage;
public ServerModuleMigrations(Plex plugin, PlexModule module, ServerModuleStorage storage)
{
this.plugin = plugin;
this.module = module;
this.storage = storage;
}
@Override
public void run(List<String> versions) throws SQLException
{
run("db/migration", versions);
}
@Override
public void run(String resourceRoot, List<String> versions) throws SQLException
{
plugin.getSqlConnection().getMigrationRunner().runModule(
plugin.getSqlConnection().getDataSource(),
module,
storage.scope(),
resourceRoot,
versions,
storage::quotedTable
);
}
}
@@ -0,0 +1,42 @@
package dev.plex.storage.module;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.dao.DaoManager;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.DatabaseTableConfig;
import dev.plex.api.storage.ModuleOrm;
import java.sql.SQLException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ServerModuleOrm implements ModuleOrm
{
private final ConnectionSource connectionSource;
private final ServerModuleStorage storage;
private final Map<String, Dao<?, ?>> daos = new ConcurrentHashMap<>();
public ServerModuleOrm(ConnectionSource connectionSource, ServerModuleStorage storage)
{
this.connectionSource = connectionSource;
this.storage = storage;
}
@Override
@SuppressWarnings("unchecked")
public <T, ID> Dao<T, ID> dao(Class<T> entityClass, String localTableName) throws SQLException
{
String key = entityClass.getName() + ":" + localTableName;
Dao<?, ?> existing = daos.get(key);
if (existing != null)
{
return (Dao<T, ID>) existing;
}
DatabaseTableConfig<T> tableConfig = DatabaseTableConfig.fromClass(connectionSource.getDatabaseType(), entityClass);
tableConfig.setTableName(storage.table(localTableName));
Dao<T, ID> dao = DaoManager.createDao(connectionSource, tableConfig);
daos.put(key, dao);
return dao;
}
}
@@ -0,0 +1,69 @@
package dev.plex.storage.module;
import com.j256.ormlite.misc.TransactionManager;
import dev.plex.Plex;
import dev.plex.api.storage.ModuleMigrations;
import dev.plex.api.storage.ModuleOrm;
import dev.plex.api.storage.ModuleStorage;
import dev.plex.api.storage.SqlCallable;
import dev.plex.module.PlexModule;
import java.sql.SQLException;
public class ServerModuleStorage implements ModuleStorage
{
private final Plex plugin;
private final PlexModule module;
private final String prefix;
private final ModuleMigrations migrations;
private final ModuleOrm orm;
public ServerModuleStorage(Plex plugin, PlexModule module)
{
this.plugin = plugin;
this.module = module;
this.prefix = ModuleNames.prefix(module);
this.migrations = new ServerModuleMigrations(plugin, module, this);
this.orm = new ServerModuleOrm(plugin.getSqlConnection().getConnectionSource(), this);
}
@Override
public String prefix()
{
return prefix;
}
@Override
public String table(String localName)
{
return ModuleNames.table(prefix, localName);
}
String quotedTable(String localName)
{
return plugin.getStorageType().quoteIdentifier(table(localName));
}
String scope()
{
return "module:" + prefix;
}
@Override
public ModuleMigrations migrations()
{
return migrations;
}
@Override
public ModuleOrm orm()
{
return orm;
}
@Override
public <T> T transaction(SqlCallable<T> callable) throws SQLException
{
return TransactionManager.callInTransaction(plugin.getSqlConnection().getConnectionSource(), callable::call);
}
}
@@ -0,0 +1,15 @@
package dev.plex.storage.player;
import com.google.gson.JsonElement;
import java.util.Optional;
import java.util.UUID;
public interface PlayerModuleDataRepository
{
Optional<JsonElement> get(UUID playerUuid, String module, String key);
void set(UUID playerUuid, String module, String key, JsonElement value);
void remove(UUID playerUuid, String module, String key);
}
@@ -57,7 +57,7 @@ public class SQLPlayerData implements PlayerRepository
{
try
{
return players.queryBuilder().where().eq("name", username).queryForFirst() != null;
return players.queryBuilder().where().eq("last_known_name", username).queryForFirst() != null;
}
catch (SQLException e)
{
@@ -84,7 +84,7 @@ public class SQLPlayerData implements PlayerRepository
try
{
PlayerEntity entity = players.queryForId(uuid.toString());
return entity == null ? null : entity.getName();
return entity == null ? null : entity.getLastKnownName();
}
catch (SQLException e)
{
@@ -102,7 +102,7 @@ public class SQLPlayerData implements PlayerRepository
{
try
{
return toPlayer(players.queryBuilder().limit(1L).where().eq("name", username).queryForFirst(), loadExtraData);
return toPlayer(players.queryBuilder().limit(1L).where().eq("last_known_name", username).queryForFirst(), loadExtraData);
}
catch (SQLException e)
{
@@ -169,13 +169,11 @@ public class SQLPlayerData implements PlayerRepository
}
PlexPlayer plexPlayer = new PlexPlayer(UUID.fromString(entity.getUuid()), false);
plexPlayer.setName(entity.getName());
plexPlayer.setName(entity.getLastKnownName());
plexPlayer.setLoginMessage(entity.getLoginMessage());
plexPlayer.setPrefix(entity.getPrefix());
plexPlayer.setStaffChat(entity.isStaffChat());
plexPlayer.setIps(parseIps(entity.getIps()));
plexPlayer.setCoins(entity.getCoins());
plexPlayer.setVanished(entity.isVanished());
plexPlayer.setCommandSpy(entity.isCommandSpy());
if (loadExtraData)
{
@@ -189,13 +187,11 @@ public class SQLPlayerData implements PlayerRepository
{
PlayerEntity entity = new PlayerEntity();
entity.setUuid(player.getUuid().toString());
entity.setName(player.getName());
entity.setLastKnownName(player.getName());
entity.setLoginMessage(player.getLoginMessage());
entity.setPrefix(player.getPrefix());
entity.setStaffChat(player.isStaffChat());
entity.setIps(GSON.toJson(player.getIps()));
entity.setCoins(player.getCoins());
entity.setVanished(player.isVanished());
entity.setCommandSpy(player.isCommandSpy());
return entity;
}
@@ -0,0 +1,86 @@
package dev.plex.storage.player;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import dev.plex.storage.SQLConnection;
import dev.plex.storage.StorageType;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;
import java.util.UUID;
public class SQLPlayerModuleData implements PlayerModuleDataRepository
{
private final SQLConnection sqlConnection;
private final StorageType storageType;
public SQLPlayerModuleData(SQLConnection sqlConnection, StorageType storageType)
{
this.sqlConnection = sqlConnection;
this.storageType = storageType;
}
@Override
public Optional<JsonElement> get(UUID playerUuid, String module, String key)
{
String sql = "SELECT value_json FROM player_module_data WHERE player_uuid = ? AND module = ? AND data_key = ?";
try (Connection connection = sqlConnection.getConnection(); PreparedStatement statement = connection.prepareStatement(sql))
{
statement.setString(1, playerUuid.toString());
statement.setString(2, module);
statement.setString(3, key);
try (ResultSet resultSet = statement.executeQuery())
{
if (!resultSet.next())
{
return Optional.empty();
}
return Optional.of(JsonParser.parseString(resultSet.getString("value_json")));
}
}
catch (SQLException | JsonSyntaxException e)
{
e.printStackTrace();
return Optional.empty();
}
}
@Override
public void set(UUID playerUuid, String module, String key, JsonElement value)
{
try (Connection connection = sqlConnection.getConnection(); PreparedStatement statement = connection.prepareStatement(storageType.playerModuleDataUpsertSql()))
{
statement.setString(1, playerUuid.toString());
statement.setString(2, module);
statement.setString(3, key);
statement.setString(4, value.toString());
statement.setLong(5, System.currentTimeMillis());
statement.executeUpdate();
}
catch (SQLException e)
{
e.printStackTrace();
}
}
@Override
public void remove(UUID playerUuid, String module, String key)
{
String sql = "DELETE FROM player_module_data WHERE player_uuid = ? AND module = ? AND data_key = ?";
try (Connection connection = sqlConnection.getConnection(); PreparedStatement statement = connection.prepareStatement(sql))
{
statement.setString(1, playerUuid.toString());
statement.setString(2, module);
statement.setString(3, key);
statement.executeUpdate();
}
catch (SQLException e)
{
e.printStackTrace();
}
}
}
@@ -16,6 +16,7 @@ import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
@@ -47,6 +48,7 @@ public class SQLNotes implements NoteRepository
return notes.queryForEq("uuid", uuid.toString()).stream()
.sorted(Comparator.comparingInt(NoteEntity::getId))
.map(this::toNote)
.flatMap(Optional::stream)
.toList();
}
catch (SQLException e)
@@ -96,16 +98,23 @@ public class SQLNotes implements NoteRepository
}, executor);
}
private Note toNote(NoteEntity entity)
private Optional<Note> toNote(NoteEntity entity)
{
try
{
Note note = new Note(
UUID.fromString(entity.getUuid()),
entity.getNote(),
UUID.fromString(entity.getWrittenBy()),
UUID.fromString(entity.getWrittenByUuid()),
ZonedDateTime.ofInstant(Instant.ofEpochMilli(entity.getTimestamp()), ZoneId.of(TimeUtils.TIMEZONE))
);
note.setId(entity.getId());
return note;
return Optional.of(note);
}
catch (IllegalArgumentException | NullPointerException e)
{
return Optional.empty();
}
}
private NoteEntity toEntity(Note note)
@@ -113,7 +122,7 @@ public class SQLNotes implements NoteRepository
NoteEntity entity = new NoteEntity();
entity.setId(note.getId());
entity.setUuid(note.getUuid().toString());
entity.setWrittenBy(note.getWrittenBy().toString());
entity.setWrittenByUuid(note.getWrittenBy().toString());
entity.setNote(note.getNote());
entity.setTimestamp(note.getTimestamp().toInstant().toEpochMilli());
return entity;
@@ -5,6 +5,7 @@ import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.dao.DaoManager;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.stmt.UpdateBuilder;
import dev.plex.api.punishment.PunishmentSource;
import dev.plex.punishment.Punishment;
import dev.plex.punishment.PunishmentType;
import dev.plex.storage.database.entity.PunishmentEntity;
@@ -59,7 +60,7 @@ public class SQLPunishment implements PunishmentRepository
{
try
{
return punishments.queryForEq("punished", uuid.toString()).stream().map(this::toPunishment).toList();
return punishments.queryForEq("punished_uuid", uuid.toString()).stream().map(this::toPunishment).toList();
}
catch (SQLException e)
{
@@ -119,7 +120,7 @@ public class SQLPunishment implements PunishmentRepository
{
UpdateBuilder<PunishmentEntity, Long> update = punishments.updateBuilder();
update.updateColumnValue("active", active);
update.where().eq("punished", punished.toString()).and().eq("type", type.name());
update.where().eq("punished_uuid", punished.toString()).and().eq("type", type.name());
update.update();
}
catch (SQLException e)
@@ -130,13 +131,13 @@ public class SQLPunishment implements PunishmentRepository
private Punishment toPunishment(PunishmentEntity entity)
{
UUID punisher = entity.getPunisher() == null || entity.getPunisher().isBlank() ? null : UUID.fromString(entity.getPunisher());
Punishment punishment = new Punishment(UUID.fromString(entity.getPunished()), punisher);
UUID punisher = entity.getPunisherUuid() == null || entity.getPunisherUuid().isBlank() ? null : UUID.fromString(entity.getPunisherUuid());
Punishment punishment = new Punishment(UUID.fromString(entity.getPunishedUuid()), punisher);
punishment.setActive(entity.isActive());
punishment.setType(PunishmentType.valueOf(entity.getType()));
punishment.setCustomTime(entity.isCustomTime());
punishment.setPunishedUsername(entity.getPunishedUsername());
punishment.setPunisherName(entity.getPunisherName());
punishment.setSource(entity.getSource() == null ? punishment.getSource() : PunishmentSource.valueOf(entity.getSource()));
punishment.setPunisherReference(entity.getPunisherReference());
punishment.setIssueDate(ZonedDateTime.ofInstant(Instant.ofEpochMilli(entity.getIssueDate()), ZoneId.of(TimeUtils.TIMEZONE)));
punishment.setEndDate(ZonedDateTime.ofInstant(Instant.ofEpochMilli(entity.getEndDate()), ZoneId.of(TimeUtils.TIMEZONE)));
punishment.setReason(entity.getReason());
@@ -147,10 +148,11 @@ public class SQLPunishment implements PunishmentRepository
private PunishmentEntity toEntity(Punishment punishment)
{
PunishmentEntity entity = new PunishmentEntity();
entity.setPunished(punishment.getPunished().toString());
entity.setPunisher(punishment.getPunisher() == null ? null : punishment.getPunisher().toString());
entity.setPunisherName(punishment.getPunisherName());
entity.setPunishedUsername(punishment.getPunishedUsername());
entity.setPunishedUuid(punishment.getPunished().toString());
entity.setPunisherUuid(punishment.getPunisher() == null ? null : punishment.getPunisher().toString());
PunishmentSource source = punishment.getSource() == null ? (punishment.getPunisher() == null ? PunishmentSource.CONSOLE : PunishmentSource.PLAYER) : punishment.getSource();
entity.setSource(source.name());
entity.setPunisherReference(punishment.getPunisherReference());
entity.setIp(punishment.getIp());
entity.setType(punishment.getType().name());
entity.setReason(punishment.getReason());
@@ -1,23 +1,21 @@
CREATE TABLE IF NOT EXISTS `players` (
`uuid` VARCHAR(46) NOT NULL,
`name` VARCHAR(18),
`last_known_name` VARCHAR(18),
`login_msg` VARCHAR(2000),
`prefix` VARCHAR(2000),
`staffChat` BOOLEAN,
`ips` VARCHAR(2000),
`coins` BIGINT,
`vanished` BOOLEAN,
`commandspy` BOOLEAN,
PRIMARY KEY (`uuid`),
INDEX `idx_players_name` (`name`)
INDEX `idx_players_last_known_name` (`last_known_name`)
);
CREATE TABLE IF NOT EXISTS `punishments` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`punished` VARCHAR(46) NOT NULL,
`punisher` VARCHAR(46),
`punisherName` VARCHAR(64),
`punishedUsername` VARCHAR(16),
`punished_uuid` VARCHAR(46) NOT NULL,
`punisher_uuid` VARCHAR(46),
`source` VARCHAR(30),
`punisher_reference` VARCHAR(200),
`ip` VARCHAR(2000),
`type` VARCHAR(30),
`reason` VARCHAR(2000),
@@ -26,7 +24,7 @@ CREATE TABLE IF NOT EXISTS `punishments` (
`issueDate` BIGINT NOT NULL,
`endDate` BIGINT,
PRIMARY KEY (`id`),
INDEX `idx_punishments_punished` (`punished`),
INDEX `idx_punishments_punished` (`punished_uuid`),
INDEX `idx_punishments_ip` (`ip`(64))
);
@@ -34,7 +32,7 @@ CREATE TABLE IF NOT EXISTS `notes` (
`row_id` BIGINT NOT NULL AUTO_INCREMENT,
`id` INT NOT NULL,
`uuid` VARCHAR(46) NOT NULL,
`written_by` VARCHAR(46),
`written_by_uuid` VARCHAR(46),
`note` VARCHAR(2000),
`timestamp` BIGINT,
PRIMARY KEY (`row_id`),
@@ -49,3 +47,12 @@ CREATE TABLE IF NOT EXISTS `player_ips` (
UNIQUE KEY `uq_player_ips_player_ip` (`player_uuid`, `ip`),
INDEX `idx_player_ips_ip` (`ip`)
);
CREATE TABLE IF NOT EXISTS `player_module_data` (
`player_uuid` VARCHAR(46) NOT NULL,
`module` VARCHAR(100) NOT NULL,
`data_key` VARCHAR(64) NOT NULL,
`value_json` LONGTEXT NOT NULL,
`updated_at` BIGINT NOT NULL,
PRIMARY KEY (`player_uuid`, `module`, `data_key`)
);
@@ -1,23 +1,21 @@
CREATE TABLE IF NOT EXISTS players (
uuid VARCHAR(46) NOT NULL PRIMARY KEY,
name VARCHAR(18),
last_known_name VARCHAR(18),
login_msg VARCHAR(2000),
prefix VARCHAR(2000),
staffChat BOOLEAN,
ips VARCHAR(2000),
coins BIGINT,
vanished BOOLEAN,
commandspy BOOLEAN
);
CREATE INDEX IF NOT EXISTS idx_players_name ON players(name);
CREATE INDEX IF NOT EXISTS idx_players_last_known_name ON players(last_known_name);
CREATE TABLE IF NOT EXISTS punishments (
id BIGSERIAL PRIMARY KEY,
punished VARCHAR(46) NOT NULL,
punisher VARCHAR(46),
punisherName VARCHAR(64),
punishedUsername VARCHAR(16),
punished_uuid VARCHAR(46) NOT NULL,
punisher_uuid VARCHAR(46),
source VARCHAR(30),
punisher_reference VARCHAR(200),
ip VARCHAR(2000),
type VARCHAR(30),
reason VARCHAR(2000),
@@ -27,14 +25,14 @@ CREATE TABLE IF NOT EXISTS punishments (
endDate BIGINT
);
CREATE INDEX IF NOT EXISTS idx_punishments_punished ON punishments(punished);
CREATE INDEX IF NOT EXISTS idx_punishments_punished ON punishments(punished_uuid);
CREATE INDEX IF NOT EXISTS idx_punishments_ip ON punishments(ip);
CREATE TABLE IF NOT EXISTS notes (
row_id BIGSERIAL PRIMARY KEY,
id INT NOT NULL,
uuid VARCHAR(46) NOT NULL,
written_by VARCHAR(46),
written_by_uuid VARCHAR(46),
note VARCHAR(2000),
timestamp BIGINT
);
@@ -49,3 +47,12 @@ CREATE TABLE IF NOT EXISTS player_ips (
);
CREATE INDEX IF NOT EXISTS idx_player_ips_ip ON player_ips(ip);
CREATE TABLE IF NOT EXISTS player_module_data (
player_uuid VARCHAR(46) NOT NULL,
module VARCHAR(100) NOT NULL,
data_key VARCHAR(64) NOT NULL,
value_json TEXT NOT NULL,
updated_at BIGINT NOT NULL,
PRIMARY KEY (player_uuid, module, data_key)
);
@@ -1,21 +1,19 @@
CREATE TABLE IF NOT EXISTS players (
uuid VARCHAR(46) NOT NULL PRIMARY KEY,
name VARCHAR(18),
last_known_name VARCHAR(18),
login_msg VARCHAR(2000),
prefix VARCHAR(2000),
staffChat BOOLEAN,
ips VARCHAR(2000),
coins BIGINT,
vanished BOOLEAN,
commandspy BOOLEAN
);
CREATE TABLE IF NOT EXISTS punishments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
punished VARCHAR(46) NOT NULL,
punisher VARCHAR(46),
punisherName VARCHAR(64),
punishedUsername VARCHAR(16),
punished_uuid VARCHAR(46) NOT NULL,
punisher_uuid VARCHAR(46),
source VARCHAR(30),
punisher_reference VARCHAR(200),
ip VARCHAR(2000),
type VARCHAR(30),
reason VARCHAR(2000),
@@ -29,7 +27,7 @@ CREATE TABLE IF NOT EXISTS notes (
row_id INTEGER PRIMARY KEY AUTOINCREMENT,
id INT NOT NULL,
uuid VARCHAR(46) NOT NULL,
written_by VARCHAR(46),
written_by_uuid VARCHAR(46),
note VARCHAR(2000),
timestamp BIGINT
);
@@ -40,8 +38,17 @@ CREATE TABLE IF NOT EXISTS player_ips (
ip VARCHAR(64) NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_players_name ON players(name);
CREATE INDEX IF NOT EXISTS idx_punishments_punished ON punishments(punished);
CREATE TABLE IF NOT EXISTS player_module_data (
player_uuid VARCHAR(46) NOT NULL,
module VARCHAR(100) NOT NULL,
data_key VARCHAR(64) NOT NULL,
value_json TEXT NOT NULL,
updated_at INTEGER NOT NULL,
PRIMARY KEY (player_uuid, module, data_key)
);
CREATE INDEX IF NOT EXISTS idx_players_last_known_name ON players(last_known_name);
CREATE INDEX IF NOT EXISTS idx_punishments_punished ON punishments(punished_uuid);
CREATE INDEX IF NOT EXISTS idx_punishments_ip ON punishments(ip);
CREATE INDEX IF NOT EXISTS idx_notes_uuid ON notes(uuid);
CREATE UNIQUE INDEX IF NOT EXISTS uq_player_ips_player_ip ON player_ips(player_uuid, ip);