HousekeepingApi.ts 25.2 Ko
Télécharger le fichier
import {
HabboSearchComposer,
HabboSearchResultEvent,
HousekeepingActionLogEvent,
HousekeepingActionResultEvent,
HousekeepingBanUserComposer,
HousekeepingDashboardEvent,
HousekeepingDeleteRoomComposer,
HousekeepingFindRoomByIdComposer,
HousekeepingFindUserByIdComposer,
HousekeepingFindUserByNameComposer,
HousekeepingForceDisconnectUserComposer,
HousekeepingGetDashboardComposer,
HousekeepingGiveCreditsComposer,
HousekeepingGiveCurrencyComposer,
HousekeepingGrantItemComposer,
HousekeepingKickAllFromRoomComposer,
HousekeepingKickUserComposer,
HousekeepingListEvent,
HousekeepingLockdownComposer,
HousekeepingMaintenanceComposer,
HousekeepingMaintenanceStatusEvent,
HousekeepingListActionLogComposer,
HousekeepingMuteRoomComposer,
HousekeepingMuteUserComposer,
HousekeepingReloadComposer,
HousekeepingRequestListComposer,
HousekeepingResetUserPasswordComposer,
HousekeepingRevokeBanComposer,
HousekeepingRoomData,
HousekeepingRoomDetailEvent,
HousekeepingRoomListEvent,
HousekeepingRoomStateComposer,
HousekeepingSaveRoomSettingsComposer,
HousekeepingSearchRoomsComposer,
HousekeepingSendHotelAlertComposer,
HousekeepingSetHcSubscriptionComposer,
HousekeepingSetPermissionComposer,
HousekeepingSetUserRankComposer,
HousekeepingTradeLockUserComposer,
HousekeepingTransferRoomOwnershipComposer,
HousekeepingUnbanUserComposer,
HousekeepingUserDetailData,
HousekeepingUserDetailEvent,
HousekeepingUserNoteComposer,
HousekeepingWordFilterComposer,
IMessageComposer
} from '@octane/renderer';
import { awaitMessageEvent } from '../octane/awaitMessageEvent';
import { SendMessageComposer } from '../octane/SendMessageComposer';
import { SerialRequestChannel } from '../octane/SerialRequestChannel';
import { HousekeepingReloadTarget } from './HousekeepingActionType';
import { HousekeepingMaintenanceAction } from './HousekeepingHotelTools';
import {
IHousekeepingActionLogEntry,
IHousekeepingActionResult,
IHousekeepingDashboard,
IHousekeepingList,
IHousekeepingMaintenanceStatus,
IHousekeepingRoom,
IHousekeepingRoomSettings,
IHousekeepingRoomSettingsInput,
IHousekeepingRoomSummary,
IHousekeepingUser,
IHousekeepingUserSummary
} from './IHousekeepingTypes';
const USER_SEARCH_LIMIT = 8;
// The room and user searches answer with packets that carry no request id, and the server rate-limits the requests
// (500 ms) and drops the extra ones: the suggestions and the exact room search waited for the same reply, a dropped
// request left the screen waiting 8 s for nothing, and an exact search could get the suggestion list. Each kind of
// search now goes through one channel: one request in flight, sent at least this far apart.
const SEARCH_REQUEST_INTERVAL_MS = 550;
const roomListChannel = new SerialRequestChannel(SEARCH_REQUEST_INTERVAL_MS);
const userSearchChannel = new SerialRequestChannel(SEARCH_REQUEST_INTERVAL_MS);
/**
* Shown when the server never answers. The usual cause is an emulator older
* than the panel: it drops a packet it has no handler for, silently.
*/
export const HOUSEKEEPING_NO_ANSWER_KEY = 'housekeeping.error.no_answer';
/** Maps a failed wait to the key the panel shows; a timeout becomes {@link HOUSEKEEPING_NO_ANSWER_KEY}. */
export const housekeepingFailureKey = (error: unknown, fallback: string): string =>
error instanceof Error && error.message === 'timeout' ? HOUSEKEEPING_NO_ANSWER_KEY : fallback;
/** Action key of the immediate answer a server sends when it refuses the housekeeping permission. */
export const HOUSEKEEPING_DENIED_ACTION_KEY = 'housekeeping.denied';
const searchUsersViaPacket = (prefix: string, signal?: AbortSignal): Promise<IHousekeepingUserSummary[]> =>
userSearchChannel.run(
() => SendMessageComposer(new HabboSearchComposer(prefix)),
() => awaitUserSearchReply(prefix),
signal
);
const awaitUserSearchReply = (prefix: string): Promise<IHousekeepingUserSummary[]> =>
// Snapshot the parser inside the subscribe callback — the renderer
// recycles parser instances after the callback returns, so any
// post-await read of `event.getParser()` comes back null.
awaitMessageEvent<HabboSearchResultEvent, IHousekeepingUserSummary[]>(HabboSearchResultEvent, {
timeoutMs: 8_000,
select: (event) => {
const parser = event.getParser();
if (!parser) return [];
const combined = [...parser.friends, ...parser.others];
const summaries: IHousekeepingUserSummary[] = [];
for (const entry of combined) {
const username = entry.avatarName || '';
if (!username.toLowerCase().startsWith(prefix.toLowerCase())) continue;
summaries.push({
id: entry.avatarId,
username,
figure: entry.avatarFigure || '',
online: entry.isAvatarOnline === true,
rank: 0
});
if (summaries.length >= USER_SEARCH_LIMIT) break;
}
return summaries;
}
});
const mapUserDetail = (user: HousekeepingUserDetailData): IHousekeepingUser => ({
id: user.id,
username: user.username,
motto: user.motto,
figure: user.figure,
rank: user.rank,
rankName: user.rankName,
online: user.online,
lastOnlineAt: user.lastOnlineAt > 0 ? user.lastOnlineAt : null,
creditsBalance: user.creditsBalance,
ducketsBalance: user.ducketsBalance,
diamondsBalance: user.diamondsBalance,
email: user.email,
ipLast: user.ipLast,
isBanned: user.isBanned,
isMuted: user.isMuted,
isTradeLocked: user.isTradeLocked,
// An account always has a creation time, so zero means the server sent no profile block.
profile:
user.accountCreatedAt > 0
? {
accountCreatedAt: user.accountCreatedAt,
achievementScore: user.achievementScore,
friendsCount: user.friendsCount,
groupsCount: user.groupsCount,
wornBadges: user.wornBadges.map((badge) => ({ slot: badge.slot, code: badge.code }))
}
: null
});
const awaitUserDetail = (): Promise<IHousekeepingUser | null> =>
awaitMessageEvent<HousekeepingUserDetailEvent, IHousekeepingUser | null>(HousekeepingUserDetailEvent, {
timeoutMs: 8_000,
select: (event) => {
const parser = event.getParser();
if (!parser || !parser.found || !parser.user) return null;
return mapUserDetail(parser.user);
}
});
const findUserByNameViaPacket = async (username: string): Promise<IHousekeepingUser | null> => {
const trimmed = (username || '').trim();
if (!trimmed) return null;
SendMessageComposer(new HousekeepingFindUserByNameComposer(trimmed));
return awaitUserDetail();
};
const findUserByIdViaPacket = async (userId: number): Promise<IHousekeepingUser | null> => {
if (!Number.isFinite(userId) || userId <= 0) return null;
SendMessageComposer(new HousekeepingFindUserByIdComposer(userId));
return awaitUserDetail();
};
/**
* Fire any HK action composer and resolve when the matching
* HousekeepingActionResultEvent arrives. The server tags each ack with
* a string `actionKey` (`user.ban`, `user.mute`, …) so the listener can
* filter via the `accept` predicate — protects against another concurrent
* action's ack slipping into a waiter that was expecting a different one.
*/
const runHkAction = async (composer: IMessageComposer<unknown[]>, expectedActionKey: string, timeoutMs = 15_000): Promise<IHousekeepingActionResult> => {
SendMessageComposer(composer);
try {
return await awaitMessageEvent<HousekeepingActionResultEvent, IHousekeepingActionResult>(HousekeepingActionResultEvent, {
timeoutMs,
// A server that refuses the permission answers at once under its own key.
accept: (e) => e.getParser()?.actionKey === expectedActionKey || e.getParser()?.actionKey === HOUSEKEEPING_DENIED_ACTION_KEY,
select: (event) => {
const parser = event.getParser();
if (!parser) return { ok: false, actionId: null, message: 'no_parser' };
return {
ok: parser.ok,
actionId: parser.actionId > 0 ? parser.actionId : null,
message: parser.message
};
}
});
} catch (err) {
return { ok: false, actionId: null, message: housekeepingFailureKey(err, err instanceof Error ? err.message : 'unknown') };
}
};
const banUserViaPacket = (userId: number, reason: string, hours: number): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingBanUserComposer(userId, reason || '', hours), 'user.ban');
const unbanUserViaPacket = (userId: number): Promise<IHousekeepingActionResult> => runHkAction(new HousekeepingUnbanUserComposer(userId), 'user.unban');
const muteUserViaPacket = (userId: number, reason: string, minutes: number): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingMuteUserComposer(userId, reason || '', minutes), 'user.mute');
const kickUserViaPacket = (userId: number, reason: string): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingKickUserComposer(userId, reason || ''), 'user.kick');
const forceDisconnectUserViaPacket = (userId: number, reason: string): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingForceDisconnectUserComposer(userId, reason || ''), 'user.disconnect');
/** durationSeconds > 0 makes the rank temporary; otherwise the packet stays the two-int form. */
const setUserRankViaPacket = (userId: number, rank: number, durationSeconds = 0): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingSetUserRankComposer(userId, rank, durationSeconds > 0 ? durationSeconds : undefined), 'user.set_rank');
const tradeLockUserViaPacket = (userId: number, hours: number, reason: string): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingTradeLockUserComposer(userId, hours, reason || ''), 'user.trade_lock');
const resetUserPasswordViaPacket = (userId: number): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingResetUserPasswordComposer(userId), 'user.reset_password');
const mapRoom = (room: HousekeepingRoomData): IHousekeepingRoom => ({
id: room.id,
name: room.name,
description: room.description,
ownerId: room.ownerId,
ownerName: room.ownerName,
userCount: room.userCount,
maxUsers: room.maxUsers,
isLocked: room.isLocked,
isMuted: room.isMuted,
isPublic: room.isPublic,
createdAt: room.createdAt,
settings: null
});
const findRoomByIdViaPacket = (roomId: number): Promise<IHousekeepingRoom | null> => {
if (!Number.isFinite(roomId) || roomId <= 0) return Promise.resolve(null);
SendMessageComposer(new HousekeepingFindRoomByIdComposer(roomId));
return awaitMessageEvent<HousekeepingRoomDetailEvent, IHousekeepingRoom | null>(HousekeepingRoomDetailEvent, {
timeoutMs: 8_000,
select: (event) => {
const parser = event.getParser();
if (!parser || !parser.found || !parser.room) return null;
// Category ids start at 1, so zero means the server sent no settings tail.
const settings: IHousekeepingRoomSettings | null =
parser.categoryId > 0 ? { categoryId: parser.categoryId, tradeMode: parser.tradeMode, state: parser.state, tags: [...parser.tags] } : null;
return { ...mapRoom(parser.room), settings };
}
});
};
const findRoomByNameViaPacket = (name: string): Promise<IHousekeepingRoom[]> => {
const trimmed = (name || '').trim();
if (!trimmed) return Promise.resolve([]);
return roomListChannel.run(
() => SendMessageComposer(new HousekeepingSearchRoomsComposer(trimmed, true, 50)),
() =>
awaitMessageEvent<HousekeepingRoomListEvent, IHousekeepingRoom[]>(HousekeepingRoomListEvent, {
timeoutMs: 8_000,
select: (event) => event.getParser()?.rooms.map(mapRoom) ?? []
})
);
};
const searchRoomsViaPacket = (prefix: string, signal?: AbortSignal): Promise<IHousekeepingRoomSummary[]> => {
const trimmed = (prefix || '').trim();
if (!trimmed) return Promise.resolve([]);
return roomListChannel.run(
() => SendMessageComposer(new HousekeepingSearchRoomsComposer(trimmed, false, 8)),
() =>
awaitMessageEvent<HousekeepingRoomListEvent, IHousekeepingRoomSummary[]>(HousekeepingRoomListEvent, {
timeoutMs: 8_000,
select: (event) =>
event.getParser()?.rooms.map((room) => ({
id: room.id,
name: room.name,
userCount: room.userCount,
ownerName: room.ownerName
})) ?? []
}),
signal
);
};
const setRoomStateViaPacket = (roomId: number, open: boolean): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingRoomStateComposer(roomId, open), open ? 'room.open' : 'room.close');
const saveRoomSettingsViaPacket = (roomId: number, input: IHousekeepingRoomSettingsInput): Promise<IHousekeepingActionResult> =>
runHkAction(
new HousekeepingSaveRoomSettingsComposer(roomId, input.name, input.description, input.maxUsers, input.categoryId, input.tradeMode, input.tags),
'room.settings'
);
const muteRoomViaPacket = (roomId: number, minutes: number): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingMuteRoomComposer(roomId, minutes), 'room.mute');
const kickAllFromRoomViaPacket = (roomId: number): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingKickAllFromRoomComposer(roomId), 'room.kick_all');
const transferRoomOwnershipViaPacket = (roomId: number, newOwnerId: number): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingTransferRoomOwnershipComposer(roomId, newOwnerId), 'room.transfer');
const deleteRoomViaPacket = (roomId: number): Promise<IHousekeepingActionResult> => runHkAction(new HousekeepingDeleteRoomComposer(roomId), 'room.delete');
const CURRENCY_DUCKETS = 0;
const CURRENCY_DIAMONDS = 5;
const giveCreditsViaPacket = (userId: number, amount: number): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingGiveCreditsComposer(userId, amount), 'user.give_credits');
const giveDucketsViaPacket = (userId: number, amount: number): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingGiveCurrencyComposer(userId, CURRENCY_DUCKETS, amount), `user.give_currency_${CURRENCY_DUCKETS}`);
const giveDiamondsViaPacket = (userId: number, amount: number): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingGiveCurrencyComposer(userId, CURRENCY_DIAMONDS, amount), `user.give_currency_${CURRENCY_DIAMONDS}`);
const grantItemViaPacket = (userId: number, itemId: number, quantity: number): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingGrantItemComposer(userId, itemId, quantity), 'user.grant_item');
const setHcSubscriptionViaPacket = (userId: number, days: number): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingSetHcSubscriptionComposer(userId, days), 'user.set_hc');
const sendHotelAlertViaPacket = (message: string, recipient?: string): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingSendHotelAlertComposer(message || '', recipient), 'hotel.alert');
const maintenanceViaPacket = (action: Exclude<HousekeepingMaintenanceAction, 'status'>, message = '', minutes = 0): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingMaintenanceComposer(action, message, minutes), `hotel.maintenance.${action}`);
const getMaintenanceStatusViaPacket = (signal?: AbortSignal): Promise<IHousekeepingMaintenanceStatus> => {
SendMessageComposer(new HousekeepingMaintenanceComposer('status', '', 0));
return awaitMessageEvent<HousekeepingMaintenanceStatusEvent, IHousekeepingMaintenanceStatus>(HousekeepingMaintenanceStatusEvent, {
signal,
timeoutMs: 8_000,
select: (event) => readMaintenanceStatus(event.getParser())
});
};
/** Reads the maintenance status parser into a plain object; read it inside `select`, the parser is recycled. */
export const readMaintenanceStatus = (parser: { enabled: boolean; minRank: number; message: string; countdownEndsAt: number }): IHousekeepingMaintenanceStatus => ({
enabled: parser.enabled,
minRank: parser.minRank,
message: parser.message,
countdownEndsAt: parser.countdownEndsAt
});
const setPermissionViaPacket = (permissionKey: string, rankId: number, value: number): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingSetPermissionComposer(permissionKey, rankId, value), 'hotel.permission.set');
const userNoteViaPacket = (action: 'add' | 'delete', userId: number, noteId: number, note: string): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingUserNoteComposer(action, userId, noteId, note), `user.note.${action}`);
const wordFilterViaPacket = (action: 'add' | 'remove', word: string, replacement = ''): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingWordFilterComposer(action, word, replacement), `hotel.wordfilter.${action}`);
const revokeBanViaPacket = (banId: number): Promise<IHousekeepingActionResult> => runHkAction(new HousekeepingRevokeBanComposer(banId), 'ban.revoke');
const reloadViaPacket = (target: HousekeepingReloadTarget): Promise<IHousekeepingActionResult> =>
runHkAction(new HousekeepingReloadComposer(target), `hotel.reload.${target}`);
const EMPTY_DASHBOARD: IHousekeepingDashboard = {
onlineUsers: 0,
totalUsers: 0,
activeRooms: 0,
totalRooms: 0,
peakOnlineToday: 0,
peakOnlineAllTime: 0,
pendingTickets: 0,
sanctionsLast24h: 0,
serverUptimeSeconds: 0,
serverVersion: ''
};
const getDashboardViaPacket = (signal?: AbortSignal): Promise<IHousekeepingDashboard> => {
SendMessageComposer(new HousekeepingGetDashboardComposer());
return awaitMessageEvent<HousekeepingDashboardEvent, IHousekeepingDashboard>(HousekeepingDashboardEvent, {
signal,
timeoutMs: 10_000,
select: (event) => {
const parser = event.getParser();
if (!parser) return EMPTY_DASHBOARD;
return {
onlineUsers: parser.onlineUsers,
totalUsers: parser.totalUsers,
activeRooms: parser.activeRooms,
totalRooms: parser.totalRooms,
peakOnlineToday: parser.peakOnlineToday,
peakOnlineAllTime: parser.peakOnlineAllTime,
pendingTickets: parser.pendingTickets,
sanctionsLast24h: parser.sanctionsLast24h,
serverUptimeSeconds: parser.serverUptimeSeconds,
serverVersion: parser.serverVersion
};
}
});
};
const listActionLogViaPacket = (limit: number, signal?: AbortSignal): Promise<IHousekeepingActionLogEntry[]> => {
const safeLimit = Math.max(1, Math.min(500, Math.floor(limit || 50)));
SendMessageComposer(new HousekeepingListActionLogComposer(safeLimit));
return awaitMessageEvent<HousekeepingActionLogEvent, IHousekeepingActionLogEntry[]>(HousekeepingActionLogEvent, {
signal,
timeoutMs: 10_000,
select: (event) =>
event.getParser()?.entries.map((entry) => ({
id: entry.id,
// The log stores unix seconds; the formatters work in milliseconds.
timestamp: entry.timestamp * 1000,
actorId: entry.actorId,
actorName: entry.actorName,
targetType: entry.targetType === 'room' || entry.targetType === 'hotel' ? entry.targetType : 'user',
targetId: entry.targetId > 0 ? entry.targetId : null,
targetLabel: entry.targetLabel,
action: entry.action,
detail: entry.detail,
success: entry.success
})) ?? []
});
};
/** reveal asks for IP addresses in clear: it needs acc_hk_view_private and the server audits it. */
const requestListViaPacket = (listKey: string, targetId: number, signal?: AbortSignal, reveal = false): Promise<IHousekeepingList> => {
SendMessageComposer(new HousekeepingRequestListComposer(listKey, targetId, reveal ? 1 : undefined));
return awaitMessageEvent<HousekeepingListEvent, IHousekeepingList>(HousekeepingListEvent, {
signal,
timeoutMs: 10_000,
// Several lists can be in flight when the operator switches quickly.
accept: (event) => event.getParser()?.listKey === listKey && event.getParser()?.targetId === targetId,
select: (event) => {
const parser = event.getParser();
return {
listKey: parser.listKey,
targetId: parser.targetId,
ok: parser.ok,
message: parser.message,
columns: [...parser.columns],
rows: parser.rows.map((row) => [...row])
};
}
});
};
export const HousekeepingApi = {
// -- dashboard -------------------------------------------------
getDashboard: (signal?: AbortSignal) => getDashboardViaPacket(signal),
// -- user lookup -----------------------------------------------
findUserByName: (username: string) => findUserByNameViaPacket(username),
findUserById: (userId: number) => findUserByIdViaPacket(userId),
searchUsers: (prefix: string, signal?: AbortSignal) => searchUsersViaPacket(prefix, signal),
// -- user actions ----------------------------------------------
banUser: (userId: number, reason: string, hours: number) => banUserViaPacket(userId, reason, hours),
unbanUser: (userId: number) => unbanUserViaPacket(userId),
muteUser: (userId: number, reason: string, minutes: number) => muteUserViaPacket(userId, reason, minutes),
kickUser: (userId: number, reason: string) => kickUserViaPacket(userId, reason),
forceDisconnectUser: (userId: number, reason: string) => forceDisconnectUserViaPacket(userId, reason),
resetUserPassword: (userId: number) => resetUserPasswordViaPacket(userId),
setUserRank: (userId: number, rank: number, durationSeconds?: number) => setUserRankViaPacket(userId, rank, durationSeconds),
tradeLockUser: (userId: number, hours: number, reason: string) => tradeLockUserViaPacket(userId, hours, reason),
// -- room lookup -----------------------------------------------
findRoomById: (roomId: number) => findRoomByIdViaPacket(roomId),
findRoomByName: (name: string) => findRoomByNameViaPacket(name),
searchRooms: (prefix: string, signal?: AbortSignal) => searchRoomsViaPacket(prefix, signal),
// -- room actions ----------------------------------------------
openRoom: (roomId: number) => setRoomStateViaPacket(roomId, true),
closeRoom: (roomId: number) => setRoomStateViaPacket(roomId, false),
saveRoomSettings: (roomId: number, input: IHousekeepingRoomSettingsInput) => saveRoomSettingsViaPacket(roomId, input),
muteRoom: (roomId: number, minutes: number) => muteRoomViaPacket(roomId, minutes),
kickAllFromRoom: (roomId: number) => kickAllFromRoomViaPacket(roomId),
transferRoomOwnership: (roomId: number, newOwnerId: number) => transferRoomOwnershipViaPacket(roomId, newOwnerId),
deleteRoom: (roomId: number) => deleteRoomViaPacket(roomId),
// -- economy actions -------------------------------------------
giveCredits: (userId: number, amount: number) => giveCreditsViaPacket(userId, amount),
giveDuckets: (userId: number, amount: number) => giveDucketsViaPacket(userId, amount),
giveDiamonds: (userId: number, amount: number) => giveDiamondsViaPacket(userId, amount),
grantItem: (userId: number, itemId: number, quantity: number) => grantItemViaPacket(userId, itemId, quantity),
setHcSubscription: (userId: number, days: number) => setHcSubscriptionViaPacket(userId, days),
// -- hotel-level -----------------------------------------------
sendHotelAlert: (message: string, recipient?: string) => sendHotelAlertViaPacket(message, recipient),
maintenance: (action: Exclude<HousekeepingMaintenanceAction, 'status'>, message?: string, minutes?: number) => maintenanceViaPacket(action, message, minutes),
getMaintenanceStatus: (signal?: AbortSignal) => getMaintenanceStatusViaPacket(signal),
wordFilter: (action: 'add' | 'remove', word: string, replacement?: string) => wordFilterViaPacket(action, word, replacement),
setPermission: (permissionKey: string, rankId: number, value: number) => setPermissionViaPacket(permissionKey, rankId, value),
userNote: (action: 'add' | 'delete', userId: number, noteId: number, note = '') => userNoteViaPacket(action, userId, noteId, note),
reload: (target: HousekeepingReloadTarget) => reloadViaPacket(target),
revokeBan: (banId: number) => revokeBanViaPacket(banId),
listActionLog: (limit: number, signal?: AbortSignal) => listActionLogViaPacket(limit, signal),
requestList: (listKey: string, targetId: number, signal?: AbortSignal, reveal?: boolean) => requestListViaPacket(listKey, targetId, signal, reveal),
setLockdown: (enabled: boolean) => runHkAction(new HousekeepingLockdownComposer(enabled), 'hotel.lockdown')
} as const;
