import { APIApplication, EventEmitter, OAuth2Scopes } from "../deps.ts";import { ClientUser } from "./structures/ClientUser.ts";import { CommandIncoming, CUSTOM_RPC_ERROR_CODE, RPC_CMD, RPC_ERROR_CODE, RPC_EVT, Transport, TransportOptions,} from "./structures/Transport.ts";import { FormatFunction, IPCTransport } from "./transport/IPC.ts";import { WebSocketTransport } from "./transport/WebSocket.ts";import { RPCError } from "./utils/RPCError.ts";import { TypedEmitter } from "./utils/TypedEmitter.ts";
export type AuthorizeOptions = { scopes: (OAuth2Scopes | `${OAuth2Scopes}`)[]; redirect_uri?: string; prompt?: "consent" | "none"; useRPCToken?: boolean;};
export interface ClientOptions { clientId: string; clientSecret?: string; instanceId?: number; transport?: { type?: "ipc" | "websocket" | { new (options: TransportOptions): Transport }; pathList?: FormatFunction[]; }; debug?: boolean;}
export type ClientEvents = { ready: () => void; connected: () => void; disconnected: () => void;};
export class Client extends (EventEmitter as new () => TypedEmitter<ClientEvents>) { clientId: string; clientSecret?: string;
instanceId?: number;
private accessToken?: string; private refreshToken?: string; private tokenType = "Bearer";
readonly transport: Transport; readonly debug: boolean;
user?: ClientUser; application?: APIApplication;
cdnHost = "https://cdn.discordapp.com"; origin = "https://localhost";
private refrestTimeout?: number; private connectionPromise?: Promise<void>; private _nonceMap = new Map< string, { resolve: (value?: any) => void; reject: (reason?: any) => void } >();
constructor(options: ClientOptions) { super();
this.clientId = options.clientId; this.clientSecret = options.clientSecret;
this.instanceId = options.instanceId;
this.debug = !!options.debug;
this.transport = options.transport && options.transport.type && options.transport.type != "ipc" ? options.transport.type === "websocket" ? new WebSocketTransport({ client: this }) : new options.transport.type({ client: this }) : new IPCTransport({ client: this, pathList: options.transport?.pathList, });
this.transport.on("message", (message) => { if (message.cmd === "DISPATCH" && message.evt === "READY") { if (message.data.user) { this.user = new ClientUser(this, message.data.user); } if (message.data.config && message.data.config.cdn_host) { this.cdnHost = `https://${message.data.config.cdn_host}`; } this.emit("connected"); } else { if (message.nonce && this._nonceMap.has(message.nonce)) { this._nonceMap.get(message.nonce)?.resolve(message); this._nonceMap.delete(message.nonce); }
this.emit((message as any).evt, message.data); } }); }
private throwRPCError(ctx: { code: RPC_ERROR_CODE; message?: string }) { throw new RPCError(ctx.code, ctx.message); }
async requestWithError<A = any, D = any>( cmd: RPC_CMD, args?: any, evt?: RPC_EVT, ): Promise<CommandIncoming<A, D>> { const response = await this.request<A, D>(cmd, args, evt);
if (response.evt == "ERROR") this.throwRPCError(response.data as any);
return response; }
async fetch( method: string, path: string, requst?: { body?: BodyInit; query?: string; headers?: HeadersInit }, ): Promise<Response> { return await fetch( `https://discord.com/api${path}${ requst?.query ? new URLSearchParams(requst?.query) : "" }`, { method, body: requst?.body, headers: { ...(requst?.headers ?? {}), ...(this.accessToken ? { Authorization: `${this.tokenType} ${this.accessToken}` } : {}), }, }, ); }
request<A = any, D = any>( cmd: RPC_CMD, args?: any, evt?: RPC_EVT, ): Promise<CommandIncoming<A, D>> { return new Promise((resolve, reject) => { const nonce = crypto.randomUUID();
this.transport.send({ cmd, args, evt, nonce }); this._nonceMap.set(nonce, { resolve, reject }); }); }
private async authenticate(): Promise<void> { const { application, user } = ( await this.requestWithError("AUTHENTICATE", { access_token: this.accessToken ?? "", }) ).data; this.application = application; this.user = new ClientUser(this, user); this.emit("ready"); }
private async refreshAccessToken(): Promise<void> { if (this.debug) console.log("CLIENT | Refreshing access token!");
this.hanleAccessTokenResponse( await ( await this.fetch("POST", "/oauth2/token", { body: new URLSearchParams({ client_id: this.clientId, client_secret: this.clientSecret ?? "", grant_type: "refresh_token", refresh_token: this.refreshToken ?? "", }), }) ).json(), ); }
private hanleAccessTokenResponse(data: any): void { this.accessToken = data.access_token; this.refreshToken = data.refresh_token; this.tokenType = data.token_type;
this.refrestTimeout = setTimeout( () => this.refreshAccessToken(), data.expires_in - 5000, ); }
private async authorize(options: AuthorizeOptions): Promise<void> { let rpcToken;
if (options.useRPCToken) { rpcToken = ( await ( await this.fetch("POST", "/oauth2/token/rpc", { body: new URLSearchParams({ client_id: this.clientId, client_secret: this.clientSecret ?? "", }), }) ).json() ).rpc_token; }
const { code } = ( await this.requestWithError("AUTHORIZE", { scopes: options.scopes, client_id: this.clientId, rpc_token: options.useRPCToken ? rpcToken : undefined, redirect_uri: options.redirect_uri ?? undefined, prompt: options.prompt ?? "consent", }) ).data;
this.hanleAccessTokenResponse( await ( await this.fetch("POST", "/oauth2/token", { body: new URLSearchParams({ client_id: this.clientId, client_secret: this.clientSecret ?? "", redirect_uri: options.redirect_uri ?? "", grant_type: "authorization_code", code, }), }) ).json(), ); }
async subscribe( event: Exclude<RPC_EVT, "READY" | "ERROR">, args?: any, ): Promise<{ unsubscribe: () => void }> { await this.requestWithError("SUBSCRIBE", args, event); return { unsubscribe: () => this.requestWithError("UNSUBSCRIBE", args, event), }; }
connect(): Promise<void> { if (this.connectionPromise) return this.connectionPromise;
this.connectionPromise = new Promise((resolve, reject) => { const timeout = setTimeout( () => reject( new RPCError( CUSTOM_RPC_ERROR_CODE.RPC_CONNECTION_TIMEOUT, "Connection timed out", ), ), 10e3, );
this.once("connected", () => { clearTimeout(timeout); resolve(); });
this.transport.once("close", () => { this._nonceMap.forEach((promise) => { promise.reject( new RPCError( CUSTOM_RPC_ERROR_CODE.RPC_CONNECTION_ENDED, "Connection ended", ), ); }); this.emit("disconnected"); reject( new RPCError( CUSTOM_RPC_ERROR_CODE.RPC_CONNECTION_ENDED, "[RPC_CONNECTION_ENDED]: Connection ended", ), ); });
this.transport.connect(); });
return this.connectionPromise; }
async login(options?: AuthorizeOptions): Promise<void> { await this.connect();
if (!options || !options.scopes) { this.emit("ready"); return; }
await this.authorize(options); await this.authenticate(); }
async destroy(): Promise<void> { if (this.refrestTimeout) { clearTimeout(this.refrestTimeout); this.refrestTimeout = undefined; }
await this.transport.close(); }}