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