chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import * as genenv from './.genenv'
|
||||
import gmail from './bp_modules/gmail'
|
||||
import slack from './bp_modules/slack'
|
||||
|
||||
export default new sdk.BotDefinition({
|
||||
states: {
|
||||
slackConversationId: {
|
||||
type: 'bot',
|
||||
schema: sdk.z.object({
|
||||
conversationId: sdk.z
|
||||
.string()
|
||||
.title('Slack Conversation ID')
|
||||
.describe('The ID of the Slack conversation to send email notifications to')
|
||||
.optional(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
.addIntegration(gmail, {
|
||||
enabled: true,
|
||||
configurationType: 'customApp',
|
||||
configuration: {
|
||||
oauthClientId: genenv.SLACKBOX_GMAIL_OAUTH_CLIENT_ID,
|
||||
oauthClientSecret: genenv.SLACKBOX_GMAIL_OAUTH_CLIENT_SECRET,
|
||||
oauthAuthorizationCode: genenv.SLACKBOX_GMAIL_OAUTH_AUTHORIZATION_CODE,
|
||||
pubsubTopicName: genenv.SLACKBOX_GMAIL_PUBSUB_TOPIC_NAME,
|
||||
pubsubWebhookSharedSecret: genenv.SLACKBOX_GMAIL_PUBSUB_WEBHOOK_SHARED_SECRET,
|
||||
pubsubWebhookServiceAccount: genenv.SLACKBOX_GMAIL_PUBSUB_WEBHOOK_SERVICE_ACCOUNT,
|
||||
},
|
||||
})
|
||||
.addIntegration(slack, {
|
||||
enabled: true,
|
||||
configuration: {
|
||||
typingIndicatorEmoji: false,
|
||||
replyBehaviour: {
|
||||
location: 'channel',
|
||||
onlyOnBotMention: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import rootConfig from '../../eslint.config.mjs'
|
||||
|
||||
export default [
|
||||
...rootConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@bp-bots/slackbox",
|
||||
"scripts": {
|
||||
"postinstall": "genenv -o ./.genenv/index.ts -e SLACKBOX_GMAIL_OAUTH_CLIENT_ID -e SLACKBOX_GMAIL_OAUTH_CLIENT_SECRET -e SLACKBOX_GMAIL_OAUTH_AUTHORIZATION_CODE -e SLACKBOX_GMAIL_PUBSUB_TOPIC_NAME -e SLACKBOX_GMAIL_PUBSUB_WEBHOOK_SHARED_SECRET -e SLACKBOX_GMAIL_PUBSUB_WEBHOOK_SERVICE_ACCOUNT -e SLACKBOX_SLACK_CHANNEL -e SLACKBOX_FALLBACK_SLACK_CHANNEL",
|
||||
"check:type": "tsc --noEmit",
|
||||
"check:bplint": "bp lint",
|
||||
"build": "bp add -y && bp build"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@botpress/client": "workspace:*",
|
||||
"@botpress/sdk": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@botpress/cli": "workspace:*",
|
||||
"@botpress/common": "workspace:*",
|
||||
"@botpress/sdk": "workspace:*",
|
||||
"@botpresshub/gmail": "workspace:*",
|
||||
"@botpresshub/slack": "workspace:*",
|
||||
"@bpinternal/genenv": "0.0.1"
|
||||
},
|
||||
"bpDependencies": {
|
||||
"slack": "../../integrations/slack",
|
||||
"gmail": "../../integrations/gmail"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": { "*": ["./*"] },
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": [".botpress/**/*", "src/**/*", "*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user