-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add global/command-specific configurable ratelimiting
- Loading branch information
Showing
10 changed files
with
325 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { Difference } from './types/Difference'; | ||
|
||
/** | ||
* Extend the Date class to provide helper methods | ||
*/ | ||
export class Time extends Date | ||
{ | ||
public constructor() { super(); } | ||
|
||
/** | ||
* Return a Difference object representing the time difference between a and b | ||
*/ | ||
public static difference(a: number, b: number): Difference | ||
{ | ||
let difference: Difference = {}; | ||
let ms: number = a - b; | ||
difference.ms = ms; | ||
|
||
let days: number = Math.floor(ms / 1000 / 60 / 60 / 24); | ||
ms -= days * 1000 * 60 * 60 * 24; | ||
let hours: number = Math.floor(ms / 1000 / 60 / 60); | ||
ms -= hours * 1000 * 60 * 60; | ||
let mins: number = Math.floor(ms / 1000 / 60); | ||
ms -= mins * 1000 * 60; | ||
let secs: number = Math.floor(ms / 1000); | ||
|
||
let timeString: string = ''; | ||
if (days) { difference.days = days; timeString += `${days} days${hours ? ', ' : ' '}`; } | ||
if (hours) { difference.hours = hours; timeString += `${hours} hours${mins ? ', ' : ' '}`; } | ||
if (mins) { difference.mins = mins; timeString += `${mins} mins${secs ? ', ' : ' '}`; } | ||
if (secs) { difference.secs = secs; timeString += `${secs} secs`; } | ||
|
||
// Returns the time string as '# days, # hours, # mins, # secs' | ||
difference.toString = () => timeString.trim() || `${(ms / 1000).toFixed(2)} seconds`; | ||
|
||
// Returns the time string as '#d #h #m #s' | ||
difference.toSimplifiedString = () => | ||
timeString.replace(/ays|ours|ins|ecs| /g, '').replace(/,/g, ' ').trim(); | ||
|
||
return difference; | ||
} | ||
|
||
/** | ||
* Return a Difference object (for convenience) measuring the | ||
* duration of the given MS | ||
*/ | ||
public static duration(time: number): Difference | ||
{ | ||
return this.difference(time * 2, time); | ||
} | ||
|
||
/** | ||
* Parse a duration shorthand string and return the duration in ms | ||
* | ||
* Shorthand examples: 10m, 5h, 1d | ||
*/ | ||
public static parseShorthand(shorthand: string): number | ||
{ | ||
let duration: number, match: RegExpMatchArray; | ||
if (/^(?:\d+(?:\.\d+)?)[s|m|h|d]$/.test(<string> shorthand)) | ||
{ | ||
match = shorthand.match(/(\d+(?:\.\d+)?)(s|m|h|d)$/); | ||
duration = parseFloat(match[1]); | ||
duration = match[2] === 's' | ||
? duration * 1000 : match[2] === 'm' | ||
? duration * 1000 * 60 : match[2] === 'h' | ||
? duration * 1000 * 60 * 60 : match[2] === 'd' | ||
? duration * 1000 * 60 * 60 * 24 : null; | ||
} | ||
return duration; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
/** | ||
* Maintains its own call count and expiry for making sure | ||
* things only happen a certain number of times within | ||
* a given timeframe | ||
*/ | ||
export class RateLimit | ||
{ | ||
private _limit: number; | ||
private _duration: number; | ||
private _count: number; | ||
private _notified: boolean; | ||
public expires: number; | ||
public constructor(limit: [number, number]) | ||
{ | ||
[this._limit, this._duration] = limit; | ||
this._reset(); | ||
} | ||
|
||
/** | ||
* Sets this RateLimit to default values | ||
*/ | ||
private _reset(): void | ||
{ | ||
this._count = 0; | ||
this.expires = 0; | ||
this._notified = false; | ||
} | ||
|
||
/** | ||
* Returns whether or not this rate limit has been capped out | ||
* for its current expiry window while incrementing calls | ||
* towards the rate limit cap if not currently capped | ||
*/ | ||
public call(): boolean | ||
{ | ||
if (this.expires < Date.now()) this._reset(); | ||
if (this._count >= this._limit) return false; | ||
this._count++; | ||
if (this._count === 1) this.expires = Date.now() + this._duration; | ||
return true; | ||
} | ||
|
||
/** | ||
* Return whether or not this ratelimit is currently capped out | ||
*/ | ||
public get isLimited(): boolean | ||
{ | ||
return (this._count >= this._limit) && (Date.now() < this.expires); | ||
} | ||
|
||
/** | ||
* Flag this RateLimit as having had the user the RateLimit | ||
* is for notified of being rate limited | ||
*/ | ||
public setNotified(): void | ||
{ | ||
this._notified = true; | ||
} | ||
|
||
/** | ||
* Return whether or not this RateLimit was flagged after | ||
* notifying the user of being rate limited | ||
*/ | ||
public get wasNotified(): boolean | ||
{ | ||
return this._notified; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { Collection } from 'discord.js'; | ||
import { RateLimit } from './RateLimit'; | ||
import { Time } from '../Time'; | ||
import { Message } from '../types/Message'; | ||
|
||
/** | ||
* Handles assigning ratelimits to guildmembers and users | ||
*/ | ||
export class RateLimiter | ||
{ | ||
private _limit: [number, number]; | ||
private _global: boolean; | ||
private _rateLimits: Collection<string, Collection<string, RateLimit>>; | ||
private _globalLimits: Collection<string, RateLimit>; | ||
public constructor(limit: string, global: boolean) | ||
{ | ||
this._limit = this._parseLimit(limit); | ||
this._global = global; | ||
|
||
this._rateLimits = new Collection<string, Collection<string, RateLimit>>(); | ||
this._globalLimits = new Collection<string, RateLimit>(); | ||
} | ||
|
||
/** | ||
* Returns the RateLimit object for the message author if global | ||
* or message member if message is in a guild | ||
*/ | ||
public get(message: Message): RateLimit | ||
{ | ||
if (this._isGlobal(message)) | ||
{ | ||
if (!this._globalLimits.has(message.author.id)) | ||
this._globalLimits.set(message.author.id, new RateLimit(this._limit)); | ||
return this._globalLimits.get(message.author.id); | ||
} | ||
else | ||
{ | ||
if (!this._rateLimits.has(message.guild.id)) | ||
this._rateLimits.set(message.guild.id, new Collection<string, RateLimit>()); | ||
|
||
if (!this._rateLimits.get(message.guild.id).has(message.author.id)) | ||
this._rateLimits.get(message.guild.id).set(message.author.id, new RateLimit(this._limit)); | ||
|
||
return this._rateLimits.get(message.guild.id).get(message.author.id); | ||
} | ||
} | ||
|
||
/** | ||
* Parse the ratelimit from the given input string | ||
*/ | ||
private _parseLimit(limitString: string): [number, number] | ||
{ | ||
const limitRegex: RegExp = /^(\d+)\/(\d+)(s|m|h|d)?$/; | ||
if (!limitRegex.test(limitString)) throw new Error(`Failed to parse a ratelimit from '${limitString}'`); | ||
let [limit, duration, post]: (string | number)[] = limitRegex.exec(limitString).slice(1, 4); | ||
|
||
if (post) duration = Time.parseShorthand(duration + post); | ||
else duration = parseInt(duration); | ||
limit = parseInt(limit); | ||
|
||
return [limit, duration]; | ||
} | ||
|
||
/** | ||
* Determine whether or not to use the global rate limit collection | ||
*/ | ||
private _isGlobal(message?: Message): boolean | ||
{ | ||
return message ? message.channel.type !== 'text' || this._global : this._global; | ||
} | ||
} |
Oops, something went wrong.