diff --git a/.gitignore b/.gitignore index d384f5d289..a8e6799fdf 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ Thumbs.db # docker **/.docker/data **/.docker/logs + +# logs +*.log diff --git a/apps/roomote/package.json b/apps/roomote/package.json index 3abf84e919..c7cc001fa5 100644 --- a/apps/roomote/package.json +++ b/apps/roomote/package.json @@ -9,7 +9,7 @@ "dev": "dotenvx run -f ../../.env.development -- next dev --turbopack --port 3001", "build": "dotenvx run -f ../../.env.production -- next build", "start": "dotenvx run -f ../../.env.production -- next start --port 3001", - "controller": "dotenvx run -f ../../.env.development -- tsx src/lib/controller.ts", + "controller": "dotenvx run -f ../../.env.development -f ../../.env.keys -f ../../.env.local -- tsx src/lib/controller.ts", "controller:production": "dotenvx run -f ../../.env.production -- tsx src/lib/controller.ts", "worker": "dotenvx run -f ../../.env.development -- tsx src/lib/worker.ts", "worker:production": "dotenvx run -f ../../.env.production -- tsx src/lib/worker.ts", diff --git a/apps/roomote/src/app/api/webhooks/slack/route.ts b/apps/roomote/src/app/api/webhooks/slack/route.ts new file mode 100644 index 0000000000..0a1bc859e8 --- /dev/null +++ b/apps/roomote/src/app/api/webhooks/slack/route.ts @@ -0,0 +1,236 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { eq } from 'drizzle-orm'; + +import { db, cloudJobs } from '@roo-code-cloud/db/server'; + +import { SlackNotifier } from '@/lib/slack'; + +import { createAndEnqueueJob } from '../github/handlers/utils'; + +const mentionedThreads = new Set(); +const pendingWorkspaceSelections = new Map(); +const slack = new SlackNotifier(); + +interface SlackEvent { + type: string; + channel: string; + user: string; + text: string; + ts: string; + thread_ts?: string; + bot_id?: string; + app_id?: string; +} + +interface SlackInteractivePayload { + type: string; + user: { + id: string; + name: string; + }; + channel: { + id: string; + name: string; + }; + message: { + ts: string; + thread_ts?: string; + }; + actions: Array<{ + action_id: string; + value: string; + text: { + text: string; + }; + }>; + response_url: string; + trigger_id: string; +} + +interface SlackWebhookBody { + type: string; + challenge?: string; + event?: SlackEvent; + team_id?: string; + payload?: string; // For interactive payloads. +} + +export async function POST(request: NextRequest) { + const contentType = request.headers.get('content-type'); + + if (contentType?.includes('application/x-www-form-urlencoded')) { + const formData = await request.formData(); + const payload = formData.get('payload') as string; + + if (payload) { + const interactivePayload: SlackInteractivePayload = JSON.parse(payload); + await handleInteractivePayload(interactivePayload); + return NextResponse.json({ ok: true }); + } + } + + const body: SlackWebhookBody = JSON.parse(await request.text()); + + if (body.type === 'url_verification') { + console.log('🔐 Slack URL verification challenge received'); + return NextResponse.json({ challenge: body.challenge }); + } + + if (body.type === 'event_callback' && body.event) { + const event = body.event; + + if (event.bot_id || event.app_id) { + return NextResponse.json({ ok: true }); + } + + console.log('🛎️ Slack Event ->', { + type: event.type, + channel: event.channel, + user: event.user, + text: event.text?.substring(0, 100), + thread_ts: event.thread_ts, + ts: event.ts, + }); + + switch (event.type) { + case 'app_mention': + await handleAppMention(event); + break; + + case 'message': + await handleMessage(event); + break; + + default: + console.log(`Unhandled event type: ${event.type}`); + } + } + + return NextResponse.json({ ok: true }); +} + +async function handleInteractivePayload(payload: SlackInteractivePayload) { + console.log('🎯 Interactive payload received:', { + type: payload.type, + user: payload.user.id, + channel: payload.channel.id, + action: payload.actions[0]?.action_id, + value: payload.actions[0]?.value, + }); + + if ( + payload.actions[0]?.action_id === 'select_roo_code_cloud' || + payload.actions[0]?.action_id === 'select_roo_code' + ) { + const workspace = payload.actions[0].value; + const threadId = payload.message.thread_ts || payload.message.ts; + + try { + const originalEvent = pendingWorkspaceSelections.get(threadId); + + if (!originalEvent) { + throw new Error('Original mention event not found'); + } + + pendingWorkspaceSelections.delete(threadId); + + const { jobId, enqueuedJobId } = await createAndEnqueueJob( + 'slack.app.mention', + { + channel: originalEvent.channel, + user: originalEvent.user, + text: originalEvent.text, + ts: originalEvent.ts, + thread_ts: threadId, + workspace, + }, + ); + + console.log( + `🔗 Enqueued slack.app.mention job for workspace ${workspace} (id: ${jobId}, enqueued: ${enqueuedJobId})`, + ); + + await slack.postMessage({ + text: `✅ Enqueued job ${enqueuedJobId} for workspace ${workspace}.`, + channel: payload.channel.id, + thread_ts: threadId, + }); + + await db + .update(cloudJobs) + .set({ slackThreadTs: threadId }) + .where(eq(cloudJobs.id, jobId)); + } catch (error) { + console.error('❌ Failed to process workspace selection:', error); + + await slack.postMessage({ + text: `❌ Sorry, something went wrong processing your request. Please try again.`, + channel: payload.channel.id, + thread_ts: threadId, + }); + } + } +} + +async function handleAppMention(event: SlackEvent) { + console.log('🤖 Bot mentioned in channel:', event.channel); + const threadId = event.thread_ts || event.ts; + mentionedThreads.add(threadId); + console.log(`📌 Tracking thread: ${threadId}`); + + try { + pendingWorkspaceSelections.set(threadId, event); + + const result = await slack.postMessage({ + text: '👋 Which workspace would you like me to work in?', + channel: event.channel, + thread_ts: threadId, + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: '👋 Which workspace would you like me to work in?', + }, + }, + { + type: 'actions', + elements: [ + { + type: 'button', + text: { type: 'plain_text', text: 'Roo-Code-Cloud', emoji: true }, + action_id: 'select_roo_code_cloud', + value: 'roo-code-cloud', + }, + { + type: 'button', + text: { type: 'plain_text', text: 'Roo-Code', emoji: true }, + action_id: 'select_roo_code', + value: 'roo-code', + }, + ], + }, + ], + }); + + console.log(`✅ Sent workspace selection to thread: ${threadId}`, result); + } catch (error) { + console.error('❌ Failed to process app mention:', error); + } +} + +async function handleMessage(event: SlackEvent) { + if (!event.thread_ts || !mentionedThreads.has(event.thread_ts)) { + return; + } + + console.log('💬 New message in tracked thread:', { + thread: event.thread_ts, + channel: event.channel, + user: event.user, + text: event.text?.substring(0, 100), + }); + + // TODO: Process the thread message. + // This is where you'd handle follow-up messages in the thread. +} diff --git a/apps/roomote/src/lib/__tests__/controller.test.ts b/apps/roomote/src/lib/__tests__/controller.test.ts index e2c12f4935..604c964c8c 100644 --- a/apps/roomote/src/lib/__tests__/controller.test.ts +++ b/apps/roomote/src/lib/__tests__/controller.test.ts @@ -1,7 +1,5 @@ // npx vitest src/lib/__tests__/controller.test.ts -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; - const mockQueue = { getWaiting: vi.fn(() => Promise.resolve([])), getActive: vi.fn(() => Promise.resolve([])), @@ -9,6 +7,10 @@ const mockQueue = { on: vi.fn(), }; +const mockWorker = { + startStalledCheckTimer: vi.fn(), +}; + const mockSpawn = vi.fn(() => ({ stdout: { pipe: vi.fn() }, stderr: { pipe: vi.fn() }, @@ -37,8 +39,11 @@ vi.mock('fs', () => ({ const mockQueueConstructor = vi.fn(() => mockQueue); +const mockWorkerConstructor = vi.fn(() => mockWorker); + vi.mock('bullmq', () => ({ Queue: mockQueueConstructor, + Worker: mockWorkerConstructor, })); describe('WorkerController', () => { diff --git a/apps/roomote/src/lib/checkStalledJobs.ts b/apps/roomote/src/lib/checkStalledJobs.ts deleted file mode 100644 index d59cb4509b..0000000000 --- a/apps/roomote/src/lib/checkStalledJobs.ts +++ /dev/null @@ -1,37 +0,0 @@ -// npx dotenvx run -f ../../.env.production -- tsx src/lib/checkStalledJobs.ts - -import { Worker } from 'bullmq'; - -import { redis } from './redis'; - -async function checkStalledJobs() { - const worker = new Worker('roomote', undefined, { - autorun: false, - connection: redis, - }); - - while (true) { - console.log('startStalledCheckTimer()'); - await worker.startStalledCheckTimer(); - await new Promise((resolve) => setTimeout(resolve, 30_000)); - } -} - -process.on('SIGTERM', async () => { - console.log('SIGTERM'); - process.exit(0); -}); - -process.on('SIGINT', async () => { - console.log('SIGINT'); - process.exit(0); -}); - -checkStalledJobs() - .then(() => { - process.exit(0); - }) - .catch((error) => { - console.error(error); - process.exit(1); - }); diff --git a/apps/roomote/src/lib/cli.ts b/apps/roomote/src/lib/cli.ts index a9457efbcc..b06dea90d1 100644 --- a/apps/roomote/src/lib/cli.ts +++ b/apps/roomote/src/lib/cli.ts @@ -1,6 +1,3 @@ -import * as path from 'path'; -import * as os from 'node:os'; - import { command, run, @@ -19,7 +16,6 @@ import { processPullRequestComment, } from '@/lib/jobs'; import { runTask } from '@/lib/runTask'; -import { Logger } from '@/lib/logger'; const fixIssueCommand = command({ name: 'fix-issue', @@ -277,11 +273,6 @@ const promptCommand = command({ jobType: 'test.prompt', jobPayload: { text }, prompt: text, - logger: new Logger({ - logDir: path.resolve(os.tmpdir(), 'logs'), - filename: 'cli.log', - tag: 'worker', - }), notify: false, workspacePath, settings: { mode }, diff --git a/apps/roomote/src/lib/controller.ts b/apps/roomote/src/lib/controller.ts index 3375583966..5ef1790215 100644 --- a/apps/roomote/src/lib/controller.ts +++ b/apps/roomote/src/lib/controller.ts @@ -1,4 +1,4 @@ -import { Queue } from 'bullmq'; +import { Queue, Worker } from 'bullmq'; import { execa } from 'execa'; import { redis } from './redis'; @@ -9,28 +9,34 @@ export class WorkerController { private readonly MAX_WORKERS = 2; private queue: Queue; + private stalledJobsWorker: Worker; public isRunning = false; private pollingInterval: NodeJS.Timeout | null = null; private activeWorkers = new Set(); constructor() { this.queue = new Queue('roomote', { connection: redis }); + + this.stalledJobsWorker = new Worker('roomote', undefined, { + autorun: false, + connection: redis, + }); } async start() { if (this.isRunning) { - console.log('Controller is already running'); return; } this.isRunning = true; - console.log('Worker controller started'); await this.checkAndSpawnWorker(); this.pollingInterval = setInterval(async () => { await this.checkAndSpawnWorker(); }, this.POLL_INTERVAL_MS); + + await this.stalledJobsWorker.startStalledCheckTimer(); } async stop() { @@ -92,7 +98,9 @@ export class WorkerController { '--network roo-code-cloud_default', `-e APP_ENV=${process.env.APP_ENV || 'development'}`, `-e GH_TOKEN=${process.env.GH_TOKEN}`, + `-e DOTENV_PRIVATE_KEY_DEVELOPMENT=${process.env.DOTENV_PRIVATE_KEY_DEVELOPMENT}`, `-e DOTENV_PRIVATE_KEY_PRODUCTION=${process.env.DOTENV_PRIVATE_KEY_PRODUCTION}`, + `-e WORKSPACE_ROOT=${process.env.WORKSPACE_ROOT}`, '-v /var/run/docker.sock:/var/run/docker.sock', '-v /tmp/roomote:/var/log/roomote', ]; diff --git a/apps/roomote/src/lib/job.ts b/apps/roomote/src/lib/job.ts index dcd1d7a92c..1fcd4f5757 100644 --- a/apps/roomote/src/lib/job.ts +++ b/apps/roomote/src/lib/job.ts @@ -1,6 +1,7 @@ import { eq } from 'drizzle-orm'; import { Job } from 'bullmq'; +import type { ClineMessage } from '@roo-code/types'; import { type JobType, type JobStatus, @@ -14,6 +15,10 @@ import { import { fixGitHubIssue } from './jobs/fixGitHubIssue'; import { processPullRequestComment } from './jobs/processPullRequestComment'; import { processIssueComment } from './jobs/processIssueComment'; +import { processSlackMention } from './jobs/processSlackMention'; +import { SlackNotifier } from './slack'; + +const slack = new SlackNotifier(); export async function processJob({ data: { type, payload, jobId }, @@ -31,20 +36,17 @@ export async function processJob({ result = await fixGitHubIssue( payload as JobPayload<'github.issue.fix'>, { - onTaskStarted: async ( + onTaskStarted: ( slackThreadTs: string | null | undefined, _rooTaskId: string, - ) => { - if (slackThreadTs) { - await updateJobStatus( - jobId, - 'processing', - undefined, - undefined, - slackThreadTs, - ); - } - }, + ) => + updateJobStatus( + jobId, + 'processing', + undefined, + undefined, + slackThreadTs, + ), }, ); @@ -53,20 +55,17 @@ export async function processJob({ result = await processIssueComment( payload as JobPayload<'github.issue.comment.respond'>, { - onTaskStarted: async ( + onTaskStarted: ( slackThreadTs: string | null | undefined, _rooTaskId: string, - ) => { - if (slackThreadTs) { - await updateJobStatus( - jobId, - 'processing', - undefined, - undefined, - slackThreadTs, - ); - } - }, + ) => + updateJobStatus( + jobId, + 'processing', + undefined, + undefined, + slackThreadTs, + ), }, ); @@ -75,34 +74,60 @@ export async function processJob({ result = await processPullRequestComment( payload as JobPayload<'github.pr.comment.respond'>, { - onTaskStarted: async ( + onTaskStarted: ( slackThreadTs: string | null | undefined, _rooTaskId: string, - ) => { - if (slackThreadTs) { - await updateJobStatus( - jobId, - 'processing', - undefined, - undefined, - slackThreadTs, - ); - } - }, + ) => + updateJobStatus( + jobId, + 'processing', + undefined, + undefined, + slackThreadTs, + ), }, ); break; + case 'slack.app.mention': { + const jobPayload = payload as JobPayload<'slack.app.mention'>; + const { channel, thread_ts } = jobPayload; + + result = await processSlackMention(jobPayload, { + onTaskStarted: ( + slackThreadTs: string | null | undefined, + _rooTaskId: string, + ) => + updateJobStatus( + jobId, + 'processing', + undefined, + undefined, + slackThreadTs, + ), + onTaskMessage: async (message: ClineMessage) => { + console.log(`onTaskMessage (${channel}, ${thread_ts}) ->`, message); + + if ( + (message.say === 'text' || message.say === 'completion_result') && + message.text && + thread_ts + ) { + slack.postMessage({ text: message.text, channel, thread_ts }); + } + }, + }); + + break; + } default: throw new Error(`Unknown job type: ${type}`); } await updateJobStatus(jobId, 'completed', result); - console.log( - `[${job.name} | ${job.id}] Job ${jobId} completed successfully`, - ); + console.log(`[${job.name} | ${job.id}] ✅`); } catch (error) { - console.error(`[${job.name} | ${job.id}] Job ${jobId} failed:`, error); + console.error(`[${job.name} | ${job.id}] ❌`, error); const errorMessage = error instanceof Error ? error.message : String(error); await updateJobStatus(jobId, 'failed', undefined, errorMessage); throw error; // Re-throw to mark job as failed in BullMQ. @@ -114,7 +139,7 @@ async function updateJobStatus( status: JobStatus, result?: unknown, error?: string, - slackThreadTs?: string, + slackThreadTs?: string | null, ) { const values: UpdateCloudJob = { status }; diff --git a/apps/roomote/src/lib/jobs/fixGitHubIssue.ts b/apps/roomote/src/lib/jobs/fixGitHubIssue.ts index d725c295b6..1d8206dd8a 100644 --- a/apps/roomote/src/lib/jobs/fixGitHubIssue.ts +++ b/apps/roomote/src/lib/jobs/fixGitHubIssue.ts @@ -1,10 +1,6 @@ -import * as path from 'path'; -import * as os from 'node:os'; - import type { JobType, JobPayload } from '@roo-code-cloud/db'; import { runTask, type RunTaskCallbacks } from '../runTask'; -import { Logger } from '../logger'; const jobType: JobType = 'github.issue.fix'; @@ -31,11 +27,6 @@ Issue #${jobPayload.issue} jobType, jobPayload, prompt, - logger: new Logger({ - logDir: path.resolve(os.tmpdir(), 'logs'), - filename: 'worker.log', - tag: 'worker', - }), callbacks, settings: { mode: 'issue-fixer', diff --git a/apps/roomote/src/lib/jobs/index.ts b/apps/roomote/src/lib/jobs/index.ts index 6ae4ffe6cc..14609e0456 100644 --- a/apps/roomote/src/lib/jobs/index.ts +++ b/apps/roomote/src/lib/jobs/index.ts @@ -1,3 +1,4 @@ export { fixGitHubIssue } from './fixGitHubIssue'; export { processIssueComment } from './processIssueComment'; export { processPullRequestComment } from './processPullRequestComment'; +export { processSlackMention } from './processSlackMention'; diff --git a/apps/roomote/src/lib/jobs/processIssueComment.ts b/apps/roomote/src/lib/jobs/processIssueComment.ts index 7c51ffe588..a9951a67a6 100644 --- a/apps/roomote/src/lib/jobs/processIssueComment.ts +++ b/apps/roomote/src/lib/jobs/processIssueComment.ts @@ -1,10 +1,6 @@ -import * as path from 'path'; -import * as os from 'node:os'; - import type { JobType, JobPayload } from '@roo-code-cloud/db'; import { runTask, type RunTaskCallbacks } from '../runTask'; -import { Logger } from '../logger'; const jobType: JobType = 'github.issue.comment.respond'; @@ -58,11 +54,6 @@ gh api repos/${jobPayload.repo}/issues/${jobPayload.issueNumber}/comments --meth jobType, jobPayload, prompt, - logger: new Logger({ - logDir: path.resolve(os.tmpdir(), 'logs'), - filename: 'worker.log', - tag: 'worker', - }), callbacks, }); diff --git a/apps/roomote/src/lib/jobs/processPullRequestComment.ts b/apps/roomote/src/lib/jobs/processPullRequestComment.ts index 097a2716ed..84610f2c9c 100644 --- a/apps/roomote/src/lib/jobs/processPullRequestComment.ts +++ b/apps/roomote/src/lib/jobs/processPullRequestComment.ts @@ -1,10 +1,6 @@ -import * as path from 'path'; -import * as os from 'node:os'; - import type { JobType, JobPayload } from '@roo-code-cloud/db'; import { runTask, type RunTaskCallbacks } from '../runTask'; -import { Logger } from '../logger'; const jobType: JobType = 'github.pr.comment.respond'; @@ -72,11 +68,6 @@ Do not create a new pull request - work directly on the existing PR branch. jobType, jobPayload, prompt, - logger: new Logger({ - logDir: path.resolve(os.tmpdir(), 'logs'), - filename: 'worker.log', - tag: 'worker', - }), callbacks, }); diff --git a/apps/roomote/src/lib/jobs/processSlackMention.ts b/apps/roomote/src/lib/jobs/processSlackMention.ts new file mode 100644 index 0000000000..7e02528bff --- /dev/null +++ b/apps/roomote/src/lib/jobs/processSlackMention.ts @@ -0,0 +1,33 @@ +import type { JobType, JobPayload } from '@roo-code-cloud/db'; + +import { runTask, type RunTaskCallbacks } from '../runTask'; + +const jobType: JobType = 'slack.app.mention'; + +type ProcessSlackMentionJobPayload = JobPayload<'slack.app.mention'>; + +export async function processSlackMention( + jobPayload: ProcessSlackMentionJobPayload, + callbacks?: RunTaskCallbacks, +): Promise<{ + channel: string; + user: string; + result: unknown; +}> { + const { text: prompt, channel, user } = jobPayload; + + // Add your workspace root to .env.local to override the default + // workspace root that our containers use. + const workspaceRoot = process.env.WORKSPACE_ROOT || '/roo/repos'; + + const result = await runTask({ + jobType, + jobPayload, + prompt, + callbacks, + notify: false, + workspacePath: `${workspaceRoot}/${jobPayload.workspace}`, + }); + + return { channel, user, result }; +} diff --git a/apps/roomote/src/lib/runTask.ts b/apps/roomote/src/lib/runTask.ts index 7762a427a8..47401dff70 100644 --- a/apps/roomote/src/lib/runTask.ts +++ b/apps/roomote/src/lib/runTask.ts @@ -8,6 +8,7 @@ import { execa } from 'execa'; import { type RooCodeSettings, + type ClineMessage, TaskCommandName, RooCodeEventName, IpcMessageType, @@ -35,6 +36,7 @@ export type RunTaskCallbacks = { slackThreadTs: string | null | undefined, rooTaskId: string, ) => Promise; + onTaskMessage?: (message: ClineMessage) => Promise; onTaskAborted?: (slackThreadTs: string | null | undefined) => Promise; onTaskCompleted?: ( slackThreadTs: string | null | undefined, @@ -50,7 +52,7 @@ type RunTaskOptions = { jobType: T; jobPayload: JobPayload; prompt: string; - logger: Logger; + logger?: Logger; callbacks?: RunTaskCallbacks; notify?: boolean; workspacePath?: string; @@ -80,6 +82,14 @@ export const runTask = async ({ ? `ROO_CODE_IPC_SOCKET_PATH=${ipcSocketPath} xvfb-run --auto-servernum --server-num=1 code --wait --log trace --disable-workspace-trust --disable-gpu --disable-lcd-text --no-sandbox --user-data-dir /roo/.vscode --password-store="basic" -n ${workspacePath}` : `ROO_CODE_IPC_SOCKET_PATH=${ipcSocketPath} code --disable-workspace-trust -n ${workspacePath}`; + if (!logger) { + logger = new Logger({ + logDir: path.resolve(os.tmpdir(), 'logs'), + filename: 'worker.log', + tag: 'worker', + }); + } + logger.info(codeCommand); const subprocess = execa({ @@ -152,6 +162,14 @@ export const runTask = async ({ logger.info(`${eventName} ->`, payload); } + if ( + eventName === RooCodeEventName.Message && + payload[0].message.partial !== true && + callbacks?.onTaskMessage + ) { + await callbacks.onTaskMessage(payload[0].message); + } + if (eventName === RooCodeEventName.TaskStarted) { taskStartedAt = Date.now(); rooTaskId = payload[0]; @@ -252,11 +270,14 @@ export const runTask = async ({ if (rooTaskId && !isClientDisconnected) { logger.info('cancelling task'); + client.sendCommand({ commandName: TaskCommandName.CancelTask, data: rooTaskId, }); - await new Promise((resolve) => setTimeout(resolve, 5_000)); // Allow some time for the task to cancel. + + // Allow some time for the task to cancel. + await new Promise((resolve) => setTimeout(resolve, 5_000)); } taskFinishedAt = Date.now(); @@ -282,11 +303,14 @@ export const runTask = async ({ if (rooTaskId && !isClientDisconnected) { logger.info('closing task'); + client.sendCommand({ commandName: TaskCommandName.CloseTask, data: rooTaskId, }); - await new Promise((resolve) => setTimeout(resolve, 2_000)); // Allow some time for the window to close. + + // Allow some time for the window to close. + await new Promise((resolve) => setTimeout(resolve, 2_000)); } if (!isClientDisconnected) { diff --git a/apps/roomote/src/lib/slack.ts b/apps/roomote/src/lib/slack.ts index 8299b9c467..dd4fded8d7 100644 --- a/apps/roomote/src/lib/slack.ts +++ b/apps/roomote/src/lib/slack.ts @@ -27,7 +27,7 @@ export class SlackNotifier { this.token = token; } - private async postMessage(message: SlackMessage): Promise { + public async postMessage(message: SlackMessage): Promise { try { const messageWithChannel = { ...message, @@ -120,6 +120,21 @@ export class SlackNotifier { ], }); } + case 'slack.app.mention': { + const payload = jobPayload as JobPayload<'slack.app.mention'>; + return await this.postMessage({ + text: `🚀 Task Started`, + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: `🚀 *Task Started*\nProcessing your Slack mention from <#${payload.channel}>\n*Message:* ${payload.text.slice(0, 100)}${payload.text.length > 100 ? '...' : ''}`, + }, + }, + ], + }); + } default: throw new Error(`Unknown job type: ${jobType}`); } @@ -159,8 +174,4 @@ export class SlackNotifier { thread_ts: threadTs, }); } - - public async sendMessage(message: SlackMessage): Promise { - return await this.postMessage(message); - } } diff --git a/packages/db/src/types.ts b/packages/db/src/types.ts index acdc453050..fc6e2479f0 100644 --- a/packages/db/src/types.ts +++ b/packages/db/src/types.ts @@ -122,6 +122,17 @@ export const createJobSchema = z.discriminatedUnion('type', [ commentUrl: z.string(), }), }), + z.object({ + type: z.literal('slack.app.mention'), + payload: z.object({ + channel: z.string(), + user: z.string(), + text: z.string(), + ts: z.string(), + thread_ts: z.string().optional(), + workspace: z.string(), + }), + }), z.object({ type: z.literal('test.prompt'), payload: z.object({