Migrate database to LuckPerms meta

This commit is contained in:
Alessandro Proto 2024-10-11 12:07:41 +02:00
parent 870b495deb
commit 69099b2cec
11 changed files with 181 additions and 288 deletions

View file

@ -1,4 +1,4 @@
Copyright 2024 project-connecticut
Copyright 2024 ReconnectedCC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View file

@ -43,7 +43,7 @@ dependencies {
annotationProcessor modImplementation("io.wispforest:owo-lib:${project.owo_version}")
include "io.wispforest:owo-sentinel:${project.owo_version}"
include implementation("org.postgresql:postgresql:${project.postgresql_version}")
compileOnly "net.luckperms:api:${project.luckpermsapi_version}"
}

View file

@ -6,10 +6,10 @@ org.gradle.parallel=true
# check these on https://fabricmc.net/develop
minecraft_version=1.20.1
yarn_mappings=1.20.1+build.10
loader_version=0.16.3
loader_version=0.16.5
# Mod Properties
mod_version=1.7.0
mod_version=1.8.4
maven_group=cc.reconnected
archives_base_name=rcc-server
@ -18,4 +18,4 @@ fabric_version=0.92.2+1.20.1
owo_version=0.11.2+1.20
postgresql_version=42.7.3
luckpermsapi_version=5.4

View file

@ -1,10 +1,9 @@
package cc.reconnected.server;
import cc.reconnected.server.commands.RccCommand;
import cc.reconnected.server.database.DatabaseClient;
import cc.reconnected.server.database.PlayerData;
import cc.reconnected.server.database.PlayerTable;
import cc.reconnected.server.events.PlayerWelcome;
import cc.reconnected.server.events.Ready;
import cc.reconnected.server.http.ServiceServer;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
@ -12,15 +11,15 @@ import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.LuckPermsProvider;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Date;
@ -44,14 +43,9 @@ public class RccServer implements ModInitializer {
return serviceServer;
}
private final DatabaseClient database = new DatabaseClient();
public DatabaseClient database() {
return database;
}
private final PlayerTable playerTable = new PlayerTable();
public PlayerTable playerTable() {
return playerTable;
private LuckPerms luckPerms;
public LuckPerms luckPerms() {
return luckPerms;
}
public static float getTPS() {
@ -77,20 +71,17 @@ public class RccServer implements ModInitializer {
CommandRegistrationCallback.EVENT.register(RccCommand::register);
try {
// Jumpstart connection
database.connection();
playerTable.ensureDatabaseCreated();
} catch (SQLException e) {
LOGGER.error("Database error", e);
}
try {
serviceServer = new ServiceServer();
} catch (IOException e) {
LOGGER.error("Unable to start HTTP server", e);
}
ServerLifecycleEvents.SERVER_STARTED.register(server -> {
luckPerms = LuckPermsProvider.get();
Ready.READY.invoker().ready(server, luckPerms);
});
ServerTickEvents.END_SERVER_TICK.register(server -> {
currentMspt = server.getTickTime();
if (currentMspt != 0) {
@ -106,23 +97,17 @@ public class RccServer implements ModInitializer {
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
currentPlayerCount = server.getCurrentPlayerCount() + 1;
var player = handler.getPlayer();
var playerData = playerTable.getPlayerData(player.getUuid());
if(playerData == null) {
// new player!
playerData = new PlayerData(handler.getPlayer().getUuid());
playerData.firstJoinedDate(new Date());
playerData.name(player.getName().getString());
playerTable.updatePlayerData(playerData);
PlayerWelcome.PLAYER_WELCOME.invoker().playerWelcome(player, playerData, server);
// TODO: make it customizable via config
broadcastMessage(server, Text.literal("Welcome " + player.getName().getString() + " to the server!").formatted(Formatting.LIGHT_PURPLE));
} else {
if (!playerData.name().equals(player.getName().getString())) {
playerData.name(player.getName().getString());
playerTable.updatePlayerData(playerData);
var playerData = PlayerData.getPlayer(player.getUuid());
playerData.set(PlayerData.KEYS.username, player.getName().getString());
var firstJoinedDate = playerData.getDate(PlayerData.KEYS.firstJoinedDate);
boolean isNewPlayer = false;
if (firstJoinedDate == null) {
playerData.setDate(PlayerData.KEYS.firstJoinedDate, new Date());
isNewPlayer = true;
}
if(isNewPlayer) {
PlayerWelcome.PLAYER_WELCOME.invoker().playerWelcome(player, playerData, server);
LOGGER.info("Player {} joined for the first time!", player.getName().getString());
}
});

View file

@ -5,5 +5,4 @@ import io.wispforest.owo.config.annotation.Config;
@Config(name = "rcc-server-config", wrapperName = "RccServerConfig")
public class RccServerConfigModel {
public short httpPort = 25581;
public String databaseUrl = "jdbc:postgresql://127.0.0.1:5432/rcc?user=myuser&password=mypassword";
}

View file

@ -7,11 +7,7 @@ import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.text.Text;
import static com.mojang.brigadier.arguments.StringArgumentType.getString;
import static com.mojang.brigadier.arguments.StringArgumentType.word;
import static net.minecraft.server.command.CommandManager.literal;
import static net.minecraft.server.command.CommandManager.argument;
import static net.minecraft.server.command.CommandManager.*;
public class RccCommand {
public static void register(CommandDispatcher<ServerCommandSource> dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) {
@ -21,13 +17,6 @@ public class RccCommand {
.executes(ctx -> {
return 1;
})
.then(literal("clearcache")
.executes(context -> {
RccServer.getInstance().playerTable().clearCache();
context.getSource().sendFeedback(() -> Text.literal("RCC PlayerTable cache cleared!"), false);
return 1;
})
)
);
}
}

View file

@ -1,15 +0,0 @@
package cc.reconnected.server.database;
import cc.reconnected.server.RccServer;
import java.sql.*;
public class DatabaseClient {
private Connection connection;
public Connection connection() throws SQLException {
if (connection == null || connection.isClosed()) {
connection = DriverManager.getConnection(RccServer.CONFIG.databaseUrl());
}
return connection;
}
}

View file

@ -1,74 +1,165 @@
package cc.reconnected.server.database;
import cc.reconnected.server.RccServer;
import net.luckperms.api.LuckPerms;
import net.luckperms.api.model.user.User;
import net.luckperms.api.node.Node;
import net.luckperms.api.node.NodeBuilder;
import net.luckperms.api.node.NodeType;
import net.luckperms.api.node.types.MetaNode;
import net.minecraft.server.network.ServerPlayerEntity;
import org.jetbrains.annotations.Nullable;
import java.util.Date;
import java.util.UUID;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
public class PlayerData {
public static final String nodePrefix = "rcc";
private static LuckPerms luckPerms() {
return RccServer.getInstance().luckPerms();
}
public static class KEYS {
public static final String username = "username";
public static final String discordId = "discord_id";
public static final String isBot = "is_bot";
public static final String isAlt = "is_alt";
public static final String pronouns = "pronouns";
public static final String firstJoinedDate = "first_joined_date";
public static final String supporterLevel = "supporter_level";
}
private final User lpUser;
private final UUID uuid;
@Nullable
private String name;
private Date firstJoinedDate;
@Nullable
private String discordId;
private boolean isBot = false;
private boolean isAlt = false;
private Set<MetaNode> rawNodes;
private Map<String, String> nodes;
@Nullable
private String pronouns = null;
public PlayerData(UUID uuid) {
private PlayerData(UUID uuid, User lpUser) {
this.uuid = uuid;
this.lpUser = lpUser;
refreshNodes();
RccServer.LOGGER.info("Player {} has the following RCC nodes", this.uuid);
nodes.forEach((key, value) -> {
RccServer.LOGGER.info("{}: {}", key, value);
});
}
public UUID uuid() {
public UUID getUuid() {
return uuid;
}
public String name() {
if (name == null) {
return uuid.toString();
}
public @Nullable String getUsername() {
var username = get(KEYS.username);
if (username == null) {
return name;
}
public void name(@Nullable String name) {
this.name = name;
return username;
}
public Date firstJoinedDate() {
return firstJoinedDate;
}
public void firstJoinedDate(Date firstJoinedDate) {
this.firstJoinedDate = firstJoinedDate;
public String getEffectiveName() {
var effName = getUsername();
if (effName == null)
return uuid.toString();
return effName;
}
public @Nullable String discordId() {
return discordId;
}
public void discordId(@Nullable String discordId) {
this.discordId = discordId;
public void refreshNodes() {
rawNodes = lpUser.getNodes(NodeType.META)
.parallelStream()
.filter(node -> node.getMetaKey().startsWith(nodePrefix + "."))
.collect(Collectors.toSet());
nodes = rawNodes.stream().collect(Collectors.toMap(MetaNode::getMetaKey, MetaNode::getMetaValue));
}
public boolean isBot() {
return isBot;
}
public void isBot(boolean isBot) {
this.isBot = isBot;
public void set(String key, @Nullable String value) {
var node = meta(key, value).build();
luckPerms().getUserManager().modifyUser(uuid, user -> {
user.data().clear(NodeType.META.predicate(mn -> mn.getMetaKey().equals(key)));
user.data().add(node);
refreshNodes();
});
}
public boolean isAlt() {
return isAlt;
}
public void isAlt(boolean isAlt) {
this.isAlt = isAlt;
public @Nullable String get(String key) {
if (!nodes.containsKey(nodePrefix + "." + key))
return null;
return nodes.get(nodePrefix + "." + key);
}
public String pronouns() {
return pronouns;
public @Nullable MetaNode getNode(String key) {
return rawNodes.stream().filter(rawNode -> rawNode.getMetaKey().equals(key)).findFirst().orElse(null);
}
public void pronouns(@Nullable String pronouns) {
this.pronouns = pronouns;
public void setBoolean(String key, boolean value) {
set(key, Boolean.toString(value));
}
public boolean getBoolean(String key) {
if (!nodes.containsKey(nodePrefix + "." + key))
return false;
return Boolean.parseBoolean(nodes.get(nodePrefix + "." + key));
}
public boolean getBoolean(String key, boolean defaultValue) {
if (!nodes.containsKey(nodePrefix + "." + key))
return defaultValue;
return Boolean.parseBoolean(nodes.get(nodePrefix + "." + key));
}
public void setDate(String key, Date date) {
var dateString = DateTimeFormatter.ISO_INSTANT.format(date.toInstant());
set(key, dateString);
}
public Date getDate(String key) {
if (!nodes.containsKey(nodePrefix + "." + key))
return null;
var dateString = nodes.get(nodePrefix + "." + key);
var ta = DateTimeFormatter.ISO_INSTANT.parse(dateString);
return Date.from(Instant.from(ta));
}
public void delete(String key) {
luckPerms().getUserManager().modifyUser(uuid, user -> {
user.data().clear(NodeType.META.predicate(mn -> mn.getMetaKey().equals(nodePrefix + "." + key)));
});
}
public static PlayerData getPlayer(UUID uuid) {
var lp = luckPerms();
var userManager = lp.getUserManager();
var userFuture = userManager.loadUser(uuid);
// TODO: ouch, not good...
var lpUser = userFuture.join();
var playerData = new PlayerData(uuid, lpUser);
playerData.name = lpUser.getUsername();
return playerData;
}
public static PlayerData getPlayer(ServerPlayerEntity player) {
var user = luckPerms().getPlayerAdapter(ServerPlayerEntity.class).getUser(player);
var playerData = new PlayerData(player.getUuid(), user);
playerData.name = player.getEntityName();
return playerData;
}
public static NodeBuilder<?, ?> node(String key) {
return Node.builder(nodePrefix + "." + key);
}
public static NodeBuilder<?, ?> meta(String key, String value) {
return MetaNode.builder(nodePrefix + "." + key, value);
}
}

View file

@ -1,175 +0,0 @@
package cc.reconnected.server.database;
import cc.reconnected.server.RccServer;
import org.jetbrains.annotations.Nullable;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Date;
import java.util.HashMap;
import java.util.UUID;
public class PlayerTable {
private final HashMap<UUID, PlayerData> cache = new HashMap<>();
private DatabaseClient database() {
return RccServer.getInstance().database();
}
public void ensureDatabaseCreated() {
try {
var conn = database().connection();
var stmt = conn.prepareStatement(
"CREATE TABLE IF NOT EXISTS players (" +
"uuid UUID NOT NULL PRIMARY KEY," +
"firstJoined TIMESTAMP DEFAULT CURRENT_TIMESTAMP," +
"lastKnownName VARCHAR(16)," +
"discordId VARCHAR," +
"isBot BOOL DEFAULT FALSE," +
"isAlt BOOL DEFAULT FALSE," +
"pronouns VARCHAR DEFAULT NULL" +
");");
stmt.executeUpdate();
stmt.close();
} catch (SQLException e) {
RccServer.LOGGER.error("Could not create players data tables", e);
}
}
public void refreshPlayerData(UUID uuid) {
cache.remove(uuid);
}
public void clearCache() {
cache.clear();
}
public boolean exists(UUID uuid) {
try {
var conn = database().connection();
var stmt = conn.prepareStatement("SELECT uuid FROM players WHERE uuid = ?;");
stmt.setObject(1, uuid);
var set = stmt.executeQuery();
var exists = set.next();
stmt.close();
return exists;
} catch (SQLException e) {
RccServer.LOGGER.error("Could not get player data from database", e);
return false;
}
}
@Nullable
public PlayerData getPlayerData(UUID uuid) {
if (cache.containsKey(uuid)) {
return cache.get(uuid);
}
try {
var conn = database().connection();
var stmt = conn.prepareStatement("SELECT * FROM players WHERE uuid = ?;");
stmt.setObject(1, uuid);
var set = stmt.executeQuery();
if (!set.next()) {
return null;
}
var playerData = new PlayerData(set.getObject("uuid", UUID.class));
var firstJoinTimestamp = set.getObject("firstJoined", Timestamp.class);
playerData.firstJoinedDate(new Date(firstJoinTimestamp.getTime()));
playerData.name(set.getString("lastKnownName"));
playerData.discordId(set.getString("discordId"));
playerData.isBot(set.getBoolean("isBot"));
playerData.isAlt(set.getBoolean("isAlt"));
playerData.pronouns(set.getString("pronouns"));
stmt.close();
cache.put(uuid, playerData);
return playerData;
} catch (SQLException e) {
RccServer.LOGGER.error("Could not get player data from database", e);
return null;
}
}
public boolean deletePlayerData(UUID uuid) {
cache.remove(uuid);
try {
var conn = database().connection();
var stmt = conn.prepareStatement("DELETE FROM players WHERE uuid = ?;");
stmt.setObject(1, uuid);
stmt.execute();
stmt.close();
return true;
} catch(SQLException e) {
RccServer.LOGGER.error("Could not delete player data from database", e);
return false;
}
}
public boolean createPlayerData(PlayerData playerData) {
if(exists(playerData.uuid())) {
return updatePlayerData(playerData);
}
cache.put(playerData.uuid(), playerData);
try {
var conn = database().connection();
var stmt = conn.prepareStatement("INSERT INTO players(uuid, firstJoined, lastKnownName, discordId, isBot, isAlt, pronouns) VALUES (?,?,?,?,?,?,?);");
stmt.setObject(1, playerData.uuid());
var timestamp = new Timestamp(playerData.firstJoinedDate().getTime());
stmt.setTimestamp(2, timestamp);
stmt.setString(3, playerData.name());
stmt.setString(4, playerData.discordId());
stmt.setBoolean(5, playerData.isBot());
stmt.setBoolean(6, playerData.isAlt());
stmt.setString(7, playerData.pronouns());
stmt.execute();
stmt.close();
return true;
} catch(SQLException e) {
RccServer.LOGGER.error("Could not create player data from database", e);
return false;
}
}
public boolean updatePlayerData(PlayerData playerData) {
if(!exists(playerData.uuid())) {
return createPlayerData(playerData);
}
cache.put(playerData.uuid(), playerData);
try {
var conn = database().connection();
var stmt = conn.prepareStatement("UPDATE players SET lastknownname = ?, discordid = ?, isBot = ?, isAlt = ?, pronouns = ? WHERE uuid = ?");
//var stmt = conn.prepareStatement("INSERT INTO players(uuid, firstJoined, lastKnownName, discordId, isBot, isAlt, pronouns) VALUES (?,?,?,?,?,?,?);");
stmt.setString(1, playerData.name());
stmt.setString(2, playerData.discordId());
stmt.setBoolean(3, playerData.isBot());
stmt.setBoolean(4, playerData.isAlt());
stmt.setString(5, playerData.pronouns());
stmt.setObject(6, playerData.uuid());
stmt.execute();
stmt.close();
return true;
} catch (SQLException e) {
RccServer.LOGGER.error("Could not update player data on database", e);
return false;
}
}
}

View file

@ -0,0 +1,17 @@
package cc.reconnected.server.events;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.luckperms.api.LuckPerms;
import net.minecraft.server.MinecraftServer;
public interface Ready {
Event<Ready> READY = EventFactory.createArrayBacked(Ready.class,
(listeners) -> (server, luckPerms) -> {
for (Ready listener : listeners) {
listener.ready(server, luckPerms);
}
});
void ready(MinecraftServer server, LuckPerms luckPerms);
}

View file

@ -6,7 +6,8 @@
"description": "Server mod for the ReconnectedCC Minecraft server",
"authors": [
"AlexDevs",
"EmmaKnijn"
"EmmaKnijn",
"ReconnectedCC"
],
"contact": {
"sources": "https://github.com/ReconnectedCC/rcc-server"
@ -25,6 +26,7 @@
"fabricloader": ">=0.16.0",
"minecraft": "~1.20.1",
"java": ">=17",
"fabric-api": "*"
"fabric-api": "*",
"luckperms": ">=5.4"
}
}