init
This commit is contained in:
10
.claude/settings.local.json
Normal file
10
.claude/settings.local.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(tree:*)",
|
||||||
|
"Bash(mvn clean compile:*)"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": []
|
||||||
|
}
|
||||||
|
}
|
||||||
17
.env.example
Normal file
17
.env.example
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# OpenAI API Configuration
|
||||||
|
OPENAI_API_KEY=your_openai_api_key_here
|
||||||
|
|
||||||
|
# Twitter API Configuration (OAuth 2.0)
|
||||||
|
TWITTER_API_KEY=your_twitter_api_key_here
|
||||||
|
TWITTER_API_SECRET=your_twitter_api_secret_here
|
||||||
|
TWITTER_ACCESS_TOKEN=your_twitter_access_token_here
|
||||||
|
TWITTER_ACCESS_TOKEN_SECRET=your_twitter_access_token_secret_here
|
||||||
|
TWITTER_BEARER_TOKEN=your_twitter_bearer_token_here
|
||||||
|
|
||||||
|
# Tweet Generation Configuration
|
||||||
|
TWEET_PROMPT=Write a short, engaging tweet about technology trends
|
||||||
|
|
||||||
|
# Image Configuration (Optional)
|
||||||
|
# Path to a local image file to attach to every tweet (jpg, png, gif)
|
||||||
|
# Leave empty or comment out to post tweets without images
|
||||||
|
IMAGE_PATH=/path/to/your/image.jpg
|
||||||
44
.gitignore
vendored
Normal file
44
.gitignore
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Environment variables (IMPORTANT: Never commit API keys!)
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Maven
|
||||||
|
target/
|
||||||
|
pom.xml.tag
|
||||||
|
pom.xml.releaseBackup
|
||||||
|
pom.xml.versionsBackup
|
||||||
|
pom.xml.next
|
||||||
|
release.properties
|
||||||
|
dependency-reduced-pom.xml
|
||||||
|
buildNumber.properties
|
||||||
|
.mvn/timing.properties
|
||||||
|
.mvn/wrapper/maven-wrapper.jar
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
tweetbot.log
|
||||||
|
|
||||||
|
# Compiled class files
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Package Files
|
||||||
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.nar
|
||||||
|
*.ear
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rar
|
||||||
|
|
||||||
|
# Java
|
||||||
|
hs_err_pid*
|
||||||
|
replay_pid*
|
||||||
432
README.md
Normal file
432
README.md
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
# TweetBot - AI-Powered Twitter Bot
|
||||||
|
|
||||||
|
An automated Twitter bot that generates and posts tweets using OpenAI's ChatGPT API. This Java application allows you to create engaging social media content with custom prompts.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Generate tweet content using ChatGPT (GPT-3.5-turbo)
|
||||||
|
- **Three operation modes:**
|
||||||
|
- Interactive mode with manual approval
|
||||||
|
- Auto-post mode for one-time tweets
|
||||||
|
- Scheduled mode (posts every 30 minutes)
|
||||||
|
- **Attach images to tweets** - Include a static image from local file system with every tweet
|
||||||
|
- Automatic tweet posting at regular intervals
|
||||||
|
- Customizable prompts for different content types
|
||||||
|
- Character limit validation (280 characters)
|
||||||
|
- Graceful error handling with automatic retry
|
||||||
|
- Comprehensive logging (console and file)
|
||||||
|
- Environment-based configuration
|
||||||
|
- Background service support
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Java 17 or higher
|
||||||
|
- Maven 3.6 or higher
|
||||||
|
- OpenAI API key
|
||||||
|
- Twitter Developer Account with API credentials
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### 1. Get OpenAI API Key
|
||||||
|
|
||||||
|
1. Go to [OpenAI Platform](https://platform.openai.com/)
|
||||||
|
2. Sign up or log in to your account
|
||||||
|
3. Navigate to API keys section
|
||||||
|
4. Create a new API key and save it securely
|
||||||
|
|
||||||
|
### 2. Get Twitter API Credentials
|
||||||
|
|
||||||
|
1. Go to [Twitter Developer Portal](https://developer.twitter.com/en/portal/dashboard)
|
||||||
|
2. Create a new project and app (or use existing)
|
||||||
|
3. Navigate to your app's "Keys and tokens" section
|
||||||
|
4. Generate and save the following credentials:
|
||||||
|
- API Key (Consumer Key)
|
||||||
|
- API Secret (Consumer Secret)
|
||||||
|
- Access Token
|
||||||
|
- Access Token Secret
|
||||||
|
- Bearer Token
|
||||||
|
|
||||||
|
**Important:** Ensure your Twitter app has **Read and Write** permissions:
|
||||||
|
- Go to your app settings
|
||||||
|
- Navigate to "User authentication settings"
|
||||||
|
- Set app permissions to "Read and Write"
|
||||||
|
|
||||||
|
### 3. Configure the Application
|
||||||
|
|
||||||
|
1. Copy the example environment file:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Edit `.env` and add your API credentials:
|
||||||
|
```
|
||||||
|
OPENAI_API_KEY=sk-your-openai-api-key-here
|
||||||
|
TWITTER_API_KEY=your-twitter-api-key
|
||||||
|
TWITTER_API_SECRET=your-twitter-api-secret
|
||||||
|
TWITTER_ACCESS_TOKEN=your-twitter-access-token
|
||||||
|
TWITTER_ACCESS_TOKEN_SECRET=your-twitter-access-token-secret
|
||||||
|
TWITTER_BEARER_TOKEN=your-twitter-bearer-token
|
||||||
|
TWEET_PROMPT=Write a short, engaging tweet about technology trends
|
||||||
|
IMAGE_PATH=/path/to/your/image.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Customize the `TWEET_PROMPT` to match your desired content theme
|
||||||
|
|
||||||
|
4. **(Optional) Configure Image Attachment:**
|
||||||
|
- Set `IMAGE_PATH` to the absolute path of an image file (jpg, png, gif)
|
||||||
|
- This image will be attached to every tweet
|
||||||
|
- Leave empty or comment out to post text-only tweets
|
||||||
|
- Example: `IMAGE_PATH=/home/user/images/logo.png`
|
||||||
|
- Supported formats: JPEG, PNG, GIF
|
||||||
|
- Maximum file size: 5MB (Twitter limit)
|
||||||
|
|
||||||
|
### 5. Build the Project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn clean package
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
- Download all dependencies
|
||||||
|
- Compile the source code
|
||||||
|
- Run tests (if any)
|
||||||
|
- Create an executable JAR file in the `target` directory
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
TweetBot supports three modes of operation:
|
||||||
|
|
||||||
|
### 1. Interactive Mode (Default)
|
||||||
|
|
||||||
|
Generate a tweet and manually approve before posting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar target/tweetbot-1.0.0.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
Or using Maven:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn exec:java -Dexec.mainClass="com.voidcode.tweetbot.TweetBot"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Scheduled Mode (Auto-post every 30 minutes)
|
||||||
|
|
||||||
|
Run continuously and automatically post tweets every 30 minutes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar target/tweetbot-1.0.0.jar --schedule
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with a custom prompt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar target/tweetbot-1.0.0.jar --schedule "Write daily tech tips"
|
||||||
|
```
|
||||||
|
|
||||||
|
The bot will:
|
||||||
|
- Post immediately when started
|
||||||
|
- Post a new tweet every 30 minutes
|
||||||
|
- Run continuously until stopped with Ctrl+C
|
||||||
|
- Automatically recover from errors and retry at the next interval
|
||||||
|
|
||||||
|
**Note:** In scheduled mode, tweets are posted automatically without manual confirmation.
|
||||||
|
|
||||||
|
### 3. Auto-Post Mode (One-time)
|
||||||
|
|
||||||
|
Generate and post a single tweet automatically without confirmation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar target/tweetbot-1.0.0.jar --auto
|
||||||
|
```
|
||||||
|
|
||||||
|
With custom prompt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar target/tweetbot-1.0.0.jar --auto "Write a motivational quote about perseverance"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command-Line Options
|
||||||
|
|
||||||
|
| Option | Short | Description |
|
||||||
|
|--------|-------|-------------|
|
||||||
|
| `--schedule` | `-s` | Run in scheduled mode (posts every 30 minutes) |
|
||||||
|
| `--auto` | `-a` | Auto-post without confirmation (one-time) |
|
||||||
|
| `--help` | `-h` | Show help message |
|
||||||
|
|
||||||
|
### Example Output
|
||||||
|
|
||||||
|
**Interactive Mode:**
|
||||||
|
```
|
||||||
|
==========================================================
|
||||||
|
Generated Tweet:
|
||||||
|
==========================================================
|
||||||
|
The future of AI is not about replacing humans, but
|
||||||
|
augmenting our capabilities. Together, we can achieve
|
||||||
|
what neither could alone.
|
||||||
|
==========================================================
|
||||||
|
Character count: 145/280
|
||||||
|
==========================================================
|
||||||
|
|
||||||
|
Do you want to post this tweet? (yes/no): yes
|
||||||
|
|
||||||
|
==========================================================
|
||||||
|
SUCCESS! Tweet posted successfully!
|
||||||
|
Tweet ID: 1234567890123456789
|
||||||
|
View at: https://twitter.com/user/status/1234567890123456789
|
||||||
|
==========================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
**Scheduled Mode:**
|
||||||
|
```
|
||||||
|
==========================================================
|
||||||
|
TweetBot - SCHEDULED MODE
|
||||||
|
==========================================================
|
||||||
|
Tweet will be posted every 30 minutes
|
||||||
|
Press Ctrl+C to stop
|
||||||
|
==========================================================
|
||||||
|
|
||||||
|
==========================================================
|
||||||
|
[2025-10-24 14:30:00] Tweet Posted Successfully!
|
||||||
|
==========================================================
|
||||||
|
AI is transforming how we work, learn, and create.
|
||||||
|
The future is collaborative intelligence.
|
||||||
|
==========================================================
|
||||||
|
Tweet ID: 1234567890123456789
|
||||||
|
View at: https://twitter.com/user/status/1234567890123456789
|
||||||
|
Next tweet in 30 minutes
|
||||||
|
==========================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
tweetbot/
|
||||||
|
├── pom.xml # Maven configuration
|
||||||
|
├── .env # Environment variables (not in git)
|
||||||
|
├── .env.example # Example environment file
|
||||||
|
├── README.md # This file
|
||||||
|
└── src/
|
||||||
|
└── main/
|
||||||
|
├── java/
|
||||||
|
│ └── com/
|
||||||
|
│ └── voidcode/
|
||||||
|
│ └── tweetbot/
|
||||||
|
│ ├── TweetBot.java # Main application
|
||||||
|
│ ├── config/
|
||||||
|
│ │ └── Config.java # Configuration loader
|
||||||
|
│ └── service/
|
||||||
|
│ ├── OpenAIService.java # ChatGPT integration
|
||||||
|
│ └── TwitterService.java # Twitter API integration
|
||||||
|
└── resources/
|
||||||
|
└── logback.xml # Logging configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
| Variable | Required | Description |
|
||||||
|
|----------|----------|-------------|
|
||||||
|
| `OPENAI_API_KEY` | Yes | Your OpenAI API key |
|
||||||
|
| `TWITTER_API_KEY` | Yes | Twitter API Key (Consumer Key) |
|
||||||
|
| `TWITTER_API_SECRET` | Yes | Twitter API Secret (Consumer Secret) |
|
||||||
|
| `TWITTER_ACCESS_TOKEN` | Yes | Twitter Access Token |
|
||||||
|
| `TWITTER_ACCESS_TOKEN_SECRET` | Yes | Twitter Access Token Secret |
|
||||||
|
| `TWITTER_BEARER_TOKEN` | Yes | Twitter Bearer Token |
|
||||||
|
| `TWEET_PROMPT` | No | Default prompt for tweet generation |
|
||||||
|
| `IMAGE_PATH` | No | Path to image file to attach to every tweet (jpg, png, gif) |
|
||||||
|
|
||||||
|
### Customizing Tweet Generation
|
||||||
|
|
||||||
|
You can customize the tweet generation by modifying the prompt. Here are some examples:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Technology news (interactive mode)
|
||||||
|
java -jar target/tweetbot-1.0.0.jar "Write a tweet about the latest AI breakthrough"
|
||||||
|
|
||||||
|
# Motivational content (auto-post)
|
||||||
|
java -jar target/tweetbot-1.0.0.jar --auto "Write an inspiring quote about success"
|
||||||
|
|
||||||
|
# Product updates (scheduled)
|
||||||
|
java -jar target/tweetbot-1.0.0.jar --schedule "Announce a new feature for a productivity app"
|
||||||
|
|
||||||
|
# Educational content (scheduled)
|
||||||
|
java -jar target/tweetbot-1.0.0.jar --schedule "Share an interesting fact about space exploration"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Attaching Images to Tweets
|
||||||
|
|
||||||
|
Every tweet can include a static image from your local file system:
|
||||||
|
|
||||||
|
**Setup:**
|
||||||
|
1. Place your image file anywhere on your system
|
||||||
|
2. Add the absolute path to `.env`:
|
||||||
|
```bash
|
||||||
|
IMAGE_PATH=/home/user/images/brand-logo.png
|
||||||
|
```
|
||||||
|
3. Run the bot normally - the image will be automatically attached to every tweet
|
||||||
|
|
||||||
|
**Image Requirements:**
|
||||||
|
- Supported formats: JPEG (.jpg, .jpeg), PNG (.png), GIF (.gif)
|
||||||
|
- Maximum file size: 5MB (Twitter API limit)
|
||||||
|
- Recommended dimensions: 1200x675 pixels for optimal display
|
||||||
|
- The image must exist at the specified path when the bot runs
|
||||||
|
|
||||||
|
**Example Use Cases:**
|
||||||
|
- Brand logo or watermark on every tweet
|
||||||
|
- Product image for promotional content
|
||||||
|
- Infographic or visual content
|
||||||
|
- Profile or avatar image
|
||||||
|
|
||||||
|
**To disable images:**
|
||||||
|
- Comment out or remove the `IMAGE_PATH` line in `.env`
|
||||||
|
- Or set it to an empty value: `IMAGE_PATH=`
|
||||||
|
|
||||||
|
**Note:** If the image file is not found or fails to upload, the tweet will still be posted without the image, and an error will be logged.
|
||||||
|
|
||||||
|
### Running as a Background Service
|
||||||
|
|
||||||
|
To run the bot continuously in the background on Linux/Mac:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run in background
|
||||||
|
nohup java -jar target/tweetbot-1.0.0.jar --schedule > tweetbot.out 2>&1 &
|
||||||
|
|
||||||
|
# Check if it's running
|
||||||
|
ps aux | grep tweetbot
|
||||||
|
|
||||||
|
# View output
|
||||||
|
tail -f tweetbot.out
|
||||||
|
|
||||||
|
# Stop the bot
|
||||||
|
pkill -f tweetbot
|
||||||
|
```
|
||||||
|
|
||||||
|
**Using systemd (Linux):**
|
||||||
|
|
||||||
|
Create a service file `/etc/systemd/system/tweetbot.service`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=TweetBot - Automated Twitter Posting
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=your-username
|
||||||
|
WorkingDirectory=/path/to/tweetbot
|
||||||
|
ExecStart=/usr/bin/java -jar /path/to/tweetbot/target/tweetbot-1.0.0.jar --schedule
|
||||||
|
Restart=always
|
||||||
|
RestartSec=60
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Then enable and start:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable tweetbot
|
||||||
|
sudo systemctl start tweetbot
|
||||||
|
sudo systemctl status tweetbot
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
sudo journalctl -u tweetbot -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
The application creates logs in two places:
|
||||||
|
- **Console output**: Real-time logs with timestamps
|
||||||
|
- **File output**: `tweetbot.log` in the project directory
|
||||||
|
|
||||||
|
Log levels:
|
||||||
|
- INFO: General application flow
|
||||||
|
- WARN: Warnings and non-critical issues
|
||||||
|
- ERROR: Errors and exceptions
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
1. **Never commit `.env` file** - It contains sensitive API keys
|
||||||
|
2. **Use environment variables** - For production deployments
|
||||||
|
3. **Rotate API keys regularly** - Update keys periodically
|
||||||
|
4. **Limit API permissions** - Only grant necessary permissions
|
||||||
|
5. **Monitor API usage** - Check for unusual activity
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Missing required environment variable"
|
||||||
|
|
||||||
|
Ensure your `.env` file exists and contains all required variables.
|
||||||
|
|
||||||
|
### "Failed to verify Twitter credentials"
|
||||||
|
|
||||||
|
1. Check that your API keys are correct
|
||||||
|
2. Verify your app has Read and Write permissions
|
||||||
|
3. Regenerate access tokens if needed
|
||||||
|
|
||||||
|
### "Failed to generate tweet"
|
||||||
|
|
||||||
|
1. Verify your OpenAI API key is valid
|
||||||
|
2. Check that you have sufficient API credits
|
||||||
|
3. Review the error message in logs
|
||||||
|
|
||||||
|
### Twitter API Rate Limits
|
||||||
|
|
||||||
|
Twitter API has rate limits:
|
||||||
|
- Tweet creation: 300 tweets per 3 hours (100 per hour average)
|
||||||
|
- The default 30-minute interval allows for 6 tweets per 3 hours, well within limits
|
||||||
|
- Monitor your usage to avoid hitting limits
|
||||||
|
- If you modify the schedule interval, ensure you stay within rate limits
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Twitter API Java SDK** (2.0.3) - Twitter API client
|
||||||
|
- **OpenAI GPT-3 Java** (0.18.2) - OpenAI API client
|
||||||
|
- **OkHttp** (4.12.0) - HTTP client
|
||||||
|
- **Jackson** (2.16.1) - JSON processing
|
||||||
|
- **SLF4J & Logback** (2.0.9) - Logging
|
||||||
|
- **Dotenv** (3.0.0) - Environment variable management
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Feel free to fork this project and customize it for your needs. Some ideas:
|
||||||
|
|
||||||
|
### Customizing the Schedule Interval
|
||||||
|
|
||||||
|
To change the posting frequency from 30 minutes to another interval, edit `TweetBot.java`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
private static final int SCHEDULE_INTERVAL_MINUTES = 30; // Change this value
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other Enhancement Ideas
|
||||||
|
|
||||||
|
- Add support for images and media attachments
|
||||||
|
- Implement tweet threading for longer content
|
||||||
|
- Add sentiment analysis before posting
|
||||||
|
- Create multiple prompt templates with rotation
|
||||||
|
- Store tweet history in a database
|
||||||
|
- Add analytics and engagement tracking
|
||||||
|
- Support for multiple Twitter accounts
|
||||||
|
- Implement tweet approval queue/review system
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is open source and available for educational purposes.
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
Use this bot responsibly and in accordance with Twitter's Terms of Service and Automation Rules. Ensure you comply with rate limits and don't spam or post misleading content.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
1. Check the troubleshooting section
|
||||||
|
2. Review application logs in `tweetbot.log`
|
||||||
|
3. Verify API credentials and permissions
|
||||||
|
4. Check Twitter and OpenAI API status pages
|
||||||
116
pom.xml
Normal file
116
pom.xml
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||||
|
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>com.voidcode.tweetbot</groupId>
|
||||||
|
<artifactId>tweetbot</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>TweetBot</name>
|
||||||
|
<description>Auto-posting tweets with ChatGPT generated content</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Twitter API v2 Client -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.twitter</groupId>
|
||||||
|
<artifactId>twitter-api-java-sdk</artifactId>
|
||||||
|
<version>2.0.3</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- OpenAI Java Client -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.theokanning.openai-gpt3-java</groupId>
|
||||||
|
<artifactId>service</artifactId>
|
||||||
|
<version>0.18.2</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- OkHttp for HTTP requests -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
|
<artifactId>okhttp</artifactId>
|
||||||
|
<version>4.12.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Jackson for JSON processing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>2.16.1</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- SLF4J and Logback for logging -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
<version>2.0.9</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>ch.qos.logback</groupId>
|
||||||
|
<artifactId>logback-classic</artifactId>
|
||||||
|
<version>1.4.14</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Dotenv for environment variables -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.cdimascio</groupId>
|
||||||
|
<artifactId>dotenv-java</artifactId>
|
||||||
|
<version>3.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.11.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>17</source>
|
||||||
|
<target>17</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<version>3.3.0</version>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<mainClass>com.voidcode.tweetbot.TweetBot</mainClass>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.1</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<transformers>
|
||||||
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
|
<mainClass>com.voidcode.tweetbot.TweetBot</mainClass>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
253
src/main/java/com/voidcode/tweetbot/TweetBot.java
Normal file
253
src/main/java/com/voidcode/tweetbot/TweetBot.java
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
package com.voidcode.tweetbot;
|
||||||
|
|
||||||
|
import com.voidcode.tweetbot.config.Config;
|
||||||
|
import com.voidcode.tweetbot.service.OpenAIService;
|
||||||
|
import com.voidcode.tweetbot.service.TwitterService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class TweetBot {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(TweetBot.class);
|
||||||
|
private static final int SCHEDULE_INTERVAL_MINUTES = 30;
|
||||||
|
private static OpenAIService openAIService;
|
||||||
|
private static TwitterService twitterService;
|
||||||
|
private static String tweetPrompt;
|
||||||
|
private static String imagePath;
|
||||||
|
private static volatile boolean isRunning = true;
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
logger.info("Starting TweetBot...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse command line arguments
|
||||||
|
boolean scheduleMode = false;
|
||||||
|
boolean autoPost = false;
|
||||||
|
String customPrompt = null;
|
||||||
|
|
||||||
|
for (int i = 0; i < args.length; i++) {
|
||||||
|
if (args[i].equals("--schedule") || args[i].equals("-s")) {
|
||||||
|
scheduleMode = true;
|
||||||
|
autoPost = true; // Auto-post in schedule mode
|
||||||
|
} else if (args[i].equals("--auto") || args[i].equals("-a")) {
|
||||||
|
autoPost = true;
|
||||||
|
} else if (args[i].equals("--help") || args[i].equals("-h")) {
|
||||||
|
printUsage();
|
||||||
|
System.exit(0);
|
||||||
|
} else {
|
||||||
|
// Everything else is treated as custom prompt
|
||||||
|
customPrompt = String.join(" ", java.util.Arrays.copyOfRange(args, i, args.length));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
logger.info("Loading configuration...");
|
||||||
|
String openAIApiKey = Config.getOpenAIApiKey();
|
||||||
|
String twitterApiKey = Config.getTwitterApiKey();
|
||||||
|
String twitterApiSecret = Config.getTwitterApiSecret();
|
||||||
|
String twitterAccessToken = Config.getTwitterAccessToken();
|
||||||
|
String twitterAccessTokenSecret = Config.getTwitterAccessTokenSecret();
|
||||||
|
tweetPrompt = customPrompt != null ? customPrompt : Config.getTweetPrompt();
|
||||||
|
imagePath = Config.getImagePath();
|
||||||
|
|
||||||
|
if (imagePath != null) {
|
||||||
|
logger.info("Image path configured: {}", imagePath);
|
||||||
|
} else {
|
||||||
|
logger.info("No image path configured, tweets will be text-only");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Configuration loaded successfully");
|
||||||
|
|
||||||
|
// Initialize services
|
||||||
|
logger.info("Initializing services...");
|
||||||
|
openAIService = new OpenAIService(openAIApiKey);
|
||||||
|
twitterService = new TwitterService(
|
||||||
|
twitterApiKey,
|
||||||
|
twitterApiSecret,
|
||||||
|
twitterAccessToken,
|
||||||
|
twitterAccessTokenSecret
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify Twitter credentials
|
||||||
|
logger.info("Verifying Twitter credentials...");
|
||||||
|
if (!twitterService.verifyCredentials()) {
|
||||||
|
logger.error("Failed to verify Twitter credentials. Please check your API keys.");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scheduleMode) {
|
||||||
|
runScheduledMode();
|
||||||
|
} else {
|
||||||
|
runOnceMode(autoPost);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
logger.error("Configuration error: {}", e.getMessage());
|
||||||
|
System.err.println("\nConfiguration Error: " + e.getMessage());
|
||||||
|
System.err.println("Please check your .env file and ensure all required variables are set.");
|
||||||
|
System.err.println("See .env.example for reference.");
|
||||||
|
System.exit(1);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("An error occurred", e);
|
||||||
|
System.err.println("\nError: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void runScheduledMode() {
|
||||||
|
logger.info("Running in SCHEDULED mode - posting every {} minutes", SCHEDULE_INTERVAL_MINUTES);
|
||||||
|
System.out.println("\n" + "=".repeat(60));
|
||||||
|
System.out.println("TweetBot - SCHEDULED MODE");
|
||||||
|
System.out.println("=".repeat(60));
|
||||||
|
System.out.println("Tweet will be posted every " + SCHEDULE_INTERVAL_MINUTES + " minutes");
|
||||||
|
System.out.println("Press Ctrl+C to stop");
|
||||||
|
System.out.println("=".repeat(60) + "\n");
|
||||||
|
|
||||||
|
// Add shutdown hook to gracefully stop
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||||
|
logger.info("Shutdown signal received, stopping TweetBot...");
|
||||||
|
isRunning = false;
|
||||||
|
if (openAIService != null) {
|
||||||
|
openAIService.shutdown();
|
||||||
|
}
|
||||||
|
logger.info("TweetBot stopped gracefully");
|
||||||
|
}));
|
||||||
|
|
||||||
|
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
||||||
|
|
||||||
|
// Post immediately on startup
|
||||||
|
postTweetAutomatically();
|
||||||
|
|
||||||
|
// Schedule to run every 30 minutes
|
||||||
|
scheduler.scheduleAtFixedRate(() -> {
|
||||||
|
if (isRunning) {
|
||||||
|
postTweetAutomatically();
|
||||||
|
}
|
||||||
|
}, SCHEDULE_INTERVAL_MINUTES, SCHEDULE_INTERVAL_MINUTES, TimeUnit.MINUTES);
|
||||||
|
|
||||||
|
// Keep the application running
|
||||||
|
try {
|
||||||
|
while (isRunning) {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
logger.info("Main thread interrupted, shutting down...");
|
||||||
|
} finally {
|
||||||
|
scheduler.shutdown();
|
||||||
|
try {
|
||||||
|
if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
|
||||||
|
scheduler.shutdownNow();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
scheduler.shutdownNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void runOnceMode(boolean autoPost) {
|
||||||
|
logger.info("Running in ONE-TIME mode");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Generate tweet content
|
||||||
|
logger.info("Generating tweet content...");
|
||||||
|
String tweetContent = openAIService.generateTweet(tweetPrompt);
|
||||||
|
|
||||||
|
// Display generated content
|
||||||
|
System.out.println("\n" + "=".repeat(60));
|
||||||
|
System.out.println("Generated Tweet:");
|
||||||
|
System.out.println("=".repeat(60));
|
||||||
|
System.out.println(tweetContent);
|
||||||
|
System.out.println("=".repeat(60));
|
||||||
|
System.out.println("Character count: " + tweetContent.length() + "/280");
|
||||||
|
System.out.println("=".repeat(60) + "\n");
|
||||||
|
|
||||||
|
boolean shouldPost = autoPost;
|
||||||
|
|
||||||
|
if (!autoPost) {
|
||||||
|
// Ask for confirmation
|
||||||
|
System.out.print("Do you want to post this tweet? (yes/no): ");
|
||||||
|
java.util.Scanner scanner = new java.util.Scanner(System.in);
|
||||||
|
String confirmation = scanner.nextLine().trim().toLowerCase();
|
||||||
|
shouldPost = confirmation.equals("yes") || confirmation.equals("y");
|
||||||
|
scanner.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldPost) {
|
||||||
|
// Post tweet
|
||||||
|
logger.info("Posting tweet to Twitter...");
|
||||||
|
String tweetId = twitterService.postTweet(tweetContent, imagePath);
|
||||||
|
|
||||||
|
System.out.println("\n" + "=".repeat(60));
|
||||||
|
System.out.println("SUCCESS! Tweet posted successfully!");
|
||||||
|
System.out.println("Tweet ID: " + tweetId);
|
||||||
|
System.out.println("View at: https://twitter.com/user/status/" + tweetId);
|
||||||
|
System.out.println("=".repeat(60) + "\n");
|
||||||
|
} else {
|
||||||
|
logger.info("Tweet posting cancelled by user");
|
||||||
|
System.out.println("\nTweet posting cancelled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// Cleanup
|
||||||
|
if (openAIService != null) {
|
||||||
|
openAIService.shutdown();
|
||||||
|
}
|
||||||
|
logger.info("TweetBot finished");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void postTweetAutomatically() {
|
||||||
|
try {
|
||||||
|
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||||
|
logger.info("[{}] Starting scheduled tweet posting...", timestamp);
|
||||||
|
|
||||||
|
// Generate tweet content
|
||||||
|
String tweetContent = openAIService.generateTweet(tweetPrompt);
|
||||||
|
logger.info("Generated tweet: {}", tweetContent);
|
||||||
|
|
||||||
|
// Post tweet
|
||||||
|
String tweetId = twitterService.postTweet(tweetContent, imagePath);
|
||||||
|
|
||||||
|
System.out.println("\n" + "=".repeat(60));
|
||||||
|
System.out.println("[" + timestamp + "] Tweet Posted Successfully!");
|
||||||
|
System.out.println("=".repeat(60));
|
||||||
|
System.out.println(tweetContent);
|
||||||
|
System.out.println("=".repeat(60));
|
||||||
|
System.out.println("Tweet ID: " + tweetId);
|
||||||
|
System.out.println("View at: https://twitter.com/user/status/" + tweetId);
|
||||||
|
System.out.println("Next tweet in " + SCHEDULE_INTERVAL_MINUTES + " minutes");
|
||||||
|
System.out.println("=".repeat(60) + "\n");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Error posting scheduled tweet", e);
|
||||||
|
System.err.println("\n[ERROR] Failed to post tweet: " + e.getMessage());
|
||||||
|
System.err.println("Will retry at next scheduled time.\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void printUsage() {
|
||||||
|
System.out.println("TweetBot - Automated Twitter posting with ChatGPT");
|
||||||
|
System.out.println("\nUsage:");
|
||||||
|
System.out.println(" java -jar tweetbot.jar [OPTIONS] [CUSTOM_PROMPT]");
|
||||||
|
System.out.println("\nOptions:");
|
||||||
|
System.out.println(" -s, --schedule Run in scheduled mode (posts every 30 minutes)");
|
||||||
|
System.out.println(" -a, --auto Auto-post without confirmation (one-time mode)");
|
||||||
|
System.out.println(" -h, --help Show this help message");
|
||||||
|
System.out.println("\nExamples:");
|
||||||
|
System.out.println(" # Interactive mode with confirmation");
|
||||||
|
System.out.println(" java -jar tweetbot.jar");
|
||||||
|
System.out.println("\n # Auto-post once with custom prompt");
|
||||||
|
System.out.println(" java -jar tweetbot.jar --auto \"Write a tweet about AI\"");
|
||||||
|
System.out.println("\n # Scheduled mode (posts every 30 minutes)");
|
||||||
|
System.out.println(" java -jar tweetbot.jar --schedule");
|
||||||
|
System.out.println("\n # Scheduled mode with custom prompt");
|
||||||
|
System.out.println(" java -jar tweetbot.jar --schedule \"Daily tech tips\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/main/java/com/voidcode/tweetbot/config/Config.java
Normal file
59
src/main/java/com/voidcode/tweetbot/config/Config.java
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package com.voidcode.tweetbot.config;
|
||||||
|
|
||||||
|
import io.github.cdimascio.dotenv.Dotenv;
|
||||||
|
|
||||||
|
public class Config {
|
||||||
|
private static final Dotenv dotenv = Dotenv.configure()
|
||||||
|
.directory(".")
|
||||||
|
.ignoreIfMissing()
|
||||||
|
.load();
|
||||||
|
|
||||||
|
// OpenAI Configuration
|
||||||
|
public static String getOpenAIApiKey() {
|
||||||
|
return getEnvOrThrow("OPENAI_API_KEY");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Twitter Configuration
|
||||||
|
public static String getTwitterApiKey() {
|
||||||
|
return getEnvOrThrow("TWITTER_API_KEY");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getTwitterApiSecret() {
|
||||||
|
return getEnvOrThrow("TWITTER_API_SECRET");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getTwitterAccessToken() {
|
||||||
|
return getEnvOrThrow("TWITTER_ACCESS_TOKEN");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getTwitterAccessTokenSecret() {
|
||||||
|
return getEnvOrThrow("TWITTER_ACCESS_TOKEN_SECRET");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getTwitterBearerToken() {
|
||||||
|
return getEnvOrThrow("TWITTER_BEARER_TOKEN");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tweet Generation Configuration
|
||||||
|
public static String getTweetPrompt() {
|
||||||
|
return dotenv.get("TWEET_PROMPT", "Write a short, engaging tweet about technology");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image Configuration (Optional)
|
||||||
|
public static String getImagePath() {
|
||||||
|
String path = dotenv.get("IMAGE_PATH");
|
||||||
|
// Return null if empty or not set, allowing tweets without images
|
||||||
|
if (path == null || path.isEmpty() || path.equals("/path/to/your/image.jpg")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getEnvOrThrow(String key) {
|
||||||
|
String value = dotenv.get(key);
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
throw new IllegalStateException("Missing required environment variable: " + key);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package com.voidcode.tweetbot.service;
|
||||||
|
|
||||||
|
import com.theokanning.openai.completion.chat.ChatCompletionRequest;
|
||||||
|
import com.theokanning.openai.completion.chat.ChatCompletionResult;
|
||||||
|
import com.theokanning.openai.completion.chat.ChatMessage;
|
||||||
|
import com.theokanning.openai.completion.chat.ChatMessageRole;
|
||||||
|
import com.theokanning.openai.service.OpenAiService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class OpenAIService {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(OpenAIService.class);
|
||||||
|
private final OpenAiService openAiService;
|
||||||
|
private static final int MAX_TWEET_LENGTH = 280;
|
||||||
|
|
||||||
|
public OpenAIService(String apiKey) {
|
||||||
|
this.openAiService = new OpenAiService(apiKey, Duration.ofSeconds(60));
|
||||||
|
logger.info("OpenAI service initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate tweet content using ChatGPT
|
||||||
|
* @param prompt The prompt to generate the tweet
|
||||||
|
* @return Generated tweet text
|
||||||
|
*/
|
||||||
|
public String generateTweet(String prompt) {
|
||||||
|
try {
|
||||||
|
logger.info("Generating tweet with prompt: {}", prompt);
|
||||||
|
|
||||||
|
// Create the system message to set context
|
||||||
|
ChatMessage systemMessage = new ChatMessage(
|
||||||
|
ChatMessageRole.SYSTEM.value(),
|
||||||
|
"You are a creative social media content creator. Generate engaging tweets that are concise, " +
|
||||||
|
"interesting, and within Twitter's character limit. Do not include hashtags unless specifically " +
|
||||||
|
"requested. Keep it under 280 characters."
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create the user message with the prompt
|
||||||
|
ChatMessage userMessage = new ChatMessage(
|
||||||
|
ChatMessageRole.USER.value(),
|
||||||
|
prompt + " Keep it under " + MAX_TWEET_LENGTH + " characters."
|
||||||
|
);
|
||||||
|
|
||||||
|
List<ChatMessage> messages = new ArrayList<>();
|
||||||
|
messages.add(systemMessage);
|
||||||
|
messages.add(userMessage);
|
||||||
|
|
||||||
|
// Create the chat completion request
|
||||||
|
ChatCompletionRequest request = ChatCompletionRequest.builder()
|
||||||
|
.model("gpt-3.5-turbo")
|
||||||
|
.messages(messages)
|
||||||
|
.temperature(0.8)
|
||||||
|
.maxTokens(100)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Get the response
|
||||||
|
ChatCompletionResult result = openAiService.createChatCompletion(request);
|
||||||
|
String generatedText = result.getChoices().get(0).getMessage().getContent().trim();
|
||||||
|
|
||||||
|
// Remove quotes if the AI wrapped the tweet in quotes
|
||||||
|
generatedText = generatedText.replaceAll("^[\"']|[\"']$", "");
|
||||||
|
|
||||||
|
// Ensure it's within Twitter's character limit
|
||||||
|
if (generatedText.length() > MAX_TWEET_LENGTH) {
|
||||||
|
logger.warn("Generated tweet exceeds {} characters, truncating", MAX_TWEET_LENGTH);
|
||||||
|
generatedText = generatedText.substring(0, MAX_TWEET_LENGTH - 3) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Generated tweet: {}", generatedText);
|
||||||
|
return generatedText;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Error generating tweet", e);
|
||||||
|
throw new RuntimeException("Failed to generate tweet: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shutdown the OpenAI service
|
||||||
|
*/
|
||||||
|
public void shutdown() {
|
||||||
|
if (openAiService != null) {
|
||||||
|
openAiService.shutdownExecutor();
|
||||||
|
logger.info("OpenAI service shut down");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
276
src/main/java/com/voidcode/tweetbot/service/TwitterService.java
Normal file
276
src/main/java/com/voidcode/tweetbot/service/TwitterService.java
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
package com.voidcode.tweetbot.service;
|
||||||
|
|
||||||
|
import com.twitter.clientlib.TwitterCredentialsOAuth2;
|
||||||
|
import com.twitter.clientlib.api.TwitterApi;
|
||||||
|
import com.twitter.clientlib.ApiException;
|
||||||
|
import com.twitter.clientlib.model.TweetCreateRequest;
|
||||||
|
import com.twitter.clientlib.model.TweetCreateRequestMedia;
|
||||||
|
import com.twitter.clientlib.model.TweetCreateResponse;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.Base64;
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class TwitterService {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(TwitterService.class);
|
||||||
|
private final TwitterApi apiInstance;
|
||||||
|
private final String apiKey;
|
||||||
|
private final String apiSecret;
|
||||||
|
private final String accessToken;
|
||||||
|
private final String accessTokenSecret;
|
||||||
|
private static final String MEDIA_UPLOAD_URL = "https://upload.twitter.com/1.1/media/upload.json";
|
||||||
|
|
||||||
|
public TwitterService(String apiKey, String apiSecret, String accessToken, String accessTokenSecret) {
|
||||||
|
try {
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
this.apiSecret = apiSecret;
|
||||||
|
this.accessToken = accessToken;
|
||||||
|
this.accessTokenSecret = accessTokenSecret;
|
||||||
|
|
||||||
|
// Initialize Twitter API with OAuth 1.0a credentials
|
||||||
|
TwitterCredentialsOAuth2 credentials = new TwitterCredentialsOAuth2(
|
||||||
|
apiKey,
|
||||||
|
apiSecret,
|
||||||
|
accessToken,
|
||||||
|
accessTokenSecret
|
||||||
|
);
|
||||||
|
|
||||||
|
apiInstance = new TwitterApi(credentials);
|
||||||
|
logger.info("Twitter service initialized successfully");
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Failed to initialize Twitter service", e);
|
||||||
|
throw new RuntimeException("Failed to initialize Twitter service: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post a tweet to Twitter without media
|
||||||
|
* @param tweetText The text content of the tweet
|
||||||
|
* @return The ID of the posted tweet
|
||||||
|
*/
|
||||||
|
public String postTweet(String tweetText) {
|
||||||
|
return postTweet(tweetText, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post a tweet to Twitter with optional image
|
||||||
|
* @param tweetText The text content of the tweet
|
||||||
|
* @param imagePath Path to the image file (null for no image)
|
||||||
|
* @return The ID of the posted tweet
|
||||||
|
*/
|
||||||
|
public String postTweet(String tweetText, String imagePath) {
|
||||||
|
try {
|
||||||
|
logger.info("Posting tweet: {}", tweetText);
|
||||||
|
|
||||||
|
// Create the tweet request
|
||||||
|
TweetCreateRequest tweetCreateRequest = new TweetCreateRequest();
|
||||||
|
tweetCreateRequest.setText(tweetText);
|
||||||
|
|
||||||
|
// Upload media if image path is provided
|
||||||
|
if (imagePath != null && !imagePath.isEmpty()) {
|
||||||
|
File imageFile = new File(imagePath);
|
||||||
|
if (!imageFile.exists()) {
|
||||||
|
logger.warn("Image file not found: {}. Posting tweet without image.", imagePath);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
String mediaId = uploadMedia(imageFile);
|
||||||
|
logger.info("Media uploaded successfully. Media ID: {}", mediaId);
|
||||||
|
|
||||||
|
// Attach media to tweet
|
||||||
|
TweetCreateRequestMedia media = new TweetCreateRequestMedia();
|
||||||
|
media.addMediaIdsItem(mediaId);
|
||||||
|
tweetCreateRequest.setMedia(media);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Failed to upload media, posting tweet without image", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post the tweet
|
||||||
|
TweetCreateResponse result = apiInstance.tweets().createTweet(tweetCreateRequest).execute();
|
||||||
|
|
||||||
|
String tweetId = result.getData().getId();
|
||||||
|
logger.info("Tweet posted successfully. Tweet ID: {}", tweetId);
|
||||||
|
logger.info("View tweet at: https://twitter.com/user/status/{}", tweetId);
|
||||||
|
|
||||||
|
return tweetId;
|
||||||
|
|
||||||
|
} catch (ApiException e) {
|
||||||
|
logger.error("Error posting tweet. Status code: {}, Response: {}",
|
||||||
|
e.getCode(), e.getResponseBody(), e);
|
||||||
|
throw new RuntimeException("Failed to post tweet: " + e.getMessage(), e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Unexpected error posting tweet", e);
|
||||||
|
throw new RuntimeException("Failed to post tweet: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload media to Twitter using v1.1 API
|
||||||
|
* @param imageFile The image file to upload
|
||||||
|
* @return The media ID
|
||||||
|
*/
|
||||||
|
private String uploadMedia(File imageFile) throws IOException {
|
||||||
|
logger.info("Uploading media file: {}", imageFile.getAbsolutePath());
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
byte[] fileContent = Files.readAllBytes(imageFile.toPath());
|
||||||
|
String base64Content = Base64.getEncoder().encodeToString(fileContent);
|
||||||
|
|
||||||
|
// Create multipart request body
|
||||||
|
String boundary = "----WebKitFormBoundary" + System.currentTimeMillis();
|
||||||
|
StringBuilder bodyBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
bodyBuilder.append("--").append(boundary).append("\r\n");
|
||||||
|
bodyBuilder.append("Content-Disposition: form-data; name=\"media_data\"\r\n\r\n");
|
||||||
|
bodyBuilder.append(base64Content).append("\r\n");
|
||||||
|
bodyBuilder.append("--").append(boundary).append("--\r\n");
|
||||||
|
|
||||||
|
byte[] body = bodyBuilder.toString().getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
// Create OAuth header
|
||||||
|
String authHeader = generateOAuthHeader("POST", MEDIA_UPLOAD_URL, new TreeMap<>());
|
||||||
|
|
||||||
|
// Make HTTP request
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) new URL(MEDIA_UPLOAD_URL).openConnection();
|
||||||
|
connection.setRequestMethod("POST");
|
||||||
|
connection.setDoOutput(true);
|
||||||
|
connection.setRequestProperty("Authorization", authHeader);
|
||||||
|
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
|
||||||
|
connection.setRequestProperty("Content-Length", String.valueOf(body.length));
|
||||||
|
|
||||||
|
// Send request
|
||||||
|
try (OutputStream os = connection.getOutputStream()) {
|
||||||
|
os.write(body);
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read response
|
||||||
|
int responseCode = connection.getResponseCode();
|
||||||
|
if (responseCode != 200) {
|
||||||
|
throw new IOException("Media upload failed with response code: " + responseCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
Scanner scanner = new Scanner(connection.getInputStream());
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
while (scanner.hasNextLine()) {
|
||||||
|
response.append(scanner.nextLine());
|
||||||
|
}
|
||||||
|
scanner.close();
|
||||||
|
|
||||||
|
// Parse media_id from response JSON
|
||||||
|
String responseStr = response.toString();
|
||||||
|
String mediaId = extractMediaId(responseStr);
|
||||||
|
|
||||||
|
if (mediaId == null) {
|
||||||
|
throw new IOException("Failed to extract media_id from response");
|
||||||
|
}
|
||||||
|
|
||||||
|
return mediaId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract media_id from JSON response
|
||||||
|
*/
|
||||||
|
private String extractMediaId(String jsonResponse) {
|
||||||
|
// Simple JSON parsing to extract media_id_string
|
||||||
|
int index = jsonResponse.indexOf("\"media_id_string\"");
|
||||||
|
if (index == -1) return null;
|
||||||
|
|
||||||
|
int startQuote = jsonResponse.indexOf("\"", index + 18);
|
||||||
|
if (startQuote == -1) return null;
|
||||||
|
|
||||||
|
int endQuote = jsonResponse.indexOf("\"", startQuote + 1);
|
||||||
|
if (endQuote == -1) return null;
|
||||||
|
|
||||||
|
return jsonResponse.substring(startQuote + 1, endQuote);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate OAuth 1.0a authorization header
|
||||||
|
*/
|
||||||
|
private String generateOAuthHeader(String method, String url, Map<String, String> params) {
|
||||||
|
try {
|
||||||
|
Map<String, String> oauthParams = new TreeMap<>();
|
||||||
|
oauthParams.put("oauth_consumer_key", apiKey);
|
||||||
|
oauthParams.put("oauth_token", accessToken);
|
||||||
|
oauthParams.put("oauth_signature_method", "HMAC-SHA1");
|
||||||
|
oauthParams.put("oauth_timestamp", String.valueOf(System.currentTimeMillis() / 1000));
|
||||||
|
oauthParams.put("oauth_nonce", UUID.randomUUID().toString().replaceAll("-", ""));
|
||||||
|
oauthParams.put("oauth_version", "1.0");
|
||||||
|
|
||||||
|
// Create signature
|
||||||
|
Map<String, String> allParams = new TreeMap<>();
|
||||||
|
allParams.putAll(oauthParams);
|
||||||
|
allParams.putAll(params);
|
||||||
|
|
||||||
|
StringBuilder paramString = new StringBuilder();
|
||||||
|
for (Map.Entry<String, String> entry : allParams.entrySet()) {
|
||||||
|
if (paramString.length() > 0) paramString.append("&");
|
||||||
|
paramString.append(urlEncode(entry.getKey())).append("=").append(urlEncode(entry.getValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
String signatureBase = method + "&" + urlEncode(url) + "&" + urlEncode(paramString.toString());
|
||||||
|
String signingKey = urlEncode(apiSecret) + "&" + urlEncode(accessTokenSecret);
|
||||||
|
|
||||||
|
Mac mac = Mac.getInstance("HmacSHA1");
|
||||||
|
SecretKeySpec secret = new SecretKeySpec(signingKey.getBytes(StandardCharsets.UTF_8), "HmacSHA1");
|
||||||
|
mac.init(secret);
|
||||||
|
byte[] digest = mac.doFinal(signatureBase.getBytes(StandardCharsets.UTF_8));
|
||||||
|
String signature = Base64.getEncoder().encodeToString(digest);
|
||||||
|
|
||||||
|
oauthParams.put("oauth_signature", signature);
|
||||||
|
|
||||||
|
// Build header
|
||||||
|
StringBuilder header = new StringBuilder("OAuth ");
|
||||||
|
for (Map.Entry<String, String> entry : oauthParams.entrySet()) {
|
||||||
|
if (header.length() > 6) header.append(", ");
|
||||||
|
header.append(urlEncode(entry.getKey())).append("=\"").append(urlEncode(entry.getValue())).append("\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
return header.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to generate OAuth header", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String urlEncode(String value) {
|
||||||
|
try {
|
||||||
|
return URLEncoder.encode(value, StandardCharsets.UTF_8.toString())
|
||||||
|
.replace("+", "%20")
|
||||||
|
.replace("*", "%2A")
|
||||||
|
.replace("%7E", "~");
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to URL encode", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify Twitter credentials by getting authenticated user info
|
||||||
|
* @return true if credentials are valid
|
||||||
|
*/
|
||||||
|
public boolean verifyCredentials() {
|
||||||
|
try {
|
||||||
|
logger.info("Verifying Twitter credentials...");
|
||||||
|
// Try to get the authenticated user's information
|
||||||
|
apiInstance.users().findMyUser().execute();
|
||||||
|
logger.info("Twitter credentials verified successfully");
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Failed to verify Twitter credentials", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/main/resources/logback.xml
Normal file
26
src/main/resources/logback.xml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration>
|
||||||
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||||
|
<file>tweetbot.log</file>
|
||||||
|
<append>true</append>
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<root level="INFO">
|
||||||
|
<appender-ref ref="CONSOLE" />
|
||||||
|
<appender-ref ref="FILE" />
|
||||||
|
</root>
|
||||||
|
|
||||||
|
<!-- Reduce noise from Twitter and OpenAI libraries -->
|
||||||
|
<logger name="com.twitter" level="WARN"/>
|
||||||
|
<logger name="com.theokanning" level="WARN"/>
|
||||||
|
<logger name="okhttp3" level="WARN"/>
|
||||||
|
</configuration>
|
||||||
Reference in New Issue
Block a user