diff --git a/README.md b/README.md index 09a721e..a43f595 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ An automated Twitter bot that generates and posts tweets using Google Gemini's f - Navigate to "User authentication settings" - Set app permissions to "Read and Write" +**Note:** This application uses Twitter API v2 for posting tweets (compatible with Free and Basic access tiers) and v1.1 for media uploads. + ### 3. Configure the Application 1. Copy the example environment file: @@ -373,6 +375,10 @@ Ensure your `.env` file exists and contains all required variables. 2. Verify your app has Read and Write permissions 3. Regenerate access tokens if needed +### "You currently have access to a subset of X API V2 endpoints" (403 error code 453) + +This error means your Twitter API access level doesn't support v1.1 tweet posting. The code has been updated to use API v2 which is supported by all access tiers (Free, Basic, Pro). Make sure you're using the latest version of the code. + ### "Failed to generate tweet" 1. Verify your Gemini API key is valid diff --git a/src/main/java/com/voidcode/tweetbot/service/TwitterService.java b/src/main/java/com/voidcode/tweetbot/service/TwitterService.java index edf4571..0bfbe6e 100644 --- a/src/main/java/com/voidcode/tweetbot/service/TwitterService.java +++ b/src/main/java/com/voidcode/tweetbot/service/TwitterService.java @@ -29,7 +29,7 @@ public class TwitterService { private final String accessTokenSecret; private final ObjectMapper objectMapper; private static final String MEDIA_UPLOAD_URL = "https://upload.twitter.com/1.1/media/upload.json"; - private static final String TWEET_CREATE_URL = "https://api.twitter.com/1.1/statuses/update.json"; + private static final String TWEET_CREATE_URL = "https://api.twitter.com/2/tweets"; public TwitterService(String apiKey, String apiSecret, String accessToken, String accessTokenSecret) { this.apiKey = apiKey; @@ -37,7 +37,7 @@ public class TwitterService { this.accessToken = accessToken; this.accessTokenSecret = accessTokenSecret; this.objectMapper = new ObjectMapper(); - logger.info("Twitter service initialized successfully with OAuth 1.0a"); + logger.info("Twitter service initialized successfully with OAuth 1.0a (using API v2 for tweets)"); } /** @@ -59,9 +59,9 @@ public class TwitterService { try { logger.info("Posting tweet: {}", tweetText); - // Build query parameters - Map params = new TreeMap<>(); - params.put("status", tweetText); + // Build JSON request body for Twitter API v2 + ObjectNode tweetData = objectMapper.createObjectNode(); + tweetData.put("text", tweetText); // Upload media if image path is provided if (imagePath != null && !imagePath.isEmpty()) { @@ -72,32 +72,35 @@ public class TwitterService { try { String mediaId = uploadMedia(imageFile); logger.info("Media uploaded successfully. Media ID: {}", mediaId); - params.put("media_ids", mediaId); + + // Add media to tweet (v2 format) + ObjectNode media = objectMapper.createObjectNode(); + media.put("media_ids", objectMapper.createArrayNode().add(mediaId)); + tweetData.set("media", media); } catch (Exception e) { logger.error("Failed to upload media, posting tweet without image", e); } } } - // Build URL with query parameters - StringBuilder urlBuilder = new StringBuilder(TWEET_CREATE_URL); - urlBuilder.append("?"); - for (Map.Entry entry : params.entrySet()) { - if (urlBuilder.charAt(urlBuilder.length() - 1) != '?') { - urlBuilder.append("&"); - } - urlBuilder.append(urlEncode(entry.getKey())).append("=").append(urlEncode(entry.getValue())); - } - String fullUrl = urlBuilder.toString(); + String requestBody = objectMapper.writeValueAsString(tweetData); + logger.debug("Request body: {}", requestBody); - // Create OAuth header (include query params in signature) - String authHeader = generateOAuthHeader("POST", TWEET_CREATE_URL, params); + // Create OAuth header (no query params for v2) + String authHeader = generateOAuthHeader("POST", TWEET_CREATE_URL, new TreeMap<>()); // Make HTTP request - HttpURLConnection connection = (HttpURLConnection) new URL(fullUrl).openConnection(); + HttpURLConnection connection = (HttpURLConnection) new URL(TWEET_CREATE_URL).openConnection(); connection.setRequestMethod("POST"); connection.setRequestProperty("Authorization", authHeader); - connection.setRequestProperty("Content-Length", "0"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setDoOutput(true); + + // Send request body + try (OutputStream os = connection.getOutputStream()) { + byte[] input = requestBody.getBytes(StandardCharsets.UTF_8); + os.write(input, 0, input.length); + } // Read response int responseCode = connection.getResponseCode(); @@ -116,14 +119,14 @@ public class TwitterService { } reader.close(); - if (responseCode != 200) { + if (responseCode != 200 && responseCode != 201) { logger.error("Failed to post tweet. Status code: {}, Response: {}", responseCode, response.toString()); throw new IOException("Failed to post tweet. Status code: " + responseCode + ", Response: " + response.toString()); } - // Parse tweet ID from response (v1.1 format) + // Parse tweet ID from response (v2 format) JsonNode responseJson = objectMapper.readTree(response.toString()); - String tweetId = responseJson.get("id_str").asText(); + String tweetId = responseJson.get("data").get("id").asText(); logger.info("Tweet posted successfully. Tweet ID: {}", tweetId); logger.info("View tweet at: https://twitter.com/user/status/{}", tweetId);