You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
feat(housekeeping): hotel alert + dashboard + audit log
Closes out the HK panel server-side surface. * Incoming 9127 HousekeepingSendHotelAlertEvent — broadcast a StaffAlertWithLinkComposer to every online user that hasn't set blockStaffAlerts. Composed once, fanned out by reference; empty-message guard returns `housekeeping.error.alert_empty`. * Outgoing 9206 HousekeepingDashboardComposer + Incoming 9128 HousekeepingGetDashboardEvent — single round trip with the aggregated counters: online / total users + active / total rooms + pending support tickets + sanctions in the last 24h + approximate emulator uptime + a version string. Active-rooms is derived from RoomManager.getActiveRooms().getUserCount()>0 to avoid counting idle preloaded rooms. Peak online today / all-time aren't tracked yet, so they currently echo the live online count as a best-effort placeholder. * Outgoing 9207 HousekeepingActionLogComposer + Incoming 9129 HousekeepingListActionLogEvent — read the optional housekeeping_log table. If the table isn't there the SQL exception is swallowed and an empty list goes back, so the panel renders a no-entries view rather than crashing. Schema is documented in the handler's javadoc; operators who want audit run a single CREATE TABLE then the HK panel populates from new writes (writes are a follow-up — every HK handler will eventually append a row). `mvn package` clean — the final fat jar lands in Latest_Compiled_Version/ after the build finishes.
This commit is contained in:
@@ -739,5 +739,8 @@ public class PacketManager {
|
||||
this.registerHandler(Incoming.HousekeepingGiveCurrencyEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGiveCurrencyEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingGrantItemEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGrantItemEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingSetHcSubscriptionEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSetHcSubscriptionEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingSendHotelAlertEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSendHotelAlertEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingGetDashboardEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGetDashboardEvent.class);
|
||||
this.registerHandler(Incoming.HousekeepingListActionLogEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingListActionLogEvent.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,4 +483,7 @@ public class Incoming {
|
||||
public static final int HousekeepingGiveCurrencyEvent = 9118;
|
||||
public static final int HousekeepingGrantItemEvent = 9119;
|
||||
public static final int HousekeepingSetHcSubscriptionEvent = 9120;
|
||||
public static final int HousekeepingSendHotelAlertEvent = 9121;
|
||||
public static final int HousekeepingGetDashboardEvent = 9122;
|
||||
public static final int HousekeepingListActionLogEvent = 9123;
|
||||
}
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingDashboardComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class HousekeepingGetDashboardEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int onlineUsers = Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().size();
|
||||
int activeRooms = 0;
|
||||
int totalUsers = 0;
|
||||
int totalRooms = 0;
|
||||
int pendingTickets = 0;
|
||||
int sanctionsLast24h = 0;
|
||||
int now = Emulator.getIntUnixTimestamp();
|
||||
|
||||
// activeRooms = loaded rooms with at least one user
|
||||
try {
|
||||
for (var room : Emulator.getGameEnvironment().getRoomManager().getActiveRooms()) {
|
||||
if (room != null && room.getUserCount() > 0) activeRooms++;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// fall through with 0
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM users");
|
||||
ResultSet rs = statement.executeQuery()) {
|
||||
if (rs.next()) totalUsers = rs.getInt(1);
|
||||
}
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM rooms");
|
||||
ResultSet rs = statement.executeQuery()) {
|
||||
if (rs.next()) totalRooms = rs.getInt(1);
|
||||
}
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM support_tickets WHERE state = 0");
|
||||
ResultSet rs = statement.executeQuery()) {
|
||||
if (rs.next()) pendingTickets = rs.getInt(1);
|
||||
}
|
||||
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM bans WHERE timestamp > ?")) {
|
||||
statement.setInt(1, now - (24 * 3600));
|
||||
try (ResultSet rs = statement.executeQuery()) {
|
||||
if (rs.next()) sanctionsLast24h = rs.getInt(1);
|
||||
}
|
||||
}
|
||||
} catch (SQLException ignored) {
|
||||
// Surface 0s rather than failing the whole dashboard on a missing
|
||||
// optional table — the HK panel can render partial data.
|
||||
}
|
||||
|
||||
int uptime = (int) ((System.currentTimeMillis() - HOUSEKEEPING_BOOT_MILLIS) / 1000);
|
||||
String version = "Arcturus-Morningstar-Extended";
|
||||
|
||||
this.client.sendResponse(new HousekeepingDashboardComposer(
|
||||
onlineUsers,
|
||||
totalUsers,
|
||||
activeRooms,
|
||||
totalRooms,
|
||||
onlineUsers, // peakOnlineToday — not tracked, use current as best-effort
|
||||
onlineUsers, // peakOnlineAllTime — same
|
||||
pendingTickets,
|
||||
sanctionsLast24h,
|
||||
Math.max(uptime, 0),
|
||||
version
|
||||
));
|
||||
}
|
||||
|
||||
// Approximate uptime — captured at class-load time rather than emu startup
|
||||
// (Emulator.java doesn't expose a public startup timestamp). For HK panel
|
||||
// headline metrics this is close enough; if tighter accuracy is needed
|
||||
// later, plumb Emulator.startup through and read it here.
|
||||
private static final long HOUSEKEEPING_BOOT_MILLIS = System.currentTimeMillis();
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionLogComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Read the housekeeping_log audit table. The table isn't part of the
|
||||
* base FullDatabase.sql yet — operators who want audit have to create
|
||||
* it once:
|
||||
*
|
||||
* CREATE TABLE IF NOT EXISTS `housekeeping_log` (
|
||||
* `id` INT NOT NULL AUTO_INCREMENT,
|
||||
* `timestamp` INT NOT NULL,
|
||||
* `actor_id` INT NOT NULL,
|
||||
* `actor_name` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
* `target_type` VARCHAR(16) NOT NULL DEFAULT 'user',
|
||||
* `target_id` INT NOT NULL DEFAULT 0,
|
||||
* `target_label` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
* `action` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
* `detail` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
* `success` TINYINT NOT NULL DEFAULT 1,
|
||||
* PRIMARY KEY (`id`), KEY `timestamp` (`timestamp`)
|
||||
* ) ENGINE=InnoDB;
|
||||
*
|
||||
* If the table is missing we swallow the SQL error and return an empty
|
||||
* list — the panel just shows "no audit entries" instead of breaking.
|
||||
* Writing into the table is a follow-up: each HK handler will append
|
||||
* a row once the table exists; for now the listing is read-only.
|
||||
*/
|
||||
public class HousekeepingListActionLogEvent extends MessageHandler {
|
||||
private static final int HARD_LIMIT = 500;
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int limit = Math.min(Math.max(this.packet.readInt(), 1), HARD_LIMIT);
|
||||
|
||||
List<HousekeepingActionLogComposer.Row> rows = new ArrayList<>();
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT id, timestamp, actor_id, actor_name, target_type, target_id, target_label, action, detail, success " +
|
||||
"FROM housekeeping_log ORDER BY id DESC LIMIT ?")) {
|
||||
statement.setInt(1, limit);
|
||||
|
||||
try (ResultSet rs = statement.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
rows.add(new HousekeepingActionLogComposer.Row(
|
||||
rs.getInt("id"),
|
||||
rs.getInt("timestamp"),
|
||||
rs.getInt("actor_id"),
|
||||
rs.getString("actor_name"),
|
||||
rs.getString("target_type"),
|
||||
rs.getInt("target_id"),
|
||||
rs.getString("target_label"),
|
||||
rs.getString("action"),
|
||||
rs.getString("detail"),
|
||||
rs.getInt("success") == 1
|
||||
));
|
||||
}
|
||||
}
|
||||
} catch (SQLException ignored) {
|
||||
// table absent — return empty list, log via emu logger left to the panel UI
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionLogComposer(rows));
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package com.eu.habbo.messages.incoming.housekeeping;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.generic.alerts.StaffAlertWithLinkComposer;
|
||||
import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Mirrors :ha — staff alert with sender attribution, broadcast to
|
||||
* every online user whose `blockStaffAlerts` flag isn't set. Composed
|
||||
* once and forwarded by reference (sendResponse compiles to the same
|
||||
* underlying buffer) so the broadcast is O(N habbos) wire writes,
|
||||
* not O(N) compose calls.
|
||||
*/
|
||||
public class HousekeepingSendHotelAlertEvent extends MessageHandler {
|
||||
private static final String ACTION_KEY = "hotel.alert";
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String message = this.packet.readString();
|
||||
|
||||
if (message == null || message.trim().isEmpty()) {
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.alert_empty"));
|
||||
return;
|
||||
}
|
||||
|
||||
String body = message + "\r\n-" + this.client.getHabbo().getHabboInfo().getUsername();
|
||||
ServerMessage broadcast = new StaffAlertWithLinkComposer(body, "").compose();
|
||||
|
||||
int reached = 0;
|
||||
|
||||
for (Map.Entry<Integer, Habbo> entry : Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().entrySet()) {
|
||||
Habbo habbo = entry.getValue();
|
||||
|
||||
if (habbo == null || habbo.getClient() == null) continue;
|
||||
if (habbo.getHabboStats() != null && habbo.getHabboStats().blockStaffAlerts) continue;
|
||||
|
||||
habbo.getClient().sendResponse(broadcast);
|
||||
reached++;
|
||||
}
|
||||
|
||||
this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, reached, ""));
|
||||
}
|
||||
}
|
||||
@@ -591,5 +591,7 @@ public class Outgoing {
|
||||
public static final int HousekeepingActionResultComposer = 9201;
|
||||
public static final int HousekeepingRoomDetailComposer = 9202;
|
||||
public static final int HousekeepingRoomListComposer = 9203;
|
||||
public static final int HousekeepingDashboardComposer = 9204;
|
||||
public static final int HousekeepingActionLogComposer = 9205;
|
||||
|
||||
}
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package com.eu.habbo.messages.outgoing.housekeeping;
|
||||
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.MessageComposer;
|
||||
import com.eu.habbo.messages.outgoing.Outgoing;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class HousekeepingActionLogComposer extends MessageComposer {
|
||||
public static class Row {
|
||||
public final int id;
|
||||
public final int timestamp;
|
||||
public final int actorId;
|
||||
public final String actorName;
|
||||
public final String targetType;
|
||||
public final int targetId;
|
||||
public final String targetLabel;
|
||||
public final String action;
|
||||
public final String detail;
|
||||
public final boolean success;
|
||||
|
||||
public Row(int id, int timestamp, int actorId, String actorName, String targetType, int targetId,
|
||||
String targetLabel, String action, String detail, boolean success) {
|
||||
this.id = id;
|
||||
this.timestamp = timestamp;
|
||||
this.actorId = actorId;
|
||||
this.actorName = actorName != null ? actorName : "";
|
||||
this.targetType = targetType != null ? targetType : "user";
|
||||
this.targetId = targetId;
|
||||
this.targetLabel = targetLabel != null ? targetLabel : "";
|
||||
this.action = action != null ? action : "";
|
||||
this.detail = detail != null ? detail : "";
|
||||
this.success = success;
|
||||
}
|
||||
}
|
||||
|
||||
private final List<Row> rows;
|
||||
|
||||
public HousekeepingActionLogComposer(List<Row> rows) {
|
||||
this.rows = rows;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.HousekeepingActionLogComposer);
|
||||
this.response.appendInt(this.rows != null ? this.rows.size() : 0);
|
||||
|
||||
if (this.rows != null) {
|
||||
for (Row r : this.rows) {
|
||||
this.response.appendInt(r.id);
|
||||
this.response.appendInt(r.timestamp);
|
||||
this.response.appendInt(r.actorId);
|
||||
this.response.appendString(r.actorName);
|
||||
this.response.appendString(r.targetType);
|
||||
this.response.appendInt(r.targetId);
|
||||
this.response.appendString(r.targetLabel);
|
||||
this.response.appendString(r.action);
|
||||
this.response.appendString(r.detail);
|
||||
this.response.appendBoolean(r.success);
|
||||
}
|
||||
}
|
||||
|
||||
return this.response;
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.eu.habbo.messages.outgoing.housekeeping;
|
||||
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.MessageComposer;
|
||||
import com.eu.habbo.messages.outgoing.Outgoing;
|
||||
|
||||
public class HousekeepingDashboardComposer extends MessageComposer {
|
||||
private final int onlineUsers;
|
||||
private final int totalUsers;
|
||||
private final int activeRooms;
|
||||
private final int totalRooms;
|
||||
private final int peakOnlineToday;
|
||||
private final int peakOnlineAllTime;
|
||||
private final int pendingTickets;
|
||||
private final int sanctionsLast24h;
|
||||
private final int serverUptimeSeconds;
|
||||
private final String serverVersion;
|
||||
|
||||
public HousekeepingDashboardComposer(int onlineUsers, int totalUsers, int activeRooms, int totalRooms,
|
||||
int peakOnlineToday, int peakOnlineAllTime, int pendingTickets,
|
||||
int sanctionsLast24h, int serverUptimeSeconds, String serverVersion) {
|
||||
this.onlineUsers = onlineUsers;
|
||||
this.totalUsers = totalUsers;
|
||||
this.activeRooms = activeRooms;
|
||||
this.totalRooms = totalRooms;
|
||||
this.peakOnlineToday = peakOnlineToday;
|
||||
this.peakOnlineAllTime = peakOnlineAllTime;
|
||||
this.pendingTickets = pendingTickets;
|
||||
this.sanctionsLast24h = sanctionsLast24h;
|
||||
this.serverUptimeSeconds = serverUptimeSeconds;
|
||||
this.serverVersion = serverVersion != null ? serverVersion : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.HousekeepingDashboardComposer);
|
||||
this.response.appendInt(this.onlineUsers);
|
||||
this.response.appendInt(this.totalUsers);
|
||||
this.response.appendInt(this.activeRooms);
|
||||
this.response.appendInt(this.totalRooms);
|
||||
this.response.appendInt(this.peakOnlineToday);
|
||||
this.response.appendInt(this.peakOnlineAllTime);
|
||||
this.response.appendInt(this.pendingTickets);
|
||||
this.response.appendInt(this.sanctionsLast24h);
|
||||
this.response.appendInt(this.serverUptimeSeconds);
|
||||
this.response.appendString(this.serverVersion);
|
||||
|
||||
return this.response;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user