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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,9 @@ node_modules
dist
.turbo
.env.test
coverage
coverage

# Defense-in-depth: ignore potential local OAuth token persistence files
oauth2.tokens.json
*.oauth2.tokens.json
.twitter-oauth*
70 changes: 55 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,36 @@ This package provides Twitter/X integration for the Eliza AI agent using the off

1. **Get Twitter Developer account** → https://developer.twitter.com
2. **Create an app** → Enable "Read and write" permissions
3. **Get OAuth 1.0a credentials** (NOT OAuth 2.0!):
- API Key & Secret (from "Consumer Keys")
- Access Token & Secret (from "Authentication Tokens")
3. Choose your auth mode:

- **Option A (default, legacy): OAuth 1.0a env vars**
- API Key & Secret (from "Consumer Keys")
- Access Token & Secret (from "Authentication Tokens")

- **Option B (recommended): “login + approve” OAuth 2.0 (PKCE)**
- Client ID (from "OAuth 2.0 Client ID")
- Redirect URI (loopback recommended)

4. **Add to `.env`:**
```bash
# Option A: legacy OAuth 1.0a (default)
TWITTER_AUTH_MODE=env
TWITTER_API_KEY=xxx
TWITTER_API_SECRET_KEY=xxx
TWITTER_ACCESS_TOKEN=xxx
TWITTER_ACCESS_TOKEN_SECRET=xxx

# Option B: OAuth 2.0 PKCE (interactive login + approve, no client secret)
# TWITTER_AUTH_MODE=oauth
# TWITTER_CLIENT_ID=xxx
# TWITTER_REDIRECT_URI=http://127.0.0.1:8080/callback

TWITTER_ENABLE_POST=true
TWITTER_POST_IMMEDIATELY=true
```
5. **Run:** `bun start`

⚠️ **Common mistake:** Using OAuth 2.0 credentials instead of OAuth 1.0a - see [Step 3](#step-3-get-the-right-credentials-oauth-10a) for details!
Tip: if you use **OAuth 2.0 PKCE**, the plugin will print an authorization URL on first run and store tokens for you (no manual token pasting).

## Features

Expand All @@ -39,7 +54,7 @@ This package provides Twitter/X integration for the Eliza AI agent using the off
## Prerequisites

- Twitter Developer Account with API v2 access
- Twitter OAuth 1.0a credentials (NOT OAuth 2.0)
- Either Twitter OAuth 1.0a credentials (legacy env vars) or OAuth 2.0 Client ID (PKCE)
- Node.js and bun installed

## 🚀 Quick Start
Expand Down Expand Up @@ -77,12 +92,12 @@ This package provides Twitter/X integration for the Eliza AI agent using the off

### Step 3: Get the RIGHT Credentials (OAuth 1.0a)

**⚠️ IMPORTANT: You need OAuth 1.0a credentials, NOT OAuth 2.0!**
You can use either legacy **OAuth 1.0a** env vars (default) or **OAuth 2.0 PKCE** (“login + approve”).

In your app's **"Keys and tokens"** page, you'll see several sections. Here's what to use:

```
✅ USE THESE (OAuth 1.0a):
✅ USE THESE when TWITTER_AUTH_MODE=env (OAuth 1.0a):
┌─────────────────────────────────────────────────┐
│ Consumer Keys │
│ ├─ API Key: xxx...xxx → TWITTER_API_KEY │
Expand All @@ -93,13 +108,13 @@ In your app's **"Keys and tokens"** page, you'll see several sections. Here's wh
│ └─ Access Token Secret: xxx → TWITTER_ACCESS_TOKEN_SECRET │
└─────────────────────────────────────────────────┘

❌ DO NOT USE THESE (OAuth 2.0):
USE THESE when TWITTER_AUTH_MODE=oauth (OAuth 2.0 PKCE):
┌─────────────────────────────────────────────────┐
│ OAuth 2.0 Client ID and Client Secret │
│ ├─ Client ID: xxx...xxx ← IGNORE
│ └─ Client Secret: xxx...xxx ← IGNORE
│ ├─ Client ID: xxx...xxx → TWITTER_CLIENT_ID
│ └─ Client Secret: xxx...xxx ← NOT USED (do not put in env)
│ │
│ Bearer Token ← IGNORE
│ Bearer Token ← NOT USED
└─────────────────────────────────────────────────┘
```

Expand All @@ -113,6 +128,12 @@ In your app's **"Keys and tokens"** page, you'll see several sections. Here's wh
Create or edit `.env` file in your project root:

```bash
# Auth mode (default: env)
# - env: legacy OAuth 1.0a keys/tokens
# - oauth: “login + approve” OAuth 2.0 PKCE (no client secret in plugin)
# - broker: stub (not implemented yet)
TWITTER_AUTH_MODE=env

# REQUIRED: OAuth 1.0a Credentials (from "Consumer Keys" section)
TWITTER_API_KEY=your_api_key_here # From "API Key"
TWITTER_API_SECRET_KEY=your_api_key_secret_here # From "API Key Secret"
Expand All @@ -121,6 +142,14 @@ TWITTER_API_SECRET_KEY=your_api_key_secret_here # From "API Key Secret"
TWITTER_ACCESS_TOKEN=your_access_token_here # Must have "Read and Write"
TWITTER_ACCESS_TOKEN_SECRET=your_token_secret_here # Regenerate after permission change

# ---- OR ----
# OAuth 2.0 PKCE (“login + approve”) configuration:
# TWITTER_AUTH_MODE=oauth
# TWITTER_CLIENT_ID=your_oauth2_client_id_here
# TWITTER_REDIRECT_URI=http://127.0.0.1:8080/callback
# Optional:
# TWITTER_SCOPES="tweet.read tweet.write users.read offline.access"

# Basic Configuration
TWITTER_DRY_RUN=false # Set to true to test without posting
TWITTER_ENABLE_POST=true # Enable autonomous tweet posting
Expand All @@ -133,6 +162,11 @@ TWITTER_POST_INTERVAL_MIN=90 # Minimum minutes between posts
TWITTER_POST_INTERVAL_MAX=150 # Maximum minutes between posts
```

When using **TWITTER_AUTH_MODE=oauth**, the plugin will:
- Print an authorization URL on first run
- Capture the callback via a local loopback server **or** ask you to paste the redirected URL
- Persist tokens via Eliza runtime cache if available, otherwise a local token file at `~/.eliza/twitter/oauth2.tokens.json`

### Step 5: Run Your Bot

```typescript
Expand Down Expand Up @@ -348,12 +382,17 @@ This is the #1 issue! Your app has read-only permissions.

### "Could not authenticate you"

Wrong credentials or using OAuth 2.0 instead of OAuth 1.0a.
This usually means your credentials don’t match your selected auth mode.

**Solution:**
- Use credentials from "Consumer Keys" section (API Key/Secret)
- Use credentials from "Authentication Tokens" section (Access Token/Secret)
- Do NOT use OAuth 2.0 Client ID, Client Secret, or Bearer Token
- If `TWITTER_AUTH_MODE=env`:
- Use credentials from "Consumer Keys" section (API Key/Secret)
- Use credentials from "Authentication Tokens" section (Access Token/Secret)
- Do not use OAuth 2.0 Client ID/Client Secret/Bearer Token for this mode
- If `TWITTER_AUTH_MODE=oauth`:
- Use OAuth 2.0 **Client ID** (`TWITTER_CLIENT_ID`)
- Set a loopback redirect URI (`TWITTER_REDIRECT_URI`, e.g. `http://127.0.0.1:8080/callback`)
- Do not set/ship a client secret (PKCE flow)

### Bot Not Posting Automatically

Expand Down Expand Up @@ -460,6 +499,7 @@ Monitor your usage at: https://developer.twitter.com/en/portal/dashboard

- [Twitter API v2 Documentation](https://developer.twitter.com/en/docs/twitter-api)
- [Twitter OAuth 1.0a Guide](https://developer.twitter.com/en/docs/authentication/oauth-1-0a)
- [Twitter OAuth 2.0 (Authorization Code with PKCE)](https://developer.twitter.com/en/docs/authentication/oauth-2-0/authorization-code)
- [Rate Limits Reference](https://developer.twitter.com/en/docs/twitter-api/rate-limits)
- [ElizaOS Documentation](https://github.com/elizaos/eliza)

Expand Down
48 changes: 40 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,30 +52,62 @@
"agentConfig": {
"pluginType": "elizaos:plugin:1.0.0",
"pluginParameters": {
"TWITTER_AUTH_MODE": {
"type": "string",
"description": "Auth mode: 'env' (legacy keys/tokens), 'oauth' (3-legged OAuth2 PKCE), or 'broker' (stub).",
"required": false,
"default": "env",
"sensitive": false
},
"TWITTER_API_KEY": {
"type": "string",
"description": "Twitter API v2 key for authentication.",
"required": true,
"description": "Twitter API key (required for TWITTER_AUTH_MODE=env).",
"required": false,
"sensitive": true
},
"TWITTER_API_SECRET_KEY": {
"type": "string",
"description": "Twitter API v2 secret key paired with the API key for authentication.",
"required": true,
"description": "Twitter API secret key (required for TWITTER_AUTH_MODE=env).",
"required": false,
"sensitive": true
},
"TWITTER_ACCESS_TOKEN": {
"type": "string",
"description": "OAuth access token for Twitter API v2 requests.",
"required": true,
"description": "OAuth1.0a access token (required for TWITTER_AUTH_MODE=env).",
"required": false,
"sensitive": true
},
"TWITTER_ACCESS_TOKEN_SECRET": {
"type": "string",
"description": "OAuth access token secret used with the access token for authentication.",
"required": true,
"description": "OAuth1.0a access token secret (required for TWITTER_AUTH_MODE=env).",
"required": false,
"sensitive": true
},
"TWITTER_CLIENT_ID": {
"type": "string",
"description": "Twitter OAuth2 client ID (required for TWITTER_AUTH_MODE=oauth).",
"required": false,
"sensitive": false
},
"TWITTER_REDIRECT_URI": {
"type": "string",
"description": "OAuth2 redirect URI (loopback recommended) (required for TWITTER_AUTH_MODE=oauth).",
"required": false,
"sensitive": false
},
"TWITTER_SCOPES": {
"type": "string",
"description": "OAuth2 scopes (space-separated).",
"required": false,
"default": "tweet.read tweet.write users.read offline.access",
"sensitive": false
},
"TWITTER_BROKER_URL": {
"type": "string",
"description": "Broker URL (required for TWITTER_AUTH_MODE=broker; stub only).",
"required": false,
"sensitive": false
},
"TWITTER_TARGET_USERS": {
"type": "string",
"description": "Comma-separated list of Twitter usernames the bot should interact with. Use '*' for all users.",
Expand Down
6 changes: 4 additions & 2 deletions src/__tests__/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@ npm test -- --coverage

### End-to-End Tests

E2E tests require real Twitter Developer API credentials.
E2E tests require real Twitter Developer API credentials and currently exercise **TWITTER_AUTH_MODE=env** (OAuth 1.0a keys/tokens).
The plugin also supports **TWITTER_AUTH_MODE=oauth** (OAuth 2.0 PKCE “login + approve”), but that flow is interactive and is not covered by these E2E tests.

#### Prerequisites

1. **Twitter Developer Account**: You need a Twitter Developer account with an app created
2. **API Credentials**: You need all four credentials:
2. **API Credentials (env mode)**: You need all four credentials:
- API Key (Consumer Key)
- API Secret Key (Consumer Secret)
- Access Token
Expand All @@ -51,6 +52,7 @@ E2E tests require real Twitter Developer API credentials.

```env
# Twitter API v2 Credentials
TWITTER_AUTH_MODE=env
TWITTER_API_KEY=your_api_key_here
TWITTER_API_SECRET_KEY=your_api_secret_key_here
TWITTER_ACCESS_TOKEN=your_access_token_here
Expand Down
22 changes: 16 additions & 6 deletions src/__tests__/TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,27 @@

## Overview

This guide explains how to test the refactored Twitter plugin after removing username/password authentication and Twitter Spaces functionality. The plugin now exclusively uses Twitter API v2 with developer credentials.
This guide explains how to test the Twitter plugin after removing username/password authentication and Twitter Spaces functionality.

The plugin supports multiple auth modes:
- `TWITTER_AUTH_MODE=env` (legacy OAuth 1.0a keys/tokens)
- `TWITTER_AUTH_MODE=oauth` (OAuth 2.0 Authorization Code + PKCE, interactive “login + approve”, no client secret)
- `TWITTER_AUTH_MODE=broker` (stub only, not implemented yet)

## Prerequisites

### 1. Twitter Developer Account

You need a Twitter Developer account with:
You need a Twitter Developer account. Which credentials you need depends on auth mode:

- API Key
- API Secret Key
- Access Token
- Access Token Secret
- For `TWITTER_AUTH_MODE=env` (E2E tests use this):
- API Key
- API Secret Key
- Access Token
- Access Token Secret
- For `TWITTER_AUTH_MODE=oauth`:
- OAuth 2.0 Client ID (`TWITTER_CLIENT_ID`)
- Redirect URI (`TWITTER_REDIRECT_URI`)

To get these credentials:

Expand All @@ -27,6 +36,7 @@ To get these credentials:
Create a `.env.test` file in the plugin root directory:

```bash
TWITTER_AUTH_MODE=env
TWITTER_API_KEY=your_api_key_here
TWITTER_API_SECRET_KEY=your_api_secret_key_here
TWITTER_ACCESS_TOKEN=your_access_token_here
Expand Down
2 changes: 0 additions & 2 deletions src/__tests__/e2e/twitter-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
beforeEach,
vi,
} from "vitest";
import { TwitterAuth } from "../../client/auth";
import { TwitterMessageService } from "../../services/MessageService";
import { TwitterPostService } from "../../services/PostService";
import { ClientBase } from "../../base";
Expand All @@ -27,7 +26,6 @@ const SKIP_E2E =
!process.env.TWITTER_ACCESS_TOKEN_SECRET;

describe.skipIf(SKIP_E2E)("Twitter E2E Integration Tests", () => {
let auth: TwitterAuth;
let client: ClientBase;
let messageService: TwitterMessageService;
let postService: TwitterPostService;
Expand Down
50 changes: 49 additions & 1 deletion src/__tests__/environment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ describe("Environment Configuration", () => {
vi.stubEnv("TWITTER_API_SECRET_KEY", "");
vi.stubEnv("TWITTER_ACCESS_TOKEN", "");
vi.stubEnv("TWITTER_ACCESS_TOKEN_SECRET", "");
vi.stubEnv("TWITTER_AUTH_MODE", "");
vi.stubEnv("TWITTER_CLIENT_ID", "");
vi.stubEnv("TWITTER_REDIRECT_URI", "");
vi.stubEnv("TWITTER_BROKER_URL", "");
});

describe("shouldTargetUser", () => {
Expand Down Expand Up @@ -87,7 +91,51 @@ describe("Environment Configuration", () => {
mockRuntime.getSetting = vi.fn(() => undefined);

await expect(validateTwitterConfig(mockRuntime)).rejects.toThrow(
"Twitter API credentials are required",
"Twitter env auth is selected",
);
});

it("should validate oauth mode without legacy env credentials", async () => {
mockRuntime.getSetting = vi.fn((key) => {
const settings: Record<string, string> = {
TWITTER_AUTH_MODE: "oauth",
TWITTER_CLIENT_ID: "client-id",
TWITTER_REDIRECT_URI: "http://127.0.0.1:8080/callback",
};
return settings[key];
});

const config = await validateTwitterConfig(mockRuntime);
expect(config.TWITTER_AUTH_MODE).toBe("oauth");
expect(config.TWITTER_CLIENT_ID).toBe("client-id");
expect(config.TWITTER_REDIRECT_URI).toBe("http://127.0.0.1:8080/callback");
});

it("should throw when oauth mode is missing required fields", async () => {
mockRuntime.getSetting = vi.fn((key) => {
const settings: Record<string, string> = {
TWITTER_AUTH_MODE: "oauth",
TWITTER_CLIENT_ID: "client-id",
// missing redirect uri
};
return settings[key];
});

await expect(validateTwitterConfig(mockRuntime)).rejects.toThrow(
"Twitter OAuth is selected",
);
});

it("should throw when broker mode is missing broker url", async () => {
mockRuntime.getSetting = vi.fn((key) => {
const settings: Record<string, string> = {
TWITTER_AUTH_MODE: "broker",
};
return settings[key];
});

await expect(validateTwitterConfig(mockRuntime)).rejects.toThrow(
"Twitter broker auth is selected",
);
});

Expand Down
Loading