UNPKG

selfbot-discord

Version:
555 lines (497 loc) • 17.8 kB
const EventEmitter = require('events'); const Constants = require('../util/Constants'); const Permissions = require('../util/Permissions'); const Util = require('../util/Util'); const RESTManager = require('./rest/RESTManager'); const ClientDataManager = require('./ClientDataManager'); const ClientManager = require('./ClientManager'); const ClientDataResolver = require('./ClientDataResolver'); const ClientVoiceManager = require('./voice/ClientVoiceManager'); const WebSocketManager = require('./websocket/WebSocketManager'); const ActionsManager = require('./actions/ActionsManager'); const Collection = require('../util/Collection'); const Presence = require('../structures/Presence').Presence; const ShardClientUtil = require('../sharding/ShardClientUtil'); const VoiceBroadcast = require('./voice/VoiceBroadcast'); /** * The main hub for interacting with the Discord API, and the starting point for any bot. * @extends {EventEmitter} */ class Client extends EventEmitter { /** * @param {ClientOptions} [options] Options for the client */ constructor(options = {}) { super(); // Obtain shard details from environment if (!options.shardId && 'SHARD_ID' in process.env) options.shardId = Number(process.env.SHARD_ID); if (!options.shardCount && 'SHARD_COUNT' in process.env) options.shardCount = Number(process.env.SHARD_COUNT); /** * The options the client was instantiated with * @type {ClientOptions} */ this.options = Util.mergeDefault(Constants.DefaultOptions, options); this._validateOptions(); /** * The REST manager of the client * @type {RESTManager} * @private */ this.rest = new RESTManager(this); /** * The data manager of the client * @type {ClientDataManager} * @private */ this.dataManager = new ClientDataManager(this); /** * The manager of the client * @type {ClientManager} * @private */ this.manager = new ClientManager(this); /** * The WebSocket manager of the client * @type {WebSocketManager} * @private */ this.ws = new WebSocketManager(this); /** * The data resolver of the client * @type {ClientDataResolver} * @private */ this.resolver = new ClientDataResolver(this); /** * The action manager of the client * @type {ActionsManager} * @private */ this.actions = new ActionsManager(this); /** * The voice manager of the client (`null` in browsers) * @type {?ClientVoiceManager} * @private */ this.voice = !this.browser ? new ClientVoiceManager(this) : null; /** * The shard helpers for the client * (only if the process was spawned as a child, such as from a {@link ShardingManager}) * @type {?ShardClientUtil} */ this.shard = process.send ? ShardClientUtil.singleton(this) : null; /** * All of the {@link User} objects that have been cached at any point, mapped by their IDs * @type {Collection<Snowflake, User>} */ this.users = new Collection(); /** * All of the guilds the client is currently handling, mapped by their IDs - * as long as sharding isn't being used, this will be *every* guild the bot is a member of * @type {Collection<Snowflake, Guild>} */ this.guilds = new Collection(); /** * All of the {@link Channel}s that the client is currently handling, mapped by their IDs - * as long as sharding isn't being used, this will be *every* channel in *every* guild, and all DM channels * @type {Collection<Snowflake, Channel>} */ this.channels = new Collection(); /** * Presences that have been received for the client user's friends, mapped by user IDs * <warn>This is only filled when using a user account.</warn> * @type {Collection<Snowflake, Presence>} * @deprecated */ this.presences = new Collection(); Object.defineProperty(this, 'token', { writable: true }); if (!this.token && 'CLIENT_TOKEN' in process.env) { /** * Authorization token for the logged in user/bot * <warn>This should be kept private at all times.</warn> * @type {?string} */ this.token = process.env.CLIENT_TOKEN; } else { this.token = null; } /** * User that the client is logged in as * @type {?ClientUser} */ this.user = null; /** * Time at which the client was last regarded as being in the `READY` state * (each time the client disconnects and successfully reconnects, this will be overwritten) * @type {?Date} */ this.readyAt = null; /** * Active voice broadcasts that have been created * @type {VoiceBroadcast[]} */ this.broadcasts = []; /** * Previous heartbeat pings of the websocket (most recent first, limited to three elements) * @type {number[]} */ this.pings = []; /** * Timeouts set by {@link Client#setTimeout} that are still active * @type {Set<Timeout>} * @private */ this._timeouts = new Set(); /** * Intervals set by {@link Client#setInterval} that are still active * @type {Set<Timeout>} * @private */ this._intervals = new Set(); /** * Les channels muted (removed 07/2023) * @type {Collection<Snowflake, Channel>} * @private */ // this._muted = new Collection(); if (this.options.messageSweepInterval > 0) { this.setInterval(this.sweepMessages.bind(this), this.options.messageSweepInterval * 1000); } } /** * Timestamp of the latest ping's start time * @type {number} * @private */ get _pingTimestamp() { return this.ws.connection ? this.ws.connection.lastPingTimestamp : 0; } /** * Current status of the client's connection to Discord * @type {Status} * @readonly */ get status() { return this.ws.connection ? this.ws.connection.status : Constants.Status.IDLE; } /** * How long it has been since the client last entered the `READY` state in milliseconds * @type {?number} * @readonly */ get uptime() { return this.readyAt ? Date.now() - this.readyAt : null; } /** * Average heartbeat ping of the websocket, obtained by averaging the {@link Client#pings} property * @type {number} * @readonly */ get ping() { return this.pings.reduce((prev, p) => prev + p, 0) / this.pings.length; } /** * All active voice connections that have been established, mapped by guild ID * @type {Collection<Snowflake, VoiceConnection>} * @readonly */ get voiceConnections() { if (this.browser) return new Collection(); return this.voice.connections; } /** * All custom emojis that the client has access to, mapped by their IDs * @type {Collection<Snowflake, Emoji>} * @readonly */ get emojis() { const emojis = new Collection(); for (const guild of this.guilds.values()) { for (const emoji of guild.emojis.values()) emojis.set(emoji.id, emoji); } return emojis; } /** * Timestamp of the time the client was last `READY` at * @type {?number} * @readonly */ get readyTimestamp() { return this.readyAt ? this.readyAt.getTime() : null; } /** * Whether the client is in a browser environment * @type {boolean} * @readonly */ get browser() { return typeof window !== 'undefined'; } /** * Creates a voice broadcast. * @returns {VoiceBroadcast} */ createVoiceBroadcast() { const broadcast = new VoiceBroadcast(this); this.broadcasts.push(broadcast); return broadcast; } /** * Logs the client in, establishing a websocket connection to Discord. * <info>Both bot and regular user accounts are supported, but it is highly recommended to use a bot account whenever * possible. User accounts are subject to harsher ratelimits and other restrictions that don't apply to bot accounts. * Bot accounts also have access to many features that user accounts cannot utilise. Automating a user account is * considered a violation of the ToS.</info> * @param {string} token Token of the account to log in with * @returns {Promise<string>} Token of the account used * @example * client.login('my token') * .then(console.log) * .catch(console.error); */ login(token = this.token) { return this.rest.methods.login(token); } /** * Logs out, terminates the connection to Discord, and destroys the client. * @returns {Promise} */ destroy() { for (const t of this._timeouts) clearTimeout(t); for (const i of this._intervals) clearInterval(i); this._timeouts.clear(); this._intervals.clear(); return this.manager.destroy(); } /** * Requests a sync of guild data with Discord. * <info>This can be done automatically every 30 seconds by enabling {@link ClientOptions#sync}.</info> * <warn>This is only available when using a user account.</warn> * @param {Guild[]|Collection<Snowflake, Guild>} [guilds=this.guilds] An array or collection of guilds to sync * @deprecated */ syncGuilds(guilds = this.guilds) { if (this.user.bot) return; this.ws.send({ op: 12, d: guilds instanceof Collection ? guilds.keyArray() : guilds.map(g => g.id), }); } /** * Obtains a user from Discord, or the user cache if it's already available. * <warn>This is only available when using a bot account.</warn> * @param {Snowflake} id ID of the user * @param {boolean} [cache=true] Whether to cache the new user object if it isn't already * @returns {Promise<User>} */ fetchUser(id, cache = true) { if (this.users.has(id)) return Promise.resolve(this.users.get(id)); return this.rest.methods.getUser(id, cache); } /** * Obtains an invite from Discord. * @param {InviteResolvable} invite Invite code or URL * @returns {Promise<Invite>} * @example * client.fetchInvite('https://discord.gg/bRCvFy9') * .then(invite => console.log(`Obtained invite with code: ${invite.code}`)) * .catch(console.error); */ fetchInvite(invite) { const code = this.resolver.resolveInviteCode(invite); return this.rest.methods.getInvite(code); } /** * Obtains a webhook from Discord. * @param {Snowflake} id ID of the webhook * @param {string} [token] Token for the webhook * @returns {Promise<Webhook>} * @example * client.fetchWebhook('id', 'token') * .then(webhook => console.log(`Obtained webhook with name: ${webhook.name}`)) * .catch(console.error); */ fetchWebhook(id, token) { return this.rest.methods.getWebhook(id, token); } /** * Obtains the available voice regions from Discord. * @returns {Collection<string, VoiceRegion>} * @example * client.fetchVoiceRegions() * .then(regions => console.log(`Available regions are: ${regions.map(region => region.name).join(', ')}`)) * .catch(console.error); */ fetchVoiceRegions() { return this.rest.methods.fetchVoiceRegions(); } /** * Sweeps all text-based channels' messages and removes the ones older than the max message lifetime. * If the message has been edited, the time of the edit is used rather than the time of the original message. * @param {number} [lifetime=this.options.messageCacheLifetime] Messages that are older than this (in seconds) * will be removed from the caches. The default is based on {@link ClientOptions#messageCacheLifetime} * @returns {number} Amount of messages that were removed from the caches, * or -1 if the message cache lifetime is unlimited */ sweepMessages(lifetime = this.options.messageCacheLifetime) { if (typeof lifetime !== 'number' || isNaN(lifetime)) throw new TypeError('The lifetime must be a number.'); if (lifetime <= 0) { this.emit('debug', 'Didn\'t sweep messages - lifetime is unlimited'); return -1; } const lifetimeMs = lifetime * 1000; const now = Date.now(); let channels = 0; let messages = 0; for (const channel of this.channels.values()) { if (!channel.messages) continue; channels++; messages += channel.messages.sweep( message => now - (message.editedTimestamp || message.createdTimestamp) > lifetimeMs ); } this.emit('debug', `Swept ${messages} messages older than ${lifetime} seconds in ${channels} text-based channels`); return messages; } /** * Obtains the OAuth Application of the bot from Discord. * <warn>Bots can only fetch their own profile.</warn> * @param {Snowflake} [id='@me'] ID of application to fetch * @returns {Promise<OAuth2Application>} * @example * client.fetchApplication() * .then(application => console.log(`Obtained application with name: ${application.name}`)) * .catch(console.error); */ fetchApplication(id = '@me') { if (id !== '@me') process.emitWarning('fetchApplication: use "@me" as an argument', 'DeprecationWarning'); return this.rest.methods.getApplication(id); } /** * Sets a timeout that will be automatically cancelled if the client is destroyed. * @param {Function} fn Function to execute * @param {number} delay Time to wait before executing (in milliseconds) * @param {...*} args Arguments for the function * @returns {Timeout} */ setTimeout(fn, delay, ...args) { const timeout = setTimeout(() => { fn(...args); this._timeouts.delete(timeout); }, delay); this._timeouts.add(timeout); return timeout; } /** * Clears a timeout. * @param {Timeout} timeout Timeout to cancel */ clearTimeout(timeout) { clearTimeout(timeout); this._timeouts.delete(timeout); } /** * Sets an interval that will be automatically cancelled if the client is destroyed. * @param {Function} fn Function to execute * @param {number} delay Time to wait before executing (in milliseconds) * @param {...*} args Arguments for the function * @returns {Timeout} */ setInterval(fn, delay, ...args) { const interval = setInterval(fn, delay, ...args); this._intervals.add(interval); return interval; } /** * Clears an interval. * @param {Timeout} interval Interval to cancel */ clearInterval(interval) { clearInterval(interval); this._intervals.delete(interval); } /** * Adds a ping to {@link Client#pings}. * @param {number} startTime Starting time of the ping * @private */ _pong(startTime) { this.pings.unshift(Date.now() - startTime); if (this.pings.length > 3) this.pings.length = 3; this.ws.lastHeartbeatAck = true; } /** * Adds/updates a friend's presence in {@link Client#presences}. * @param {Snowflake} id ID of the user * @param {Object} presence Raw presence object from Discord * @private */ _setPresence(id, presence) { if (this.presences.has(id)) { this.presences.get(id).update(presence); return; } this.presences.set(id, new Presence(presence, this)); } /** * Calls {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval} on a script * with the client as `this`. * @param {string} script Script to eval * @returns {*} * @private */ _eval(script) { return eval(script); } /** * Validates the client options. * @param {ClientOptions} [options=this.options] Options to validate * @private */ _validateOptions(options = this.options) { // eslint-disable-line complexity if (typeof options.shardCount !== 'number' || isNaN(options.shardCount)) { throw new TypeError('The shardCount option must be a number.'); } if (typeof options.shardId !== 'number' || isNaN(options.shardId)) { throw new TypeError('The shardId option must be a number.'); } if (options.shardCount < 0) throw new RangeError('The shardCount option must be at least 0.'); if (options.shardId < 0) throw new RangeError('The shardId option must be at least 0.'); if (options.shardId !== 0 && options.shardId >= options.shardCount) { throw new RangeError('The shardId option must be less than shardCount.'); } if (typeof options.messageCacheMaxSize !== 'number' || isNaN(options.messageCacheMaxSize)) { throw new TypeError('The messageCacheMaxSize option must be a number.'); } if (typeof options.messageCacheLifetime !== 'number' || isNaN(options.messageCacheLifetime)) { throw new TypeError('The messageCacheLifetime option must be a number.'); } if (typeof options.messageSweepInterval !== 'number' || isNaN(options.messageSweepInterval)) { throw new TypeError('The messageSweepInterval option must be a number.'); } if (typeof options.fetchAllMembers !== 'boolean') { throw new TypeError('The fetchAllMembers option must be a boolean.'); } if (typeof options.disableEveryone !== 'boolean') { throw new TypeError('The disableEveryone option must be a boolean.'); } if (typeof options.restWsBridgeTimeout !== 'number' || isNaN(options.restWsBridgeTimeout)) { throw new TypeError('The restWsBridgeTimeout option must be a number.'); } if (!(options.disabledEvents instanceof Array)) throw new TypeError('The disabledEvents option must be an Array.'); if (typeof options.retryLimit !== 'number' || isNaN(options.retryLimit)) { throw new TypeError('The retryLimit options must be a number.'); } } } module.exports = Client; /** * Emitted for general warnings. * @event Client#warn * @param {string} info The warning */ /** * Emitted for general debugging information. * @event Client#debug * @param {string} info The debug information */