Skip to content

Commit e0fa581

Browse files
authored
feat: added name history /bmnames (#1032)
* feat: added name history /bmnames fixes #980 fixes #895
1 parent 5d2a264 commit e0fa581

File tree

35 files changed

+866
-1824
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

35 files changed

+866
-1824
lines changed

bukkit/src/main/java/me/confuser/banmanager/bukkit/BukkitServer.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import me.confuser.banmanager.common.util.ColorUtils;
1010
import me.confuser.banmanager.common.util.Message;
1111
import org.bukkit.Bukkit;
12+
import org.bukkit.ChatColor;
1213
import org.bukkit.World;
1314
import org.bukkit.command.BlockCommandSender;
1415
import org.bukkit.command.CommandSender;
@@ -87,7 +88,7 @@ public void broadcast(String message, String permission, CommonSender sender) {
8788
}
8889

8990
public static String formatMessage(String message) {
90-
return ColorUtils.toDownsampledLegacy(message);
91+
return ChatColor.translateAlternateColorCodes('&', ColorUtils.toDownsampledLegacy(message));
9192
}
9293

9394
@Override

common/src/main/java/me/confuser/banmanager/common/BanManagerPlugin.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ public CommonCommand[] getCommands() {
421421
new ExportCommand(this),
422422
new FindAltsCommand(this),
423423
new InfoCommand(this),
424+
new NamesCommand(this),
424425
new ImportCommand(this),
425426
new KickCommand(this),
426427
new KickAllCommand(this),

common/src/main/java/me/confuser/banmanager/common/api/BmAPI.java

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import me.confuser.banmanager.common.util.UUIDUtils;
1212

1313
import java.sql.SQLException;
14+
import java.util.ArrayList;
1415
import java.util.List;
1516
import java.util.UUID;
1617

@@ -487,6 +488,57 @@ public static CloseableIterator<PlayerWarnData> getWarnings(PlayerData player) t
487488
return BanManagerPlugin.getInstance().getPlayerWarnStorage().getWarnings(player);
488489
}
489490

491+
/**
492+
* Get all known names for a player (summary view with first/last seen).
493+
* This method is not thread safe and should not be called on the main thread.
494+
*
495+
* @param uuid Player UUID
496+
* @return List of PlayerNameSummary ordered by lastSeen descending (most recent first)
497+
* @throws SQLException
498+
*/
499+
public static List<PlayerNameSummary> getPlayerNames(UUID uuid) throws SQLException {
500+
PlayerData player = getPlayer(uuid);
501+
502+
if (player == null) return new ArrayList<>();
503+
504+
return BanManagerPlugin.getInstance().getPlayerHistoryStorage().getNamesSummary(player);
505+
}
506+
507+
/**
508+
* Get the full session history for a player.
509+
* This method is not thread safe and should not be called on the main thread.
510+
*
511+
* @param uuid Player UUID
512+
* @param since Unix timestamp in seconds to get sessions since
513+
* @param page Page number (0-indexed, 10 results per page)
514+
* @return Iterator of PlayerHistoryData ordered by join descending (most recent first), null if player not found
515+
* @throws SQLException
516+
*/
517+
public static CloseableIterator<PlayerHistoryData> getPlayerHistory(UUID uuid, long since, int page) throws SQLException {
518+
PlayerData player = getPlayer(uuid);
519+
520+
if (player == null) return null;
521+
522+
return BanManagerPlugin.getInstance().getPlayerHistoryStorage().getSince(player, since, page);
523+
}
524+
525+
/**
526+
* Get the name a player was using at a specific timestamp.
527+
* This method is not thread safe and should not be called on the main thread.
528+
*
529+
* @param uuid Player UUID
530+
* @param timestamp Unix timestamp in seconds
531+
* @return The name at that time, or null if not found
532+
* @throws SQLException
533+
*/
534+
public static String getPlayerNameAt(UUID uuid, long timestamp) throws SQLException {
535+
PlayerData player = getPlayer(uuid);
536+
537+
if (player == null) return null;
538+
539+
return BanManagerPlugin.getInstance().getPlayerHistoryStorage().getNameAt(player, timestamp);
540+
}
541+
490542
/**
491543
* @param key The message config node within messages.yml, e.g. "ban.notify"
492544
* @return String
@@ -503,4 +555,3 @@ public static long toTimestamp(String time, boolean future) throws Exception {
503555
return DateUtils.parseDateDiff(time, future);
504556
}
505557
}
506-

common/src/main/java/me/confuser/banmanager/common/commands/InfoCommand.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import me.confuser.banmanager.common.kyori.text.Component;
99
import me.confuser.banmanager.common.kyori.text.TextComponent;
1010
import me.confuser.banmanager.common.kyori.text.event.ClickEvent;
11+
import me.confuser.banmanager.common.kyori.text.format.NamedTextColor;
1112
import me.confuser.banmanager.common.maxmind.db.model.CountryResponse;
1213
import me.confuser.banmanager.common.ormlite.dao.CloseableIterator;
1314
import me.confuser.banmanager.common.util.DateUtils;
@@ -478,6 +479,31 @@ public void playerInfo(CommonSender sender, String name, Integer index, InfoComm
478479
}
479480
}
480481

482+
if (sender.hasPermission("bm.command.bminfo.names")) {
483+
try {
484+
List<PlayerNameSummary> playerNames = getPlugin().getPlayerHistoryStorage().getNamesSummary(player);
485+
486+
if (!playerNames.isEmpty()) {
487+
messages.add(Message.get("names.header").set("player", player.getName()).toString());
488+
489+
String dateTimeFormat = Message.getString("names.dateTimeFormat");
490+
491+
if (!sender.isConsole()) {
492+
messages.add(buildNamesComponent(playerNames, dateTimeFormat));
493+
} else {
494+
StringBuilder namesBuilder = new StringBuilder();
495+
for (PlayerNameSummary nameData : playerNames) {
496+
namesBuilder.append(nameData.getName()).append(", ");
497+
}
498+
if (namesBuilder.length() >= 2) namesBuilder.setLength(namesBuilder.length() - 2);
499+
messages.add(namesBuilder.toString());
500+
}
501+
}
502+
} catch (SQLException e) {
503+
e.printStackTrace();
504+
}
505+
}
506+
481507
if (sender.hasPermission("bm.command.bminfo.ipstats")) {
482508

483509
long ipBanTotal = getPlugin().getIpBanRecordStorage().getCount(player.getIp());
@@ -618,4 +644,31 @@ private void handleIpHistory(ArrayList<Object> messages, PlayerData player, long
618644
if (iterator != null) iterator.closeQuietly();
619645
}
620646
}
647+
648+
private TextComponent buildNamesComponent(List<PlayerNameSummary> names, String dateTimeFormat) {
649+
TextComponent.Builder message = Component.text();
650+
int index = 0;
651+
652+
for (PlayerNameSummary nameData : names) {
653+
String hoverText = Message.get("names.row")
654+
.set("name", nameData.getName())
655+
.set("firstSeen", DateUtils.format(dateTimeFormat, nameData.getFirstSeen()))
656+
.set("lastSeen", DateUtils.format(dateTimeFormat, nameData.getLastSeen()))
657+
.toString();
658+
659+
message.append(
660+
Component.text(nameData.getName())
661+
.color(NamedTextColor.YELLOW)
662+
.clickEvent(ClickEvent.runCommand("/bminfo " + nameData.getName()))
663+
.hoverEvent(Component.text(hoverText)));
664+
665+
if (index != names.size() - 1) {
666+
message.append(Component.text(", ").color(NamedTextColor.GRAY));
667+
}
668+
669+
index++;
670+
}
671+
672+
return message.build();
673+
}
621674
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package me.confuser.banmanager.common.commands;
2+
3+
import me.confuser.banmanager.common.BanManagerPlugin;
4+
import me.confuser.banmanager.common.CommonPlayer;
5+
import me.confuser.banmanager.common.data.PlayerData;
6+
import me.confuser.banmanager.common.data.PlayerNameSummary;
7+
import me.confuser.banmanager.common.kyori.text.Component;
8+
import me.confuser.banmanager.common.kyori.text.TextComponent;
9+
import me.confuser.banmanager.common.kyori.text.event.ClickEvent;
10+
import me.confuser.banmanager.common.kyori.text.format.NamedTextColor;
11+
import me.confuser.banmanager.common.util.DateUtils;
12+
import me.confuser.banmanager.common.util.Message;
13+
14+
import java.sql.SQLException;
15+
import java.util.List;
16+
17+
public class NamesCommand extends CommonCommand {
18+
19+
public NamesCommand(BanManagerPlugin plugin) {
20+
super(plugin, "bmnames", true);
21+
}
22+
23+
@Override
24+
public boolean onCommand(final CommonSender sender, CommandParser parser) {
25+
if (parser.args.length < 1) {
26+
return false;
27+
}
28+
29+
final String playerName = parser.args[0];
30+
31+
if (playerName.length() > 16) {
32+
Message message = Message.get("sender.error.invalidPlayer");
33+
message.set("player", playerName);
34+
sender.sendMessage(message.toString());
35+
return true;
36+
}
37+
38+
getPlugin().getScheduler().runAsync(() -> {
39+
PlayerData player = getPlugin().getPlayerStorage().retrieve(playerName, true);
40+
41+
if (player == null) {
42+
sender.sendMessage(Message.get("sender.error.notFound").set("player", playerName).toString());
43+
return;
44+
}
45+
46+
List<PlayerNameSummary> names;
47+
try {
48+
names = getPlugin().getPlayerHistoryStorage().getNamesSummary(player);
49+
} catch (SQLException e) {
50+
sender.sendMessage(Message.get("sender.error.exception").toString());
51+
e.printStackTrace();
52+
return;
53+
}
54+
55+
sender.sendMessage(Message.get("names.header").set("player", player.getName()).toString());
56+
57+
if (names.isEmpty()) {
58+
sender.sendMessage(Message.get("names.none").toString());
59+
return;
60+
}
61+
62+
String dateTimeFormat = Message.getString("names.dateTimeFormat");
63+
64+
if (!sender.isConsole()) {
65+
((CommonPlayer) sender).sendJSONMessage(buildNamesComponent(names, dateTimeFormat));
66+
} else {
67+
for (PlayerNameSummary nameData : names) {
68+
sender.sendMessage(Message.get("names.row")
69+
.set("name", nameData.getName())
70+
.set("firstSeen", DateUtils.format(dateTimeFormat, nameData.getFirstSeen()))
71+
.set("lastSeen", DateUtils.format(dateTimeFormat, nameData.getLastSeen()))
72+
.toString());
73+
}
74+
}
75+
});
76+
77+
return true;
78+
}
79+
80+
private TextComponent buildNamesComponent(List<PlayerNameSummary> names, String dateTimeFormat) {
81+
TextComponent.Builder message = Component.text();
82+
int index = 0;
83+
84+
for (PlayerNameSummary nameData : names) {
85+
String hoverText = Message.get("names.row")
86+
.set("name", nameData.getName())
87+
.set("firstSeen", DateUtils.format(dateTimeFormat, nameData.getFirstSeen()))
88+
.set("lastSeen", DateUtils.format(dateTimeFormat, nameData.getLastSeen()))
89+
.toString();
90+
91+
message.append(
92+
Component.text(nameData.getName())
93+
.color(NamedTextColor.YELLOW)
94+
.clickEvent(ClickEvent.runCommand("/bminfo " + nameData.getName()))
95+
.hoverEvent(Component.text(hoverText)));
96+
97+
if (index != names.size() - 1) {
98+
message.append(Component.text(", ").color(NamedTextColor.GRAY));
99+
}
100+
101+
index++;
102+
}
103+
104+
return message.build();
105+
}
106+
}

common/src/main/java/me/confuser/banmanager/common/data/PlayerHistoryData.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,43 @@ public class PlayerHistoryData {
1414
@Getter
1515
@DatabaseField(generatedId = true)
1616
private int id;
17+
1718
@Getter
1819
@DatabaseField(canBeNull = false, foreign = true, persisterClass = ByteArray.class, columnDefinition = "BINARY(16) NOT NULL")
1920
private PlayerData player;
21+
22+
@Getter
23+
@DatabaseField(index = true, width = 16, columnDefinition = "VARCHAR(16) NOT NULL")
24+
private String name;
25+
2026
@Getter
21-
@DatabaseField(index = true, persisterClass = IpAddress.class, canBeNull = false, columnDefinition = "VARBINARY(16) NOT NULL")
27+
@DatabaseField(index = true, persisterClass = IpAddress.class, canBeNull = true, columnDefinition = "VARBINARY(16)")
2228
private IPAddress ip;
2329

24-
// Should always be database time
2530
@Getter
31+
@Setter
2632
@DatabaseField(index = true, columnDefinition = "BIGINT UNSIGNED NOT NULL")
27-
private long join = System.currentTimeMillis() / 1000L;
33+
private long join = 0L;
34+
2835
@Getter
2936
@Setter
3037
@DatabaseField(index = true, columnDefinition = "BIGINT UNSIGNED NOT NULL")
31-
private long leave = System.currentTimeMillis() / 1000L;
38+
private long leave = 0L;
3239

3340
PlayerHistoryData() {
34-
3541
}
3642

37-
public PlayerHistoryData(PlayerData player) {
43+
/**
44+
* Create a session history record.
45+
* Join time is set to the current time when the session is created.
46+
*
47+
* @param player The player data
48+
* @param logIp Whether to record the IP address (false = ip will be null)
49+
*/
50+
public PlayerHistoryData(PlayerData player, boolean logIp) {
3851
this.player = player;
39-
this.ip = player.getIp();
52+
this.name = player.getName();
53+
this.ip = logIp ? player.getIp() : null;
54+
this.join = System.currentTimeMillis() / 1000L;
4055
}
4156
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package me.confuser.banmanager.common.data;
2+
3+
import lombok.Getter;
4+
5+
/**
6+
* Summary of a player's usage of a particular name.
7+
* Aggregated from interval history: firstSeen = min(fromSeen), lastSeen = max(toSeen or current time).
8+
*/
9+
public class PlayerNameSummary {
10+
11+
@Getter
12+
private final String name;
13+
14+
@Getter
15+
private final long firstSeen;
16+
17+
@Getter
18+
private final long lastSeen;
19+
20+
public PlayerNameSummary(String name, long firstSeen, long lastSeen) {
21+
this.name = name;
22+
this.firstSeen = firstSeen;
23+
this.lastSeen = lastSeen;
24+
}
25+
}

common/src/main/java/me/confuser/banmanager/common/listeners/CommonJoinListener.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,11 @@ public void onPreJoin(UUID id, String name, IPAddress address) {
252252
return;
253253
}
254254

255-
if (plugin.getConfig().isLogIpsEnabled()) plugin.getPlayerHistoryStorage().create(player);
255+
try {
256+
plugin.getPlayerHistoryStorage().startSession(player, plugin.getConfig().isLogIpsEnabled());
257+
} catch (SQLException e) {
258+
e.printStackTrace();
259+
}
256260
}
257261

258262
public void onJoin(final CommonPlayer player) {

common/src/main/java/me/confuser/banmanager/common/listeners/CommonLeaveListener.java

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package me.confuser.banmanager.common.listeners;
22

33
import me.confuser.banmanager.common.BanManagerPlugin;
4-
import me.confuser.banmanager.common.data.PlayerHistoryData;
54
import me.confuser.banmanager.common.data.PlayerMuteData;
65

76
import java.sql.SQLException;
@@ -15,20 +14,15 @@ public CommonLeaveListener(BanManagerPlugin plugin) {
1514
}
1615

1716
public void onLeave(UUID id, String name) {
18-
if (plugin.getConfig().isLogIpsEnabled()) {
19-
final PlayerHistoryData data = plugin.getPlayerHistoryStorage().remove(id);
20-
21-
if (data == null) {
22-
plugin.getLogger().warning("Could not find " + name + " session history, perhaps they " +
23-
"disconnected too quickly?");
24-
return;
25-
}
26-
27-
data.setLeave(System.currentTimeMillis() / 1000L);
17+
final Integer sessionId = plugin.getPlayerHistoryStorage().removeSession(id);
2818

19+
if (sessionId == null) {
20+
plugin.getLogger().warning("Could not find " + name + " session history, perhaps they " +
21+
"disconnected too quickly?");
22+
} else {
2923
plugin.getScheduler().runAsync(() -> {
3024
try {
31-
plugin.getPlayerHistoryStorage().create(data);
25+
plugin.getPlayerHistoryStorage().endSessionById(sessionId);
3226
} catch (SQLException e) {
3327
e.printStackTrace();
3428
}

0 commit comments

Comments
 (0)