Skip to content
Open
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
2 changes: 2 additions & 0 deletions .github/workflows/publish-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ jobs:
"@commandkit/devtools:packages/devtools"
"@commandkit/cache:packages/cache"
"@commandkit/analytics:packages/analytics"
"@commandkit/ratelimit:packages/ratelimit"
"@commandkit/ai:packages/ai"
"@commandkit/queue:packages/queue"
"@commandkit/tasks:packages/tasks"
Expand All @@ -88,6 +89,7 @@ jobs:
"@commandkit/devtools"
"@commandkit/cache"
"@commandkit/analytics"
"@commandkit/ratelimit"
"@commandkit/ai"
"@commandkit/queue"
"@commandkit/tasks"
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/publish-latest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ jobs:
"@commandkit/devtools:packages/devtools"
"@commandkit/cache:packages/cache"
"@commandkit/analytics:packages/analytics"
"@commandkit/ratelimit:packages/ratelimit"
"@commandkit/ai:packages/ai"
"@commandkit/queue:packages/queue"
"@commandkit/tasks:packages/tasks"
Expand Down Expand Up @@ -125,6 +126,7 @@ jobs:
"@commandkit/devtools:packages/devtools"
"@commandkit/cache:packages/cache"
"@commandkit/analytics:packages/analytics"
"@commandkit/ratelimit:packages/ratelimit"
"@commandkit/ai:packages/ai"
"@commandkit/queue:packages/queue"
"@commandkit/tasks:packages/tasks"
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/publish-rc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ jobs:
"@commandkit/devtools:packages/devtools"
"@commandkit/cache:packages/cache"
"@commandkit/analytics:packages/analytics"
"@commandkit/ratelimit:packages/ratelimit"
"@commandkit/ai:packages/ai"
"@commandkit/queue:packages/queue"
"@commandkit/tasks:packages/tasks"
Expand Down Expand Up @@ -114,6 +115,7 @@ jobs:
"@commandkit/devtools"
"@commandkit/cache"
"@commandkit/analytics"
"@commandkit/ratelimit"
"@commandkit/ai"
"@commandkit/queue"
"@commandkit/tasks"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ If you are looking for support or want to provide suggestions, check out the [Di
- [@commandkit/queue](./packages/queue)
- [@commandkit/redis](./packages/redis)
- [@commandkit/tasks](./packages/tasks)
- [@commandkit/ratelimit](./packages/ratelimit)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like you created the package manually (otherwise it would have added the new package to github action as well). Could you include this to the github action?


## Contributing

Expand Down
2 changes: 2 additions & 0 deletions apps/test-bot/commandkit.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ai } from '@commandkit/ai';
import { tasks, setDriver } from '@commandkit/tasks';
import { SQLiteDriver } from '@commandkit/tasks/sqlite';
import { workflow } from '@commandkit/workflow';
import { ratelimit } from '@commandkit/ratelimit';

noBuildOnly(() => {
setDriver(new SQLiteDriver());
Expand All @@ -21,6 +22,7 @@ export default defineConfig({
i18n(),
devtools(),
cache(),
ratelimit(),
ai(),
tasks({
initializeDefaultDriver: false,
Expand Down
1 change: 1 addition & 0 deletions apps/test-bot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@commandkit/devtools": "workspace:*",
"@commandkit/i18n": "workspace:*",
"@commandkit/legacy": "workspace:*",
"@commandkit/ratelimit": "workspace:*",
"@commandkit/tasks": "workspace:*",
"@commandkit/workflow": "workspace:*",
"commandkit": "workspace:*",
Expand Down
7 changes: 7 additions & 0 deletions apps/test-bot/src/app/commands/(general)/(animal)/cat.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
ChatInputCommand,
CommandData,
CommandMetadata,
MessageCommand,
MessageContextMenuCommand,
} from 'commandkit';
Expand All @@ -24,6 +25,12 @@ export const command: CommandData = {
],
};

export const metadata: CommandMetadata = {
nameAliases: {
message: 'Cat Message',
},
};

export const messageContextMenu: MessageContextMenuCommand = async (ctx) => {
const content = ctx.interaction.targetMessage.content || 'No content found';

Expand Down
155 changes: 155 additions & 0 deletions apps/test-bot/src/app/commands/(general)/ratelimit-admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Admin/demo command for managing rate limit exemptions and resets.
//
// Keeps the workflows in one place for test-bot demos.

import type { ChatInputCommand, CommandData } from 'commandkit';
import { ApplicationCommandOptionType, PermissionFlagsBits } from 'discord.js';
import {
grantRateLimitExemption,
listRateLimitExemptions,
resetAllRateLimits,
resetRateLimit,
revokeRateLimitExemption,
} from '@commandkit/ratelimit';

const actions = ['grant', 'revoke', 'list', 'reset', 'resetAll'] as const;
type Action = (typeof actions)[number];

const actionChoices = actions.map((action) => ({
name: action,
value: action,
}));

const isAction = (value: string): value is Action =>
actions.includes(value as Action);

const demoCommandName = 'ratelimit-basic';

export const command: CommandData = {
name: 'ratelimit-admin',
description: 'Manage rate limit exemptions and resets for demos.',
options: [
{
name: 'action',
description: 'Action to perform.',
type: ApplicationCommandOptionType.String,
required: true,
choices: actionChoices,
},
{
name: 'duration',
description: 'Exemption duration (ex: 1m, 10m, 1h).',
type: ApplicationCommandOptionType.String,
required: false,
},
],
};

export const chatInput: ChatInputCommand = async (ctx) => {
const hasAdminPermission = ctx.interaction.memberPermissions?.has(
PermissionFlagsBits.Administrator,
);
if (!hasAdminPermission) {
await ctx.interaction.reply({
content: 'You are not authorized to use this command.',
ephemeral: true,
});
return;
}

const actionValue = ctx.options.getString('action', true);
if (!isAction(actionValue)) {
await ctx.interaction.reply({
content: `Unknown action: ${actionValue}`,
ephemeral: true,
});
return;
}

const action = actionValue;
const duration = ctx.options.getString('duration') ?? '1m';
const userId = ctx.interaction.user.id;

try {
switch (action) {
case 'grant': {
await grantRateLimitExemption({
scope: 'user',
id: userId,
duration,
});

await ctx.interaction.reply({
content: `Granted user exemption for ${duration}.`,
ephemeral: true,
});
return;
}
case 'revoke': {
await revokeRateLimitExemption({
scope: 'user',
id: userId,
});

await ctx.interaction.reply({
content: 'Revoked user exemption.',
ephemeral: true,
});
return;
}
case 'list': {
const exemptions = await listRateLimitExemptions({
scope: 'user',
id: userId,
});

const lines = [`Exemptions: ${exemptions.length}`];
for (const exemption of exemptions) {
const expiresIn =
exemption.expiresInMs === null
? 'unknown'
: `${Math.ceil(exemption.expiresInMs / 1000)}s`;
lines.push(`expiresIn: ${expiresIn}`);
}

await ctx.interaction.reply({
content: lines.join('\n'),
ephemeral: true,
});
return;
}
case 'reset': {
await resetRateLimit({
scope: 'user',
userId,
commandName: demoCommandName,
});

await ctx.interaction.reply({
content: `Reset rate limit for ${demoCommandName}.`,
ephemeral: true,
});
return;
}
case 'resetAll': {
await resetAllRateLimits({ commandName: demoCommandName });

await ctx.interaction.reply({
content: `Reset all rate limits for ${demoCommandName}.`,
ephemeral: true,
});
return;
}
default: {
const _exhaustive: never = action;
throw new Error(`Unsupported action: ${_exhaustive}`);
}
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
await ctx.interaction.reply({
content: `Ratelimit admin error: ${message}`,
ephemeral: true,
});
}
};
47 changes: 47 additions & 0 deletions apps/test-bot/src/app/commands/(general)/ratelimit-basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Demo command for reading aggregated rate limit info.
//
// Reports remaining/reset values captured in the env store.

import type { ChatInputCommand, CommandData, CommandMetadata } from 'commandkit';
import { getRateLimitInfo } from '@commandkit/ratelimit';

export const command: CommandData = {
name: 'ratelimit-basic',
description: 'Hit a strict limiter and show remaining/reset info.',
};

export const metadata: CommandMetadata = {
ratelimit: {
limiter: 'strict',
},
};

export const chatInput: ChatInputCommand = async (ctx) => {
const info = getRateLimitInfo(ctx);

if (!info) {
await ctx.interaction.reply({
content:
'No rate limit info was found. Ensure the ratelimit() plugin is enabled.',
});
return;
}

const now = Date.now();
const resetAt = info.resetAt
? new Date(info.resetAt).toISOString()
: 'n/a';
const resetInMs = info.resetAt ? Math.max(0, info.resetAt - now) : 0;

const lines = [
`limited: ${info.limited}`,
`remaining: ${info.remaining}`,
`retryAfterMs: ${info.retryAfter}`,
`resetAt: ${resetAt}`,
`resetInMs: ${resetInMs}`,
];

await ctx.interaction.reply({
content: lines.join('\n'),
});
};
35 changes: 35 additions & 0 deletions apps/test-bot/src/app/commands/(general)/ratelimit-directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Demo for the "use ratelimit" directive.
//
// Catches RateLimitError and replies with retry info.

import type { ChatInputCommand, CommandData } from 'commandkit';
import { RateLimitError } from '@commandkit/ratelimit';

export const command: CommandData = {
name: 'ratelimit-directive',
description: 'Demo the use ratelimit directive on a helper function.',
};

const doWork = async () => {
'use ratelimit';
return `work-${Date.now()}`;
};

export const chatInput: ChatInputCommand = async (ctx) => {
try {
const value = await doWork();
await ctx.interaction.reply({
content: `Directive call succeeded: ${value}`,
});
} catch (error) {
if (error instanceof RateLimitError) {
const retrySeconds = Math.ceil(error.result.retryAfter / 1000);
await ctx.interaction.reply({
content: `Rate limited. Retry after ${retrySeconds}s.`,
});
return;
}

throw error;
}
};
32 changes: 32 additions & 0 deletions apps/test-bot/src/app/commands/(general)/ratelimit-queue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Demo command for queued rate limiting.
//
// Shows queue delay by comparing interaction creation vs handling time.

import type { ChatInputCommand, CommandData, CommandMetadata } from 'commandkit';

export const command: CommandData = {
name: 'ratelimit-queue',
description: 'Demo queued rate limiting with timestamps.',
};

export const metadata: CommandMetadata = {
ratelimit: {
limiter: 'queued',
},
};

export const chatInput: ChatInputCommand = async (ctx) => {
const createdAtMs = ctx.interaction.createdTimestamp;
const handledAtMs = Date.now();
const delayMs = Math.max(0, handledAtMs - createdAtMs);

const lines = [
`createdAt: ${new Date(createdAtMs).toISOString()}`,
`handledAt: ${new Date(handledAtMs).toISOString()}`,
`delayMs: ${delayMs}`,
];

await ctx.interaction.reply({
content: lines.join('\n'),
});
};
Loading