diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Permission.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Permission.java index 11e042e2..674c6909 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Permission.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Permission.java @@ -8,6 +8,7 @@ public class Permission { public static String ACC_SEE_WHISPERS = "acc_see_whispers"; public static String ACC_SEE_TENTCHAT = "acc_see_tentchat"; public static String ACC_SUPERWIRED = "acc_superwired"; + public static String ACC_HOUSEKEEPING = "acc_housekeeping"; public static String ACC_SUPPORTTOOL = "acc_supporttool"; public static String ACC_UNKICKABLE = "acc_unkickable"; public static String ACC_GUILDGATE = "acc_guildgate"; 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 fbbc684a..a5e9b84a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -719,5 +719,31 @@ public class PacketManager { this.registerHandler(Incoming.YouTubeRoomPlayEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomPlayEvent.class); this.registerHandler(Incoming.YouTubeRoomWatchingEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomWatchingEvent.class); this.registerHandler(Incoming.YouTubeRoomSettingsEvent, com.eu.habbo.messages.incoming.rooms.youtube.YouTubeRoomSettingsEvent.class); + + // Housekeeping (in-client admin panel) + this.registerHandler(Incoming.HousekeepingFindUserByNameEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingFindUserByNameEvent.class); + this.registerHandler(Incoming.HousekeepingFindUserByIdEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingFindUserByIdEvent.class); + this.registerHandler(Incoming.HousekeepingBanUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingBanUserEvent.class); + this.registerHandler(Incoming.HousekeepingUnbanUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingUnbanUserEvent.class); + this.registerHandler(Incoming.HousekeepingMuteUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingMuteUserEvent.class); + this.registerHandler(Incoming.HousekeepingKickUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingKickUserEvent.class); + this.registerHandler(Incoming.HousekeepingForceDisconnectUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingForceDisconnectUserEvent.class); + this.registerHandler(Incoming.HousekeepingSetUserRankEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSetUserRankEvent.class); + this.registerHandler(Incoming.HousekeepingTradeLockUserEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingTradeLockUserEvent.class); + this.registerHandler(Incoming.HousekeepingResetUserPasswordEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingResetUserPasswordEvent.class); + this.registerHandler(Incoming.HousekeepingFindRoomByIdEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingFindRoomByIdEvent.class); + this.registerHandler(Incoming.HousekeepingSearchRoomsEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingSearchRoomsEvent.class); + this.registerHandler(Incoming.HousekeepingRoomStateEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingRoomStateEvent.class); + this.registerHandler(Incoming.HousekeepingMuteRoomEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingMuteRoomEvent.class); + this.registerHandler(Incoming.HousekeepingKickAllFromRoomEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingKickAllFromRoomEvent.class); + this.registerHandler(Incoming.HousekeepingTransferRoomOwnershipEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingTransferRoomOwnershipEvent.class); + this.registerHandler(Incoming.HousekeepingDeleteRoomEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingDeleteRoomEvent.class); + this.registerHandler(Incoming.HousekeepingGiveCreditsEvent, com.eu.habbo.messages.incoming.housekeeping.HousekeepingGiveCreditsEvent.class); + 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 77ef0064..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 @@ -460,4 +460,30 @@ public class Incoming { public static final int YouTubeRoomPlayEvent = 8001; public static final int YouTubeRoomWatchingEvent = 8002; public static final int YouTubeRoomSettingsEvent = 8003; + + // Housekeeping (in-client admin panel) — IDs 9100..9199 reserved + public static final int HousekeepingFindUserByNameEvent = 9100; + public static final int HousekeepingFindUserByIdEvent = 9101; + public static final int HousekeepingBanUserEvent = 9102; + public static final int HousekeepingUnbanUserEvent = 9103; + public static final int HousekeepingMuteUserEvent = 9104; + public static final int HousekeepingKickUserEvent = 9105; + public static final int HousekeepingForceDisconnectUserEvent = 9106; + public static final int HousekeepingSetUserRankEvent = 9107; + public static final int HousekeepingTradeLockUserEvent = 9108; + public static final int HousekeepingResetUserPasswordEvent = 9109; + public static final int HousekeepingFindRoomByIdEvent = 9110; + public static final int HousekeepingSearchRoomsEvent = 9111; + public static final int HousekeepingRoomStateEvent = 9112; + public static final int HousekeepingMuteRoomEvent = 9113; + public static final int HousekeepingKickAllFromRoomEvent = 9114; + public static final int HousekeepingTransferRoomOwnershipEvent = 9115; + public static final int HousekeepingDeleteRoomEvent = 9116; + public static final int HousekeepingGiveCreditsEvent = 9117; + 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/HousekeepingBanUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java new file mode 100644 index 00000000..4b2c1619 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java @@ -0,0 +1,61 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.modtool.ModToolBan; +import com.eu.habbo.habbohotel.modtool.ModToolBanType; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; + +import java.util.List; + +/** + * Apply an arbitrary-duration account ban. Duration is taken in hours + * from the wire and converted to seconds for ModToolManager.ban — + * unlike ModToolSanctionBanEvent which only accepts the four fixed + * Habbo-protocol banType buckets. + */ +public class HousekeepingBanUserEvent extends MessageHandler { + private static final String ACTION_KEY = "user.ban"; + private static final int SECONDS_IN_HOUR = 3600; + // 100-year ceiling, matches ModToolSanctionBanEvent's permanent ban. + private static final int MAX_DURATION_SECONDS = 100 * 365 * 24 * 3600; + + @Override + public int getRatelimit() { + return 1000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int userId = this.packet.readInt(); + String reason = this.packet.readString(); + int hours = this.packet.readInt(); + + if (userId <= 0 || hours <= 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); + return; + } + + long durationLong = (long) hours * SECONDS_IN_HOUR; + int duration = durationLong > MAX_DURATION_SECONDS ? MAX_DURATION_SECONDS : (int) durationLong; + + List bans = Emulator.getGameEnvironment().getModToolManager() + .ban(userId, this.client.getHabbo(), reason != null ? reason : "", duration, ModToolBanType.ACCOUNT, 0); + + if (bans == null || bans.isEmpty()) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.ban_failed")); + return; + } + + // ModToolBan doesn't expose the `bans` table autoinc id on the + // object, so we return the target user id as the actionId — it's + // the only stable handle the client can use until a dedicated + // housekeeping_log row id supersedes it. + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingDeleteRoomEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingDeleteRoomEvent.java new file mode 100644 index 00000000..9e85deb5 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingDeleteRoomEvent.java @@ -0,0 +1,68 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Permanently delete a room. Mirrors the minimum-viable subset of + * RequestDeleteRoomEvent: eject all users from the live room, dispose + * + uncache, then DELETE FROM rooms. Pets/guild/custom-layout cleanup + * is intentionally skipped on this slice — leftover rows in those + * tables become orphans but don't crash the emulator; a follow-up + * pass can cascade once we have a HK audit-log row to attach the + * orphan-cleanup to. + */ +public class HousekeepingDeleteRoomEvent extends MessageHandler { + private static final String ACTION_KEY = "room.delete"; + + @Override + public int getRatelimit() { + return 2000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int roomId = this.packet.readInt(); + + if (roomId <= 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); + return; + } + + Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false); + + if (room != null) { + room.ejectAll(); + room.preventUnloading = false; + room.dispose(); + Emulator.getGameEnvironment().getRoomManager().uncacheRoom(room); + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("DELETE FROM rooms WHERE id = ? LIMIT 1")) { + statement.setInt(1, roomId); + int rows = statement.executeUpdate(); + + if (rows == 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found")); + return; + } + } catch (SQLException e) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed")); + return; + } + + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindRoomByIdEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindRoomByIdEvent.java new file mode 100644 index 00000000..dc9267c4 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindRoomByIdEvent.java @@ -0,0 +1,37 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingRoomDetailComposer; + +public class HousekeepingFindRoomByIdEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 500; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int roomId = this.packet.readInt(); + + if (roomId <= 0) { + this.client.sendResponse(new HousekeepingRoomDetailComposer(null)); + return; + } + + // loadRoom covers both the in-memory cache (already-loaded rooms) + // and the offline path (SELECT * FROM rooms WHERE id=?). Pass + // false for loadData so we don't pull furni/bots/pets just to + // render an HK panel summary — getOwnerName / getUserCount work + // on the pre-loaded skeleton. + Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false); + + this.client.sendResponse(new HousekeepingRoomDetailComposer(room)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByIdEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByIdEvent.java new file mode 100644 index 00000000..c78735a9 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByIdEvent.java @@ -0,0 +1,35 @@ +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.HabboInfo; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingUserDetailComposer; + +public class HousekeepingFindUserByIdEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 500; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int userId = this.packet.readInt(); + + if (userId <= 0) { + this.client.sendResponse(new HousekeepingUserDetailComposer(null)); + return; + } + + // HabboManager.getHabboInfo(int) returns the in-memory record for + // online users and falls through to the offline SQL lookup + // otherwise, so a single call covers both branches. + HabboInfo info = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(userId); + + this.client.sendResponse(new HousekeepingUserDetailComposer(info)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByNameEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByNameEvent.java new file mode 100644 index 00000000..070733c6 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByNameEvent.java @@ -0,0 +1,35 @@ +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.habbohotel.users.HabboInfo; +import com.eu.habbo.habbohotel.users.HabboManager; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingUserDetailComposer; + +public class HousekeepingFindUserByNameEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 500; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + String username = this.packet.readString(); + + if (username == null || username.isEmpty()) { + this.client.sendResponse(new HousekeepingUserDetailComposer(null)); + return; + } + + Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(username); + HabboInfo info = online != null ? online.getHabboInfo() : HabboManager.getOfflineHabboInfo(username); + + this.client.sendResponse(new HousekeepingUserDetailComposer(info)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingForceDisconnectUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingForceDisconnectUserEvent.java new file mode 100644 index 00000000..8e642ffa --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingForceDisconnectUserEvent.java @@ -0,0 +1,54 @@ +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.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; + +/** + * Force-close the session of an online user. Unlike kick (which only + * removes them from the current room), this drops their socket. Equivalent + * to /disconnect in command form but issued through the HK panel. + */ +public class HousekeepingForceDisconnectUserEvent extends MessageHandler { + private static final String ACTION_KEY = "user.disconnect"; + + @Override + public int getRatelimit() { + return 1000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int userId = this.packet.readInt(); + String reason = this.packet.readString(); + + if (userId <= 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); + return; + } + + Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); + + if (target == null) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_offline")); + return; + } + + if (reason != null && !reason.isEmpty()) { + target.alert(reason); + } + + // ACK first so the action result lands before the target's socket + // closes (otherwise an alerted user on the same emulator thread may + // already be torn down when we try to write). + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); + + target.disconnect(); + } +} 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/HousekeepingGiveCreditsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java new file mode 100644 index 00000000..e0d16e2d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java @@ -0,0 +1,62 @@ +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.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public class HousekeepingGiveCreditsEvent extends MessageHandler { + private static final String ACTION_KEY = "user.give_credits"; + + @Override + public int getRatelimit() { + return 1000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int userId = this.packet.readInt(); + int amount = this.packet.readInt(); + + if (userId <= 0 || amount == 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); + return; + } + + Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); + + if (online != null) { + // giveCredits already pushes UserCreditsComposer and persists via the + // standard HabboInfo write path; nothing extra needed for the online branch. + online.giveCredits(amount); + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); + return; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("UPDATE users SET credits = credits + ? WHERE id = ? LIMIT 1")) { + statement.setInt(1, amount); + statement.setInt(2, userId); + int rows = statement.executeUpdate(); + + if (rows == 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found")); + return; + } + } catch (SQLException e) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed")); + return; + } + + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java new file mode 100644 index 00000000..ba347b93 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java @@ -0,0 +1,74 @@ +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.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Generic non-credits currency grant. Wire field `currencyType`: + * 0 => duckets / pixels, 5 => diamonds, 101 => seasonal-primary. + * Online users go through Habbo.givePoints / givePixels which dispatches + * a UserCurrencyComposer; offline goes straight to `users_currency`. + */ +public class HousekeepingGiveCurrencyEvent extends MessageHandler { + private static final int CURRENCY_DUCKETS = 0; + + @Override + public int getRatelimit() { + return 1000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int userId = this.packet.readInt(); + int currencyType = this.packet.readInt(); + int amount = this.packet.readInt(); + + String actionKey = "user.give_currency_" + currencyType; + + if (userId <= 0 || amount == 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.invalid_input")); + return; + } + + Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); + + if (online != null) { + // givePixels writes users_currency type=0 and ships UserCurrency; + // givePoints(type, amount) is the generalised path for everything else. + if (currencyType == CURRENCY_DUCKETS) { + online.givePixels(amount); + } else { + online.givePoints(currencyType, amount); + } + + this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, userId, "")); + return; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "INSERT INTO users_currency (user_id, type, amount) VALUES (?, ?, ?) " + + "ON DUPLICATE KEY UPDATE amount = amount + VALUES(amount)")) { + statement.setInt(1, userId); + statement.setInt(2, currencyType); + statement.setInt(3, amount); + statement.executeUpdate(); + } catch (SQLException e) { + this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.db_failed")); + return; + } + + this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, userId, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantItemEvent.java new file mode 100644 index 00000000..cf1ecb6b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantItemEvent.java @@ -0,0 +1,62 @@ +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.HousekeepingActionResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Grant a furni item (by items_base id) `quantity` times. Each row in + * the `items` table is one furni unit; quantity > 1 just batches the + * insert. The online user's HabboInventory isn't proactively refreshed + * — they'll see the new items next time they open the hand inventory + * (or after a relog). + */ +public class HousekeepingGrantItemEvent extends MessageHandler { + private static final String ACTION_KEY = "user.grant_item"; + private static final int MAX_QUANTITY_PER_CALL = 100; + + @Override + public int getRatelimit() { + return 2000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int userId = this.packet.readInt(); + int itemId = this.packet.readInt(); + int quantity = this.packet.readInt(); + + if (userId <= 0 || itemId <= 0 || quantity <= 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); + return; + } + + if (quantity > MAX_QUANTITY_PER_CALL) { + quantity = MAX_QUANTITY_PER_CALL; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("INSERT INTO items (user_id, item_id, extra_data) VALUES (?, ?, '')")) { + for (int i = 0; i < quantity; i++) { + statement.setInt(1, userId); + statement.setInt(2, itemId); + statement.addBatch(); + } + statement.executeBatch(); + } catch (SQLException e) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.economy_failed")); + return; + } + + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickAllFromRoomEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickAllFromRoomEvent.java new file mode 100644 index 00000000..76ae8e82 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickAllFromRoomEvent.java @@ -0,0 +1,41 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; + +public class HousekeepingKickAllFromRoomEvent extends MessageHandler { + private static final String ACTION_KEY = "room.kick_all"; + + @Override + public int getRatelimit() { + return 1000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int roomId = this.packet.readInt(); + + if (roomId <= 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); + return; + } + + Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false); + + if (room == null) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found")); + return; + } + + room.ejectAll(); + + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickUserEvent.java new file mode 100644 index 00000000..57bc8341 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickUserEvent.java @@ -0,0 +1,60 @@ +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.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; + +/** + * Kick a user out of their current room. Mirrors ModToolManager.kick + * (leave room + alert), but the legacy method gates on ACC_SUPPORTTOOL, + * which would force HK operators to also hold the support-tool permission. + * Replicating the few lines locally keeps the HK module self-gated on + * ACC_HOUSEKEEPING. + */ +public class HousekeepingKickUserEvent extends MessageHandler { + private static final String ACTION_KEY = "user.kick"; + + @Override + public int getRatelimit() { + return 1000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int userId = this.packet.readInt(); + String reason = this.packet.readString(); + + if (userId <= 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); + return; + } + + Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); + + if (target == null) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_offline")); + return; + } + + if (target.hasPermission(Permission.ACC_UNKICKABLE)) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.target_unkickable")); + return; + } + + if (target.getHabboInfo().getCurrentRoom() != null) { + Emulator.getGameEnvironment().getRoomManager().leaveRoom(target, target.getHabboInfo().getCurrentRoom()); + } + + if (reason != null && !reason.isEmpty()) { + target.alert(reason); + } + + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); + } +} 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/HousekeepingMuteRoomEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteRoomEvent.java new file mode 100644 index 00000000..b471affc --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteRoomEvent.java @@ -0,0 +1,50 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; + +/** + * Toggle room-wide mute. Habbo's Room.setMuted is boolean, not duration- + * scoped, so the wire `minutes` arg picks the semantic: minutes==0 => + * unmute, minutes>0 => mute. An emulator-side scheduled unmute could + * use the value as a timer, but for now the mute stays until the + * operator unmutes manually — the minutes is reserved as a forward- + * compat field on the wire. + */ +public class HousekeepingMuteRoomEvent extends MessageHandler { + private static final String ACTION_KEY = "room.mute"; + + @Override + public int getRatelimit() { + return 1000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int roomId = this.packet.readInt(); + int minutes = this.packet.readInt(); + + if (roomId <= 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); + return; + } + + Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false); + + if (room == null) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found")); + return; + } + + room.setMuted(minutes > 0); + + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java new file mode 100644 index 00000000..43f12eb1 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java @@ -0,0 +1,55 @@ +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.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; + +/** + * Apply an arbitrary-duration in-room mute. Habbo.mute is a session-only + * mute (it stores remaining seconds on the live Habbo object), so the + * target must be online for the action to take effect — when the target + * isn't online the handler returns ok=false with `user_offline` so the + * UI can fall back to ModToolSanctionMute or surface a clear error. + */ +public class HousekeepingMuteUserEvent extends MessageHandler { + private static final String ACTION_KEY = "user.mute"; + private static final int SECONDS_IN_MINUTE = 60; + + @Override + public int getRatelimit() { + return 1000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int userId = this.packet.readInt(); + String reason = this.packet.readString(); + int minutes = this.packet.readInt(); + + if (userId <= 0 || minutes <= 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); + return; + } + + Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); + + if (target == null) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_offline")); + return; + } + + target.mute(minutes * SECONDS_IN_MINUTE, false); + + if (reason != null && !reason.isEmpty()) { + target.alert(reason); + } + + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingResetUserPasswordEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingResetUserPasswordEvent.java new file mode 100644 index 00000000..14142002 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingResetUserPasswordEvent.java @@ -0,0 +1,89 @@ +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.HousekeepingActionResultComposer; +import org.mindrot.jbcrypt.BCrypt; + +import java.security.SecureRandom; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Reset a user's password to a fresh random 12-character alphanumeric + * string. Persists a BCrypt `$2a$` hash of the new password into + * `users.password` (matches what `AuthHttpUtil.checkPassword` / + * `SessionEndpoints` / `AccountChangeEndpoints` already write and read), + * clears `auth_ticket` so any active session can't be re-used to bypass + * the reset, and ships the PLAINTEXT new password back to the operator + * in the action-result `message` so they can communicate it out-of-band. + */ +public class HousekeepingResetUserPasswordEvent extends MessageHandler { + private static final String ACTION_KEY = "user.reset_password"; + private static final String PASSWORD_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"; + private static final int PASSWORD_LENGTH = 12; + private static final int BCRYPT_COST = 10; + + private static final SecureRandom RNG = new SecureRandom(); + + @Override + public int getRatelimit() { + return 2000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int userId = this.packet.readInt(); + + if (userId <= 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); + return; + } + + String plain = randomPassword(); + String hash; + + try { + hash = BCrypt.hashpw(plain, BCrypt.gensalt(BCRYPT_COST)); + } catch (IllegalArgumentException e) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.hash_failed")); + return; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("UPDATE users SET password = ?, auth_ticket = '' WHERE id = ? LIMIT 1")) { + statement.setString(1, hash); + statement.setInt(2, userId); + int rows = statement.executeUpdate(); + + if (rows == 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found")); + return; + } + } catch (SQLException e) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed")); + return; + } + + // Plaintext flows through `message` — the client surfaces it via the + // status banner so the operator can read it once. SSL is on the + // operator: the only secure transport for the WS is wss://. + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, plain)); + } + + private static String randomPassword() { + StringBuilder sb = new StringBuilder(PASSWORD_LENGTH); + + for (int i = 0; i < PASSWORD_LENGTH; i++) { + sb.append(PASSWORD_ALPHABET.charAt(RNG.nextInt(PASSWORD_ALPHABET.length()))); + } + + return sb.toString(); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingRoomStateEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingRoomStateEvent.java new file mode 100644 index 00000000..ea902db8 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingRoomStateEvent.java @@ -0,0 +1,49 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomState; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; + +/** + * Toggle the room state between OPEN (open) and LOCKED (closed). The + * client picks which transition it wants via the boolean — true => OPEN, + * false => LOCKED. Persists state through `Room.save()` so the change + * outlives an unload. + */ +public class HousekeepingRoomStateEvent extends MessageHandler { + @Override + public int getRatelimit() { + return 1000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int roomId = this.packet.readInt(); + boolean open = this.packet.readBoolean(); + String actionKey = open ? "room.open" : "room.close"; + + if (roomId <= 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.invalid_input")); + return; + } + + Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(roomId, false); + + if (room == null) { + this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.room_not_found")); + return; + } + + room.setState(open ? RoomState.OPEN : RoomState.LOCKED); + room.save(); + + this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, true, roomId, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSearchRoomsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSearchRoomsEvent.java new file mode 100644 index 00000000..985250c9 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSearchRoomsEvent.java @@ -0,0 +1,75 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingRoomListComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * Search rooms by name. `exactMatch=true` => `name = ?` (used by the + * findByName autocomplete that wants a unique hit). `exactMatch=false` + * => `name LIKE concat(?, '%')` (used by the prefix dropdown). + * + * Both branches go through the same packet because the wire shape is + * identical — the client picks which mode it wants by toggling the + * boolean. + */ +public class HousekeepingSearchRoomsEvent extends MessageHandler { + private static final int HARD_LIMIT = 50; + + @Override + public int getRatelimit() { + return 500; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + String query = this.packet.readString(); + boolean exactMatch = this.packet.readBoolean(); + int limit = Math.min(Math.max(this.packet.readInt(), 1), HARD_LIMIT); + + if (query == null) query = ""; + query = query.trim(); + + if (query.isEmpty()) { + this.client.sendResponse(new HousekeepingRoomListComposer(new ArrayList<>())); + return; + } + + String sql = exactMatch + ? "SELECT id FROM rooms WHERE name = ? LIMIT ?" + : "SELECT id FROM rooms WHERE name LIKE ? LIMIT ?"; + + List rooms = new ArrayList<>(); + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, exactMatch ? query : query + "%"); + statement.setInt(2, limit); + + try (ResultSet set = statement.executeQuery()) { + while (set.next()) { + Room room = Emulator.getGameEnvironment().getRoomManager().loadRoom(set.getInt("id"), false); + + if (room != null) rooms.add(room); + } + } + } catch (SQLException ignored) { + // fall through with whatever we collected before the failure + } + + this.client.sendResponse(new HousekeepingRoomListComposer(rooms)); + } +} 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/incoming/housekeeping/HousekeepingSetHcSubscriptionEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetHcSubscriptionEvent.java new file mode 100644 index 00000000..9facf4f4 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetHcSubscriptionEvent.java @@ -0,0 +1,76 @@ +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.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Extend a user's HC by `days`. Adds to the existing club_expire_timestamp + * if it's still in the future, otherwise stretches from `now`. Days==0 + * means cancel the active subscription (timestamp clamped to `now`). + */ +public class HousekeepingSetHcSubscriptionEvent extends MessageHandler { + private static final String ACTION_KEY = "user.set_hc"; + private static final int SECONDS_IN_DAY = 24 * 3600; + + @Override + public int getRatelimit() { + return 1000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int userId = this.packet.readInt(); + int days = this.packet.readInt(); + + if (userId <= 0 || days < 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); + return; + } + + int now = Emulator.getIntUnixTimestamp(); + int newExpire; + + Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); + + if (days == 0) { + newExpire = now; + } else if (online != null) { + int current = online.getHabboStats().getClubExpireTimestamp(); + newExpire = (current > now ? current : now) + (days * SECONDS_IN_DAY); + } else { + newExpire = now + (days * SECONDS_IN_DAY); // best-effort offline; can't read previous expiry cheaply + } + + if (online != null) { + online.getHabboStats().setClubExpireTimestamp(newExpire); + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET club_expire_timestamp = ? WHERE user_id = ? LIMIT 1")) { + statement.setInt(1, newExpire); + statement.setInt(2, userId); + int rows = statement.executeUpdate(); + + if (rows == 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found")); + return; + } + } catch (SQLException e) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed")); + return; + } + + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java new file mode 100644 index 00000000..67f35d8a --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java @@ -0,0 +1,71 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.habbohotel.permissions.PermissionsManager; +import com.eu.habbo.habbohotel.permissions.Rank; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; +import com.eu.habbo.messages.outgoing.users.UserPermissionsComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public class HousekeepingSetUserRankEvent extends MessageHandler { + private static final String ACTION_KEY = "user.set_rank"; + + @Override + public int getRatelimit() { + return 1000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int userId = this.packet.readInt(); + int rankId = this.packet.readInt(); + + if (userId <= 0 || rankId <= 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); + return; + } + + PermissionsManager permissions = Emulator.getGameEnvironment().getPermissionsManager(); + + if (!permissions.rankExists(rankId)) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_not_found")); + return; + } + + Rank rank = permissions.getRank(rankId); + + // Persist for the offline path. Online users get their in-memory + // HabboInfo.rank rebound below so server-side hasPermission() + // checks land on the new permission set without a relogin. + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("UPDATE users SET rank = ? WHERE id = ? LIMIT 1")) { + statement.setInt(1, rankId); + statement.setInt(2, userId); + statement.execute(); + } catch (SQLException e) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed")); + return; + } + + Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); + + if (online != null) { + online.getHabboInfo().setRank(rank); + // Ship the refreshed permissions snapshot — same payload the + // :update_permissions command emits when a rank is rebound. + online.getClient().sendResponse(new UserPermissionsComposer(online)); + } + + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java new file mode 100644 index 00000000..cf961c15 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java @@ -0,0 +1,77 @@ +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.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Apply an arbitrary-duration trade lock. Writes + * `users_settings.trade_locked_until = now + hours*3600` so the lock + * survives logout/login — that column is the canonical timestamp the + * mod-tool user-info composer queries on. Online users also get their + * in-memory HabboStats.allowTrade flag cleared so the lock takes + * effect on the active session without waiting for a relog. + */ +public class HousekeepingTradeLockUserEvent extends MessageHandler { + private static final String ACTION_KEY = "user.trade_lock"; + private static final int SECONDS_IN_HOUR = 3600; + private static final int MAX_DURATION_SECONDS = 100 * 365 * 24 * 3600; + + @Override + public int getRatelimit() { + return 1000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int userId = this.packet.readInt(); + int hours = this.packet.readInt(); + String reason = this.packet.readString(); + + if (userId <= 0 || hours <= 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); + return; + } + + long durationLong = (long) hours * SECONDS_IN_HOUR; + int duration = durationLong > MAX_DURATION_SECONDS ? MAX_DURATION_SECONDS : (int) durationLong; + int lockedUntil = Emulator.getIntUnixTimestamp() + duration; + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET trade_locked_until = ? WHERE user_id = ? LIMIT 1")) { + statement.setInt(1, lockedUntil); + statement.setInt(2, userId); + int rows = statement.executeUpdate(); + + if (rows == 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found")); + return; + } + } catch (SQLException e) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed")); + return; + } + + Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); + + if (online != null) { + online.getHabboStats().setAllowTrade(false); + + if (reason != null && !reason.isEmpty()) { + online.alert(reason); + } + } + + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTransferRoomOwnershipEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTransferRoomOwnershipEvent.java new file mode 100644 index 00000000..70c6bf80 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTransferRoomOwnershipEvent.java @@ -0,0 +1,67 @@ +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.HabboInfo; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Transfer ownership of a room to a different user. Updates both + * `rooms.owner_id` and `rooms.owner_name` so the cached owner name on + * the navigator stays in sync without forcing a relog. The room is + * touched via direct SQL rather than via Room.setOwnerId() because + * the room may not be loaded. + */ +public class HousekeepingTransferRoomOwnershipEvent extends MessageHandler { + private static final String ACTION_KEY = "room.transfer"; + + @Override + public int getRatelimit() { + return 2000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int roomId = this.packet.readInt(); + int newOwnerId = this.packet.readInt(); + + if (roomId <= 0 || newOwnerId <= 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); + return; + } + + HabboInfo newOwner = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(newOwnerId); + + if (newOwner == null) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.new_owner_not_found")); + return; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("UPDATE rooms SET owner_id = ?, owner_name = ? WHERE id = ? LIMIT 1")) { + statement.setInt(1, newOwnerId); + statement.setString(2, newOwner.getUsername()); + statement.setInt(3, roomId); + int rows = statement.executeUpdate(); + + if (rows == 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.room_not_found")); + return; + } + } catch (SQLException e) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.db_failed")); + return; + } + + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, roomId, "")); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingUnbanUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingUnbanUserEvent.java new file mode 100644 index 00000000..74ed38e4 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingUnbanUserEvent.java @@ -0,0 +1,44 @@ +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.HabboInfo; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultComposer; + +public class HousekeepingUnbanUserEvent extends MessageHandler { + private static final String ACTION_KEY = "user.unban"; + + @Override + public int getRatelimit() { + return 1000; + } + + @Override + public void handle() throws Exception { + if (!this.client.getHabbo().hasPermission(Permission.ACC_HOUSEKEEPING)) { + return; + } + + int userId = this.packet.readInt(); + + if (userId <= 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); + return; + } + + HabboInfo info = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(userId); + + if (info == null) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found")); + return; + } + + // ModToolManager.unban only takes a username; the SQL UPDATE + // happens against active bans (ban_expire > now), so calling it + // on a never-banned user is a benign no-op that returns false. + boolean cleared = Emulator.getGameEnvironment().getModToolManager().unban(info.getUsername()); + + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, cleared, cleared ? userId : 0, cleared ? "" : "housekeeping.error.no_active_ban")); + } +} 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 45d8c986..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 @@ -586,4 +586,12 @@ public class Outgoing { public static final int YouTubeRoomWatchersComposer = 8002; public static final int YouTubeRoomSettingsComposer = 8003; + // Housekeeping (in-client admin panel) — IDs 9200..9299 reserved + public static final int HousekeepingUserDetailComposer = 9200; + 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/HousekeepingActionResultComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingActionResultComposer.java new file mode 100644 index 00000000..30fdfa9c --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingActionResultComposer.java @@ -0,0 +1,36 @@ +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; + +/** + * Generic ack for any housekeeping action (ban, mute, kick, give-credits, + * room-close, …). The client matches it back to the originating call via + * the `actionKey` field, which lets multiple in-flight actions share the + * same event stream without ordering bugs. + */ +public class HousekeepingActionResultComposer extends MessageComposer { + private final String actionKey; + private final boolean ok; + private final int actionId; + private final String message; + + public HousekeepingActionResultComposer(String actionKey, boolean ok, int actionId, String message) { + this.actionKey = actionKey != null ? actionKey : ""; + this.ok = ok; + this.actionId = actionId; + this.message = message != null ? message : ""; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.HousekeepingActionResultComposer); + this.response.appendString(this.actionKey); + this.response.appendBoolean(this.ok); + this.response.appendInt(this.actionId); + this.response.appendString(this.message); + + 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; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingRoomDetailComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingRoomDetailComposer.java new file mode 100644 index 00000000..b3bd4285 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingRoomDetailComposer.java @@ -0,0 +1,49 @@ +package com.eu.habbo.messages.outgoing.housekeeping; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomState; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class HousekeepingRoomDetailComposer extends MessageComposer { + private final Room room; + + public HousekeepingRoomDetailComposer(Room room) { + this.room = room; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.HousekeepingRoomDetailComposer); + + if (this.room == null) { + this.response.appendBoolean(false); + return this.response; + } + + this.response.appendBoolean(true); + appendRoomFields(this.response, this.room); + + return this.response; + } + + /** Shared by HousekeepingRoomListComposer too. */ + public static void appendRoomFields(ServerMessage response, Room room) { + response.appendInt(room.getId()); + response.appendString(safe(room.getName())); + response.appendString(safe(room.getDescription())); + response.appendInt(room.getOwnerId()); + response.appendString(safe(room.getOwnerName())); + response.appendInt(room.getUserCount()); + response.appendInt(room.getUsersMax()); + response.appendBoolean(room.getState() != null && room.getState() != RoomState.OPEN); + response.appendBoolean(room.isMuted()); + response.appendBoolean(room.isPublicRoom()); + response.appendInt(0); // createdAt — Room doesn't expose; left as 0 until a schema-side timestamp surfaces. + } + + private static String safe(String value) { + return value != null ? value : ""; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingRoomListComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingRoomListComposer.java new file mode 100644 index 00000000..7fc6b81f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingRoomListComposer.java @@ -0,0 +1,30 @@ +package com.eu.habbo.messages.outgoing.housekeeping; + +import com.eu.habbo.habbohotel.rooms.Room; +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 HousekeepingRoomListComposer extends MessageComposer { + private final List rooms; + + public HousekeepingRoomListComposer(List rooms) { + this.rooms = rooms; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.HousekeepingRoomListComposer); + this.response.appendInt(this.rooms != null ? this.rooms.size() : 0); + + if (this.rooms != null) { + for (Room room : this.rooms) { + HousekeepingRoomDetailComposer.appendRoomFields(this.response, room); + } + } + + return this.response; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingUserDetailComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingUserDetailComposer.java new file mode 100644 index 00000000..8718e7f3 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/housekeeping/HousekeepingUserDetailComposer.java @@ -0,0 +1,59 @@ +package com.eu.habbo.messages.outgoing.housekeeping; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.modtool.ModToolBan; +import com.eu.habbo.habbohotel.permissions.Rank; +import com.eu.habbo.habbohotel.users.HabboInfo; +import com.eu.habbo.messages.ServerMessage; +import com.eu.habbo.messages.outgoing.MessageComposer; +import com.eu.habbo.messages.outgoing.Outgoing; + +public class HousekeepingUserDetailComposer extends MessageComposer { + private static final int CURRENCY_DUCKETS = 0; + private static final int CURRENCY_DIAMONDS = 5; + + private final HabboInfo info; + + public HousekeepingUserDetailComposer(HabboInfo info) { + this.info = info; + } + + @Override + protected ServerMessage composeInternal() { + this.response.init(Outgoing.HousekeepingUserDetailComposer); + + if (this.info == null) { + this.response.appendBoolean(false); + return this.response; + } + + Rank rank = this.info.getRank(); + ModToolBan ban = Emulator.getGameEnvironment().getModToolManager().checkForBan(this.info.getId()); + + this.response.appendBoolean(true); + this.response.appendInt(this.info.getId()); + this.response.appendString(safe(this.info.getUsername())); + this.response.appendString(safe(this.info.getMotto())); + this.response.appendString(safe(this.info.getLook())); + this.response.appendInt(rank != null ? rank.getId() : 0); + this.response.appendString(rank != null ? safe(rank.getName()) : ""); + this.response.appendBoolean(this.info.isOnline()); + this.response.appendInt(this.info.getLastOnline()); + this.response.appendInt(this.info.getCredits()); + this.response.appendInt(this.info.getCurrencyAmount(CURRENCY_DUCKETS)); + this.response.appendInt(this.info.getCurrencyAmount(CURRENCY_DIAMONDS)); + this.response.appendString(safe(this.info.getMail())); + this.response.appendString(safe(this.info.getIpLogin())); + this.response.appendBoolean(ban != null); + // Mute / trade-lock surface as future packet extensions; see the + // optional-trailing-field parser pattern on the renderer side. + this.response.appendBoolean(false); + this.response.appendBoolean(false); + + return this.response; + } + + private static String safe(String value) { + return value != null ? value : ""; + } +}