init
This commit is contained in:
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