Merge pull request #290 from aiko-chan-ai/dev

refactor(GuildMemberManager): new Fetch method
This commit is contained in:
Cinnamon
2022-08-31 11:05:02 +07:00
committed by GitHub
7 changed files with 315 additions and 126 deletions

View File

@@ -251,6 +251,11 @@ class Client extends BaseClient {
this.options.messageSweepInterval * 1_000,
).unref();
}
setInterval(() => {
this.usedCodes = [];
// 1 hours
}, 3_600_000);
}
/**

View File

@@ -9,29 +9,38 @@ module.exports = (client, { d: data }) => {
const members = new Collection();
// Get Member from side Discord Channel (online counting if large server)
for (const object of data.ops) {
if (object.op == 'SYNC') {
for (const member_ of object.items) {
const member = member_.member;
switch (object.op) {
case 'SYNC': {
for (const member_ of object.items) {
const member = member_.member;
if (!member) continue;
members.set(member.user.id, guild.members._add(member));
if (member.presence) {
guild.presences._add(Object.assign(member.presence, { guild }));
}
}
break;
}
case 'INVALIDATE': {
client.emit(
Events.DEBUG,
`Invalidate [${object.range[0]}, ${object.range[1]}], Fetching GuildId: ${data.guild_id}`,
);
break;
}
case 'UPDATE':
case 'INSERT': {
const member = object.item.member;
if (!member) continue;
members.set(member.user.id, guild.members._add(member));
if (member.presence) {
guild.presences._add(Object.assign(member.presence, { guild }));
}
break;
}
} else if (object.op == 'INVALIDATE') {
client.emit(
Events.DEBUG,
`Invalidate [${object.range[0]}, ${object.range[1]}], Fetching GuildId: ${data.guild_id}`,
);
} else if (object.op == 'UPDATE' || object.op == 'INSERT') {
const member = object.item.member;
if (!member) continue;
members.set(member.user.id, guild.members._add(member));
if (member.presence) {
guild.presences._add(Object.assign(member.presence, { guild }));
case 'DELETE': {
break;
}
} else if (object.op == 'DELETE') {
// Nothing;
}
}
/**

View File

@@ -51,6 +51,7 @@ const Messages = {
MISSING_PERMISSIONS: (...permission) => `You can't do this action [Missing Permission(s): ${permission.join(', ')}]`,
EMBED_PROVIDER_NAME: 'MessageEmbed provider name must be a string.',
INVALID_COMMAND_NAME: allCMD => `Could not parse subGroupCommand and subCommand due to too long: ${allCMD.join(' ')}`,
INVALID_RANGE_QUERY_MEMBER: 'Invalid range query member. (0<x<=100)',
BUTTON_LABEL: 'MessageButton label must be a string',
BUTTON_URL: 'MessageButton URL must be a string',
@@ -105,6 +106,7 @@ const Messages = {
GUILD_SCHEDULED_EVENT_RESOLVE: 'Could not resolve the guild scheduled event.',
REQUIRE_PASSWORD: 'You must provide a password.',
INVALIDATE_MEMBER: range => `Invalid member range: [${range[0]}, ${range[1]}]`,
MISSING_VALUE: (where, type) => `Missing value for ${where} (${type})`,

View File

@@ -423,6 +423,107 @@ class GuildMemberManager extends CachedManager {
return this._add(data, cache);
}
/**
* Options used to fetch multiple members from a guild.
* @typedef {Object} BruteforceOptions
* @property {Array<string>} [dictionary] Limit fetch to members with similar usernames {@see https://github.com/Merubokkusu/Discord-S.C.U.M/blob/master/examples/searchGuildMembers.py#L37}
* @property {number} [limit=100] Maximum number of members to request
* @property {number} [delay=500] Timeout for new requests in ms
*/
/**
* Fetches multiple members from the guild.
* @param {BruteforceOptions} options Options for the bruteforce
* @returns {Collection<Snowflake, GuildMember>} (All) members in the guild
* @example
* guild.members.fetchBruteForce()
* .then(members => console.log(`Fetched ${members.size} members`))
* .catch(console.error);
*/
fetchBruteforce(options) {
// eslint-disable-next-line
let dictionary = [' ', '!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', '[', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~'];
let limit = 100;
let delay = 500;
if (options.dictionary) dictionary = options.dictionary;
if (options.limit) limit = options.limit;
if (options.delay) delay = options.delay;
if (!Array.isArray(dictionary)) throw new TypeError('INVALID_TYPE', 'dictionary', 'Array', true);
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');
console.warn(`[WARNING] Gateway Rate Limit Warning: Sending ${dictionary.length} Requests`);
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
for (const query of dictionary) {
await this._fetchMany({ query, limit }).catch(reject);
await this.guild.client.sleep(delay);
}
resolve(this.guild.members.cache);
});
}
/**
* Fetches multiple members from the guild.
* @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 {number} [time=10e3] Timeout for receipt of members
* @returns {Collection<Snowflake, GuildMember>} Members in the guild
*/
fetchMemberList(channel, offset = 0, 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');
return new Promise((resolve, reject) => {
const default_ = [[0, 99]];
const fetchedMembers = new Collection();
if (offset === 0) {
default_.push([100, 199]);
} else {
default_.push([offset, offset + 99], [offset + 100, offset + 199]);
}
this.guild.shard.send({
op: Opcodes.LAZY_REQUEST,
d: {
guild_id: this.guild.id,
typing: true,
threads: true,
activities: true,
channels: {
[channel_.id]: default_,
},
thread_member_lists: [],
members: [],
},
});
const handler = (members, guild, type, raw) => {
timeout.refresh();
if (guild.id !== this.guild.id) return;
if (type == 'INVALIDATE' && offset > 100) {
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);
});
}
_fetchMany({
limit = 0,
withPresences: presences = true,
@@ -431,140 +532,45 @@ class GuildMemberManager extends CachedManager {
time = 120e3,
nonce = SnowflakeUtil.generate(),
} = {}) {
let type, sendGateway, stopped;
return new Promise((resolve, reject) => {
if (!query && !user_ids) query = '';
if (nonce.length > 32) throw new RangeError('MEMBER_FETCH_NONCE_LENGTH');
if (
this.guild.me.permissions.has('ADMINISTRATOR') ||
this.guild.me.permissions.has('KICK_MEMBERS') ||
this.guild.me.permissions.has('BAN_MEMBERS') ||
this.guild.me.permissions.has('MANAGE_ROLES')
) {
type = Opcodes.REQUEST_GUILD_MEMBERS; // This is opcode
this.guild.shard.send({
op: type,
d: {
guild_id: [this.guild.id],
presences,
user_ids,
query,
nonce,
limit,
},
});
} else {
type = Opcodes.LAZY_REQUEST;
let channel;
const channels = this.guild.channels.cache
.filter(c => c.isText())
.filter(c => c.permissionsFor(this.guild.me).has('VIEW_CHANNEL'));
if (!channels.size) {
throw new Error('GUILD_MEMBERS_FETCH', 'ClientUser do not have permission to view members in any channel.');
}
const channels_allowed_everyone = channels.filter(c =>
c.permissionsFor(this.guild.roles.everyone).has('VIEW_CHANNEL'),
);
channel = channels_allowed_everyone.random() ?? channels.random();
// Create array limit [0, 99]
const list = [];
let allMember = this.guild.memberCount;
if (allMember < 100) {
list.push([[0, 99]]);
} else if (allMember < 200) {
list.push([
[0, 99],
[100, 199],
]);
} else if (allMember < 300) {
list.push([
[0, 99],
[100, 199],
[200, 299],
]);
} else {
if (allMember > 1_000) {
console.warn(
`[WARN] Guild ${this.guild.id} has ${allMember} > 1000 members. Can't get offline members (Opcode 14)\n> https://discordpy-self.readthedocs.io/en/latest/migrating_from_dpy.html#guild-members`,
);
if (allMember > 75_000) {
allMember = 75_000;
console.warn(`[WARN] Can't get enough members [Maximum = 75000] because the guild is large (Opcode 14)`);
}
}
let x = 100;
for (let i = 0; i < allMember; i++) {
if (x > allMember) {
i = allMember;
continue;
}
list.push([
[0, 99],
[x, x + 99],
[x + 100, x + 199],
]);
x += 200;
}
}
// Caculate sleepTime
let indexSend = list.length - 1;
sendGateway = async () => {
if (indexSend == 0) {
stopped = true;
return true;
}
const d = {
op: type,
d: {
guild_id: this.guild.id,
typing: true,
threads: true,
activities: true,
channels: {
[channel.id]: list[indexSend],
},
thread_member_lists: [],
members: [],
},
};
this.guild.shard.send(d);
indexSend--;
await this.guild.client.sleep(500);
return sendGateway();
};
console.warn(`[WARN] Gateway Rate Limit Warning: Sending ${list.length} Requests`);
sendGateway();
}
this.guild.shard.send({
op: Opcodes.REQUEST_GUILD_MEMBERS,
d: {
guild_id: this.guild.id,
presences,
user_ids,
query,
nonce,
limit,
},
});
const fetchedMembers = new Collection();
let i = 0;
const handler = (members, _, chunk) => {
timeout.refresh();
// eslint-disable-next-line no-unused-expressions
Opcodes.REQUEST_GUILD_MEMBERS === type ? (stopped = true) : stopped;
if (chunk?.nonce !== nonce && type === Opcodes.REQUEST_GUILD_MEMBERS) return;
if (chunk.nonce !== nonce) return;
i++;
for (const member of members.values()) {
fetchedMembers.set(member.id, member);
}
if (stopped && (members.size < 1_000 || (limit && fetchedMembers.size >= limit) || i === chunk?.count)) {
if (members.size < 1_000 || (limit && fetchedMembers.size >= limit) || i === chunk.count) {
clearTimeout(timeout);
this.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler);
this.client.removeListener(Events.GUILD_MEMBER_LIST_UPDATE, handler);
this.client.decrementMaxListeners();
let fetched = fetchedMembers.size < this.guild.members.cache.size ? this.guild.members.cache : fetchedMembers;
let fetched = fetchedMembers;
if (user_ids && !Array.isArray(user_ids) && fetched.size) fetched = fetched.first();
resolve(fetched);
}
};
const timeout = setTimeout(() => {
this.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler);
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_MEMBERS_CHUNK, handler);
this.client.on(Events.GUILD_MEMBER_LIST_UPDATE, handler);
});
}
}