Skip to content

Commit d74b07f

Browse files
committed
Add a dedicated player page
1 parent 823ee61 commit d74b07f

23 files changed

Lines changed: 1619 additions & 327 deletions

.claude/settings.local.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"permissions": {
3+
"additionalDirectories": [
4+
"C:\\Users\\telesphoreo\\IdeaProjects\\Plex"
5+
]
6+
}
7+
}

build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ plugins {
66
}
77

88
group = "dev.plex"
9-
version = "1.6"
9+
version = "1.7"
1010
description = "Module-HTTPD"
1111

1212
repositories {
@@ -27,7 +27,7 @@ dependencies {
2727
implementation("org.projectlombok:lombok:1.18.46")
2828
annotationProcessor("org.projectlombok:lombok:1.18.46")
2929
compileOnly("io.papermc.paper:paper-api:26.1.2.build.+")
30-
implementation("dev.plex:server:1.6")
30+
implementation("dev.plex:server:1.7-SNAPSHOT")
3131
implementation("org.json:json:20251224")
3232
implementation("org.reflections:reflections:0.10.2")
3333
plexLibrary("org.eclipse.jetty:jetty-server:12.1.9")

src/main/java/dev/plex/HTTPDModule.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
import dev.plex.module.PlexModule;
88
import dev.plex.ratelimit.RateLimitFilter;
99
import dev.plex.request.AbstractServlet;
10+
import dev.plex.request.PlayerActionServlet;
11+
import dev.plex.request.PlayersStreamServlet;
1012
import dev.plex.request.SchematicUploadServlet;
13+
import dev.plex.request.StaffPlayersStreamServlet;
14+
import dev.plex.request.StatsStreamServlet;
1115
import dev.plex.request.impl.*;
1216
import dev.plex.util.PlexLog;
1317
import jakarta.servlet.DispatcherType;
@@ -93,20 +97,28 @@ public void enable()
9397

9498
context.addFilter(new FilterHolder(new RateLimitFilter()), "/*", EnumSet.of(DispatcherType.REQUEST));
9599

100+
StatsBroadcaster.get().start();
101+
PlayersBroadcaster.get().start();
102+
96103
new IndefBansEndpoint();
97104
new IndexEndpoint();
98105
new ListEndpoint();
99106
new PunishmentsEndpoint();
100107
new CommandsEndpoint();
101108
new SchematicDownloadEndpoint();
102109
new SchematicUploadEndpoint();
103-
new StatsEndpoint();
104110
new PlayersEndpoint();
111+
new PlayerAdminEndpoint();
105112
new AssetsEndpoint();
106113
new PunishmentsUIEndpoint();
107114
new IndefBansUIEndpoint();
108115
new AuthenticationEndpoint();
109116

117+
HTTPDModule.context.addServlet(StatsStreamServlet.class, "/api/stats/stream");
118+
HTTPDModule.context.addServlet(PlayersStreamServlet.class, "/api/players/stream");
119+
HTTPDModule.context.addServlet(StaffPlayersStreamServlet.class, "/api/players/stream/staff");
120+
HTTPDModule.context.addServlet(PlayerActionServlet.class, "/api/admin/action");
121+
110122
ServletHolder uploadHolder = HTTPDModule.context.addServlet(SchematicUploadServlet.class, "/api/schematics/uploading");
111123

112124
File uploadLoc = new File(System.getProperty("java.io.tmpdir"), "schematic-temp-dir");
@@ -139,6 +151,22 @@ public void disable()
139151
{
140152
PlexLog.debug("Stopping Jetty server");
141153
try
154+
{
155+
StatsBroadcaster.get().shutdown();
156+
}
157+
catch (Throwable t)
158+
{
159+
t.printStackTrace();
160+
}
161+
try
162+
{
163+
PlayersBroadcaster.get().shutdown();
164+
}
165+
catch (Throwable t)
166+
{
167+
t.printStackTrace();
168+
}
169+
try
142170
{
143171
atomicServer.get().stop();
144172
atomicServer.get().destroy();
Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,10 @@
11
package dev.plex.authentication;
22

3-
import lombok.Data;
43
import lombok.experimental.Accessors;
54

65
import java.time.Instant;
76

8-
@Data
97
@Accessors(fluent = true)
10-
public class AuthenticatedUser
11-
{
12-
private final int userId;
13-
private final String username;
14-
private final boolean staff;
15-
private final UserType userType;
16-
private final String accessToken;
17-
private final Instant accessTokenExpiresAt;
18-
private final Instant authenticatedAt;
8+
public record AuthenticatedUser(int userId, String username, boolean staff, UserType userType, String accessToken,
9+
Instant accessTokenExpiresAt, Instant authenticatedAt) {
1910
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package dev.plex.request;
2+
3+
import dev.plex.Plex;
4+
import dev.plex.authentication.AuthenticatedUser;
5+
import dev.plex.cache.DataUtils;
6+
import dev.plex.logging.Log;
7+
import dev.plex.player.PlexPlayer;
8+
import dev.plex.punishment.Punishment;
9+
import dev.plex.punishment.PunishmentType;
10+
import dev.plex.util.BungeeUtil;
11+
import dev.plex.util.TimeUtils;
12+
import jakarta.servlet.ServletException;
13+
import jakarta.servlet.http.HttpServlet;
14+
import jakarta.servlet.http.HttpServletRequest;
15+
import jakarta.servlet.http.HttpServletResponse;
16+
import org.bukkit.Bukkit;
17+
import org.bukkit.entity.Player;
18+
19+
import java.io.IOException;
20+
import java.time.ZoneId;
21+
import java.time.ZonedDateTime;
22+
import java.util.List;
23+
import java.util.UUID;
24+
25+
public class PlayerActionServlet extends HttpServlet
26+
{
27+
private static final long FAR_FUTURE_DAYS = 365L * 50L;
28+
private static final List<String> PERMANENT_ACTIONS = List.of("ban", "mute");
29+
private static final List<String> TEMP_ACTIONS = List.of("tempban", "tempmute", "freeze");
30+
31+
@Override
32+
protected void doPost(HttpServletRequest request, HttpServletResponse response)
33+
throws ServletException, IOException
34+
{
35+
AuthenticatedUser staff = AbstractServlet.currentStaff(request);
36+
if (staff == null)
37+
{
38+
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
39+
response.getWriter().write("Not authorized.");
40+
return;
41+
}
42+
43+
String uuidStr = request.getParameter("uuid");
44+
String action = request.getParameter("action");
45+
String reason = request.getParameter("reason");
46+
String durationStr = request.getParameter("duration");
47+
48+
if (uuidStr == null || action == null)
49+
{
50+
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
51+
response.getWriter().write("Missing parameters.");
52+
return;
53+
}
54+
if (!PERMANENT_ACTIONS.contains(action) && !TEMP_ACTIONS.contains(action))
55+
{
56+
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
57+
response.getWriter().write("Unknown action.");
58+
return;
59+
}
60+
61+
UUID uuid;
62+
try
63+
{
64+
uuid = UUID.fromString(uuidStr);
65+
}
66+
catch (IllegalArgumentException e)
67+
{
68+
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
69+
response.getWriter().write("Bad UUID.");
70+
return;
71+
}
72+
73+
PlexPlayer target = DataUtils.getPlayer(uuid);
74+
if (target == null)
75+
{
76+
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
77+
response.getWriter().write("Player not found.");
78+
return;
79+
}
80+
81+
String safeReason = (reason == null || reason.isBlank()) ? "No reason provided" : reason.trim();
82+
if (safeReason.length() > 500) safeReason = safeReason.substring(0, 500);
83+
84+
PunishmentType type = mapType(action);
85+
ZonedDateTime now = ZonedDateTime.now(ZoneId.of(TimeUtils.TIMEZONE));
86+
ZonedDateTime endDate = TEMP_ACTIONS.contains(action)
87+
? now.plusSeconds(parseDurationSeconds(durationStr))
88+
: now.plusDays(FAR_FUTURE_DAYS);
89+
90+
Punishment punishment = new Punishment(uuid, null);
91+
punishment.setType(type);
92+
punishment.setReason(safeReason);
93+
punishment.setPunishedUsername(target.getName());
94+
punishment.setPunisherName("xf:" + staff.username());
95+
punishment.setEndDate(endDate);
96+
punishment.setCustomTime(TEMP_ACTIONS.contains(action));
97+
punishment.setActive(true);
98+
List<String> ips = target.getIps();
99+
if (ips != null && !ips.isEmpty()) punishment.setIp(ips.getLast());
100+
101+
String ipAddress = request.getRemoteAddr();
102+
if ("127.0.0.1".equals(ipAddress))
103+
{
104+
String forwarded = request.getHeader("X-FORWARDED-FOR");
105+
if (forwarded != null) ipAddress = forwarded;
106+
}
107+
Log.log(ipAddress + " (xf:" + staff.username() + ") issued " + action + " on " + target.getName() + " (" + uuid + ")");
108+
109+
final boolean kick = action.equals("ban") || action.equals("tempban");
110+
final Punishment toApply = punishment;
111+
Bukkit.getScheduler().runTask(Plex.get(), () ->
112+
{
113+
try
114+
{
115+
Plex.get().getPunishmentManager().punish(target, toApply);
116+
}
117+
catch (Throwable t)
118+
{
119+
t.printStackTrace();
120+
return;
121+
}
122+
if (kick)
123+
{
124+
Player online = Bukkit.getPlayer(uuid);
125+
if (online != null)
126+
{
127+
try { BungeeUtil.kickPlayer(online, Punishment.generateBanMessage(toApply)); }
128+
catch (Throwable t) { t.printStackTrace(); }
129+
}
130+
}
131+
});
132+
133+
response.sendRedirect("/player/" + uuid);
134+
}
135+
136+
private static PunishmentType mapType(String action)
137+
{
138+
return switch (action)
139+
{
140+
case "ban" -> PunishmentType.BAN;
141+
case "tempban" -> PunishmentType.TEMPBAN;
142+
case "mute", "tempmute" -> PunishmentType.MUTE;
143+
case "freeze" -> PunishmentType.FREEZE;
144+
default -> throw new IllegalArgumentException("unknown action: " + action);
145+
};
146+
}
147+
148+
private static long parseDurationSeconds(String s)
149+
{
150+
if (s == null || s.length() < 2) return 24L * 3600L;
151+
char unit = s.charAt(s.length() - 1);
152+
long n;
153+
try { n = Long.parseLong(s.substring(0, s.length() - 1)); }
154+
catch (NumberFormatException e) { return 24L * 3600L; }
155+
if (n <= 0) return 24L * 3600L;
156+
return switch (unit)
157+
{
158+
case 'm' -> Math.min(n, 60L * 24L * 365L) * 60L;
159+
case 'h' -> Math.min(n, 24L * 365L) * 3600L;
160+
case 'd' -> Math.min(n, 365L * 50L) * 86400L;
161+
default -> 24L * 3600L;
162+
};
163+
}
164+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package dev.plex.request;
2+
3+
import dev.plex.logging.Log;
4+
import dev.plex.request.impl.PlayersBroadcaster;
5+
import jakarta.servlet.AsyncContext;
6+
import jakarta.servlet.AsyncEvent;
7+
import jakarta.servlet.AsyncListener;
8+
import jakarta.servlet.ServletException;
9+
import jakarta.servlet.http.HttpServlet;
10+
import jakarta.servlet.http.HttpServletRequest;
11+
import jakarta.servlet.http.HttpServletResponse;
12+
13+
import java.io.IOException;
14+
import java.io.PrintWriter;
15+
16+
public class PlayersStreamServlet extends HttpServlet
17+
{
18+
@Override
19+
protected void doGet(HttpServletRequest request, HttpServletResponse response)
20+
throws ServletException, IOException
21+
{
22+
String ipAddress = request.getRemoteAddr();
23+
if ("127.0.0.1".equals(ipAddress))
24+
{
25+
String forwarded = request.getHeader("X-FORWARDED-FOR");
26+
if (forwarded != null) ipAddress = forwarded;
27+
}
28+
Log.log(ipAddress + " opened SSE stream /api/players/stream");
29+
30+
PlayersBroadcaster broadcaster = PlayersBroadcaster.get();
31+
if (broadcaster.atCapacity())
32+
{
33+
response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
34+
response.setHeader("Retry-After", "30");
35+
return;
36+
}
37+
38+
response.setStatus(HttpServletResponse.SC_OK);
39+
response.setContentType("text/event-stream");
40+
response.setCharacterEncoding("UTF-8");
41+
response.setHeader("Cache-Control", "no-cache, no-transform");
42+
response.setHeader("Connection", "keep-alive");
43+
response.setHeader("X-Accel-Buffering", "no");
44+
45+
final AsyncContext ctx = request.startAsync();
46+
ctx.setTimeout(0L);
47+
ctx.addListener(new AsyncListener()
48+
{
49+
@Override public void onComplete(AsyncEvent event) { broadcaster.removeSubscriber(ctx); }
50+
@Override public void onTimeout(AsyncEvent event) { broadcaster.removeSubscriber(ctx); }
51+
@Override public void onError(AsyncEvent event) { broadcaster.removeSubscriber(ctx); }
52+
@Override public void onStartAsync(AsyncEvent event) {}
53+
});
54+
55+
PrintWriter writer;
56+
try
57+
{
58+
writer = response.getWriter();
59+
}
60+
catch (IOException e)
61+
{
62+
ctx.complete();
63+
return;
64+
}
65+
66+
if (!broadcaster.addSubscriber(ctx, writer, false))
67+
{
68+
response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
69+
ctx.complete();
70+
return;
71+
}
72+
73+
try
74+
{
75+
writer.write("retry: 5000\n\n");
76+
writer.write("data: ");
77+
writer.write(broadcaster.currentPayload(false));
78+
writer.write("\n\n");
79+
writer.flush();
80+
if (writer.checkError())
81+
{
82+
broadcaster.removeSubscriber(ctx);
83+
ctx.complete();
84+
}
85+
}
86+
catch (Throwable t)
87+
{
88+
broadcaster.removeSubscriber(ctx);
89+
try { ctx.complete(); } catch (Throwable ignored) {}
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)