chat-adapter-twitter 0.1.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 ADDED
@@ -0,0 +1,94 @@
1
+ # chat-adapter-twitter
2
+
3
+ [![npm version](https://img.shields.io/npm/v/chat-adapter-twitter)](https://www.npmjs.com/package/chat-adapter-twitter)
4
+ [![npm downloads](https://img.shields.io/npm/dm/chat-adapter-twitter)](https://www.npmjs.com/package/chat-adapter-twitter)
5
+
6
+ Twitter / X Webhooks adapter for [Chat SDK](https://chat-sdk.dev/docs).
7
+
8
+ This adapter uses the **X Account Activity API** (Enterprise/Pro tier required) to receive Direct Messages in real-time and the **X API v2** to send responses.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install chat chat-adapter-twitter
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```typescript
19
+ import { Chat } from "chat";
20
+ import { createTwitterAdapter } from "chat-adapter-twitter";
21
+ import { createMemoryState } from "@chat-adapter/state-memory";
22
+
23
+ const bot = new Chat({
24
+ userName: "my_twitter_bot",
25
+ adapters: {
26
+ twitter: createTwitterAdapter({
27
+ consumerKey: process.env.TWITTER_CONSUMER_KEY!,
28
+ consumerSecret: process.env.TWITTER_CONSUMER_SECRET!,
29
+ accessToken: process.env.TWITTER_ACCESS_TOKEN!,
30
+ accessTokenSecret: process.env.TWITTER_ACCESS_TOKEN_SECRET!,
31
+ bearerToken: process.env.TWITTER_BEARER_TOKEN!,
32
+ }),
33
+ },
34
+ state: createMemoryState(), // Required for deduping
35
+ });
36
+
37
+ // Twitter DMs are treated as standard messages (not mentions)
38
+ bot.onNewMessage(async (thread, message) => {
39
+ await thread.post(`Echo: ${message.text}`);
40
+ });
41
+ ```
42
+
43
+ ## Environment variables
44
+
45
+ If you don't pass options into `createTwitterAdapter()`, it will automatically read from these environment variables:
46
+
47
+ | Variable | Required | Description |
48
+ |----------|----------|-------------|
49
+ | `TWITTER_CONSUMER_KEY` | Yes | App API Key |
50
+ | `TWITTER_CONSUMER_SECRET` | Yes | App API Secret |
51
+ | `TWITTER_ACCESS_TOKEN` | Yes | Bot account access token |
52
+ | `TWITTER_ACCESS_TOKEN_SECRET` | Yes | Bot account access token secret |
53
+ | `TWITTER_BEARER_TOKEN` | Yes | App Bearer token (for v2 read endpoints) |
54
+ | `TWITTER_BOT_USERNAME` | No | Override the bot display name |
55
+ | `TWITTER_WEBHOOK_ENV` | No | Account Activity environment name (default: "production") |
56
+
57
+ ## Configuration
58
+
59
+ | Option | Type | Default | Description |
60
+ |--------|------|---------|-------------|
61
+ | `consumerKey` | `string` | `process.env.TWITTER_CONSUMER_KEY` | App API Key |
62
+ | `consumerSecret` | `string` | `process.env.TWITTER_CONSUMER_SECRET` | App API Secret for CRC hashing |
63
+ | `accessToken` | `string` | `process.env.TWITTER_ACCESS_TOKEN` | Bot account access token |
64
+ | `accessTokenSecret` | `string` | `process.env.TWITTER_ACCESS_TOKEN_SECRET` | Bot account access token secret |
65
+ | `bearerToken` | `string` | `process.env.TWITTER_BEARER_TOKEN` | App Bearer token |
66
+ | `userName` | `string` | `process.env.TWITTER_BOT_USERNAME` | Bot display name |
67
+ | `apiBaseUrl` | `string` | `"https://api.twitter.com"` | Override domain for testing |
68
+
69
+ ## Platform setup
70
+
71
+ 1. Create a project in the [X Developer Portal](https://developer.x.com).
72
+ 2. Generate your **Consumer Key**, **Consumer Secret**, and **Bearer Token**.
73
+ 3. Set up **OAuth 1.0a User Authentication** in your app settings with Read/Write/Direct Messages permissions.
74
+ 4. Generate the **Access Token** and **Access Token Secret** for your bot account.
75
+ 5. Apply for the **Account Activity API** (requires Pro or Enterprise access).
76
+ 6. Start your server so the webhook endpoint is active.
77
+ 7. Register your webhook URL and subscribe your bot account using the Account Activity API.
78
+
79
+ ## Features
80
+
81
+ - **Direct Messages**: Receive and reply to 1-1 DMs
82
+ - **CRC Hashing**: Automatically responds to Twitter's Challenge-Response Checks
83
+ - **Media Attachments**: Extracts image and video URLs from incoming DMs
84
+ - **Plain Text Rendering**: Automatically converts markdown AST to plain text (with ASCII tables) since Twitter DMs don't support rich formatting
85
+
86
+ ### Limitations
87
+ - **No Message Editing**: The Twitter API does not support editing DMs. `editMessage` throws `NotImplementedError`.
88
+ - **Typing Indicators**: The X API doesn't support bot typing indicators.
89
+ - **Rate Limits**: The DM API is subject to X's strict rate limits.
90
+ - **Premium Tier Requirement**: Requires Account Activity API access, which is not available on free or basic tiers.
91
+
92
+ ## License
93
+
94
+ MIT
@@ -0,0 +1,327 @@
1
+ import { Logger, BaseFormatConverter, Root, AdapterPostableMessage, Adapter, ChatInstance, WebhookOptions, RawMessage, EmojiValue, FetchOptions, FetchResult, ThreadInfo, Message, FormattedContent } from 'chat';
2
+
3
+ /**
4
+ * Twitter / X adapter types.
5
+ */
6
+
7
+ /**
8
+ * Twitter adapter configuration.
9
+ */
10
+ interface TwitterAdapterConfig {
11
+ /**
12
+ * Twitter API v2 Bearer Token for read-only endpoints.
13
+ * Defaults to TWITTER_BEARER_TOKEN env var.
14
+ */
15
+ bearerToken?: string;
16
+ /**
17
+ * Twitter API consumer key (API Key).
18
+ * Used for OAuth 1.0a signing and CRC validation.
19
+ * Defaults to TWITTER_CONSUMER_KEY env var.
20
+ */
21
+ consumerKey?: string;
22
+ /**
23
+ * Twitter API consumer secret (API Secret).
24
+ * Used for CRC HMAC-SHA256 computation.
25
+ * Defaults to TWITTER_CONSUMER_SECRET env var.
26
+ */
27
+ consumerSecret?: string;
28
+ /**
29
+ * OAuth 1.0a access token for the bot account.
30
+ * Defaults to TWITTER_ACCESS_TOKEN env var.
31
+ */
32
+ accessToken?: string;
33
+ /**
34
+ * OAuth 1.0a access token secret for the bot account.
35
+ * Defaults to TWITTER_ACCESS_TOKEN_SECRET env var.
36
+ */
37
+ accessTokenSecret?: string;
38
+ /** Logger instance for error reporting. Defaults to ConsoleLogger. */
39
+ logger?: Logger;
40
+ /** Override bot username (optional). Defaults to TWITTER_BOT_USERNAME env var. */
41
+ userName?: string;
42
+ /**
43
+ * Optional custom API base URL (defaults to https://api.twitter.com).
44
+ * Useful for testing with a mock server.
45
+ */
46
+ apiBaseUrl?: string;
47
+ /**
48
+ * Optional webhook environment name for Account Activity API.
49
+ * Defaults to TWITTER_WEBHOOK_ENV env var or "production".
50
+ */
51
+ webhookEnvironment?: string;
52
+ }
53
+ /**
54
+ * Twitter thread ID components.
55
+ * A "thread" in Twitter maps to a DM conversation.
56
+ */
57
+ interface TwitterThreadId {
58
+ /** DM conversation ID */
59
+ conversationId: string;
60
+ }
61
+ /**
62
+ * Twitter user object from the Account Activity API webhook payload.
63
+ */
64
+ interface TwitterUser {
65
+ id: string;
66
+ created_timestamp: string;
67
+ name: string;
68
+ screen_name: string;
69
+ location?: string;
70
+ description?: string;
71
+ url?: string;
72
+ protected: boolean;
73
+ verified: boolean;
74
+ followers_count: number;
75
+ friends_count: number;
76
+ statuses_count: number;
77
+ profile_image_url_https?: string;
78
+ }
79
+ /**
80
+ * Twitter DM event message data.
81
+ */
82
+ interface TwitterMessageData {
83
+ text: string;
84
+ entities?: {
85
+ hashtags: TwitterEntity[];
86
+ symbols: TwitterEntity[];
87
+ user_mentions: TwitterMentionEntity[];
88
+ urls: TwitterUrlEntity[];
89
+ };
90
+ attachment?: {
91
+ type: string;
92
+ media: TwitterMedia;
93
+ };
94
+ }
95
+ interface TwitterEntity {
96
+ text?: string;
97
+ indices: [number, number];
98
+ }
99
+ interface TwitterMentionEntity extends TwitterEntity {
100
+ id: number;
101
+ id_str: string;
102
+ name: string;
103
+ screen_name: string;
104
+ }
105
+ interface TwitterUrlEntity extends TwitterEntity {
106
+ url: string;
107
+ expanded_url: string;
108
+ display_url: string;
109
+ }
110
+ interface TwitterMedia {
111
+ id: number;
112
+ id_str: string;
113
+ media_url: string;
114
+ media_url_https: string;
115
+ type: string;
116
+ sizes?: Record<string, {
117
+ w: number;
118
+ h: number;
119
+ resize: string;
120
+ }>;
121
+ }
122
+ /**
123
+ * A single DM event within the Account Activity webhook payload.
124
+ */
125
+ interface TwitterDirectMessageEvent {
126
+ type: "message_create";
127
+ id: string;
128
+ created_timestamp: string;
129
+ message_create: {
130
+ target: {
131
+ recipient_id: string;
132
+ };
133
+ sender_id: string;
134
+ source_app_id?: string;
135
+ message_data: TwitterMessageData;
136
+ };
137
+ }
138
+ /**
139
+ * Twitter API v2 DM event from GET /2/direct_messages/events.
140
+ */
141
+ interface TwitterDMEventV2 {
142
+ id: string;
143
+ event_type: "MessageCreate" | "ParticipantsJoin" | "ParticipantsLeave";
144
+ text: string;
145
+ dm_conversation_id?: string;
146
+ created_at?: string;
147
+ sender_id?: string;
148
+ participant_ids?: string[];
149
+ attachments?: {
150
+ media_keys?: string[];
151
+ };
152
+ referenced_tweets?: Array<{
153
+ id: string;
154
+ type: string;
155
+ }>;
156
+ }
157
+ /**
158
+ * Response envelope for Twitter API v2 DM send.
159
+ */
160
+ interface TwitterDMSendResponse {
161
+ dm_conversation_id: string;
162
+ dm_event_id: string;
163
+ }
164
+ /**
165
+ * Twitter API v2 user object.
166
+ */
167
+ interface TwitterUserV2 {
168
+ id: string;
169
+ name: string;
170
+ username: string;
171
+ profile_image_url?: string;
172
+ verified?: boolean;
173
+ }
174
+ /**
175
+ * Full webhook payload from Account Activity API.
176
+ * Can contain DM events, tweet events, follow events, etc.
177
+ * We focus on DM events for the adapter.
178
+ */
179
+ interface TwitterAccountActivityPayload {
180
+ for_user_id: string;
181
+ direct_message_events?: TwitterDirectMessageEvent[];
182
+ users?: Record<string, TwitterUser>;
183
+ apps?: Record<string, {
184
+ id: string;
185
+ name: string;
186
+ url: string;
187
+ }>;
188
+ tweet_create_events?: unknown[];
189
+ favorite_events?: unknown[];
190
+ follow_events?: unknown[];
191
+ block_events?: unknown[];
192
+ mute_events?: unknown[];
193
+ }
194
+ /**
195
+ * Raw message type for the Twitter adapter.
196
+ * This is the DM event as received from the webhook.
197
+ */
198
+ type TwitterRawMessage = TwitterDirectMessageEvent;
199
+
200
+ /**
201
+ * Twitter / X format conversion.
202
+ *
203
+ * Twitter DMs use plain text with entity annotations (similar to Telegram).
204
+ * For outbound messages, we send plain text since the DM API doesn't
205
+ * support rich formatting (bold/italic/etc.) natively.
206
+ */
207
+
208
+ declare class TwitterFormatConverter extends BaseFormatConverter {
209
+ /**
210
+ * Convert platform text to mdast AST.
211
+ * Twitter DMs are plain text — we parse as if they were markdown
212
+ * so that any user-typed markdown is preserved in the AST.
213
+ */
214
+ toAst(text: string): Root;
215
+ /**
216
+ * Convert mdast AST to platform text format.
217
+ * Twitter DMs don't support rich formatting, so we try to
218
+ * produce readable plain text. Tables get converted to ASCII art.
219
+ */
220
+ fromAst(ast: Root): string;
221
+ renderPostable(message: AdapterPostableMessage): string;
222
+ }
223
+
224
+ declare class TwitterAdapter implements Adapter<TwitterThreadId, TwitterRawMessage> {
225
+ readonly name = "twitter";
226
+ readonly persistMessageHistory = true;
227
+ private readonly consumerKey;
228
+ private readonly consumerSecret;
229
+ private readonly accessToken;
230
+ private readonly accessTokenSecret;
231
+ private readonly bearerToken;
232
+ private readonly apiBaseUrl;
233
+ private readonly webhookEnvironment;
234
+ private readonly logger;
235
+ private readonly formatConverter;
236
+ private readonly messageCache;
237
+ private chat;
238
+ private _botUserId?;
239
+ private _userName;
240
+ private readonly hasExplicitUserName;
241
+ get botUserId(): string | undefined;
242
+ get userName(): string;
243
+ constructor(config?: TwitterAdapterConfig);
244
+ initialize(chat: ChatInstance): Promise<void>;
245
+ /**
246
+ * Handle incoming webhook requests from the X Account Activity API.
247
+ *
248
+ * GET requests are CRC challenges — we respond with the HMAC-SHA256 hash.
249
+ * POST requests are webhook events (DMs, tweets, etc.).
250
+ */
251
+ handleWebhook(request: Request, options?: WebhookOptions): Promise<Response>;
252
+ /**
253
+ * Handle CRC challenge from the X Account Activity API.
254
+ * Responds with HMAC-SHA256 hash of the crc_token using consumer secret.
255
+ */
256
+ private handleCrcChallenge;
257
+ /**
258
+ * Process DM events from the Account Activity webhook payload.
259
+ */
260
+ private processDMEvents;
261
+ /**
262
+ * Derive a deterministic conversation ID from two user IDs.
263
+ * Twitter's 1:1 DM conversation IDs are formed by sorting the two user
264
+ * IDs and joining them. For simplicity, we use the smaller ID first.
265
+ */
266
+ private deriveConversationId;
267
+ /**
268
+ * Parse a Twitter DM event into a normalized Message.
269
+ */
270
+ private parseDMEvent;
271
+ /**
272
+ * Extract attachments from a DM event.
273
+ */
274
+ private extractAttachments;
275
+ /**
276
+ * Check if the bot is mentioned in the text.
277
+ */
278
+ private checkMention;
279
+ postMessage(threadId: string, message: AdapterPostableMessage): Promise<RawMessage<TwitterRawMessage>>;
280
+ editMessage(_threadId: string, _messageId: string, _message: AdapterPostableMessage): Promise<RawMessage<TwitterRawMessage>>;
281
+ deleteMessage(_threadId: string, messageId: string): Promise<void>;
282
+ addReaction(_threadId: string, _messageId: string, _emoji: EmojiValue | string): Promise<void>;
283
+ removeReaction(_threadId: string, _messageId: string, _emoji: EmojiValue | string): Promise<void>;
284
+ startTyping(_threadId: string): Promise<void>;
285
+ fetchMessages(threadId: string, options?: FetchOptions): Promise<FetchResult<TwitterRawMessage>>;
286
+ fetchThread(threadId: string): Promise<ThreadInfo>;
287
+ channelIdFromThreadId(threadId: string): string;
288
+ openDM(userId: string): Promise<string>;
289
+ isDM(_threadId: string): boolean;
290
+ encodeThreadId(platformData: TwitterThreadId): string;
291
+ decodeThreadId(threadId: string): TwitterThreadId;
292
+ parseMessage(raw: TwitterRawMessage): Message<TwitterRawMessage>;
293
+ renderFormatted(content: FormattedContent): string;
294
+ private resolveThreadId;
295
+ /**
296
+ * Get the recipient user ID from a conversation ID.
297
+ * Conversation IDs are in the format `{smallerId}-{largerId}`.
298
+ * The recipient is the ID that is NOT the bot's.
299
+ */
300
+ private getRecipientFromConversation;
301
+ /**
302
+ * Send a DM via the Twitter API v2.
303
+ */
304
+ private sendDM;
305
+ /**
306
+ * Make an authenticated API call using OAuth 1.0a.
307
+ */
308
+ private twitterFetchOAuth;
309
+ /**
310
+ * Make an authenticated API call using Bearer Token (API v2 read endpoints).
311
+ */
312
+ private twitterFetchV2;
313
+ /**
314
+ * Map HTTP status codes to appropriate error classes.
315
+ */
316
+ private throwTwitterApiError;
317
+ private truncateMessage;
318
+ private escapeRegex;
319
+ private cacheMessage;
320
+ private findCachedMessage;
321
+ private deleteCachedMessage;
322
+ private compareMessages;
323
+ private paginateMessages;
324
+ }
325
+ declare function createTwitterAdapter(config?: TwitterAdapterConfig): TwitterAdapter;
326
+
327
+ export { type TwitterAccountActivityPayload, TwitterAdapter, type TwitterAdapterConfig, type TwitterDMEventV2, type TwitterDMSendResponse, type TwitterDirectMessageEvent, TwitterFormatConverter, type TwitterRawMessage, type TwitterThreadId, type TwitterUser, type TwitterUserV2, createTwitterAdapter };
package/dist/index.js ADDED
@@ -0,0 +1,789 @@
1
+ // src/index.ts
2
+ import {
3
+ AdapterRateLimitError,
4
+ AuthenticationError,
5
+ cardToFallbackText,
6
+ extractCard,
7
+ NetworkError,
8
+ PermissionError,
9
+ ResourceNotFoundError,
10
+ ValidationError
11
+ } from "@chat-adapter/shared";
12
+ import { ConsoleLogger, Message, NotImplementedError } from "chat";
13
+
14
+ // src/markdown.ts
15
+ import {
16
+ BaseFormatConverter,
17
+ isTableNode,
18
+ parseMarkdown,
19
+ stringifyMarkdown,
20
+ tableToAscii,
21
+ walkAst
22
+ } from "chat";
23
+ var TwitterFormatConverter = class extends BaseFormatConverter {
24
+ /**
25
+ * Convert platform text to mdast AST.
26
+ * Twitter DMs are plain text — we parse as if they were markdown
27
+ * so that any user-typed markdown is preserved in the AST.
28
+ */
29
+ toAst(text) {
30
+ return parseMarkdown(text);
31
+ }
32
+ /**
33
+ * Convert mdast AST to platform text format.
34
+ * Twitter DMs don't support rich formatting, so we try to
35
+ * produce readable plain text. Tables get converted to ASCII art.
36
+ */
37
+ fromAst(ast) {
38
+ const transformed = walkAst(structuredClone(ast), (node) => {
39
+ if (isTableNode(node)) {
40
+ return {
41
+ type: "code",
42
+ value: tableToAscii(node),
43
+ lang: void 0
44
+ };
45
+ }
46
+ return node;
47
+ });
48
+ return stringifyMarkdown(transformed).trim();
49
+ }
50
+ renderPostable(message) {
51
+ if (typeof message === "string") {
52
+ return message;
53
+ }
54
+ if ("raw" in message) {
55
+ return message.raw;
56
+ }
57
+ if ("markdown" in message) {
58
+ return this.fromMarkdown(message.markdown);
59
+ }
60
+ if ("ast" in message) {
61
+ return this.fromAst(message.ast);
62
+ }
63
+ return super.renderPostable(message);
64
+ }
65
+ };
66
+
67
+ // src/index.ts
68
+ var TWITTER_API_BASE = "https://api.twitter.com";
69
+ var TWITTER_DM_MESSAGE_LIMIT = 1e4;
70
+ var CRC_TOKEN_PARAM = "crc_token";
71
+ async function computeHmacSha256(key, message) {
72
+ const encoder = new TextEncoder();
73
+ const keyData = encoder.encode(key);
74
+ const messageData = encoder.encode(message);
75
+ const cryptoKey = await crypto.subtle.importKey(
76
+ "raw",
77
+ keyData,
78
+ { name: "HMAC", hash: "SHA-256" },
79
+ false,
80
+ ["sign"]
81
+ );
82
+ const signature = await crypto.subtle.sign("HMAC", cryptoKey, messageData);
83
+ return btoa(String.fromCharCode(...new Uint8Array(signature)));
84
+ }
85
+ async function generateOAuth1Header(params) {
86
+ const {
87
+ method,
88
+ url,
89
+ consumerKey,
90
+ consumerSecret,
91
+ accessToken,
92
+ accessTokenSecret,
93
+ additionalParams
94
+ } = params;
95
+ const timestamp = Math.floor(Date.now() / 1e3).toString();
96
+ const nonce = crypto.randomUUID().replace(/-/g, "");
97
+ const oauthParams = {
98
+ oauth_consumer_key: consumerKey,
99
+ oauth_nonce: nonce,
100
+ oauth_signature_method: "HMAC-SHA1",
101
+ oauth_timestamp: timestamp,
102
+ oauth_token: accessToken,
103
+ oauth_version: "1.0",
104
+ ...additionalParams
105
+ };
106
+ const parsedUrl = new URL(url);
107
+ for (const [key, value] of parsedUrl.searchParams.entries()) {
108
+ oauthParams[key] = value;
109
+ }
110
+ const sortedParams = Object.entries(oauthParams).sort(([a], [b]) => a.localeCompare(b)).map(
111
+ ([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
112
+ ).join("&");
113
+ const baseUrl = `${parsedUrl.origin}${parsedUrl.pathname}`;
114
+ const signatureBase = `${method.toUpperCase()}&${encodeURIComponent(baseUrl)}&${encodeURIComponent(sortedParams)}`;
115
+ const signingKey = `${encodeURIComponent(consumerSecret)}&${encodeURIComponent(accessTokenSecret)}`;
116
+ const encoder = new TextEncoder();
117
+ const keyData = encoder.encode(signingKey);
118
+ const messageData = encoder.encode(signatureBase);
119
+ const cryptoKey = await crypto.subtle.importKey(
120
+ "raw",
121
+ keyData,
122
+ { name: "HMAC", hash: "SHA-1" },
123
+ false,
124
+ ["sign"]
125
+ );
126
+ const signature = await crypto.subtle.sign("HMAC", cryptoKey, messageData);
127
+ const signatureBase64 = btoa(
128
+ String.fromCharCode(...new Uint8Array(signature))
129
+ );
130
+ const authParams = {
131
+ oauth_consumer_key: consumerKey,
132
+ oauth_nonce: nonce,
133
+ oauth_signature: signatureBase64,
134
+ oauth_signature_method: "HMAC-SHA1",
135
+ oauth_timestamp: timestamp,
136
+ oauth_token: accessToken,
137
+ oauth_version: "1.0"
138
+ };
139
+ const authHeader = Object.entries(authParams).sort(([a], [b]) => a.localeCompare(b)).map(
140
+ ([key, value]) => `${encodeURIComponent(key)}="${encodeURIComponent(value)}"`
141
+ ).join(", ");
142
+ return `OAuth ${authHeader}`;
143
+ }
144
+ var TwitterAdapter = class {
145
+ name = "twitter";
146
+ persistMessageHistory = true;
147
+ consumerKey;
148
+ consumerSecret;
149
+ accessToken;
150
+ accessTokenSecret;
151
+ bearerToken;
152
+ apiBaseUrl;
153
+ webhookEnvironment;
154
+ logger;
155
+ formatConverter = new TwitterFormatConverter();
156
+ messageCache = /* @__PURE__ */ new Map();
157
+ chat = null;
158
+ _botUserId;
159
+ _userName;
160
+ hasExplicitUserName;
161
+ get botUserId() {
162
+ return this._botUserId;
163
+ }
164
+ get userName() {
165
+ return this._userName;
166
+ }
167
+ constructor(config = {}) {
168
+ const consumerKey = config.consumerKey ?? process.env.TWITTER_CONSUMER_KEY;
169
+ const consumerSecret = config.consumerSecret ?? process.env.TWITTER_CONSUMER_SECRET;
170
+ const accessToken = config.accessToken ?? process.env.TWITTER_ACCESS_TOKEN;
171
+ const accessTokenSecret = config.accessTokenSecret ?? process.env.TWITTER_ACCESS_TOKEN_SECRET;
172
+ const bearerToken = config.bearerToken ?? process.env.TWITTER_BEARER_TOKEN;
173
+ if (!consumerKey) {
174
+ throw new ValidationError(
175
+ "twitter",
176
+ "Consumer key is required. Set TWITTER_CONSUMER_KEY or provide it in config."
177
+ );
178
+ }
179
+ if (!consumerSecret) {
180
+ throw new ValidationError(
181
+ "twitter",
182
+ "Consumer secret is required. Set TWITTER_CONSUMER_SECRET or provide it in config."
183
+ );
184
+ }
185
+ if (!accessToken) {
186
+ throw new ValidationError(
187
+ "twitter",
188
+ "Access token is required. Set TWITTER_ACCESS_TOKEN or provide it in config."
189
+ );
190
+ }
191
+ if (!accessTokenSecret) {
192
+ throw new ValidationError(
193
+ "twitter",
194
+ "Access token secret is required. Set TWITTER_ACCESS_TOKEN_SECRET or provide it in config."
195
+ );
196
+ }
197
+ if (!bearerToken) {
198
+ throw new ValidationError(
199
+ "twitter",
200
+ "Bearer token is required. Set TWITTER_BEARER_TOKEN or provide it in config."
201
+ );
202
+ }
203
+ this.consumerKey = consumerKey;
204
+ this.consumerSecret = consumerSecret;
205
+ this.accessToken = accessToken;
206
+ this.accessTokenSecret = accessTokenSecret;
207
+ this.bearerToken = bearerToken;
208
+ this.apiBaseUrl = (config.apiBaseUrl ?? process.env.TWITTER_API_BASE_URL ?? TWITTER_API_BASE).replace(/\/+$/, "");
209
+ this.webhookEnvironment = config.webhookEnvironment ?? process.env.TWITTER_WEBHOOK_ENV ?? "production";
210
+ this.logger = config.logger ?? new ConsoleLogger("info").child("twitter");
211
+ const userName = config.userName ?? process.env.TWITTER_BOT_USERNAME;
212
+ this._userName = userName ?? "bot";
213
+ this.hasExplicitUserName = Boolean(userName);
214
+ }
215
+ async initialize(chat) {
216
+ this.chat = chat;
217
+ if (!this.hasExplicitUserName) {
218
+ const chatUserName = chat.getUserName?.();
219
+ if (typeof chatUserName === "string" && chatUserName.trim()) {
220
+ this._userName = chatUserName;
221
+ }
222
+ }
223
+ try {
224
+ const me = await this.twitterFetchV2(
225
+ "/2/users/me",
226
+ "GET"
227
+ );
228
+ if (me) {
229
+ this._botUserId = me.id;
230
+ if (!this.hasExplicitUserName && me.username) {
231
+ this._userName = me.username;
232
+ }
233
+ }
234
+ this.logger.info("Twitter adapter initialized", {
235
+ botUserId: this._botUserId,
236
+ userName: this._userName
237
+ });
238
+ } catch (error) {
239
+ this.logger.warn("Failed to fetch Twitter bot identity", {
240
+ error: String(error)
241
+ });
242
+ }
243
+ }
244
+ /**
245
+ * Handle incoming webhook requests from the X Account Activity API.
246
+ *
247
+ * GET requests are CRC challenges — we respond with the HMAC-SHA256 hash.
248
+ * POST requests are webhook events (DMs, tweets, etc.).
249
+ */
250
+ async handleWebhook(request, options) {
251
+ if (request.method === "GET") {
252
+ return this.handleCrcChallenge(request);
253
+ }
254
+ const body = await request.text();
255
+ let payload;
256
+ try {
257
+ payload = JSON.parse(body);
258
+ } catch {
259
+ return new Response("Invalid JSON", { status: 400 });
260
+ }
261
+ if (!this.chat) {
262
+ this.logger.warn(
263
+ "Chat instance not initialized, ignoring Twitter webhook"
264
+ );
265
+ return new Response("OK", { status: 200 });
266
+ }
267
+ try {
268
+ this.processDMEvents(payload, options);
269
+ } catch (error) {
270
+ this.logger.warn("Failed to process Twitter webhook payload", {
271
+ error: String(error),
272
+ forUserId: payload.for_user_id
273
+ });
274
+ }
275
+ return new Response("OK", { status: 200 });
276
+ }
277
+ /**
278
+ * Handle CRC challenge from the X Account Activity API.
279
+ * Responds with HMAC-SHA256 hash of the crc_token using consumer secret.
280
+ */
281
+ async handleCrcChallenge(request) {
282
+ const url = new URL(request.url);
283
+ const crcToken = url.searchParams.get(CRC_TOKEN_PARAM);
284
+ if (!crcToken) {
285
+ return new Response("Missing crc_token parameter", { status: 400 });
286
+ }
287
+ const responseToken = await computeHmacSha256(
288
+ this.consumerSecret,
289
+ crcToken
290
+ );
291
+ return new Response(
292
+ JSON.stringify({
293
+ response_token: `sha256=${responseToken}`
294
+ }),
295
+ {
296
+ status: 200,
297
+ headers: { "Content-Type": "application/json" }
298
+ }
299
+ );
300
+ }
301
+ /**
302
+ * Process DM events from the Account Activity webhook payload.
303
+ */
304
+ processDMEvents(payload, options) {
305
+ if (!this.chat || !payload.direct_message_events) {
306
+ return;
307
+ }
308
+ for (const dmEvent of payload.direct_message_events) {
309
+ if (dmEvent.type !== "message_create") {
310
+ continue;
311
+ }
312
+ const senderId = dmEvent.message_create.sender_id;
313
+ if (senderId === this._botUserId) {
314
+ continue;
315
+ }
316
+ if (senderId === payload.for_user_id && senderId === this._botUserId) {
317
+ continue;
318
+ }
319
+ if (this._botUserId && payload.for_user_id !== this._botUserId) {
320
+ continue;
321
+ }
322
+ const recipientId = dmEvent.message_create.target.recipient_id;
323
+ const conversationId = this.deriveConversationId(senderId, recipientId);
324
+ const threadId = this.encodeThreadId({ conversationId });
325
+ const parsedMessage = this.parseDMEvent(
326
+ dmEvent,
327
+ threadId,
328
+ payload.users
329
+ );
330
+ this.cacheMessage(parsedMessage);
331
+ this.chat.processMessage(this, threadId, parsedMessage, options);
332
+ }
333
+ }
334
+ /**
335
+ * Derive a deterministic conversation ID from two user IDs.
336
+ * Twitter's 1:1 DM conversation IDs are formed by sorting the two user
337
+ * IDs and joining them. For simplicity, we use the smaller ID first.
338
+ */
339
+ deriveConversationId(userId1, userId2) {
340
+ const ids = [userId1, userId2].sort();
341
+ return `${ids[0]}-${ids[1]}`;
342
+ }
343
+ /**
344
+ * Parse a Twitter DM event into a normalized Message.
345
+ */
346
+ parseDMEvent(dmEvent, threadId, users) {
347
+ const senderId = dmEvent.message_create.sender_id;
348
+ const text = dmEvent.message_create.message_data.text;
349
+ const user = users?.[senderId];
350
+ const author = {
351
+ userId: senderId,
352
+ userName: user?.screen_name ?? senderId,
353
+ fullName: user?.name ?? user?.screen_name ?? senderId,
354
+ isBot: senderId === this._botUserId,
355
+ isMe: senderId === this._botUserId
356
+ };
357
+ const attachments = this.extractAttachments(dmEvent);
358
+ const isMention = this.checkMention(text);
359
+ return new Message({
360
+ id: dmEvent.id,
361
+ threadId,
362
+ text,
363
+ formatted: this.formatConverter.toAst(text),
364
+ raw: dmEvent,
365
+ author,
366
+ metadata: {
367
+ dateSent: new Date(Number.parseInt(dmEvent.created_timestamp, 10)),
368
+ edited: false
369
+ },
370
+ attachments,
371
+ isMention
372
+ });
373
+ }
374
+ /**
375
+ * Extract attachments from a DM event.
376
+ */
377
+ extractAttachments(dmEvent) {
378
+ const attachments = [];
379
+ const attachment = dmEvent.message_create.message_data.attachment;
380
+ if (attachment?.media) {
381
+ const media = attachment.media;
382
+ const type = media.type === "video" ? "video" : "image";
383
+ const largestSize = media.sizes ? Object.values(media.sizes).reduce(
384
+ (acc, size) => size.w * size.h > (acc?.w ?? 0) * (acc?.h ?? 0) ? size : acc,
385
+ void 0
386
+ ) : void 0;
387
+ attachments.push({
388
+ type,
389
+ url: media.media_url_https,
390
+ width: largestSize?.w,
391
+ height: largestSize?.h
392
+ });
393
+ }
394
+ return attachments;
395
+ }
396
+ /**
397
+ * Check if the bot is mentioned in the text.
398
+ */
399
+ checkMention(text) {
400
+ if (!text || !this._userName) {
401
+ return false;
402
+ }
403
+ const mentionPattern = new RegExp(
404
+ `@${this.escapeRegex(this._userName)}\\b`,
405
+ "i"
406
+ );
407
+ return mentionPattern.test(text);
408
+ }
409
+ async postMessage(threadId, message) {
410
+ const { conversationId } = this.resolveThreadId(threadId);
411
+ const card = extractCard(message);
412
+ const text = this.truncateMessage(
413
+ card ? cardToFallbackText(card) : this.formatConverter.renderPostable(message)
414
+ );
415
+ if (!text.trim()) {
416
+ throw new ValidationError("twitter", "Message text cannot be empty");
417
+ }
418
+ const recipientId = this.getRecipientFromConversation(conversationId);
419
+ const response = await this.sendDM(recipientId, text);
420
+ const syntheticEvent = {
421
+ type: "message_create",
422
+ id: response.dm_event_id,
423
+ created_timestamp: String(Date.now()),
424
+ message_create: {
425
+ target: { recipient_id: recipientId },
426
+ sender_id: this._botUserId ?? "",
427
+ message_data: { text }
428
+ }
429
+ };
430
+ const resultThreadId = this.encodeThreadId({
431
+ conversationId: response.dm_conversation_id
432
+ });
433
+ const parsedMessage = new Message({
434
+ id: response.dm_event_id,
435
+ threadId: resultThreadId,
436
+ text,
437
+ formatted: this.formatConverter.toAst(text),
438
+ raw: syntheticEvent,
439
+ author: {
440
+ userId: this._botUserId ?? "",
441
+ userName: this._userName,
442
+ fullName: this._userName,
443
+ isBot: true,
444
+ isMe: true
445
+ },
446
+ metadata: {
447
+ dateSent: /* @__PURE__ */ new Date(),
448
+ edited: false
449
+ },
450
+ attachments: []
451
+ });
452
+ this.cacheMessage(parsedMessage);
453
+ return {
454
+ id: parsedMessage.id,
455
+ threadId: parsedMessage.threadId,
456
+ raw: syntheticEvent
457
+ };
458
+ }
459
+ async editMessage(_threadId, _messageId, _message) {
460
+ throw new NotImplementedError(
461
+ "Twitter DMs cannot be edited after sending",
462
+ "editMessage"
463
+ );
464
+ }
465
+ async deleteMessage(_threadId, messageId) {
466
+ await this.twitterFetchOAuth(
467
+ `/2/dm_conversations/events/${messageId}`,
468
+ "DELETE"
469
+ );
470
+ this.deleteCachedMessage(messageId);
471
+ }
472
+ async addReaction(_threadId, _messageId, _emoji) {
473
+ this.logger.warn(
474
+ "Twitter DM reactions via API are not fully supported"
475
+ );
476
+ }
477
+ async removeReaction(_threadId, _messageId, _emoji) {
478
+ this.logger.warn(
479
+ "Twitter DM reaction removal via API is not fully supported"
480
+ );
481
+ }
482
+ async startTyping(_threadId) {
483
+ }
484
+ async fetchMessages(threadId, options = {}) {
485
+ const messages = [
486
+ ...this.messageCache.get(threadId) ?? []
487
+ ].sort((a, b) => this.compareMessages(a, b));
488
+ return this.paginateMessages(messages, options);
489
+ }
490
+ async fetchThread(threadId) {
491
+ const { conversationId } = this.resolveThreadId(threadId);
492
+ return {
493
+ id: threadId,
494
+ channelId: conversationId,
495
+ isDM: true,
496
+ metadata: {
497
+ conversationId
498
+ }
499
+ };
500
+ }
501
+ channelIdFromThreadId(threadId) {
502
+ const { conversationId } = this.resolveThreadId(threadId);
503
+ return `twitter:${conversationId}`;
504
+ }
505
+ async openDM(userId) {
506
+ if (!this._botUserId) {
507
+ throw new ValidationError(
508
+ "twitter",
509
+ "Bot user ID is not available. Ensure the adapter is initialized."
510
+ );
511
+ }
512
+ const conversationId = this.deriveConversationId(
513
+ this._botUserId,
514
+ userId
515
+ );
516
+ return this.encodeThreadId({ conversationId });
517
+ }
518
+ isDM(_threadId) {
519
+ return true;
520
+ }
521
+ encodeThreadId(platformData) {
522
+ return `twitter:${platformData.conversationId}`;
523
+ }
524
+ decodeThreadId(threadId) {
525
+ const parts = threadId.split(":");
526
+ if (parts[0] !== "twitter" || parts.length !== 2) {
527
+ throw new ValidationError(
528
+ "twitter",
529
+ `Invalid Twitter thread ID: ${threadId}`
530
+ );
531
+ }
532
+ const conversationId = parts[1];
533
+ if (!conversationId) {
534
+ throw new ValidationError(
535
+ "twitter",
536
+ `Invalid Twitter thread ID: ${threadId}`
537
+ );
538
+ }
539
+ return { conversationId };
540
+ }
541
+ parseMessage(raw) {
542
+ const senderId = raw.message_create.sender_id;
543
+ const recipientId = raw.message_create.target.recipient_id;
544
+ const conversationId = this.deriveConversationId(senderId, recipientId);
545
+ const threadId = this.encodeThreadId({ conversationId });
546
+ const message = this.parseDMEvent(raw, threadId);
547
+ this.cacheMessage(message);
548
+ return message;
549
+ }
550
+ renderFormatted(content) {
551
+ return this.formatConverter.fromAst(content);
552
+ }
553
+ // ─── Private Helpers ──────────────────────────────────────────────────
554
+ resolveThreadId(value) {
555
+ if (value.startsWith("twitter:")) {
556
+ return this.decodeThreadId(value);
557
+ }
558
+ return { conversationId: value };
559
+ }
560
+ /**
561
+ * Get the recipient user ID from a conversation ID.
562
+ * Conversation IDs are in the format `{smallerId}-{largerId}`.
563
+ * The recipient is the ID that is NOT the bot's.
564
+ */
565
+ getRecipientFromConversation(conversationId) {
566
+ const parts = conversationId.split("-");
567
+ if (parts.length !== 2) {
568
+ throw new ValidationError(
569
+ "twitter",
570
+ `Cannot determine recipient from conversation ID: ${conversationId}`
571
+ );
572
+ }
573
+ const [id1, id2] = parts;
574
+ if (id1 === this._botUserId) {
575
+ return id2;
576
+ }
577
+ if (id2 === this._botUserId) {
578
+ return id1;
579
+ }
580
+ return id2;
581
+ }
582
+ /**
583
+ * Send a DM via the Twitter API v2.
584
+ */
585
+ async sendDM(recipientId, text) {
586
+ const url = `${this.apiBaseUrl}/2/dm_conversations/with/${recipientId}/messages`;
587
+ const response = await this.twitterFetchOAuth(url, "POST", { text });
588
+ const data = response;
589
+ if (!data.data) {
590
+ throw new NetworkError(
591
+ "twitter",
592
+ "Twitter DM API returned no data"
593
+ );
594
+ }
595
+ return data.data;
596
+ }
597
+ /**
598
+ * Make an authenticated API call using OAuth 1.0a.
599
+ */
600
+ async twitterFetchOAuth(urlOrPath, method, body) {
601
+ const url = urlOrPath.startsWith("http") ? urlOrPath : `${this.apiBaseUrl}${urlOrPath}`;
602
+ const authHeader = await generateOAuth1Header({
603
+ method,
604
+ url,
605
+ consumerKey: this.consumerKey,
606
+ consumerSecret: this.consumerSecret,
607
+ accessToken: this.accessToken,
608
+ accessTokenSecret: this.accessTokenSecret
609
+ });
610
+ let response;
611
+ try {
612
+ response = await fetch(url, {
613
+ method,
614
+ headers: {
615
+ Authorization: authHeader,
616
+ "Content-Type": "application/json"
617
+ },
618
+ body: body ? JSON.stringify(body) : void 0
619
+ });
620
+ } catch (error) {
621
+ throw new NetworkError(
622
+ "twitter",
623
+ `Network error calling Twitter API: ${method} ${urlOrPath}`,
624
+ error instanceof Error ? error : void 0
625
+ );
626
+ }
627
+ if (response.status === 204) {
628
+ return {};
629
+ }
630
+ let data;
631
+ try {
632
+ data = await response.json();
633
+ } catch {
634
+ throw new NetworkError(
635
+ "twitter",
636
+ `Failed to parse Twitter API response for ${method} ${urlOrPath}`
637
+ );
638
+ }
639
+ if (!response.ok) {
640
+ this.throwTwitterApiError(method, urlOrPath, response.status, data);
641
+ }
642
+ return data;
643
+ }
644
+ /**
645
+ * Make an authenticated API call using Bearer Token (API v2 read endpoints).
646
+ */
647
+ async twitterFetchV2(path, method, queryParams) {
648
+ const url = new URL(`${this.apiBaseUrl}${path}`);
649
+ if (queryParams) {
650
+ for (const [key, value] of Object.entries(queryParams)) {
651
+ url.searchParams.set(key, value);
652
+ }
653
+ }
654
+ let response;
655
+ try {
656
+ response = await fetch(url.toString(), {
657
+ method,
658
+ headers: {
659
+ Authorization: `Bearer ${this.bearerToken}`
660
+ }
661
+ });
662
+ } catch (error) {
663
+ throw new NetworkError(
664
+ "twitter",
665
+ `Network error calling Twitter API: ${method} ${path}`,
666
+ error instanceof Error ? error : void 0
667
+ );
668
+ }
669
+ let data;
670
+ try {
671
+ data = await response.json();
672
+ } catch {
673
+ throw new NetworkError(
674
+ "twitter",
675
+ `Failed to parse Twitter API v2 response for ${method} ${path}`
676
+ );
677
+ }
678
+ if (!response.ok) {
679
+ this.throwTwitterApiError(method, path, response.status, data);
680
+ }
681
+ return data.data ?? null;
682
+ }
683
+ /**
684
+ * Map HTTP status codes to appropriate error classes.
685
+ */
686
+ throwTwitterApiError(method, path, status, data) {
687
+ const errors = data?.errors;
688
+ const firstError = errors?.[0];
689
+ const description = firstError?.detail ?? firstError?.message ?? `Twitter API ${method} ${path} failed`;
690
+ if (status === 429) {
691
+ throw new AdapterRateLimitError("twitter");
692
+ }
693
+ if (status === 401) {
694
+ throw new AuthenticationError("twitter", description);
695
+ }
696
+ if (status === 403) {
697
+ throw new PermissionError("twitter", `${method} ${path}`);
698
+ }
699
+ if (status === 404) {
700
+ throw new ResourceNotFoundError("twitter", path);
701
+ }
702
+ if (status >= 400 && status < 500) {
703
+ throw new ValidationError("twitter", description);
704
+ }
705
+ throw new NetworkError(
706
+ "twitter",
707
+ `${description} (status ${status})`
708
+ );
709
+ }
710
+ truncateMessage(text) {
711
+ if (text.length <= TWITTER_DM_MESSAGE_LIMIT) {
712
+ return text;
713
+ }
714
+ return `${text.slice(0, TWITTER_DM_MESSAGE_LIMIT - 3)}...`;
715
+ }
716
+ escapeRegex(input) {
717
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
718
+ }
719
+ cacheMessage(message) {
720
+ const existing = this.messageCache.get(message.threadId) ?? [];
721
+ const index = existing.findIndex((item) => item.id === message.id);
722
+ if (index >= 0) {
723
+ existing[index] = message;
724
+ } else {
725
+ existing.push(message);
726
+ }
727
+ existing.sort((a, b) => this.compareMessages(a, b));
728
+ this.messageCache.set(message.threadId, existing);
729
+ }
730
+ findCachedMessage(messageId) {
731
+ for (const messages of this.messageCache.values()) {
732
+ const found = messages.find((message) => message.id === messageId);
733
+ if (found) {
734
+ return found;
735
+ }
736
+ }
737
+ return void 0;
738
+ }
739
+ deleteCachedMessage(messageId) {
740
+ for (const [threadId, messages] of this.messageCache.entries()) {
741
+ const filtered = messages.filter(
742
+ (message) => message.id !== messageId
743
+ );
744
+ if (filtered.length === 0) {
745
+ this.messageCache.delete(threadId);
746
+ } else if (filtered.length !== messages.length) {
747
+ this.messageCache.set(threadId, filtered);
748
+ }
749
+ }
750
+ }
751
+ compareMessages(a, b) {
752
+ return a.metadata.dateSent.getTime() - b.metadata.dateSent.getTime();
753
+ }
754
+ paginateMessages(messages, options) {
755
+ const limit = Math.max(1, Math.min(options.limit ?? 50, 100));
756
+ const direction = options.direction ?? "backward";
757
+ if (messages.length === 0) {
758
+ return { messages: [] };
759
+ }
760
+ const messageIndexById = new Map(
761
+ messages.map((message, index) => [message.id, index])
762
+ );
763
+ if (direction === "backward") {
764
+ const end2 = options.cursor && messageIndexById.has(options.cursor) ? messageIndexById.get(options.cursor) ?? messages.length : messages.length;
765
+ const start2 = Math.max(0, end2 - limit);
766
+ const page2 = messages.slice(start2, end2);
767
+ return {
768
+ messages: page2,
769
+ nextCursor: start2 > 0 ? page2[0]?.id : void 0
770
+ };
771
+ }
772
+ const start = options.cursor && messageIndexById.has(options.cursor) ? (messageIndexById.get(options.cursor) ?? -1) + 1 : 0;
773
+ const end = Math.min(messages.length, start + limit);
774
+ const page = messages.slice(start, end);
775
+ return {
776
+ messages: page,
777
+ nextCursor: end < messages.length ? page.at(-1)?.id : void 0
778
+ };
779
+ }
780
+ };
781
+ function createTwitterAdapter(config) {
782
+ return new TwitterAdapter(config ?? {});
783
+ }
784
+ export {
785
+ TwitterAdapter,
786
+ TwitterFormatConverter,
787
+ createTwitterAdapter
788
+ };
789
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/markdown.ts"],"sourcesContent":["import {\n AdapterRateLimitError,\n AuthenticationError,\n cardToFallbackText,\n extractCard,\n NetworkError,\n PermissionError,\n ResourceNotFoundError,\n ValidationError,\n} from \"@chat-adapter/shared\";\nimport type {\n Adapter,\n AdapterPostableMessage,\n Attachment,\n ChatInstance,\n EmojiValue,\n FetchOptions,\n FetchResult,\n FormattedContent,\n Logger,\n RawMessage,\n ThreadInfo,\n WebhookOptions,\n} from \"chat\";\nimport { ConsoleLogger, Message, NotImplementedError } from \"chat\";\nimport { TwitterFormatConverter } from \"./markdown\";\nimport type {\n TwitterAccountActivityPayload,\n TwitterAdapterConfig,\n TwitterApiV2Response,\n TwitterDirectMessageEvent,\n TwitterDMSendResponse,\n TwitterRawMessage,\n TwitterThreadId,\n TwitterUser,\n TwitterUserV2,\n} from \"./types\";\n\nconst TWITTER_API_BASE = \"https://api.twitter.com\";\nconst TWITTER_DM_MESSAGE_LIMIT = 10000;\nconst CRC_TOKEN_PARAM = \"crc_token\";\n\ninterface TwitterMessageAuthor {\n fullName: string;\n isBot: boolean | \"unknown\";\n isMe: boolean;\n userId: string;\n userName: string;\n}\n\n/**\n * Compute HMAC-SHA256 for CRC validation.\n * Uses the Web Crypto API (available in Node 18+ and edge runtimes).\n */\nasync function computeHmacSha256(\n key: string,\n message: string\n): Promise<string> {\n const encoder = new TextEncoder();\n const keyData = encoder.encode(key);\n const messageData = encoder.encode(message);\n\n const cryptoKey = await crypto.subtle.importKey(\n \"raw\",\n keyData,\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"]\n );\n\n const signature = await crypto.subtle.sign(\"HMAC\", cryptoKey, messageData);\n return btoa(String.fromCharCode(...new Uint8Array(signature)));\n}\n\n/**\n * Generate OAuth 1.0a Authorization header.\n *\n * This is a simplified OAuth 1.0a implementation for the X API.\n * It generates the HMAC-SHA1 signature required by Twitter's API.\n */\nasync function generateOAuth1Header(params: {\n method: string;\n url: string;\n consumerKey: string;\n consumerSecret: string;\n accessToken: string;\n accessTokenSecret: string;\n additionalParams?: Record<string, string>;\n}): Promise<string> {\n const {\n method,\n url,\n consumerKey,\n consumerSecret,\n accessToken,\n accessTokenSecret,\n additionalParams,\n } = params;\n\n const timestamp = Math.floor(Date.now() / 1000).toString();\n const nonce = crypto.randomUUID().replace(/-/g, \"\");\n\n const oauthParams: Record<string, string> = {\n oauth_consumer_key: consumerKey,\n oauth_nonce: nonce,\n oauth_signature_method: \"HMAC-SHA1\",\n oauth_timestamp: timestamp,\n oauth_token: accessToken,\n oauth_version: \"1.0\",\n ...additionalParams,\n };\n\n // Parse URL to get any query parameters\n const parsedUrl = new URL(url);\n for (const [key, value] of parsedUrl.searchParams.entries()) {\n oauthParams[key] = value;\n }\n\n // Sort parameters\n const sortedParams = Object.entries(oauthParams)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(\n ([key, value]) =>\n `${encodeURIComponent(key)}=${encodeURIComponent(value)}`\n )\n .join(\"&\");\n\n // Base URL without query string\n const baseUrl = `${parsedUrl.origin}${parsedUrl.pathname}`;\n\n // Create signature base string\n const signatureBase = `${method.toUpperCase()}&${encodeURIComponent(baseUrl)}&${encodeURIComponent(sortedParams)}`;\n const signingKey = `${encodeURIComponent(consumerSecret)}&${encodeURIComponent(accessTokenSecret)}`;\n\n // Compute HMAC-SHA1\n const encoder = new TextEncoder();\n const keyData = encoder.encode(signingKey);\n const messageData = encoder.encode(signatureBase);\n\n const cryptoKey = await crypto.subtle.importKey(\n \"raw\",\n keyData,\n { name: \"HMAC\", hash: \"SHA-1\" },\n false,\n [\"sign\"]\n );\n\n const signature = await crypto.subtle.sign(\"HMAC\", cryptoKey, messageData);\n const signatureBase64 = btoa(\n String.fromCharCode(...new Uint8Array(signature))\n );\n\n // Build Authorization header\n const authParams: Record<string, string> = {\n oauth_consumer_key: consumerKey,\n oauth_nonce: nonce,\n oauth_signature: signatureBase64,\n oauth_signature_method: \"HMAC-SHA1\",\n oauth_timestamp: timestamp,\n oauth_token: accessToken,\n oauth_version: \"1.0\",\n };\n\n const authHeader = Object.entries(authParams)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(\n ([key, value]) =>\n `${encodeURIComponent(key)}=\"${encodeURIComponent(value)}\"`\n )\n .join(\", \");\n\n return `OAuth ${authHeader}`;\n}\n\nexport class TwitterAdapter\n implements Adapter<TwitterThreadId, TwitterRawMessage>\n{\n readonly name = \"twitter\";\n readonly persistMessageHistory = true;\n\n private readonly consumerKey: string;\n private readonly consumerSecret: string;\n private readonly accessToken: string;\n private readonly accessTokenSecret: string;\n private readonly bearerToken: string;\n private readonly apiBaseUrl: string;\n private readonly webhookEnvironment: string;\n private readonly logger: Logger;\n private readonly formatConverter = new TwitterFormatConverter();\n private readonly messageCache = new Map<\n string,\n Message<TwitterRawMessage>[]\n >();\n\n private chat: ChatInstance | null = null;\n private _botUserId?: string;\n private _userName: string;\n private readonly hasExplicitUserName: boolean;\n\n get botUserId(): string | undefined {\n return this._botUserId;\n }\n\n get userName(): string {\n return this._userName;\n }\n\n constructor(config: TwitterAdapterConfig = {}) {\n const consumerKey =\n config.consumerKey ?? process.env.TWITTER_CONSUMER_KEY;\n const consumerSecret =\n config.consumerSecret ?? process.env.TWITTER_CONSUMER_SECRET;\n const accessToken =\n config.accessToken ?? process.env.TWITTER_ACCESS_TOKEN;\n const accessTokenSecret =\n config.accessTokenSecret ?? process.env.TWITTER_ACCESS_TOKEN_SECRET;\n const bearerToken =\n config.bearerToken ?? process.env.TWITTER_BEARER_TOKEN;\n\n if (!consumerKey) {\n throw new ValidationError(\n \"twitter\",\n \"Consumer key is required. Set TWITTER_CONSUMER_KEY or provide it in config.\"\n );\n }\n if (!consumerSecret) {\n throw new ValidationError(\n \"twitter\",\n \"Consumer secret is required. Set TWITTER_CONSUMER_SECRET or provide it in config.\"\n );\n }\n if (!accessToken) {\n throw new ValidationError(\n \"twitter\",\n \"Access token is required. Set TWITTER_ACCESS_TOKEN or provide it in config.\"\n );\n }\n if (!accessTokenSecret) {\n throw new ValidationError(\n \"twitter\",\n \"Access token secret is required. Set TWITTER_ACCESS_TOKEN_SECRET or provide it in config.\"\n );\n }\n if (!bearerToken) {\n throw new ValidationError(\n \"twitter\",\n \"Bearer token is required. Set TWITTER_BEARER_TOKEN or provide it in config.\"\n );\n }\n\n this.consumerKey = consumerKey;\n this.consumerSecret = consumerSecret;\n this.accessToken = accessToken;\n this.accessTokenSecret = accessTokenSecret;\n this.bearerToken = bearerToken;\n this.apiBaseUrl = (\n config.apiBaseUrl ??\n process.env.TWITTER_API_BASE_URL ??\n TWITTER_API_BASE\n ).replace(/\\/+$/, \"\");\n this.webhookEnvironment =\n config.webhookEnvironment ??\n process.env.TWITTER_WEBHOOK_ENV ??\n \"production\";\n this.logger =\n config.logger ?? new ConsoleLogger(\"info\").child(\"twitter\");\n\n const userName =\n config.userName ?? process.env.TWITTER_BOT_USERNAME;\n this._userName = userName ?? \"bot\";\n this.hasExplicitUserName = Boolean(userName);\n }\n\n async initialize(chat: ChatInstance): Promise<void> {\n this.chat = chat;\n\n if (!this.hasExplicitUserName) {\n const chatUserName = chat.getUserName?.();\n if (typeof chatUserName === \"string\" && chatUserName.trim()) {\n this._userName = chatUserName;\n }\n }\n\n // Fetch bot's own user info via API v2\n try {\n const me = await this.twitterFetchV2<TwitterUserV2>(\n \"/2/users/me\",\n \"GET\"\n );\n if (me) {\n this._botUserId = me.id;\n if (!this.hasExplicitUserName && me.username) {\n this._userName = me.username;\n }\n }\n\n this.logger.info(\"Twitter adapter initialized\", {\n botUserId: this._botUserId,\n userName: this._userName,\n });\n } catch (error) {\n this.logger.warn(\"Failed to fetch Twitter bot identity\", {\n error: String(error),\n });\n }\n }\n\n /**\n * Handle incoming webhook requests from the X Account Activity API.\n *\n * GET requests are CRC challenges — we respond with the HMAC-SHA256 hash.\n * POST requests are webhook events (DMs, tweets, etc.).\n */\n async handleWebhook(\n request: Request,\n options?: WebhookOptions\n ): Promise<Response> {\n // Handle CRC challenge (GET request)\n if (request.method === \"GET\") {\n return this.handleCrcChallenge(request);\n }\n\n // For POST requests, verify the webhook signature\n const body = await request.text();\n\n // Parse the payload\n let payload: TwitterAccountActivityPayload;\n try {\n payload = JSON.parse(body) as TwitterAccountActivityPayload;\n } catch {\n return new Response(\"Invalid JSON\", { status: 400 });\n }\n\n if (!this.chat) {\n this.logger.warn(\n \"Chat instance not initialized, ignoring Twitter webhook\"\n );\n return new Response(\"OK\", { status: 200 });\n }\n\n // Process DM events\n try {\n this.processDMEvents(payload, options);\n } catch (error) {\n this.logger.warn(\"Failed to process Twitter webhook payload\", {\n error: String(error),\n forUserId: payload.for_user_id,\n });\n }\n\n return new Response(\"OK\", { status: 200 });\n }\n\n /**\n * Handle CRC challenge from the X Account Activity API.\n * Responds with HMAC-SHA256 hash of the crc_token using consumer secret.\n */\n private async handleCrcChallenge(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const crcToken = url.searchParams.get(CRC_TOKEN_PARAM);\n\n if (!crcToken) {\n return new Response(\"Missing crc_token parameter\", { status: 400 });\n }\n\n const responseToken = await computeHmacSha256(\n this.consumerSecret,\n crcToken\n );\n\n return new Response(\n JSON.stringify({\n response_token: `sha256=${responseToken}`,\n }),\n {\n status: 200,\n headers: { \"Content-Type\": \"application/json\" },\n }\n );\n }\n\n /**\n * Process DM events from the Account Activity webhook payload.\n */\n private processDMEvents(\n payload: TwitterAccountActivityPayload,\n options?: WebhookOptions\n ): void {\n if (!this.chat || !payload.direct_message_events) {\n return;\n }\n\n for (const dmEvent of payload.direct_message_events) {\n if (dmEvent.type !== \"message_create\") {\n continue;\n }\n\n // Skip messages sent by the bot itself to prevent loops\n const senderId = dmEvent.message_create.sender_id;\n if (senderId === this._botUserId) {\n continue;\n }\n\n // Skip messages sent by the bot to the `for_user_id`\n // (outbound DMs also appear in the webhook)\n if (senderId === payload.for_user_id && senderId === this._botUserId) {\n continue;\n }\n\n // Duplicate suppression: if two subscribed users are in the same DM,\n // we receive the event twice (once per user). We only process events\n // where `for_user_id` matches our bot's user ID.\n if (this._botUserId && payload.for_user_id !== this._botUserId) {\n continue;\n }\n\n // Determine the conversation ID\n // In a 1:1 DM, the conversation ID is derived from the user IDs\n const recipientId = dmEvent.message_create.target.recipient_id;\n const conversationId = this.deriveConversationId(senderId, recipientId);\n\n const threadId = this.encodeThreadId({ conversationId });\n\n const parsedMessage = this.parseDMEvent(\n dmEvent,\n threadId,\n payload.users\n );\n this.cacheMessage(parsedMessage);\n\n this.chat.processMessage(this, threadId, parsedMessage, options);\n }\n }\n\n /**\n * Derive a deterministic conversation ID from two user IDs.\n * Twitter's 1:1 DM conversation IDs are formed by sorting the two user\n * IDs and joining them. For simplicity, we use the smaller ID first.\n */\n private deriveConversationId(\n userId1: string,\n userId2: string\n ): string {\n const ids = [userId1, userId2].sort();\n return `${ids[0]}-${ids[1]}`;\n }\n\n /**\n * Parse a Twitter DM event into a normalized Message.\n */\n private parseDMEvent(\n dmEvent: TwitterDirectMessageEvent,\n threadId: string,\n users?: Record<string, TwitterUser>\n ): Message<TwitterRawMessage> {\n const senderId = dmEvent.message_create.sender_id;\n const text = dmEvent.message_create.message_data.text;\n const user = users?.[senderId];\n\n const author: TwitterMessageAuthor = {\n userId: senderId,\n userName: user?.screen_name ?? senderId,\n fullName: user?.name ?? user?.screen_name ?? senderId,\n isBot: senderId === this._botUserId,\n isMe: senderId === this._botUserId,\n };\n\n const attachments = this.extractAttachments(dmEvent);\n const isMention = this.checkMention(text);\n\n return new Message<TwitterRawMessage>({\n id: dmEvent.id,\n threadId,\n text,\n formatted: this.formatConverter.toAst(text),\n raw: dmEvent,\n author,\n metadata: {\n dateSent: new Date(Number.parseInt(dmEvent.created_timestamp, 10)),\n edited: false,\n },\n attachments,\n isMention,\n });\n }\n\n /**\n * Extract attachments from a DM event.\n */\n private extractAttachments(\n dmEvent: TwitterDirectMessageEvent\n ): Attachment[] {\n const attachments: Attachment[] = [];\n const attachment = dmEvent.message_create.message_data.attachment;\n\n if (attachment?.media) {\n const media = attachment.media;\n const type = media.type === \"video\" ? \"video\" : \"image\";\n\n // Find the largest image size\n const largestSize = media.sizes\n ? Object.values(media.sizes).reduce(\n (acc, size) =>\n size.w * size.h > (acc?.w ?? 0) * (acc?.h ?? 0) ? size : acc,\n undefined as { w: number; h: number; resize: string } | undefined\n )\n : undefined;\n\n attachments.push({\n type,\n url: media.media_url_https,\n width: largestSize?.w,\n height: largestSize?.h,\n });\n }\n\n return attachments;\n }\n\n /**\n * Check if the bot is mentioned in the text.\n */\n private checkMention(text: string): boolean {\n if (!text || !this._userName) {\n return false;\n }\n\n const mentionPattern = new RegExp(\n `@${this.escapeRegex(this._userName)}\\\\b`,\n \"i\"\n );\n return mentionPattern.test(text);\n }\n\n async postMessage(\n threadId: string,\n message: AdapterPostableMessage\n ): Promise<RawMessage<TwitterRawMessage>> {\n const { conversationId } = this.resolveThreadId(threadId);\n\n const card = extractCard(message);\n const text = this.truncateMessage(\n card\n ? cardToFallbackText(card)\n : this.formatConverter.renderPostable(message)\n );\n\n if (!text.trim()) {\n throw new ValidationError(\"twitter\", \"Message text cannot be empty\");\n }\n\n // Determine the recipient from the conversation ID\n const recipientId = this.getRecipientFromConversation(conversationId);\n\n const response = await this.sendDM(recipientId, text);\n\n // Create a synthetic raw DM event for the sent message\n const syntheticEvent: TwitterDirectMessageEvent = {\n type: \"message_create\",\n id: response.dm_event_id,\n created_timestamp: String(Date.now()),\n message_create: {\n target: { recipient_id: recipientId },\n sender_id: this._botUserId ?? \"\",\n message_data: { text },\n },\n };\n\n const resultThreadId = this.encodeThreadId({\n conversationId: response.dm_conversation_id,\n });\n\n const parsedMessage = new Message<TwitterRawMessage>({\n id: response.dm_event_id,\n threadId: resultThreadId,\n text,\n formatted: this.formatConverter.toAst(text),\n raw: syntheticEvent,\n author: {\n userId: this._botUserId ?? \"\",\n userName: this._userName,\n fullName: this._userName,\n isBot: true,\n isMe: true,\n },\n metadata: {\n dateSent: new Date(),\n edited: false,\n },\n attachments: [],\n });\n\n this.cacheMessage(parsedMessage);\n\n return {\n id: parsedMessage.id,\n threadId: parsedMessage.threadId,\n raw: syntheticEvent,\n };\n }\n\n async editMessage(\n _threadId: string,\n _messageId: string,\n _message: AdapterPostableMessage\n ): Promise<RawMessage<TwitterRawMessage>> {\n throw new NotImplementedError(\n \"Twitter DMs cannot be edited after sending\",\n \"editMessage\"\n );\n }\n\n async deleteMessage(\n _threadId: string,\n messageId: string\n ): Promise<void> {\n // Twitter API v2 supports deleting DM events\n await this.twitterFetchOAuth(\n `/2/dm_conversations/events/${messageId}`,\n \"DELETE\"\n );\n\n this.deleteCachedMessage(messageId);\n }\n\n async addReaction(\n _threadId: string,\n _messageId: string,\n _emoji: EmojiValue | string\n ): Promise<void> {\n // Twitter DMs do support reactions, but the API is limited.\n // For now, log a warning that this is not fully supported.\n this.logger.warn(\n \"Twitter DM reactions via API are not fully supported\"\n );\n }\n\n async removeReaction(\n _threadId: string,\n _messageId: string,\n _emoji: EmojiValue | string\n ): Promise<void> {\n this.logger.warn(\n \"Twitter DM reaction removal via API is not fully supported\"\n );\n }\n\n async startTyping(_threadId: string): Promise<void> {\n // Twitter DM API doesn't have a native typing indicator endpoint.\n // No-op to fulfill the interface contract.\n }\n\n async fetchMessages(\n threadId: string,\n options: FetchOptions = {}\n ): Promise<FetchResult<TwitterRawMessage>> {\n const messages = [\n ...(this.messageCache.get(threadId) ?? []),\n ].sort((a, b) => this.compareMessages(a, b));\n\n return this.paginateMessages(messages, options);\n }\n\n async fetchThread(threadId: string): Promise<ThreadInfo> {\n const { conversationId } = this.resolveThreadId(threadId);\n\n return {\n id: threadId,\n channelId: conversationId,\n isDM: true,\n metadata: {\n conversationId,\n },\n };\n }\n\n channelIdFromThreadId(threadId: string): string {\n const { conversationId } = this.resolveThreadId(threadId);\n return `twitter:${conversationId}`;\n }\n\n async openDM(userId: string): Promise<string> {\n if (!this._botUserId) {\n throw new ValidationError(\n \"twitter\",\n \"Bot user ID is not available. Ensure the adapter is initialized.\"\n );\n }\n\n const conversationId = this.deriveConversationId(\n this._botUserId,\n userId\n );\n return this.encodeThreadId({ conversationId });\n }\n\n isDM(_threadId: string): boolean {\n // All Twitter adapter threads are DM conversations\n return true;\n }\n\n encodeThreadId(platformData: TwitterThreadId): string {\n return `twitter:${platformData.conversationId}`;\n }\n\n decodeThreadId(threadId: string): TwitterThreadId {\n const parts = threadId.split(\":\");\n if (parts[0] !== \"twitter\" || parts.length !== 2) {\n throw new ValidationError(\n \"twitter\",\n `Invalid Twitter thread ID: ${threadId}`\n );\n }\n\n const conversationId = parts[1];\n if (!conversationId) {\n throw new ValidationError(\n \"twitter\",\n `Invalid Twitter thread ID: ${threadId}`\n );\n }\n\n return { conversationId };\n }\n\n parseMessage(raw: TwitterRawMessage): Message<TwitterRawMessage> {\n const senderId = raw.message_create.sender_id;\n const recipientId = raw.message_create.target.recipient_id;\n const conversationId = this.deriveConversationId(senderId, recipientId);\n const threadId = this.encodeThreadId({ conversationId });\n\n const message = this.parseDMEvent(raw, threadId);\n this.cacheMessage(message);\n return message;\n }\n\n renderFormatted(content: FormattedContent): string {\n return this.formatConverter.fromAst(content);\n }\n\n // ─── Private Helpers ──────────────────────────────────────────────────\n\n private resolveThreadId(value: string): TwitterThreadId {\n if (value.startsWith(\"twitter:\")) {\n return this.decodeThreadId(value);\n }\n return { conversationId: value };\n }\n\n /**\n * Get the recipient user ID from a conversation ID.\n * Conversation IDs are in the format `{smallerId}-{largerId}`.\n * The recipient is the ID that is NOT the bot's.\n */\n private getRecipientFromConversation(conversationId: string): string {\n const parts = conversationId.split(\"-\");\n if (parts.length !== 2) {\n throw new ValidationError(\n \"twitter\",\n `Cannot determine recipient from conversation ID: ${conversationId}`\n );\n }\n\n const [id1, id2] = parts;\n if (id1 === this._botUserId) {\n return id2;\n }\n if (id2 === this._botUserId) {\n return id1;\n }\n\n // If bot user ID is unknown, default to the second ID\n return id2;\n }\n\n /**\n * Send a DM via the Twitter API v2.\n */\n private async sendDM(\n recipientId: string,\n text: string\n ): Promise<{ dm_event_id: string; dm_conversation_id: string }> {\n const url = `${this.apiBaseUrl}/2/dm_conversations/with/${recipientId}/messages`;\n\n const response = await this.twitterFetchOAuth(url, \"POST\", { text });\n\n const data = response as { data?: { dm_event_id: string; dm_conversation_id: string } };\n if (!data.data) {\n throw new NetworkError(\n \"twitter\",\n \"Twitter DM API returned no data\"\n );\n }\n\n return data.data;\n }\n\n /**\n * Make an authenticated API call using OAuth 1.0a.\n */\n private async twitterFetchOAuth(\n urlOrPath: string,\n method: string,\n body?: Record<string, unknown>\n ): Promise<unknown> {\n const url = urlOrPath.startsWith(\"http\")\n ? urlOrPath\n : `${this.apiBaseUrl}${urlOrPath}`;\n\n const authHeader = await generateOAuth1Header({\n method,\n url,\n consumerKey: this.consumerKey,\n consumerSecret: this.consumerSecret,\n accessToken: this.accessToken,\n accessTokenSecret: this.accessTokenSecret,\n });\n\n let response: Response;\n try {\n response = await fetch(url, {\n method,\n headers: {\n Authorization: authHeader,\n \"Content-Type\": \"application/json\",\n },\n body: body ? JSON.stringify(body) : undefined,\n });\n } catch (error) {\n throw new NetworkError(\n \"twitter\",\n `Network error calling Twitter API: ${method} ${urlOrPath}`,\n error instanceof Error ? error : undefined\n );\n }\n\n if (response.status === 204) {\n return {};\n }\n\n let data: unknown;\n try {\n data = await response.json();\n } catch {\n throw new NetworkError(\n \"twitter\",\n `Failed to parse Twitter API response for ${method} ${urlOrPath}`\n );\n }\n\n if (!response.ok) {\n this.throwTwitterApiError(method, urlOrPath, response.status, data);\n }\n\n return data;\n }\n\n /**\n * Make an authenticated API call using Bearer Token (API v2 read endpoints).\n */\n private async twitterFetchV2<TResult>(\n path: string,\n method: string,\n queryParams?: Record<string, string>\n ): Promise<TResult | null> {\n const url = new URL(`${this.apiBaseUrl}${path}`);\n if (queryParams) {\n for (const [key, value] of Object.entries(queryParams)) {\n url.searchParams.set(key, value);\n }\n }\n\n let response: Response;\n try {\n response = await fetch(url.toString(), {\n method,\n headers: {\n Authorization: `Bearer ${this.bearerToken}`,\n },\n });\n } catch (error) {\n throw new NetworkError(\n \"twitter\",\n `Network error calling Twitter API: ${method} ${path}`,\n error instanceof Error ? error : undefined\n );\n }\n\n let data: TwitterApiV2Response<TResult>;\n try {\n data = (await response.json()) as TwitterApiV2Response<TResult>;\n } catch {\n throw new NetworkError(\n \"twitter\",\n `Failed to parse Twitter API v2 response for ${method} ${path}`\n );\n }\n\n if (!response.ok) {\n this.throwTwitterApiError(method, path, response.status, data);\n }\n\n return data.data ?? null;\n }\n\n /**\n * Map HTTP status codes to appropriate error classes.\n */\n private throwTwitterApiError(\n method: string,\n path: string,\n status: number,\n data: unknown\n ): never {\n const errors = (data as { errors?: Array<{ message?: string; detail?: string }> })\n ?.errors;\n const firstError = errors?.[0];\n const description =\n firstError?.detail ??\n firstError?.message ??\n `Twitter API ${method} ${path} failed`;\n\n if (status === 429) {\n throw new AdapterRateLimitError(\"twitter\");\n }\n\n if (status === 401) {\n throw new AuthenticationError(\"twitter\", description);\n }\n\n if (status === 403) {\n throw new PermissionError(\"twitter\", `${method} ${path}`);\n }\n\n if (status === 404) {\n throw new ResourceNotFoundError(\"twitter\", path);\n }\n\n if (status >= 400 && status < 500) {\n throw new ValidationError(\"twitter\", description);\n }\n\n throw new NetworkError(\n \"twitter\",\n `${description} (status ${status})`\n );\n }\n\n private truncateMessage(text: string): string {\n if (text.length <= TWITTER_DM_MESSAGE_LIMIT) {\n return text;\n }\n return `${text.slice(0, TWITTER_DM_MESSAGE_LIMIT - 3)}...`;\n }\n\n private escapeRegex(input: string): string {\n return input.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n }\n\n private cacheMessage(message: Message<TwitterRawMessage>): void {\n const existing = this.messageCache.get(message.threadId) ?? [];\n const index = existing.findIndex((item) => item.id === message.id);\n\n if (index >= 0) {\n existing[index] = message;\n } else {\n existing.push(message);\n }\n\n existing.sort((a, b) => this.compareMessages(a, b));\n this.messageCache.set(message.threadId, existing);\n }\n\n private findCachedMessage(\n messageId: string\n ): Message<TwitterRawMessage> | undefined {\n for (const messages of this.messageCache.values()) {\n const found = messages.find((message) => message.id === messageId);\n if (found) {\n return found;\n }\n }\n return undefined;\n }\n\n private deleteCachedMessage(messageId: string): void {\n for (const [threadId, messages] of this.messageCache.entries()) {\n const filtered = messages.filter(\n (message) => message.id !== messageId\n );\n if (filtered.length === 0) {\n this.messageCache.delete(threadId);\n } else if (filtered.length !== messages.length) {\n this.messageCache.set(threadId, filtered);\n }\n }\n }\n\n private compareMessages(\n a: Message<TwitterRawMessage>,\n b: Message<TwitterRawMessage>\n ): number {\n return (\n a.metadata.dateSent.getTime() - b.metadata.dateSent.getTime()\n );\n }\n\n private paginateMessages(\n messages: Message<TwitterRawMessage>[],\n options: FetchOptions\n ): FetchResult<TwitterRawMessage> {\n const limit = Math.max(1, Math.min(options.limit ?? 50, 100));\n const direction = options.direction ?? \"backward\";\n\n if (messages.length === 0) {\n return { messages: [] };\n }\n\n const messageIndexById = new Map(\n messages.map((message, index) => [message.id, index])\n );\n\n if (direction === \"backward\") {\n const end =\n options.cursor && messageIndexById.has(options.cursor)\n ? (messageIndexById.get(options.cursor) ?? messages.length)\n : messages.length;\n const start = Math.max(0, end - limit);\n const page = messages.slice(start, end);\n\n return {\n messages: page,\n nextCursor: start > 0 ? page[0]?.id : undefined,\n };\n }\n\n const start =\n options.cursor && messageIndexById.has(options.cursor)\n ? (messageIndexById.get(options.cursor) ?? -1) + 1\n : 0;\n const end = Math.min(messages.length, start + limit);\n const page = messages.slice(start, end);\n\n return {\n messages: page,\n nextCursor: end < messages.length ? page.at(-1)?.id : undefined,\n };\n }\n}\n\nexport function createTwitterAdapter(\n config?: TwitterAdapterConfig\n): TwitterAdapter {\n return new TwitterAdapter(config ?? {});\n}\n\nexport { TwitterFormatConverter } from \"./markdown\";\nexport type {\n TwitterAccountActivityPayload,\n TwitterAdapterConfig,\n TwitterDirectMessageEvent,\n TwitterDMEventV2,\n TwitterDMSendResponse,\n TwitterRawMessage,\n TwitterThreadId,\n TwitterUser,\n TwitterUserV2,\n} from \"./types\";\n","/**\n * Twitter / X format conversion.\n *\n * Twitter DMs use plain text with entity annotations (similar to Telegram).\n * For outbound messages, we send plain text since the DM API doesn't\n * support rich formatting (bold/italic/etc.) natively.\n */\n\nimport {\n type AdapterPostableMessage,\n BaseFormatConverter,\n type Content,\n isTableNode,\n parseMarkdown,\n type Root,\n stringifyMarkdown,\n tableToAscii,\n walkAst,\n} from \"chat\";\n\nexport class TwitterFormatConverter extends BaseFormatConverter {\n /**\n * Convert platform text to mdast AST.\n * Twitter DMs are plain text — we parse as if they were markdown\n * so that any user-typed markdown is preserved in the AST.\n */\n toAst(text: string): Root {\n return parseMarkdown(text);\n }\n\n /**\n * Convert mdast AST to platform text format.\n * Twitter DMs don't support rich formatting, so we try to\n * produce readable plain text. Tables get converted to ASCII art.\n */\n fromAst(ast: Root): string {\n const transformed = walkAst(structuredClone(ast), (node: Content) => {\n if (isTableNode(node)) {\n return {\n type: \"code\" as const,\n value: tableToAscii(node),\n lang: undefined,\n } as Content;\n }\n return node;\n });\n return stringifyMarkdown(transformed).trim();\n }\n\n override renderPostable(message: AdapterPostableMessage): string {\n if (typeof message === \"string\") {\n return message;\n }\n if (\"raw\" in message) {\n return message.raw;\n }\n if (\"markdown\" in message) {\n return this.fromMarkdown(message.markdown);\n }\n if (\"ast\" in message) {\n return this.fromAst(message.ast);\n }\n return super.renderPostable(message);\n }\n}\n"],"mappings":"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","names":["end","start","page"]}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "chat-adapter-twitter",
3
+ "version": "0.1.0",
4
+ "description": "Twitter / X adapter for chat",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "dev": "tsup --watch",
21
+ "test": "vitest run --coverage",
22
+ "test:watch": "vitest",
23
+ "typecheck": "tsc --noEmit",
24
+ "clean": "rm -rf dist"
25
+ },
26
+ "dependencies": {
27
+ "@chat-adapter/shared": "^4.20.2"
28
+ },
29
+ "peerDependencies": {
30
+ "chat": "^4.20.2"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^25.3.2",
34
+ "tsup": "^8.3.5",
35
+ "typescript": "^5.7.2",
36
+ "vitest": "^4.0.18"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/vercel/chat.git",
41
+ "directory": "packages/adapter-twitter"
42
+ },
43
+ "homepage": "https://github.com/vercel/chat#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/vercel/chat/issues"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "keywords": [
51
+ "chat",
52
+ "twitter",
53
+ "x",
54
+ "bot",
55
+ "adapter"
56
+ ],
57
+ "license": "MIT"
58
+ }