Quota: Refactor how Quotas are being handled

also renamed limits to quota

I believe this new approach would allow me and bot hosters
to add, implement or change the quota behavior more easily.

Reimplemented the currently existing "Message count" limit
to use the new IQuota, refactoring a code *a little*.
This commit is contained in:
Wroclaw 2023-09-21 07:07:43 +02:00
parent 46bb5c867d
commit 339ef06ff9
5 changed files with 214 additions and 107 deletions

141
src/quota/messageCount.ts Normal file
View file

@ -0,0 +1,141 @@
import { GuildMember, User } from "discord.js";
import IQuota, { userQuotaData, userQuotaRecoveryData } from "../IQuota";
import { apiRequest } from "../execution";
import { database } from "../index";
/**
* Quota based on Request message count and function calls
*/
export default class MessageCount implements IQuota {
lookback: number;
defaultQuota: number;
constructor(
defaultQuota: number = 25,
lookback: number = 1000 * 60 * 60 * 24
) {
this.defaultQuota = defaultQuota;
this.lookback = lookback;
}
private getUserQuota(id: string) {
return database.limits.findUnique({
where: { user: BigInt(id) },
});
}
async checkUser(
user: User | GuildMember | string,
request: apiRequest
): Promise<userQuotaData> {
const userId: string = typeof user === "string" ? user : user.id;
const userQuota = await this.getUserQuota(userId);
const usedLimit = (
await database.usage.count({
select: { _all: true },
where: {
user: BigInt(userId),
timestamp: {
gte: new Date(request.createdAt.getTime() - this.lookback),
},
},
})
)._all;
if (userQuota?.vip) return this.createUserQuotaData(Infinity, usedLimit);
if (userQuota?.limit)
return this.createUserQuotaData(userQuota.limit, usedLimit);
return this.createUserQuotaData(this.defaultQuota, usedLimit);
}
async getUserQuotaRecovery(
user: string | User | GuildMember,
request: apiRequest,
unitCount: number | undefined = 1
): Promise<userQuotaRecoveryData> {
const userId = typeof user === "string" ? user : user.id;
const [userQuota, nthUseInLimit] = await Promise.all([
this.checkUser(userId, request),
database.usage.findFirst({
where: {
user: BigInt(userId),
timestamp: {
gte: new Date(request.createdTimestamp - this.lookback),
},
},
orderBy: { timestamp: "asc" },
skip: unitCount - 1,
}),
]);
return {
...userQuota,
recoveryTimestamp: nthUseInLimit ? this.lookback + nthUseInLimit?.timestamp.valueOf() : Infinity,
};
}
private createUserQuotaData(quota: number, used: number): userQuotaData {
const humanReadable = milisecondsToHumanReadable(this.lookback);
return {
quota: quota,
used: used,
unitName: "messages and model function calls",
toString() {
return `${this.used} of ${this.quota} ${this.unitName} in ${humanReadable}`;
},
};
}
}
function milisecondsToHumanReadable(totalMiliseconds: number): string {
const negative = totalMiliseconds < 0;
if (negative) totalMiliseconds = -totalMiliseconds;
const totalSeconds = totalMiliseconds / 1000;
const totalMinutes = totalSeconds / 60;
const totalHours = totalMinutes / 60;
const totalDays = totalHours / 24;
const totalYears = totalDays / 365.2425;
//const hours = Math.floor(totalHours % 24);
const minutes = Math.floor(totalMinutes % 60);
const seconds = Math.floor(totalSeconds % 60);
const miliseconds = Math.floor(totalMiliseconds % 1000);
if (totalYears > 1.01) return `${Math.ceil(totalYears * 100) / 100} years`;
if (totalDays > 16) return `${Math.ceil(totalDays * 100) / 100} days`;
if (totalDays > 1 && totalDays % 1 === 0) return `${totalDays} days`;
if (totalDays > 4 && totalHours % 1 === 0)
return `${Math.floor(totalDays)} days ${
Math.ceil((totalHours % 24) * 100) / 100
} hours`;
const textHour = totalHours === 0 ? "" : totalHours === 1 ? "1 hour" : `${totalHours} hours`;
const textMinutes =
minutes === 0 ? "" : minutes === 1 ? "1 minute" : `${minutes} minutes`;
const textSeconds =
seconds === 0 ? "" : seconds === 1 ? "1 second" : `${seconds} seconds`;
const textMiliseconds =
miliseconds === 0
? ""
: miliseconds === 1
? "1 milisecond"
: `${Math.ceil(miliseconds)} miliseconds`;
const textMilisecondsPrecise =
miliseconds === 0
? ""
: miliseconds === 1
? "1 milisecond"
: `${Math.ceil(miliseconds * 1e6) / 1e6} miliseconds`;
if (totalHours >= 1) return [textHour, textMinutes].join(" ").trim();
if (totalMinutes >= 1) return [textMinutes, textSeconds].join(" ").trim();
if (totalSeconds >= 10)
return [textSeconds, textMiliseconds].join(" ").trim();
if (totalMiliseconds >= 10) return [textMiliseconds].join(" ").trim();
return textMilisecondsPrecise;
}