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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/add-twitter-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@chat-adapter/twitter": minor
---

Initial release of the Twitter / X adapter.

* Receive DMs natively via Account Activity API webhooks
* Post DMs via X API v2
* CRC Challenge hash verification
* AST to plain-text and ASCII table rendering support

72 changes: 36 additions & 36 deletions apps/docs/content/docs/adapters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,51 +14,51 @@ Ready to build your own? Follow the [building](/docs/contributing/building) guid

### Messaging

| Feature | [Slack](/adapters/slack) | [Teams](/adapters/teams) | [Google Chat](/adapters/google-chat) | [Discord](/adapters/discord) | [Telegram](/adapters/telegram) | [GitHub](/adapters/github) | [Linear](/adapters/linear) | [WhatsApp](/adapters/whatsapp) |
|---------|-------|-------|-------------|---------|---------|--------|--------|-----------|
| Post message | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Edit message | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Delete message | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| File uploads | ✅ | ✅ | ❌ | ✅ | ⚠️ Single file | ❌ | ❌ | ✅ Images, audio, docs |
| Streaming | ✅ Native | ⚠️ Post+Edit | ⚠️ Post+Edit | ⚠️ Post+Edit | ⚠️ Post+Edit | ❌ | ❌ | ❌ |
| Scheduled messages | ✅ Native | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Feature | [Slack](/adapters/slack) | [Teams](/adapters/teams) | [Google Chat](/adapters/google-chat) | [Discord](/adapters/discord) | [Telegram](/adapters/telegram) | [GitHub](/adapters/github) | [Linear](/adapters/linear) | [WhatsApp](/adapters/whatsapp) | [Twitter](/adapters/twitter) |
|---------|-------|-------|-------------|---------|---------|--------|--------|-----------|---------|
| Post message | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Edit message | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| Delete message | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| File uploads | ✅ | ✅ | ❌ | ✅ | ⚠️ Single file | ❌ | ❌ | ✅ Images, audio, docs | ❌ |
| Streaming | ✅ Native | ⚠️ Post+Edit | ⚠️ Post+Edit | ⚠️ Post+Edit | ⚠️ Post+Edit | ❌ | ❌ | ❌ | ❌ |
| Scheduled messages | ✅ Native | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |

### Rich content

| Feature | Slack | Teams | Google Chat | Discord | Telegram | GitHub | Linear | WhatsApp |
|---------|-------|-------|-------------|---------|----------|--------|--------|-----------|
| Card format | Block Kit | Adaptive Cards | Google Chat Cards | Embeds | Markdown + inline keyboard buttons | GFM Markdown | Markdown | WhatsApp templates |
| Buttons | ✅ | ✅ | ✅ | ✅ | ⚠️ Inline keyboard callbacks | ❌ | ❌ | ✅ Interactive replies |
| Link buttons | ✅ | ✅ | ✅ | ✅ | ⚠️ Inline keyboard URLs | ❌ | ❌ | ❌ |
| Select menus | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Tables | ✅ Block Kit | ✅ GFM | ⚠️ ASCII | ✅ GFM | ⚠️ ASCII | ✅ GFM | ✅ GFM | ❌ |
| Fields | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ Template variables |
| Images in cards | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ |
| Modals | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Feature | Slack | Teams | Google Chat | Discord | Telegram | GitHub | Linear | WhatsApp | Twitter |
|---------|-------|-------|-------------|---------|----------|--------|--------|-----------|---------|
| Card format | Block Kit | Adaptive Cards | Google Chat Cards | Embeds | Markdown + inline keyboard buttons | GFM Markdown | Markdown | WhatsApp templates | Plain text |
| Buttons | ✅ | ✅ | ✅ | ✅ | ⚠️ Inline keyboard callbacks | ❌ | ❌ | ✅ Interactive replies | ❌ |
| Link buttons | ✅ | ✅ | ✅ | ✅ | ⚠️ Inline keyboard URLs | ❌ | ❌ | ❌ | ❌ |
| Select menus | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Tables | ✅ Block Kit | ✅ GFM | ⚠️ ASCII | ✅ GFM | ⚠️ ASCII | ✅ GFM | ✅ GFM | ❌ | ⚠️ ASCII |
| Fields | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ Template variables | ❌ |
| Images in cards | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ |
| Modals | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |

### Conversations

| Feature | Slack | Teams | Google Chat | Discord | Telegram | GitHub | Linear | WhatsApp |
|---------|-------|-------|-------------|---------|----------|--------|--------|-----------|
| Slash commands | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Mentions | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| Add reactions | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| Remove reactions | ✅ | ❌ | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | ❌ |
| Typing indicator | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
| DMs | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
| Ephemeral messages | ✅ Native | ❌ | ✅ Native | ❌ | ❌ | ❌ | ❌ | ❌ |
| Feature | Slack | Teams | Google Chat | Discord | Telegram | GitHub | Linear | WhatsApp | Twitter |
|---------|-------|-------|-------------|---------|----------|--------|--------|-----------|---------|
| Slash commands | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Mentions | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Add reactions | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| Remove reactions | ✅ | ❌ | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | ❌ | ❌ |
| Typing indicator | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| DMs | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ |
| Ephemeral messages | ✅ Native | ❌ | ✅ Native | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |

### Message history

| Feature | Slack | Teams | Google Chat | Discord | Telegram | GitHub | Linear | WhatsApp |
|---------|-------|-------|-------------|---------|----------|--------|--------|-----------|
| Fetch messages | ✅ | ✅ | ✅ | ✅ | ⚠️ Cached | ✅ | ✅ | ⚠️ Cached sent messages only |
| Fetch single message | ✅ | ❌ | ❌ | ❌ | ⚠️ Cached | ❌ | ❌ | ⚠️ Cached sent messages only |
| Fetch thread info | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| Fetch channel messages | ✅ | ✅ | ✅ | ✅ | ⚠️ Cached | ✅ | ❌ | ⚠️ Cached sent messages only |
| List threads | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ |
| Fetch channel info | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| Post channel message | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
| Feature | Slack | Teams | Google Chat | Discord | Telegram | GitHub | Linear | WhatsApp | Twitter |
|---------|-------|-------|-------------|---------|----------|--------|--------|-----------|---------|
| Fetch messages | ✅ | ✅ | ✅ | ✅ | ⚠️ Cached | ✅ | ✅ | ⚠️ Cached sent messages only | ⚠️ Cached |
| Fetch single message | ✅ | ❌ | ❌ | ❌ | ⚠️ Cached | ❌ | ❌ | ⚠️ Cached sent messages only | ⚠️ Cached |
| Fetch thread info | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Fetch channel messages | ✅ | ✅ | ✅ | ✅ | ⚠️ Cached | ✅ | ❌ | ⚠️ Cached sent messages only | ❌ |
| List threads | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| Fetch channel info | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| Post channel message | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ |

<Callout type="info">
⚠️ indicates partial support — the feature works with limitations. See individual adapter pages for details.
Expand Down
94 changes: 94 additions & 0 deletions packages/adapter-twitter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# @chat-adapter/twitter

[![npm version](https://img.shields.io/npm/v/@chat-adapter/twitter)](https://www.npmjs.com/package/@chat-adapter/twitter)
[![npm downloads](https://img.shields.io/npm/dm/@chat-adapter/twitter)](https://www.npmjs.com/package/@chat-adapter/twitter)

Twitter / X Webhooks adapter for [Chat SDK](https://chat-sdk.dev/docs).

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.

## Installation

```bash
npm install chat @chat-adapter/twitter
```

## Usage

```typescript
import { Chat } from "chat";
import { createTwitterAdapter } from "@chat-adapter/twitter";
import { createMemoryState } from "@chat-adapter/state-memory";

const bot = new Chat({
userName: "my_twitter_bot",
adapters: {
twitter: createTwitterAdapter({
consumerKey: process.env.TWITTER_CONSUMER_KEY!,
consumerSecret: process.env.TWITTER_CONSUMER_SECRET!,
accessToken: process.env.TWITTER_ACCESS_TOKEN!,
accessTokenSecret: process.env.TWITTER_ACCESS_TOKEN_SECRET!,
bearerToken: process.env.TWITTER_BEARER_TOKEN!,
}),
},
state: createMemoryState(), // Required for deduping
});

// Twitter DMs are treated as standard messages (not mentions)
bot.onNewMessage(async (thread, message) => {
await thread.post(`Echo: ${message.text}`);
});
```

## Environment variables

If you don't pass options into `createTwitterAdapter()`, it will automatically read from these environment variables:

| Variable | Required | Description |
|----------|----------|-------------|
| `TWITTER_CONSUMER_KEY` | Yes | App API Key |
| `TWITTER_CONSUMER_SECRET` | Yes | App API Secret |
| `TWITTER_ACCESS_TOKEN` | Yes | Bot account access token |
| `TWITTER_ACCESS_TOKEN_SECRET` | Yes | Bot account access token secret |
| `TWITTER_BEARER_TOKEN` | Yes | App Bearer token (for v2 read endpoints) |
| `TWITTER_BOT_USERNAME` | No | Override the bot display name |
| `TWITTER_WEBHOOK_ENV` | No | Account Activity environment name (default: "production") |

## Configuration

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `consumerKey` | `string` | `process.env.TWITTER_CONSUMER_KEY` | App API Key |
| `consumerSecret` | `string` | `process.env.TWITTER_CONSUMER_SECRET` | App API Secret for CRC hashing |
| `accessToken` | `string` | `process.env.TWITTER_ACCESS_TOKEN` | Bot account access token |
| `accessTokenSecret` | `string` | `process.env.TWITTER_ACCESS_TOKEN_SECRET` | Bot account access token secret |
| `bearerToken` | `string` | `process.env.TWITTER_BEARER_TOKEN` | App Bearer token |
| `userName` | `string` | `process.env.TWITTER_BOT_USERNAME` | Bot display name |
| `apiBaseUrl` | `string` | `"https://api.twitter.com"` | Override domain for testing |

## Platform setup

1. Create a project in the [X Developer Portal](https://developer.x.com).
2. Generate your **Consumer Key**, **Consumer Secret**, and **Bearer Token**.
3. Set up **OAuth 1.0a User Authentication** in your app settings with Read/Write/Direct Messages permissions.
4. Generate the **Access Token** and **Access Token Secret** for your bot account.
5. Apply for the **Account Activity API** (requires Pro or Enterprise access).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Step 5 required more research for me to understand. Some tweaks to help the next fella:

  1. "Apply for..." – cant find the application – can you attach a link?

  2. "Account Activity API" – specify version (Enterprise or V2)?

6. Start your server so the webhook endpoint is active.
7. Register your webhook URL and subscribe your bot account using the Account Activity API.

## Features

- **Direct Messages**: Receive and reply to 1-1 DMs
- **CRC Hashing**: Automatically responds to Twitter's Challenge-Response Checks
- **Media Attachments**: Extracts image and video URLs from incoming DMs
- **Plain Text Rendering**: Automatically converts markdown AST to plain text (with ASCII tables) since Twitter DMs don't support rich formatting

### Limitations
- **No Message Editing**: The Twitter API does not support editing DMs. `editMessage` throws `NotImplementedError`.
- **Typing Indicators**: The X API doesn't support bot typing indicators.
- **Rate Limits**: The DM API is subject to X's strict rate limits.
- **Premium Tier Requirement**: Requires Account Activity API access, which is not available on free or basic tiers.

## License

MIT
58 changes: 58 additions & 0 deletions packages/adapter-twitter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"name": "@chat-adapter/twitter",
"version": "0.1.0",
"description": "Twitter / X adapter for chat",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest run --coverage",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist"
},
"dependencies": {
"@chat-adapter/shared": "workspace:*"
},
"peerDependencies": {
"chat": "workspace:*"
},
"devDependencies": {
"@types/node": "^25.3.2",
"tsup": "^8.3.5",
"typescript": "^5.7.2",
"vitest": "^4.0.18"
},
"repository": {
"type": "git",
"url": "git+https://github.com/vercel/chat.git",
"directory": "packages/adapter-twitter"
},
"homepage": "https://github.com/vercel/chat#readme",
"bugs": {
"url": "https://github.com/vercel/chat/issues"
},
"publishConfig": {
"access": "public"
},
"keywords": [
"chat",
"twitter",
"x",
"bot",
"adapter"
],
"license": "MIT"
}
140 changes: 140 additions & 0 deletions packages/adapter-twitter/sample-messages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Twitter / X Sample Messages

## Direct Message Webhook Payload

This is what a standard payload looks like when a user sends a Direct Message to the bot, as delivered by the Account Activity API.

```json
{
"for_user_id": "987654321",
"direct_message_events": [
{
"type": "message_create",
"id": "1638290192839218391",
"created_timestamp": "1679354012000",
"message_create": {
"target": {
"recipient_id": "987654321"
},
"sender_id": "123456789",
"message_data": {
"text": "Hello Twitter bot!",
"entities": {
"hashtags": [],
"symbols": [],
"user_mentions": [],
"urls": []
}
}
}
}
],
"users": {
"123456789": {
"id": "123456789",
"created_timestamp": "1422556069340",
"name": "Alex Developer",
"screen_name": "alexdev",
"protected": false,
"verified": true,
"followers_count": 1500,
"friends_count": 800,
"statuses_count": 5200,
"profile_image_url_https": "https://pbs.twimg.com/profile_images/123/avatar.jpg"
},
"987654321": {
"id": "987654321",
"created_timestamp": "1512340000000",
"name": "Cool Bot",
"screen_name": "cool_bot_123",
"protected": false,
"verified": false,
"followers_count": 50,
"friends_count": 2,
"statuses_count": 15,
"profile_image_url_https": "https://pbs.twimg.com/profile_images/456/bot.jpg"
}
}
}
```

## Challenge-Response Check (CRC) Request

Twitter sporadically issues GET requests to verify webhook ownership. The URL will look like:

```text
GET https://your-domain.com/api/webhooks/twitter?crc_token=1CDehg9...
```

The adapter computes the HMAC-SHA256 of `1CDehg9...` using the `TWITTER_CONSUMER_SECRET` and responds with:

```json
{
"response_token": "sha256=MzA3O..."
}
```

## Direct Message with Media Attachment

When a user attaches an image or video to a DM.

```json
{
"for_user_id": "987654321",
"direct_message_events": [
{
"type": "message_create",
"id": "1638290192839218392",
"created_timestamp": "1679354055000",
"message_create": {
"target": {
"recipient_id": "987654321"
},
"sender_id": "123456789",
"message_data": {
"text": "Check out this screenshot https://t.co/abc123def",
"entities": {
"hashtags": [],
"symbols": [],
"user_mentions": [],
"urls": [
{
"url": "https://t.co/abc123def",
"expanded_url": "...",
"display_url": "pic.twitter.com/xyz",
"indices": [24, 46]
}
]
},
"attachment": {
"type": "media",
"media": {
"id": 16382901500000000,
"id_str": "1638290150000000000",
"media_url": "http://pbs.twimg.com/media/FxsX_Y_WYAETZ...jpg",
"media_url_https": "https://pbs.twimg.com/media/FxsX_Y_WYAETZ...jpg",
"url": "https://t.co/abc123def",
"display_url": "pic.twitter.com/xyz",
"expanded_url": "https://twitter.com/messages/media/1638290150000000000",
"type": "photo",
"sizes": {
"medium": { "w": 1200, "h": 675, "resize": "fit" },
"thumb": { "w": 150, "h": 150, "resize": "crop" },
"small": { "w": 680, "h": 383, "resize": "fit" },
"large": { "w": 1920, "h": 1080, "resize": "fit" }
}
}
}
}
}
}
],
"users": {
"123456789": {
"id": "123456789",
"name": "Alex Developer",
"screen_name": "alexdev"
}
}
}
```
Loading