5180 fichiers 38.5 Mo 0 téléchargements 249 vues mis à jour il y a 5 heures ui
nitro-v36-with-renderer-v36 / src / api / housekeeping / HousekeepingApi.ts
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;
15Sujets
1Dépôts
20Membres
2En ligne
Profile
Inscrit
Dernière visite
Messages
0
Votes positifs
0
Votes négatifs
0

Nous utilisons des cookies pour assurer le bon fonctionnement du site et améliorer votre expérience. en savoir plus

Autoriser