chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
import { Conversation } from '@botpress/client'
|
||||
import { AnyIncomingMessage } from '@botpress/sdk/dist/bot'
|
||||
import * as genenv from '../.genenv'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
const DEFAULT_SLACK_CHANNEL = genenv.SLACKBOX_SLACK_CHANNEL
|
||||
const FALLBACK_SLACK_CHANNEL = genenv.SLACKBOX_FALLBACK_SLACK_CHANNEL || DEFAULT_SLACK_CHANNEL
|
||||
|
||||
const cachedSlackConversationIds: Record<string, string> = {}
|
||||
|
||||
const bot = new bp.Bot({
|
||||
actions: {},
|
||||
})
|
||||
|
||||
bot.on.message('*', async (props) => {
|
||||
const { conversation, message, client, ctx, logger } = props
|
||||
|
||||
if (!conversation.integration.includes('gmail')) {
|
||||
logger.info('[Slackbox] Not a Gmail message, skipping')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const shouldForward = await _shouldForwardEmail(client, conversation, logger)
|
||||
if (!shouldForward) {
|
||||
logger.info('Email filtered out - no matching Integrations label')
|
||||
return
|
||||
}
|
||||
|
||||
const subject = (conversation.tags['gmail:subject'] || conversation.tags.subject) as string | undefined
|
||||
const targetChannel = _getTargetChannel(subject)
|
||||
const slackConversationId = await _getSlackConversationId(client, logger, targetChannel)
|
||||
|
||||
const notificationMessage = _mapGmailToSlack(conversation, message)
|
||||
|
||||
await client.createMessage({
|
||||
type: 'text',
|
||||
conversationId: slackConversationId,
|
||||
tags: {},
|
||||
userId: ctx.botId,
|
||||
payload: {
|
||||
text: notificationMessage,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`Failed to send email notification: ${error}`)
|
||||
}
|
||||
})
|
||||
|
||||
const _shouldForwardEmail = async (
|
||||
client: bp.Client,
|
||||
conversation: Conversation,
|
||||
logger: bp.MessageHandlerProps['logger']
|
||||
): Promise<boolean> => {
|
||||
const threadId = conversation.tags['gmail:id']
|
||||
|
||||
if (!threadId) {
|
||||
logger.info('[LabelCheck] No threadId, forwarding email')
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
const labelsResponse = await client.callAction({
|
||||
type: 'gmail:listLabels',
|
||||
input: {},
|
||||
})
|
||||
const labels = labelsResponse.output.labels || []
|
||||
|
||||
const threadResponse = await client.callAction({
|
||||
type: 'gmail:getThread',
|
||||
input: { id: threadId },
|
||||
})
|
||||
const messages = threadResponse.output.messages || []
|
||||
|
||||
const allLabelIds = new Set<string>()
|
||||
messages.forEach((msg) => {
|
||||
msg.labelIds?.forEach((labelId) => allLabelIds.add(labelId))
|
||||
})
|
||||
|
||||
if (allLabelIds.size === 0) {
|
||||
logger.info('[LabelCheck] No labels on thread, forwarding email')
|
||||
return true
|
||||
}
|
||||
|
||||
const labelsById = new Map<string, { type?: string; name?: string }>()
|
||||
labels.forEach((label) => {
|
||||
if (label.id) {
|
||||
labelsById.set(label.id, { type: label.type || undefined, name: label.name || undefined })
|
||||
}
|
||||
})
|
||||
|
||||
const userLabels = [...allLabelIds].filter((labelId) => {
|
||||
const label = labelsById.get(labelId)
|
||||
return label?.type === 'user'
|
||||
})
|
||||
|
||||
if (userLabels.length === 0) {
|
||||
logger.info('[LabelCheck] No user labels, forwarding email')
|
||||
return true
|
||||
}
|
||||
|
||||
const hasIntegrationLabel = userLabels.some((labelId) => {
|
||||
const label = labelsById.get(labelId)
|
||||
return label?.name === 'Integrations' || label?.name?.startsWith('Integrations/')
|
||||
})
|
||||
|
||||
return hasIntegrationLabel
|
||||
} catch (error) {
|
||||
logger.error(`[LabelCheck] Error checking email labels: ${error}`)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const _resolveSlackChannelId = async (client: bp.Client, channelName: string): Promise<string | undefined> => {
|
||||
const { output } = await client.callAction({
|
||||
type: 'slack:findTarget',
|
||||
input: { query: channelName, channel: 'channel' },
|
||||
})
|
||||
const exact = output.targets.find((t) => t.displayName === channelName)
|
||||
return (exact ?? output.targets[0])?.tags.id
|
||||
}
|
||||
|
||||
const _getSlackConversationId = async (
|
||||
client: bp.Client,
|
||||
logger: bp.MessageHandlerProps['logger'],
|
||||
channelName: string
|
||||
): Promise<string> => {
|
||||
if (cachedSlackConversationIds[channelName]) {
|
||||
return cachedSlackConversationIds[channelName]
|
||||
}
|
||||
|
||||
logger.info(`Fetching Slack conversation ID for channel: ${channelName}`)
|
||||
const maxRetries = 3
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
const channelId = await _resolveSlackChannelId(client, channelName)
|
||||
if (!channelId) {
|
||||
throw new Error(`Could not find Slack channel '${channelName}'`)
|
||||
}
|
||||
const response = await client.callAction({
|
||||
type: 'slack:getOrCreateChannelConversation',
|
||||
input: {
|
||||
conversation: {
|
||||
channelId,
|
||||
},
|
||||
},
|
||||
})
|
||||
cachedSlackConversationIds[channelName] = response.output.conversationId
|
||||
return cachedSlackConversationIds[channelName] as string
|
||||
} catch (err) {
|
||||
logger.warn(`Attempt ${attempt}/${maxRetries} failed: ${err}`)
|
||||
if (attempt === maxRetries) {
|
||||
throw err
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Failed to get Slack conversation ID after retries')
|
||||
}
|
||||
|
||||
const _getTargetChannel = (subject: string | undefined): string => {
|
||||
if (subject?.toLowerCase().includes('test')) {
|
||||
return FALLBACK_SLACK_CHANNEL
|
||||
}
|
||||
return DEFAULT_SLACK_CHANNEL
|
||||
}
|
||||
|
||||
const _mapGmailToSlack = (conversation: Conversation, message: AnyIncomingMessage<bp.TBot>) => {
|
||||
const subject = (conversation.tags['gmail:subject'] || conversation.tags.subject) as string | undefined
|
||||
const fromEmail = (conversation.tags['gmail:email'] || conversation.tags.email) as string | undefined
|
||||
const messageText =
|
||||
message.type === 'text' ? (message.payload as { text?: string }).text || 'No content' : 'New email received'
|
||||
|
||||
const preview = messageText.length > 200 ? messageText.substring(0, 200) + '...' : messageText
|
||||
const notificationMessage =
|
||||
'📦 *New email received!*\n\n' +
|
||||
`*Subject:* ${subject || '(No subject)'}\n` +
|
||||
`*From:* ${fromEmail || 'Unknown'}\n` +
|
||||
`*Body:*\n${preview || 'Preview unavailable'}`
|
||||
|
||||
return notificationMessage
|
||||
}
|
||||
|
||||
export default bot
|
||||
Reference in New Issue
Block a user