feat(uwuify): rate limits
vi v@vt3e.cat
Sat, 21 Mar 2026 17:06:26 +0000
3 files changed,
93 insertions(+),
3 deletions(-)
M
src/database/schema/userPreferences.ts
→
src/database/schema/userPreferences.ts
@@ -1,4 +1,5 @@
-import { sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { sql } from "drizzle-orm"; +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; export const userPreferences = sqliteTable("user_preferences", { userId: text("user_id").primaryKey(),@@ -7,4 +8,11 @@ .default("disabled")
.notNull(), }); +export const uwuifyRateLimit = sqliteTable("uwuify_rate_limit", { + userId: text("user_id").primaryKey(), + count: integer("count").default(0).notNull(), + resetAt: integer("reset_at", { mode: "timestamp" }).notNull(), +}); + export type UserPreference = typeof userPreferences.$inferSelect; +export type UwuifyRateLimit = typeof uwuifyRateLimit.$inferSelect;
M
src/events/messageCreate.ts
→
src/events/messageCreate.ts
@@ -20,6 +20,7 @@
import config from "@/config"; import { loggers } from "@/utils/logging"; import { hasManagerPermissions } from "@/utils/permissions"; +import { checkUwuifyRateLimit, getRateLimitInfo } from "@/utils/rateLimit"; const logger = loggers.events.child({ name: "messageCreate" }); type UploadStatus = "queued" | "uploading" | "done" | "failed";@@ -36,6 +37,24 @@ .get();
if (userPref?.uwuifyMessages !== "enabled") return false; + // Check rate limit + const allowed = await checkUwuifyRateLimit(message.author.id); + if (!allowed) { + const info = await getRateLimitInfo(message.author.id); + const resetTime = info?.resetAt + ? Math.floor(info.resetAt.getTime() / 1000) + : Date.now(); + + try { + await message.reply({ + content: `you've reached the uwuification limit (25 per hour). your limit resets <t:${resetTime}:R>.`, + }); + } catch (err) { + logger.warn(err, "Failed to send rate limit message"); + } + return false; + } + const uwuifier = new Uwuifier(); const uwuifiedContent = message.content ? uwuifier.uwuifySentence(message.content)@@ -88,7 +107,7 @@ logger.error(error, `Failed to download attachment ${attachment.name}`);
} } - await message.delete(); + message.delete(); const contentParts = [ `**<@${message.author.id}>**: ${finalText || "(no text)"}`,@@ -113,7 +132,7 @@ messageReference: replyReference.id,
}; } - await message.channel.send({ + message.channel.send({ allowedMentions: { users: [], },
A
src/utils/rateLimit.ts
@@ -0,0 +1,63 @@
+import { db, uwuifyRateLimit } from "@/database"; +import { eq } from "drizzle-orm"; + +const LIMIT_PER_HOUR = 25; +const HOUR_MS = 60 * 60 * 1000; + +export async function checkUwuifyRateLimit(userId: string): Promise<boolean> { + const now = new Date(); + const existing = db + .select() + .from(uwuifyRateLimit) + .where(eq(uwuifyRateLimit.userId, userId)) + .get(); + + if (!existing) { + await db.insert(uwuifyRateLimit).values({ + userId, + count: 1, + resetAt: new Date(now.getTime() + HOUR_MS), + }); + return true; + } + + const now_ms = now.getTime(); + const resetAt_ms = existing.resetAt.getTime(); + + if (now_ms >= resetAt_ms) { + await db + .update(uwuifyRateLimit) + .set({ + count: 1, + resetAt: new Date(now_ms + HOUR_MS), + }) + .where(eq(uwuifyRateLimit.userId, userId)); + return true; + } + + if (existing.count >= LIMIT_PER_HOUR) { + return false; + } + + await db + .update(uwuifyRateLimit) + .set({ count: existing.count + 1 }) + .where(eq(uwuifyRateLimit.userId, userId)); + + return true; +} + +export function getRateLimitInfo( + userId: string, +): Promise<{ remaining: number; resetAt: Date } | null> { + return db + .select() + .from(uwuifyRateLimit) + .where(eq(uwuifyRateLimit.userId, userId)) + .then((res) => { + if (!res || res.length === 0) return null; + const record = res[0]; + const remaining = Math.max(0, LIMIT_PER_HOUR - record.count); + return { remaining, resetAt: record.resetAt }; + }); +}