rate limiter

This commit is contained in:
Erik 2022-03-02 19:08:09 +02:00
parent d9323753aa
commit e57948051b
No known key found for this signature in database
GPG Key ID: FEFF4B220DDF5589
2 changed files with 205 additions and 0 deletions

View File

@ -7,6 +7,7 @@ const StorageManager = require('./storage/StorageManager.js');
const { DefaultGuild } = require('../constants/');
const options = require('../../options.json');
const RateLimiter = require('./client/RateLimiter');
class DiscordClient extends Client {
@ -25,6 +26,7 @@ class DiscordClient extends Client {
this.storageManager = new StorageManager(this, options.storage);
this.registry = new Registry(this);
this.resolver = new Resolver(this);
this.rateLimiter = new RateLimiter(this);
this.wrapperClasses = {
...require('./client/wrappers')

View File

@ -0,0 +1,203 @@
const { TextChannel, Message } = require("discord.js");
class RateLimiter {
constructor(client) {
this.client = client;
this.sendQueue = {}; //items to bulk send, sendQueue[CHANNEL_ID] = ARRAY[TEXT]
this.sendTimeouts = {}; //timeouts for sends in each channel
this.sendQueueEmbed = {}; //same thing, except for things that are to be sent in one embed
this.sendEmbedTimeouts = {}; // same as above
this.deleteQueue = {}; //items to bulk delete
this.deleteTimeouts = {}; //same as above
this.lastSend = {}; //used by limitSend
this.lastDelete = {};
this.sendInterval = 7.5; //How frequently sending is allowed in seconds
this.deleteInterval = 2.5; //How frequently delete queues should be executed
}
/**
* Queues message deletion for bulk deletes, to avoid multiple singular delete calls
*
* @param {TextChannel} channel The channel in which to delete
* @param {Message} message The message to delete
* @returns {Promise<Boolean>}
* @memberof RateLimiter
*/
queueDelete(channel, message) {
return new Promise((resolve, reject) => {
if (!channel || !(channel instanceof TextChannel)) reject(new Error('Missing channel'));
if (!message || !(message instanceof Message)) reject(new Error('Missing message'));
if (!channel.permissionsFor(channel.guild.me).has('MANAGE_MESSAGES')) reject(new Error('Missing permission MANAGE_MESSAGES'));
if (!this.deleteQueue[channel.id]) this.deleteQueue[channel.id] = [];
this.deleteQueue[channel.id].push({ message, resolve, reject });
//if(!this.deleteTimeouts[channel.id] || this.deleteTimeouts[channel.id]._destroyed) this.deleteTimeouts[channel.id] = setTimeout(this.delete.bind(this), this.deleteInterval*1000, channel);
this.delete(channel);
});
}
async delete(channel) {
if (!this.deleteQueue[channel.id] || !this.deleteQueue[channel.id].length) return;
const resolves = [],
rejects = [],
queue = [...this.deleteQueue[channel.id]],
deleteThese = [];
const lastDelete = this.lastDelete[channel.id];
const now = Math.floor(Date.now() / 1000);
if (now - lastDelete < this.deleteInterval) {
const timeout = this.deleteTimeouts[channel.id];
if (!timeout || timeout._destroyed)
this.deleteTimeouts[channel.id] = setTimeout(this.delete.bind(this), this.deleteInterval * 1000, channel);
return;
}
this.lastDelete[channel.id] = now;
for (const item of queue) { // Organise into arrays
const { message, resolve, reject } = item;
if (deleteThese.length <= 100) {
deleteThese.push(message);
resolves.push(resolve);
rejects.push(reject);
this.deleteQueue[channel.id].shift();
} else { // left over messages go in next batch
this.deleteTimeouts[channel.id] = setTimeout(this.delete.bind(this), this.deleteInterval * 1000, channel);
break;
}
}
if (deleteThese.length === 1) {
deleteThese[0].delete().then(resolves[0]).catch(rejects[0]);
} else try {
await channel.bulkDelete(deleteThese);
for (const resolve of resolves) resolve(true);
} catch (err) {
for (const reject of rejects) reject(err);
}
}
/**
* Queue sending of multiple messages into one
*
* @param {TextChannel} channel The channel in which to send
* @param {String} message The text to send
* @returns {Promise<Message>} Resolves when the message is sent, rejects if sending fails
* @memberof RateLimiter
*/
queueSend(channel, message) {
return new Promise((resolve, reject) => {
if (!channel || !(channel instanceof TextChannel)) reject(new Error('Missing channel.'));
if (!message || !message.length) reject(new Error('Missing message.'));
if (!channel.permissionsFor(channel.guild.me).has('SEND_MESSAGES')) reject(new Error('Missing permission SEND_MESSAGES'));
//Initiate queue
if (!this.sendQueue[channel.id]) this.sendQueue[channel.id] = [];
//Possibly check for duplicates, probably not necessary
this.sendQueue[channel.id].push({ message, resolve, reject });
//Check if an active timeout exists, if not, create one
if (!this.sendTimeouts[channel.id] || this.sendTimeouts[channel.id]._destroyed)
this.sendTimeouts[channel.id] = setTimeout(this.send.bind(this), this.sendInterval * 1000, channel);
});
}
async send(channel) {
if (!this.sendQueue[channel.id] || !this.sendQueue[channel.id].length) return;
const resolves = [],
rejects = [],
queue = [...this.sendQueue[channel.id]];
let sendThis = '',
temp = '';
//Compile all messages into one
for (const item of queue) {
const { message, resolve, reject } = item;
temp = `\n${message}`;
if (sendThis.length + temp.length > 2000) {
//Max length message, send the remaining messages at the next send
this.sendTimeouts[channel.id] = setTimeout(this.send.bind(this), this.sendInterval * 1000, [channel]);
break;
} else {
sendThis += temp;
resolves.push(resolve);
rejects.push(reject);
this.sendQueue[channel.id].shift();
}
}
try {
const message = await channel.send(sendThis);
for (const resolve of resolves) resolve(message);
} catch (err) {
for (const reject of rejects) reject(err);
}
}
queueSendEmbed(channel, message) {
//TODO
}
async sendEmbed(channel) {
//TODO
}
/**
* Limit sending of messages to one every x seconds.
* Useful for stopping multiple instances of "Invites aren't permitted" being sent
*
* @param {TextChannel} channel channel in which to send
* @param {String} message the message to send
* @param {Number} [limit=15] how frequently the message can send
* @param {String} utility Limit by utility, ex invitefilter or messagefilter - so they don't overlap
* @returns {Promise<Message>} The message object of the sent message
* @memberof RateLimiter
*/
limitSend(channel, message, limit = 15, utility = 'default') {
return new Promise((resolve, reject) => {
if (!channel || !(channel instanceof TextChannel)) reject(new Error('Missing channel'));
if (!channel.permissionsFor(channel.guild.me).has('SEND_MESSAGES')) reject(new Error('Missing permission SEND_MESSAGES'));
if (!message) reject(new Error('Missing message'));
if (limit === null) limit = 15;
const now = Date.now();
if (!this.lastSend[channel.id]) this.lastSend[channel.id] = {};
if (!this.lastSend[channel.id][utility]) this.lastSend[channel.id][utility] = 0;
const lastSent = this.lastSend[channel.id][utility];
if (now - limit * 1000 >= lastSent) {
this.lastSend[channel.id][utility] = now;
resolve(channel.send(message));
} else resolve(false);
});
}
}
module.exports = RateLimiter;