Closes out the users-domain HK actions.
* Incoming 9107 HousekeepingSetUserRankEvent — (userId, rankId).
Validates the rank exists in `permission_ranks`, UPDATEs users.rank,
and if the target is online rebinds their HabboInfo to the fresh
Rank object and ships a UserPermissionsComposer so server-side
hasPermission() and the client's useHasPermission(key) consumers
re-render against the new permissions without a relog.
* Incoming 9108 HousekeepingTradeLockUserEvent — (userId, hours,
reason). Writes `users_settings.trade_locked_until = now + hours*3600`
so the lock survives logout/login. Online targets also get their
in-memory HabboStats.allowTrade cleared and an optional alert.
* Incoming 9109 HousekeepingResetUserPasswordEvent — (userId).
Generates a 12-char alphanumeric (SecureRandom over a curated
ambiguity-free alphabet), writes its SHA-256 hex to users.password
(the column is varchar(64) — already sized for SHA-256 hex) and
blanks auth_ticket so any live SSO ticket can't bypass the reset.
Plaintext is returned to the operator in the action-result
message — they relay it out-of-band. If your CMS uses a hash other
than SHA-256, swap the MessageDigest.getInstance constant.
`mvn compile` clean.
Every HK action handler returned bare error slugs (\"invalid_input\",
\"user_offline\", \"no_active_ban\", \"target_unkickable\", \"ban_failed\",
\"user_not_found\") in HousekeepingActionResultComposer.message. The
client's `localizeOrPassthrough` only treats a value as a translation
key when it contains a dot, so those bare slugs were rendered raw in
the status banner and the toast — ugly and untranslatable.
Re-prefix all error messages with `housekeeping.error.` so the EN +
IT dictionaries can resolve them. Success path is unchanged (server
sends empty string, client falls back to `housekeeping.action.success`).
Companion dictionary entries land on the client side.
Incoming 9106 HousekeepingForceDisconnectUserEvent — (userId, reason).
Sends the optional reason as a Habbo.alert, dispatches the action ack
BEFORE calling target.disconnect() so the result lands on the wire
before the target's socket closes, then drops the session. Online-only;
offline target returns `user_offline`.
`mvn compile` clean.
Incoming 9104 HousekeepingMuteUserEvent — (userId, reason, minutes).
Unlike ModToolSanctionMute which takes a fixed-bucket minutes arg
from a CFH context, this one applies an arbitrary in-session mute via
Habbo.mute(seconds, false). Mute is online-only (the live Habbo object
holds the remaining seconds), so an offline target returns ok=false
with `user_offline`. The reason string, if non-empty, is delivered via
Habbo.alert so the muted user sees why.
Incoming 9105 HousekeepingKickUserEvent — (userId, reason). Replicates
the ModToolManager.kick body (leave room + alert) locally so HK doesn't
piggyback on ACC_SUPPORTTOOL the way ModToolManager.kick does — keeps
the permission model `acc_housekeeping`-only. Respects ACC_UNKICKABLE
the same way the legacy path does.
Both reuse HousekeepingActionResultComposer with their own actionKey
(user.mute / user.kick).
`mvn compile` clean.
Incoming 9103 HousekeepingUnbanUserEvent — reads userId, resolves
the username via HabboManager.getHabboInfo(int) (covers both online
and offline paths in one call), then dispatches to
ModToolManager.unban(username) which clears all active rows from
the `bans` table for that user.
Reuses HousekeepingActionResultComposer with actionKey `user.unban`.
If the user never had an active ban the SQL UPDATE matches zero rows
and the handler responds with `ok: false, message: 'no_active_ban'`
— from a UI standpoint that's a no-op, not an error.
`mvn compile` clean.
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.
Adds Incoming 9101 HousekeepingFindUserByIdEvent, which replies on
the existing HousekeepingUserDetailComposer (Outgoing 9200) — the
composer is shape-agnostic about how the lookup was issued, so the
two find-* handlers share the same response packet.
The by-id handler uses HabboManager.getHabboInfo(int) directly, which
already covers both the online (in-memory hashmap) and offline (SQL
LIMIT 1 on users) branches in one call. The by-name path still has
to do online + offline manually because the equivalent String overload
doesn't exist as an instance method, only as a static.
Also introduces Permission.ACC_HOUSEKEEPING ("acc_housekeeping") so
the in-client housekeeping panel doesn't piggyback on ACC_SUPPORTTOOL.
Both HK handlers now gate on the new permission; the toolbar UI on
the client side was already checking `acc_housekeeping`, so this
closes the loop. Operators must add the permission to
permission_definitions for the desired rank:
INSERT INTO permission_definitions
(permission_key, max_value, comment,
rank_1, rank_2, rank_3, rank_4, rank_5, rank_6, rank_7)
VALUES
('acc_housekeeping', 1,
'Allows access to the in-client Housekeeping admin panel ...',
0, 0, 0, 0, 0, 0, 1)
ON DUPLICATE KEY UPDATE rank_7 = 1, comment = VALUES(comment);
`mvn package` clean (Habbo-4.2.12-jar-with-dependencies.jar).
First wire-level packet for the in-client housekeeping admin panel.
Adds an incoming handler (Incoming 9100) that resolves a username to
a HabboInfo (online via the HabboManager hashmap, offline via the
users-table SQL fallback) and replies with HousekeepingUserDetail
(Outgoing 9200) containing id / username / motto / look / rank / rank
name / online / lastOnline / credits / duckets / diamonds / email /
ipLogin / isBanned. Active-mute and active-trade-lock are written as
trailing booleans (currently false) so the renderer parser can pick
them up later behind a bytesAvailable guard once those manager APIs
are surfaced offline-side.
Permission gate is ACC_SUPPORTTOOL — same one ModTools already uses.
Avoids adding a new column to the permissions table on this slice; a
dedicated ACC_HOUSEKEEPING permission can be introduced later when
the destructive HK operations (give-credits, delete-room, reset-pwd)
go in.
Reserves the 9100..9199 / 9200..9299 ID blocks for the rest of the
HK packet surface (search-prefix, find-by-id, ban/mute/kick with
arbitrary duration, room actions, economy, catalog admin, dashboard
aggregate, audit log read).
`mvn compile` clean on Habbo 4.2.12.
The User Info panel reads its CFH / Cautions / Bans / Trade locks
counters from `users_settings.cfh_send` / `cfh_warnings` / `cfh_bans`
(via totalBans) / `tradelock_amount`. Historically only `cfh_send`
was ever incremented (by `InsertModToolIssue` on CFH submit), so a
user could accumulate any number of Alert / Mute / Ban / TradeLock
sanctions without the stats reflecting it — every panel showed all
zeros even on accounts with a long sanction history visible in the
modern `sanctions` table.
The two systems aren't going away — `ModToolSanctions` (the modern
one) tracks individual sanction events with probation timestamps,
while the legacy `users_settings.cfh_*` columns are flat counters
the ModTool UI displays. Both need to stay in sync.
Wire them up:
`ModToolManager.bumpUserSettingCounter(userId, column)`
Static helper, column-whitelisted (`cfh_warnings` / `cfh_bans` /
`cfh_abusive` / `tradelock_amount`) to keep the dynamic SQL safe.
Single UPDATE per call; SQL exceptions logged, never thrown.
`ModToolSanctionAlertEvent`, `ModToolSanctionMuteEvent` → bump
`cfh_warnings`. Mute is a punitive but non-banning action; both it
and Alert are recorded as a warning on the legacy counter, matching
what the Cautions stat card represents in the new UI.
`ModToolSanctionBanEvent` → bump `cfh_bans`. The `totalBans` field
the composer sends ALREADY counts entries in the `bans` table, so
the wire field reflects reality immediately — this column bump is
a defensive duplicate so any code that reads `users_settings.cfh_bans`
directly (e.g. plugin scripts, CMS dashboards) stays in sync.
`ModToolSanctionTradeLockEvent` → bump `tradelock_amount`. Mirrors
what `AllowTradingCommand` already does for the command-line path.
`ModToolManager.closeTicketAsAbusive` → bump `cfh_abusive` for the
REPORTER (issue.senderId), not the reported user. The Abusive
counter measures false reports filed by the user, so it belongs on
whoever opened the CFH that got closed as abusive.
No client-side changes — counter columns are unchanged, only the
write paths are.
ModToolUserInfoComposer used to send three trailing fields hardcoded
to empty/zero — the client rendered placeholders for every user, on
every panel open:
appendString(""); // Trading lock expiry timestamp
appendString(""); // Last Purchase Timestamp
appendInt(0); // Number of account bans
These are useful moderation signals and the data already exists in
the live tables. Wire them up.
Last Purchase
Query MAX(timestamp) FROM logs_shop_purchases WHERE user_id = ?.
Returns the most recent purchase epoch. Rendered as yyyy-MM-dd HH:mm.
Empty when the user has never bought anything (the query returns
NULL → getInt returns 0 → formatUnixTimestamp emits "").
Trading lock expiry
Query MAX(trade_locked_until) FROM sanctions WHERE habbo_id = ? AND
trade_locked_until > <now>. Latest ACTIVE lock only — past entries
don't count. Same yyyy-MM-dd HH:mm format. Empty when no active
lock.
Identity related bans
Count of DISTINCT other user accounts that have a ban entry against
the same machine_id as the target. Self is excluded since the target's
own bans already show up in banCount. An empty machine_id (default
'') short-circuits to 0 so we never match accounts whose machine
fingerprint was never recorded.
The existing totalBans counter is extracted into a helper alongside
the three new ones — cleaner than the inline try-catch tower it used
to live in, same behaviour.
Format choice yyyy-MM-dd HH:mm matches the timestamp shown elsewhere
in moderation UI; both string fields go through the same formatter so
the empty case stays consistent (empty string, not "1970-01-01...").
No client-side changes needed — ModeratorUserInfoData already parses
both strings and the int, and the React ModToolsUserView already
renders them. They were just always empty before.
The default permission_definitions seed for acc_supporttool used the
pattern (0, 1, 1, 1, 1, 0, 1) across rank_1..rank_7 — apparently
shifted by two columns:
* rank_2 (VIP) and rank_3 (X) had ALLOWED. With acc_supporttool=1
the SecureLoginEvent path sends ModeratorInitMessageEvent on
login, which makes the React client surface the ModTools toolbar
button and let the user open room/user info windows. The actual
sanction endpoints (ModToolSanctionBanEvent, ModToolWarnEvent,
…) still gate on ACC_SUPPORTTOOL so a VIP cannot actually take
moderator action — but they can request user info, room info
and chatlogs they have no business reading.
* rank_6 (Super Mod) was DISALLOWED, which is obviously not what
the name says.
Corrected pattern: (0, 0, 0, 1, 1, 1, 1) — Support (4), Moderator
(5), Super Mod (6), Administrator (7). Matches the convention used
by the other staff-only acc_modtool_* keys.
Two changes:
- Default Database/FullDatabase.sql: fix the seed for fresh
installs.
- Database Updates/004_fix_acc_supporttool_rank.sql: idempotent
UPDATE to realign existing deployments.
Found by user report: a rank-2 (VIP) account on the live retro had
the ModTools button visible in the toolbar after login.
`PermissionsManager.reload()` rebuilds the rank table from
`permission_ranks` + `permission_definitions`, but every Habbo
currently online still holds a reference to the OLD `Rank` object
on `HabboInfo.rank`. Server-side `hasPermission()` therefore keeps
returning stale results, and any Nitro client that reads permission
state from the wire keeps gating UI on the map shipped at login
— until a relogin or `:give_rank` forces a per-user refresh.
Extend the existing `UpdatePermissionsCommand` so after `reload()`
it:
1. Iterates the online Habbos via `HabboManager.getOnlineHabbos()`.
2. Re-binds each one's `HabboInfo.rank` to the FRESH `Rank` object
returned by `PermissionsManager.getRank(currentRankId)`. Falls
back to rank 1 if the admin deleted the rank from
`permission_ranks` between sessions, so the user is never left
with a null `Rank` reference.
3. Sends a fresh `UserPermissionsComposer` to each client.
With the companion composer extension PR also merged, this
broadcasts the rank metadata + resolved permission map runtime —
the Nitro React-side `useHasPermission(key)` / `useUserRank()`
consumers re-render against the freshly-loaded tables without
requiring an F5.
The whisper feedback now reports how many connected users were
refreshed, useful for ops feedback after a large `permission_ranks`
edit.
Defensive null guards on habbo / habboInfo / client survive
transient state during the broadcast (e.g. a user disconnecting
mid-iteration).
Backward-compatible wire extension of `UserPermissionsComposer`
(header 411) that lets Nitro clients display per-deployment rank
info and drive UI gates against the actual `permission_definitions`
table instead of hardcoded SecurityLevel constants.
Wire layout after this change (each trailing block is guarded by
`bytesAvailable` on the client side so older Nitro builds keep
parsing the prefix and stop):
int clubLevel
int rank.level // mapped to securityLevel on the client
bool isAmbassador // existing ACC_AMBASSADOR flag
--- new: rank metadata ---
int rank.id
string rank.name // permission_ranks.rank_name
string rank.badge
string rank.prefix
string rank.prefixColor
--- new: resolved permission map ---
int count
loop: string permission_key + int value // 1 = ALLOWED, 2 = ROOM_OWNER
The permission map is the union of:
* Rank entries whose `PermissionSetting != DISALLOWED` (value 1
for ALLOWED, 2 for ROOM_OWNER).
* For every rank-DISALLOWED key, each installed
`HabboPlugin.hasPermission(habbo, key)` is consulted; if any
plugin grants the permission, the key lands on the wire with
value 1 (plugins do not have a ROOM_OWNER concept).
Iterating `rank.getPermissions().keySet()` covers every key in
`permission_definitions` because `PermissionsManager.loadPermissionsNormalized()`
calls `rank.setPermission(key, ...)` for every row of the table —
including DISALLOWED ones. Custom keys a plugin invents that are
not in `permission_definitions` stay invisible (there is no
enumeration API on `HabboPlugin` to discover them); this is a rare
case documented in the class-level Javadoc.
The result is a client-side permission map whose semantics match
exactly what `PermissionsManager.hasPermission(habbo, key)` would
return server-side — including plugin-granted permissions, which
were invisible to the client before.
Performance: at login the loop is O(N keys × P plugins), with
N ≈ 200 (size of permission_definitions) and P typically 1-5.
`HabboPlugin.hasPermission` is O(1) hashset lookups in
real-world implementations. Sub-millisecond at login, and the
composer is only sent at login + `HabboManager.setRank` +
`:update_permissions` broadcast.
Backward compatibility: all new fields are appended in tail
position with `bytesAvailable` guards on the parser side, so:
* existing Nitro clients keep parsing only the prefix and ignore
the trailing bytes (no error, no behavior change);
* new Nitro clients with the matching parser extension expose the
extra data via `IUserDataSnapshot` snapshot getters and the
React-side `useUserRank()` / `useHasPermission(key)` /
`useUserPermissions()` hooks (see companion PRs on
`duckietm/Nitro_Render_V3` and `duckietm/Nitro-V3`).