UNPKG

selfbot.js

Version:

Un module fais pour intéragir avec l'api de discord. (version modifié pour mieux supporter les selfbots)(renommé car beaucoup de bug)

565 lines (507 loc) 18.5 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'); /** * La class principale pour intéragir avec l'api de discord et le début de tout bots ou selfbots. * @extends {BaseClient} */ class Client extends EventEmitter { /** * @param {ClientOptions} [options] Les options du 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(); /** * Le manager REST du client * @type {RESTManager} * @private */ this.rest = new RESTManager(this); /** * Le manager de données du client * @type {ClientDataManager} * @private */ this.dataManager = new ClientDataManager(this); /** * The manager of the client * @type {ClientManager} * @private */ this.manager = new ClientManager(this); /** * le Manager WebSocket du client. * @type {WebSocketManager} * @private */ this.ws = new WebSocketManager(this); /** * Le resolver de données du client * @type {ClientDataResolver} * @private */ this.resolver = new ClientDataResolver(this); /** * Le manager d'Actions du client. * @type {ActionsManager} * @private */ this.actions = new ActionsManager(this); /** * Le Manager Vocal du client (`null` dans un navigateur). * @type {?ClientVoiceManager} * @private */ this.voice = !this.browser ? new ClientVoiceManager(this) : null; /** * L'aideur de shard du client * (Seulement si le process vient de {@link ShardingManager}) * @type {?ShardClientUtil} */ this.shard = process.send ? ShardClientUtil.singleton(this) : null; /** * Tous les objets {@link User} qui ont été mis en cache, Ajouté par ID. * @type {Collection<Snowflake, User>} */ this.users = new Collection(); /** * Tous les serveurs que le client a accès, Ajouté par ID. - * Tant que il n'y as pas de shard, *tous* les serveurs dont le client est dedans sera ici. * @type {Collection<Snowflake, Guild>} */ this.guilds = new Collection(); /** * tous les {@link Channel}s que le client a accès. - * Tant que il n'y as pas de shard, *tous* les channel dans *tous* les serveurs dont le client * en est membre. Notez que les MP ne sont pas initialement mis en cache, et ne sont pas présent * sans leur fetch explicit ou alors leur utilisation. * @type {Collection<Snowflake, Channel>} */ this.channels = new Collection(); /** * Les presences des amis de l'utilisateur connecté, Classé par ID. * <warn>ceci est seulement rempli en utilisant un compte utilisateur.</warn> * @type {Collection<Snowflake, Presence>} */ this.presences = new Collection(); Object.defineProperty(this, 'token', { writable: true }); if (!this.token && 'CLIENT_TOKEN' in process.env) { /** * Le token du bot/user * <warn>cela devrait être garder secret tout le temps.</warn> * @type {?string} */ this.token = process.env.CLIENT_TOKEN; } else { this.token = null; } /** * L'utilisateur dont le client est connecté * @type {?ClientUser} */ this.user = null; /** * La dernère date dont le client a été en état `READY`. * (chaque fois que le client se déconnecte et se reconnecte, c'est reécrit). * @type {?Date} */ this.readyAt = null; /** * Active voice broadcasts that have been created * @type {VoiceBroadcast[]} */ this.broadcasts = []; /** * Les anciens ping du webSocket, le plus récent en premier. * @type {number[]} */ this.pings = []; /** * Les timeouts mis par {@link Client#setTimeout} qui sont toujours actifs * @type {Set<Timeout>} * @private */ this._timeouts = new Set(); /** * Les intervals mis par {@link Client#setInterval} qui sont toujours actifs * @type {Set<Timeout>} * @private */ this._intervals = new Set(); 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; } /** * Le status actuel de la connection a discord. * @type {?number} * @readonly */ get status() { return this.ws.connection.status; } /** * Le nombre de milliseconds depuis la dernière fois que le client a été en état `READY`. * @type {?number} * @readonly */ get uptime() { return this.readyAt ? Date.now() - this.readyAt : null; } /** * La moyenne de ping du websocket, depuis {@link Client#pings}. * @type {number} * @readonly */ get ping() { return this.pings.reduce((prev, p) => prev + p, 0) / this.pings.length; } /** * Toutes les connections vocals en cours, mappé par l'id du serveur. * @type {Collection<Snowflake, VoiceConnection>} * @readonly */ get voiceConnections() { if (this.browser) return new Collection(); return this.voice.connections; } /** * Tous les emojis personalisé de tous les serveurs dont le client a accès. * @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; } /** * le dernier timestamp dont le client a été en état `READY`. * @type {?number} * @readonly */ get readyTimestamp() { return this.readyAt ? this.readyAt.getTime() : null; } /** * Si le client est dans un navigateur. * @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; } /** * Connecte le client, Établis une connexion en websocket à discord. * <info>Les bots et les comptes utilisateur sont supportés, mais c'est mieux d'utiliser un bot quand c'est * possible. les comptes utilisateur ont de plus grosse rate limits et d'autres restrictions que les bots n'ont pas. * les bots ont aussi accès à plein de fonctionnalités que les comptes en utilisateur ne peuvent pas utiliser. Automatiser un compte utilsateur est * considéré comme une violation des ToS.</info> * @param {string} token le token du compte avec lequel se connecter * @returns {Promise<string>} Le token du compte utilisé * @example * client.login('my token') * .then(console.log) * .catch(console.error); */ login(token = this.token) { return this.rest.methods.login(token); } /** * Se déconnecte, termine al connection a discord et détruit le 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(); } /** * Syncronise les données avec discord. * <info>Ceci peut être fait automatiquement en utilisant {@link ClientOptions#sync}.</info> * <warn>Ceci est utilisable uniquement en utilisant un compte utilisateur.</warn> * @param {Guild[]|Collection<Snowflake, Guild>} [guilds=this.guilds] Une array de serveurs a synchroniser */ 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), }); } /** * Prend un utilisateur de discord ou dans le cache si un bot est connecté. * <warn>Ceci est utilisable uniquement en utilisant un compte bot.</warn> * @param {Snowflake} id L'ID de l'user * @param {boolean} [cache=true] Si on devrait regarder dans le cache ou directement dans discord. * @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); } /** * Prend les informations d'une invitation de discord. * @param {InviteResolvable} invite Le code ou l'url. * @returns {Promise<Invite>} * @example * client.fetchInvite('https://discord.gg/bRCvFy9') * .then(invite => console.log(`Invitation obtenus avec le code: ${invite.code}`) * .catch(console.error); */ fetchInvite(invite) { const code = this.resolver.resolveInviteCode(invite); return this.rest.methods.getInvite(code); } /** * Prend les informations d'un webhook de discord. * @param {Snowflake} id L'ID du webhook * @param {string} [token] Le token du webhook * @returns {Promise<Webhook>} * @example * client.fetchWebhook('id', 'token') * .then(webhook => console.log(`Webhook obtenus avec le nom: ${webhook.name}`)) * .catch(console.error); */ fetchWebhook(id, token) { return this.rest.methods.getWebhook(id, token); } /** * Prend les régions vocal de discord. * @returns {Collection<string, VoiceRegion>} * @example * client.fetchVoiceRegions() * .then(regions => console.log(`Les régions vocals disponibles sont: ${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; } /** * Prend l'application OAuth2 de discord du bot connecté. * <warn>Les bots peuvent seulement récupérer leur propre application.</warn> * @param {Snowflake} [id='@me'] L'id de l'application a prendre * @returns {Promise<OAuth2Application>} * @example * client.fetchApplication() * .then(application => console.log(`Application avec le nom: ${application.name} fetched`) * .catch(console.error); */ fetchApplication(id = '@me') { if (id !== '@me') process.emitWarning('fetchApplication: use "@me" as an argument', 'DeprecationWarning'); return this.rest.methods.getApplication(id); } /** * Génère un lien qui permet d'ajouter le bot à un serveur. * <warn>Ceci est utilisable uniquement en utilisant un compte bot.</warn> * @param {PermissionResolvable} [permissions] les permissions nécessaires * @returns {Promise<string>} * @example * client.generateInvite(['SEND_MESSAGES', 'MANAGE_GUILD', 'MENTION_EVERYONE']) * .then(link => console.log(`Lien d'invitation généré: ${link}`)) * .catch(console.error); */ generateInvite(permissions) { permissions = typeof permissions === 'undefined' ? 0 : Permissions.resolve(permissions); return this.fetchApplication().then(application => `https://discordapp.com/oauth2/authorize?client_id=${application.id}&permissions=${permissions}&scope=bot` ); } /** * Construit un Timeout qui sera automatiquement annulé si le client est détruit. * @param {Function} fn La fonction à executer * @param {number} delay Le temps a attendre avant d'executer (en millisecondes) * @param {...*} args Les arguments de la fonction * @returns {Timeout} */ setTimeout(fn, delay, ...args) { const timeout = setTimeout(() => { fn(...args); this._timeouts.delete(timeout); }, delay); this._timeouts.add(timeout); return timeout; } /** * Supprime un timeout. * @param {Timeout} timeout Le timeout a annuler. */ clearTimeout(timeout) { clearTimeout(timeout); this._timeouts.delete(timeout); } /** * Construit un Interval qui sera automatiquement annulé si le client est détruit. * @param {Function} fn La fonction à executer * @param {number} delay Le temps a attendre entre les executions (en millisecondes) * @param {...*} args Les arguments de la fonction * @returns {Timeout} */ setInterval(fn, delay, ...args) { const interval = setInterval(fn, delay, ...args); this._intervals.add(interval); return interval; } /** * Supprime un Interval. * @param {Timeout} interval L'interval à supprimer */ clearInterval(interval) { clearInterval(interval); this._intervals.delete(interval); } /** * Ajoute un ping à {@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; } /** * Ajoute/modifie une presence dans: {@link Client#presences}. * @param {Snowflake} id L'id de l'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)); } /** * appelle {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval} sur un script * avec le client en tant que `this`. * @param {string} script le script a eval * @returns {*} * @private */ _eval(script) { return eval(script); } /** * Valide les options du client. * @param {ClientOptions} [options=this.options] les options a valider. * @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; /** * Émis pour un avertissement général. * @event Client#warn * @param {string} info l'avertissement */ /** * Émis pour une information général. * @event Client#debug * @param {string} info L'information */