feat(housekeeping): ban-user with arbitrary duration + ack composer

Adds two new packets:

* Incoming 9102 HousekeepingBanUserEvent — reads (userId, reason,
  hours). Unlike ModToolSanctionBanEvent which only accepts the four
  fixed Habbo-protocol banType buckets (18h / 7d / 30d / 100y), this
  one converts the hours arg straight to seconds and feeds them into
  ModToolManager.ban with ModToolBanType.ACCOUNT and cfhTopic=0.
  Duration is clamped to 100 years to keep it inside `int` range.

* Outgoing 9201 HousekeepingActionResultComposer — generic ack
  for any HK action (ban / mute / kick / give-credits / room-close /
  …). Wire shape is (actionKey, ok, actionId, message). The
  actionKey lets the client filter multiple in-flight actions to
  the right Promise via `accept`, so concurrent admin operations
  don't cross-resolve.

actionId here is the target user id because ModToolBan doesn't
expose the `bans` autoinc id on the object — there's a TODO to swap
this for a dedicated housekeeping_log row id once that table goes in.

Same ACC_HOUSEKEEPING permission gate as the find-user packets, so
operators only need to grant the permission once.

`mvn compile` clean.
This commit is contained in:
simoleo89
2026-05-24 10:53:38 +02:00
committed by simoleo89
parent 655e039df7
commit 1a0d783ff7
5 changed files with 100 additions and 0 deletions
@@ -720,5 +720,6 @@ public class PacketManager {
// 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);
}
}
@@ -464,4 +464,5 @@ public class Incoming {
// 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;
}
@@ -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, "invalid_input"));
return;
}
long durationLong = (long) hours * SECONDS_IN_HOUR;
int duration = durationLong > MAX_DURATION_SECONDS ? MAX_DURATION_SECONDS : (int) durationLong;
List<ModToolBan> 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, "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, ""));
}
}
@@ -588,5 +588,6 @@ public class Outgoing {
// Housekeeping (in-client admin panel) — IDs 9200..9299 reserved
public static final int HousekeepingUserDetailComposer = 9200;
public static final int HousekeepingActionResultComposer = 9201;
}
@@ -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;
}
}