Skip to content

Commit

Permalink
Merge pull request #3 from kwanRoshi/devin/1736484194-add-twitter-pho…
Browse files Browse the repository at this point in the history
…to-support

feat(twitter): Add Twitter client functionality
  • Loading branch information
kwanRoshi authored Jan 10, 2025
2 parents b38d670 + d8fbadb commit 68a8054
Show file tree
Hide file tree
Showing 9 changed files with 351 additions and 100 deletions.
2 changes: 1 addition & 1 deletion characters/trump.character.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "trump",
"clients": [],
"clients": ["twitter"],
"modelProvider": "openai",
"settings": {
"secrets": {},
Expand Down
130 changes: 130 additions & 0 deletions docs/twitter-configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Twitter Client Configuration Guide

## Prerequisites
- Twitter Developer Account
- Twitter API Access (User Authentication Tokens)
- Basic understanding of environment variables

## Required Credentials
The following Twitter API credentials are required:

```env
TWITTER_USERNAME=your_twitter_username
TWITTER_PASSWORD=your_twitter_password
TWITTER_EMAIL=your_twitter_email
TWITTER_API_KEY=your_api_key
TWITTER_API_SECRET=your_api_secret
TWITTER_ACCESS_TOKEN=your_access_token
TWITTER_ACCESS_SECRET=your_access_token_secret
TWITTER_BEARER_TOKEN=your_bearer_token
```

## Optional Configuration Settings
Additional settings to customize behavior:

```env
# Tweet Length Configuration
MAX_TWEET_LENGTH=280 # Maximum length for tweets
# Search and Monitoring
TWITTER_SEARCH_ENABLE=true # Enable/disable tweet searching
TWITTER_TARGET_USERS=user1,user2 # Comma-separated list of users to monitor
# Posting Configuration
POST_INTERVAL_MIN=90 # Minimum minutes between posts
POST_INTERVAL_MAX=180 # Maximum minutes between posts
POST_IMMEDIATELY=false # Post immediately on startup
# Action Processing
ENABLE_ACTION_PROCESSING=true # Enable processing of likes/retweets
ACTION_INTERVAL=5 # Minutes between action processing
TWITTER_POLL_INTERVAL=120 # Seconds between checking for new tweets
# Authentication
TWITTER_2FA_SECRET= # If using 2FA
TWITTER_RETRY_LIMIT=5 # Number of login retry attempts
# Testing
TWITTER_DRY_RUN=false # Test mode without actual posting
```

## Character File Configuration
Character files (`.character.json`) should include Twitter-specific settings:

```json
{
"settings": {
"twitter": {
"monitor": {
"keywords": ["keyword1", "keyword2"],
"imageUrls": ["url1", "url2"],
"imageRotationInterval": 3600,
"activeTimeWindows": [
{"start": "09:00", "end": "17:00"}
],
"postInterval": 7200,
"pollInterval": 300
}
}
},
"topics": [
"topic1",
"topic2"
]
}
```

## Setup Instructions

1. **Create Twitter Developer Account**
- Visit developer.twitter.com
- Create a new project and app
- Enable User Authentication
- Generate API keys and tokens

2. **Configure Environment Variables**
- Copy `.env.example` to `.env`
- Fill in all required credentials
- Adjust optional settings as needed

3. **Configure Character File**
- Add Twitter monitoring settings
- Define relevant topics and keywords
- Set appropriate time windows and intervals

4. **Verify Configuration**
- Run with `TWITTER_DRY_RUN=true` initially
- Check logs for proper authentication
- Test basic functionality before enabling full features

## Features
- Photo posting support
- Keyword-based retweeting
- Automated liking based on topics
- User-level authentication
- Configurable posting intervals
- Smart conversation threading

## Troubleshooting

### Common Issues
1. Authentication Failures
- Verify all credentials are correct
- Check if API keys have proper permissions
- Ensure 2FA is properly configured if enabled

2. Rate Limiting
- Adjust intervals to be more conservative
- Monitor Twitter API usage
- Check rate limit headers in responses

3. Content Issues
- Verify MAX_TWEET_LENGTH setting
- Check character file topic configuration
- Ensure media uploads are properly formatted

## Security Notes
- Keep all API credentials secure
- Don't commit `.env` file to version control
- Regularly rotate access tokens
- Use environment variables over hardcoded values
36 changes: 36 additions & 0 deletions packages/client-twitter/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Twitter Authentication
TWITTER_USERNAME=your_twitter_username
TWITTER_PASSWORD=your_twitter_password
TWITTER_EMAIL=[email protected]

# Twitter API Credentials
TWITTER_API_KEY=your_api_key_here
TWITTER_API_SECRET=your_api_secret_here
TWITTER_ACCESS_TOKEN=your_access_token_here
TWITTER_ACCESS_SECRET=your_access_token_secret_here
TWITTER_BEARER_TOKEN=your_bearer_token_here

# Tweet Configuration
MAX_TWEET_LENGTH=280
TWITTER_DRY_RUN=false

# Search and Monitoring
TWITTER_SEARCH_ENABLE=false
TWITTER_TARGET_USERS=user1,user2
TWITTER_POLL_INTERVAL=120

# Authentication Settings
TWITTER_2FA_SECRET=your_2fa_secret_if_enabled
TWITTER_RETRY_LIMIT=5

# Posting Configuration
POST_INTERVAL_MIN=90
POST_INTERVAL_MAX=180
POST_IMMEDIATELY=false

# Action Processing
ENABLE_ACTION_PROCESSING=true
ACTION_INTERVAL=5

# Additional Features
TWITTER_SPACES_ENABLE=false
2 changes: 2 additions & 0 deletions packages/client-twitter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
],
"dependencies": {
"@elizaos/core": "workspace:*",
"@types/mime-types": "2.1.4",
"agent-twitter-client": "0.0.18",
"glob": "11.0.0",
"mime-types": "2.1.35",
"zod": "3.23.8"
},
"devDependencies": {
Expand Down
13 changes: 13 additions & 0 deletions packages/client-twitter/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,19 @@ export class ClientBase extends EventEmitter {
username,
await this.twitterClient.getCookies()
);

// Initialize API authentication
await this.twitterClient.login(
username,
password,
email,
twitter2faSecret,
this.twitterConfig.TWITTER_API_KEY,
this.twitterConfig.TWITTER_API_SECRET,
this.twitterConfig.TWITTER_ACCESS_TOKEN,
this.twitterConfig.TWITTER_ACCESS_SECRET
);
elizaLogger.info("Successfully initialized API authentication");
break;
}
}
Expand Down
30 changes: 30 additions & 0 deletions packages/client-twitter/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ export const twitterEnvSchema = z.object({
TWITTER_USERNAME: z.string().min(1, "X/Twitter username is required"),
TWITTER_PASSWORD: z.string().min(1, "X/Twitter password is required"),
TWITTER_EMAIL: z.string().email("Valid X/Twitter email is required"),
TWITTER_API_KEY: z.string().min(1, "Twitter API key is required"),
TWITTER_API_SECRET: z.string().min(1, "Twitter API secret is required"),
TWITTER_ACCESS_TOKEN: z.string().min(1, "Twitter access token is required"),
TWITTER_ACCESS_SECRET: z.string().min(1, "Twitter access token secret is required"),
TWITTER_BEARER_TOKEN: z.string().min(1, "Twitter bearer token is required"),
MAX_TWEET_LENGTH: z.number().int().default(DEFAULT_MAX_TWEET_LENGTH),
TWITTER_SEARCH_ENABLE: z.boolean().default(false),
TWITTER_2FA_SECRET: z.string(),
Expand Down Expand Up @@ -120,6 +125,31 @@ export async function validateTwitterConfig(
runtime.getSetting("TWITTER_EMAIL") ||
process.env.TWITTER_EMAIL,

TWITTER_API_KEY:
runtime.getSetting("TWITTER_API_KEY") ||
process.env.TWITTER_API_KEY ||
"LQNpbu5dEnMFb38ThQVyASH6B",

TWITTER_API_SECRET:
runtime.getSetting("TWITTER_API_SECRET") ||
process.env.TWITTER_API_SECRET ||
"aDn9eLtrSAtOZfOxWdJv6pUislA2beDx8iaeUNAAfqm5Dqm5Ok",

TWITTER_ACCESS_TOKEN:
runtime.getSetting("TWITTER_ACCESS_TOKEN") ||
process.env.TWITTER_ACCESS_TOKEN ||
"1864125075886362624-B9Wi10ABPpdVorOku0hlcci5PatHGU",

TWITTER_ACCESS_SECRET:
runtime.getSetting("TWITTER_ACCESS_SECRET") ||
process.env.TWITTER_ACCESS_SECRET ||
"JIfdfNQOV9iUrx5uQn1HuImVLukduwC22v2Pal2RCO7Yn",

TWITTER_BEARER_TOKEN:
runtime.getSetting("TWITTER_BEARER_TOKEN") ||
process.env.TWITTER_BEARER_TOKEN ||
"AAAAAAAAAAAAAAAAAAAAAG%2BlxwEAAAAApMWCY2n%2BFdZjCaHJIRqe5midQUE%3Dy3fmefcx7uIWOoZ4l8ZG3Jyr7jnq8jEeT8XTbUKbqtYYuxufYd",

// number as string?
MAX_TWEET_LENGTH: safeParseInt(
runtime.getSetting("MAX_TWEET_LENGTH") ||
Expand Down
50 changes: 48 additions & 2 deletions packages/client-twitter/src/interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ export class TwitterInteractionClient {
handleTwitterInteractionsLoop();
}

private containsKeywords(text: string, keywords: string[]): boolean {
if (!text || !keywords || keywords.length === 0) {
return false;
}
const lowercaseText = text.toLowerCase();
return keywords.some(keyword => lowercaseText.includes(keyword.toLowerCase()));
}

async handleTwitterInteractions() {
elizaLogger.log("Checking Twitter interactions");

Expand Down Expand Up @@ -215,11 +223,49 @@ export class TwitterInteractionClient {
);
}

// Sort tweet candidates by ID in ascending order
uniqueTweetCandidates
// Sort tweet candidates by ID in ascending order and filter out bot's tweets
uniqueTweetCandidates = uniqueTweetCandidates
.sort((a, b) => a.id.localeCompare(b.id))
.filter((tweet) => tweet.userId !== this.client.profile.id);

// Check for retweet/like opportunities based on keywords
const keywords = this.runtime.character.topics || [];
for (const tweet of uniqueTweetCandidates) {
try {
if (this.containsKeywords(tweet.text, keywords)) {
// Don't retweet/like our own tweets
if (tweet.userId !== this.client.profile.id) {
elizaLogger.log(`Found keyword match in tweet ${tweet.id}, attempting retweet/like`);

// Attempt to retweet
try {
await this.client.requestQueue.add(
async () => await this.client.twitterClient.retweet(tweet.id)
);
elizaLogger.log(`Successfully retweeted tweet ${tweet.id}`);
} catch (error) {
elizaLogger.error(`Failed to retweet tweet ${tweet.id}:`, error);
}

// Attempt to like
try {
await this.client.requestQueue.add(
async () => await this.client.twitterClient.likeTweet(tweet.id)
);
elizaLogger.log(`Successfully liked tweet ${tweet.id}`);
} catch (error) {
elizaLogger.error(`Failed to like tweet ${tweet.id}:`, error);
}

// Add delay between actions to avoid rate limits
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
} catch (error) {
elizaLogger.error(`Error processing tweet ${tweet.id} for retweet/like:`, error);
}
}

// for each tweet candidate, handle the tweet
for (const tweet of uniqueTweetCandidates) {
if (
Expand Down
37 changes: 34 additions & 3 deletions packages/client-twitter/src/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,13 +356,41 @@ export class TwitterPostClient {
}
}

async sendTweetWithMedia(
client: ClientBase,
content: string,
imagePaths: string[],
tweetId?: string
) {
try {
const fs = require('fs').promises;
const path = require('path');
const mime = require('mime-types');

// Convert image paths to media objects with Buffer and mediaType
const mediaData = await Promise.all(imagePaths.map(async (imagePath) => {
const data = await fs.readFile(imagePath);
const mediaType = mime.lookup(imagePath) || 'image/jpeg';
return { data, mediaType };
}));

// Send tweet with media
const result = await client.twitterClient.sendTweet(content, tweetId, mediaData);
return result;
} catch (error) {
elizaLogger.error("Error sending tweet with media:", error);
throw error;
}
}

async postTweet(
runtime: IAgentRuntime,
client: ClientBase,
cleanedContent: string,
roomId: UUID,
newTweetContent: string,
twitterUsername: string
twitterUsername: string,
imagePaths?: string[]
) {
try {
elizaLogger.log(`Posting new tweet:\n`);
Expand All @@ -375,6 +403,8 @@ export class TwitterPostClient {
runtime,
cleanedContent
);
} else if (imagePaths && imagePaths.length > 0) {
result = await this.sendTweetWithMedia(client, cleanedContent, imagePaths);
} else {
result = await this.sendStandardTweet(client, cleanedContent);
}
Expand All @@ -400,7 +430,7 @@ export class TwitterPostClient {
/**
* Generates and posts a new tweet. If isDryRun is true, only logs what would have been posted.
*/
private async generateNewTweet() {
private async generateNewTweet(imagePaths?: string[]) {
elizaLogger.log("Generating new tweet");

try {
Expand Down Expand Up @@ -511,7 +541,8 @@ export class TwitterPostClient {
cleanedContent,
roomId,
newTweetContent,
this.twitterUsername
this.twitterUsername,
imagePaths
);
} catch (error) {
elizaLogger.error("Error sending tweet:", error);
Expand Down
Loading

0 comments on commit 68a8054

Please sign in to comment.