import config from "@stealth-developers/config"; export interface RobloxClientOptions { cookie: string; apiKey: string; } export interface RobloxCloudUser { path: string; id: string; createTime: string; name: string; displayName: string; about: string; locale: string; premium: boolean; idVerified: boolean; } export interface UsernameLookupResponse { data: Array<{ requestedUsername: string; hasVerifiedBadge: boolean; id: number; name: string; displayName: string; }>; } export interface UserSearchResponse { previousPageCursor: string | null; nextPageCursor: string | null; data: Array<{ previousUsernames: string[]; hasVerifiedBadge: boolean; id: number; name: string; displayName: string; }>; } export interface ThumbnailOptions { size?: "150" | "352" | "420" | "720"; format?: "PNG" | "JPEG"; shape?: "ROUND" | "SQUARE"; } export interface ThumbnailResponse { imageUri: string; } export interface RobloxOperation { path: string; done: boolean; response?: T; metadata?: unknown; } export interface GameJoinRestriction { active: boolean; startTime?: string; duration?: string; privateReason?: string; displayReason?: string; excludeAltAccounts?: boolean; inherited?: boolean; } export interface UserRestriction { path: string; user: string; gameJoinRestriction: GameJoinRestriction; } export interface ListUserRestrictionsResponse { userRestrictions: UserRestriction[]; nextPageToken?: string; } export interface GameJoinRestrictionLogBehavior { action: "CREATE" | "UPDATE" | "DELETE" | string; duration?: string; privateReason?: string; displayReason?: string; excludeAltAccounts?: boolean; } export interface UserRestrictionLog { path: string; user: string; duration?: string; moderator: | { robloxUser: `users/${string}`; } | { gameScript: unknown; }; createTime: string; active: boolean; privateReason: string; displayReason: string; excludeAltAccounts?: boolean; } export interface ListUserRestrictionLogsResponse { logs: UserRestrictionLog[]; nextPageToken?: string; } export interface RequestOptions { maxPageSize?: number; pageToken?: string; filter?: string; } export class RobloxUserClient { private cookie: string; private apiKey: string; private csrfToken: string | null = null; constructor(options: RobloxClientOptions) { this.cookie = options.cookie.includes(".ROBLOSECURITY=") ? options.cookie : `.ROBLOSECURITY=${options.cookie}`; this.apiKey = options.apiKey; } private async request( url: string, method: "GET" | "POST", body?: unknown, queryParams?: Record, isRetry = false, ): Promise { const headers: Record = { Cookie: this.cookie, Accept: "application/json", ["x-api-key"]: this.apiKey, }; if (body) headers["Content-Type"] = "application/json"; if (this.csrfToken) headers["X-CSRF-TOKEN"] = this.csrfToken; const urlObj = new URL(url); if (queryParams) { Object.entries(queryParams).forEach(([key, val]) => { if (val !== undefined && val !== null) { urlObj.searchParams.append(key, val); } }); } const response = await fetch(urlObj.toString(), { method, headers, ...(method !== "GET" && body ? { body: JSON.stringify(body) } : {}), }); if (response.status === 403) { const csrfHeader = response.headers.get("x-csrf-token"); if (csrfHeader && !isRetry) { this.csrfToken = csrfHeader; return this.request(url, method, body, queryParams, true); } } if (!response.ok) { const errorText = await response.text(); throw new Error(`Roblox API Error [${response.status}]: ${errorText}`); } return response.json() as Promise; } // --- async getUserProfile(userId: string | number): Promise { return this.request(`https://apis.roblox.com/cloud/v2/users/${userId}`, "GET"); } // --- async getUsersByUsernames( usernames: string[], excludeBannedUsers = false, ): Promise { return this.request( "https://users.roblox.com/v1/usernames/users", "POST", { usernames, excludeBannedUsers }, ); } async getUserId(username: string): Promise { const result = await this.getUsersByUsernames([username]); const user = result.data[0]; return user ? user.id : null; } async getUserIds(usernames: string[]): Promise { const result = await this.getUsersByUsernames(usernames); return result.data.map((user) => user.id); } // --- async searchUsers( keyword: string, options?: { limit?: number; cursor?: string }, ): Promise { const queryParams: Record = { keyword }; if (options?.limit) queryParams.limit = options.limit.toString(); if (options?.cursor) queryParams.cursor = options.cursor; return this.request( "https://users.roblox.com/v1/users/search", "GET", undefined, queryParams, ); } // --- async generateUserThumbnail( userId: string | number, options?: ThumbnailOptions, ): Promise> { return this.request>( `https://apis.roblox.com/cloud/v2/users/${userId}:generateThumbnail`, "GET", undefined, options as Record, ); } async getAvatarUrl(userId: string | number, options?: ThumbnailOptions): Promise { const operation = await this.generateUserThumbnail(userId, options); if (operation.done && operation.response) { return operation.response.imageUri ?? null; } else { const finalResult = await this.pollThumbnailOperation( userId, operation.path.split("/").pop()!, ); return finalResult.imageUri; } } async pollThumbnailOperation( userId: string | number, operationId: string, intervalMs = 1000, maxRetries = 15, ): Promise { let attempts = 0; while (attempts < maxRetries) { const result = await this.request>( `https://apis.roblox.com/cloud/v2/users/${userId}/operations/${operationId}`, "GET", ); if (result.done) { if (result.response) return result.response; throw new Error("Operation completed but response data was missing."); } attempts++; await new Promise((resolve) => setTimeout(resolve, intervalMs)); } throw new Error(`Polling operation timed out after ${maxRetries} attempts.`); } // --- async listUserRestrictions( universeId: string | number, options?: RequestOptions, ): Promise { const queryParams: Record = {}; if (options?.maxPageSize) queryParams.maxPageSize = options.maxPageSize.toString(); if (options?.pageToken) queryParams.pageToken = options.pageToken; if (options?.filter) queryParams.filter = options.filter; return this.request( `https://apis.roblox.com/user/cloud/v2/universes/${universeId}/user-restrictions`, "GET", undefined, queryParams, ); } // --- async getUserRestriction( universeId: string | number, userRestrictionId: string | number, ): Promise { const restrictionId = typeof userRestrictionId === "number" || !userRestrictionId.includes("/") ? `users/${userRestrictionId}` : userRestrictionId; return this.request( `https://apis.roblox.com/user/cloud/v2/universes/${universeId}/user-restrictions/${restrictionId}`, "GET", ); } // --- async listUserRestrictionLogs( universeId: string | number, options?: RequestOptions, ): Promise { const queryParams: Record = {}; if (options?.maxPageSize) queryParams.maxPageSize = options.maxPageSize.toString(); if (options?.pageToken) queryParams.pageToken = options.pageToken; if (options?.filter) queryParams.filter = options.filter; return this.request( `https://apis.roblox.com/user/cloud/v2/universes/${universeId}/user-restrictions:listLogs`, "GET", undefined, queryParams, ); } } export const client = new RobloxUserClient({ cookie: config.roblox.cookie, apiKey: config.roblox.apiKey, });