mirror of https://github.com/buster-so/buster.git
Enhance Slack integration by adding support for direct messages (DMs) and updating event handling. Introduced new environment variables for Slack integration, updated deployment command to use the latest version, and improved message filtering logic for DMs. Refactored Slack event processing to differentiate between app mentions and direct messages, ensuring proper handling and logging. Updated webhook types to include message IM events.
This commit is contained in:
commit
1d3146e35b
|
@ -43,4 +43,4 @@ jobs:
|
||||||
TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}
|
TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
cd apps/trigger
|
cd apps/trigger
|
||||||
pnpm dlx trigger.dev@v4-beta deploy --env ${{ github.ref_name == 'main' && 'production' || 'staging' }}
|
pnpm dlx trigger.dev@latest deploy --env ${{ github.ref_name == 'main' && 'production' || 'staging' }}
|
|
@ -1,6 +1,12 @@
|
||||||
import { chats, db, getSecretByName, slackIntegrations } from '@buster/database';
|
import { chats, db, getSecretByName, slackIntegrations } from '@buster/database';
|
||||||
import type { SlackEventsResponse } from '@buster/server-shared/slack';
|
import type { SlackEventsResponse } from '@buster/server-shared/slack';
|
||||||
import { type SlackWebhookPayload, addReaction, isEventCallback } from '@buster/slack';
|
import {
|
||||||
|
type SlackWebhookPayload,
|
||||||
|
addReaction,
|
||||||
|
isAppMentionEvent,
|
||||||
|
isEventCallback,
|
||||||
|
isMessageImEvent,
|
||||||
|
} from '@buster/slack';
|
||||||
import { tasks } from '@trigger.dev/sdk';
|
import { tasks } from '@trigger.dev/sdk';
|
||||||
import { and, eq } from 'drizzle-orm';
|
import { and, eq } from 'drizzle-orm';
|
||||||
import type { Context } from 'hono';
|
import type { Context } from 'hono';
|
||||||
|
@ -38,6 +44,7 @@ export async function findOrCreateSlackChat({
|
||||||
userId,
|
userId,
|
||||||
slackChatAuthorization,
|
slackChatAuthorization,
|
||||||
teamId,
|
teamId,
|
||||||
|
isDM = false,
|
||||||
}: {
|
}: {
|
||||||
threadTs: string;
|
threadTs: string;
|
||||||
channelId: string;
|
channelId: string;
|
||||||
|
@ -45,6 +52,7 @@ export async function findOrCreateSlackChat({
|
||||||
userId: string;
|
userId: string;
|
||||||
slackChatAuthorization: 'unauthorized' | 'authorized' | 'auto_added';
|
slackChatAuthorization: 'unauthorized' | 'authorized' | 'auto_added';
|
||||||
teamId: string;
|
teamId: string;
|
||||||
|
isDM?: boolean;
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
// Run both queries concurrently for better performance
|
// Run both queries concurrently for better performance
|
||||||
const [existingChat, slackIntegration] = await Promise.all([
|
const [existingChat, slackIntegration] = await Promise.all([
|
||||||
|
@ -101,11 +109,22 @@ export async function findOrCreateSlackChat({
|
||||||
slackChatAuthorization,
|
slackChatAuthorization,
|
||||||
slackThreadTs: threadTs,
|
slackThreadTs: threadTs,
|
||||||
slackChannelId: channelId,
|
slackChannelId: channelId,
|
||||||
// Set workspace sharing based on Slack integration settings
|
// DM chats are NEVER shared with workspace, regardless of settings
|
||||||
workspaceSharing: defaultSharingPermissions === 'shareWithWorkspace' ? 'can_view' : 'none',
|
workspaceSharing: isDM
|
||||||
workspaceSharingEnabledBy: defaultSharingPermissions === 'shareWithWorkspace' ? userId : null,
|
? 'none'
|
||||||
workspaceSharingEnabledAt:
|
: defaultSharingPermissions === 'shareWithWorkspace'
|
||||||
defaultSharingPermissions === 'shareWithWorkspace' ? new Date().toISOString() : null,
|
? 'can_view'
|
||||||
|
: 'none',
|
||||||
|
workspaceSharingEnabledBy: isDM
|
||||||
|
? null
|
||||||
|
: defaultSharingPermissions === 'shareWithWorkspace'
|
||||||
|
? userId
|
||||||
|
: null,
|
||||||
|
workspaceSharingEnabledAt: isDM
|
||||||
|
? null
|
||||||
|
: defaultSharingPermissions === 'shareWithWorkspace'
|
||||||
|
? new Date().toISOString()
|
||||||
|
: null,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
@ -171,16 +190,21 @@ export async function handleSlackEventsEndpoint(c: Context) {
|
||||||
export async function eventsHandler(payload: SlackWebhookPayload): Promise<SlackEventsResponse> {
|
export async function eventsHandler(payload: SlackWebhookPayload): Promise<SlackEventsResponse> {
|
||||||
try {
|
try {
|
||||||
// Handle the event based on type
|
// Handle the event based on type
|
||||||
if (isEventCallback(payload) && payload.event.type === 'app_mention') {
|
if (isEventCallback(payload)) {
|
||||||
// Handle app_mention event
|
|
||||||
const event = payload.event;
|
const event = payload.event;
|
||||||
|
|
||||||
console.info('App mentioned:', {
|
// Check if this is an app_mention or DM event
|
||||||
|
const isAppMention = isAppMentionEvent(event);
|
||||||
|
const isDM = isMessageImEvent(event);
|
||||||
|
|
||||||
|
if (isAppMention || isDM) {
|
||||||
|
console.info(isDM ? 'DM received:' : 'App mentioned:', {
|
||||||
team_id: payload.team_id,
|
team_id: payload.team_id,
|
||||||
channel: event.channel,
|
channel: event.channel,
|
||||||
user: event.user,
|
user: event.user,
|
||||||
text: event.text,
|
text: event.text,
|
||||||
event_id: payload.event_id,
|
event_id: payload.event_id,
|
||||||
|
is_dm: isDM,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Authenticate the Slack user
|
// Authenticate the Slack user
|
||||||
|
@ -258,11 +282,13 @@ export async function eventsHandler(payload: SlackWebhookPayload): Promise<Slack
|
||||||
userId,
|
userId,
|
||||||
slackChatAuthorization: mapAuthResultToDbEnum(authResult.type),
|
slackChatAuthorization: mapAuthResultToDbEnum(authResult.type),
|
||||||
teamId: payload.team_id,
|
teamId: payload.team_id,
|
||||||
|
isDM,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Queue the task
|
// Queue the task
|
||||||
await queueSlackAgentTask(chatId, userId);
|
await queueSlackAgentTask(chatId, userId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -123,26 +123,57 @@ export const slackAgentTask: ReturnType<
|
||||||
hasParentMessage: slackMessages.length > 0,
|
hasParentMessage: slackMessages.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if this is a DM (channel ID starts with 'D')
|
||||||
|
const isDM = chatDetails.slackChannelId?.startsWith('D') || false;
|
||||||
|
|
||||||
// Filter messages to only include non-bot messages after the most recent app mention
|
// Filter messages to only include non-bot messages after the most recent app mention
|
||||||
if (!integration.botUserId) {
|
if (!integration.botUserId) {
|
||||||
logger.error('No bot user ID found for Slack integration');
|
logger.error('No bot user ID found for Slack integration');
|
||||||
throw new Error('Slack integration is missing bot user ID');
|
throw new Error('Slack integration is missing bot user ID');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { filteredMessages: relevantMessages, mentionMessageTs } =
|
let relevantMessages: typeof slackMessages;
|
||||||
filterMessagesAfterLastMention(slackMessages, integration.botUserId);
|
let mentionMessageTs: string | null;
|
||||||
|
|
||||||
logger.log('Filtered relevant messages', {
|
if (isDM) {
|
||||||
|
// For DMs, we don't need to look for mentions - all messages are for the bot
|
||||||
|
// Use the most recent message timestamp as the "mention" timestamp for reactions
|
||||||
|
relevantMessages = slackMessages.filter((msg) => msg.user !== integration.botUserId);
|
||||||
|
mentionMessageTs =
|
||||||
|
relevantMessages.length > 0
|
||||||
|
? relevantMessages[relevantMessages.length - 1]?.ts || null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
logger.log('Processing DM messages', {
|
||||||
|
originalCount: slackMessages.length,
|
||||||
|
filteredCount: relevantMessages.length,
|
||||||
|
botUserId: integration.botUserId,
|
||||||
|
mentionMessageTs,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// For channel messages, look for @Buster mentions
|
||||||
|
const filterResult = filterMessagesAfterLastMention(slackMessages, integration.botUserId);
|
||||||
|
relevantMessages = filterResult.filteredMessages;
|
||||||
|
mentionMessageTs = filterResult.mentionMessageTs;
|
||||||
|
|
||||||
|
logger.log('Filtered channel messages', {
|
||||||
originalCount: slackMessages.length,
|
originalCount: slackMessages.length,
|
||||||
filteredCount: relevantMessages.length,
|
filteredCount: relevantMessages.length,
|
||||||
botUserId: integration.botUserId,
|
botUserId: integration.botUserId,
|
||||||
mentionMessageTs,
|
mentionMessageTs,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If no mention was found, we can't proceed
|
// If no mention was found in a channel, we can't proceed
|
||||||
if (!mentionMessageTs) {
|
if (!mentionMessageTs) {
|
||||||
logger.error('No @Buster mention found in thread');
|
logger.error('No @Buster mention found in channel thread');
|
||||||
throw new Error('No @Buster mention found in the thread');
|
throw new Error('No @Buster mention found in the channel thread');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no relevant timestamp found (shouldn't happen), we can't proceed
|
||||||
|
if (!mentionMessageTs) {
|
||||||
|
logger.error('No message timestamp found for reactions');
|
||||||
|
throw new Error('No message timestamp found to react to');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all bot messages in the thread to determine if this is a follow-up
|
// Find all bot messages in the thread to determine if this is a follow-up
|
||||||
|
@ -154,6 +185,25 @@ export const slackAgentTask: ReturnType<
|
||||||
// Get all messages for context, not just after the mention
|
// Get all messages for context, not just after the mention
|
||||||
let messagesToInclude: typeof slackMessages;
|
let messagesToInclude: typeof slackMessages;
|
||||||
|
|
||||||
|
if (isDM) {
|
||||||
|
// For DMs, handle follow-ups differently
|
||||||
|
if (isFollowUp) {
|
||||||
|
// Find the timestamp of the last bot message
|
||||||
|
const lastBotMessageTs = Math.max(
|
||||||
|
...previousBotMessages.map((msg) => Number.parseFloat(msg.ts))
|
||||||
|
);
|
||||||
|
const lastBotMessageIndex = slackMessages.findIndex(
|
||||||
|
(msg) => Number.parseFloat(msg.ts) === lastBotMessageTs
|
||||||
|
);
|
||||||
|
|
||||||
|
// Include messages after the last bot response
|
||||||
|
messagesToInclude = slackMessages.slice(lastBotMessageIndex + 1);
|
||||||
|
} else {
|
||||||
|
// Include all messages for first request
|
||||||
|
messagesToInclude = slackMessages;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For channel messages, use the existing logic
|
||||||
if (isFollowUp) {
|
if (isFollowUp) {
|
||||||
// Find the timestamp of the last bot message before the current mention
|
// Find the timestamp of the last bot message before the current mention
|
||||||
const lastBotMessageTs = Math.max(
|
const lastBotMessageTs = Math.max(
|
||||||
|
@ -169,13 +219,14 @@ export const slackAgentTask: ReturnType<
|
||||||
// Include all messages in the thread for first request
|
// Include all messages in the thread for first request
|
||||||
messagesToInclude = slackMessages;
|
messagesToInclude = slackMessages;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Filter out bot messages and format the conversation
|
// Filter out bot messages and format the conversation
|
||||||
const formattedMessages = messagesToInclude
|
const formattedMessages = messagesToInclude
|
||||||
.filter((msg) => msg.user !== integration.botUserId) // Exclude bot messages
|
.filter((msg) => msg.user !== integration.botUserId) // Exclude bot messages
|
||||||
.map((msg) => {
|
.map((msg) => {
|
||||||
let text = msg.text || '';
|
let text = msg.text || '';
|
||||||
// Replace bot user ID mentions with @Buster
|
// Replace bot user ID mentions with @Buster for consistency
|
||||||
if (integration.botUserId) {
|
if (integration.botUserId) {
|
||||||
text = text.replace(new RegExp(`<@${integration.botUserId}>`, 'g'), '@Buster');
|
text = text.replace(new RegExp(`<@${integration.botUserId}>`, 'g'), '@Buster');
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -8,9 +8,14 @@ export {
|
||||||
urlVerificationSchema,
|
urlVerificationSchema,
|
||||||
slackRequestHeadersSchema,
|
slackRequestHeadersSchema,
|
||||||
appMentionEventSchema,
|
appMentionEventSchema,
|
||||||
|
messageImEventSchema,
|
||||||
eventCallbackSchema,
|
eventCallbackSchema,
|
||||||
slackEventEnvelopeSchema,
|
slackEventEnvelopeSchema,
|
||||||
slackWebhookPayloadSchema,
|
slackWebhookPayloadSchema,
|
||||||
|
isUrlVerification,
|
||||||
|
isEventCallback,
|
||||||
|
isAppMentionEvent,
|
||||||
|
isMessageImEvent,
|
||||||
} from './types/webhooks';
|
} from './types/webhooks';
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
|
|
|
@ -39,6 +39,23 @@ export const appMentionEventSchema = z.object({
|
||||||
|
|
||||||
export type AppMentionEvent = z.infer<typeof appMentionEventSchema>;
|
export type AppMentionEvent = z.infer<typeof appMentionEventSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message IM Event
|
||||||
|
* Sent when a user sends a direct message to the bot
|
||||||
|
*/
|
||||||
|
export const messageImEventSchema = z.object({
|
||||||
|
type: z.literal('message'),
|
||||||
|
channel_type: z.literal('im'),
|
||||||
|
user: z.string(),
|
||||||
|
text: z.string(),
|
||||||
|
ts: z.string(),
|
||||||
|
channel: z.string(),
|
||||||
|
event_ts: z.string(),
|
||||||
|
thread_ts: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type MessageImEvent = z.infer<typeof messageImEventSchema>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event Callback Envelope
|
* Event Callback Envelope
|
||||||
* The wrapper for all event_callback type events
|
* The wrapper for all event_callback type events
|
||||||
|
@ -47,7 +64,7 @@ export const eventCallbackSchema = z.object({
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
team_id: z.string(),
|
team_id: z.string(),
|
||||||
api_app_id: z.string(),
|
api_app_id: z.string(),
|
||||||
event: appMentionEventSchema,
|
event: z.union([appMentionEventSchema, messageImEventSchema]),
|
||||||
type: z.literal('event_callback'),
|
type: z.literal('event_callback'),
|
||||||
event_id: z.string(),
|
event_id: z.string(),
|
||||||
event_time: z.number(),
|
event_time: z.number(),
|
||||||
|
@ -88,3 +105,11 @@ export function isUrlVerification(payload: SlackWebhookPayload): payload is UrlV
|
||||||
export function isEventCallback(payload: SlackWebhookPayload): payload is EventCallback {
|
export function isEventCallback(payload: SlackWebhookPayload): payload is EventCallback {
|
||||||
return payload.type === 'event_callback';
|
return payload.type === 'event_callback';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAppMentionEvent(event: EventCallback['event']): event is AppMentionEvent {
|
||||||
|
return event.type === 'app_mention';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMessageImEvent(event: EventCallback['event']): event is MessageImEvent {
|
||||||
|
return event.type === 'message' && 'channel_type' in event && event.channel_type === 'im';
|
||||||
|
}
|
||||||
|
|
|
@ -125,7 +125,11 @@
|
||||||
"R2_BUCKET",
|
"R2_BUCKET",
|
||||||
|
|
||||||
"PLAYWRIGHT_START_COMMAND",
|
"PLAYWRIGHT_START_COMMAND",
|
||||||
"DAYTONA_API_KEY"
|
"DAYTONA_API_KEY",
|
||||||
|
|
||||||
|
"SLACK_CLIENT_ID",
|
||||||
|
"SLACK_CLIENT_SECRET",
|
||||||
|
"SLACK_SIGNING_SECRET"
|
||||||
],
|
],
|
||||||
"envMode": "strict"
|
"envMode": "strict"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue