feat: add get info ctx menu command
willow hai@wlo.moe
Thu, 05 Jun 2025 18:08:23 +0100
4 files changed,
266 insertions(+),
0 deletions(-)
M
src/config.ts
→
src/config.ts
@@ -19,10 +19,20 @@ iconURL: z.string().optional(),
}), ); +const robloxSchema = z.object({ + apiKey: z.string(), +}); + +const bloxlinkSchema = z.object({ + token: z.string(), +}); + const schema = z.object({ discord: discordSchema, mongodb: mongodbSchema, projects: projectSchema, + bloxlink: bloxlinkSchema, + roblox: robloxSchema, trelloBoardId: z.string().optional(), });
A
src/types/roblox.ts
@@ -0,0 +1,132 @@
+type V2ErrorCode = + | "INVALID_ARGUMENT" + | "PERMISSION_DENIED" + | "NOT_FOUND" + | "ABORTED" + | "RESOURCE_EXHAUSTED" + | "CANCELLED" + | "INTERNAL" + | "NOT_IMPLEMENTED" + | "UNAVAILABLE"; + +interface V2ErrorResponse { + code: V2ErrorCode; + message: string; + details: Array<Record<string, unknown>>; +} + +// v1 resource err model +export type V1ErrorCode = + | "INVALID_ARGUMENT" + | "INSUFFICIENT_SCOPE" + | "PERMISSION_DENIED" + | "NOT_FOUND" + | "ABORTED" + | "RESOURCE_EXHAUSTED" + | "CANCELLED" + | "INTERNAL" + | "NOT_IMPLEMENTED" + | "UNAVAILABLE"; + +export interface V1ErrorResponse { + error: V1ErrorCode; + message: string; + errorDetails: Array<{ + errorDetailType: string; + [key: string]: unknown; + }>; +} + +// OrderedDataStores err model +export interface OrderedDataStoresErrorResponse { + code: V1ErrorCode; + message: string; +} + +// UserSearch api +export interface UserSearchParams { + keyword: string; + sessionId?: string; + limit?: 10 | 25 | 50 | 100; + cursor?: string; +} + +export type UserSearchResponse = + | UserSearchOkResponse + | UserSearchBadResponse + | UserSearchRateLimitResponse; + +export type BLApiResponse = + | { + robloxID: string; + resolved: object; + } + | { + error: "User not found"; + }; + +export type ThumbnailResponse = + | { + done: true; + response: { imageUri: string }; + } + | { + done: false; + }; + +export interface UserSearchPlayer { + previousUsernames: string[]; + hasVerifiedBadge: boolean; + id: number; + name: string; + displayName: string; +} + +export interface UserSearchOkResponse { + previousPageCursor: string | null; + nextPageCursor: string | null; + data: Array<UserSearchPlayer>; +} + +export interface UserSearchBadResponse { + statusCode: 400; + error: "INVALID_ARGUMENT"; + message: "The keyword was filtered." | "The keyword is too short."; +} + +export interface UserSearchRateLimitResponse { + statusCode: 429; + error: "RESOURCE_EXHAUSTED"; + message: "Too many requests."; +} + +export type UserSearchError = + | UserSearchBadResponse + | UserSearchRateLimitResponse; + +export interface GetUserResponse { + path: string; + /** RFC 3339 formatted date string */ + createTime: string; + id: string; + name: string; + displayName: string; + about?: string; + locale: string; + premium: boolean; + idVerified?: boolean; + socialNetworkProfiles: { + facebook?: string; + twitter?: string; + youtube?: string; + twitch?: string; + guilded?: string; + visibility: + | "SOCIAL_NETWORK_VISIBILITY_UNSPECIFIED" + | "NO_ONE" + | "FRIENDS" + | "FRIENDS_AND_FOLLOWING" + | "FRIENDS_FOLLOWING_AND_FOLLOWERS" + | "EVERYONE"; + }; +}
A
src/utils/roblox.ts
@@ -0,0 +1,49 @@
+import config from "@/config"; +import type { + BLApiResponse, + GetUserResponse, + ThumbnailResponse, +} from "@/types/roblox"; + +export async function getRobloxUser(userId: string): Promise<{ + user: GetUserResponse; + thumbnail: ThumbnailResponse; +}> { + const getUserUrl = `https://apis.roblox.com/cloud/v2/users/${userId}`; + const getThumbnailUrl = `https://apis.roblox.com/cloud/v2/users/${userId}:generateThumbnail`; + const [userRes, thumbnailRes] = await Promise.all([ + fetch(getUserUrl, { + headers: { + "Content-Type": "application/json", + "x-api-key": config.data.roblox.apiKey, + }, + }), + fetch(getThumbnailUrl, { + headers: { + "Content-Type": "application/json", + "x-api-key": config.data.roblox.apiKey, + }, + }), + ]); + + const [userData, thumbnailData] = await Promise.all([ + userRes.json(), + thumbnailRes.json(), + ]); + + return { user: userData, thumbnail: thumbnailData }; +} + +export async function getConnectedRobloxUser( + discordId: string, +): Promise<BLApiResponse> { + const getUserUrl = `https://api.blox.link/v4/public/discord-to-roblox/${discordId}`; + + const response = await fetch(getUserUrl, { + headers: { + Authorization: config.data.bloxlink.token, + }, + }); + + return await response.json(); +}