`HousekeepingResetUserPasswordEvent` was writing a SHA-256 hex digest
into `users.password`, but the Nitro auth path
(`SessionEndpoints` / `AccountChangeEndpoints` → `AuthHttpUtil.checkPassword`)
only does `BCrypt.checkpw`. A SHA-256 hex string doesn't start with
`$2…$`, so jbcrypt throws `IllegalArgumentException`, `checkPassword`
returns false, and operators saw "credenziali invalide" on every
account whose password had been reset from the in-client panel.
Switch to `BCrypt.hashpw(plain, BCrypt.gensalt(10))` — same idiom
already used by `SessionEndpoints.java:351` and
`AccountChangeEndpoints.java:98`. Cost 10 (vs 12 there) is fine for a
server-generated 12-char random password: gensalt(10) keeps the
operator-facing reset snappy and the output is identical-shape
(`$2a$…`) to what jbcrypt 0.4 already accepts.
Side-effects:
- drops the `MessageDigest` / `NoSuchAlgorithmException` /
`StandardCharsets` imports and the local `sha256Hex` helper
- repurposes the existing `housekeeping.error.hash_failed` key for
`BCrypt.gensalt`'s only failure mode (invalid cost / log_rounds out
of range) so the client error surface is unchanged
- updates the file javadoc to stop telling future readers to "swap the
MessageDigest constant" — Arcturus itself only verifies BCrypt
Companion of duckietm/Nitro-V3#157 (`feat/housekeeping-panel`). The
client/UI is untouched — packet 9200, the action-result reveal card,
the copy button, and the plaintext flow through `message` are all
unchanged.
Closes out the HK panel server-side surface.
* Incoming 9127 HousekeepingSendHotelAlertEvent — broadcast a
StaffAlertWithLinkComposer to every online user that hasn't
set blockStaffAlerts. Composed once, fanned out by reference;
empty-message guard returns `housekeeping.error.alert_empty`.
* Outgoing 9206 HousekeepingDashboardComposer + Incoming 9128
HousekeepingGetDashboardEvent — single round trip with the
aggregated counters: online / total users + active / total
rooms + pending support tickets + sanctions in the last 24h +
approximate emulator uptime + a version string. Active-rooms
is derived from RoomManager.getActiveRooms().getUserCount()>0
to avoid counting idle preloaded rooms. Peak online today /
all-time aren't tracked yet, so they currently echo the live
online count as a best-effort placeholder.
* Outgoing 9207 HousekeepingActionLogComposer + Incoming 9129
HousekeepingListActionLogEvent — read the optional
housekeeping_log table. If the table isn't there the SQL
exception is swallowed and an empty list goes back, so the
panel renders a no-entries view rather than crashing. Schema
is documented in the handler's javadoc; operators who want
audit run a single CREATE TABLE then the HK panel populates
from new writes (writes are a follow-up — every HK handler
will eventually append a row).
`mvn package` clean — the final fat jar lands in
Latest_Compiled_Version/ after the build finishes.
* Incoming 9117 HousekeepingGiveCreditsEvent — Habbo.giveCredits for
online (ships UserCreditsComposer) or UPDATE users.credits for offline.
* Incoming 9118 HousekeepingGiveCurrencyEvent — generic across the
non-credits currencies. currencyType 0 => duckets/pixels (givePixels),
5 => diamonds (givePoints(5,n)), anything else routes through
givePoints(type,n). Offline path INSERT ... ON DUPLICATE KEY UPDATE
users_currency.
* Incoming 9119 HousekeepingGrantItemEvent — batch-INSERT N rows into
the items table with item_id = base furni id. Capped at 100 per call
so a typo can't bury the DB. Online inventory refresh deferred — the
user picks the new items up on next hand-inventory open or relog.
* Incoming 9120 HousekeepingSetHcSubscriptionEvent — extends
users_settings.club_expire_timestamp by `days*86400`. Stacks on top
of the existing expiry if it's still in the future, otherwise starts
from now. days==0 clamps to now (effective cancel).
All four reuse HousekeepingActionResultComposer (no new outgoing
composer this slice).
`mvn compile` clean.
Eight new incoming handlers + two new outgoing composers cover the
full rooms-domain HK panel.
* Outgoing 9202 HousekeepingRoomDetailComposer — single room with a
leading `found` boolean. Writes the IHousekeepingRoom shape via a
static `appendRoomFields` that HousekeepingRoomListComposer shares.
* Outgoing 9203 HousekeepingRoomListComposer — `count` then N rooms.
Used for both find-by-name (exact match, up to 50) and the prefix
autocomplete dropdown (up to 8).
* Incoming 9110 HousekeepingFindRoomByIdEvent — loadRoom(id, false)
covers both the in-memory cache and the offline `SELECT * FROM rooms`
path. No `loadData` so HK doesn't pull furni/bots/pets just to
render a summary.
* Incoming 9111 HousekeepingSearchRoomsEvent — (query, exactMatch,
limit). Branches between `name = ?` and `name LIKE ?` so the same
wire packet serves both the autocomplete and the exact-find flows.
Hard-capped to 50.
* Incoming 9112 HousekeepingRoomStateEvent — (roomId, open). Toggles
Room.setState(OPEN | LOCKED) and persists via Room.save(). One
packet covers both the open and close API endpoints.
* Incoming 9113 HousekeepingMuteRoomEvent — (roomId, minutes). Room.
setMuted is a boolean, so minutes==0 unmutes and minutes>0 mutes.
A scheduled auto-unmute is left for a future slice; the wire field
is reserved.
* Incoming 9114 HousekeepingKickAllFromRoomEvent — Room.ejectAll().
* Incoming 9115 HousekeepingTransferRoomOwnershipEvent — UPDATEs both
rooms.owner_id and rooms.owner_name so the navigator cached name
doesn't go stale. Validates the new owner exists via
HabboManager.getHabboInfo before touching the row.
* Incoming 9116 HousekeepingDeleteRoomEvent — ejectAll + dispose +
uncacheRoom + DELETE FROM rooms, mirroring the minimum-viable
subset of RequestDeleteRoomEvent. Pets/guild/custom-layout cleanup
is skipped on this slice (orphans don't crash the emulator).
`mvn compile` clean.
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.