Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ OmegaBot is online
│ │ ├── discord
│ │ │ ├── commandLoader.ts
│ │ │ ├── commandMeta.ts
│ │ │ ├── cooldowns.ts
│ │ │ ├── fetchChannelMessages.ts
│ │ │ └── interactionHandler.ts
│ │ ├── faq
Expand Down
41 changes: 41 additions & 0 deletions src/services/discord/cooldowns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// src/services/discord/cooldowns.ts

/**
* In-memory cooldown store.
*
* Key format: `${userId}:${commandName}`
* Value: timestamp (ms) when the command can be used again
*/
const cooldowns = new Map<string, number>();

function makeKey(userId: string, commandName: string): string {
return `${userId}:${commandName}`;
}

/**
* Returns remaining cooldown time in milliseconds.
* Returns 0 if no cooldown is active.
*/
export function getCooldownRemainingMs(userId: string, commandName: string): number {
const key = makeKey(userId, commandName);
const expiresAt = cooldowns.get(key);

if (!expiresAt) return 0;

const remaining = expiresAt - Date.now();
return remaining > 0 ? remaining : 0;
}

/**
* Sets a cooldown for a user + command.
*/
export function setCooldown(
userId: string,
commandName: string,
durationMs: number,
): void {
if (durationMs <= 0) return;

const key = makeKey(userId, commandName);
cooldowns.set(key, Date.now() + durationMs);
}
42 changes: 42 additions & 0 deletions src/services/discord/interactionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,23 @@
import type { Interaction, ChatInputCommandInteraction } from "discord.js";
import type { CommandClient } from "./commandLoader.js";
import { logger } from "../../utils/logger.js";
import { getCooldownRemainingMs, setCooldown } from "./cooldowns.js";

/**
* Per-command cooldown durations (ms).
*
* Keep this list small and focused:
* - Only expensive commands should be throttled (LLM calls, external APIs).
* - Defaults to 0 (no cooldown) for commands not listed here.
*
* Adjust as needed.
*/
const COOLDOWN_MS: Record<string, number> = {
summary: 30_000,
// github lookups (example)
// gh: 5_000,
// pr: 5_000,
};

/**
* Handle a single Discord interaction.
Expand Down Expand Up @@ -73,6 +90,31 @@ export async function handleInteraction(
return;
}

/**
* Cooldowns (per-user, per-command)
*
* Cooldown is enforced here so every command benefits consistently.
* We set cooldown BEFORE execution to prevent spam even if the command fails.
*/
const userId = interaction.user.id;
const commandName = interaction.commandName;

const remainingMs = getCooldownRemainingMs(userId, commandName);
if (remainingMs > 0) {
const seconds = Math.ceil(remainingMs / 1000);

await interaction.reply({
content: `⏳ That command is on cooldown. Try again in ${seconds}s.`,
ephemeral: true,
});
return;
}

const durationMs = COOLDOWN_MS[commandName] ?? 0;
if (durationMs > 0) {
setCooldown(userId, commandName, durationMs);
}

/**
* Execute the command safely.
*
Expand Down