commit b6cb26063535870e32fcd89169ca504358bb2ee1 Author: Xelara Networks Date: Sat Jun 13 18:45:07 2026 -0400 New diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..bf63c8c --- /dev/null +++ b/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + com.dirtbagmc + DirtReferrals + 1.0.0 + jar + + DirtReferrals + Referral tracking plugin for Paper + + + 21 + UTF-8 + + + + + papermc-repo + https://repo.papermc.io/repository/maven-public/ + + + + + + io.papermc.paper + paper-api + 1.21.1-R0.1-SNAPSHOT + provided + + + org.xerial + sqlite-jdbc + 3.46.1.3 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + + + + + diff --git a/src/main/java/com/dirtbagmc/dirtreferrals/AdminCommand.java b/src/main/java/com/dirtbagmc/dirtreferrals/AdminCommand.java new file mode 100644 index 0000000..581c4c0 --- /dev/null +++ b/src/main/java/com/dirtbagmc/dirtreferrals/AdminCommand.java @@ -0,0 +1,152 @@ +package com.dirtbagmc.dirtreferrals; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; + +import java.util.List; +import java.util.UUID; + +public class AdminCommand implements CommandExecutor { + + private final DirtReferralsPlugin plugin; + + public AdminCommand(DirtReferralsPlugin plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!sender.hasPermission("dirtreferrals.admin")) { + plugin.getMessageManager().send(sender, "no-permission"); + return true; + } + + if (args.length == 0) { + plugin.getMessageManager().send(sender, "usage-admin"); + return true; + } + + switch (args[0].toLowerCase()) { + case "reload" -> { + plugin.reloadPlugin(); + plugin.getMessageManager().send(sender, "reloaded"); + return true; + } + case "stats" -> { + if (args.length < 2) { + plugin.getMessageManager().send(sender, "usage-stats"); + return true; + } + + UUID uuid = plugin.getDatabaseManager().getPlayerUuidByName(args[1]); + if (uuid == null) { + plugin.getMessageManager().send(sender, "no-data"); + return true; + } + + String name = plugin.getDatabaseManager().getPlayerName(uuid); + int count = plugin.getDatabaseManager().getReferralCount(uuid); + + sender.sendMessage(plugin.getMessageManager().get("stats-header") + .replace("%player%", name == null ? args[1] : name)); + sender.sendMessage(plugin.getMessageManager().get("stats-count") + .replace("%count%", String.valueOf(count))); + return true; + } + case "top" -> { + List top = plugin.getDatabaseManager().getTopReferrers(10); + sender.sendMessage(plugin.getMessageManager().get("top-header")); + + if (top.isEmpty()) { + plugin.getMessageManager().send(sender, "no-data"); + return true; + } + + int position = 1; + for (TopEntry entry : top) { + sender.sendMessage(plugin.getMessageManager().get("top-entry") + .replace("%position%", String.valueOf(position)) + .replace("%player%", entry.getName()) + .replace("%count%", String.valueOf(entry.getCount()))); + position++; + } + return true; + } + case "give" -> { + if (args.length < 3) { + plugin.getMessageManager().send(sender, "usage-give"); + return true; + } + + UUID uuid = plugin.getDatabaseManager().getPlayerUuidByName(args[1]); + if (uuid == null) { + plugin.getMessageManager().send(sender, "no-data"); + return true; + } + + int amount; + try { + amount = Integer.parseInt(args[2]); + } catch (NumberFormatException e) { + plugin.getMessageManager().send(sender, "invalid-number"); + return true; + } + + if (amount < 0) { + plugin.getMessageManager().send(sender, "invalid-number"); + return true; + } + + plugin.getDatabaseManager().addReferralCount(uuid, amount); + + sender.sendMessage(plugin.getMessageManager().get("credit-added") + .replace("%amount%", String.valueOf(amount)) + .replace("%player%", plugin.getDatabaseManager().getPlayerName(uuid))); + return true; + } + case "take" -> { + if (args.length < 3) { + plugin.getMessageManager().send(sender, "usage-take"); + return true; + } + + UUID uuid = plugin.getDatabaseManager().getPlayerUuidByName(args[1]); + if (uuid == null) { + plugin.getMessageManager().send(sender, "no-data"); + return true; + } + + int amount; + try { + amount = Integer.parseInt(args[2]); + } catch (NumberFormatException e) { + plugin.getMessageManager().send(sender, "invalid-number"); + return true; + } + + if (amount < 0) { + plugin.getMessageManager().send(sender, "invalid-number"); + return true; + } + + int current = plugin.getDatabaseManager().getReferralCount(uuid); + if (current < amount) { + plugin.getMessageManager().send(sender, "not-enough-credit"); + return true; + } + + plugin.getDatabaseManager().setReferralCount(uuid, current - amount); + + sender.sendMessage(plugin.getMessageManager().get("credit-removed") + .replace("%amount%", String.valueOf(amount)) + .replace("%player%", plugin.getDatabaseManager().getPlayerName(uuid))); + return true; + } + default -> { + plugin.getMessageManager().send(sender, "unknown-subcommand"); + return true; + } + } + } +} diff --git a/src/main/java/com/dirtbagmc/dirtreferrals/DatabaseManager.java b/src/main/java/com/dirtbagmc/dirtreferrals/DatabaseManager.java new file mode 100644 index 0000000..b6bafa8 --- /dev/null +++ b/src/main/java/com/dirtbagmc/dirtreferrals/DatabaseManager.java @@ -0,0 +1,333 @@ +package com.dirtbagmc.dirtreferrals; + +import java.io.File; +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class DatabaseManager { + + private final DirtReferralsPlugin plugin; + private Connection connection; + + public DatabaseManager(DirtReferralsPlugin plugin) { + this.plugin = plugin; + } + + public void initialize() { + try { + if (!plugin.getDataFolder().exists()) { + plugin.getDataFolder().mkdirs(); + } + + String fileName = plugin.getConfig().getString("database.file", "referrals.db"); + File dbFile = new File(plugin.getDataFolder(), fileName); + connection = DriverManager.getConnection("jdbc:sqlite:" + dbFile.getAbsolutePath()); + + createTables(); + } catch (SQLException e) { + plugin.getLogger().severe("Failed to initialize SQLite database."); + e.printStackTrace(); + } + } + + private void createTables() throws SQLException { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate(""" + CREATE TABLE IF NOT EXISTS players ( + uuid TEXT PRIMARY KEY, + name TEXT NOT NULL, + referral_count INTEGER NOT NULL DEFAULT 0 + ) + """); + + statement.executeUpdate(""" + CREATE TABLE IF NOT EXISTS referrals ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + referrer_uuid TEXT NOT NULL, + referred_uuid TEXT NOT NULL, + referred_name TEXT NOT NULL, + join_host TEXT, + ip_address TEXT, + counted INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL + ) + """); + + statement.executeUpdate(""" + CREATE TABLE IF NOT EXISTS player_ips ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + player_uuid TEXT NOT NULL, + ip_address TEXT NOT NULL, + created_at INTEGER NOT NULL + ) + """); + + statement.executeUpdate(""" + CREATE TABLE IF NOT EXISTS claimed_rewards ( + referrer_uuid TEXT NOT NULL, + milestone INTEGER NOT NULL, + claimed_at INTEGER NOT NULL, + PRIMARY KEY (referrer_uuid, milestone) + ) + """); + } + } + + public void close() { + if (connection != null) { + try { + connection.close(); + } catch (SQLException e) { + plugin.getLogger().severe("Failed to close database connection."); + e.printStackTrace(); + } + } + } + + public void upsertPlayer(UUID uuid, String name) { + String sql = """ + INSERT INTO players (uuid, name, referral_count) + VALUES (?, ?, 0) + ON CONFLICT(uuid) DO UPDATE SET name = excluded.name + """; + + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, uuid.toString()); + ps.setString(2, name); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().severe("Failed to upsert player " + name); + e.printStackTrace(); + } + } + + public String getPlayerName(UUID uuid) { + String sql = "SELECT name FROM players WHERE uuid = ?"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, uuid.toString()); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return rs.getString("name"); + } + } + } catch (SQLException e) { + plugin.getLogger().severe("Failed to get player name."); + e.printStackTrace(); + } + return null; + } + + public UUID getPlayerUuidByName(String name) { + String sql = "SELECT uuid FROM players WHERE LOWER(name) = LOWER(?)"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, name); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return UUID.fromString(rs.getString("uuid")); + } + } + } catch (SQLException e) { + plugin.getLogger().severe("Failed to get UUID by player name."); + e.printStackTrace(); + } + return null; + } + + public int getReferralCount(UUID uuid) { + String sql = "SELECT referral_count FROM players WHERE uuid = ?"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, uuid.toString()); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return rs.getInt("referral_count"); + } + } + } catch (SQLException e) { + plugin.getLogger().severe("Failed to get referral count."); + e.printStackTrace(); + } + return 0; + } + + public void setReferralCount(UUID uuid, int count) { + String sql = "UPDATE players SET referral_count = ? WHERE uuid = ?"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setInt(1, count); + ps.setString(2, uuid.toString()); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().severe("Failed to set referral count."); + e.printStackTrace(); + } + } + + public void addReferralCount(UUID uuid, int amount) { + setReferralCount(uuid, Math.max(0, getReferralCount(uuid) + amount)); + } + + public boolean hasReferralRecord(UUID referrerUuid, UUID referredUuid) { + String sql = "SELECT 1 FROM referrals WHERE referrer_uuid = ? AND referred_uuid = ? LIMIT 1"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, referrerUuid.toString()); + ps.setString(2, referredUuid.toString()); + try (ResultSet rs = ps.executeQuery()) { + return rs.next(); + } + } catch (SQLException e) { + plugin.getLogger().severe("Failed checking referral record."); + e.printStackTrace(); + } + return false; + } + + public boolean hasAnyReferralRecordForReferred(UUID referredUuid) { + String sql = "SELECT 1 FROM referrals WHERE referred_uuid = ? LIMIT 1"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, referredUuid.toString()); + try (ResultSet rs = ps.executeQuery()) { + return rs.next(); + } + } catch (SQLException e) { + plugin.getLogger().severe("Failed checking referred player history."); + e.printStackTrace(); + } + return false; + } + + public void insertReferral(UUID referrerUuid, UUID referredUuid, String referredName, String joinHost, String ipAddress, boolean counted) { + String sql = """ + INSERT INTO referrals (referrer_uuid, referred_uuid, referred_name, join_host, ip_address, counted, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + """; + + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, referrerUuid.toString()); + ps.setString(2, referredUuid.toString()); + ps.setString(3, referredName); + ps.setString(4, joinHost); + ps.setString(5, ipAddress); + ps.setInt(6, counted ? 1 : 0); + ps.setLong(7, System.currentTimeMillis()); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().severe("Failed to insert referral."); + e.printStackTrace(); + } + } + + public void addPlayerIp(UUID playerUuid, String ipAddress) { + String checkSql = "SELECT 1 FROM player_ips WHERE player_uuid = ? AND ip_address = ? LIMIT 1"; + String insertSql = "INSERT INTO player_ips (player_uuid, ip_address, created_at) VALUES (?, ?, ?)"; + + try (PreparedStatement check = connection.prepareStatement(checkSql)) { + check.setString(1, playerUuid.toString()); + check.setString(2, ipAddress); + try (ResultSet rs = check.executeQuery()) { + if (rs.next()) { + return; + } + } + } catch (SQLException e) { + plugin.getLogger().severe("Failed checking existing player IP."); + e.printStackTrace(); + return; + } + + try (PreparedStatement insert = connection.prepareStatement(insertSql)) { + insert.setString(1, playerUuid.toString()); + insert.setString(2, ipAddress); + insert.setLong(3, System.currentTimeMillis()); + insert.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().severe("Failed storing player IP."); + e.printStackTrace(); + } + } + + public boolean playerHasIp(UUID playerUuid, String ipAddress) { + String sql = "SELECT 1 FROM player_ips WHERE player_uuid = ? AND ip_address = ? LIMIT 1"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, playerUuid.toString()); + ps.setString(2, ipAddress); + try (ResultSet rs = ps.executeQuery()) { + return rs.next(); + } + } catch (SQLException e) { + plugin.getLogger().severe("Failed checking player IP."); + e.printStackTrace(); + } + return false; + } + + public boolean anyReferralUsesIpForReferrer(UUID referrerUuid, String ipAddress) { + String sql = "SELECT 1 FROM referrals WHERE referrer_uuid = ? AND ip_address = ? LIMIT 1"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, referrerUuid.toString()); + ps.setString(2, ipAddress); + try (ResultSet rs = ps.executeQuery()) { + return rs.next(); + } + } catch (SQLException e) { + plugin.getLogger().severe("Failed checking referral IP reuse."); + e.printStackTrace(); + } + return false; + } + + public boolean hasClaimedReward(UUID referrerUuid, int milestone) { + String sql = "SELECT 1 FROM claimed_rewards WHERE referrer_uuid = ? AND milestone = ? LIMIT 1"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, referrerUuid.toString()); + ps.setInt(2, milestone); + try (ResultSet rs = ps.executeQuery()) { + return rs.next(); + } + } catch (SQLException e) { + plugin.getLogger().severe("Failed checking claimed reward."); + e.printStackTrace(); + } + return false; + } + + public void setRewardClaimed(UUID referrerUuid, int milestone) { + String sql = """ + INSERT OR IGNORE INTO claimed_rewards (referrer_uuid, milestone, claimed_at) + VALUES (?, ?, ?) + """; + + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, referrerUuid.toString()); + ps.setInt(2, milestone); + ps.setLong(3, System.currentTimeMillis()); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().severe("Failed marking reward as claimed."); + e.printStackTrace(); + } + } + + public List getTopReferrers(int limit) { + List results = new ArrayList<>(); + String sql = "SELECT uuid, name, referral_count FROM players ORDER BY referral_count DESC, name ASC LIMIT ?"; + + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setInt(1, limit); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + results.add(new TopEntry( + UUID.fromString(rs.getString("uuid")), + rs.getString("name"), + rs.getInt("referral_count") + )); + } + } + } catch (SQLException e) { + plugin.getLogger().severe("Failed loading top referrers."); + e.printStackTrace(); + } + + return results; + } +} diff --git a/src/main/java/com/dirtbagmc/dirtreferrals/DirtReferralsPlugin.java b/src/main/java/com/dirtbagmc/dirtreferrals/DirtReferralsPlugin.java new file mode 100644 index 0000000..c65c0f7 --- /dev/null +++ b/src/main/java/com/dirtbagmc/dirtreferrals/DirtReferralsPlugin.java @@ -0,0 +1,96 @@ +package com.dirtbagmc.dirtreferrals; + +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.io.IOException; + +public class DirtReferralsPlugin extends JavaPlugin { + + private File messagesFile; + private FileConfiguration messages; + + private DatabaseManager databaseManager; + private ReferralManager referralManager; + private MessageManager messageManager; + + @Override + public void onEnable() { + saveDefaultConfig(); + createMessagesFile(); + this.messageManager = new MessageManager(this); + this.databaseManager = new DatabaseManager(this); + this.databaseManager.initialize(); + this.referralManager = new ReferralManager(this); + + getCommand("refer").setExecutor(new ReferralCommand(this)); + getCommand("dirtreferrals").setExecutor(new AdminCommand(this)); + + Bukkit.getPluginManager().registerEvents(new PlayerListener(this), this); + + getLogger().info("DirtReferrals enabled."); + } + + @Override + public void onDisable() { + if (referralManager != null) { + referralManager.shutdown(); + } + if (databaseManager != null) { + databaseManager.close(); + } + getLogger().info("DirtReferrals disabled."); + } + + public void reloadPlugin() { + reloadConfig(); + reloadMessages(); + if (referralManager != null) { + referralManager.reload(); + } + } + + private void createMessagesFile() { + if (!getDataFolder().exists()) { + getDataFolder().mkdirs(); + } + + messagesFile = new File(getDataFolder(), "messages.yml"); + if (!messagesFile.exists()) { + saveResource("messages.yml", false); + } + messages = YamlConfiguration.loadConfiguration(messagesFile); + } + + public void reloadMessages() { + messages = YamlConfiguration.loadConfiguration(messagesFile); + } + + public FileConfiguration getMessages() { + return messages; + } + + public void saveMessages() { + try { + messages.save(messagesFile); + } catch (IOException e) { + getLogger().severe("Could not save messages.yml"); + e.printStackTrace(); + } + } + + public DatabaseManager getDatabaseManager() { + return databaseManager; + } + + public ReferralManager getReferralManager() { + return referralManager; + } + + public MessageManager getMessageManager() { + return messageManager; + } +} diff --git a/src/main/java/com/dirtbagmc/dirtreferrals/MessageManager.java b/src/main/java/com/dirtbagmc/dirtreferrals/MessageManager.java new file mode 100644 index 0000000..a1a2803 --- /dev/null +++ b/src/main/java/com/dirtbagmc/dirtreferrals/MessageManager.java @@ -0,0 +1,35 @@ +package com.dirtbagmc.dirtreferrals; + +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +public class MessageManager { + + private final DirtReferralsPlugin plugin; + + public MessageManager(DirtReferralsPlugin plugin) { + this.plugin = plugin; + } + + public String get(String path) { + String prefix = plugin.getMessages().getString("prefix", "&6[DirtReferrals] "); + String message = plugin.getMessages().getString(path, "&cMissing message: " + path); + return color(prefix + message); + } + + public String getRaw(String path) { + return color(plugin.getMessages().getString(path, "&cMissing message: " + path)); + } + + public String color(String input) { + return ChatColor.translateAlternateColorCodes('&', input); + } + + public void send(CommandSender sender, String path) { + sender.sendMessage(get(path)); + } + + public void sendRaw(CommandSender sender, String message) { + sender.sendMessage(color(message)); + } +} diff --git a/src/main/java/com/dirtbagmc/dirtreferrals/PlayerListener.java b/src/main/java/com/dirtbagmc/dirtreferrals/PlayerListener.java new file mode 100644 index 0000000..02d32dd --- /dev/null +++ b/src/main/java/com/dirtbagmc/dirtreferrals/PlayerListener.java @@ -0,0 +1,42 @@ +package com.dirtbagmc.dirtreferrals; + +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerLoginEvent; + +public class PlayerListener implements Listener { + + private final DirtReferralsPlugin plugin; + + public PlayerListener(DirtReferralsPlugin plugin) { + this.plugin = plugin; + } + + @EventHandler + public void onLogin(PlayerLoginEvent event) { + String joinHost = null; + + try { + if (event.getHostname() != null && !event.getHostname().isBlank()) { + joinHost = event.getHostname(); + } + } catch (NoSuchMethodError ignored) { + } + + if (joinHost != null) { + plugin.getLogger().info("Player " + event.getPlayer().getName() + " joined with hostname: " + joinHost); + } + + Player player = event.getPlayer(); + plugin.getReferralManager().queueReferral(player, joinHost); + } + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + plugin.getReferralManager().registerPlayer(player); + plugin.getReferralManager().processQueuedReferral(player); + } +} diff --git a/src/main/java/com/dirtbagmc/dirtreferrals/ReferralCommand.java b/src/main/java/com/dirtbagmc/dirtreferrals/ReferralCommand.java new file mode 100644 index 0000000..9342064 --- /dev/null +++ b/src/main/java/com/dirtbagmc/dirtreferrals/ReferralCommand.java @@ -0,0 +1,35 @@ +package com.dirtbagmc.dirtreferrals; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class ReferralCommand implements CommandExecutor { + + private final DirtReferralsPlugin plugin; + + public ReferralCommand(DirtReferralsPlugin plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player player)) { + plugin.getMessageManager().send(sender, "player-only"); + return true; + } + + if (!player.hasPermission("dirtreferrals.use")) { + plugin.getMessageManager().send(player, "no-permission"); + return true; + } + + String address = plugin.getReferralManager().getReferralAddress(player); + String message = plugin.getMessageManager().get("referral-link") + .replace("%referral_ip%", address); + + player.sendMessage(message); + return true; + } +} diff --git a/src/main/java/com/dirtbagmc/dirtreferrals/ReferralManager.java b/src/main/java/com/dirtbagmc/dirtreferrals/ReferralManager.java new file mode 100644 index 0000000..56a7f46 --- /dev/null +++ b/src/main/java/com/dirtbagmc/dirtreferrals/ReferralManager.java @@ -0,0 +1,229 @@ +package com.dirtbagmc.dirtreferrals; + +import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; + +import java.net.InetAddress; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class ReferralManager { + + private final DirtReferralsPlugin plugin; + private final Map pendingReferrals = new ConcurrentHashMap<>(); + + public ReferralManager(DirtReferralsPlugin plugin) { + this.plugin = plugin; + } + + public void reload() { + pendingReferrals.clear(); + } + + public void shutdown() { + pendingReferrals.clear(); + } + + public String getReferralAddress(Player player) { + String baseDomain = plugin.getConfig().getString("domain.base", "dirtbagmc.com"); + boolean lowercase = plugin.getConfig().getBoolean("domain.use-lowercase-playernames", true); + String name = lowercase ? player.getName().toLowerCase(Locale.ROOT) : player.getName(); + return name + "." + baseDomain; + } + + public void registerPlayer(Player player) { + plugin.getDatabaseManager().upsertPlayer(player.getUniqueId(), player.getName()); + + String ip = getPlayerIp(player); + if (ip != null && plugin.getConfig().getBoolean("referral.store-ip-addresses", true)) { + plugin.getDatabaseManager().addPlayerIp(player.getUniqueId(), transformIp(ip)); + } + } + + public void queueReferral(Player player, String joinHost) { + if (joinHost == null || joinHost.isBlank()) { + return; + } + + String baseDomain = plugin.getConfig().getString("domain.base", "dirtbagmc.com").toLowerCase(Locale.ROOT); + String normalizedHost = joinHost.toLowerCase(Locale.ROOT); + + if (!normalizedHost.endsWith("." + baseDomain)) { + return; + } + + String subdomain = normalizedHost.substring(0, normalizedHost.length() - ("." + baseDomain).length()); + if (subdomain.isBlank()) { + return; + } + + UUID referrerUuid = plugin.getDatabaseManager().getPlayerUuidByName(subdomain); + if (referrerUuid == null) { + return; + } + + pendingReferrals.put(player.getUniqueId(), new PendingReferral(referrerUuid, normalizedHost, System.currentTimeMillis())); + } + + public void processQueuedReferral(Player player) { + PendingReferral pending = pendingReferrals.remove(player.getUniqueId()); + if (pending == null) { + return; + } + + UUID referredUuid = player.getUniqueId(); + UUID referrerUuid = pending.referrerUuid(); + + if (plugin.getConfig().getBoolean("referral.block-self-referrals", true) && referredUuid.equals(referrerUuid)) { + return; + } + + if (plugin.getConfig().getBoolean("referral.count-only-first-join", true) + && plugin.getDatabaseManager().hasAnyReferralRecordForReferred(referredUuid)) { + return; + } + + if (plugin.getDatabaseManager().hasReferralRecord(referrerUuid, referredUuid)) { + return; + } + + String rawIp = getPlayerIp(player); + String transformedIp = rawIp == null ? null : transformIp(rawIp); + + if (plugin.getConfig().getBoolean("referral.block-same-ip", true) && transformedIp != null) { + if (plugin.getConfig().getBoolean("referral.compare-against-referrer-known-ips", true) + && plugin.getDatabaseManager().playerHasIp(referrerUuid, transformedIp)) { + alertBlocked(player.getName(), plugin.getDatabaseManager().getPlayerName(referrerUuid), "same IP as referrer"); + plugin.getDatabaseManager().insertReferral(referrerUuid, referredUuid, player.getName(), pending.joinHost(), transformedIp, false); + return; + } + + if (plugin.getConfig().getBoolean("referral.compare-against-other-referred-ips", true) + && plugin.getDatabaseManager().anyReferralUsesIpForReferrer(referrerUuid, transformedIp)) { + alertBlocked(player.getName(), plugin.getDatabaseManager().getPlayerName(referrerUuid), "reused referral IP"); + plugin.getDatabaseManager().insertReferral(referrerUuid, referredUuid, player.getName(), pending.joinHost(), transformedIp, false); + return; + } + } + + int requiredMinutes = plugin.getConfig().getInt("referral.require-player-to-stay-online-minutes", 5); + if (requiredMinutes <= 0) { + countReferralNow(player, referrerUuid, pending.joinHost(), transformedIp); + return; + } + + Bukkit.getScheduler().runTaskLater(plugin, () -> { + Player online = Bukkit.getPlayer(referredUuid); + if (online == null || !online.isOnline()) { + return; + } + + if (plugin.getDatabaseManager().hasReferralRecord(referrerUuid, referredUuid)) { + return; + } + + countReferralNow(online, referrerUuid, pending.joinHost(), transformedIp); + }, requiredMinutes * 60L * 20L); + } + + private void countReferralNow(Player referredPlayer, UUID referrerUuid, String joinHost, String transformedIp) { + UUID referredUuid = referredPlayer.getUniqueId(); + + if (plugin.getDatabaseManager().hasReferralRecord(referrerUuid, referredUuid)) { + return; + } + + plugin.getDatabaseManager().insertReferral(referrerUuid, referredUuid, referredPlayer.getName(), joinHost, transformedIp, true); + plugin.getDatabaseManager().addReferralCount(referrerUuid, 1); + + String referrerName = plugin.getDatabaseManager().getPlayerName(referrerUuid); + checkMilestones(referrerUuid, referrerName); + + String message = plugin.getMessageManager().get("referral-counted") + .replace("%referrer%", referrerName == null ? "Unknown" : referrerName); + referredPlayer.sendMessage(message); + } + + private void checkMilestones(UUID referrerUuid, String referrerName) { + int count = plugin.getDatabaseManager().getReferralCount(referrerUuid); + ConfigurationSection milestones = plugin.getConfig().getConfigurationSection("rewards.milestones"); + if (milestones == null) { + return; + } + + for (String key : milestones.getKeys(false)) { + int milestone; + try { + milestone = Integer.parseInt(key); + } catch (NumberFormatException e) { + continue; + } + + if (count < milestone) { + continue; + } + + if (plugin.getDatabaseManager().hasClaimedReward(referrerUuid, milestone)) { + continue; + } + + List commands = plugin.getConfig().getStringList("rewards.milestones." + key + ".commands"); + for (String command : commands) { + String parsed = command + .replace("%referrer%", referrerName == null ? "Unknown" : referrerName) + .replace("%count%", String.valueOf(count)) + .replace("%milestone%", String.valueOf(milestone)); + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), parsed); + } + + plugin.getDatabaseManager().setRewardClaimed(referrerUuid, milestone); + + Player referrer = Bukkit.getPlayer(referrerUuid); + if (referrer != null && referrer.isOnline()) { + String rewardMessage = plugin.getConfig().getString("rewards.milestones." + key + ".message", + "&aYou reached " + milestone + " referrals and earned a reward!"); + referrer.sendMessage(plugin.getMessageManager().color( + plugin.getMessages().getString("prefix", "&6[DirtReferrals] ") + rewardMessage + )); + } + } + } + + private void alertBlocked(String referredName, String referrerName, String reason) { + if (!plugin.getConfig().getBoolean("alerts.notify-on-blocked-same-ip", true)) { + return; + } + + String permission = plugin.getConfig().getString("alerts.notify-permission", "dirtreferrals.alerts"); + String message = plugin.getMessages().getString("prefix", "&6[DirtReferrals] ") + + "&cBlocked referral: &e" + referredName + " &7-> &e" + + (referrerName == null ? "Unknown" : referrerName) + " &7(" + reason + ")"; + + String colored = plugin.getMessageManager().color(message); + for (Player online : Bukkit.getOnlinePlayers()) { + if (online.hasPermission(permission)) { + online.sendMessage(colored); + } + } + } + + private String getPlayerIp(Player player) { + if (player.getAddress() == null) { + return null; + } + InetAddress address = player.getAddress().getAddress(); + return address == null ? null : address.getHostAddress(); + } + + private String transformIp(String ip) { + boolean hash = plugin.getConfig().getBoolean("referral.hash-ip-addresses", false); + if (!hash) { + return ip; + } + return Integer.toHexString(ip.hashCode()); + } + + private record PendingReferral(UUID referrerUuid, String joinHost, long createdAt) { + } +} diff --git a/src/main/java/com/dirtbagmc/dirtreferrals/TopEntry.java b/src/main/java/com/dirtbagmc/dirtreferrals/TopEntry.java new file mode 100644 index 0000000..164aa3e --- /dev/null +++ b/src/main/java/com/dirtbagmc/dirtreferrals/TopEntry.java @@ -0,0 +1,28 @@ +package com.dirtbagmc.dirtreferrals; + +import java.util.UUID; + +public class TopEntry { + + private final UUID uuid; + private final String name; + private final int count; + + public TopEntry(UUID uuid, String name, int count) { + this.uuid = uuid; + this.name = name; + this.count = count; + } + + public UUID getUuid() { + return uuid; + } + + public String getName() { + return name; + } + + public int getCount() { + return count; + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..6d25950 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,38 @@ +domain: + base: "dirtbagmc.com" + use-lowercase-playernames: true + +referral: + count-only-first-join: true + require-player-to-stay-online-minutes: 5 + block-self-referrals: true + block-same-ip: true + compare-against-referrer-known-ips: true + compare-against-other-referred-ips: true + store-ip-addresses: true + hash-ip-addresses: false + +rewards: + milestones: + "2": + commands: + - "say %referrer% reached 2 referrals!" + message: "&aYou reached 2 referrals and earned a reward!" + "5": + commands: + - "say %referrer% reached 5 referrals!" + message: "&aYou reached 5 referrals and earned a reward!" + "10": + commands: + - "say %referrer% reached 10 referrals!" + message: "&6You reached 10 referrals and earned a reward!" + +alerts: + notify-on-blocked-same-ip: true + notify-permission: "dirtreferrals.alerts" + +database: + file: "referrals.db" + +admin: + stats-page-size: 10 diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml new file mode 100644 index 0000000..e39541f --- /dev/null +++ b/src/main/resources/messages.yml @@ -0,0 +1,26 @@ +prefix: "&6[DirtReferrals] " + +player-only: "&cPlayers only." +no-permission: "&cYou do not have permission." +reloaded: "&aDirtReferrals reloaded." +unknown-subcommand: "&cUnknown subcommand." +usage-admin: "&eUsage: /dirtreferrals " +usage-stats: "&eUsage: /dirtreferrals stats " +usage-give: "&eUsage: /dirtreferrals give " +usage-take: "&eUsage: /dirtreferrals take " + +referral-link: "&aHere is your referral IP! Have your friends join using it to receive rewards: &e%referral_ip%" +stats-header: "&6Referral stats for &e%player%&6:" +stats-count: "&eCount: &a%count%" +top-header: "&6Top Referrals:" +top-entry: "&e#%position% &f%player% &7- &a%count%" +no-data: "&cNo data found for that player." + +credit-added: "&aAdded &e%amount% &acredit to &e%player%&a." +credit-removed: "&aRemoved &e%amount% &acredit from &e%player%&a." +not-enough-credit: "&cThat player does not have enough referral credit." +invalid-number: "&cInvalid number." +blocked-same-ip: "&cReferral blocked due to same IP." +blocked-self-referral: "&cYou cannot refer yourself." +milestone-reward: "&aYou earned a referral reward for %amount% referrals!" +referral-counted: "&aReferral counted for &e%referrer%&a." diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..d4cbb6b --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,26 @@ +name: DirtReferrals +version: 1.0.0 +main: com.dirtbagmc.dirtreferrals.DirtReferralsPlugin +api-version: '1.21' +author: DirtBagMC +description: Referral tracking plugin with subdomain-based joins and milestone rewards + +commands: + refer: + description: Show your referral address + usage: /refer + aliases: [referrals] + dirtreferrals: + description: DirtReferrals admin commands + usage: /dirtreferrals + +permissions: + dirtreferrals.use: + description: Allows use of referral commands + default: true + dirtreferrals.admin: + description: Allows admin commands + default: op + dirtreferrals.alerts: + description: Receive blocked referral alerts + default: op diff --git a/target/DirtReferrals-1.0.0.jar b/target/DirtReferrals-1.0.0.jar new file mode 100644 index 0000000..bc632cf Binary files /dev/null and b/target/DirtReferrals-1.0.0.jar differ diff --git a/target/classes/com/dirtbagmc/dirtreferrals/AdminCommand.class b/target/classes/com/dirtbagmc/dirtreferrals/AdminCommand.class new file mode 100644 index 0000000..1556f4c Binary files /dev/null and b/target/classes/com/dirtbagmc/dirtreferrals/AdminCommand.class differ diff --git a/target/classes/com/dirtbagmc/dirtreferrals/DatabaseManager.class b/target/classes/com/dirtbagmc/dirtreferrals/DatabaseManager.class new file mode 100644 index 0000000..89b9cd5 Binary files /dev/null and b/target/classes/com/dirtbagmc/dirtreferrals/DatabaseManager.class differ diff --git a/target/classes/com/dirtbagmc/dirtreferrals/DirtReferralsPlugin.class b/target/classes/com/dirtbagmc/dirtreferrals/DirtReferralsPlugin.class new file mode 100644 index 0000000..2014575 Binary files /dev/null and b/target/classes/com/dirtbagmc/dirtreferrals/DirtReferralsPlugin.class differ diff --git a/target/classes/com/dirtbagmc/dirtreferrals/MessageManager.class b/target/classes/com/dirtbagmc/dirtreferrals/MessageManager.class new file mode 100644 index 0000000..50b9e70 Binary files /dev/null and b/target/classes/com/dirtbagmc/dirtreferrals/MessageManager.class differ diff --git a/target/classes/com/dirtbagmc/dirtreferrals/PlayerListener.class b/target/classes/com/dirtbagmc/dirtreferrals/PlayerListener.class new file mode 100644 index 0000000..0e26e8f Binary files /dev/null and b/target/classes/com/dirtbagmc/dirtreferrals/PlayerListener.class differ diff --git a/target/classes/com/dirtbagmc/dirtreferrals/ReferralCommand.class b/target/classes/com/dirtbagmc/dirtreferrals/ReferralCommand.class new file mode 100644 index 0000000..80198f7 Binary files /dev/null and b/target/classes/com/dirtbagmc/dirtreferrals/ReferralCommand.class differ diff --git a/target/classes/com/dirtbagmc/dirtreferrals/ReferralManager$PendingReferral.class b/target/classes/com/dirtbagmc/dirtreferrals/ReferralManager$PendingReferral.class new file mode 100644 index 0000000..1ee7ba6 Binary files /dev/null and b/target/classes/com/dirtbagmc/dirtreferrals/ReferralManager$PendingReferral.class differ diff --git a/target/classes/com/dirtbagmc/dirtreferrals/ReferralManager.class b/target/classes/com/dirtbagmc/dirtreferrals/ReferralManager.class new file mode 100644 index 0000000..420ef79 Binary files /dev/null and b/target/classes/com/dirtbagmc/dirtreferrals/ReferralManager.class differ diff --git a/target/classes/com/dirtbagmc/dirtreferrals/TopEntry.class b/target/classes/com/dirtbagmc/dirtreferrals/TopEntry.class new file mode 100644 index 0000000..6c52de2 Binary files /dev/null and b/target/classes/com/dirtbagmc/dirtreferrals/TopEntry.class differ diff --git a/target/classes/config.yml b/target/classes/config.yml new file mode 100644 index 0000000..6d25950 --- /dev/null +++ b/target/classes/config.yml @@ -0,0 +1,38 @@ +domain: + base: "dirtbagmc.com" + use-lowercase-playernames: true + +referral: + count-only-first-join: true + require-player-to-stay-online-minutes: 5 + block-self-referrals: true + block-same-ip: true + compare-against-referrer-known-ips: true + compare-against-other-referred-ips: true + store-ip-addresses: true + hash-ip-addresses: false + +rewards: + milestones: + "2": + commands: + - "say %referrer% reached 2 referrals!" + message: "&aYou reached 2 referrals and earned a reward!" + "5": + commands: + - "say %referrer% reached 5 referrals!" + message: "&aYou reached 5 referrals and earned a reward!" + "10": + commands: + - "say %referrer% reached 10 referrals!" + message: "&6You reached 10 referrals and earned a reward!" + +alerts: + notify-on-blocked-same-ip: true + notify-permission: "dirtreferrals.alerts" + +database: + file: "referrals.db" + +admin: + stats-page-size: 10 diff --git a/target/classes/messages.yml b/target/classes/messages.yml new file mode 100644 index 0000000..e39541f --- /dev/null +++ b/target/classes/messages.yml @@ -0,0 +1,26 @@ +prefix: "&6[DirtReferrals] " + +player-only: "&cPlayers only." +no-permission: "&cYou do not have permission." +reloaded: "&aDirtReferrals reloaded." +unknown-subcommand: "&cUnknown subcommand." +usage-admin: "&eUsage: /dirtreferrals " +usage-stats: "&eUsage: /dirtreferrals stats " +usage-give: "&eUsage: /dirtreferrals give " +usage-take: "&eUsage: /dirtreferrals take " + +referral-link: "&aHere is your referral IP! Have your friends join using it to receive rewards: &e%referral_ip%" +stats-header: "&6Referral stats for &e%player%&6:" +stats-count: "&eCount: &a%count%" +top-header: "&6Top Referrals:" +top-entry: "&e#%position% &f%player% &7- &a%count%" +no-data: "&cNo data found for that player." + +credit-added: "&aAdded &e%amount% &acredit to &e%player%&a." +credit-removed: "&aRemoved &e%amount% &acredit from &e%player%&a." +not-enough-credit: "&cThat player does not have enough referral credit." +invalid-number: "&cInvalid number." +blocked-same-ip: "&cReferral blocked due to same IP." +blocked-self-referral: "&cYou cannot refer yourself." +milestone-reward: "&aYou earned a referral reward for %amount% referrals!" +referral-counted: "&aReferral counted for &e%referrer%&a." diff --git a/target/classes/plugin.yml b/target/classes/plugin.yml new file mode 100644 index 0000000..d4cbb6b --- /dev/null +++ b/target/classes/plugin.yml @@ -0,0 +1,26 @@ +name: DirtReferrals +version: 1.0.0 +main: com.dirtbagmc.dirtreferrals.DirtReferralsPlugin +api-version: '1.21' +author: DirtBagMC +description: Referral tracking plugin with subdomain-based joins and milestone rewards + +commands: + refer: + description: Show your referral address + usage: /refer + aliases: [referrals] + dirtreferrals: + description: DirtReferrals admin commands + usage: /dirtreferrals + +permissions: + dirtreferrals.use: + description: Allows use of referral commands + default: true + dirtreferrals.admin: + description: Allows admin commands + default: op + dirtreferrals.alerts: + description: Receive blocked referral alerts + default: op diff --git a/target/maven-archiver/pom.properties b/target/maven-archiver/pom.properties new file mode 100644 index 0000000..4f60892 --- /dev/null +++ b/target/maven-archiver/pom.properties @@ -0,0 +1,5 @@ +#Generated by Maven +#Sat Jun 13 18:22:07 EDT 2026 +artifactId=DirtReferrals +groupId=com.dirtbagmc +version=1.0.0 diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..dd78400 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,9 @@ +com/dirtbagmc/dirtreferrals/AdminCommand.class +com/dirtbagmc/dirtreferrals/ReferralManager$PendingReferral.class +com/dirtbagmc/dirtreferrals/PlayerListener.class +com/dirtbagmc/dirtreferrals/DatabaseManager.class +com/dirtbagmc/dirtreferrals/MessageManager.class +com/dirtbagmc/dirtreferrals/ReferralCommand.class +com/dirtbagmc/dirtreferrals/TopEntry.class +com/dirtbagmc/dirtreferrals/ReferralManager.class +com/dirtbagmc/dirtreferrals/DirtReferralsPlugin.class diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..ea3599a --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,8 @@ +/home/bitnix/Desktop/DirtRewards/src/main/java/com/dirtbagmc/dirtreferrals/AdminCommand.java +/home/bitnix/Desktop/DirtRewards/src/main/java/com/dirtbagmc/dirtreferrals/DatabaseManager.java +/home/bitnix/Desktop/DirtRewards/src/main/java/com/dirtbagmc/dirtreferrals/DirtReferralsPlugin.java +/home/bitnix/Desktop/DirtRewards/src/main/java/com/dirtbagmc/dirtreferrals/MessageManager.java +/home/bitnix/Desktop/DirtRewards/src/main/java/com/dirtbagmc/dirtreferrals/PlayerListener.java +/home/bitnix/Desktop/DirtRewards/src/main/java/com/dirtbagmc/dirtreferrals/ReferralCommand.java +/home/bitnix/Desktop/DirtRewards/src/main/java/com/dirtbagmc/dirtreferrals/ReferralManager.java +/home/bitnix/Desktop/DirtRewards/src/main/java/com/dirtbagmc/dirtreferrals/TopEntry.java