Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions apps/docs/content/docs/guides/slack-nextjs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ oauth_config:
- reactions:read
- reactions:write
- users:read
- users:read.email

settings:
event_subscriptions:
Expand Down Expand Up @@ -142,7 +143,7 @@ type Platform = keyof typeof bot.webhooks;

export async function POST(
request: Request,
context: RouteContext<"/api/webhooks/[platform]">
context: RouteContext<"/api/webhooks/[platform]">,
) {
const { platform } = await context.params;

Expand Down Expand Up @@ -209,7 +210,9 @@ bot.onAction("info", async (event) => {
```

<Callout type="info">
The file extension must be `.tsx` (not `.ts`) when using JSX components like `Card` and `Button`. Make sure your `tsconfig.json` has `"jsx": "react-jsx"` and `"jsxImportSource": "chat"`.
The file extension must be `.tsx` (not `.ts`) when using JSX components like
`Card` and `Button`. Make sure your `tsconfig.json` has `"jsx": "react-jsx"`
and `"jsxImportSource": "chat"`.
</Callout>

## Deploy to Vercel
Expand Down
1 change: 1 addition & 0 deletions examples/nextjs-chat/slack-manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ oauth_config:
- reactions:write
# User info for display names
- users:read
- users:read.email

settings:
event_subscriptions:
Expand Down
124 changes: 67 additions & 57 deletions packages/adapter-slack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ oauth_config:
- reactions:read
- reactions:write
- users:read
- users:read.email

settings:
event_subscriptions:
Expand Down Expand Up @@ -180,17 +181,17 @@ After creating the app, go to **Basic Information** → **App Credentials** and

All options are auto-detected from environment variables when not provided. You can call `createSlackAdapter()` with no arguments if the env vars are set.

| Option | Required | Description |
|--------|----------|-------------|
| `botToken` | No | Bot token (`xoxb-...`). Auto-detected from `SLACK_BOT_TOKEN` |
| `signingSecret` | No* | Signing secret for webhook verification. Auto-detected from `SLACK_SIGNING_SECRET` |
| `clientId` | No | App client ID for multi-workspace OAuth. Auto-detected from `SLACK_CLIENT_ID` |
| `clientSecret` | No | App client secret for multi-workspace OAuth. Auto-detected from `SLACK_CLIENT_SECRET` |
| `encryptionKey` | No | AES-256-GCM key for encrypting stored tokens. Auto-detected from `SLACK_ENCRYPTION_KEY` |
| `installationKeyPrefix` | No | Prefix for the state key used to store workspace installations. Defaults to `slack:installation`. The full key is `{prefix}:{teamId}` |
| `logger` | No | Logger instance (defaults to `ConsoleLogger("info")`) |
| Option | Required | Description |
| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `botToken` | No | Bot token (`xoxb-...`). Auto-detected from `SLACK_BOT_TOKEN` |
| `signingSecret` | No\* | Signing secret for webhook verification. Auto-detected from `SLACK_SIGNING_SECRET` |
| `clientId` | No | App client ID for multi-workspace OAuth. Auto-detected from `SLACK_CLIENT_ID` |
| `clientSecret` | No | App client secret for multi-workspace OAuth. Auto-detected from `SLACK_CLIENT_SECRET` |
| `encryptionKey` | No | AES-256-GCM key for encrypting stored tokens. Auto-detected from `SLACK_ENCRYPTION_KEY` |
| `installationKeyPrefix` | No | Prefix for the state key used to store workspace installations. Defaults to `slack:installation`. The full key is `{prefix}:{teamId}` |
| `logger` | No | Logger instance (defaults to `ConsoleLogger("info")`) |

*`signingSecret` is required — either via config or `SLACK_SIGNING_SECRET` env var.
\*`signingSecret` is required — either via config or `SLACK_SIGNING_SECRET` env var.

## Environment variables

Expand All @@ -206,59 +207,59 @@ SLACK_ENCRYPTION_KEY=... # Optional, for token encryption

### Messaging

| Feature | Supported |
|---------|-----------|
| Post message | Yes |
| Edit message | Yes |
| Delete message | Yes |
| File uploads | Yes |
| Streaming | Native API |
| Feature | Supported |
| ------------------ | ------------------------- |
| Post message | Yes |
| Edit message | Yes |
| Delete message | Yes |
| File uploads | Yes |
| Streaming | Native API |
| Scheduled messages | Yes (native, with cancel) |

### Rich content

| Feature | Supported |
|---------|-----------|
| Card format | Block Kit |
| Buttons | Yes |
| Link buttons | Yes |
| Select menus | Yes |
| Tables | Block Kit |
| Fields | Yes |
| Images in cards | Yes |
| Modals | Yes |
| Feature | Supported |
| --------------- | --------- |
| Card format | Block Kit |
| Buttons | Yes |
| Link buttons | Yes |
| Select menus | Yes |
| Tables | Block Kit |
| Fields | Yes |
| Images in cards | Yes |
| Modals | Yes |

### Conversations

| Feature | Supported |
|---------|-----------|
| Slash commands | Yes |
| Mentions | Yes |
| Add reactions | Yes |
| Remove reactions | Yes |
| Typing indicator | Yes |
| DMs | Yes |
| Feature | Supported |
| ------------------ | ------------ |
| Slash commands | Yes |
| Mentions | Yes |
| Add reactions | Yes |
| Remove reactions | Yes |
| Typing indicator | Yes |
| DMs | Yes |
| Ephemeral messages | Yes (native) |

### Message history

| Feature | Supported |
|---------|-----------|
| Fetch messages | Yes |
| Fetch single message | Yes |
| Fetch thread info | Yes |
| Fetch channel messages | Yes |
| List threads | Yes |
| Fetch channel info | Yes |
| Post channel message | Yes |
| Feature | Supported |
| ---------------------- | --------- |
| Fetch messages | Yes |
| Fetch single message | Yes |
| Fetch thread info | Yes |
| Fetch channel messages | Yes |
| List threads | Yes |
| Fetch channel info | Yes |
| Post channel message | Yes |

### Platform-specific

| Feature | Supported |
|---------|-----------|
| Assistants API | Yes |
| Member joined channel | Yes |
| App Home tab | Yes |
| Feature | Supported |
| --------------------- | --------- |
| Assistants API | Yes |
| Member joined channel | Yes |
| App Home tab | Yes |

## Slack Assistants API

Expand Down Expand Up @@ -286,13 +287,13 @@ bot.onAssistantContextChanged(async (event) => {

The `SlackAdapter` exposes these methods for the Assistants API:

| Method | Description |
|--------|-------------|
| `setSuggestedPrompts(channelId, threadTs, prompts, title?)` | Show prompt suggestions in the thread |
| `setAssistantStatus(channelId, threadTs, status)` | Show a thinking/status indicator |
| `setAssistantTitle(channelId, threadTs, title)` | Set the thread title (shown in History) |
| `publishHomeView(userId, view)` | Publish a Home tab view for a user |
| `startTyping(threadId, status)` | Show a custom loading status (requires `assistant:write` scope) |
| Method | Description |
| ----------------------------------------------------------- | --------------------------------------------------------------- |
| `setSuggestedPrompts(channelId, threadTs, prompts, title?)` | Show prompt suggestions in the thread |
| `setAssistantStatus(channelId, threadTs, status)` | Show a thinking/status indicator |
| `setAssistantTitle(channelId, threadTs, title)` | Set the thread title (shown in History) |
| `publishHomeView(userId, view)` | Publish a Home tab view for a user |
| `startTyping(threadId, status)` | Show a custom loading status (requires `assistant:write` scope) |

### Required scopes and events

Expand All @@ -318,7 +319,16 @@ When streaming in an assistant thread, you can attach Block Kit elements to the
```typescript
await thread.stream(textStream, {
stopBlocks: [
{ type: "actions", elements: [{ type: "button", text: { type: "plain_text", text: "Retry" }, action_id: "retry" }] },
{
type: "actions",
elements: [
{
type: "button",
text: { type: "plain_text", text: "Retry" },
action_id: "retry",
},
],
},
],
});
```
Expand Down
43 changes: 43 additions & 0 deletions packages/adapter-slack/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3018,6 +3018,49 @@ describe("fetchMessages", () => {
})
);
});

it("fetches author email via lookupUser when fetching messages", async () => {
const adapter = createSlackAdapter({
botToken: "xoxb-test-token",
signingSecret: secret,
logger: mockLogger,
botUserId: "U_BOT",
});

mockClientMethod(
adapter,
"conversations.replies",
vi.fn().mockResolvedValue({
ok: true,
messages: [
{
type: "message",
user: "U1",
text: "test message",
ts: "1000.000",
channel: "C123",
},
],
has_more: false,
})
);
mockClientMethod(
adapter,
"users.info",
vi.fn().mockResolvedValue({
ok: true,
user: { name: "user1", profile: { email: "user1@example.com" } },
})
);

const state = createMockState();
await adapter.initialize(createMockChatInstance(state));

const result = await adapter.fetchMessages("slack:C123:1234567890.000000");

expect(result.messages.length).toBe(1);
expect(result.messages[0].author.email).toBe("user1@example.com");
});
});

// ============================================================================
Expand Down
21 changes: 16 additions & 5 deletions packages/adapter-slack/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ type SlackInteractivePayload =
/** Cached user info */
interface CachedUser {
displayName: string;
email?: string;
realName: string;
}

Expand Down Expand Up @@ -700,14 +701,18 @@ export class SlackAdapter implements Adapter<SlackThreadId, unknown> {
*/
private async lookupUser(
userId: string
): Promise<{ displayName: string; realName: string }> {
): Promise<{ displayName: string; realName: string; email?: string }> {
const cacheKey = `slack:user:${userId}`;

// Check cache first (via state adapter for serverless compatibility)
if (this.chat) {
const cached = await this.chat.getState().get<CachedUser>(cacheKey);
if (cached) {
return { displayName: cached.displayName, realName: cached.realName };
return {
displayName: cached.displayName,
realName: cached.realName,
email: cached.email,
};
}
}

Expand All @@ -718,7 +723,7 @@ export class SlackAdapter implements Adapter<SlackThreadId, unknown> {
const user = result.user as {
name?: string;
real_name?: string;
profile?: { display_name?: string; real_name?: string };
profile?: { display_name?: string; real_name?: string; email?: string };
};

// Slack user naming: profile.display_name > profile.real_name > real_name > name > userId
Expand All @@ -730,14 +735,15 @@ export class SlackAdapter implements Adapter<SlackThreadId, unknown> {
userId;
const realName =
user?.real_name || user?.profile?.real_name || displayName;
const email = user?.profile?.email;

// Cache the result via state adapter
if (this.chat) {
await this.chat
.getState()
.set<CachedUser>(
cacheKey,
{ displayName, realName },
{ displayName, realName, email },
SlackAdapter.USER_CACHE_TTL_MS
);

Expand All @@ -757,8 +763,9 @@ export class SlackAdapter implements Adapter<SlackThreadId, unknown> {
userId,
displayName,
realName,
email,
});
return { displayName, realName };
return { displayName, realName, email };
} catch (error) {
this.logger.warn("Could not fetch user info", { userId, error });
// Fall back to user ID
Expand Down Expand Up @@ -1868,12 +1875,14 @@ export class SlackAdapter implements Adapter<SlackThreadId, unknown> {
// since Slack events only include the user ID, not the username
let userName = event.username || "unknown";
let fullName = event.username || "unknown";
let userEmail = "unknown";

// If we have a user ID but no username, look up the user info
if (event.user && !event.username) {
const userInfo = await this.lookupUser(event.user);
userName = userInfo.displayName;
fullName = userInfo.realName;
userEmail = userInfo.email || "unknown";
}

// Track thread participants for outgoing mention resolution (skip dupes)
Expand Down Expand Up @@ -1911,6 +1920,7 @@ export class SlackAdapter implements Adapter<SlackThreadId, unknown> {
userId: event.user || event.bot_id || "unknown",
userName,
fullName,
email: userEmail,
isBot: !!event.bot_id,
isMe,
},
Expand Down Expand Up @@ -3372,6 +3382,7 @@ export class SlackAdapter implements Adapter<SlackThreadId, unknown> {
userId: event.user || event.bot_id || "unknown",
userName,
fullName,
email: "unknown",
isBot: !!event.bot_id,
isMe,
},
Expand Down
2 changes: 2 additions & 0 deletions packages/chat/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,8 @@ export interface RawMessage<TRawMessage = unknown> {
}

export interface Author {
/** Email address (if supported by platform and scopes) */
email?: string;
/** Display name */
fullName: string;
/** Whether the author is a bot */
Expand Down