@haelp/teto
Version:
A typescript-based controllable TETR.IO client.
295 lines (294 loc) • 11.1 kB
JavaScript
import { API, CONSTANTS, parseToken } from "../../utils/index.mjs";
import { Ribbon } from "../ribbon/index.mjs";
import { Room } from "../room/index.mjs";
import { Social } from "../social/index.mjs";
import { ClientUtils } from "../utils/index.mjs";
export class Client {
/** User information */ user;
/** Whether the client has been disconnected. If true, the client needs to be reconnected with `.reconnect()` or destroyed */ disconnected = false;
/**
* Utils for the client.
* @deprecated - functionality has been moved to other sections. This may be removed in the future.
*/ utils;
/** The client's token */ token;
/** @hidden */ _handling;
/** Raw ribbon client, the backbone of TETR.IO multiplayer. You probably don't want to touch this unless you know what you are doing. */ ribbon;
/** A helpful manager for all things social on TETR.IO (friends, dms etc.) */ social;
/** The room the client is in (if it is in a room). You can make it non-nullable with `client.room!` */ room;
/** The game the client is currently in if it is in a game. */ game;
/** Useful for connecting to the main game API when none of the client helpers have the request you want to send. You can use `client.api.get` and `client.api.post` to easily send GET and POST requests. */ api;
rooms;
/**
* Raw ribbon handler.
* @example
* client.on('social.dm', () => console.log('DM received!'));
*/ on;
/** Raw ribbon handler. */ off;
/** Raw ribbon handler. You likely want to use `client.wait` instead. */ once;
/** Raw ribbon handler for sending messages. */ emit;
/** @hideconstructor */ constructor(token, sessionID, ribbon, me, userAgent = CONSTANTS.userAgent, handling){
this.token = token;
this.ribbon = ribbon;
this.on = ribbon.emitter.on.bind(ribbon.emitter);
this.off = ribbon.emitter.off.bind(ribbon.emitter);
this.once = ribbon.emitter.once.bind(ribbon.emitter);
this.emit = ribbon.emit.bind(ribbon);
this.user = {
id: me._id,
role: me.role,
sessionID,
username: me.username,
userAgent
};
this._handling = handling;
this.utils = new ClientUtils(this);
this.api = new API({
token: this.token,
userAgent
});
this.rooms = {
list: ()=>this.api.rooms(),
join: async (id)=>{
return await this.wrap("room.join", id.toUpperCase(), "client.room.join");
},
create: async (type = "private")=>{
return await this.wrap("room.create", type === "public", "client.room.join");
}
};
this.init();
}
/**
* Create a new client
* @example
* const client = await Client.connect({ token: 'your.jwt.token' });
* @example
* const client = await Client.connect({ username: 'halp', password: 'password' });
* @example
* // If playing games, pass in handling
* const client = await Client.connect({
* // ...login info
* handling: {
* arr: 0,
* cancel: false,
* das: 5,
* dcd: 0,
* safelock: false,
* may20g: true,
* sdf: 41
* };
* });
* @example
* // You can pass in a custom user agent
* const client = await Client.connect({
* // ...login info
* userAgent: "v8/001"
* });
*/ static async connect(options) {
const api = new API();
if (options.userAgent) {
api.update({
userAgent: options.userAgent
});
}
if (options.turnstile) {
api.update({
turnstile: options.turnstile
});
}
const sessionID = `SESS-${Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)}`;
let self;
if ("token" in options) {
if (!options.token) throw new Error("No valid token was provided");
self = {
token: options.token,
id: parseToken(options.token)
};
} else if ("username" in options) {
self = await api.users.authenticate(options.username, options.password);
} else {
throw new Error("Invalid client options");
}
const token = self.token;
api.update({
token
});
const me = api.users.me();
const handling = options.handling || {
arr: 0,
cancel: false,
das: 5,
dcd: 0,
safelock: false,
may20g: true,
sdf: 41,
irs: "tap",
ihs: "tap"
};
const ribbon = await Ribbon.create({
token,
handling,
userAgent: options.userAgent || CONSTANTS.userAgent,
...options.ribbon ?? {}
});
const data = await new Promise((resolve, reject)=>{
const t = setTimeout(()=>{
ribbon.destroy();
reject("Failed to connect");
}, 5000);
ribbon.emitter.once("client.ready", (d)=>{
if (d) {
clearTimeout(t);
resolve(d);
}
});
});
const client = new Client(token, sessionID, ribbon, await me, options.userAgent || CONSTANTS.userAgent, handling);
client.social = await Social.create(client, data.social);
return client;
}
/**
* Wait for an event to occur. Wraps `client.once` into a typed Promise.
* @returns the data from the event
* @example
* // wait for a notification (although you probably want to use `client.on` for this instead)
* console.log(await client.wait('social.notification'));
*/ wait(event) {
return new Promise((resolve)=>this.once(event, resolve));
}
/**
* Send a message and then wait for another message. Throws an error if a 'err' message is received before the response message
* @param event - the `command` of the event to send
* @param data - the data to send along with the command. For void (no) data, just pass in `undefined`
* @param listen - the event to wait for before resolving.
* @param error - a list of custom error events to listen for. Defaults to `[client.error]`.
* @returns the data sent by the `listen` event
* @throws an error if the error event provided (or `client.error`) is received from TETR.IO
* @example
* // This is just for example, use `client.room!.chat` instead
* await client.wrap('room.chat', { content: 'Hello', pinned: false }, 'room.chat');
*/ wrap(event, data, listen, error = [
"client.error"
]) {
return new Promise((resolve, reject)=>{
const disband = ()=>{
this.off(listen, rs);
error.forEach((err)=>this.off(err, rj));
};
const rs = (data)=>{
disband();
resolve(data);
};
const rj = (error)=>{
disband();
reject(error);
};
this.on(listen, rs);
error.forEach((err)=>this.on(err, rj));
// @ts-expect-error
this.emit(event, data);
});
}
/** @hidden */ init() {
this.on("room.join", async ()=>{
const data = await this.wait("room.update");
this.room = new Room(this, data);
this.emit("client.room.join", this.room);
});
this.on("notify", (notif)=>{
if (typeof notif === "string") {
return this.emit("client.notify", {
msg: notif
});
} else if ("type" in notif) {
switch(notif.type){
case "deny":
this.emit("client.notify", {
msg: notif.msg,
color: "#FF2200",
icon: "denied",
timeout: notif.timeout
});
break;
case "warn":
this.emit("client.notify", {
msg: notif.msg,
color: "#FFF43C",
icon: "warning"
});
break;
case "err":
this.emit("client.error", notif.msg);
this.emit("client.notify", {
msg: notif.msg,
color: "#FF4200",
icon: "error"
});
break;
case "announce":
this.emit("client.notify", {
msg: notif.msg,
color: "#FFCC00",
icon: "announcement",
timeout: 1e4
});
break;
case "ok":
this.emit("client.notify", {
msg: notif.msg,
color: "#6AFF3C",
icon: "ok"
});
break;
}
} else {
this.emit("client.notify", notif);
}
});
this.on("client.dead", async ()=>{
this.disconnected = true;
});
}
/**
* Reconnect the client to TETR.IO.
* @throws {Error} if the client is already connected
*/ async reconnect() {
if (!this.disconnected) {
throw new Error("Client is not disconnected.");
}
const newRibbon = await this.ribbon.clone();
this.ribbon.destroy();
this.ribbon = newRibbon;
const data = await new Promise((resolve, reject)=>{
const t = setTimeout(()=>{
newRibbon.destroy();
reject("Failed to connect");
}, 5000);
this.ribbon.emitter.once("client.ready", (d)=>{
if (d) {
clearTimeout(t);
resolve(d);
}
});
});
this.social = await Social.create(this, data.social);
}
/** The client's current handling. */ get handling() {
return this._handling;
}
/** Change the client's current handling (do not use while in a room) */ set handling(handling) {
if (this.room) throw new Error("Do not set the handling in a room (you will be banned)!");
this._handling = handling;
this.emit("config.handling", handling);
}
/** Clean up the client. Leaves any rooms first. */ async destroy() {
if (this.room) {
try {
await this.room.leave();
} catch (e) {}
}
this.ribbon.destroy();
if (this.room) delete this.room;
if (this.game) delete this.game;
}
}
//# sourceMappingURL=index.js.map