@chat-adapter/slack 4.18.0 → 4.20.0
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 +324 -9
- package/dist/index.d.ts +4 -1
- package/dist/index.js +150 -35
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@chat-adapter/slack)
|
|
4
4
|
[](https://www.npmjs.com/package/@chat-adapter/slack)
|
|
5
5
|
|
|
6
|
-
Slack adapter for [Chat SDK](https://chat-sdk.dev
|
|
6
|
+
Slack adapter for [Chat SDK](https://chat-sdk.dev). Configure single-workspace or multi-workspace OAuth deployments.
|
|
7
7
|
|
|
8
8
|
## Installation
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
|
|
11
|
+
pnpm add @chat-adapter/slack
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Single-workspace mode
|
|
15
|
+
|
|
16
|
+
For bots deployed to a single Slack workspace. The adapter auto-detects `SLACK_BOT_TOKEN` and `SLACK_SIGNING_SECRET` from environment variables:
|
|
15
17
|
|
|
16
18
|
```typescript
|
|
17
19
|
import { Chat } from "chat";
|
|
@@ -20,10 +22,7 @@ import { createSlackAdapter } from "@chat-adapter/slack";
|
|
|
20
22
|
const bot = new Chat({
|
|
21
23
|
userName: "mybot",
|
|
22
24
|
adapters: {
|
|
23
|
-
slack: createSlackAdapter(
|
|
24
|
-
botToken: process.env.SLACK_BOT_TOKEN!,
|
|
25
|
-
signingSecret: process.env.SLACK_SIGNING_SECRET!,
|
|
26
|
-
}),
|
|
25
|
+
slack: createSlackAdapter(),
|
|
27
26
|
},
|
|
28
27
|
});
|
|
29
28
|
|
|
@@ -32,9 +31,325 @@ bot.onNewMention(async (thread, message) => {
|
|
|
32
31
|
});
|
|
33
32
|
```
|
|
34
33
|
|
|
35
|
-
##
|
|
34
|
+
## Multi-workspace mode
|
|
35
|
+
|
|
36
|
+
For apps installed across multiple Slack workspaces via OAuth, omit `botToken` and provide OAuth credentials instead. The adapter resolves tokens dynamically from your state adapter using the `team_id` from incoming webhooks.
|
|
37
|
+
|
|
38
|
+
When you pass any auth-related config (like `clientId`), the adapter won't fall back to env vars for other auth fields, preventing accidental mixing of auth modes.
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { createSlackAdapter } from "@chat-adapter/slack";
|
|
42
|
+
import { createRedisState } from "@chat-adapter/state-redis";
|
|
43
|
+
|
|
44
|
+
const slackAdapter = createSlackAdapter({
|
|
45
|
+
clientId: process.env.SLACK_CLIENT_ID!,
|
|
46
|
+
clientSecret: process.env.SLACK_CLIENT_SECRET!,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const bot = new Chat({
|
|
50
|
+
userName: "mybot",
|
|
51
|
+
adapters: { slack: slackAdapter },
|
|
52
|
+
state: createRedisState(),
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### OAuth callback
|
|
57
|
+
|
|
58
|
+
The adapter handles the full Slack OAuth V2 exchange. Point your OAuth redirect URL to a route that calls `handleOAuthCallback`:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { slackAdapter } from "@/lib/bot";
|
|
62
|
+
|
|
63
|
+
export async function GET(request: Request) {
|
|
64
|
+
const { teamId } = await slackAdapter.handleOAuthCallback(request);
|
|
65
|
+
return new Response(`Installed for team ${teamId}!`);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Using the adapter outside webhooks
|
|
70
|
+
|
|
71
|
+
During webhook handling, the adapter resolves tokens automatically from `team_id`. Outside that context (e.g. cron jobs or background workers), use `getInstallation` and `withBotToken`:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
const install = await slackAdapter.getInstallation(teamId);
|
|
75
|
+
if (!install) throw new Error("Workspace not installed");
|
|
76
|
+
|
|
77
|
+
await slackAdapter.withBotToken(install.botToken, async () => {
|
|
78
|
+
const thread = bot.thread("slack:C12345:1234567890.123456");
|
|
79
|
+
await thread.post("Hello from a cron job!");
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`withBotToken` uses `AsyncLocalStorage` under the hood, so concurrent calls with different tokens are isolated.
|
|
84
|
+
|
|
85
|
+
### Removing installations
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
await slackAdapter.deleteInstallation(teamId);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Token encryption
|
|
92
|
+
|
|
93
|
+
Pass a base64-encoded 32-byte key as `encryptionKey` to encrypt bot tokens at rest using AES-256-GCM:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
openssl rand -base64 32
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
When `encryptionKey` is set, `setInstallation()` encrypts the token before storing and `getInstallation()` decrypts it transparently.
|
|
100
|
+
|
|
101
|
+
## Slack app setup
|
|
102
|
+
|
|
103
|
+
### 1. Create a Slack app from manifest
|
|
104
|
+
|
|
105
|
+
1. Go to [api.slack.com/apps](https://api.slack.com/apps)
|
|
106
|
+
2. Click **Create New App** then **From an app manifest**
|
|
107
|
+
3. Select your workspace and paste the following manifest:
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
display_information:
|
|
111
|
+
name: My Bot
|
|
112
|
+
description: A bot built with chat-sdk
|
|
113
|
+
|
|
114
|
+
features:
|
|
115
|
+
bot_user:
|
|
116
|
+
display_name: My Bot
|
|
117
|
+
always_online: true
|
|
118
|
+
|
|
119
|
+
oauth_config:
|
|
120
|
+
scopes:
|
|
121
|
+
bot:
|
|
122
|
+
- app_mentions:read
|
|
123
|
+
- channels:history
|
|
124
|
+
- channels:read
|
|
125
|
+
- chat:write
|
|
126
|
+
- groups:history
|
|
127
|
+
- groups:read
|
|
128
|
+
- im:history
|
|
129
|
+
- im:read
|
|
130
|
+
- mpim:history
|
|
131
|
+
- mpim:read
|
|
132
|
+
- reactions:read
|
|
133
|
+
- reactions:write
|
|
134
|
+
- users:read
|
|
135
|
+
|
|
136
|
+
settings:
|
|
137
|
+
event_subscriptions:
|
|
138
|
+
request_url: https://your-domain.com/api/webhooks/slack
|
|
139
|
+
bot_events:
|
|
140
|
+
- app_mention
|
|
141
|
+
- message.channels
|
|
142
|
+
- message.groups
|
|
143
|
+
- message.im
|
|
144
|
+
- message.mpim
|
|
145
|
+
- member_joined_channel
|
|
146
|
+
- assistant_thread_started
|
|
147
|
+
- assistant_thread_context_changed
|
|
148
|
+
interactivity:
|
|
149
|
+
is_enabled: true
|
|
150
|
+
request_url: https://your-domain.com/api/webhooks/slack
|
|
151
|
+
org_deploy_enabled: false
|
|
152
|
+
socket_mode_enabled: false
|
|
153
|
+
token_rotation_enabled: false
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
4. Replace `https://your-domain.com/api/webhooks/slack` with your deployed webhook URL
|
|
157
|
+
5. Click **Create**
|
|
158
|
+
|
|
159
|
+
### 2. Get credentials
|
|
160
|
+
|
|
161
|
+
After creating the app, go to **Basic Information** → **App Credentials** and copy:
|
|
162
|
+
|
|
163
|
+
- **Signing Secret** as `SLACK_SIGNING_SECRET`
|
|
164
|
+
- **Client ID** as `SLACK_CLIENT_ID` (multi-workspace only)
|
|
165
|
+
- **Client Secret** as `SLACK_CLIENT_SECRET` (multi-workspace only)
|
|
166
|
+
|
|
167
|
+
**Single workspace:** Go to **OAuth & Permissions**, click **Install to Workspace**, and copy the **Bot User OAuth Token** (`xoxb-...`) as `SLACK_BOT_TOKEN`.
|
|
168
|
+
|
|
169
|
+
**Multi-workspace:** Enable **Manage Distribution** under **Basic Information** and set up an OAuth redirect URL pointing to your callback route.
|
|
170
|
+
|
|
171
|
+
### 3. Configure slash commands (optional)
|
|
172
|
+
|
|
173
|
+
1. Go to **Slash Commands** in your app settings
|
|
174
|
+
2. Click **Create New Command**
|
|
175
|
+
3. Set **Command** (e.g., `/feedback`)
|
|
176
|
+
4. Set **Request URL** to `https://your-domain.com/api/webhooks/slack`
|
|
177
|
+
5. Add a description and click **Save**
|
|
178
|
+
|
|
179
|
+
## Configuration
|
|
180
|
+
|
|
181
|
+
All options are auto-detected from environment variables when not provided. You can call `createSlackAdapter()` with no arguments if the env vars are set.
|
|
182
|
+
|
|
183
|
+
| Option | Required | Description |
|
|
184
|
+
|--------|----------|-------------|
|
|
185
|
+
| `botToken` | No | Bot token (`xoxb-...`). Auto-detected from `SLACK_BOT_TOKEN` |
|
|
186
|
+
| `signingSecret` | No* | Signing secret for webhook verification. Auto-detected from `SLACK_SIGNING_SECRET` |
|
|
187
|
+
| `clientId` | No | App client ID for multi-workspace OAuth. Auto-detected from `SLACK_CLIENT_ID` |
|
|
188
|
+
| `clientSecret` | No | App client secret for multi-workspace OAuth. Auto-detected from `SLACK_CLIENT_SECRET` |
|
|
189
|
+
| `encryptionKey` | No | AES-256-GCM key for encrypting stored tokens. Auto-detected from `SLACK_ENCRYPTION_KEY` |
|
|
190
|
+
| `installationKeyPrefix` | No | Prefix for the state key used to store workspace installations. Defaults to `slack:installation`. The full key is `{prefix}:{teamId}` |
|
|
191
|
+
| `logger` | No | Logger instance (defaults to `ConsoleLogger("info")`) |
|
|
192
|
+
|
|
193
|
+
*`signingSecret` is required — either via config or `SLACK_SIGNING_SECRET` env var.
|
|
194
|
+
|
|
195
|
+
## Environment variables
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
SLACK_BOT_TOKEN=xoxb-... # Single-workspace only
|
|
199
|
+
SLACK_SIGNING_SECRET=...
|
|
200
|
+
SLACK_CLIENT_ID=... # Multi-workspace only
|
|
201
|
+
SLACK_CLIENT_SECRET=... # Multi-workspace only
|
|
202
|
+
SLACK_ENCRYPTION_KEY=... # Optional, for token encryption
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Features
|
|
206
|
+
|
|
207
|
+
### Messaging
|
|
208
|
+
|
|
209
|
+
| Feature | Supported |
|
|
210
|
+
|---------|-----------|
|
|
211
|
+
| Post message | Yes |
|
|
212
|
+
| Edit message | Yes |
|
|
213
|
+
| Delete message | Yes |
|
|
214
|
+
| File uploads | Yes |
|
|
215
|
+
| Streaming | Native API |
|
|
216
|
+
| Scheduled messages | Yes (native, with cancel) |
|
|
217
|
+
|
|
218
|
+
### Rich content
|
|
219
|
+
|
|
220
|
+
| Feature | Supported |
|
|
221
|
+
|---------|-----------|
|
|
222
|
+
| Card format | Block Kit |
|
|
223
|
+
| Buttons | Yes |
|
|
224
|
+
| Link buttons | Yes |
|
|
225
|
+
| Select menus | Yes |
|
|
226
|
+
| Tables | Block Kit |
|
|
227
|
+
| Fields | Yes |
|
|
228
|
+
| Images in cards | Yes |
|
|
229
|
+
| Modals | Yes |
|
|
230
|
+
|
|
231
|
+
### Conversations
|
|
232
|
+
|
|
233
|
+
| Feature | Supported |
|
|
234
|
+
|---------|-----------|
|
|
235
|
+
| Slash commands | Yes |
|
|
236
|
+
| Mentions | Yes |
|
|
237
|
+
| Add reactions | Yes |
|
|
238
|
+
| Remove reactions | Yes |
|
|
239
|
+
| Typing indicator | Yes |
|
|
240
|
+
| DMs | Yes |
|
|
241
|
+
| Ephemeral messages | Yes (native) |
|
|
242
|
+
|
|
243
|
+
### Message history
|
|
244
|
+
|
|
245
|
+
| Feature | Supported |
|
|
246
|
+
|---------|-----------|
|
|
247
|
+
| Fetch messages | Yes |
|
|
248
|
+
| Fetch single message | Yes |
|
|
249
|
+
| Fetch thread info | Yes |
|
|
250
|
+
| Fetch channel messages | Yes |
|
|
251
|
+
| List threads | Yes |
|
|
252
|
+
| Fetch channel info | Yes |
|
|
253
|
+
| Post channel message | Yes |
|
|
254
|
+
|
|
255
|
+
### Platform-specific
|
|
256
|
+
|
|
257
|
+
| Feature | Supported |
|
|
258
|
+
|---------|-----------|
|
|
259
|
+
| Assistants API | Yes |
|
|
260
|
+
| Member joined channel | Yes |
|
|
261
|
+
| App Home tab | Yes |
|
|
262
|
+
|
|
263
|
+
## Slack Assistants API
|
|
264
|
+
|
|
265
|
+
The adapter supports Slack's [Assistants API](https://api.slack.com/docs/apps/ai) for building AI-powered assistant experiences. This enables suggested prompts, status indicators, and thread titles in assistant DM threads.
|
|
266
|
+
|
|
267
|
+
### Event handlers
|
|
268
|
+
|
|
269
|
+
Register handlers on the `Chat` instance:
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
bot.onAssistantThreadStarted(async (event) => {
|
|
273
|
+
const slack = bot.getAdapter("slack") as SlackAdapter;
|
|
274
|
+
await slack.setSuggestedPrompts(event.channelId, event.threadTs, [
|
|
275
|
+
{ title: "Summarize", message: "Summarize this channel" },
|
|
276
|
+
{ title: "Draft", message: "Help me draft a message" },
|
|
277
|
+
]);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
bot.onAssistantContextChanged(async (event) => {
|
|
281
|
+
// User navigated to a different channel with the assistant panel open
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Adapter methods
|
|
286
|
+
|
|
287
|
+
The `SlackAdapter` exposes these methods for the Assistants API:
|
|
288
|
+
|
|
289
|
+
| Method | Description |
|
|
290
|
+
|--------|-------------|
|
|
291
|
+
| `setSuggestedPrompts(channelId, threadTs, prompts, title?)` | Show prompt suggestions in the thread |
|
|
292
|
+
| `setAssistantStatus(channelId, threadTs, status)` | Show a thinking/status indicator |
|
|
293
|
+
| `setAssistantTitle(channelId, threadTs, title)` | Set the thread title (shown in History) |
|
|
294
|
+
| `publishHomeView(userId, view)` | Publish a Home tab view for a user |
|
|
295
|
+
| `startTyping(threadId, status)` | Show a custom loading status (requires `assistant:write` scope) |
|
|
296
|
+
|
|
297
|
+
### Required scopes and events
|
|
298
|
+
|
|
299
|
+
Add these to your Slack app manifest for Assistants API support:
|
|
300
|
+
|
|
301
|
+
```yaml
|
|
302
|
+
oauth_config:
|
|
303
|
+
scopes:
|
|
304
|
+
bot:
|
|
305
|
+
- assistant:write
|
|
306
|
+
|
|
307
|
+
settings:
|
|
308
|
+
event_subscriptions:
|
|
309
|
+
bot_events:
|
|
310
|
+
- assistant_thread_started
|
|
311
|
+
- assistant_thread_context_changed
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Stream with stop blocks
|
|
315
|
+
|
|
316
|
+
When streaming in an assistant thread, you can attach Block Kit elements to the final message:
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
await thread.stream(textStream, {
|
|
320
|
+
stopBlocks: [
|
|
321
|
+
{ type: "actions", elements: [{ type: "button", text: { type: "plain_text", text: "Retry" }, action_id: "retry" }] },
|
|
322
|
+
],
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Troubleshooting
|
|
327
|
+
|
|
328
|
+
### `handleOAuthCallback` throws "Adapter not initialized"
|
|
329
|
+
|
|
330
|
+
- Call `await bot.initialize()` before `handleOAuthCallback()` in your callback route.
|
|
331
|
+
- In a Next.js app, this ensures:
|
|
332
|
+
- state adapter is connected
|
|
333
|
+
- the Slack adapter is attached to Chat
|
|
334
|
+
- installation writes succeed
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
const slackAdapter = bot.getAdapter("slack");
|
|
338
|
+
|
|
339
|
+
await bot.initialize();
|
|
340
|
+
await slackAdapter.handleOAuthCallback(request);
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### "Invalid signature" error
|
|
344
|
+
|
|
345
|
+
- Verify `SLACK_SIGNING_SECRET` is correct
|
|
346
|
+
- Check that the request timestamp is within 5 minutes (clock sync issue)
|
|
347
|
+
|
|
348
|
+
### Bot not responding to messages
|
|
36
349
|
|
|
37
|
-
|
|
350
|
+
- Verify event subscriptions are configured
|
|
351
|
+
- Check that the bot has been added to the channel
|
|
352
|
+
- Ensure the webhook URL is correct and accessible
|
|
38
353
|
|
|
39
354
|
## License
|
|
40
355
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CardElement, BaseFormatConverter, AdapterPostableMessage, Root, Logger, Adapter, ChatInstance, WebhookOptions, RawMessage, EphemeralMessage, ModalElement, EmojiValue, StreamChunk, StreamOptions, FetchOptions, FetchResult, ThreadInfo, Message, ListThreadsOptions, ListThreadsResult, ChannelInfo, FormattedContent } from 'chat';
|
|
1
|
+
import { CardElement, BaseFormatConverter, AdapterPostableMessage, Root, Logger, Adapter, ChatInstance, WebhookOptions, RawMessage, EphemeralMessage, ScheduledMessage, ModalElement, EmojiValue, StreamChunk, StreamOptions, FetchOptions, FetchResult, ThreadInfo, Message, ListThreadsOptions, ListThreadsResult, ChannelInfo, FormattedContent } from 'chat';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Slack Block Kit converter for cross-platform cards.
|
|
@@ -320,6 +320,9 @@ declare class SlackAdapter implements Adapter<SlackThreadId, unknown> {
|
|
|
320
320
|
private renderWithTableBlocks;
|
|
321
321
|
postMessage(threadId: string, message: AdapterPostableMessage): Promise<RawMessage<unknown>>;
|
|
322
322
|
postEphemeral(threadId: string, userId: string, message: AdapterPostableMessage): Promise<EphemeralMessage>;
|
|
323
|
+
scheduleMessage(threadId: string, message: AdapterPostableMessage, options: {
|
|
324
|
+
postAt: Date;
|
|
325
|
+
}): Promise<ScheduledMessage>;
|
|
323
326
|
openModal(triggerId: string, modal: ModalElement, contextId?: string): Promise<{
|
|
324
327
|
viewId: string;
|
|
325
328
|
}>;
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { AsyncLocalStorage } from "async_hooks";
|
|
|
3
3
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
4
4
|
import {
|
|
5
5
|
AdapterRateLimitError,
|
|
6
|
+
AuthenticationError,
|
|
6
7
|
extractCard,
|
|
7
8
|
extractFiles,
|
|
8
9
|
NetworkError,
|
|
@@ -11,7 +12,6 @@ import {
|
|
|
11
12
|
} from "@chat-adapter/shared";
|
|
12
13
|
import { WebClient } from "@slack/web-api";
|
|
13
14
|
import {
|
|
14
|
-
ChatError,
|
|
15
15
|
ConsoleLogger,
|
|
16
16
|
convertEmojiPlaceholders,
|
|
17
17
|
defaultEmojiResolver,
|
|
@@ -772,9 +772,9 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
772
772
|
if (this.defaultBotToken) {
|
|
773
773
|
return this.defaultBotToken;
|
|
774
774
|
}
|
|
775
|
-
throw new
|
|
776
|
-
"
|
|
777
|
-
"
|
|
775
|
+
throw new AuthenticationError(
|
|
776
|
+
"slack",
|
|
777
|
+
"No bot token available. In multi-workspace mode, ensure the webhook is being processed."
|
|
778
778
|
);
|
|
779
779
|
}
|
|
780
780
|
/**
|
|
@@ -819,9 +819,9 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
819
819
|
*/
|
|
820
820
|
async setInstallation(teamId, installation) {
|
|
821
821
|
if (!this.chat) {
|
|
822
|
-
throw new
|
|
823
|
-
"
|
|
824
|
-
"
|
|
822
|
+
throw new ValidationError(
|
|
823
|
+
"slack",
|
|
824
|
+
"Adapter not initialized. Ensure chat.initialize() has been called first."
|
|
825
825
|
);
|
|
826
826
|
}
|
|
827
827
|
const state = this.chat.getState();
|
|
@@ -841,9 +841,9 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
841
841
|
*/
|
|
842
842
|
async getInstallation(teamId) {
|
|
843
843
|
if (!this.chat) {
|
|
844
|
-
throw new
|
|
845
|
-
"
|
|
846
|
-
"
|
|
844
|
+
throw new ValidationError(
|
|
845
|
+
"slack",
|
|
846
|
+
"Adapter not initialized. Ensure chat.initialize() has been called first."
|
|
847
847
|
);
|
|
848
848
|
}
|
|
849
849
|
const state = this.chat.getState();
|
|
@@ -870,17 +870,17 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
870
870
|
*/
|
|
871
871
|
async handleOAuthCallback(request) {
|
|
872
872
|
if (!(this.clientId && this.clientSecret)) {
|
|
873
|
-
throw new
|
|
874
|
-
"
|
|
875
|
-
"
|
|
873
|
+
throw new ValidationError(
|
|
874
|
+
"slack",
|
|
875
|
+
"clientId and clientSecret are required for OAuth. Pass them in createSlackAdapter()."
|
|
876
876
|
);
|
|
877
877
|
}
|
|
878
878
|
const url = new URL(request.url);
|
|
879
879
|
const code = url.searchParams.get("code");
|
|
880
880
|
if (!code) {
|
|
881
|
-
throw new
|
|
882
|
-
"
|
|
883
|
-
"
|
|
881
|
+
throw new ValidationError(
|
|
882
|
+
"slack",
|
|
883
|
+
"Missing 'code' query parameter in OAuth callback request."
|
|
884
884
|
);
|
|
885
885
|
}
|
|
886
886
|
const redirectUri = url.searchParams.get("redirect_uri") ?? void 0;
|
|
@@ -891,9 +891,9 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
891
891
|
redirect_uri: redirectUri
|
|
892
892
|
});
|
|
893
893
|
if (!(result.ok && result.access_token && result.team?.id)) {
|
|
894
|
-
throw new
|
|
895
|
-
|
|
896
|
-
"
|
|
894
|
+
throw new AuthenticationError(
|
|
895
|
+
"slack",
|
|
896
|
+
`Slack OAuth failed: ${result.error || "missing access_token or team.id"}`
|
|
897
897
|
);
|
|
898
898
|
}
|
|
899
899
|
const teamId = result.team.id;
|
|
@@ -910,9 +910,9 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
910
910
|
*/
|
|
911
911
|
async deleteInstallation(teamId) {
|
|
912
912
|
if (!this.chat) {
|
|
913
|
-
throw new
|
|
914
|
-
"
|
|
915
|
-
"
|
|
913
|
+
throw new ValidationError(
|
|
914
|
+
"slack",
|
|
915
|
+
"Adapter not initialized. Ensure chat.initialize() has been called first."
|
|
916
916
|
);
|
|
917
917
|
}
|
|
918
918
|
const state = this.chat.getState();
|
|
@@ -1329,7 +1329,10 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1329
1329
|
if (isJSX(modal)) {
|
|
1330
1330
|
const converted = toModalElement(modal);
|
|
1331
1331
|
if (!converted) {
|
|
1332
|
-
throw new
|
|
1332
|
+
throw new ValidationError(
|
|
1333
|
+
"slack",
|
|
1334
|
+
"Invalid JSX element: must be a Modal element"
|
|
1335
|
+
);
|
|
1333
1336
|
}
|
|
1334
1337
|
return converted;
|
|
1335
1338
|
}
|
|
@@ -1403,7 +1406,7 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1403
1406
|
channel: event.channel,
|
|
1404
1407
|
threadTs
|
|
1405
1408
|
});
|
|
1406
|
-
const isMention =
|
|
1409
|
+
const isMention = event.type === "app_mention";
|
|
1407
1410
|
const factory = async () => {
|
|
1408
1411
|
const msg = await this.parseSlackMessage(event, threadId);
|
|
1409
1412
|
if (isMention) {
|
|
@@ -1416,7 +1419,7 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1416
1419
|
/**
|
|
1417
1420
|
* Handle reaction events from Slack (reaction_added, reaction_removed).
|
|
1418
1421
|
*/
|
|
1419
|
-
handleReactionEvent(event, options) {
|
|
1422
|
+
async handleReactionEvent(event, options) {
|
|
1420
1423
|
if (!this.chat) {
|
|
1421
1424
|
this.logger.warn("Chat instance not initialized, ignoring reaction");
|
|
1422
1425
|
return;
|
|
@@ -1427,9 +1430,32 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1427
1430
|
});
|
|
1428
1431
|
return;
|
|
1429
1432
|
}
|
|
1433
|
+
let parentTs = event.item.ts;
|
|
1434
|
+
try {
|
|
1435
|
+
const result = await this.client.conversations.replies(
|
|
1436
|
+
this.withToken({
|
|
1437
|
+
channel: event.item.channel,
|
|
1438
|
+
ts: event.item.ts,
|
|
1439
|
+
limit: 1
|
|
1440
|
+
})
|
|
1441
|
+
);
|
|
1442
|
+
const firstMessage = result.messages?.[0];
|
|
1443
|
+
if (firstMessage?.thread_ts) {
|
|
1444
|
+
parentTs = firstMessage.thread_ts;
|
|
1445
|
+
}
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
this.logger.warn(
|
|
1448
|
+
"Failed to resolve parent thread for reaction, using message ts",
|
|
1449
|
+
{
|
|
1450
|
+
error: String(error),
|
|
1451
|
+
channel: event.item.channel,
|
|
1452
|
+
ts: event.item.ts
|
|
1453
|
+
}
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1430
1456
|
const threadId = this.encodeThreadId({
|
|
1431
1457
|
channel: event.item.channel,
|
|
1432
|
-
threadTs:
|
|
1458
|
+
threadTs: parentTs
|
|
1433
1459
|
});
|
|
1434
1460
|
const messageId = event.item.ts;
|
|
1435
1461
|
const rawEmoji = event.reaction;
|
|
@@ -1996,6 +2022,95 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
1996
2022
|
this.handleSlackError(error);
|
|
1997
2023
|
}
|
|
1998
2024
|
}
|
|
2025
|
+
async scheduleMessage(threadId, message, options) {
|
|
2026
|
+
const { channel, threadTs } = this.decodeThreadId(threadId);
|
|
2027
|
+
const postAtUnix = Math.floor(options.postAt.getTime() / 1e3);
|
|
2028
|
+
if (postAtUnix <= Math.floor(Date.now() / 1e3)) {
|
|
2029
|
+
throw new ValidationError("slack", "postAt must be in the future");
|
|
2030
|
+
}
|
|
2031
|
+
const files = extractFiles(message);
|
|
2032
|
+
if (files.length > 0) {
|
|
2033
|
+
throw new ValidationError(
|
|
2034
|
+
"slack",
|
|
2035
|
+
"File uploads are not supported in scheduled messages"
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
const token = this.getToken();
|
|
2039
|
+
try {
|
|
2040
|
+
const card = extractCard(message);
|
|
2041
|
+
if (card) {
|
|
2042
|
+
const blocks = cardToBlockKit(card);
|
|
2043
|
+
const fallbackText = cardToFallbackText(card);
|
|
2044
|
+
this.logger.debug("Slack API: chat.scheduleMessage (blocks)", {
|
|
2045
|
+
channel,
|
|
2046
|
+
threadTs,
|
|
2047
|
+
postAt: postAtUnix,
|
|
2048
|
+
blockCount: blocks.length
|
|
2049
|
+
});
|
|
2050
|
+
const result2 = await this.client.chat.scheduleMessage({
|
|
2051
|
+
token,
|
|
2052
|
+
channel,
|
|
2053
|
+
thread_ts: threadTs || void 0,
|
|
2054
|
+
post_at: postAtUnix,
|
|
2055
|
+
text: fallbackText,
|
|
2056
|
+
blocks,
|
|
2057
|
+
unfurl_links: false,
|
|
2058
|
+
unfurl_media: false
|
|
2059
|
+
});
|
|
2060
|
+
const scheduledMessageId2 = result2.scheduled_message_id;
|
|
2061
|
+
const adapter2 = this;
|
|
2062
|
+
return {
|
|
2063
|
+
scheduledMessageId: scheduledMessageId2,
|
|
2064
|
+
channelId: channel,
|
|
2065
|
+
postAt: options.postAt,
|
|
2066
|
+
raw: result2,
|
|
2067
|
+
async cancel() {
|
|
2068
|
+
await adapter2.client.chat.deleteScheduledMessage({
|
|
2069
|
+
token,
|
|
2070
|
+
channel,
|
|
2071
|
+
scheduled_message_id: scheduledMessageId2
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
const text = convertEmojiPlaceholders(
|
|
2077
|
+
this.formatConverter.renderPostable(message),
|
|
2078
|
+
"slack"
|
|
2079
|
+
);
|
|
2080
|
+
this.logger.debug("Slack API: chat.scheduleMessage", {
|
|
2081
|
+
channel,
|
|
2082
|
+
threadTs,
|
|
2083
|
+
postAt: postAtUnix,
|
|
2084
|
+
textLength: text.length
|
|
2085
|
+
});
|
|
2086
|
+
const result = await this.client.chat.scheduleMessage({
|
|
2087
|
+
token,
|
|
2088
|
+
channel,
|
|
2089
|
+
thread_ts: threadTs || void 0,
|
|
2090
|
+
post_at: postAtUnix,
|
|
2091
|
+
text,
|
|
2092
|
+
unfurl_links: false,
|
|
2093
|
+
unfurl_media: false
|
|
2094
|
+
});
|
|
2095
|
+
const scheduledMessageId = result.scheduled_message_id;
|
|
2096
|
+
const adapter = this;
|
|
2097
|
+
return {
|
|
2098
|
+
scheduledMessageId,
|
|
2099
|
+
channelId: channel,
|
|
2100
|
+
postAt: options.postAt,
|
|
2101
|
+
raw: result,
|
|
2102
|
+
async cancel() {
|
|
2103
|
+
await adapter.client.chat.deleteScheduledMessage({
|
|
2104
|
+
token,
|
|
2105
|
+
channel,
|
|
2106
|
+
scheduled_message_id: scheduledMessageId
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
};
|
|
2110
|
+
} catch (error) {
|
|
2111
|
+
this.handleSlackError(error);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
1999
2114
|
async openModal(triggerId, modal, contextId) {
|
|
2000
2115
|
const metadata = encodeModalMetadata({
|
|
2001
2116
|
contextId,
|
|
@@ -2313,9 +2428,9 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
2313
2428
|
*/
|
|
2314
2429
|
async stream(threadId, textStream, options) {
|
|
2315
2430
|
if (!(options?.recipientUserId && options?.recipientTeamId)) {
|
|
2316
|
-
throw new
|
|
2317
|
-
"
|
|
2318
|
-
"
|
|
2431
|
+
throw new ValidationError(
|
|
2432
|
+
"slack",
|
|
2433
|
+
"Slack streaming requires recipientUserId and recipientTeamId in options"
|
|
2319
2434
|
);
|
|
2320
2435
|
}
|
|
2321
2436
|
const { channel, threadTs } = this.decodeThreadId(threadId);
|
|
@@ -2958,9 +3073,9 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
2958
3073
|
} else {
|
|
2959
3074
|
const message = options?.message;
|
|
2960
3075
|
if (!message) {
|
|
2961
|
-
throw new
|
|
2962
|
-
"
|
|
2963
|
-
"
|
|
3076
|
+
throw new ValidationError(
|
|
3077
|
+
"slack",
|
|
3078
|
+
"Message required for replace action"
|
|
2964
3079
|
);
|
|
2965
3080
|
}
|
|
2966
3081
|
const card = extractCard(message);
|
|
@@ -3008,9 +3123,9 @@ var SlackAdapter = class _SlackAdapter {
|
|
|
3008
3123
|
status: response.status,
|
|
3009
3124
|
body: errorText
|
|
3010
3125
|
});
|
|
3011
|
-
throw new
|
|
3012
|
-
|
|
3013
|
-
|
|
3126
|
+
throw new NetworkError(
|
|
3127
|
+
"slack",
|
|
3128
|
+
`Failed to ${action} via response_url: ${errorText}`
|
|
3014
3129
|
);
|
|
3015
3130
|
}
|
|
3016
3131
|
const responseText = await response.text();
|