UNPKG

chat

Version:

Unified chat abstraction for Slack, Teams, Google Chat, and Discord

298 lines (219 loc) 8.64 kB
--- title: Threads, Messages, and Channels description: Work with threads, messages, and channels across platforms. type: guide prerequisites: - /docs/usage related: - /docs/handling-events - /docs/posting-messages --- ## Threads A `Thread` represents a conversation thread on any platform. It provides methods for posting messages, managing subscriptions, and accessing message history. Thread instances are most often supplied by the SDK to your event handlers. You can also construct one explicitly from a thread ID — useful for cron jobs, workflow steps, or any other context outside an inbound webhook: ```typescript title="lib/bot.ts" lineNumbers const thread = bot.thread("slack:C123ABC:1234567890.123456"); await thread.post("Reminder from a cron job"); ``` For DM-style conversations, use [`bot.openDM(userIdOrAuthor)`](/docs/direct-messages) instead. It resolves the right channel and thread for user ID formats the SDK can infer. ### Post a message ```typescript title="lib/bot.ts" lineNumbers // Plain text await thread.post("Hello world"); // Markdown (converted to each platform's format) await thread.post("**Bold** and _italic_ text"); // Structured message with attachments await thread.post({ markdown: "Here's a file:", files: [{ data: buffer, filename: "report.pdf" }], }); ``` ### Subscribe and unsubscribe Subscriptions persist across restarts (stored in your state adapter). When a non-DM thread is subscribed, all messages route to `onSubscribedMessage`. DM threads route to `onDirectMessage` first when a direct message handler is registered. ```typescript title="lib/bot.ts" lineNumbers await thread.subscribe(); await thread.unsubscribe(); const subscribed = await thread.isSubscribed(); ``` ### Participants Get the unique human participants in a thread. Returns deduplicated authors, excluding all bots. Useful for deciding whether to subscribe based on how many humans are in the conversation. ```typescript title="lib/bot.ts" lineNumbers bot.onNewMention(async (thread) => { const participants = await thread.getParticipants(); if (participants.length === 1) { await thread.subscribe(); await thread.post("I'm here to help!"); } }); bot.onSubscribedMessage(async (thread) => { const participants = await thread.getParticipants(); if (participants.length > 1) { await thread.unsubscribe(); return; } // respond... }); ``` <Callout type="warn"> Each call fetches the full message history to find all participants. On threads with long history this makes multiple API calls to the platform. Consider checking `message.author` against a known set before calling `getParticipants()` on every incoming message. </Callout> ### Typing indicator ```typescript title="lib/bot.ts" await thread.startTyping(); ``` <Callout type="info"> Not all platforms support typing indicators. The call is a no-op on unsupported platforms. See the [adapter feature matrix](/docs/adapters) for details. </Callout> ### Message history Access recent messages or iterate through full history: ```typescript title="lib/bot.ts" lineNumbers // Cached messages from the webhook payload const recent = thread.recentMessages; // Newest first (auto-paginates) for await (const msg of thread.messages) { console.log(msg.text); } // Oldest first (auto-paginates) for await (const msg of thread.allMessages) { console.log(msg.text); } ``` ### Thread state Store typed, per-thread state that persists across requests. Pass a generic type parameter to `Chat` to get typed thread state across all handlers: ```typescript title="lib/bot.ts" lineNumbers interface ThreadState { aiMode?: boolean; context?: string; } const bot = new Chat<typeof adapters, ThreadState>({ // ...config }); bot.onNewMention(async (thread) => { await thread.setState({ aiMode: true }); const state = await thread.state; // ThreadState | null if (state?.aiMode) { // AI mode is enabled } }); ``` State is stored in your state adapter with a 30-day TTL. Use `{ replace: true }` to replace state entirely instead of merging: ```typescript title="lib/bot.ts" await thread.setState({ aiMode: false }, { replace: true }); ``` ### Scheduled messages Schedule a message for future delivery. The returned `ScheduledMessage` includes a `cancel()` method to abort before it's sent. ```typescript title="lib/bot.ts" lineNumbers const scheduled = await thread.schedule("Reminder: standup in 5 minutes!", { postAt: new Date("2026-03-09T09:00:00Z"), }); // Cancel before it's sent await scheduled.cancel(); ``` <Callout type="info"> Scheduled messages are currently only supported by the Slack adapter. Other adapters throw `NotImplementedError`. See the [feature matrix](/docs/adapters) for details. </Callout> ## Messages Incoming messages are normalized across platforms into a consistent format: | Property | Type | Description | |----------|------|-------------| | `id` | `string` | Platform message ID | | `threadId` | `string` | Thread ID in `adapter:channel:thread` format | | `text` | `string` | Plain text content | | `formatted` | `Root` | mdast AST representation | | `raw` | `unknown` | Original platform-specific payload | | `author` | `Author` | Message author info | | `metadata` | `MessageMetadata` | Timestamps and edit status | | `attachments` | `Attachment[]` (optional) | File attachments | | `isMention` | `boolean` (optional) | Whether the bot was @-mentioned | ### Author ```typescript lineNumbers interface Author { userId: string; userName: string; fullName: string; isBot: boolean | "unknown"; isMe: boolean; // true if message is from the bot itself } ``` For richer user info (email, avatar), use [`chat.getUser()`](/docs/api/chat#getuser): ```typescript title="lib/bot.ts" const user = await bot.getUser(message.author); console.log(user?.email); // "alice@company.com" ``` ### Sent messages When you post a message, you get back a `SentMessage` with methods to edit, delete, and react: ```typescript title="lib/bot.ts" lineNumbers const sent = await thread.post("Processing..."); // Do some work... await sent.edit("Done!"); // Or delete await sent.delete(); // Add/remove reactions await sent.addReaction(emoji.check); await sent.removeReaction(emoji.check); ``` ## Channels A `Channel` represents the container that holds threads (e.g., a Slack channel, a Teams conversation). Navigate to a channel from a thread or get one directly: ```typescript title="lib/bot.ts" lineNumbers // From a thread const channel = thread.channel; // Directly by ID const channel = bot.channel("slack:C123ABC"); ``` ### List threads Iterate threads in a channel, most recently active first: ```typescript title="lib/bot.ts" lineNumbers for await (const thread of channel.threads()) { console.log(thread.rootMessage.text, thread.replyCount); } ``` ### Channel messages Iterate top-level messages (not thread replies): ```typescript title="lib/bot.ts" lineNumbers for await (const msg of channel.messages) { console.log(msg.text); } ``` ### Post to a channel Post a top-level message (not inside a thread): ```typescript title="lib/bot.ts" await channel.post("Hello channel!"); ``` ### Channel metadata ```typescript title="lib/bot.ts" const info = await channel.fetchMetadata(); console.log(info.name, info.memberCount); ``` ## Thread ID format All thread IDs follow the pattern `{adapter}:{channel}:{thread}`: - **Slack**: `slack:C123ABC:1234567890.123456` - **Teams**: `teams:{base64(conversationId)}:{base64(serviceUrl)}` - **Google Chat**: `gchat:spaces/ABC123:{base64(threadName)}` - **Discord**: `discord:{guildId}:{channelId}/{messageId}` You typically don't need to construct these yourself — they're provided by the SDK in event handlers. ## Logging The `logger` option is optional — if omitted, Chat SDK uses `ConsoleLogger("info")` by default. Each adapter also creates its own child logger automatically. ```typescript title="lib/bot.ts" lineNumbers // Use defaults (ConsoleLogger at "info" level) const bot = new Chat({ // ... }); // Or set a specific log level const bot = new Chat({ // ... logger: "debug", // "debug" | "info" | "warn" | "error" | "silent" }); // Or use a custom ConsoleLogger for child loggers import { ConsoleLogger } from "chat"; const logger = new ConsoleLogger("info"); const bot = new Chat({ // ... logger, }); ``` You can pass child loggers to adapters for prefixed log output, but adapters create their own child loggers by default: ```typescript title="lib/bot.ts" createSlackAdapter({ logger: logger.child("slack"), // optional — auto-created if omitted }); ```