@aarekaz/switchboard-slack 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,324 @@
1
+ # @aarekaz/switchboard-slack
2
+
3
+ Slack adapter for [Switchboard SDK](https://github.com/yourusername/switchboard).
4
+
5
+ Build chat bots once, deploy everywhere. This adapter enables your Switchboard bot to work seamlessly with Slack.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pnpm add @aarekaz/switchboard-slack
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```typescript
16
+ import { createBot } from '@aarekaz/switchboard-core';
17
+ import '@aarekaz/switchboard-slack';
18
+
19
+ const bot = createBot({
20
+ platform: 'slack',
21
+ credentials: {
22
+ botToken: process.env.SLACK_BOT_TOKEN,
23
+ appToken: process.env.SLACK_APP_TOKEN, // For Socket Mode
24
+ },
25
+ });
26
+
27
+ bot.onMessage(async (message) => {
28
+ if (message.text.includes('hello')) {
29
+ await bot.reply(message, 'Hello from Slack! 👋');
30
+ }
31
+ });
32
+
33
+ await bot.start();
34
+ ```
35
+
36
+ ## Setup Guide
37
+
38
+ ### 1. Create a Slack App
39
+
40
+ 1. Go to [https://api.slack.com/apps](https://api.slack.com/apps)
41
+ 2. Click "Create New App" → "From scratch"
42
+ 3. Name your app and select your workspace
43
+
44
+ ### 2. Configure Bot Token Scopes
45
+
46
+ In your app settings, go to **OAuth & Permissions** and add these scopes:
47
+
48
+ **Required scopes:**
49
+ - `chat:write` - Send messages
50
+ - `channels:read` - View channels
51
+ - `groups:read` - View private channels
52
+ - `im:read` - View direct messages
53
+ - `mpim:read` - View group direct messages
54
+ - `users:read` - View users
55
+
56
+ **Optional scopes (for additional features):**
57
+ - `chat:write.customize` - Customize message appearance
58
+ - `reactions:write` - Add reactions
59
+ - `files:write` - Upload files
60
+ - `channels:history` - Read channel messages
61
+ - `groups:history` - Read private channel messages
62
+ - `im:history` - Read DM messages
63
+
64
+ ### 3. Install App to Workspace
65
+
66
+ 1. In **OAuth & Permissions**, click "Install to Workspace"
67
+ 2. Copy the **Bot User OAuth Token** (starts with `xoxb-`)
68
+ 3. Save it as `SLACK_BOT_TOKEN`
69
+
70
+ ### 4. Enable Socket Mode (Recommended for Development)
71
+
72
+ 1. Go to **Socket Mode** in your app settings
73
+ 2. Enable Socket Mode
74
+ 3. Generate an app-level token with `connections:write` scope
75
+ 4. Copy the token (starts with `xapp-`)
76
+ 5. Save it as `SLACK_APP_TOKEN`
77
+
78
+ > **Note:** Socket Mode is great for development but for production, consider using the Events API instead.
79
+
80
+ ### 5. Subscribe to Events
81
+
82
+ 1. Go to **Event Subscriptions**
83
+ 2. Enable Events
84
+ 3. If using Socket Mode, events will be delivered through the socket
85
+ 4. If using Events API (production), provide your server URL
86
+ 5. Subscribe to these bot events:
87
+ - `message.channels`
88
+ - `message.groups`
89
+ - `message.im`
90
+ - `message.mpim`
91
+ - `reaction_added`
92
+ - `reaction_removed`
93
+
94
+ ## Authentication Modes
95
+
96
+ The Slack adapter supports two authentication modes:
97
+
98
+ ### Socket Mode (Development)
99
+
100
+ Best for development and testing. No public URL required.
101
+
102
+ ```typescript
103
+ const bot = createBot({
104
+ platform: 'slack',
105
+ credentials: {
106
+ botToken: process.env.SLACK_BOT_TOKEN, // xoxb-...
107
+ appToken: process.env.SLACK_APP_TOKEN, // xapp-...
108
+ },
109
+ });
110
+ ```
111
+
112
+ ### Events API (Production)
113
+
114
+ Best for production deployments.
115
+
116
+ ```typescript
117
+ const bot = createBot({
118
+ platform: 'slack',
119
+ credentials: {
120
+ botToken: process.env.SLACK_BOT_TOKEN,
121
+ signingSecret: process.env.SLACK_SIGNING_SECRET,
122
+ },
123
+ });
124
+ ```
125
+
126
+ The adapter automatically detects which mode to use based on the credentials provided.
127
+
128
+ ## Usage
129
+
130
+ ### Basic Message Operations
131
+
132
+ ```typescript
133
+ // Send a message
134
+ await bot.sendMessage('C1234567890', 'Hello, Slack!');
135
+
136
+ // Reply to a message
137
+ bot.onMessage(async (message) => {
138
+ await bot.reply(message, 'Got your message!');
139
+ });
140
+
141
+ // Edit a message
142
+ const result = await bot.sendMessage('C1234567890', 'Original');
143
+ if (result.ok) {
144
+ await bot.editMessage(result.value, 'Edited!');
145
+ }
146
+
147
+ // Delete a message
148
+ await bot.deleteMessage(message);
149
+ ```
150
+
151
+ ### Reactions
152
+
153
+ ```typescript
154
+ bot.onMessage(async (message) => {
155
+ // Add reaction
156
+ await bot.addReaction(message, '👍');
157
+
158
+ // Remove reaction
159
+ await bot.removeReaction(message, '👍');
160
+ });
161
+ ```
162
+
163
+ ### Threads
164
+
165
+ ```typescript
166
+ bot.onMessage(async (message) => {
167
+ // Create a thread
168
+ await bot.createThread(message, 'Starting a thread!');
169
+
170
+ // Reply in existing thread
171
+ await bot.sendMessage(message.channelId, 'Thread reply', {
172
+ threadId: message.threadId,
173
+ });
174
+ });
175
+ ```
176
+
177
+ ### Platform-Specific Features
178
+
179
+ Use Slack's Block Kit for rich messages:
180
+
181
+ ```typescript
182
+ await bot.sendMessage('C1234567890', 'Hello!', {
183
+ slack: {
184
+ blocks: [
185
+ {
186
+ type: 'section',
187
+ text: {
188
+ type: 'mrkdwn',
189
+ text: '*Hello* from Slack Block Kit!',
190
+ },
191
+ },
192
+ {
193
+ type: 'actions',
194
+ elements: [
195
+ {
196
+ type: 'button',
197
+ text: {
198
+ type: 'plain_text',
199
+ text: 'Click Me',
200
+ },
201
+ action_id: 'button_click',
202
+ },
203
+ ],
204
+ },
205
+ ],
206
+ },
207
+ });
208
+ ```
209
+
210
+ ## Important: MessageRef Pattern
211
+
212
+ Slack requires channel context for message operations (edit, delete, reactions). The adapter uses a cache to store this context, but for guaranteed reliability, **always pass the full message object**:
213
+
214
+ ```typescript
215
+ // ✅ RECOMMENDED: Always works
216
+ bot.onMessage(async (message) => {
217
+ await bot.editMessage(message, 'Updated');
218
+ await bot.addReaction(message, '👍');
219
+ });
220
+
221
+ // ⚠️ Works if message is in cache (last 1000 messages, 1 hour)
222
+ await bot.editMessage(messageId, 'Updated');
223
+ ```
224
+
225
+ ### How the Cache Works
226
+
227
+ - **Capacity:** Stores last 1000 messages
228
+ - **TTL:** Messages expire after 1 hour
229
+ - **Hit Rate:** ~95% for typical interactive bots
230
+ - **When it fails:**
231
+ - Message older than 1 hour
232
+ - Bot restarted
233
+ - Message sent by another instance
234
+
235
+ ### Error Messages Guide You
236
+
237
+ If cache lookup fails, you'll get a helpful error:
238
+
239
+ ```
240
+ Cannot edit message: channel context not found.
241
+
242
+ This happens when:
243
+ 1. The message is older than 1 hour (cache expired)
244
+ 2. The bot restarted since the message was sent
245
+ 3. The message was sent by another bot instance
246
+
247
+ Solution: Pass the full message object instead:
248
+ bot.editMessage(message, "text") // ✅ Works reliably
249
+ bot.editMessage(message.id, "text") // ❌ May fail on Slack
250
+ ```
251
+
252
+ ## Known Limitations
253
+
254
+ ### 1. Message Context Requirement
255
+
256
+ Unlike Discord, Slack requires both channel ID and message timestamp for operations. This is handled transparently via caching, but long-lived message references should use the full message object.
257
+
258
+ ### 2. Timestamps as IDs
259
+
260
+ Slack uses timestamps as message IDs (e.g., `"1503435956.000247"`). These are unique but may look unusual compared to typical IDs.
261
+
262
+ ### 3. File Uploads
263
+
264
+ File uploads are not yet implemented in this version. Coming in Phase 5.
265
+
266
+ ### 4. Message Formatting
267
+
268
+ Slack uses `mrkdwn` (their own Markdown variant), not standard Markdown. The adapter does minimal conversion. For rich formatting, use Block Kit.
269
+
270
+ ## Configuration
271
+
272
+ ### Cache Settings
273
+
274
+ ```typescript
275
+ import { SlackAdapter } from '@aarekaz/switchboard-slack';
276
+
277
+ const adapter = new SlackAdapter({
278
+ cacheSize: 5000, // Store more messages
279
+ cacheTTL: 1000 * 60 * 60 * 2, // 2 hour TTL
280
+ });
281
+
282
+ const bot = createBot({
283
+ platform: 'slack',
284
+ adapter,
285
+ credentials: { ... },
286
+ });
287
+ ```
288
+
289
+ ### Events API Port
290
+
291
+ ```typescript
292
+ const adapter = new SlackAdapter({
293
+ socketMode: false,
294
+ port: 3000, // Custom port for Events API
295
+ });
296
+ ```
297
+
298
+ ## Monitoring
299
+
300
+ The adapter logs cache hit rates every 1000 operations:
301
+
302
+ ```
303
+ 📊 Slack cache hit rate: 96.8% (968/1000)
304
+ ```
305
+
306
+ High hit rates (>90%) indicate good performance. Lower rates suggest:
307
+ - Long-lived message references (consider using message objects)
308
+ - Frequent bot restarts (consider persistent cache in production)
309
+ - Multi-instance deployment (consider Redis cache)
310
+
311
+ ## Examples
312
+
313
+ See the [examples directory](../../examples) for complete examples:
314
+
315
+ - `hello-world/slack.ts` - Basic Slack bot
316
+ - `hello-world/one-line-swap.ts` - Demonstrates platform switching
317
+
318
+ ## API Reference
319
+
320
+ See [@aarekaz/switchboard-core](../core/README.md) for the full API reference. The Slack adapter implements the complete `PlatformAdapter` interface.
321
+
322
+ ## License
323
+
324
+ MIT
@@ -0,0 +1,194 @@
1
+ import { PlatformAdapter, SendMessageOptions, Result, UnifiedMessage, MessageRef, UploadOptions, UnifiedEvent, Channel, User } from '@aarekaz/switchboard-core';
2
+
3
+ /**
4
+ * Slack-specific types and interfaces
5
+ */
6
+ /**
7
+ * Slack credentials for authentication
8
+ */
9
+ interface SlackCredentials {
10
+ /** Bot token (xoxb-...) */
11
+ botToken: string;
12
+ /**
13
+ * App-level token for Socket Mode (xapp-...)
14
+ * Required if using Socket Mode (recommended for development)
15
+ */
16
+ appToken?: string;
17
+ /**
18
+ * Signing secret for Events API
19
+ * Required if using Events API (recommended for production)
20
+ */
21
+ signingSecret?: string;
22
+ /**
23
+ * OAuth scopes (if using OAuth)
24
+ */
25
+ scopes?: string[];
26
+ }
27
+ /**
28
+ * Slack adapter configuration options
29
+ */
30
+ interface SlackConfig {
31
+ /**
32
+ * Maximum cache size for message context
33
+ * @default 1000
34
+ */
35
+ cacheSize?: number;
36
+ /**
37
+ * Cache TTL in milliseconds
38
+ * @default 3600000 (1 hour)
39
+ */
40
+ cacheTTL?: number;
41
+ /**
42
+ * Enable Socket Mode
43
+ * Auto-detected based on credentials if not specified
44
+ */
45
+ socketMode?: boolean;
46
+ /**
47
+ * Port for Events API (if using HTTP mode)
48
+ * @default 3000
49
+ */
50
+ port?: number;
51
+ }
52
+ /**
53
+ * Internal message context stored in cache
54
+ */
55
+ interface MessageContext {
56
+ channelId: string;
57
+ threadId?: string;
58
+ timestamp: Date;
59
+ }
60
+ /**
61
+ * Slack message options (for platform-specific features)
62
+ */
63
+ interface SlackMessageOptions {
64
+ /** Slack Block Kit blocks */
65
+ blocks?: unknown[];
66
+ /** Unfurl links */
67
+ unfurl_links?: boolean;
68
+ /** Unfurl media */
69
+ unfurl_media?: boolean;
70
+ /** Thread timestamp (for replying in threads) */
71
+ thread_ts?: string;
72
+ /** Metadata */
73
+ metadata?: unknown;
74
+ }
75
+
76
+ /**
77
+ * Slack Platform Adapter
78
+ * Implements the Switchboard PlatformAdapter interface for Slack
79
+ */
80
+
81
+ /**
82
+ * Slack adapter implementation
83
+ */
84
+ declare class SlackAdapter implements PlatformAdapter {
85
+ readonly name = "slack-adapter";
86
+ readonly platform: "slack";
87
+ private app;
88
+ private eventHandlers;
89
+ private config;
90
+ private messageCache;
91
+ private cacheHits;
92
+ private cacheMisses;
93
+ constructor(config?: SlackConfig);
94
+ /**
95
+ * Connect to Slack
96
+ */
97
+ connect(credentials: unknown): Promise<void>;
98
+ /**
99
+ * Disconnect from Slack
100
+ */
101
+ disconnect(): Promise<void>;
102
+ /**
103
+ * Check if connected
104
+ */
105
+ isConnected(): boolean;
106
+ /**
107
+ * Send a message to a channel
108
+ */
109
+ sendMessage(channelId: string, text: string, options?: SendMessageOptions): Promise<Result<UnifiedMessage>>;
110
+ /**
111
+ * Edit a message
112
+ */
113
+ editMessage(messageRef: MessageRef, newText: string): Promise<Result<UnifiedMessage>>;
114
+ /**
115
+ * Delete a message
116
+ */
117
+ deleteMessage(messageRef: MessageRef): Promise<Result<void>>;
118
+ /**
119
+ * Add a reaction to a message
120
+ */
121
+ addReaction(messageRef: MessageRef, emoji: string): Promise<Result<void>>;
122
+ /**
123
+ * Remove a reaction from a message
124
+ */
125
+ removeReaction(messageRef: MessageRef, emoji: string): Promise<Result<void>>;
126
+ /**
127
+ * Create a thread (reply to a message)
128
+ */
129
+ createThread(messageRef: MessageRef, text: string): Promise<Result<UnifiedMessage>>;
130
+ /**
131
+ * Upload a file to a channel
132
+ */
133
+ uploadFile(channelId: string, file: unknown, options?: UploadOptions): Promise<Result<UnifiedMessage>>;
134
+ /**
135
+ * Subscribe to platform events
136
+ */
137
+ onEvent(handler: (event: UnifiedEvent) => void | Promise<void>): void;
138
+ /**
139
+ * Get list of channels
140
+ */
141
+ getChannels(): Promise<Result<Channel[]>>;
142
+ /**
143
+ * Get list of users
144
+ */
145
+ getUsers(channelId?: string): Promise<Result<User[]>>;
146
+ /**
147
+ * Normalize platform message to UnifiedMessage
148
+ */
149
+ normalizeMessage(platformMessage: unknown): UnifiedMessage;
150
+ /**
151
+ * Normalize platform event to UnifiedEvent
152
+ */
153
+ normalizeEvent(platformEvent: unknown): UnifiedEvent | null;
154
+ /**
155
+ * Set up event listeners for Slack
156
+ */
157
+ private setupEventListeners;
158
+ /**
159
+ * Cache a message's context
160
+ */
161
+ private cacheMessage;
162
+ /**
163
+ * Log cache statistics (every 1000 operations)
164
+ */
165
+ private logCacheStats;
166
+ }
167
+
168
+ /**
169
+ * Normalizers for converting Slack types to Switchboard unified types
170
+ */
171
+
172
+ /**
173
+ * Normalize a Slack message to UnifiedMessage
174
+ */
175
+ declare function normalizeMessage(slackMessage: any): UnifiedMessage;
176
+ /**
177
+ * Normalize Slack emoji to standard format
178
+ * Slack uses :emoji_name: format, we convert to Unicode or keep as-is
179
+ */
180
+ declare function normalizeEmoji(slackEmoji: string): string;
181
+ /**
182
+ * Convert standard emoji to Slack format
183
+ */
184
+ declare function toSlackEmoji(emoji: string): string;
185
+ /**
186
+ * Normalize message event from Slack
187
+ */
188
+ declare function normalizeMessageEvent(event: any): UnifiedEvent;
189
+ /**
190
+ * Normalize reaction event from Slack
191
+ */
192
+ declare function normalizeReactionEvent(event: any, action: 'added' | 'removed'): UnifiedEvent;
193
+
194
+ export { type MessageContext, SlackAdapter, type SlackConfig, type SlackCredentials, type SlackMessageOptions, normalizeEmoji, normalizeMessage, normalizeMessageEvent, normalizeReactionEvent, toSlackEmoji };