"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Client = void 0; const IPC_1 = require("./transport/IPC"); const axios_1 = __importDefault(require("axios")); const WebSocket_1 = require("./transport/WebSocket"); const ClientUser_1 = require("./structures/ClientUser"); const RPCError_1 = require("./utils/RPCError"); const node_events_1 = require("node:events"); const node_crypto_1 = __importDefault(require("node:crypto")); const Transport_1 = require("./structures/Transport"); class Client extends node_events_1.EventEmitter { /** * application id */ clientId; /** * application secret */ clientSecret; /** * pipe id */ pipeId; accessToken; refreshToken; tokenType = "Bearer"; /** * transport instance */ transport; /** * current user */ user; /** * current application */ application; /** * @hidden */ cdnHost = "https://cdn.discordapp.com"; /** * @hidden */ origin = "https://localhost"; get isConnected() { return this.transport.isConnected; } refreshTimeout; connectionPromise; _nonceMap = new Map(); constructor(options) { super(); this.clientId = options.clientId; this.clientSecret = options.clientSecret; this.pipeId = options.pipeId; this.transport = options.transport?.type === undefined || options.transport.type === "ipc" ? new IPC_1.IPCTransport({ client: this, pathList: options.transport?.pathList }) : new (options.transport.type === "websocket" ? WebSocket_1.WebSocketTransport : options.transport.type)({ client: this }); this.transport.on("message", (message) => { if (message.cmd === "DISPATCH" && message.evt === "READY") { if (message.data.user) this.user = new ClientUser_1.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)) { const nonceObj = this._nonceMap.get(message.nonce); if (message.evt === "ERROR") { nonceObj.error.code = message.data.code; nonceObj.error.message = message.data.message; nonceObj?.reject(nonceObj.error); } else nonceObj?.resolve(message); this._nonceMap.delete(message.nonce); } this.emit(message.evt, message.data); } }); } /** * For RPC event, make sure to subscribe to the event. */ on(event, listener) { return super.on(event, listener); } /** * For RPC event, make sure to subscribe to the event. */ once(event, listener) { return super.once(event, listener); } // #region Request Handlers /** * @hidden */ async fetch(method, path, req) { const url = new URL(`https://discord.com/api${path}`); if (req?.query) for (const [key, value] of req.query) url.searchParams.append(key, value); return await (0, axios_1.default)({ url: url.toString(), method, data: req?.data ?? undefined, headers: { ...(req?.headers ?? {}), ...(this.accessToken ? { Authorization: `${this.tokenType} ${this.accessToken}` } : {}) } }); } /** * @hidden */ async request(cmd, args, evt) { const error = new RPCError_1.RPCError(Transport_1.RPC_ERROR_CODE.RPC_UNKNOWN_ERROR); RPCError_1.RPCError.captureStackTrace(error, this.request); return new Promise((resolve, reject) => { const nonce = node_crypto_1.default.randomUUID(); this.transport.send({ cmd, args, evt, nonce }); this._nonceMap.set(nonce, { resolve, reject, error }); }); } // #endregion // #region Authorization handlers async authenticate() { const { application, user } = (await this.request("AUTHENTICATE", { access_token: this.accessToken ?? "" })) .data; this.application = application; this.user = new ClientUser_1.ClientUser(this, user); this.emit("ready"); } async refreshAccessToken() { this.emit("debug", "CLIENT | Refreshing access token!"); this.hanleAccessTokenResponse((await this.fetch("POST", "/oauth2/token", { data: new URLSearchParams({ client_id: this.clientId, client_secret: this.clientSecret ?? "", grant_type: "refresh_token", refresh_token: this.refreshToken ?? "" }), headers: { "content-type": "application/x-www-form-urlencoded" } })).data); } hanleAccessTokenResponse(data) { if (!("access_token" in data) || !("refresh_token" in data) || !("expires_in" in data) || !("token_type" in data)) throw new TypeError(`Invalid access token response!\nData: ${JSON.stringify(data, null, 2)}`); this.accessToken = data.access_token; this.refreshToken = data.refresh_token; this.tokenType = data.token_type; this.refreshTimeout = setTimeout(() => void this.refreshAccessToken(), data.expires_in); } async authorize(options) { let rpcToken; if (options.useRPCToken) { rpcToken = (await this.fetch("POST", "/oauth2/token/rpc", { data: new URLSearchParams({ client_id: this.clientId, client_secret: this.clientSecret ?? "" }), headers: { "content-type": "application/x-www-form-urlencoded" } })).data.rpc_token; } const { code } = (await this.request("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 this.fetch("POST", "/oauth2/token", { data: new URLSearchParams({ client_id: this.clientId, client_secret: this.clientSecret ?? "", redirect_uri: options.redirect_uri ?? "", grant_type: "authorization_code", code }), headers: { "content-type": "application/x-www-form-urlencoded" } })).data); } // #endregion /** * Used to subscribe to events. `evt` of the payload should be set to the event being subscribed to. `args` of the payload should be set to the args needed for the event. * @param event event name now subscribed to * @param args args for the event * @returns an object to unsubscribe from the event */ async subscribe(event, args) { await this.request("SUBSCRIBE", args, event); return { /** * Unsubscribes from the event */ unsubscribe: () => this.request("UNSUBSCRIBE", args, event) }; } /////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * connect to the local rpc server */ async connect() { if (this.connectionPromise) return this.connectionPromise; const error = new RPCError_1.RPCError(Transport_1.RPC_ERROR_CODE.RPC_UNKNOWN_ERROR); RPCError_1.RPCError.captureStackTrace(error, this.connect); this.connectionPromise = new Promise((resolve, reject) => { const timeout = setTimeout(() => { this.connectionPromise = undefined; error.code = Transport_1.CUSTOM_RPC_ERROR_CODE.RPC_CONNECTION_TIMEOUT; error.message = "Connection timed out"; reject(error); }, 10e3); timeout.unref(); this.once("connected", () => { this.connectionPromise = undefined; this.transport.once("close", (reason) => { this._nonceMap.forEach((promise) => { promise.error.code = typeof reason === "object" ? reason.code : Transport_1.CUSTOM_RPC_ERROR_CODE.RPC_CONNECTION_ENDED; promise.error.message = typeof reason === "object" ? reason.message : reason ?? "Connection ended"; promise.reject(promise.error); }); this.emit("disconnected"); }); clearTimeout(timeout); resolve(); }); this.transport.connect().catch(reject); }); return this.connectionPromise; } /** * will try to authorize if a scope is specified, else it's the same as `connect()` * @param options options for the authorization */ async login(options) { await this.connect(); if (!options || !options.scopes) { this.emit("ready"); return; } await this.authorize(options); await this.authenticate(); } /** * disconnects from the local rpc server */ async destroy() { if (this.refreshTimeout) { clearTimeout(this.refreshTimeout); this.refreshTimeout = undefined; this.refreshToken = undefined; } await this.transport.close(); } } exports.Client = Client;