diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 10c14e2..24244a5 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -3,7 +3,8 @@ "allow": [ "Bash(tree:*)", "Bash(mvn clean compile:*)", - "WebFetch(domain:github.com)" + "WebFetch(domain:github.com)", + "Bash(rm:*)" ], "deny": [], "ask": [] diff --git a/.env.example b/.env.example index 4e94538..47211e8 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,13 @@ -# OpenAI API Configuration -OPENAI_API_KEY=your_openai_api_key_here +# Google Gemini API Configuration +# Get your free API key from: https://aistudio.google.com/app/apikey +GEMINI_API_KEY=your_gemini_api_key_here -# Twitter API Configuration (OAuth 2.0) +# Twitter API Configuration (OAuth 1.0a) +# Get your credentials from Twitter Developer Portal: https://developer.twitter.com/ 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 diff --git a/.gitignore b/.gitignore index b9a60e3..a86511d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Environment variables (IMPORTANT: Never commit API keys!) .env +# OAuth 2.0 tokens (IMPORTANT: Never commit access tokens!) +.twitter_tokens.json + # Maven target/ pom.xml.tag diff --git a/README.md b/README.md index 4cef3a7..09a721e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # 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. +An automated Twitter bot that generates and posts tweets using Google Gemini's free 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) +- Generate tweet content using Google Gemini (gemini-2.0-flash-exp) - **Free Tier** - **Three operation modes:** - Interactive mode with manual approval - Auto-post mode for one-time tweets @@ -22,17 +22,24 @@ An automated Twitter bot that generates and posts tweets using OpenAI's ChatGPT - Java 17 or higher - Maven 3.6 or higher -- OpenAI API key +- Google Gemini API key (free tier available) - Twitter Developer Account with API credentials ## Setup Instructions -### 1. Get OpenAI API Key +### 1. Get Google Gemini API Key (Free) -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 +1. Go to [Google AI Studio](https://aistudio.google.com/app/apikey) +2. Sign in with your Google account +3. Click "Create API Key" +4. Select or create a Google Cloud project +5. Copy the API key and save it securely + +**Note:** The free tier of Google Gemini includes: +- 15 requests per minute +- 1 million tokens per minute +- 1,500 requests per day +- More than enough for a tweet bot! ### 2. Get Twitter API Credentials @@ -44,7 +51,6 @@ An automated Twitter bot that generates and posts tweets using OpenAI's ChatGPT - 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 @@ -60,12 +66,11 @@ An automated Twitter bot that generates and posts tweets using OpenAI's ChatGPT 2. Edit `.env` and add your API credentials: ``` - OPENAI_API_KEY=sk-your-openai-api-key-here + GEMINI_API_KEY=your-gemini-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 ``` @@ -80,7 +85,7 @@ An automated Twitter bot that generates and posts tweets using OpenAI's ChatGPT - Supported formats: JPEG, PNG, GIF - Maximum file size: 5MB (Twitter limit) -### 5. Build the Project +### 4. Build the Project ```bash mvn clean package @@ -216,7 +221,7 @@ tweetbot/ │ ├── config/ │ │ └── Config.java # Configuration loader │ └── service/ - │ ├── OpenAIService.java # ChatGPT integration + │ ├── GeminiService.java # Google Gemini integration │ └── TwitterService.java # Twitter API integration └── resources/ └── logback.xml # Logging configuration @@ -228,12 +233,11 @@ tweetbot/ | Variable | Required | Description | |----------|----------|-------------| -| `OPENAI_API_KEY` | Yes | Your OpenAI API key | +| `GEMINI_API_KEY` | Yes | Your Google Gemini API key (free tier available) | | `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) | @@ -371,9 +375,10 @@ Ensure your `.env` file exists and contains all required variables. ### "Failed to generate tweet" -1. Verify your OpenAI API key is valid -2. Check that you have sufficient API credits +1. Verify your Gemini API key is valid +2. Check that you're within the free tier rate limits (15 RPM, 1500 RPD) 3. Review the error message in logs +4. Ensure your Google Cloud project is properly configured ### Twitter API Rate Limits @@ -385,9 +390,7 @@ Twitter API has 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 +- **OkHttp** (4.12.0) - HTTP client for API requests - **Jackson** (2.16.1) - JSON processing - **SLF4J & Logback** (2.0.9) - Logging - **Dotenv** (3.0.0) - Environment variable management @@ -406,7 +409,6 @@ 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 @@ -429,4 +431,4 @@ 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 +4. Check Twitter and Google AI Studio status pages diff --git a/pom.xml b/pom.xml index 81e143a..cad0354 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ jar TweetBot - Auto-posting tweets with ChatGPT generated content + Auto-posting tweets with Google Gemini generated content 17 @@ -20,13 +20,6 @@ - - - com.theokanning.openai-gpt3-java - service - 0.18.2 - - com.squareup.okhttp3 diff --git a/src/main/java/com/voidcode/tweetbot/TweetBot.java b/src/main/java/com/voidcode/tweetbot/TweetBot.java index ecc7b26..24c9136 100644 --- a/src/main/java/com/voidcode/tweetbot/TweetBot.java +++ b/src/main/java/com/voidcode/tweetbot/TweetBot.java @@ -1,7 +1,7 @@ package com.voidcode.tweetbot; import com.voidcode.tweetbot.config.Config; -import com.voidcode.tweetbot.service.OpenAIService; +import com.voidcode.tweetbot.service.GeminiService; import com.voidcode.tweetbot.service.TwitterService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,7 +15,7 @@ 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 GeminiService geminiService; private static TwitterService twitterService; private static String tweetPrompt; private static String imagePath; @@ -48,7 +48,7 @@ public class TweetBot { // Load configuration logger.info("Loading configuration..."); - String openAIApiKey = Config.getOpenAIApiKey(); + String geminiApiKey = Config.getGeminiApiKey(); String twitterApiKey = Config.getTwitterApiKey(); String twitterApiSecret = Config.getTwitterApiSecret(); String twitterAccessToken = Config.getTwitterAccessToken(); @@ -66,7 +66,7 @@ public class TweetBot { // Initialize services logger.info("Initializing services..."); - openAIService = new OpenAIService(openAIApiKey); + geminiService = new GeminiService(geminiApiKey); twitterService = new TwitterService( twitterApiKey, twitterApiSecret, @@ -114,8 +114,8 @@ public class TweetBot { Runtime.getRuntime().addShutdownHook(new Thread(() -> { logger.info("Shutdown signal received, stopping TweetBot..."); isRunning = false; - if (openAIService != null) { - openAIService.shutdown(); + if (geminiService != null) { + geminiService.shutdown(); } logger.info("TweetBot stopped gracefully"); })); @@ -157,7 +157,7 @@ public class TweetBot { try { // Generate tweet content logger.info("Generating tweet content..."); - String tweetContent = openAIService.generateTweet(tweetPrompt); + String tweetContent = geminiService.generateTweet(tweetPrompt); // Display generated content System.out.println("\n" + "=".repeat(60)); @@ -196,8 +196,8 @@ public class TweetBot { } finally { // Cleanup - if (openAIService != null) { - openAIService.shutdown(); + if (geminiService != null) { + geminiService.shutdown(); } logger.info("TweetBot finished"); } @@ -209,7 +209,7 @@ public class TweetBot { logger.info("[{}] Starting scheduled tweet posting...", timestamp); // Generate tweet content - String tweetContent = openAIService.generateTweet(tweetPrompt); + String tweetContent = geminiService.generateTweet(tweetPrompt); logger.info("Generated tweet: {}", tweetContent); // Post tweet @@ -233,7 +233,7 @@ public class TweetBot { } private static void printUsage() { - System.out.println("TweetBot - Automated Twitter posting with ChatGPT"); + System.out.println("TweetBot - Automated Twitter posting with Google Gemini"); System.out.println("\nUsage:"); System.out.println(" java -jar tweetbot.jar [OPTIONS] [CUSTOM_PROMPT]"); System.out.println("\nOptions:"); diff --git a/src/main/java/com/voidcode/tweetbot/config/Config.java b/src/main/java/com/voidcode/tweetbot/config/Config.java index bba5060..d8b41aa 100644 --- a/src/main/java/com/voidcode/tweetbot/config/Config.java +++ b/src/main/java/com/voidcode/tweetbot/config/Config.java @@ -8,12 +8,12 @@ public class Config { .ignoreIfMissing() .load(); - // OpenAI Configuration - public static String getOpenAIApiKey() { - return getEnvOrThrow("OPENAI_API_KEY"); + // Google Gemini Configuration + public static String getGeminiApiKey() { + return getEnvOrThrow("GEMINI_API_KEY"); } - // Twitter Configuration + // Twitter Configuration (OAuth 1.0a) public static String getTwitterApiKey() { return getEnvOrThrow("TWITTER_API_KEY"); } @@ -30,10 +30,6 @@ public class Config { 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"); diff --git a/src/main/java/com/voidcode/tweetbot/service/GeminiService.java b/src/main/java/com/voidcode/tweetbot/service/GeminiService.java new file mode 100644 index 0000000..f8abf9e --- /dev/null +++ b/src/main/java/com/voidcode/tweetbot/service/GeminiService.java @@ -0,0 +1,132 @@ +package com.voidcode.tweetbot.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +public class GeminiService { + private static final Logger logger = LoggerFactory.getLogger(GeminiService.class); + private static final int MAX_TWEET_LENGTH = 280; + private static final String GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent"; + private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); + + private final String apiKey; + private final OkHttpClient httpClient; + private final ObjectMapper objectMapper; + + public GeminiService(String apiKey) { + this.apiKey = apiKey; + this.httpClient = new OkHttpClient.Builder() + .connectTimeout(60, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS) + .build(); + this.objectMapper = new ObjectMapper(); + logger.info("Gemini service initialized with model: gemini-2.0-flash-exp"); + } + + /** + * Generate tweet content using Google Gemini + * @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); + + // Build the request body + String systemInstruction = "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."; + + String userPrompt = prompt + " Keep it under " + MAX_TWEET_LENGTH + " characters."; + + String jsonBody = String.format( + "{\"contents\":[{\"parts\":[{\"text\":\"%s\\n\\n%s\"}]}],\"generationConfig\":{\"temperature\":0.8,\"maxOutputTokens\":100}}", + escapeJson(systemInstruction), + escapeJson(userPrompt) + ); + + // Build the request + Request request = new Request.Builder() + .url(GEMINI_API_URL + "?key=" + apiKey) + .post(RequestBody.create(jsonBody, JSON)) + .build(); + + // Execute the request + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + String errorBody = response.body() != null ? response.body().string() : "No error body"; + logger.error("Gemini API request failed: {} - {}", response.code(), errorBody); + throw new IOException("Gemini API request failed: " + response.code() + " - " + errorBody); + } + + String responseBody = response.body().string(); + logger.debug("Gemini API response: {}", responseBody); + + // Parse the response + JsonNode rootNode = objectMapper.readTree(responseBody); + JsonNode candidates = rootNode.get("candidates"); + + if (candidates == null || candidates.isEmpty()) { + throw new RuntimeException("No candidates in Gemini response"); + } + + JsonNode content = candidates.get(0).get("content"); + if (content == null) { + throw new RuntimeException("No content in Gemini response"); + } + + JsonNode parts = content.get("parts"); + if (parts == null || parts.isEmpty()) { + throw new RuntimeException("No parts in Gemini response"); + } + + String generatedText = parts.get(0).get("text").asText().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 (IOException e) { + logger.error("Error generating tweet", e); + throw new RuntimeException("Failed to generate tweet: " + e.getMessage(), e); + } + } + + /** + * Escape special characters for JSON + */ + private String escapeJson(String text) { + return text.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } + + /** + * Shutdown the Gemini service + */ + public void shutdown() { + if (httpClient != null) { + httpClient.dispatcher().executorService().shutdown(); + httpClient.connectionPool().evictAll(); + logger.info("Gemini service shut down"); + } + } +} diff --git a/src/main/java/com/voidcode/tweetbot/service/OpenAIService.java b/src/main/java/com/voidcode/tweetbot/service/OpenAIService.java deleted file mode 100644 index cf93e2d..0000000 --- a/src/main/java/com/voidcode/tweetbot/service/OpenAIService.java +++ /dev/null @@ -1,91 +0,0 @@ -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 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"); - } - } -}