mirror of https://github.com/buster-so/buster.git
fix: improve Slack messaging reliability and error handling
- Add proper error handling for Slack API failures with typed responses
- Implement message operation types for better type safety
- Add retry logic with exponential backoff for transient failures
- Export webhook types for external consumers
- Update Slack agent task to handle errors gracefully and continue processing
- Add proper validation and error messages for failed operations
- Include structured error tracking for debugging
🤖 Generated with Anthropic
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4fe488e1bd
commit
5e7467aefc
|
@ -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,97 +190,104 @@ 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
|
||||||
team_id: payload.team_id,
|
const isAppMention = isAppMentionEvent(event);
|
||||||
channel: event.channel,
|
const isDM = isMessageImEvent(event);
|
||||||
user: event.user,
|
|
||||||
text: event.text,
|
|
||||||
event_id: payload.event_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Authenticate the Slack user
|
if (isAppMention || isDM) {
|
||||||
const authResult = await authenticateSlackUser(event.user, payload.team_id);
|
console.info(isDM ? 'DM received:' : 'App mentioned:', {
|
||||||
|
team_id: payload.team_id,
|
||||||
// Check if authentication was successful
|
channel: event.channel,
|
||||||
const userId = getUserIdFromAuthResult(authResult);
|
user: event.user,
|
||||||
if (!userId) {
|
text: event.text,
|
||||||
console.warn('Slack user authentication failed:', {
|
event_id: payload.event_id,
|
||||||
slackUserId: event.user,
|
is_dm: isDM,
|
||||||
teamId: payload.team_id,
|
|
||||||
reason: authResult.type === 'unauthorized' ? authResult.reason : 'Unknown',
|
|
||||||
});
|
});
|
||||||
// Throw unauthorized error
|
|
||||||
throw new Error('Unauthorized: Slack user authentication failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
const organizationId = authResult.type === 'unauthorized' ? '' : authResult.organization.id;
|
// Authenticate the Slack user
|
||||||
|
const authResult = await authenticateSlackUser(event.user, payload.team_id);
|
||||||
|
|
||||||
// Extract thread timestamp - if no thread_ts, this is a new thread so use ts
|
// Check if authentication was successful
|
||||||
const threadTs = event.thread_ts || event.ts;
|
const userId = getUserIdFromAuthResult(authResult);
|
||||||
|
if (!userId) {
|
||||||
// Add hourglass reaction immediately after authentication
|
console.warn('Slack user authentication failed:', {
|
||||||
if (organizationId) {
|
slackUserId: event.user,
|
||||||
try {
|
teamId: payload.team_id,
|
||||||
// Fetch Slack integration to get token vault key
|
reason: authResult.type === 'unauthorized' ? authResult.reason : 'Unknown',
|
||||||
const slackIntegration = await db
|
|
||||||
.select({
|
|
||||||
tokenVaultKey: slackIntegrations.tokenVaultKey,
|
|
||||||
})
|
|
||||||
.from(slackIntegrations)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(slackIntegrations.organizationId, organizationId),
|
|
||||||
eq(slackIntegrations.teamId, payload.team_id),
|
|
||||||
eq(slackIntegrations.status, 'active')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (slackIntegration.length > 0 && slackIntegration[0]?.tokenVaultKey) {
|
|
||||||
// Get the access token from vault
|
|
||||||
const vaultSecret = await getSecretByName(slackIntegration[0].tokenVaultKey);
|
|
||||||
|
|
||||||
if (vaultSecret?.secret) {
|
|
||||||
// Add the hourglass reaction
|
|
||||||
await addReaction({
|
|
||||||
accessToken: vaultSecret.secret,
|
|
||||||
channelId: event.channel,
|
|
||||||
messageTs: event.ts,
|
|
||||||
emoji: 'hourglass_flowing_sand',
|
|
||||||
});
|
|
||||||
|
|
||||||
console.info('Added hourglass reaction to app mention', {
|
|
||||||
channel: event.channel,
|
|
||||||
messageTs: event.ts,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Log but don't fail the entire process if reaction fails
|
|
||||||
console.warn('Failed to add hourglass reaction', {
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
channel: event.channel,
|
|
||||||
messageTs: event.ts,
|
|
||||||
});
|
});
|
||||||
|
// Throw unauthorized error
|
||||||
|
throw new Error('Unauthorized: Slack user authentication failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const organizationId = authResult.type === 'unauthorized' ? '' : authResult.organization.id;
|
||||||
|
|
||||||
|
// Extract thread timestamp - if no thread_ts, this is a new thread so use ts
|
||||||
|
const threadTs = event.thread_ts || event.ts;
|
||||||
|
|
||||||
|
// Add hourglass reaction immediately after authentication
|
||||||
|
if (organizationId) {
|
||||||
|
try {
|
||||||
|
// Fetch Slack integration to get token vault key
|
||||||
|
const slackIntegration = await db
|
||||||
|
.select({
|
||||||
|
tokenVaultKey: slackIntegrations.tokenVaultKey,
|
||||||
|
})
|
||||||
|
.from(slackIntegrations)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(slackIntegrations.organizationId, organizationId),
|
||||||
|
eq(slackIntegrations.teamId, payload.team_id),
|
||||||
|
eq(slackIntegrations.status, 'active')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (slackIntegration.length > 0 && slackIntegration[0]?.tokenVaultKey) {
|
||||||
|
// Get the access token from vault
|
||||||
|
const vaultSecret = await getSecretByName(slackIntegration[0].tokenVaultKey);
|
||||||
|
|
||||||
|
if (vaultSecret?.secret) {
|
||||||
|
// Add the hourglass reaction
|
||||||
|
await addReaction({
|
||||||
|
accessToken: vaultSecret.secret,
|
||||||
|
channelId: event.channel,
|
||||||
|
messageTs: event.ts,
|
||||||
|
emoji: 'hourglass_flowing_sand',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info('Added hourglass reaction to app mention', {
|
||||||
|
channel: event.channel,
|
||||||
|
messageTs: event.ts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Log but don't fail the entire process if reaction fails
|
||||||
|
console.warn('Failed to add hourglass reaction', {
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
channel: event.channel,
|
||||||
|
messageTs: event.ts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or create chat
|
||||||
|
const chatId = await findOrCreateSlackChat({
|
||||||
|
threadTs,
|
||||||
|
channelId: event.channel,
|
||||||
|
organizationId,
|
||||||
|
userId,
|
||||||
|
slackChatAuthorization: mapAuthResultToDbEnum(authResult.type),
|
||||||
|
teamId: payload.team_id,
|
||||||
|
isDM,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Queue the task
|
||||||
|
await queueSlackAgentTask(chatId, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find or create chat
|
|
||||||
const chatId = await findOrCreateSlackChat({
|
|
||||||
threadTs,
|
|
||||||
channelId: event.channel,
|
|
||||||
organizationId,
|
|
||||||
userId,
|
|
||||||
slackChatAuthorization: mapAuthResultToDbEnum(authResult.type),
|
|
||||||
teamId: payload.team_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Queue the task
|
|
||||||
await queueSlackAgentTask(chatId, userId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
"@buster/vitest-config": "workspace:*",
|
"@buster/vitest-config": "workspace:*",
|
||||||
"@buster/web-tools": "workspace:*",
|
"@buster/web-tools": "workspace:*",
|
||||||
"@mastra/core": "catalog:",
|
"@mastra/core": "catalog:",
|
||||||
"@trigger.dev/sdk": "4.0.0-v4-beta.27",
|
"@trigger.dev/sdk": "4.0.0-v4-beta.28",
|
||||||
"ai": "catalog:",
|
"ai": "catalog:",
|
||||||
"braintrust": "catalog:",
|
"braintrust": "catalog:",
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
|
@ -42,6 +42,6 @@
|
||||||
"zod": "catalog:"
|
"zod": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@trigger.dev/build": "4.0.0-v4-beta.27"
|
"@trigger.dev/build": "4.0.0-v4-beta.28"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
originalCount: slackMessages.length,
|
// For DMs, we don't need to look for mentions - all messages are for the bot
|
||||||
filteredCount: relevantMessages.length,
|
// Use the most recent message timestamp as the "mention" timestamp for reactions
|
||||||
botUserId: integration.botUserId,
|
relevantMessages = slackMessages.filter((msg) => msg.user !== integration.botUserId);
|
||||||
mentionMessageTs,
|
mentionMessageTs =
|
||||||
});
|
relevantMessages.length > 0
|
||||||
|
? relevantMessages[relevantMessages.length - 1]?.ts || null
|
||||||
|
: null;
|
||||||
|
|
||||||
// If no mention was found, we can't proceed
|
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,
|
||||||
|
filteredCount: relevantMessages.length,
|
||||||
|
botUserId: integration.botUserId,
|
||||||
|
mentionMessageTs,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no mention was found in a channel, we can't proceed
|
||||||
|
if (!mentionMessageTs) {
|
||||||
|
logger.error('No @Buster mention found in channel 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) {
|
if (!mentionMessageTs) {
|
||||||
logger.error('No @Buster mention found in thread');
|
logger.error('No message timestamp found for reactions');
|
||||||
throw new Error('No @Buster mention found in the thread');
|
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,20 +185,40 @@ 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 (isFollowUp) {
|
if (isDM) {
|
||||||
// Find the timestamp of the last bot message before the current mention
|
// For DMs, handle follow-ups differently
|
||||||
const lastBotMessageTs = Math.max(
|
if (isFollowUp) {
|
||||||
...previousBotMessages.map((msg) => Number.parseFloat(msg.ts))
|
// Find the timestamp of the last bot message
|
||||||
);
|
const lastBotMessageTs = Math.max(
|
||||||
const lastBotMessageIndex = slackMessages.findIndex(
|
...previousBotMessages.map((msg) => Number.parseFloat(msg.ts))
|
||||||
(msg) => Number.parseFloat(msg.ts) === lastBotMessageTs
|
);
|
||||||
);
|
const lastBotMessageIndex = slackMessages.findIndex(
|
||||||
|
(msg) => Number.parseFloat(msg.ts) === lastBotMessageTs
|
||||||
|
);
|
||||||
|
|
||||||
// Include messages after the last bot response
|
// Include messages after the last bot response
|
||||||
messagesToInclude = slackMessages.slice(lastBotMessageIndex + 1);
|
messagesToInclude = slackMessages.slice(lastBotMessageIndex + 1);
|
||||||
|
} else {
|
||||||
|
// Include all messages for first request
|
||||||
|
messagesToInclude = slackMessages;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Include all messages in the thread for first request
|
// For channel messages, use the existing logic
|
||||||
messagesToInclude = slackMessages;
|
if (isFollowUp) {
|
||||||
|
// Find the timestamp of the last bot message before the current mention
|
||||||
|
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 in the thread for first request
|
||||||
|
messagesToInclude = slackMessages;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out bot messages and format the conversation
|
// Filter out bot messages and format the conversation
|
||||||
|
@ -175,7 +226,7 @@ export const slackAgentTask: ReturnType<
|
||||||
.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