UNPKG

royal-selfbot

Version:

A Discord self-bot library. USE WITH EXTREME CAUTION - AGAINST DISCORD TOS.

331 lines (284 loc) 13.3 kB
// src/Client.js const EventEmitter = require('events'); const RESTManager = require('./rest/RESTManager'); const WebSocketManager = require('./gateway/WebSocketManager'); const GuildManager = require('./managers/GuildManager'); const ChannelManager = require('./managers/ChannelManager'); const UserManager = require('./managers/UserManager'); const MessageManager = require('./managers/MessageManager'); // Global message cache (optional, usually per-channel) const User = require('./structures/User'); // For client.user const { VoiceManager } = require('./voice/VoiceManager'); // Import the class const { Gateway } = require('./util/Constants'); /** * The main hub for interacting with the Discord API. * @extends {EventEmitter} */ class Client extends EventEmitter { /** * @param {object} [options={}] Options for the client. (Currently unused, placeholder) * @param {number[]} [options.disabledEvents=[]] Optional array of gateway events to disable handling for. */ constructor(options = {}) { super(); /** * The options the client was instantiated with. * @type {object} */ this.options = options; // Store options if needed later /** * The Discord user token used for authentication. * @type {?string} */ this.token = null; /** * The user account that the client is logged in as. * Populated after the READY event. * @type {?User} */ this.user = null; // Client user info, populated on READY /** * The session ID for the current gateway session. * Populated after the READY event. Null if not connected. * @type {?string} */ this.sessionId = null; // --- Managers --- /** * Manages the guilds the client is in. * @type {GuildManager} */ this.guilds = new GuildManager(this); /** * Manages all channels the client has access to (guild channels and DMs). * @type {ChannelManager} */ this.channels = new ChannelManager(this); // Global channel manager /** * Manages user objects encountered by the client. * @type {UserManager} */ this.users = new UserManager(this); /** * Manages messages, often used more effectively per-channel. * This top-level one could be a limited global cache if desired, or removed. * @type {MessageManager} */ // Consider if a global message manager is truly useful or if it should only exist per-channel. // For simplicity, we instantiate one here, but its use might be limited. this.messages = new MessageManager(this, null); // Pass null for channel as it's global /** * Manages voice connections and adapters via @discordjs/voice. * @type {VoiceManager} */ this.voice = new VoiceManager(this); // Initialize Voice Manager // --- Core Components --- /** * The REST manager for handling API requests. * @type {RESTManager} */ this.rest = new RESTManager(this); /** * The WebSocket manager for handling the gateway connection. * @type {WebSocketManager} */ this.ws = new WebSocketManager(this); // --- Event Binding --- this._bindWSEvents(); // Add a handler for process exit signals for graceful shutdown this._setupCleanup(); } /** * Binds essential internal event listeners from the WebSocketManager. * @private */ _bindWSEvents() { // Listen for the internal 'ready' event from WebSocketManager *after* patching this.ws.on(Gateway.Events.READY, () => { // The 'ready' event listeners added by the user (`client.on('ready', ...)`) // will fire *after* this internal handler completes. console.log('[Client] Client is ready!'); // Emit the public 'ready' event, passing the client instance itself this.emit('ready', this); }); // Forward WebSocket errors to the client's error event this.ws.on(Gateway.Events.ERROR, (error) => { console.error('[Client] Received WebSocket Error:', error); this.emit(Gateway.Events.ERROR, error); // Forward the error }); // Optional: Listen for other WS states if needed (e.g., reconnecting, resumed) // this.ws.on('reconnecting', () => { console.log('[Client] WebSocket is reconnecting...'); }); // this.ws.on(Gateway.Events.RESUMED, () => { console.log('[Client] Session resumed.'); }); } /** * Patches the client state with data received from the READY gateway event. * Called internally by WebSocketManager. * @param {object} data The data payload from the READY event. * @protected */ _patch(data) { if (!data) return; // Patch client user if (data.user) { // Use the UserManager's _add method which creates/updates and caches this.user = this.users._add(data.user); // console.debug('[Client Patch] Patched client user:', this.user.tag); } // Update session ID if (data.session_id) { this.sessionId = data.session_id; // console.debug('[Client Patch] Session ID set:', this.sessionId); } // Handle initial guilds - GuildManager's _add method handles structure creation & patching if (data.guilds) { // console.debug(`[Client Patch] Receiving ${data.guilds.length} initial guilds.`); data.guilds.forEach(guildData => { // Ensure unavailable is properly handled if GUILD_CREATE comes for an initially unavailable guild const existing = this.guilds.cache.get(guildData.id); if (existing && existing.unavailable) { guildData.unavailable = false; // Mark as available now } this.guilds._add(guildData); // _add handles creation/patching }); } // Handle initial private channels (DMs) if (data.private_channels) { // console.debug(`[Client Patch] Receiving ${data.private_channels.length} initial DMs.`); data.private_channels.forEach(dmData => { this.channels._add(dmData); // Use the global ChannelManager }); } // Handle unavailable guilds if any (these guilds didn't fire GUILD_CREATE yet) // Note: 'unavailable_guilds' is less common in READY now, guilds array usually contains { id: ..., unavailable: true } if (data.unavailable_guilds) { // console.debug(`[Client Patch] Receiving ${data.unavailable_guilds.length} unavailable guilds.`); data.unavailable_guilds.forEach(guildData => { if (!this.guilds.cache.has(guildData.id)) { this.guilds._add({ id: guildData.id, unavailable: true }); } }); } // console.debug(`[Client Patch] Guild cache size after READY: ${this.guilds.cache.size}`); } /** * Logs in to Discord. * @param {string} token The Discord user token. **Using user tokens is against Discord ToS.** * @returns {Promise<string>} The token used. */ async login(token) { if (!token || typeof token !== 'string') { throw new Error('A valid token must be provided.'); } // Basic token format check (doesn't guarantee validity or type) if (!/^[A-Za-z0-9._-]+$/.test(token)) { console.warn("Token format looks potentially invalid. Ensure you're using the correct token."); } this.token = token; console.log('[Client] Logging in with token...'); try { // Initiate connection - WS manager handles identify/resume/ready this.ws.connect(this.token); // Return a promise that resolves when the 'ready' event is emitted by the client await new Promise((resolve, reject) => { const cleanup = () => { this.off('ready', onReady); this.off(Gateway.Events.ERROR, onError); }; const onReady = () => { cleanup(); resolve(); // Resolve promise when client is ready }; const onError = (err) => { cleanup(); this.destroy(); // Clean up client state on login failure reject(err); // Reject promise on error }; this.once('ready', onReady); // Listen for the public ready event this.once(Gateway.Events.ERROR, onError); // Listen for any connection/auth errors }); return this.token; // Return token on successful login/ready } catch (error) { console.error("[Client Login Error]", error); this.destroy(); // Ensure cleanup on error during login process throw error; // Re-throw error for external handling } } /** * Destroys the client, closing the WebSocket connection and cleaning up. */ destroy() { console.log('[Client] Destroying client...'); // Destroy WebSocket connection (sends close frame) this.ws.destroy({ reason: 'Client destroyed' }); // Clean up voice connections (important!) // Iterate through active adapters and tell associated connections to disconnect/destroy if (this.voice && this.voice.adapters.size > 0) { console.log(`[Client Destroy] Cleaning up ${this.voice.adapters.size} voice adapter(s)...`); try { // Requires access to @discordjs/voice's getVoiceConnection function const { getVoiceConnection } = require('@discordjs/voice'); for (const guildId of this.voice.adapters.keys()) { const connection = getVoiceConnection(guildId); if (connection) { connection.destroy(); } } this.voice.adapters.clear(); // Clear the adapters map } catch (e) { console.error("[Client Destroy] Error cleaning up voice connections (is @discordjs/voice installed?):", e); } } // Clear caches (optional, depends if you want state after destroy) this.guilds.cache.clear(); this.channels.cache.clear(); this.users.cache.clear(); this.messages.cache.clear(); // Reset client state this.token = null; this.user = null; this.sessionId = null; // Remove all listeners to prevent memory leaks this.removeAllListeners(); // Remove process exit listeners if added this._cleanupProcessListeners(); console.log('[Client] Destroyed.'); } /** * Sets up listeners for process exit signals for graceful shutdown. * @private */ _setupCleanup() { // Store bound listeners so they can be removed later this._boundProcessExit = this._gracefulShutdown.bind(this, 'processExit'); this._boundSigInt = this._gracefulShutdown.bind(this, 'SIGINT'); this._boundSigTerm = this._gracefulShutdown.bind(this, 'SIGTERM'); process.on('exit', this._boundProcessExit); // Doesn't allow async, best effort cleanup process.on('SIGINT', this._boundSigInt); // Ctrl+C process.on('SIGTERM', this._boundSigTerm); // Kill command } /** * Removes process exit listeners. * @private */ _cleanupProcessListeners() { if (this._boundProcessExit) process.removeListener('exit', this._boundProcessExit); if (this._boundSigInt) process.removeListener('SIGINT', this._boundSigInt); if (this._boundSigTerm) process.removeListener('SIGTERM', this._boundSigTerm); } /** * Handles graceful shutdown on process exit signals. * @param {string} signal The signal received. * @private */ _gracefulShutdown(signal) { console.log(`[Client] Received ${signal}. Initiating graceful shutdown...`); this.destroy(); // In SIGINT/SIGTERM, we might have a small window for async ops, but exit handlers don't. // For SIGINT/SIGTERM, explicitly exit after cleanup attempt if (signal !== 'processExit') { // Give a very short moment for WS close frame to potentially send // setTimeout(() => process.exit(0), 500); process.exit(0); // Exit immediately after calling destroy } } } module.exports = Client;