chat 4.15.0 → 4.16.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/dist/{chunk-VGF42GJ2.js → chunk-A2J5CUHD.js} +368 -9
- package/dist/chunk-A2J5CUHD.js.map +1 -0
- package/dist/index.d.ts +196 -12
- package/dist/index.js +444 -250
- package/dist/index.js.map +1 -1
- package/dist/{jsx-runtime-Wowykq7Z.d.ts → jsx-runtime-Bokk9xw5.d.ts} +47 -2
- package/dist/jsx-runtime.d.ts +1 -1
- package/dist/jsx-runtime.js +1 -1
- package/docs/actions.mdx +1 -1
- package/docs/adapters/discord.mdx +1 -0
- package/docs/adapters/index.mdx +16 -0
- package/docs/adapters/slack.mdx +3 -0
- package/docs/adapters/telegram.mdx +60 -0
- package/docs/api/cards.mdx +35 -1
- package/docs/api/channel.mdx +4 -1
- package/docs/api/chat.mdx +5 -0
- package/docs/api/markdown.mdx +42 -0
- package/docs/api/postable-message.mdx +12 -4
- package/docs/api/thread.mdx +6 -3
- package/docs/cards.mdx +24 -0
- package/docs/contributing/building.mdx +626 -0
- package/docs/contributing/documenting.mdx +218 -0
- package/docs/contributing/meta.json +4 -0
- package/docs/contributing/publishing.mdx +161 -0
- package/docs/contributing/testing.mdx +494 -0
- package/docs/error-handling.mdx +1 -1
- package/docs/getting-started.mdx +23 -1
- package/docs/handling-events.mdx +402 -0
- package/docs/meta.json +6 -1
- package/docs/posting-messages.mdx +14 -11
- package/docs/slash-commands.mdx +23 -0
- package/docs/state/meta.json +1 -6
- package/docs/streaming.mdx +108 -4
- package/docs/threads-messages-channels.mdx +237 -0
- package/docs/usage.mdx +82 -276
- package/package.json +4 -3
- package/dist/chunk-VGF42GJ2.js.map +0 -1
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Building a community adapter
|
|
3
|
+
description: Learn how to build, package, and publish your own Chat SDK adapter for any messaging platform.
|
|
4
|
+
type: guide
|
|
5
|
+
prerequisites:
|
|
6
|
+
- /docs/getting-started
|
|
7
|
+
- /docs/adapters
|
|
8
|
+
related:
|
|
9
|
+
- /docs/contributing/testing
|
|
10
|
+
- /docs/cards
|
|
11
|
+
- /docs/actions
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## What adapters are
|
|
15
|
+
|
|
16
|
+
Adapters are the bridge between Chat SDK and a messaging platform. Each adapter handles webhook verification, message parsing, and API calls for one platform so your handler code stays platform-agnostic.
|
|
17
|
+
|
|
18
|
+
Chat SDK ships with Vercel-maintained adapters for Slack, Teams, Google Chat, Discord, Telegram, GitHub, and Linear. Community developers can build adapters for any other platform using the same `Adapter` interface.
|
|
19
|
+
|
|
20
|
+
### Adapter tiers
|
|
21
|
+
|
|
22
|
+
| Tier | Description | Examples |
|
|
23
|
+
|------|-------------|---------|
|
|
24
|
+
| Official | Published under `@chat-adapter/*` by Vercel | Slack, Teams, Discord |
|
|
25
|
+
| Vendor official | Built and maintained by the platform company itself | Resend building a Resend adapter |
|
|
26
|
+
| Community | Built by third-party developers | Any open-source adapter |
|
|
27
|
+
|
|
28
|
+
<Callout type="warn">
|
|
29
|
+
The `@chat-adapter/` npm scope is reserved for official adapters. Publish your adapter under your own scope or as an unscoped package.
|
|
30
|
+
</Callout>
|
|
31
|
+
|
|
32
|
+
#### Qualifications for vendor official tier
|
|
33
|
+
|
|
34
|
+
- Committment for continued maintenance of the adapter.
|
|
35
|
+
- GitHub hosting in official vendor-owned org.
|
|
36
|
+
- Documentation of the adapter in primary vendor docs.
|
|
37
|
+
- Announcement of the adapter in blog post or changelog and social media.
|
|
38
|
+
|
|
39
|
+
## Project setup
|
|
40
|
+
|
|
41
|
+
This guide uses a hypothetical **Matrix** adapter as a running example. Replace "matrix" with your platform name throughout.
|
|
42
|
+
|
|
43
|
+
### package.json
|
|
44
|
+
|
|
45
|
+
```json title="package.json" lineNumbers
|
|
46
|
+
{
|
|
47
|
+
"name": "chat-adapter-matrix",
|
|
48
|
+
"version": "0.1.0",
|
|
49
|
+
"description": "Matrix adapter for Chat SDK",
|
|
50
|
+
"type": "module",
|
|
51
|
+
"main": "./dist/index.js",
|
|
52
|
+
"module": "./dist/index.js",
|
|
53
|
+
"types": "./dist/index.d.ts",
|
|
54
|
+
"exports": {
|
|
55
|
+
".": {
|
|
56
|
+
"types": "./dist/index.d.ts",
|
|
57
|
+
"import": "./dist/index.js"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"files": ["dist"],
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "tsup",
|
|
63
|
+
"dev": "tsup --watch",
|
|
64
|
+
"test": "vitest run --coverage",
|
|
65
|
+
"test:watch": "vitest",
|
|
66
|
+
"typecheck": "tsc --noEmit",
|
|
67
|
+
"clean": "rm -rf dist"
|
|
68
|
+
},
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"chat": "^4.0.0"
|
|
71
|
+
},
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"@chat-adapter/shared": "^4.0.0"
|
|
74
|
+
},
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@types/node": "^22.0.0",
|
|
77
|
+
"chat": "^4.0.0",
|
|
78
|
+
"tsup": "^8.3.0",
|
|
79
|
+
"typescript": "^5.7.0",
|
|
80
|
+
"vitest": "^4.0.0"
|
|
81
|
+
},
|
|
82
|
+
"publishConfig": {
|
|
83
|
+
"access": "public"
|
|
84
|
+
},
|
|
85
|
+
"keywords": ["chat-sdk", "chat-adapter", "matrix"],
|
|
86
|
+
"license": "MIT"
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Key points:
|
|
91
|
+
- ESM-only (`"type": "module"`)
|
|
92
|
+
- `chat` is a **peer dependency** — your adapter runs inside the consumer's Chat instance
|
|
93
|
+
- `@chat-adapter/shared` provides error classes and utility functions
|
|
94
|
+
|
|
95
|
+
### tsup.config.ts
|
|
96
|
+
|
|
97
|
+
```typescript title="tsup.config.ts" lineNumbers
|
|
98
|
+
import { defineConfig } from "tsup";
|
|
99
|
+
|
|
100
|
+
export default defineConfig({
|
|
101
|
+
entry: ["src/index.ts"],
|
|
102
|
+
format: ["esm"],
|
|
103
|
+
dts: true,
|
|
104
|
+
clean: true,
|
|
105
|
+
sourcemap: true,
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### tsconfig.json
|
|
110
|
+
|
|
111
|
+
```json title="tsconfig.json" lineNumbers
|
|
112
|
+
{
|
|
113
|
+
"compilerOptions": {
|
|
114
|
+
"target": "ES2022",
|
|
115
|
+
"module": "ESNext",
|
|
116
|
+
"moduleResolution": "bundler",
|
|
117
|
+
"declaration": true,
|
|
118
|
+
"outDir": "./dist",
|
|
119
|
+
"rootDir": "./src",
|
|
120
|
+
"strict": true,
|
|
121
|
+
"strictNullChecks": true,
|
|
122
|
+
"esModuleInterop": true,
|
|
123
|
+
"skipLibCheck": true
|
|
124
|
+
},
|
|
125
|
+
"include": ["src/**/*"],
|
|
126
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### vitest.config.ts
|
|
131
|
+
|
|
132
|
+
```typescript title="vitest.config.ts" lineNumbers
|
|
133
|
+
import { defineProject } from "vitest/config";
|
|
134
|
+
|
|
135
|
+
export default defineProject({
|
|
136
|
+
test: {
|
|
137
|
+
globals: true,
|
|
138
|
+
environment: "node",
|
|
139
|
+
coverage: {
|
|
140
|
+
provider: "v8",
|
|
141
|
+
reporter: ["text", "json-summary"],
|
|
142
|
+
include: ["src/**/*.ts"],
|
|
143
|
+
exclude: ["src/**/*.test.ts"],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Define your types
|
|
150
|
+
|
|
151
|
+
Start by defining the platform-specific types your adapter needs.
|
|
152
|
+
|
|
153
|
+
```typescript title="src/types.ts" lineNumbers
|
|
154
|
+
/** Decoded thread ID components for Matrix */
|
|
155
|
+
export interface MatrixThreadId {
|
|
156
|
+
/** Matrix room ID (e.g., "!abc123:matrix.org") */
|
|
157
|
+
roomId: string;
|
|
158
|
+
/** Matrix event ID for the thread root (e.g., "$event123") */
|
|
159
|
+
eventId?: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Configuration for the Matrix adapter */
|
|
163
|
+
export interface MatrixAdapterConfig {
|
|
164
|
+
/** Matrix homeserver URL */
|
|
165
|
+
homeserverUrl: string;
|
|
166
|
+
/** Access token for the bot account */
|
|
167
|
+
accessToken: string;
|
|
168
|
+
/** Optional bot display name override */
|
|
169
|
+
userName?: string;
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Every adapter needs:
|
|
174
|
+
1. A **thread ID interface** — the decoded components of your `{adapter}:{segment1}:{segment2}` thread ID
|
|
175
|
+
2. A **config interface** — credentials and options needed to connect to the platform
|
|
176
|
+
|
|
177
|
+
## Implement the Adapter interface
|
|
178
|
+
|
|
179
|
+
Create your adapter class implementing the `Adapter` interface from `chat`. The following sections walk through each group of methods you need to implement.
|
|
180
|
+
|
|
181
|
+
Start with the class skeleton and constructor:
|
|
182
|
+
|
|
183
|
+
```typescript title="src/adapter.ts" lineNumbers
|
|
184
|
+
import {
|
|
185
|
+
extractCard,
|
|
186
|
+
extractFiles,
|
|
187
|
+
toBuffer,
|
|
188
|
+
ValidationError,
|
|
189
|
+
} from "@chat-adapter/shared";
|
|
190
|
+
import type {
|
|
191
|
+
Adapter,
|
|
192
|
+
AdapterPostableMessage,
|
|
193
|
+
ChatInstance,
|
|
194
|
+
EmojiValue,
|
|
195
|
+
FetchOptions,
|
|
196
|
+
FetchResult,
|
|
197
|
+
FormattedContent,
|
|
198
|
+
Logger,
|
|
199
|
+
RawMessage,
|
|
200
|
+
ThreadInfo,
|
|
201
|
+
WebhookOptions,
|
|
202
|
+
} from "chat";
|
|
203
|
+
import { ConsoleLogger, Message } from "chat";
|
|
204
|
+
import { MatrixFormatConverter } from "./format-converter";
|
|
205
|
+
import type { MatrixAdapterConfig, MatrixThreadId } from "./types";
|
|
206
|
+
|
|
207
|
+
export class MatrixAdapter implements Adapter<MatrixThreadId, unknown> {
|
|
208
|
+
readonly name = "matrix";
|
|
209
|
+
readonly userName: string;
|
|
210
|
+
readonly botUserId?: string;
|
|
211
|
+
|
|
212
|
+
private chat: ChatInstance | null = null;
|
|
213
|
+
private logger: Logger;
|
|
214
|
+
private config: MatrixAdapterConfig;
|
|
215
|
+
private converter = new MatrixFormatConverter();
|
|
216
|
+
|
|
217
|
+
constructor(config: MatrixAdapterConfig & { logger?: Logger }) {
|
|
218
|
+
this.config = config;
|
|
219
|
+
this.userName = config.userName ?? "matrix-bot";
|
|
220
|
+
this.logger = config.logger ?? new ConsoleLogger();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Methods shown in sections below...
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The `Adapter` interface takes two generics: `TThreadId` (your decoded thread ID shape) and `TRawMessage` (the platform's raw message type).
|
|
228
|
+
|
|
229
|
+
### Initialization
|
|
230
|
+
|
|
231
|
+
The SDK calls `initialize` once when the `Chat` instance is created. Use it to store the `ChatInstance` reference, set up your logger, validate credentials, and fetch bot info.
|
|
232
|
+
|
|
233
|
+
```typescript title="src/adapter.ts"
|
|
234
|
+
async initialize(chat: ChatInstance): Promise<void> {
|
|
235
|
+
this.chat = chat;
|
|
236
|
+
this.logger = chat.getLogger("matrix");
|
|
237
|
+
|
|
238
|
+
// Validate credentials, fetch bot user info, etc.
|
|
239
|
+
// Example: const me = await this.apiCall("/account/whoami");
|
|
240
|
+
// this.botUserId = me.user_id;
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Thread ID encode/decode
|
|
245
|
+
|
|
246
|
+
Thread IDs typically follow the pattern `{adapter}:{segment1}:{segment2}`, though some adapters use more or fewer segments. The `encodeThreadId` and `decodeThreadId` methods must roundtrip consistently. Use `base64url` encoding for segments that contain special characters.
|
|
247
|
+
|
|
248
|
+
```typescript title="src/adapter.ts" lineNumbers
|
|
249
|
+
encodeThreadId(data: MatrixThreadId): string {
|
|
250
|
+
const roomSegment = Buffer.from(data.roomId).toString("base64url");
|
|
251
|
+
if (data.eventId) {
|
|
252
|
+
const eventSegment = Buffer.from(data.eventId).toString("base64url");
|
|
253
|
+
return `matrix:${roomSegment}:${eventSegment}`;
|
|
254
|
+
}
|
|
255
|
+
return `matrix:${roomSegment}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
decodeThreadId(threadId: string): MatrixThreadId {
|
|
259
|
+
const parts = threadId.split(":");
|
|
260
|
+
if (parts.length < 2 || parts[0] !== "matrix") {
|
|
261
|
+
throw new ValidationError(`Invalid Matrix thread ID: ${threadId}`);
|
|
262
|
+
}
|
|
263
|
+
const roomId = Buffer.from(parts[1], "base64url").toString();
|
|
264
|
+
const eventId = parts[2]
|
|
265
|
+
? Buffer.from(parts[2], "base64url").toString()
|
|
266
|
+
: undefined;
|
|
267
|
+
return { roomId, eventId };
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Webhook handling
|
|
272
|
+
|
|
273
|
+
`handleWebhook` is the entry point for all incoming platform events. Always:
|
|
274
|
+
1. Verify the request signature first (return 401 if invalid)
|
|
275
|
+
2. Parse the platform payload
|
|
276
|
+
3. Call `this.chat.processMessage()` with positional args — it handles `waitUntil` internally
|
|
277
|
+
4. Return a fast 200 response immediately
|
|
278
|
+
|
|
279
|
+
```typescript title="src/adapter.ts" lineNumbers
|
|
280
|
+
async handleWebhook(
|
|
281
|
+
request: Request,
|
|
282
|
+
options?: WebhookOptions
|
|
283
|
+
): Promise<Response> {
|
|
284
|
+
// 1. Verify request signature
|
|
285
|
+
const signature = request.headers.get("x-matrix-signature");
|
|
286
|
+
if (!signature) {
|
|
287
|
+
return new Response("Missing signature", { status: 401 });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const body = await request.text();
|
|
291
|
+
const isValid = this.verifySignature(body, signature);
|
|
292
|
+
if (!isValid) {
|
|
293
|
+
return new Response("Invalid signature", { status: 401 });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 2. Parse the webhook payload
|
|
297
|
+
const payload = JSON.parse(body);
|
|
298
|
+
|
|
299
|
+
// 3. Process the message asynchronously
|
|
300
|
+
if (this.chat && payload.type === "m.room.message") {
|
|
301
|
+
const threadId = this.encodeThreadId({
|
|
302
|
+
roomId: payload.room_id,
|
|
303
|
+
eventId: payload.thread_root_id,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Use a factory function for lazy async parsing
|
|
307
|
+
const isMention = this.checkMention(payload);
|
|
308
|
+
const factory = async (): Promise<Message<unknown>> => {
|
|
309
|
+
const msg = this.parseMessage(payload);
|
|
310
|
+
if (isMention) {
|
|
311
|
+
msg.isMention = true;
|
|
312
|
+
}
|
|
313
|
+
return msg;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// processMessage handles waitUntil registration internally
|
|
317
|
+
this.chat.processMessage(this, threadId, factory, options);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 4. Return a fast 200 to acknowledge receipt
|
|
321
|
+
return new Response("OK", { status: 200 });
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Message parsing
|
|
326
|
+
|
|
327
|
+
Convert the raw platform message into a normalized `Message` instance. The `author` fields use `userId` and `userName`, and `isBot` accepts `boolean | "unknown"`. Include a `metadata` object with `dateSent` and `edited` instead of a top-level `createdAt`.
|
|
328
|
+
|
|
329
|
+
```typescript title="src/adapter.ts" lineNumbers
|
|
330
|
+
parseMessage(raw: unknown): Message<unknown> {
|
|
331
|
+
const payload = raw as Record<string, unknown>;
|
|
332
|
+
|
|
333
|
+
return new Message({
|
|
334
|
+
id: payload.event_id as string,
|
|
335
|
+
threadId: this.encodeThreadId({
|
|
336
|
+
roomId: payload.room_id as string,
|
|
337
|
+
eventId: payload.thread_root_id as string | undefined,
|
|
338
|
+
}),
|
|
339
|
+
text: payload.body as string,
|
|
340
|
+
formatted: this.converter.toAst(payload.body as string),
|
|
341
|
+
raw,
|
|
342
|
+
author: {
|
|
343
|
+
userId: payload.sender as string,
|
|
344
|
+
userName: payload.sender as string,
|
|
345
|
+
fullName: payload.sender_display_name as string ?? "",
|
|
346
|
+
isBot: (payload.sender as string).startsWith("@bot"),
|
|
347
|
+
isMe: false,
|
|
348
|
+
},
|
|
349
|
+
metadata: {
|
|
350
|
+
dateSent: new Date(payload.origin_server_ts as number),
|
|
351
|
+
edited: false,
|
|
352
|
+
},
|
|
353
|
+
attachments: [],
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Sending messages
|
|
359
|
+
|
|
360
|
+
Use `extractCard()` and `extractFiles()` from `@chat-adapter/shared` to check for rich content. Use your format converter's `renderPostable()` to convert the message to platform format.
|
|
361
|
+
|
|
362
|
+
```typescript title="src/adapter.ts" lineNumbers
|
|
363
|
+
async postMessage(
|
|
364
|
+
threadId: string,
|
|
365
|
+
message: AdapterPostableMessage
|
|
366
|
+
): Promise<RawMessage<unknown>> {
|
|
367
|
+
const { roomId, eventId } = this.decodeThreadId(threadId);
|
|
368
|
+
|
|
369
|
+
const card = extractCard(message);
|
|
370
|
+
const files = extractFiles(message);
|
|
371
|
+
|
|
372
|
+
// Upload files if present
|
|
373
|
+
for (const file of files) {
|
|
374
|
+
const buffer = await toBuffer(file.data);
|
|
375
|
+
// Upload to Matrix media repo...
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Render text content
|
|
379
|
+
const text = card
|
|
380
|
+
? this.converter.renderPostable({ card: message.card })
|
|
381
|
+
: this.converter.renderPostable(message);
|
|
382
|
+
|
|
383
|
+
const response = await this.sendMatrixMessage(roomId, text, eventId);
|
|
384
|
+
return { raw: response, id: response.event_id };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async editMessage(
|
|
388
|
+
threadId: string,
|
|
389
|
+
messageId: string,
|
|
390
|
+
message: AdapterPostableMessage
|
|
391
|
+
): Promise<RawMessage<unknown>> {
|
|
392
|
+
const { roomId } = this.decodeThreadId(threadId);
|
|
393
|
+
const text = this.converter.renderPostable(message);
|
|
394
|
+
const response = await this.editMatrixMessage(roomId, messageId, text);
|
|
395
|
+
return { raw: response, id: response.event_id };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async deleteMessage(threadId: string, messageId: string): Promise<void> {
|
|
399
|
+
const { roomId } = this.decodeThreadId(threadId);
|
|
400
|
+
await this.redactMatrixEvent(roomId, messageId);
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Reactions
|
|
405
|
+
|
|
406
|
+
Handle both `EmojiValue` objects and plain strings. `EmojiValue` has a `name` property and `toString()` method — there is no `unicode` field.
|
|
407
|
+
|
|
408
|
+
```typescript title="src/adapter.ts" lineNumbers
|
|
409
|
+
async addReaction(
|
|
410
|
+
threadId: string,
|
|
411
|
+
messageId: string,
|
|
412
|
+
emoji: EmojiValue | string
|
|
413
|
+
): Promise<void> {
|
|
414
|
+
const { roomId } = this.decodeThreadId(threadId);
|
|
415
|
+
const emojiStr = typeof emoji === "string" ? emoji : emoji.name;
|
|
416
|
+
await this.sendReaction(roomId, messageId, emojiStr);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async removeReaction(
|
|
420
|
+
threadId: string,
|
|
421
|
+
messageId: string,
|
|
422
|
+
emoji: EmojiValue | string
|
|
423
|
+
): Promise<void> {
|
|
424
|
+
const { roomId } = this.decodeThreadId(threadId);
|
|
425
|
+
const emojiStr = typeof emoji === "string" ? emoji : emoji.name;
|
|
426
|
+
await this.removeMatrixReaction(roomId, messageId, emojiStr);
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Fetching and typing
|
|
431
|
+
|
|
432
|
+
`fetchMessages` should return messages in chronological order (oldest first). The `nextCursor` enables pagination.
|
|
433
|
+
|
|
434
|
+
```typescript title="src/adapter.ts" lineNumbers
|
|
435
|
+
async fetchMessages(
|
|
436
|
+
threadId: string,
|
|
437
|
+
options?: FetchOptions
|
|
438
|
+
): Promise<FetchResult<unknown>> {
|
|
439
|
+
const { roomId } = this.decodeThreadId(threadId);
|
|
440
|
+
// Fetch from platform API with pagination
|
|
441
|
+
return { messages: [], nextCursor: undefined };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async fetchThread(threadId: string): Promise<ThreadInfo> {
|
|
445
|
+
const { roomId } = this.decodeThreadId(threadId);
|
|
446
|
+
return {
|
|
447
|
+
id: threadId,
|
|
448
|
+
title: undefined,
|
|
449
|
+
createdAt: new Date(),
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async startTyping(threadId: string): Promise<void> {
|
|
454
|
+
const { roomId } = this.decodeThreadId(threadId);
|
|
455
|
+
// Send typing notification via platform API
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Formatting
|
|
460
|
+
|
|
461
|
+
Delegate to your format converter (covered in the next section).
|
|
462
|
+
|
|
463
|
+
```typescript title="src/adapter.ts"
|
|
464
|
+
renderFormatted(content: FormattedContent): string {
|
|
465
|
+
return this.converter.fromAst(content.ast);
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
## Build a format converter
|
|
470
|
+
|
|
471
|
+
Each adapter needs a format converter that translates between the platform's text format and mdast (Markdown AST), the canonical format used by Chat SDK.
|
|
472
|
+
|
|
473
|
+
```typescript title="src/format-converter.ts" lineNumbers
|
|
474
|
+
import {
|
|
475
|
+
BaseFormatConverter,
|
|
476
|
+
type Root,
|
|
477
|
+
parseMarkdown,
|
|
478
|
+
stringifyMarkdown,
|
|
479
|
+
text,
|
|
480
|
+
strong,
|
|
481
|
+
emphasis,
|
|
482
|
+
inlineCode,
|
|
483
|
+
codeBlock,
|
|
484
|
+
link,
|
|
485
|
+
paragraph,
|
|
486
|
+
root,
|
|
487
|
+
} from "chat";
|
|
488
|
+
import type { AdapterPostableMessage } from "chat";
|
|
489
|
+
|
|
490
|
+
export class MatrixFormatConverter extends BaseFormatConverter {
|
|
491
|
+
/**
|
|
492
|
+
* Convert platform text to mdast AST.
|
|
493
|
+
* If your platform uses standard markdown, just use parseMarkdown().
|
|
494
|
+
*/
|
|
495
|
+
toAst(platformText: string): Root {
|
|
496
|
+
// Matrix supports standard markdown, so we can parse directly
|
|
497
|
+
return parseMarkdown(platformText);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Convert mdast AST to platform text format.
|
|
502
|
+
* Walk the AST and produce platform-specific markup.
|
|
503
|
+
*/
|
|
504
|
+
fromAst(ast: Root): string {
|
|
505
|
+
// Matrix supports standard markdown, so we can stringify directly
|
|
506
|
+
return stringifyMarkdown(ast);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Override renderPostable only if your platform needs custom rendering
|
|
511
|
+
* (e.g., converting @mentions to platform-specific syntax).
|
|
512
|
+
* The base class already handles text/formatted/card fallback logic.
|
|
513
|
+
*/
|
|
514
|
+
renderPostable(message: AdapterPostableMessage): string {
|
|
515
|
+
// Example: convert @mention syntax to Matrix pill format
|
|
516
|
+
const rendered = super.renderPostable(message);
|
|
517
|
+
return rendered.replace(
|
|
518
|
+
/@(\w+)/g,
|
|
519
|
+
(_, name) => `<a href="https://matrix.to/#/@${name}:matrix.org">@${name}</a>`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
For platforms with non-standard formatting (e.g., Slack's `mrkdwn`), implement custom parsing in `toAst()` and rendering in `fromAst()`. See the [Discord adapter](https://github.com/vercel/chat/blob/main/packages/adapter-discord/src/markdown.ts) for an example of handling platform-specific mention syntax.
|
|
526
|
+
|
|
527
|
+
## Optional methods
|
|
528
|
+
|
|
529
|
+
These methods are not required but extend your adapter's capabilities:
|
|
530
|
+
|
|
531
|
+
| Method | Purpose |
|
|
532
|
+
|--------|---------|
|
|
533
|
+
| `openDM(userId)` | Open a direct message conversation |
|
|
534
|
+
| `isDM(threadId)` | Check if a thread is a DM |
|
|
535
|
+
| `stream(threadId, textStream)` | Stream AI responses in real-time |
|
|
536
|
+
| `openModal(triggerId, modal)` | Open a modal/dialog form |
|
|
537
|
+
| `postEphemeral(threadId, userId, message)` | Post a message visible to one user |
|
|
538
|
+
| `postChannelMessage(channelId, message)` | Post a top-level message (not in a thread) |
|
|
539
|
+
| `onThreadSubscribe(threadId)` | Hook for platform-specific subscription setup |
|
|
540
|
+
| `fetchChannelInfo(channelId)` | Fetch channel metadata |
|
|
541
|
+
| `listThreads(channelId)` | List threads in a channel |
|
|
542
|
+
| `fetchMessage(threadId, messageId)` | Fetch a single message by ID |
|
|
543
|
+
| `fetchChannelMessages(channelId)` | Fetch top-level channel messages |
|
|
544
|
+
| `channelIdFromThreadId(threadId)` | Extract channel ID from a thread ID |
|
|
545
|
+
|
|
546
|
+
Implement only the methods your platform supports. The SDK gracefully handles missing optional methods.
|
|
547
|
+
|
|
548
|
+
## Factory function
|
|
549
|
+
|
|
550
|
+
Export a factory function that creates your adapter with environment variable fallbacks:
|
|
551
|
+
|
|
552
|
+
```typescript title="src/factory.ts" lineNumbers
|
|
553
|
+
import { ConsoleLogger } from "chat";
|
|
554
|
+
import type { Logger } from "chat";
|
|
555
|
+
import { ValidationError } from "@chat-adapter/shared";
|
|
556
|
+
import { MatrixAdapter } from "./adapter";
|
|
557
|
+
import type { MatrixAdapterConfig } from "./types";
|
|
558
|
+
|
|
559
|
+
export function createMatrixAdapter(
|
|
560
|
+
config?: Partial<MatrixAdapterConfig> & { logger?: Logger }
|
|
561
|
+
): MatrixAdapter {
|
|
562
|
+
const homeserverUrl =
|
|
563
|
+
config?.homeserverUrl ?? process.env.MATRIX_HOMESERVER_URL;
|
|
564
|
+
const accessToken =
|
|
565
|
+
config?.accessToken ?? process.env.MATRIX_ACCESS_TOKEN;
|
|
566
|
+
|
|
567
|
+
if (!homeserverUrl) {
|
|
568
|
+
throw new ValidationError(
|
|
569
|
+
"Matrix homeserver URL is required. Pass it in config or set MATRIX_HOMESERVER_URL."
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
if (!accessToken) {
|
|
573
|
+
throw new ValidationError(
|
|
574
|
+
"Matrix access token is required. Pass it in config or set MATRIX_ACCESS_TOKEN."
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return new MatrixAdapter({
|
|
579
|
+
homeserverUrl,
|
|
580
|
+
accessToken,
|
|
581
|
+
userName: config?.userName,
|
|
582
|
+
logger: config?.logger,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
Then export both the class and factory from your entry point:
|
|
588
|
+
|
|
589
|
+
```typescript title="src/index.ts" lineNumbers
|
|
590
|
+
export { MatrixAdapter } from "./adapter";
|
|
591
|
+
export { MatrixFormatConverter } from "./format-converter";
|
|
592
|
+
export { createMatrixAdapter } from "./factory";
|
|
593
|
+
export type { MatrixAdapterConfig, MatrixThreadId } from "./types";
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
## Shared utilities
|
|
597
|
+
|
|
598
|
+
The `@chat-adapter/shared` package provides utilities you should use instead of reimplementing:
|
|
599
|
+
|
|
600
|
+
### Error classes
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
import {
|
|
604
|
+
AdapterError, // Base error class
|
|
605
|
+
AdapterRateLimitError, // Platform rate limit hit
|
|
606
|
+
AuthenticationError, // Invalid credentials
|
|
607
|
+
ResourceNotFoundError, // Thread/message not found
|
|
608
|
+
PermissionError, // Insufficient permissions
|
|
609
|
+
ValidationError, // Invalid input
|
|
610
|
+
NetworkError, // HTTP/connection failure
|
|
611
|
+
} from "@chat-adapter/shared";
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
Throw these errors from your adapter methods. The SDK catches and logs them with appropriate context.
|
|
615
|
+
|
|
616
|
+
### Message utilities
|
|
617
|
+
|
|
618
|
+
```typescript
|
|
619
|
+
import {
|
|
620
|
+
extractCard, // Extract CardElement from AdapterPostableMessage
|
|
621
|
+
extractFiles, // Extract FileUpload[] from AdapterPostableMessage
|
|
622
|
+
toBuffer, // Convert FileDataInput to Buffer (async)
|
|
623
|
+
toBufferSync, // Convert FileDataInput to Buffer (sync)
|
|
624
|
+
cardToFallbackText, // Convert card to plain text
|
|
625
|
+
} from "@chat-adapter/shared";
|
|
626
|
+
```
|