chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:34:48 +08:00
commit 77bb5bf71f
3762 changed files with 353249 additions and 0 deletions
+41
View File
@@ -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,
},
},
})
+13
View File
@@ -0,0 +1,13 @@
import rootConfig from '../../eslint.config.mjs'
export default [
...rootConfig,
{
languageOptions: {
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: import.meta.dirname,
},
},
},
]
+26
View File
@@ -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"
}
}
+186
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"paths": { "*": ["./*"] },
"outDir": "dist"
},
"include": [".botpress/**/*", "src/**/*", "*.ts"]
}