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
@@ -0,0 +1,21 @@
import { createActionWrapper } from '@botpress/common'
import { wrapAsyncFnWithTryCatch, SlackClient } from '../slack-api'
import * as bp from '.botpress'
export const wrapActionAndInjectSlackClient: typeof _wrapActionAndInjectTools = (meta, actionImpl) =>
_wrapActionAndInjectTools(meta, (props) =>
wrapAsyncFnWithTryCatch(() => {
props.logger.forBot().debug(`Running action "${meta.actionName}" [bot id: ${props.ctx.botId}]`)
return actionImpl(props as Parameters<typeof actionImpl>[0], props.input)
}, `Action Error: ${meta.errorMessage}`)()
)
const _wrapActionAndInjectTools = createActionWrapper<bp.IntegrationProps>()({
toolFactories: {
slackClient: (props) => SlackClient.createFromStates(props),
},
extraMetadata: {} as {
errorMessage: string
},
})
@@ -0,0 +1,25 @@
import * as sdk from '@botpress/sdk'
import { wrapActionAndInjectSlackClient } from 'src/actions/action-wrapper'
import { retrieveChannelAndMessageTs } from './utils/message-utils'
export const addReaction = wrapActionAndInjectSlackClient(
{ actionName: 'addReaction', errorMessage: 'Failed to add reaction' },
async ({ client, logger, slackClient }, { messageId, name }) => {
if (!messageId) {
throw new sdk.RuntimeError('Missing Botpress message ID')
}
const { channel, ts } = await retrieveChannelAndMessageTs({
client,
messageId,
})
logger.forBot().debug('Sending reaction to Slack')
await slackClient.addReactionToMessage({
channelId: channel,
messageTs: ts,
reactionName: name,
})
}
)
@@ -0,0 +1,55 @@
import Fuse from 'fuse.js'
import { wrapActionAndInjectSlackClient } from 'src/actions/action-wrapper'
import { Target } from '../../definitions/actions'
const fuse = new Fuse<Target>([], {
shouldSort: true,
threshold: 0.3,
minMatchCharLength: 2,
isCaseSensitive: false,
includeMatches: true,
findAllMatches: true,
useExtendedSearch: true,
ignoreLocation: true,
keys: ['displayName'],
})
export const findTarget = wrapActionAndInjectSlackClient(
{ actionName: 'findTarget', errorMessage: 'Failed to find any target' },
async ({ slackClient }, { channel, query }) => {
const targets: (Target & Record<string, unknown>)[] =
channel === 'dm'
? await slackClient
.enumerateAllMembers()
.collect()
.then((members) =>
members.map((member) => ({
// TODO: perform mapping in the slack client directly; we don't want
// to expose raw slack member objects outside of the custom
// slack client
displayName: member.name!,
name: member.profile?.real_name ?? '',
email: member.profile?.email ?? '',
tags: { id: member.id! },
channel: 'dm',
}))
)
: await slackClient
.enumerateAllPublicChannels()
.collect()
.then((channels) =>
channels.map((channel) => ({
displayName: channel.name!,
tags: { id: channel.id! },
channel: 'channel',
}))
)
fuse.setCollection(targets)
const filteredTargets: Target[] = fuse.search<Target>(query).map((x) => x.item)
return {
targets: filteredTargets,
}
}
)
@@ -0,0 +1,18 @@
import { wrapActionAndInjectSlackClient } from 'src/actions/action-wrapper'
export const getChannelsInfo = wrapActionAndInjectSlackClient(
{ actionName: 'getChannelsInfo', errorMessage: 'Failed to get channels info' },
async ({ slackClient }, { includeArchived, includePrivate, includeDm, cursor }) => {
const { channels, nextCursor } = await slackClient.getChannelsInfo({
includeArchived: includeArchived ?? false,
includePrivate: includePrivate ?? false,
includeDm: includeDm ?? false,
cursor,
})
return {
channels,
nextCursor: nextCursor ?? '',
}
}
)
@@ -0,0 +1,18 @@
import { RuntimeError } from '@botpress/client'
import { wrapActionAndInjectSlackClient } from 'src/actions/action-wrapper'
export const getOrCreateChannelConversation = wrapActionAndInjectSlackClient(
{ actionName: 'getOrCreateChannelConversation', errorMessage: 'Failed to get or create channel conversation' },
async ({ client }, { conversation: { channelId } }) => {
if (!channelId) {
throw new RuntimeError('channelId must be provided')
}
const { conversation } = await client.getOrCreateConversation({
channel: 'channel',
tags: { id: channelId },
})
return { conversationId: conversation.id }
}
)
@@ -0,0 +1,15 @@
import { wrapActionAndInjectSlackClient } from 'src/actions/action-wrapper'
export const getUserProfile = wrapActionAndInjectSlackClient(
{ actionName: 'getUserProfile', errorMessage: 'Failed to retrieve user profile' },
async ({ slackClient }, { userId }) => {
const userProfile = await slackClient.getUserProfile({ userId })
return {
firstName: userProfile?.first_name,
lastName: userProfile?.last_name,
email: userProfile?.email,
displayName: userProfile?.display_name,
}
}
)
+23
View File
@@ -0,0 +1,23 @@
import { addReaction } from './add-reaction'
import { findTarget } from './find-target'
import { getChannelsInfo } from './get-channels-info'
import { getOrCreateChannelConversation } from './get-or-create-channel-conversation'
import { getUserProfile } from './get-user-profile'
import { retrieveMessage } from './retrieve-message'
import { syncMembers } from './sync-members'
import { startTypingIndicator, stopTypingIndicator } from './typing-indicator'
import { updateChannelTopic } from './update-channel-topic'
import type * as bp from '.botpress'
export default {
addReaction,
findTarget,
getChannelsInfo,
getOrCreateChannelConversation,
retrieveMessage,
syncMembers,
updateChannelTopic,
startTypingIndicator,
stopTypingIndicator,
getUserProfile,
} satisfies bp.IntegrationProps['actions']
@@ -0,0 +1,20 @@
import { wrapActionAndInjectSlackClient } from 'src/actions/action-wrapper'
export const retrieveMessage = wrapActionAndInjectSlackClient(
{ actionName: 'retrieveMessage', errorMessage: 'Failed to retrieve message' },
async ({ logger, slackClient }, { ts, channel }) => {
const message = await slackClient.retrieveMessage({ channel, messageTs: ts })
if (!message.type || !message.ts || !message.user || !message.text) {
logger.forBot().error('Message is missing required fields')
throw new Error('Message is missing required fields')
}
return {
type: message.type,
ts: message.ts,
user: message.user,
text: message.text,
}
}
)
@@ -0,0 +1,99 @@
import { User } from '@botpress/client'
import { createOrUpdateUser } from '@botpress/common'
import { isApiError } from '@botpress/sdk'
import { Member } from '@slack/web-api/dist/response/UsersListResponse'
import { wrapActionAndInjectSlackClient } from 'src/actions/action-wrapper'
import * as bp from '.botpress'
export const syncMembers = wrapActionAndInjectSlackClient(
{ actionName: 'syncMembers', errorMessage: 'Failed to sync Slack users' },
async ({ logger, ctx, client, slackClient }, {}) => {
let { usersLastSyncTs } = await _getSyncState(client, ctx)
logger.forBot().debug(`Last sync timestamp for Slack users: ${usersLastSyncTs}`)
const unsortedMembers: Member[] = await slackClient.enumerateAllMembers().collect()
const recentlyUpdatedMembers = unsortedMembers
.filter((x) => (x.updated ?? 0) > (usersLastSyncTs ?? -1))
.sort((a, b) => (a.updated ?? 0) - (b.updated ?? 0))
logger
.forBot()
.debug(`Found ${recentlyUpdatedMembers.length}/${unsortedMembers.length} members to sync in Slack workspace`)
for (const slackMember of recentlyUpdatedMembers) {
logger
.forBot()
.debug('Syncing Slack user to Botpress...', { user: slackMember.name, updated: slackMember.updated })
try {
const user = await syncSlackUserToBotpressUser(slackMember, client)
logger.forBot().debug(`Synced Slack user ${user.name} (${user.id})`)
usersLastSyncTs = Math.max(usersLastSyncTs ?? 0, slackMember.updated ?? 0)
await _saveSyncState(client, ctx, { usersLastSyncTs })
} catch (err) {
logger.forBot().error(`Failed to sync Slack user ${slackMember.name}`, err)
}
}
logger.forBot().debug(`Synced ${recentlyUpdatedMembers.length} Slack users to Botpress`)
return { syncedCount: recentlyUpdatedMembers.length }
}
)
const syncSlackUserToBotpressUser = async (member: Member, botpressClient: bp.Client): Promise<User> => {
try {
const { user } = await createOrUpdateUser({
client: botpressClient,
name: member.name,
pictureUrl: member.profile?.image_512,
tags: {
id: member.id,
avatar_hash: member.profile?.avatar_hash,
display_name: member.profile?.display_name,
display_name_normalized: member.profile?.display_name_normalized,
email: member.profile?.email,
image_24: member.profile?.image_24,
image_48: member.profile?.image_48,
image_192: member.profile?.image_192,
image_512: member.profile?.image_512,
image_1024: member.profile?.image_1024,
is_admin: member.is_admin?.toString(),
is_bot: member.is_bot?.toString(),
phone: member.profile?.phone,
real_name: member.profile?.real_name,
real_name_normalized: member.profile?.real_name_normalized,
status_emoji: member.profile?.status_emoji,
status_text: member.profile?.status_text,
team: member.team_id,
title: member.profile?.title,
tz: member.tz,
},
discriminateByTags: ['id'],
})
return user
} catch (err) {
if (isApiError(err) && err.type === 'RateLimited') {
await new Promise((resolve) => setTimeout(resolve, 10 * 1000))
return syncSlackUserToBotpressUser(member, botpressClient)
}
throw err
}
}
type SyncState = { usersLastSyncTs?: number }
const _getSyncState = async (client: bp.Client, ctx: bp.Context): Promise<SyncState> => {
const {
state: { payload },
} = await client.getState({ type: 'integration', name: 'sync', id: ctx.integrationId }).catch(() => ({
state: { payload: {} as any },
}))
return payload as SyncState
}
const _saveSyncState = async (client: bp.Client, ctx: bp.Context, state: SyncState) => {
await client.setState({ type: 'integration', name: 'sync', id: ctx.integrationId, payload: state })
}
@@ -0,0 +1,57 @@
import { wrapActionAndInjectSlackClient } from './action-wrapper'
import { retrieveChannelAndMessageTs } from './utils/message-utils'
const TYPING_INDICATOR_EMOJI = 'eyes'
export const startTypingIndicator = wrapActionAndInjectSlackClient(
{ actionName: 'startTypingIndicator', errorMessage: 'Failed to start typing indicator' },
async ({ ctx, client, slackClient, logger }, { conversationId, messageId }) => {
const { channel, ts } = await retrieveChannelAndMessageTs({
client,
messageId,
})
if (ctx.configuration.typingIndicatorEmoji) {
logger
.forBot()
.debug(
`Adding reaction "${TYPING_INDICATOR_EMOJI}" to message ${messageId} in conversation ${conversationId} (typing indicator)`
)
await slackClient.addReactionToMessage({
channelId: channel,
messageTs: ts,
reactionName: TYPING_INDICATOR_EMOJI,
})
}
logger.forBot().debug(`Marking message ${messageId} as seen in conversation ${conversationId} (typing indicator)`)
await slackClient.markMessageAsSeen({
channelId: channel,
messageTs: ts,
})
}
)
export const stopTypingIndicator = wrapActionAndInjectSlackClient(
{ actionName: 'stopTypingIndicator', errorMessage: 'Failed to stop typing indicator' },
async ({ client, slackClient, logger, ctx }, { messageId, conversationId }) => {
const { channel, ts } = await retrieveChannelAndMessageTs({
client,
messageId,
})
if (ctx.configuration.typingIndicatorEmoji) {
logger
.forBot()
.debug(
`Removing reaction "${TYPING_INDICATOR_EMOJI}" from message ${messageId} in conversation ${conversationId} (typing indicator)`
)
await slackClient.removeReactionFromMessage({
channelId: channel,
messageTs: ts,
reactionName: TYPING_INDICATOR_EMOJI,
})
}
}
)
@@ -0,0 +1,8 @@
import { wrapActionAndInjectSlackClient } from './action-wrapper'
export const updateChannelTopic = wrapActionAndInjectSlackClient(
{ actionName: 'updateChannelTopic', errorMessage: 'Failed to update channel topic' },
async ({ slackClient }, { channelId, topic }) => {
await slackClient.setConversationTopic({ channelId, topic })
}
)
@@ -0,0 +1,18 @@
import * as sdk from '@botpress/client'
import * as bp from '.botpress'
export const retrieveChannelAndMessageTs = async ({ client, messageId }: { client: bp.Client; messageId: string }) => {
const { message } = await client.getMessage({ id: messageId })
const { conversation } = await client.getConversation({ id: message.conversationId })
const channel = conversation.tags.id
const ts = message.tags.ts
if (!channel) {
throw new sdk.RuntimeError('Channel ID is missing in conversation tags')
}
if (!ts) {
throw new sdk.RuntimeError('Timestamp is missing in message tags')
}
return { channel, ts }
}