Skip to content

Commit 3200588

Browse files
feat(slack): add manifest copying
1 parent 6fd1767 commit 3200588

7 files changed

Lines changed: 502 additions & 18 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export { ScheduleInfo } from './schedule-info/schedule-info'
2222
export { SelectorInput, type SelectorOverrides } from './selector-input/selector-input'
2323
export { ShortInput } from './short-input/short-input'
2424
export { SkillInput } from './skill-input/skill-input'
25+
export { SlackManifestGenerator } from './slack-manifest-generator/slack-manifest-generator'
2526
export { SliderInput } from './slider-input/slider-input'
2627
export { SortBuilder } from './sort-builder/sort-builder'
2728
export { InputFormat } from './starter/input-format'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/**
2+
* Slack app capabilities that can be toggled on in the manifest generator.
3+
*
4+
* @remarks
5+
* Each capability maps to a set of bot OAuth scopes and bot events that must
6+
* be declared in the Slack app manifest for the capability to work.
7+
* See https://api.slack.com/reference/manifests for the manifest schema.
8+
*/
9+
10+
export type SlackCapabilityGroup = 'trigger' | 'action'
11+
12+
export interface SlackCapability {
13+
id: string
14+
label: string
15+
description: string
16+
group: SlackCapabilityGroup
17+
scopes: readonly string[]
18+
events: readonly string[]
19+
defaultEnabled: boolean
20+
}
21+
22+
export const SLACK_CAPABILITIES: readonly SlackCapability[] = [
23+
{
24+
id: 'mention',
25+
label: '@mention',
26+
description: 'Trigger the workflow when someone @-mentions your bot.',
27+
group: 'trigger',
28+
scopes: ['app_mentions:read'],
29+
events: ['app_mention'],
30+
defaultEnabled: true,
31+
},
32+
{
33+
id: 'dm',
34+
label: 'Direct message',
35+
description: 'Trigger the workflow when a user sends your bot a 1:1 direct message.',
36+
group: 'trigger',
37+
scopes: ['im:history', 'im:read'],
38+
events: ['message.im'],
39+
defaultEnabled: true,
40+
},
41+
{
42+
id: 'group_dm',
43+
label: 'Group direct message',
44+
description: 'Trigger on messages in multi-person DMs your bot is part of.',
45+
group: 'trigger',
46+
scopes: ['mpim:history', 'mpim:read'],
47+
events: ['message.mpim'],
48+
defaultEnabled: true,
49+
},
50+
{
51+
id: 'public_channel',
52+
label: 'Public channel message',
53+
description: 'Trigger on messages in public channels your bot has been invited to.',
54+
group: 'trigger',
55+
scopes: ['channels:history', 'channels:read'],
56+
events: ['message.channels'],
57+
defaultEnabled: true,
58+
},
59+
{
60+
id: 'private_channel',
61+
label: 'Private channel message',
62+
description: 'Trigger on messages in private channels your bot has been invited to.',
63+
group: 'trigger',
64+
scopes: ['groups:history', 'groups:read'],
65+
events: ['message.groups'],
66+
defaultEnabled: true,
67+
},
68+
{
69+
id: 'public_channel_reaction',
70+
label: 'Public channel reaction',
71+
description: 'Trigger when emoji reactions are added or removed in public channels.',
72+
group: 'trigger',
73+
scopes: ['reactions:read'],
74+
events: ['reaction_added', 'reaction_removed'],
75+
defaultEnabled: true,
76+
},
77+
{
78+
id: 'any_reaction',
79+
label: 'Reaction (any channel)',
80+
description: 'Trigger on any emoji reaction your bot can see — public or private.',
81+
group: 'trigger',
82+
scopes: ['reactions:read'],
83+
events: ['reaction_added', 'reaction_removed'],
84+
defaultEnabled: true,
85+
},
86+
{
87+
id: 'send',
88+
label: 'Send messages',
89+
description: 'Let the bot post messages into channels it is a member of.',
90+
group: 'action',
91+
scopes: ['chat:write'],
92+
events: [],
93+
defaultEnabled: true,
94+
},
95+
{
96+
id: 'add_reaction',
97+
label: 'Add reactions',
98+
description: 'Let the bot add emoji reactions to messages.',
99+
group: 'action',
100+
scopes: ['reactions:write'],
101+
events: [],
102+
defaultEnabled: true,
103+
},
104+
{
105+
id: 'read_files',
106+
label: 'Read file attachments',
107+
description: 'Let the bot download file attachments on incoming messages.',
108+
group: 'action',
109+
scopes: ['files:read'],
110+
events: [],
111+
defaultEnabled: true,
112+
},
113+
{
114+
id: 'read_users',
115+
label: 'Look up users',
116+
description: 'Resolve user IDs to names, profiles, and email addresses.',
117+
group: 'action',
118+
scopes: ['users:read', 'users:read.email'],
119+
events: [],
120+
defaultEnabled: true,
121+
},
122+
] as const
123+
124+
const WEBHOOK_URL_PLACEHOLDER = '<deploy workflow to generate webhook URL>'
125+
126+
export interface BuildManifestOptions {
127+
appName: string
128+
webhookUrl: string | null
129+
}
130+
131+
/**
132+
* Builds a Slack app manifest object from a set of enabled capability ids.
133+
*
134+
* @remarks
135+
* - Deduplicates scopes and events across overlapping capabilities.
136+
* - Omits `settings.event_subscriptions` entirely when no events are selected —
137+
* Slack's manifest validator rejects an empty `bot_events` array.
138+
* - When `webhookUrl` is null, embeds a human-readable placeholder so the
139+
* shape is visible before the workflow is deployed.
140+
*/
141+
export function buildSlackManifest(
142+
enabled: ReadonlySet<string>,
143+
{ appName, webhookUrl }: BuildManifestOptions
144+
): Record<string, unknown> {
145+
const active = SLACK_CAPABILITIES.filter((c) => enabled.has(c.id))
146+
const scopes = [...new Set(active.flatMap((c) => c.scopes))].sort()
147+
const events = [...new Set(active.flatMap((c) => c.events))].sort()
148+
const displayName = appName.trim() || 'Sim Workflow Bot'
149+
150+
const manifest: Record<string, unknown> = {
151+
display_information: { name: displayName },
152+
features: {
153+
bot_user: { display_name: displayName, always_online: true },
154+
},
155+
oauth_config: {
156+
scopes: { bot: scopes },
157+
},
158+
settings: {
159+
org_deploy_enabled: false,
160+
socket_mode_enabled: false,
161+
token_rotation_enabled: false,
162+
},
163+
}
164+
165+
if (events.length > 0) {
166+
const settings = manifest.settings as Record<string, unknown>
167+
settings.event_subscriptions = {
168+
request_url: webhookUrl ?? WEBHOOK_URL_PLACEHOLDER,
169+
bot_events: events,
170+
}
171+
}
172+
173+
return manifest
174+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { SlackManifestGenerator } from './slack-manifest-generator'

0 commit comments

Comments
 (0)