import config from "@stealth-developers/config"; import type { ApiError, ApiErrorV2, ErrorCode, GatewayError, RobloxResult } from "./types"; import type { GenerateThumbnailOptions, GenerateThumbnailResponse, GetUserResponse, GetUsersByUsernamesPayload, GetUsersByUsernamesResponse, } from "./types/users"; const API_KEY = config.roblox.apiKey; const COOKIE = config.roblox.cookie; const ERROR_CODES: Set = new Set([ "INVALID_ARGUMENT", "PERMISSION_DENIED", "NOT_FOUND", "ABORTED", "RESOURCE_EXHAUSTED", "CANCELLED", "INTERNAL", "NOT_IMPLEMENTED", "UNAVAILABLE", ]); function isErrorCode(value: string): value is ErrorCode { return ERROR_CODES.has(value); } export function isApiErrorV2(x: unknown): x is ApiErrorV2 { return ( typeof x === "object" && x !== null && "code" in x && "message" in x && typeof (x as Record).code === "string" && typeof (x as Record).message === "string" && isErrorCode((x as Record).code as string) ); } export function isGatewayError(x: unknown): x is GatewayError { return ( typeof x === "object" && x !== null && "errors" in x && Array.isArray((x as Record).errors) ); } function isApiError(x: unknown): x is ApiError { return isApiErrorV2(x) || isGatewayError(x); } function errorFromStatus(status: number, statusText: string): ApiErrorV2 { const codeMap: Record = { 400: "INVALID_ARGUMENT", 403: "PERMISSION_DENIED", 404: "NOT_FOUND", 409: "ABORTED", 429: "RESOURCE_EXHAUSTED", 499: "CANCELLED", 500: "INTERNAL", 501: "NOT_IMPLEMENTED", 503: "UNAVAILABLE", }; return { code: codeMap[status] ?? "INTERNAL", message: statusText, }; } export async function makeRequest(path: string, init?: RequestInit): Promise> { let baseUrl = "https://apis.roblox.com"; if (path.startsWith("https://")) baseUrl = ""; try { const res = await fetch(`${baseUrl}${path}`, { ...init, headers: { Cookie: COOKIE, "x-api-key": API_KEY, accept: "application/json", "Content-Type": "application/json", UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36", ...init?.headers, }, }); const body = await res.json(); if (!res.ok || isApiError(body)) { const err: ApiError = isApiError(body) ? body : errorFromStatus(res.status, res.statusText); return [null, err]; } return [body as T, null]; } catch (e) { return [null, { code: "INTERNAL", message: String(e) }]; } } export async function getUser(userId: string) { const path = `/cloud/v2/users/${userId}`; return makeRequest(path); } export async function getThumbnail(userId: string, options?: GenerateThumbnailOptions) { const _path = `/cloud/v2/users/${userId}:generateThumbnail`; const query = new URLSearchParams(); if (options) { for (const [key, value] of Object.entries(options)) { query.set(key, value.toString()); } } const path = `${_path}?${query.toString()}`; return makeRequest(path); } export async function getUsersByUsernames(usernames: string[]) { const payload: GetUsersByUsernamesPayload = { usernames, excludeBannedUsers: false, }; const path = "https://users.roblox.com/v1/usernames/users"; return makeRequest(path, { method: "POST", body: JSON.stringify(payload), }); }