5180 fichiers 38.5 Mo 0 téléchargements 191 vues mis à jour il y a 5 heures ui
nitro-v36-with-renderer-v36 / src / api / wired / variablesExplorer.ts
variablesExplorer.ts 10.1 Ko Télécharger le fichier
import type { IWiredVariableHolder } from '@octane/renderer';
import {
    VariablesWebApiError,
    WebApiEntry,
    WebApiFurniTargetKind,
    WebApiHolderScope,
    WebApiProfile,
    WebApiSortField,
    WebApiSortOrder,
    WebApiTargetKind,
    WebApiUserTargetKind,
    WebApiVariable
} from './variablesWebApi';
import { normalizeWebApiBaseUrl, WebApiHotel } from './WiredWebApi';

/** Pure pieces of the Variables Explorer window. */

export interface ExplorerConnectInput {
    hotelUrl: string;
    roomId: string;
    readKey: string;
    writeKey: string;
}

export interface ExplorerConnection {
    hotelUrl: string;
    roomId: number;
    readKey: string;
    writeKey: string;
}

export type ExplorerConnectField = keyof ExplorerConnectInput;

const KEY_PATTERN = /^[A-Za-z0-9_-]{20,128}$/;
const MAX_ROOM_ID = 2147483647;

export const sanitizeRoomIdInput = (value: string): string => (value ?? '').replace(/[^0-9]/g, '').slice(0, 10);

export const validateExplorerConnect = (
    input: ExplorerConnectInput,
    hotels: WebApiHotel[]
): { errors: Partial<Record<ExplorerConnectField, string>>; connection: ExplorerConnection | null } => {
    const errors: Partial<Record<ExplorerConnectField, string>> = {};
    const hotelUrl = normalizeWebApiBaseUrl(input.hotelUrl);
    const readKey = (input.readKey ?? '').trim();
    const writeKey = (input.writeKey ?? '').trim();
    const roomText = (input.roomId ?? '').trim();
    const roomId = /^[0-9]{1,10}$/.test(roomText) ? Number(roomText) : 0;

    if (!hotelUrl || !hotels.some((hotel) => hotel.url === hotelUrl)) errors.hotelUrl = 'Pick a hotel';
    if (roomId <= 0 || roomId > MAX_ROOM_ID) errors.roomId = 'Enter a room id';
    if (readKey && !KEY_PATTERN.test(readKey)) errors.readKey = 'This is not a valid key';
    if (writeKey && !KEY_PATTERN.test(writeKey)) errors.writeKey = 'This is not a valid key';
    if (!readKey && !writeKey) errors.readKey = 'Enter a read key or a write key';

    if (Object.keys(errors).length) return { errors, connection: null };

    return { errors, connection: { hotelUrl, roomId, readKey, writeKey } };
};

const REMEMBER_PREFIX = 'octane.variables_explorer.keys';

export const rememberedKeysStorageKey = (hotelUrl: string, roomId: number): string => `${REMEMBER_PREFIX}:${hotelUrl}:${roomId}`;

export const loadRememberedKeys = (
    hotelUrl: string,
    roomId: number,
    storage: Storage | null = safeLocalStorage()
): { readKey: string; writeKey: string } | null => {
    if (!storage || !hotelUrl || !roomId) return null;

    try {
        const raw = storage.getItem(rememberedKeysStorageKey(hotelUrl, roomId));

        if (!raw) return null;

        const parsed = JSON.parse(raw) as { readKey?: unknown; writeKey?: unknown };

        return {
            readKey: typeof parsed?.readKey === 'string' ? parsed.readKey : '',
            writeKey: typeof parsed?.writeKey === 'string' ? parsed.writeKey : ''
        };
    } catch {
        return null;
    }
};

export const saveRememberedKeys = (connection: ExplorerConnection, remember: boolean, storage: Storage | null = safeLocalStorage()): void => {
    if (!storage) return;

    const key = rememberedKeysStorageKey(connection.hotelUrl, connection.roomId);

    try {
        if (remember) storage.setItem(key, JSON.stringify({ readKey: connection.readKey, writeKey: connection.writeKey }));
        else storage.removeItem(key);
    } catch {}
};

function safeLocalStorage(): Storage | null {
    try {
        return typeof window !== 'undefined' ? window.localStorage : null;
    } catch {
        return null;
    }
}

export const USER_TARGET_KINDS: WebApiUserTargetKind[] = ['users', 'pets', 'bots'];
/** Habbo's furni kinds; Builders Club items have their own. */
export const FURNI_TARGET_KINDS: WebApiFurniTargetKind[] = ['furni', 'wall-items', 'furni-bc', 'wall-items-bc'];

export const targetKindsOf = (scope: WebApiHolderScope): WebApiTargetKind[] => (scope === 'furni' ? FURNI_TARGET_KINDS : USER_TARGET_KINDS);

export const isWallTargetKind = (kind: string): boolean => kind === 'wall-items' || kind === 'wall-items-bc';

export const targetKindLabel = (kind: string): string => {
    switch (kind) {
        case 'users':
            return 'Habbo';
        case 'pets':
            return 'Pet';
        case 'bots':
            return 'Bot';
        case 'furni':
            return 'Floor furni';
        case 'wall-items':
            return 'Wall furni';
        case 'furni-bc':
            return 'Builders Club floor furni';
        case 'wall-items-bc':
            return 'Builders Club wall furni';
        default:
            return kind || 'Room';
    }
};

export const holderScopeOfKind = (kind: WebApiTargetKind): WebApiHolderScope => (FURNI_TARGET_KINDS.includes(kind as WebApiFurniTargetKind) ? 'furni' : 'user');

export interface ExplorerSortOption {
    id: string;
    label: string;
    sort: WebApiSortField;
    order: WebApiSortOrder;
}

export const EXPLORER_SORT_OPTIONS: ExplorerSortOption[] = [
    { id: 'id_asc', label: 'Entity id', sort: 'id', order: 'asc' },
    { id: 'value_desc', label: 'Highest value', sort: 'value', order: 'desc' },
    { id: 'value_asc', label: 'Lowest value', sort: 'value', order: 'asc' },
    { id: 'id_desc', label: 'Newest entity id', sort: 'id', order: 'desc' },
    { id: 'updated_desc', label: 'Last updated', sort: 'update_time', order: 'desc' }
];

export const sortOptionById = (id: string): ExplorerSortOption => EXPLORER_SORT_OPTIONS.find((option) => option.id === id) ?? EXPLORER_SORT_OPTIONS[0];

export const formatUnixSeconds = (seconds: number | null | undefined): string => {
    if (!seconds || !Number.isFinite(seconds) || seconds <= 0) return '';

    const date = new Date(seconds * 1000);
    const pad = (value: number) => String(value).padStart(2, '0');

    return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
};

/** The server's holder kinds as the owners table knows them (`HOLDER_TYPE_USER` / `HOLDER_TYPE_FURNI`). */
export const webApiEntryToHolder = (entry: WebApiEntry, scope: WebApiHolderScope, kind: WebApiTargetKind): IWiredVariableHolder => ({
    entityType: scope === 'furni' ? 2 : 1,
    entityId: entry.entityId,
    entityName: entry.name || `${targetKindLabel(kind)} #${entry.entityId}`,
    storage: {
        value: entry.value ?? 0,
        creationTime: (entry.createdAt ?? 0) * 1000,
        creationTimeStr: formatUnixSeconds(entry.createdAt),
        lastUpdateTime: (entry.updatedAt ?? 0) * 1000,
        lastUpdateTimeStr: formatUnixSeconds(entry.updatedAt)
    }
});

export interface ExplorerProfileEntry {
    name: string;
    hasValue: boolean;
    value: number | null;
    createdAt: number;
    updatedAt: number;
}

/** A profile's variables in name order, with `hasValue` taken from the room's variable list when known. */
export const profileToEntries = (profile: WebApiProfile | null, variables: WebApiVariable[]): ExplorerProfileEntry[] => {
    if (!profile?.variables) return [];

    return Object.entries(profile.variables)
        .map(([name, stored]) => {
            const definition = variables.find((variable) => variable.name === name);
            const hasValue = definition ? definition.hasValue : typeof stored?.value === 'number';

            return {
                name,
                hasValue,
                value: hasValue && typeof stored?.value === 'number' ? stored.value : null,
                createdAt: stored?.createdAt ?? 0,
                updatedAt: stored?.updatedAt ?? 0
            };
        })
        .sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: 'base' }));
};

/** Variables of the scope that the holder does not have yet. */
export const giveableVariables = (variables: WebApiVariable[], scope: WebApiHolderScope, entries: ExplorerProfileEntry[]): WebApiVariable[] =>
    variables
        .filter((variable) => variable.scope === scope && !entries.some((entry) => entry.name === variable.name))
        .sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: 'base' }));

/** A 32-bit signed integer typed by the user, or `null`. */
export const parseInt32 = (text: string): number | null => {
    const trimmed = (text ?? '').trim();

    if (!/^-?\d{1,10}$/.test(trimmed)) return null;

    const value = Number(trimmed);

    return value >= -2147483648 && value <= 2147483647 ? value : null;
};

/** What each of the API's error codes means for the person using the explorer. */
const ERROR_MESSAGES: Record<string, string> = {
    'wired.variables.key_missing': 'This needs the write key.',
    'wired.variables.key_invalid': 'The key was not accepted.',
    'wired.variables.api_disabled': 'The Variables Web API add-on does not work in this room.',
    'wired.variables.bulk_delete_not_enabled': 'This write key may not bulk delete. Allow mass deletion in the Web API add-on first.',
    'wired.variables.operation_not_allowed': 'This room does not allow that.',
    'wired.variables.user_not_participating': 'That user is not in the room and has no saved variables in it.',
    'wired.variables.entity_not_found': 'Nothing with that id is in the room.',
    'wired.variables.not_found': 'Not found.',
    'room.not_found': 'That room was not found.',
    'wired.variables.unknown_endpoint': 'The Variables Web API is not available on this hotel.',
    'wired.variables.invalid_value': 'That value is not accepted.',
    'wired.variables.invalid_target': 'That holder is not valid for this variable.',
    'wired.variables.bulk_delete_invalid_variable': 'That variable is not in this room.',
    'timeout': 'The hotel did not answer in time.',
    'network': 'The hotel could not be reached.'
};

export const explorerErrorMessage = (error: unknown): string => {
    if (error instanceof VariablesWebApiError) {
        if (error.code === 'wired.variables.too_many_requests') return `Too many requests. Try again in ${error.retryAfterSeconds ?? 10} s.`;
        if (error.code === 'no_key') return error.message;

        return ERROR_MESSAGES[error.code] ?? (error.message || 'Something went wrong.');
    }

    return error instanceof Error && error.message ? error.message : 'Something went wrong.';
};
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