diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index b7ec88f4..e5887e01 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -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); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index bf9dcfa8..4222bdfa 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -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; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGetDashboardEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGetDashboardEvent.java new file mode 100644 index 00000000..c243c297 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGetDashboardEvent.java @@ -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(); +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingListActionLogEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingListActionLogEvent.java new file mode 100644 index 00000000..10979346 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingListActionLogEvent.java @@ -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 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)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSendHotelAlertEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSendHotelAlertEvent.java new file mode 100644 index 00000000..e3a83daa --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSendHotelAlertEvent.java @@ -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 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, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java index 0904698a..3e6bf2ec 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java @@ -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; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingActionLogComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingActionLogComposer.java new file mode 100644 index 00000000..6fafa0e0 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingActionLogComposer.java @@ -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 rows; + + public HousekeepingActionLogComposer(List 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; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingDashboardComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingDashboardComposer.java new file mode 100644 index 00000000..694ae6bf --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingDashboardComposer.java @@ -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; + } +}