From 609cce36311e9db7c5f106fac3965a8b6310d209 Mon Sep 17 00:00:00 2001 From: Elysia <71698422+aiko-chan-ai@users.noreply.github.com> Date: Thu, 11 Jan 2024 19:23:00 +0700 Subject: [PATCH] update --- src/managers/GuildMemberManager.js | 281 ++++++----------------------- src/structures/DMChannel.js | 130 +++---------- src/structures/GroupDMChannel.js | 74 ++++++++ 3 files changed, 154 insertions(+), 331 deletions(-) diff --git a/src/managers/GuildMemberManager.js b/src/managers/GuildMemberManager.js index a97cce3..243ef56 100644 --- a/src/managers/GuildMemberManager.js +++ b/src/managers/GuildMemberManager.js @@ -1,10 +1,9 @@ +/* eslint-disable newline-per-chained-call */ 'use strict'; const { Buffer } = require('node:buffer'); const { setTimeout } = require('node:timers'); const { Collection } = require('@discordjs/collection'); -require('lodash.permutations'); -const _ = require('lodash'); const CachedManager = require('./CachedManager'); const { Error, TypeError, RangeError } = require('../errors'); const BaseGuildVoiceChannel = require('../structures/BaseGuildVoiceChannel'); @@ -191,25 +190,17 @@ class GuildMemberManager extends CachedManager { * guild.members.fetch({ query: 'hydra', limit: 1 }) * .then(console.log) * .catch(console.error); - * @see {@link https://github.com/aiko-chan-ai/discord.js-selfbot-v13/blob/main/Document/FetchGuildMember.md} */ fetch(options) { - if (!options || (typeof options === 'object' && !('user' in options) && !('query' in options))) { + if (!options) { if ( - this.guild.members.me.permissions.has('KICK_MEMBERS') || - this.guild.members.me.permissions.has('BAN_MEMBERS') || - this.guild.members.me.permissions.has('MANAGE_ROLES') + this.me.permissions.has('KICK_MEMBERS') || + this.me.permissions.has('BAN_MEMBERS') || + this.me.permissions.has('MANAGE_ROLES') ) { return this._fetchMany(); - } else if (this.guild.memberCount <= 10000) { - return this.fetchByMemberSafety(); } else { - // NOTE: This is a very slow method, and can take up to 999+ minutes to complete. - return this.fetchBruteforce({ - delay: 50, - skipWarn: true, - depth: 1, - }); + return this.fetchByMemberSafety(); } } const user = this.client.users.resolveId(options); @@ -471,221 +462,6 @@ class GuildMemberManager extends CachedManager { return this._add(data, cache); } - /** - * Options used to fetch multiple members from a guild. - * @typedef {Object} BruteforceOptions - * @property {number} [limit=100] Maximum number of members per request - * @property {number} [delay=500] Timeout for new requests in ms - * @property {number} [depth=1] Permutations length - */ - - /** - * Fetches multiple members from the guild. - * @param {BruteforceOptions} options Options for the bruteforce - * @returns {Collection} (All) members in the guild - * @see https://github.com/aiko-chan-ai/discord.js-selfbot-v13/blob/main/Document/FetchGuildMember.md - * @example - * guild.members.fetchBruteforce() - * .then(members => console.log(`Fetched ${members.size} members`)) - * .catch(console.error); - */ - fetchBruteforce(options = {}) { - const defaultQuery = 'abcdefghijklmnopqrstuvwxyz0123456789!"#$%&\'()*+,-./:;<=>?@[]^_`{|}~ '; - let dictionary; - let limit = 100; - let delay = 500; - let depth = 1; - if (options?.limit) limit = options?.limit; - if (options?.delay) delay = options?.delay; - if (options?.depth) depth = options?.depth; - if (typeof limit !== 'number') throw new TypeError('INVALID_TYPE', 'limit', 'Number'); - if (limit < 1 || limit > 100) throw new RangeError('INVALID_RANGE_QUERY_MEMBER'); - if (typeof delay !== 'number') throw new TypeError('INVALID_TYPE', 'delay', 'Number'); - if (typeof depth !== 'number') throw new TypeError('INVALID_TYPE', 'depth', 'Number'); - if (depth < 1) throw new RangeError('INVALID_RANGE_QUERY_MEMBER'); - if (depth > 2) { - console.warn(`[WARNING] GuildMemberManager#fetchBruteforce: depth greater than 2, can lead to very slow speeds`); - } - if (delay < 500 && !options?.skipWarn) { - console.warn( - `[WARNING] GuildMemberManager#fetchBruteforce: delay is less than 500ms, this may cause rate limits.`, - ); - } - let skipValues = []; - // eslint-disable-next-line no-async-promise-executor - return new Promise(async (resolve, reject) => { - for (let i = 1; i <= depth; i++) { - dictionary = _(defaultQuery) - .permutations(i) - .map(v => _.join(v, '')) - .value(); - for (const query of dictionary) { - if (this.guild.members.cache.size >= this.guild.memberCount) break; - this.client.emit( - 'debug', - `[INFO] GuildMemberManager#fetchBruteforce: Querying ${query}, Skip: [${skipValues.join(', ')}]`, - ); - if (skipValues.some(v => query.startsWith(v))) continue; - await this._fetchMany({ query, limit }) - .then(members => { - if (members.size === 0) skipValues.push(query); - }) - .catch(reject); - await this.guild.client.sleep(delay); - } - } - resolve(this.guild.members.cache); - }); - } - - /** - * Experimental method to fetch members from the guild. - * Lists up to 10000 members of the guild. - * @param {number} [timeout=15_000] Timeout for receipt of members in ms - * @returns {Promise>} - */ - fetchByMemberSafety(timeout = 15_000) { - return new Promise(resolve => { - const nonce = SnowflakeUtil.generate(); - let timeout_ = setTimeout(() => { - this.client.removeListener(Events.GUILD_MEMBER_LIST_UPDATE, handler); - resolve(this.guild.members.cache); - }, timeout).unref(); - const handler = (members, guild, raw) => { - if (guild.id == this.guild.id && raw.nonce == nonce) { - if (members.size > 0) { - this.client.ws.broadcast({ - op: 35, - d: { - guild_id: this.guild.id, - query: '', - continuation_token: members.first()?.id, - nonce, - }, - }); - } else { - clearTimeout(timeout_); - this.client.removeListener(Events.GUILD_MEMBER_LIST_UPDATE, handler); - resolve(this.guild.members.cache); - } - } - }; - this.client.on('guildMembersChunk', handler); - this.client.ws.broadcast({ - op: 35, - d: { - guild_id: this.guild.id, - query: '', - continuation_token: null, - nonce, - }, - }); - }); - } - - /** - * Fetches multiple members from the guild in the channel. - * @param {GuildTextChannelResolvable} channel The channel to get members from (Members has VIEW_CHANNEL permission) - * @param {number} [offset=0] Start index of the members to get - * @param {boolean} [double=false] Whether to use double range - * @param {number} [retryMax=3] Number of retries - * @param {number} [time=10e3] Timeout for receipt of members - * @returns {Collection} Members in the guild - * @see {@link https://github.com/aiko-chan-ai/discord.js-selfbot-v13/blob/main/Document/FetchGuildMember.md} - * @example - * const guild = client.guilds.cache.get('id'); - * const channel = guild.channels.cache.get('id'); - * // Overlap (slow) - * for (let index = 0; index <= guild.memberCount; index += 100) { - * await guild.members.fetchMemberList(channel, index, index !== 100).catch(() => {}); - * await client.sleep(500); - * } - * // Non-overlap (fast) - * for (let index = 0; index <= guild.memberCount; index += 200) { - * await guild.members.fetchMemberList(channel, index == 0 ? 100 : index, index !== 100).catch(() => {}); - * await client.sleep(500); - * } - * console.log(guild.members.cache.size); // will print the number of members in the guild - */ - fetchMemberList(channel, offset = 0, double = false, retryMax = 3, time = 10_000) { - const channel_ = this.guild.channels.resolve(channel); - if (!channel_?.isText()) throw new TypeError('INVALID_TYPE', 'channel', 'GuildTextChannelResolvable'); - if (typeof offset !== 'number') throw new TypeError('INVALID_TYPE', 'offset', 'Number'); - if (typeof time !== 'number') throw new TypeError('INVALID_TYPE', 'time', 'Number'); - if (typeof retryMax !== 'number') throw new TypeError('INVALID_TYPE', 'retryMax', 'Number'); - if (retryMax < 1) throw new RangeError('INVALID_RANGE_RETRY'); - if (typeof double !== 'boolean') throw new TypeError('INVALID_TYPE', 'double', 'Boolean'); - // TODO: if (this.guild.large) throw new Error('GUILD_IS_LARGE'); - return new Promise((resolve, reject) => { - const default_ = [[0, 99]]; - const fetchedMembers = new Collection(); - if (offset > 99) { - // eslint-disable-next-line no-unused-expressions - double - ? default_.push([offset, offset + 99], [offset + 100, offset + 199]) - : default_.push([offset, offset + 99]); - } - let retry = 0; - const handler = (members, guild, type, raw) => { - timeout.refresh(); - if (guild.id !== this.guild.id) return; - if (type == 'INVALIDATE' && offset > 100) { - if (retry < retryMax) { - this.guild.shard.send({ - op: Opcodes.GUILD_SUBSCRIPTIONS, - d: { - guild_id: this.guild.id, - typing: true, - threads: true, - activities: true, - channels: { - [channel_.id]: default_, - }, - thread_member_lists: [], - members: [], - }, - }); - retry++; - } else { - clearTimeout(timeout); - this.client.removeListener(Events.GUILD_MEMBER_LIST_UPDATE, handler); - this.client.decrementMaxListeners(); - reject(new Error('INVALIDATE_MEMBER', raw.ops[0].range)); - } - } else { - for (const member of members.values()) { - fetchedMembers.set(member.id, member); - } - clearTimeout(timeout); - this.client.removeListener(Events.GUILD_MEMBER_LIST_UPDATE, handler); - this.client.decrementMaxListeners(); - resolve(fetchedMembers); - } - }; - const timeout = setTimeout(() => { - this.client.removeListener(Events.GUILD_MEMBER_LIST_UPDATE, handler); - this.client.decrementMaxListeners(); - reject(new Error('GUILD_MEMBERS_TIMEOUT')); - }, time).unref(); - this.client.incrementMaxListeners(); - this.client.on(Events.GUILD_MEMBER_LIST_UPDATE, handler); - this.guild.shard.send({ - op: Opcodes.GUILD_SUBSCRIPTIONS, - d: { - guild_id: this.guild.id, - typing: true, - threads: true, - activities: true, - channels: { - [channel_.id]: default_, - }, - thread_member_lists: [], - members: [], - }, - }); - }); - } - /** * Adds a role to a member. * @param {GuildMemberResolvable} user The user to add the role from @@ -718,6 +494,51 @@ class GuildMemberManager extends CachedManager { return this.resolve(user) ?? this.client.users.resolve(user) ?? userId; } + /** + * Experimental method to fetch members from the guild. + * Lists up to 10000 members of the guild. + * @param {number} [timeout=15_000] Timeout for receipt of members in ms + * @returns {Promise>} + */ + fetchByMemberSafety(timeout = 15_000) { + return new Promise(resolve => { + const nonce = SnowflakeUtil.generate(); + let timeout_ = setTimeout(() => { + this.client.removeListener(Events.GUILD_MEMBER_LIST_UPDATE, handler); + resolve(this.guild.members.cache); + }, timeout).unref(); + const handler = (members, guild, raw) => { + if (guild.id == this.guild.id && raw.nonce == nonce) { + if (members.size > 0) { + this.client.ws.broadcast({ + op: Opcodes.SEARCH_RECENT_MEMBERS, + d: { + guild_id: this.guild.id, + query: '', + continuation_token: members.first()?.id, + nonce, + }, + }); + } else { + clearTimeout(timeout_); + this.client.removeListener(Events.GUILD_MEMBER_LIST_UPDATE, handler); + resolve(this.guild.members.cache); + } + } + }; + this.client.on('guildMembersChunk', handler); + this.client.ws.broadcast({ + op: Opcodes.SEARCH_RECENT_MEMBERS, + d: { + guild_id: this.guild.id, + query: '', + continuation_token: null, + nonce, + }, + }); + }); + } + _fetchMany({ limit = 0, withPresences: presences = true, diff --git a/src/structures/DMChannel.js b/src/structures/DMChannel.js index 748dc70..b1ad09d 100644 --- a/src/structures/DMChannel.js +++ b/src/structures/DMChannel.js @@ -1,12 +1,10 @@ 'use strict'; const { Collection } = require('@discordjs/collection'); -const { joinVoiceChannel, entersState, VoiceConnectionStatus } = require('@discordjs/voice'); const { Channel } = require('./Channel'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); -const InteractionManager = require('../managers/InteractionManager'); const MessageManager = require('../managers/MessageManager'); -const { Status, Opcodes } = require('../util/Constants'); +const { Opcodes, Status } = require('../util/Constants'); /** * Represents a direct message channel between two users. @@ -25,12 +23,6 @@ class DMChannel extends Channel { * @type {MessageManager} */ this.messages = new MessageManager(this); - - /** - * A manager of the interactions sent to this channel - * @type {InteractionManager} - */ - this.interactions = new InteractionManager(this); } _patch(data) { @@ -65,7 +57,7 @@ class DMChannel extends Channel { if ('is_message_request' in data) { /** * Whether the channel is a message request - * @type {boolean} + * @type {?boolean} */ this.messageRequest = data.is_message_request; } @@ -138,78 +130,6 @@ class DMChannel extends Channel { return this.recipient.toString(); } - // These are here only for documentation purposes - they are implemented by TextBasedChannel - /* eslint-disable no-empty-function */ - get lastMessage() {} - get lastPinAt() {} - send() {} - sendTyping() {} - createMessageCollector() {} - awaitMessages() {} - createMessageComponentCollector() {} - awaitMessageComponent() {} - sendSlash() {} - searchInteraction() {} - // Doesn't work on DM channels; bulkDelete() {} - // Doesn't work on DM channels; setRateLimitPerUser() {} - // Doesn't work on DM channels; setNSFW() {} - // Testing feature: Call - // URL: https://discord.com/api/v9/channels/:DMchannelId/call/ring - /** - * Call this DMChannel. Return discordjs/voice VoiceConnection - * @param {CallOptions} options Options for the call - * @returns {Promise} - */ - call(options = {}) { - options = Object.assign( - { - ring: true, - }, - options || {}, - ); - return new Promise((resolve, reject) => { - if (!this.client.options.patchVoice) { - reject( - new Error( - 'VOICE_NOT_PATCHED', - 'Enable voice patching in client options\nhttps://discordjs-self-v13.netlify.app/#/docs/docs/main/typedef/ClientOptions', - ), - ); - } else { - if (options.ring) { - this.ring(); - } - const connection = joinVoiceChannel({ - channelId: this.id, - guildId: null, - adapterCreator: this.voiceAdapterCreator, - selfDeaf: options.selfDeaf ?? false, - selfMute: options.selfMute ?? false, - }); - entersState(connection, VoiceConnectionStatus.Ready, 30000) - .then(connection => { - resolve(connection); - }) - .catch(err => { - connection.destroy(); - reject(err); - }); - } - }); - } - - /** - * Ring the user's phone / PC (call) - * @returns {Promise} - */ - ring() { - return this.client.api.channels(this.id).call.ring.post({ - data: { - recipients: null, - }, - }); - } - /** * Sync VoiceState of this DMChannel. * @returns {undefined} @@ -222,6 +142,19 @@ class DMChannel extends Channel { }, }); } + + /** + * Ring the user's phone / PC (call) + * @returns {Promise} + */ + ring() { + return this.client.api.channels(this.id).call.ring.post({ + data: { + recipients: null, + }, + }); + } + /** * The user in this voice-based channel * @type {Collection} @@ -236,18 +169,7 @@ class DMChannel extends Channel { } return coll; } - /** - * Get connection to current call - * @type {?VoiceConnection} - * @readonly - */ - get voiceConnection() { - const check = this.client.callVoice?.joinConfig?.channelId == this.id; - if (check) { - return this.client.callVoice; - } - return null; - } + /** * Get current shard * @type {WebSocketShard} @@ -256,6 +178,7 @@ class DMChannel extends Channel { get shard() { return this.client.ws.shards.first(); } + /** * The voice state adapter for this client that can be used with @discordjs/voice to play audio in DM / Group DM channels. * @type {?Function} @@ -276,14 +199,19 @@ class DMChannel extends Channel { }; }; } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + /* eslint-disable no-empty-function */ + get lastMessage() {} + get lastPinAt() {} + send() {} + sendTyping() {} + createMessageCollector() {} + awaitMessages() {} + // Doesn't work on DM channels; setRateLimitPerUser() {} + // Doesn't work on DM channels; setNSFW() {} } -TextBasedChannel.applyToClass(DMChannel, true, [ - 'bulkDelete', - 'fetchWebhooks', - 'createWebhook', - 'setRateLimitPerUser', - 'setNSFW', -]); +TextBasedChannel.applyToClass(DMChannel, true, ['fetchWebhooks', 'createWebhook', 'setRateLimitPerUser', 'setNSFW']); module.exports = DMChannel; diff --git a/src/structures/GroupDMChannel.js b/src/structures/GroupDMChannel.js index 723f2ec..3f6f160 100644 --- a/src/structures/GroupDMChannel.js +++ b/src/structures/GroupDMChannel.js @@ -5,6 +5,7 @@ const { Channel } = require('./Channel'); const Invite = require('./Invite'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); const MessageManager = require('../managers/MessageManager'); +const { Status, Opcodes } = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); /** @@ -291,6 +292,79 @@ class GroupDMChannel extends Channel { return this; } + /** + * Ring the user's phone / PC (call) + * @param {UserResolvable[]} [recipients] Array of recipients + * @returns {Promise} + */ + ring(recipients) { + if (!recipients || !Array.isArray(recipients) || recipients.length == 0) recipients = null; + recipients = recipients.map(r => this.client.users.resolveId(r)).filter(r => r && this.recipients.get(r)); + return this.client.api.channels(this.id).call.ring.post({ + data: { + recipients, + }, + }); + } + + /** + * Sync VoiceState of this Group DMChannel. + * @returns {undefined} + */ + sync() { + this.client.ws.broadcast({ + op: Opcodes.DM_UPDATE, + d: { + channel_id: this.id, + }, + }); + } + + /** + * The user in this voice-based channel + * @type {Collection} + * @readonly + */ + get voiceUsers() { + const coll = new Collection(); + for (const state of this.client.voiceStates.cache.values()) { + if (state.channelId === this.id && state.user) { + coll.set(state.id, state.user); + } + } + return coll; + } + + /** + * Get current shard + * @type {WebSocketShard} + * @readonly + */ + get shard() { + return this.client.ws.shards.first(); + } + + /** + * The voice state adapter for this client that can be used with @discordjs/voice to play audio in DM / Group DM channels. + * @type {?Function} + * @readonly + */ + get voiceAdapterCreator() { + return methods => { + this.client.voice.adapters.set(this.id, methods); + return { + sendPayload: data => { + if (this.shard.status !== Status.READY) return false; + this.shard.send(data); + return true; + }, + destroy: () => { + this.client.voice.adapters.delete(this.id); + }, + }; + }; + } + // These are here only for documentation purposes - they are implemented by TextBasedChannel /* eslint-disable no-empty-function */ get lastMessage() {}