UNPKG

@haelp/teto

Version:

A typescript-based controllable TETR.IO client.

1 lines 75.9 kB
{"version":3,"sources":["../../src/channel/index.ts"],"sourcesContent":["const NodeError = Error;\n\nexport namespace ChannelAPI {\n export class Error extends NodeError {\n constructor(type: string, message: string) {\n super(`[CH API] ${type}: ${message}`);\n }\n }\n\n export const randomSessionID = (length = 20) =>\n Array.from(\n { length },\n () =>\n [\"qwertyuiop[asdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890\"][\n Math.floor(Math.random() * (26 + 26 + 10))\n ]\n ).join(\"\");\n\n const config: Types.Config = {\n sessionID: randomSessionID(),\n host: \"https://ch.tetr.io/api/\",\n caching: true\n };\n\n export const getConfig = () => config;\n export const setConfig = (newConfig: Partial<Types.Config>) => {\n Object.assign(config, newConfig);\n };\n\n const cache: { [key: string]: { until: number; data: any } } = {};\n export const clearCache = () => {\n Object.keys(cache).forEach((k) => delete cache[k]);\n };\n\n export interface GetOptions {\n sessionID?: string | null;\n host?: string;\n }\n\n export const get = async <Res = any>({\n route,\n args = {\n data: {},\n format: []\n },\n query = {},\n options = config\n }: {\n route: string;\n args?: {\n data: { [k: string]: string };\n format: string[];\n };\n query?: { [k: string]: string };\n options?: GetOptions;\n }): Promise<Res> => {\n let uri = route;\n\n args.format.forEach((arg) => {\n if (arg in args.data) {\n uri = uri.replaceAll(`:${arg}`, args.data[arg]);\n } else {\n throw new Error(\n \"Argument Error\",\n `Missing argument ${arg.toString()} in route ${route}`\n );\n }\n });\n\n Object.keys(query).forEach((key) => {\n uri += `?${key}=${query[key]}`;\n });\n\n if (config.caching && cache[uri]) {\n if (cache[uri].until > Date.now()) {\n return cache[uri].data;\n } else {\n delete cache[uri];\n }\n }\n\n let res: ChannelAPI.Types.Response;\n try {\n res = (await fetch(`${options.host || config.host}${uri}`, {\n headers:\n options.sessionID || config.sessionID\n ? {\n \"X-Session-ID\": options.sessionID || config.sessionID!\n }\n : {}\n }).then((r) => r.json())) as any;\n if (res.success === false) {\n throw new Error(\"Server Error\", `${res.error.msg} at ${uri}`);\n } else {\n if (config.caching) {\n cache[uri] = {\n until: res.cache.cached_until,\n data: res.data\n };\n }\n return res.data;\n }\n } catch (e: any) {\n if (e instanceof Error) throw e;\n throw new Error(\"Network Error\", `${e.message} at ${uri}`);\n }\n };\n\n export namespace generator {\n export const empty =\n <Res extends object, ResKey extends keyof Res | undefined = undefined>(\n route: string,\n res?: ResKey\n ) =>\n async (): Promise<\n ResKey extends undefined ? Res : Res[Extract<ResKey, keyof Res>]\n > => {\n const r = await get({ route });\n if (res) return r[res];\n return r;\n };\n export const args = <\n Req extends object,\n Res extends object,\n ArgValues extends any[],\n ResKey extends keyof Res | undefined = undefined\n >(\n route: string,\n res?: ResKey\n ) => {\n const base = route\n .split(\"/\")\n .filter((v) => v.startsWith(\":\"))\n .map((v) => v.slice(1));\n async function getArgs(\n ...args: ArgValues | [Types.ArgsObject<Req>]\n ): Promise<\n ResKey extends undefined ? Res : Res[Extract<ResKey, keyof Res>]\n > {\n const argData: { [k: string]: string } = {};\n if (typeof args[0] === \"string\") {\n if (args.length !== base.length)\n throw new Error(\n \"Argument Error\",\n `Invalid number of arguments for ${route}: Expected ${base.length}, found ${args.length}`\n );\n base.forEach((v, i) => (argData[v as any] = args[i] as string));\n } else {\n base.forEach((v) => {\n if (!(v in args[0]))\n throw new Error(\n \"Argument Error\",\n `Missing argument ${v.toString()} for ${route}`\n );\n argData[v as any] = (args[0] as { [k: string]: string })[v as any];\n });\n }\n\n const r = await get({\n route,\n args: { data: argData, format: base as string[] }\n });\n if (res) return r[res];\n return r;\n }\n return getArgs;\n };\n\n export const query =\n <\n Res extends object,\n QueryParams extends object,\n ResKey extends keyof Res | undefined = undefined\n >(\n route: string,\n res?: ResKey\n ) =>\n async (\n query: QueryParams = {} as any\n ): Promise<\n ResKey extends undefined ? Res : Res[Extract<ResKey, keyof Res>]\n > => {\n const r = await get({ route, query: query as any });\n if (res) return r[res];\n return r;\n };\n\n export const argsAndQuery = <\n Req extends object,\n Res extends object,\n QueryParams extends object,\n ArgValues extends any[],\n ResKey extends keyof Res | undefined = undefined\n >(\n route: string,\n res?: ResKey\n ) => {\n const base = route\n .split(\"/\")\n .filter((v) => v.startsWith(\":\"))\n .map((v) => v.slice(1));\n async function getArgsAndQuery(\n ...args:\n | [...ArgValues, QueryParams]\n | ArgValues\n | [Types.ArgsObject<Req>, QueryParams]\n | [Types.ArgsObject<Req>]\n ): Promise<\n ResKey extends undefined ? Res : Res[Extract<ResKey, keyof Res>]\n > {\n const argData: { [k: string]: string } = {};\n let query: QueryParams = {} as QueryParams;\n if (typeof args[0] === \"string\") {\n if (\n args.length === base.length + 1 &&\n typeof args[base.length] === \"object\"\n ) {\n query = args.pop() as QueryParams;\n }\n if (args.length !== base.length)\n throw new Error(\n \"Argument Error\",\n `Invalid number of arguments for ${route}: Expected ${base.length}, found ${args.length}`\n );\n base.forEach((v, i) => (argData[v as any] = args[i] as string));\n } else {\n base.forEach((v) => {\n if (!(v in args))\n throw new Error(\n \"Argument Error\",\n `Missing argument ${v.toString()} for ${route}`\n );\n argData[v as any] = (args[0] as { [k: string]: string })[v as any];\n });\n if (typeof args[1] === \"object\") {\n query = args[1] as QueryParams;\n }\n }\n\n const r = await get({\n route,\n args: { data: argData, format: base as string[] },\n query: query as any\n });\n if (res) return r[res];\n return r;\n }\n return getArgsAndQuery;\n };\n }\n\n export namespace general {\n export namespace Stats {\n /**\n * Some statistics about the service.\n */\n export interface Response {\n /**\n * The amount of users on the server, including anonymous accounts.\n */\n usercount: number;\n /**\n * The amount of users created a second (through the last minute).\n */\n usercount_delta: number;\n /**\n * The amount of anonymous accounts on the server.\n */\n anoncount: number;\n /**\n * The total amount of accounts ever created (including pruned anons etc.).\n */\n totalaccounts: number;\n /**\n * The amount of ranked (visible in TETRA LEAGUE leaderboard) accounts on the server.\n */\n rankedcount: number;\n /**\n * The amount of game records stored on the server.\n */\n recordcount: number;\n /**\n * The amount of games played across all users, including both off- and online modes.\n */\n gamesplayed: number;\n /**\n * The amount of games played a second (through the last minute).\n */\n gamesplayed_delta: number;\n /**\n * The amount of games played across all users, including both off- and online modes, excluding games that were not completed (e.g. retries)\n */\n gamesfinished: number;\n /**\n * The amount of seconds spent playing across all users, including both off- and online modes.\n */\n gametime: number;\n /**\n * The amount of keys pressed across all users, including both off- and online modes.\n */\n inputs: number;\n /**\n * The amount of pieces placed across all users, including both off- and online modes.\n */\n piecesplaced: number;\n }\n }\n /**\n * Gets statistics about TETR.IO\n */\n export const stats = generator.empty<Stats.Response>(\"general/stats\");\n export namespace Activity {\n /**\n * A graph of user activity over the last 2 days. A user is seen as active if they logged in or received XP within the last 30 minutes.\n */\n export interface Response {\n /**\n * An array of plot points, newest points first.\n */\n activity: number[];\n }\n export interface Request {}\n }\n\n /**\n * Gets a graph of user activity over the last 2 days. A user is seen as active if they logged in or received XP within the last 30 minutes.\n */\n export const activity = generator.empty<Activity.Response, \"activity\">(\n \"general/activity\",\n \"activity\"\n );\n }\n\n export namespace users {\n /**\n * An object describing the user in detail.\n */\n export interface Response extends ChannelAPI.Types.User {}\n export interface Request {\n /**\n * The lowercase username or user ID to look up.\n */\n user: string;\n }\n\n export const get: {\n (user: string): Promise<Response>;\n ({ user }: { user: string }): Promise<Response>;\n } = generator.args<Request, Response, [string]>(\"users/:user\");\n\n export namespace summaries {\n export namespace FourtyLines {\n /**\n * An object describing a summary of the user's 40 LINES games.\n */\n export interface Response\n extends ChannelAPI.Types.BaseSummaryResponse {}\n export interface Request {\n /**\n * The lowercase username or user ID to look up.\n */\n user: string;\n }\n }\n\n export const fourtyLines: {\n (user: string): Promise<FourtyLines.Response>;\n ({ user }: { user: string }): Promise<FourtyLines.Response>;\n } = generator.args<FourtyLines.Request, FourtyLines.Response, [string]>(\n \"users/:user/summaries/40l\"\n );\n\n export namespace Blitz {\n /**\n * An object describing a summary of the user's BLITZ games.\n */\n export interface Response\n extends ChannelAPI.Types.BaseSummaryResponse {}\n export interface Request {\n /**\n * The lowercase username or user ID to look up.\n */\n user: string;\n }\n }\n export const blitz: {\n (user: string): Promise<Blitz.Response>;\n ({ user }: { user: string }): Promise<Blitz.Response>;\n } = generator.args<Blitz.Request, Blitz.Response, [string]>(\n \"users/:user/summaries/BLITZ\"\n );\n\n export namespace QuickPlay {\n export interface Request {\n /**\n * The lowercase username or user ID to look up.\n */\n user: string;\n }\n /**\n * An object describing a summary of the user's QUICK PLAY games.\n */\n export interface Response extends ChannelAPI.Types.BaseSummaryResponse {\n /**\n * The user's career best:\n */\n best: {\n /**\n * The user's best record, or null if the user hasn't placed one yet.\n */\n record?: ChannelAPI.Types.Record;\n /**\n * The rank said record had in global leaderboards at the end of the week, or -1 if it was not ranked.\n */\n rank: number;\n };\n }\n }\n export const quickPlay: {\n (user: string): Promise<QuickPlay.Response>;\n ({ user }: { user: string }): Promise<QuickPlay.Response>;\n } = generator.args<QuickPlay.Request, QuickPlay.Response, [string]>(\n \"users/:user/summaries/zenith\"\n );\n /** Alias of quickPlay */\n export const zenith = quickPlay;\n\n export namespace ExpertQuickPlay {\n /**\n * An object describing a summary of the user's EXPERT QUICK PLAY games.\n */\n export interface Response extends ChannelAPI.Types.BaseSummaryResponse {\n /**\n * The user's career best:\n */\n best: {\n /**\n * The user's best record, or null if the user hasn't placed one yet.\n */\n record?: ChannelAPI.Types.Record;\n /**\n * The rank said record had in global leaderboards at the end of the week, or -1 if it was not ranked.\n */\n rank: number;\n };\n }\n export interface Request {\n /**\n * The lowercase username or user ID to look up.\n */\n user: string;\n }\n }\n export const expertQuickPlay: {\n (user: string): Promise<ExpertQuickPlay.Response>;\n ({ user }: { user: string }): Promise<ExpertQuickPlay.Response>;\n } = generator.args<\n ExpertQuickPlay.Request,\n ExpertQuickPlay.Response,\n [string]\n >(\"users/:user/summaries/zenithex\");\n /** Alias of expertQuickPlay */\n export const zenthiex = expertQuickPlay;\n\n export namespace TetraLeague {\n /**\n * An object describing a summary of the user's TETRA LEAGUE standing.\n */\n export interface Response {\n gamesplayed: number;\n gameswon: number;\n glicko: number;\n rd?: number;\n decaying: boolean;\n tr: number;\n gxe: number;\n rank: string;\n bestrank?: string;\n apm?: number;\n pps?: number;\n vs?: number;\n standing?: number;\n standing_local?: number;\n percentile?: number;\n percentile_rank?: string;\n next_rank?: string;\n prev_rank?: string;\n next_at?: number;\n prev_at?: number;\n /**\n * An object mapping past season IDs to past season final placement information. A season will include the following:\n */\n past: {\n [key: string]: {\n /**\n * The season ID.\n */\n season: string;\n /**\n * The username the user had at the time.\n */\n username: string;\n /**\n * The country the user represented at the time.\n */\n country?: string;\n /**\n * This user's final position in the season's global leaderboards.\n */\n placement?: number;\n /**\n * Whether the user was ranked at the time of the season's end.\n */\n ranked: boolean;\n /**\n * The amount of TETRA LEAGUE games played by this user.\n */\n gamesplayed: number;\n /**\n * The amount of TETRA LEAGUE games won by this user.\n */\n gameswon: number;\n /**\n * This user's final Glicko-2 rating.\n */\n glicko: number;\n /**\n * This user's final Glicko-2 Rating Deviation.\n */\n rd: number;\n /**\n * This user's final TR (Tetra Rating).\n */\n tr: number;\n /**\n * This user's final GLIXARE score (a % chance of beating an average player).\n */\n gxe: number;\n /**\n * This user's final letter rank. z is unranked.\n */\n rank: string;\n /**\n * This user's highest achieved rank in the season.\n */\n bestrank?: string;\n /**\n * This user's average APM (attack per minute) over the last 10 games in the season.\n */\n apm: number;\n /**\n * This user's average PPS (pieces per second) over the last 10 games in the season.\n */\n pps: number;\n /**\n * This user's average VS (versus score) over the last 10 games in the season.\n */\n vs: number;\n };\n };\n }\n export interface Request {\n /**\n * The lowercase username or user ID to look up.\n */\n user: string;\n }\n }\n export const tetraLeague: {\n (user: string): Promise<TetraLeague.Response>;\n ({ user }: { user: string }): Promise<TetraLeague.Response>;\n } = generator.args<TetraLeague.Request, TetraLeague.Response, [string]>(\n \"users/:user/summaries/league\"\n );\n /** Alias of tetraLeague */\n export const tl = tetraLeague;\n\n export namespace Zen {\n /**\n * An object describing a summary of the user's ZEN progress.\n */\n export interface Response {\n /**\n * The user's level.\n */\n level: number;\n /**\n * The user's score.\n */\n score: number;\n }\n export interface Request {\n /**\n * The lowercase username or user ID to look up.\n */\n user: string;\n }\n }\n export const zen: {\n (user: string): Promise<Zen.Response>;\n ({ user }: { user: string }): Promise<Zen.Response>;\n } = generator.args<Zen.Request, Zen.Response, [string]>(\n \"users/:user/summaries/zen\"\n );\n\n export namespace Achievements {\n /**\n * An object containing all the user's achievements.\n */\n export interface Response {\n achievements: ChannelAPI.Types.Achievement[];\n }\n export interface Request {\n /**\n * The lowercase username or user ID to look up.\n */\n user: string;\n }\n }\n export const achievements: {\n (user: string): Promise<Achievements.Response[\"achievements\"]>;\n ({\n user\n }: {\n user: string;\n }): Promise<Achievements.Response[\"achievements\"]>;\n } = generator.args<\n Achievements.Request,\n Achievements.Response,\n [string],\n \"achievements\"\n >(\"users/:user/summaries/achievements\", \"achievements\");\n\n export namespace All {\n /**\n * An object containing all the user's summaries in one.\n */\n export interface Response {\n /**\n * See User Summary: 40 LINES.\n */\n \"40l\": FourtyLines.Response;\n /**\n * See User Summary: BLITZ.\n */\n blitz: Blitz.Response;\n /**\n * See User Summary: QUICK PLAY.\n */\n zenith: QuickPlay.Response;\n /**\n * See User Summary: EXPERT QUICK PLAY.\n */\n zenithex: ExpertQuickPlay.Response;\n /**\n * See User Summary: TETRA LEAGUE.\n */\n league: TetraLeague.Response;\n /**\n * See User Summary: ZEN.\n */\n zen: Zen.Response;\n /**\n * See User Summary: Achievements.\n */\n achievements: Achievements.Response;\n }\n export interface Request {\n /**\n * The lowercase username or user ID to look up.\n */\n user: string;\n }\n }\n export const all: {\n (user: string): Promise<All.Response>;\n ({ user }: { user: string }): Promise<All.Response>;\n } = generator.args<All.Request, All.Response, [string]>(\n \"users/:user/summaries\"\n );\n }\n\n export namespace Search {\n /**\n * An object describing the user found, or null if none found.\n */\n export interface Response {\n /**\n * The requested user:\n */\n users?: {\n /**\n * The user's internal ID.\n */\n _id: string;\n /**\n * The user's username.\n */\n username: string;\n }[];\n }\n\n export type Query =\n | `discord:id:${string}` // Discord user ID (snowflake)\n | `discord:username:${string}` // Discord username\n | `twitch:id:${string}` // Twitch user ID\n | `twitch:username:${string}` // Twitch username (URL)\n | `twitch:display_username:${string}` // Twitch display name\n | `twitter:id:${string}` // X (Twitter) user ID\n | `twitter:username:${string}` // X handle (URL)\n | `twitter:display_username:${string}` // X display name\n | `reddit:id:${string}` // Reddit user ID\n | `reddit:username:${string}` // Reddit username\n | `youtube:id:${string}` // YouTube user ID\n | `youtube:username:${string}` // YouTube display name\n | `steam:id:${string}` // SteamID\n | `steam:username:${string}`; // Steam display name\n\n export interface Request {\n /**\n * The social connection to look up. Must be one of:\n\t\t\t\t\tdiscord:id:<snowflake> - a Discord user ID\n\t\t\t\t\tdiscord:username:<username> - a Discord username\n\t\t\t\t\ttwitch:id:<userid> - a Twitch user ID\n\t\t\t\t\ttwitch:username:<username> - a Twitch username (as used in the URL)\n\t\t\t\t\ttwitch:display_username:<username> - a Twitch display name (may include Unicode)\n\t\t\t\t\ttwitter:id:<userid> - an X user ID\n\t\t\t\t\ttwitter:username:<handle> - an X handle (as used in the URL)\n\t\t\t\t\ttwitter:display_username:<username> - an X display name (may include Unicode)\n\t\t\t\t\treddit:id:<userid> - a Reddit user ID\n\t\t\t\t\treddit:username:<username> - a Reddit username\n\t\t\t\t\tyoutube:id:<userid> - a YouTube user ID (as used in the URL)\n\t\t\t\t\tyoutube:username:<username> - a YouTube display name\n\t\t\t\t\tsteam:id:<steamid> - a SteamID\n\t\t\t\t\tsteam:username:<username> - a Steam display name\n */\n query: Query;\n }\n }\n export const search: {\n (query: Search.Query): Promise<Search.Response[\"users\"]>;\n ({ query }: { query: Search.Query }): Promise<Search.Response[\"users\"]>;\n } = generator.args<Search.Request, Search.Response, [string], \"users\">(\n \"users/search/:query\",\n \"users\"\n );\n\n export namespace Leaderboard {\n export interface Response {\n /**\n * The matched users:\n */\n entries: ChannelAPI.Types.LeaderboardEntry[];\n }\n export interface Request {\n /**\n * The leaderboard to sort users by. Must be one of:\n * league — the TETRA LEAGUE leaderboard.\n * xp — the XP leaderboard.\n * ar — the Achievement Rating leaderboard.\n */\n leaderboard: \"league\" | \"xp\" | \"ar\";\n }\n export interface QueryParams {\n /**\n * The upper bound. Use this to paginate downwards: take the lowest seen prisecter and pass that back through this field to continue scrolling.\n */\n after?: string;\n /**\n * The lower bound. Use this to paginate upwards: take the highest seen prisecter and pass that back through this field to continue scrolling. If set, the search order is reversed (returning the lowest items that match the query)\n */\n before?: string;\n /**\n * The amount of entries to return, between 1 and 100. 50 by default.\n */\n limit?: number;\n /**\n * The ISO 3166-1 country code to filter to. Leave unset to not filter by country.\n */\n country?: string;\n }\n }\n export const leaderboard: {\n (\n leaderboard: Leaderboard.Request[\"leaderboard\"]\n ): Promise<Leaderboard.Response[\"entries\"]>;\n ({\n leaderboard\n }: {\n leaderboard: Leaderboard.Request[\"leaderboard\"];\n }): Promise<Leaderboard.Response[\"entries\"]>;\n (\n leaderboard: Leaderboard.Request[\"leaderboard\"],\n query: Leaderboard.QueryParams\n ): Promise<Leaderboard.Response[\"entries\"]>;\n (\n { leaderboard }: { leaderboard: Leaderboard.Request[\"leaderboard\"] },\n query: Leaderboard.QueryParams\n ): Promise<Leaderboard.Response[\"entries\"]>;\n } = generator.argsAndQuery<\n Leaderboard.Request,\n Leaderboard.Response,\n Leaderboard.QueryParams,\n [Leaderboard.Request[\"leaderboard\"]],\n \"entries\"\n >(\"users/by/:leaderboard\", \"entries\");\n /** Alias of leaderboard */\n export const lb = leaderboard;\n\n export namespace History {\n export interface Response {\n /**\n * The matched users:\n */\n entries: ChannelAPI.Types.HistoricalLeaderboardEntry[];\n }\n export interface Request {\n /**\n * The leaderboard to sort users by. Must be:\n * league — the TETRA LEAGUE leaderboard.\n */\n leaderboard: \"league\";\n /**\n * The season to look up.\n */\n season: string;\n }\n export type QueryParams = (\n | {\n /**\n * The upper bound. Use this to paginate downwards: take the lowest seen prisecter and pass that back through this field to continue scrolling.\n */\n after?: string;\n }\n | {\n /**\n * The lower bound. Use this to paginate upwards: take the highest seen prisecter and pass that back through this field to continue scrolling. If set, the search order is reversed (returning the lowest items that match the query)\n */\n before?: string;\n }\n ) & {\n /**\n * The amount of entries to return, between 1 and 100. 50 by default.\n */\n limit?: number;\n /**\n * The ISO 3166-1 country code to filter to. Leave unset to not filter by country.\n */\n country?: string;\n };\n }\n export const history: {\n (\n leaderboard: History.Request[\"leaderboard\"],\n season: History.Request[\"season\"]\n ): Promise<History.Response[\"entries\"]>;\n ({\n leaderboard,\n season\n }: History.Request): Promise<History.Response[\"entries\"]>;\n (\n leaderboard: History.Request[\"leaderboard\"],\n season: History.Request[\"season\"],\n query: History.QueryParams\n ): Promise<History.Response[\"entries\"]>;\n (\n { leaderboard, season }: History.Request,\n query: History.QueryParams\n ): Promise<History.Response[\"entries\"]>;\n } = generator.argsAndQuery<\n History.Request,\n History.Response,\n History.QueryParams,\n [History.Request[\"leaderboard\"], History.Request[\"season\"]],\n \"entries\"\n >(\"users/history/:leaderboard/:season\", \"entries\");\n\n export namespace PersonalRecords {\n export type Response = ChannelAPI.Types.Record[];\n\n export interface Request {\n /**\n * The lowercase username or user ID to look up.\n */\n user: string;\n /**\n * The game mode to look up. One of:\n * 40l — their 40 LINES records.\n * blitz — their BLITZ records.\n * zenith — their QUICK PLAY records.\n * zenithex — their EXPERT QUICK PLAY records.\n * league — their TETRA LEAGUE history.\n */\n gamemode: \"40l\" | \"blitz\" | \"zenith\" | \"zenithex\" | \"league\";\n /**\n * The personal leaderboard to look up. One of:\n * top — their top scores.\n * recent — their most recently placed records.\n * progression — their top scores (PBs only).\n */\n leaderboard: \"top\" | \"recent\" | \"progression\";\n }\n export type QueryParams = (\n | {\n /**\n * The upper bound. Use this to paginate downwards: take the lowest seen prisecter and pass that back through this field to continue scrolling.\n */\n after?: string;\n }\n | {\n /**\n * The lower bound. Use this to paginate upwards: take the highest seen prisecter and pass that back through this field to continue scrolling. If set, the search order is reversed (returning the lowest items that match the query)\n */\n before?: string;\n }\n ) & {\n /**\n * The amount of entries to return, between 1 and 100. 50 by default.\n */\n limit?: number;\n };\n }\n export const personalRecords: {\n (\n user: PersonalRecords.Request[\"user\"],\n gamemode: PersonalRecords.Request[\"gamemode\"],\n leaderboard: PersonalRecords.Request[\"leaderboard\"]\n ): Promise<PersonalRecords.Response[\"entries\"]>;\n ({\n user,\n gamemode,\n leaderboard\n }: PersonalRecords.Request): Promise<PersonalRecords.Response[\"entries\"]>;\n (\n user: PersonalRecords.Request[\"user\"],\n gamemode: PersonalRecords.Request[\"gamemode\"],\n leaderboard: PersonalRecords.Request[\"leaderboard\"],\n query: PersonalRecords.QueryParams\n ): Promise<PersonalRecords.Response[\"entries\"]>;\n (\n { user, gamemode, leaderboard }: PersonalRecords.Request,\n query: PersonalRecords.QueryParams\n ): Promise<PersonalRecords.Response[\"entries\"]>;\n } = generator.argsAndQuery<\n PersonalRecords.Request,\n PersonalRecords.Response,\n PersonalRecords.QueryParams,\n [\n PersonalRecords.Request[\"user\"],\n PersonalRecords.Request[\"gamemode\"],\n PersonalRecords.Request[\"leaderboard\"]\n ],\n \"entries\"\n >(\"users/:user/records/:gamemode/:leaderboard\", \"entries\");\n /** Alias of personalRecords */\n export const records = personalRecords;\n }\n\n export namespace records {\n export namespace Leaderboard {\n export interface Response {\n /**\n * The requested records. The record will additionally include:\n */\n entries: (ChannelAPI.Types.Record & {\n /**\n * The prisecter of this entry:\n */\n p: {\n /**\n * The primary sort key.\n */\n pri: number;\n /**\n * The secondary sort key.\n */\n sec: number;\n /**\n * The tertiary sort key.\n */\n ter: number;\n };\n })[];\n }\n export interface Request {\n /**\n * The leaderboard to look up (e.g. 40l_global, blitz_country_XM, zenith_global@2024w31). Leaderboard IDs consist of:\n * the game mode, e.g. 40l,\n * the scope, either _global or a country, e.g. _country_XM,\n * an optional Revolution ID, e.g. @2024w31.\n */\n leaderboard: string;\n }\n export type QueryParams = (\n | {\n /**\n * The upper bound. Use this to paginate downwards: take the lowest seen prisecter and pass that back through this field to continue scrolling.\n */\n after?: string;\n }\n | {\n /**\n * The lower bound. Use this to paginate upwards: take the highest seen prisecter and pass that back through this field to continue scrolling. If set, the search order is reversed (returning the lowest items that match the query)\n */\n before?: string;\n }\n ) & {\n /**\n * The amount of entries to return, between 1 and 100. 50 by default.\n */\n limit?: number;\n };\n }\n // export const leaderboard = createAPIMethod<\n // Leaderboard.Request,\n // Leaderboard.Response,\n // Leaderboard.QueryParams\n // >(\"/records/:leaderboard\", [\"leaderboard\"], [\"after\", \"before\", \"limit\"]);\n export const leaderboard: {\n (\n leaderboard: Leaderboard.Request[\"leaderboard\"]\n ): Promise<Leaderboard.Response[\"entries\"]>;\n ({\n leaderboard\n }: {\n leaderboard: Leaderboard.Request[\"leaderboard\"];\n }): Promise<Leaderboard.Response[\"entries\"]>;\n (\n leaderboard: Leaderboard.Request[\"leaderboard\"],\n query: Leaderboard.QueryParams\n ): Promise<Leaderboard.Response[\"entries\"]>;\n (\n { leaderboard }: { leaderboard: Leaderboard.Request[\"leaderboard\"] },\n query: Leaderboard.QueryParams\n ): Promise<Leaderboard.Response[\"entries\"]>;\n } = generator.argsAndQuery<\n Leaderboard.Request,\n Leaderboard.Response,\n Leaderboard.QueryParams,\n [Leaderboard.Request[\"leaderboard\"]],\n \"entries\"\n >(\"/records/:leaderboard\", \"entries\");\n /** Alias of leaderboard */\n export const lb = leaderboard;\n\n export namespace Search {\n export interface Response {\n /**\n * If successful and found, the requested record.\n */\n record?: ChannelAPI.Types.Record;\n }\n export interface Request {}\n export interface QueryParams {\n /**\n * The user ID to look up.\n */\n user: string;\n /**\n * The game mode to look up.\n */\n gamemode: string;\n /**\n * The timestamp of the record to find.\n */\n ts: number;\n }\n }\n export const search: {\n (query: Search.QueryParams): Promise<Search.Response>;\n } = generator.query<Search.Response, Search.QueryParams>(\"records/search\");\n }\n\n export namespace news {\n export namespace All {\n export interface Response {\n /**\n * The latest news items:\n */\n news: ChannelAPI.Types.NewsItem[];\n }\n\n export interface Request {}\n export interface QueryParams {\n /**\n * The amount of entries to return, between 1 and 100. 25 by default.\n */\n limit?: number;\n }\n }\n export const all: {\n (): Promise<All.Response>;\n (query: All.QueryParams): Promise<All.Response>;\n } = generator.query<All.Response, All.QueryParams>(\"news/\");\n\n export namespace Latest {\n export interface Response {\n /**\n * The latest news items:\n */\n news: ChannelAPI.Types.NewsItem[];\n }\n export interface Request {\n /**\n * The news stream to look up (either \"global\" or \"user_{ userID }\").\n */\n stream: ChannelAPI.Types.StreamID;\n }\n export interface QueryParams {\n /**\n * The amount of entries to return, between 1 and 100. 25 by default.\n */\n limit?: number;\n }\n }\n export const latest: {\n (stream: Latest.Request[\"stream\"]): Promise<Latest.Response[\"news\"]>;\n ({\n stream\n }: {\n stream: Latest.Request[\"stream\"];\n }): Promise<Latest.Response[\"news\"]>;\n (\n stream: Latest.Request[\"stream\"],\n query: Latest.QueryParams\n ): Promise<Latest.Response[\"news\"]>;\n ({\n stream,\n query\n }: {\n stream: Latest.Request[\"stream\"];\n query: Latest.QueryParams;\n }): Promise<Latest.Response[\"news\"]>;\n } = generator.argsAndQuery<\n Latest.Request,\n Latest.Response,\n Latest.QueryParams,\n [Latest.Request[\"stream\"]],\n \"news\"\n >(\"news/:stream\", \"news\");\n /** Alias of latest */\n export const stream = latest;\n }\n export namespace labs {\n export namespace ScoreFlow {\n export interface Response {\n /**\n * The timestamp of the oldest record found.\n */\n startTime: number;\n /**\n * The points in the chart:\n */\n points: [\n /**\n * The timestamp offset. Add startTime to get the true timestamp.\n */\n number,\n /**\n * Whether the score set was a PB. 0 = not a PB, 1 = PB.\n */\n 0 | 1,\n /**\n * The score achieved. (For 40 LINES, this is negative.)\n */\n number\n ][];\n }\n export interface Request {\n /**\n * The lowercase username or user ID to look up.\n */\n user: string;\n /**\n * The game mode to look up.\n */\n gamemode: string;\n }\n }\n export const scoreflow: {\n (user: string, gamemode: string): Promise<ScoreFlow.Response>;\n ({ user, gamemode }: ScoreFlow.Request): Promise<ScoreFlow.Response>;\n } = generator.args<ScoreFlow.Request, ScoreFlow.Response, [string, string]>(\n \"labs/scoreflow/:user/:gamemode\"\n );\n\n export namespace LeagueFlow {\n export interface Response {\n /**\n * The timestamp of the oldest record found.\n */\n startTime: number;\n /**\n * The points in the chart:\n */\n points: [\n /**\n * The timestamp offset. Add startTime to get the true timestamp.\n */\n number,\n /**\n * The result of the match, where:\n * 1 = victory,\n * 2 = defeat,\n * 3 = victory by disqualification,\n * 4 = defeat by disqualification,\n * 5 = tie,\n * 6 = no contest,\n * 7 = match nullified.\n */\n 1 | 2 | 3 | 4 | 5 | 6 | 7,\n /**\n * The user's TR after the match.\n */\n number,\n /**\n * The opponent's TR before the match. (If the opponent was unranked, same as 2.)\n */\n number\n ][];\n }\n export interface Request {\n /**\n * The lowercase username or user ID to look up.\n */\n user: string;\n }\n }\n export const leagueflow: {\n (user: string): Promise<LeagueFlow.Response>;\n ({ user }: { user: string }): Promise<LeagueFlow.Response>;\n } = generator.args<LeagueFlow.Request, LeagueFlow.Response, [string]>(\n \"labs/leagueflow/:user\"\n );\n }\n\n export namespace Achievements {\n export interface Response {\n /**\n * The achievement info.\n */\n achievement: ChannelAPI.Types.Achievement;\n /**\n * The entries in the achievement's leaderboard:\n */\n leaderboard: ChannelAPI.Types.AchievementLeaderboardEntry[];\n /**\n * Scores required to obtain the achievement:\n */\n cutoffs: ChannelAPI.Types.AchievementCutoffs;\n }\n export interface Request {\n /**\n * The achievement ID to look up.\n */\n k: number;\n }\n }\n export const achievements: {\n (k: number): Promise<Achievements.Response>;\n ({ k }: { k: number }): Promise<Achievements.Response>;\n } = generator.args<Achievements.Request, Achievements.Response, [number]>(\n \"achievements/:k\"\n );\n\n // TYPES\n export namespace Types {\n export type ArgsObject<Req extends object> = { [k in keyof Req]: Req[k] };\n\n export interface Config {\n sessionID: string | null;\n /** Must include the trailing slash. Include the full url. Example: https://ch.tetr.io/api/ */\n host: string;\n caching: boolean;\n }\n\n /**\n * Cache is not shared between workers. Load balancing may therefore give you unexpected responses. To use the same worker, pass the same X-Session-ID header for all requests that should use the same cache.\n */\n export interface Cache {\n /**\n * Whether the cache was hit. Either \"hit\", \"miss\", or \"awaited\" (resource was already being requested by another client)\n */\n status: \"hit\" | \"miss\" | \"awaited\";\n /**\n * When this resource was cached.\n */\n cached_at: number;\n /**\n * When this resource's cache expires.\n */\n cached_until: number;\n }\n\n export interface SuccessfulResponse<Data = any> {\n /** Whether the request was successful */\n success: true;\n /** If successful, data about how this request was cached */\n cache: Cache;\n /** If successful, the requested data */\n data: Data;\n }\n\n export interface UnsuccessfulResponse {\n /** Whether the request was successful */\n success: false;\n /** If unsuccessful, the reason the request failed */\n error: { msg: string };\n }\n\n export type Response<Data = any> =\n | SuccessfulResponse<Data>\n | UnsuccessfulResponse;\n\n export interface User {\n /**\n * The user's internal ID.\n */\n _id: string;\n /**\n * The user's username.\n */\n username: string;\n /**\n * The user's role (one of \"anon\", \"user\", \"bot\", \"halfmod\", \"mod\", \"admin\", \"sysop\", \"hidden\", \"banned\").\n */\n role:\n | \"anon\"\n | \"user\"\n | \"bot\"\n | \"halfmod\"\n | \"mod\"\n | \"admin\"\n | \"sysop\"\n | \"hidden\"\n | \"banned\";\n /**\n * When the user account was created. If not set, this account was created before join dates were recorded.\n */\n ts?: string;\n /**\n * If this user is a bot, the bot's operator.\n */\n botmaster?: string;\n /**\n * The user's badges:\n */\n badges: {\n /**\n * The badge's internal ID, and the filename of the badge icon (all PNGs within /res/badges/). Note that badge IDs may include forward slashes. Please do not encode them! Follow the folder structure.\n */\n id: string;\n /**\n * The badge's group ID. If multiple badges have the same group ID, they are rendered together.\n */\n group?: string;\n /**\n * The badge's label, shown when hovered.\n */\n label: string;\n /**\n * The badge's timestamp, if shown.\n */\n ts?: string;\n }[];\n /**\n * The user's XP in points.\n */\n xp: number;\n /**\n * The amount of online games played by this user. If the user has chosen to hide this statistic, it will be -1.\n */\n gamesplayed: number;\n /**\n * The amount of online games won by this user. If the user has chosen to hide this statistic, it will be -1.\n */\n gameswon: number;\n /**\n * The amount of seconds this user spent playing, both on- and offline. If the user has chosen to hide this statistic, it will be -1.\n */\n gametime: number;\n /**\n * The user's ISO 3166-1 country code, or null if hidden/unknown. Some vanity flags exist.\n */\n country?: string;\n /**\n * Whether this user currently has a bad standing (recently banned).\n */\n badstanding?: boolean;\n /**\n * Whether this user is currently supporting TETR.IO <3\n */\n supporter: boolean;\n /**\n * An indicator of their total amount supported, between 0 and 4 inclusive.\n */\n supporter_tier: number;\n /**\n * This user's avatar ID. Get their avatar at https://tetr.io/user-content/avatars/{ USERID }.jpg?rv={ AVATAR_REVISION }\n */\n avatar_revision?: number;\n /**\n * This user's banner ID. Get their banner at https://tetr.io/user-content/banners/{ USERID }.jpg?rv={ BANNER_REVISION }. Ignore this field if the user is not a supporter.\n */\n banner_revision?: number;\n /**\n * This user's \"About Me\" section. Ignore this field if the user is not a supporter.\n */\n bio?: string;\n /**\n * This user's third party connections:\n */\n connections: {\n /**\n * This user's connection to Discord:\n */\n discord?: {\n /**\n * This user's Discord ID.\n */\n id: string;\n /**\n * This user's Discord username.\n */\n username: string;\n /**\n * Same as username.\n */\n display_username: string;\n };\n /**\n * This user's connection to Twitch:\n */\n twitch?: {\n /**\n * This user's Twitch user ID.\n */\n id: string;\n /**\n * This user's Twitch username (as used in the URL).\n */\n username: string;\n /**\n * This user's Twitch display name (may include Unicode).\n */\n display_username: string;\n };\n /**\n * This user's connection to X (kept in the API as twitter for readability):\n */\n twitter?: {\n /**\n * This user's X user ID.\n */\n id: string;\n /**\n * This user's X handle (as used in the URL).\n */\n username: string;\n /**\n * This user's X display name (may include Unicode).\n */\n display_username: string;\n };\n /**\n * This user's connection to Reddit:\n */\n reddit?: {\n /**\n * This user's Reddit user ID.\n */\n id: string;\n /**\n * This user's Reddit username.\n */\n username: string;\n /**\n * Same as username.\n */\n display_username: string;\n };\n /**\n * This user's connection to YouTube:\n */\n youtube?: {\n /**\n * This user's YouTube user ID (as used in the URL).\n */\n id: string;\n /**\n * This user's YouTube display name.\n */\n username: string;\n /**\n * Same as username.\n */\n display_username: string;\n };\n /**\n * This user's connection to Steam:\n */\n steam?: {\n /**\n * This user's SteamID.\n */\n id: string;\n /**\n * This user's Steam display name.\n */\n username: string;\n /**\n * Same as username.\n */\n display_username: string;\n };\n };\n /**\n * The amount of players who have added this user to their friends list.\n */\n friend_count: number;\n /**\n * This user's distinguishment banner, if any. Must at least have:\n */\n distinguishment?: {\n /**\n * The type of distinguishment banner.\n */\n type: string;\n };\n /**\n * This user's featured achievements. Up to three integers which correspond to Achievement IDs.\n */\n achievements: number[];\n /**\n * This user's Achievement Rating.\n */\n ar: number;\n /**\n * The breakdown of the source of this user's Achievement Rating:\n */\n ar_counts: {\n /**\n * The amount of ranked Bronze achievements this user has.\n */\n 1?: number;\n /**\n * The amount of ranked Silver achievements this user has.\n */\n 2?: number;\n /**\n * The amount of ranked Gold achievements this user has.\n */\n 3?: number;\n /**\n * The amount of ranked Platinum achievements this user has.\n */\n 4?: number;\n /**\n * The amount of ranked Diamond achievements this user has.\n */\n 5?: number;\n /**\n * The amount of ranked Issued achievements this user has.\n */\n 100?: number;\n /**\n * The amount of competitive achievements this user has ranked into the top 100 with.\n */\n t100?: number;\n /**\n * The amount of competitive achieve