Skip to content

Commit 1fc61e1

Browse files
committed
feat: tasks package
1 parent 02fdef7 commit 1fc61e1

12 files changed

Lines changed: 787 additions & 18 deletions

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { CommandData, SlashCommand } from 'commandkit';
2+
import { ApplicationCommandOptionType, PermissionFlagsBits } from 'discord.js';
3+
import { UnmuteTaskData } from '../unmute';
4+
5+
export const command: CommandData = {
6+
name: 'mute',
7+
description: 'Mute a user for a specified duration',
8+
options: [
9+
{
10+
name: 'user',
11+
description: 'The user to mute',
12+
type: ApplicationCommandOptionType.User,
13+
required: true,
14+
},
15+
{
16+
name: 'duration',
17+
description: 'Duration of the mute (e.g., 30m, 1h, 2d)',
18+
type: ApplicationCommandOptionType.String,
19+
required: true,
20+
},
21+
{
22+
name: 'reason',
23+
description: 'Reason for the mute',
24+
type: ApplicationCommandOptionType.String,
25+
},
26+
],
27+
defaultMemberPermissions: [PermissionFlagsBits.ModerateMembers],
28+
};
29+
30+
export const chatInput: SlashCommand = async (ctx) => {
31+
const userToMute = ctx.options.getUser('user', true);
32+
const duration = ctx.options.getString('duration', true);
33+
const reason = ctx.options.getString('reason');
34+
35+
try {
36+
// Get the member to mute
37+
const member = await ctx.guild?.members.fetch(userToMute.id);
38+
39+
if (!member) {
40+
return ctx.reply({
41+
content: '❌ Could not find that user in this server.',
42+
ephemeral: true,
43+
});
44+
}
45+
46+
// In a real implementation, you would add a muted role here
47+
// await member.roles.add('muted-role-id');
48+
49+
// Create a task to unmute the user after the specified duration
50+
const task = await ctx.tasks.create({
51+
name: 'unmute', // must match the name of the task file
52+
data: {
53+
guildId: ctx.guild?.id as string,
54+
userId: userToMute.id,
55+
reason,
56+
} as UnmuteTaskData,
57+
duration: duration,
58+
});
59+
60+
await ctx.reply({
61+
content: `✅ ${userToMute} has been muted for ${duration}. They will be automatically unmuted. (Task ID: ${task.id})`,
62+
});
63+
64+
// You could also save the task ID to a database to reference it later
65+
// This allows you to cancel or update the task if needed
66+
} catch (error) {
67+
console.error('Error in mute command:', error);
68+
await ctx.reply({
69+
content: '❌ An error occurred while trying to mute that user.',
70+
ephemeral: true,
71+
});
72+
}
73+
};
74+
75+
// Example of how to cancel a mute task early (e.g., in an unmute command)
76+
export async function cancelMuteTask(ctx: any, userId: string) {
77+
// You would need to retrieve the task ID from your database
78+
// const taskId = await getTaskIdFromDatabase(guildId, userId);
79+
const taskId = 'task-id-from-database';
80+
81+
if (taskId) {
82+
await ctx.tasks.cancel(taskId);
83+
console.log(`Cancelled unmute task for user ${userId}`);
84+
}
85+
}

packages/tasks/examples/config.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { CommandKit } from 'commandkit';
2+
import { Client, GatewayIntentBits } from 'discord.js';
3+
import { tasks, configureTaskManager, bullmq } from '../src';
4+
5+
// Create a Discord.js client
6+
const client = new Client({
7+
intents: [
8+
GatewayIntentBits.Guilds,
9+
GatewayIntentBits.GuildMembers,
10+
// Add other intents as needed
11+
],
12+
});
13+
14+
// Set up CommandKit with the tasks plugin
15+
const commandkit = new CommandKit({
16+
client,
17+
devGuildIds: ['YOUR_DEV_GUILD_ID'],
18+
commandsPath: `${__dirname}/commands`,
19+
plugins: [tasks()],
20+
});
21+
22+
// Configure the task manager to use the BullMQ driver
23+
configureTaskManager(async (manager) => {
24+
// Create a BullMQ driver with Redis configuration
25+
const driver = bullmq({
26+
redis: {
27+
host: 'localhost', // Redis host
28+
port: 6379, // Redis port
29+
// password: 'your-redis-password', // Uncomment if needed
30+
// url: 'redis://localhost:6379', // Alternative to host/port
31+
},
32+
queue: 'my-discord-bot-tasks', // Custom queue name
33+
});
34+
35+
// Set the driver for the task manager
36+
manager.useDriver(driver);
37+
});
38+
39+
// Login with your bot token
40+
client.login('YOUR_BOT_TOKEN');
41+
42+
// Handle process termination
43+
process.on('SIGINT', async () => {
44+
console.log('Shutting down...');
45+
await commandkit.destroy();
46+
process.exit(0);
47+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { TaskContext, TaskConfig } from '../src';
2+
3+
export const config: TaskConfig = {
4+
name: 'refresh-exchange-rate', // uses file name by default
5+
// Run every 24 hours using cron syntax
6+
pattern: '0 0 * * *',
7+
// Alternative way to specify using friendly syntax
8+
// every: '1 day',
9+
};
10+
11+
export default async function refreshExchangeRate(ctx: TaskContext) {
12+
// ctx contains client and commandkit instances
13+
const { client, commandkit } = ctx;
14+
15+
// This is where you would fetch exchange rate data from an API
16+
console.log(`[${new Date().toISOString()}] Refreshing exchange rates...`);
17+
18+
// Example: Update exchange rates in your database or cache
19+
// const rates = await fetchExchangeRates();
20+
// await updateDatabase(rates);
21+
22+
console.log('Exchange rates updated successfully!');
23+
}

packages/tasks/examples/unmute.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { TaskContext } from '../src';
2+
3+
// Define the data structure for the unmute task
4+
export interface UnmuteTaskData {
5+
guildId: string;
6+
userId: string;
7+
reason?: string;
8+
}
9+
10+
/**
11+
* Unmute task that removes the muted role from a user after a specified duration
12+
*/
13+
export default async function unmute(ctx: TaskContext<UnmuteTaskData>) {
14+
const { client, data, taskId } = ctx;
15+
const { guildId, userId, reason } = data;
16+
17+
console.log(
18+
`[${new Date().toISOString()}] Running unmute task ${taskId} for user ${userId} in guild ${guildId}`,
19+
);
20+
21+
try {
22+
// Get the guild
23+
const guild = await client.guilds.fetch(guildId);
24+
25+
// Get the member
26+
const member = await guild.members.fetch(userId);
27+
28+
// This would be the actual implementation to remove the muted role
29+
// In a real bot, you would have a configuration for the muted role ID
30+
// await member.roles.remove('muted-role-id');
31+
32+
console.log(
33+
`Unmuted ${member.user.tag} ${reason ? `(Reason: ${reason})` : ''}`,
34+
);
35+
} catch (error) {
36+
console.error(
37+
`Failed to unmute user ${userId} in guild ${guildId}:`,
38+
error,
39+
);
40+
}
41+
}

packages/tasks/package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,19 @@
2626
"url": "https://github.com/underctrl-io/commandkit/issues"
2727
},
2828
"homepage": "https://github.com/underctrl-io/commandkit#readme",
29-
"devDependencies": {
29+
"dependencies": {
3030
"bullmq": "^5.48.1",
31+
"cron-parser": "^4.9.0",
32+
"ms": "^2.1.3"
33+
},
34+
"devDependencies": {
35+
"@types/ms": "^0.7.34",
3136
"commandkit": "workspace:*",
3237
"tsconfig": "workspace:*",
3338
"typescript": "^5.7.3"
39+
},
40+
"peerDependencies": {
41+
"commandkit": "^1.0.0",
42+
"discord.js": "^14.0.0"
3443
}
3544
}

packages/tasks/src/TaskContextManager.ts

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { CommandKitTask, CreateTaskOptions, UpdateTaskOptions } from './driver';
12
import { TaskManager } from './TaskManager';
23

34
export type JSONEncodable =
@@ -18,17 +19,81 @@ export interface TaskDefinition {
1819
export class TaskContextManager {
1920
public constructor(private manager: TaskManager) {}
2021

21-
public async create(task: TaskDefinition) {}
22+
/**
23+
* Create a new dynamic task
24+
* @param task Task definition
25+
* @returns The created task
26+
*/
27+
public async create(task: TaskDefinition): Promise<CommandKitTask> {
28+
const driver = this.manager.getDriver();
29+
if (!driver) {
30+
throw new Error('Task driver not configured');
31+
}
2232

23-
public async cancel(task: string) {}
33+
const executor = this.manager.getExecutor(task.name);
34+
if (!executor) {
35+
throw new Error(`Task '${task.name}' not found`);
36+
}
2437

25-
public async complete(task: string) {}
38+
const options: CreateTaskOptions = {
39+
name: task.name,
40+
id: task.id || `${task.name}:${Date.now()}`,
41+
data: task.data,
42+
duration: task.duration,
43+
};
2644

27-
public async fail(task: string) {}
45+
return driver.createTask(options, executor);
46+
}
47+
48+
/**
49+
* Cancel a task by its ID
50+
* @param taskId The ID of the task to cancel
51+
* @returns Whether the task was successfully canceled
52+
*/
53+
public async cancel(taskId: string): Promise<boolean> {
54+
const driver = this.manager.getDriver();
55+
if (!driver) {
56+
throw new Error('Task driver not configured');
57+
}
58+
59+
return driver.cancelTask(taskId);
60+
}
61+
62+
/**
63+
* Immediately invoke a task
64+
* @param taskId The ID of the task to invoke
65+
* @param data Optional data to pass to the task
66+
*/
67+
public async invoke(taskId: string, data?: JSONEncodable): Promise<void> {
68+
const driver = this.manager.getDriver();
69+
if (!driver) {
70+
throw new Error('Task driver not configured');
71+
}
2872

29-
public async invoke(task: string, data: JSONEncodable) {}
73+
await driver.invokeTask(taskId, data);
74+
}
3075

31-
public async update(task: string, data: JSONEncodable) {}
76+
/**
77+
* Update an existing task
78+
* @param taskId The ID of the task to update
79+
* @param options Update options including data and/or duration
80+
* @returns The updated task
81+
*/
82+
public async update(
83+
taskId: string,
84+
options: UpdateTaskOptions,
85+
): Promise<CommandKitTask> {
86+
const driver = this.manager.getDriver();
87+
if (!driver) {
88+
throw new Error('Task driver not configured');
89+
}
90+
91+
return driver.updateTask(taskId, options);
92+
}
93+
94+
public async complete(task: string) {}
95+
96+
public async fail(task: string) {}
3297

3398
public async get(task: string) {}
3499

0 commit comments

Comments
 (0)