Using API v2 for posting
This commit is contained in:
@@ -57,6 +57,8 @@ An automated Twitter bot that generates and posts tweets using Google Gemini's f
|
|||||||
- Navigate to "User authentication settings"
|
- Navigate to "User authentication settings"
|
||||||
- Set app permissions to "Read and Write"
|
- 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
|
### 3. Configure the Application
|
||||||
|
|
||||||
1. Copy the example environment file:
|
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
|
2. Verify your app has Read and Write permissions
|
||||||
3. Regenerate access tokens if needed
|
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"
|
### "Failed to generate tweet"
|
||||||
|
|
||||||
1. Verify your Gemini API key is valid
|
1. Verify your Gemini API key is valid
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public class TwitterService {
|
|||||||
private final String accessTokenSecret;
|
private final String accessTokenSecret;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private static final String MEDIA_UPLOAD_URL = "https://upload.twitter.com/1.1/media/upload.json";
|
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) {
|
public TwitterService(String apiKey, String apiSecret, String accessToken, String accessTokenSecret) {
|
||||||
this.apiKey = apiKey;
|
this.apiKey = apiKey;
|
||||||
@@ -37,7 +37,7 @@ public class TwitterService {
|
|||||||
this.accessToken = accessToken;
|
this.accessToken = accessToken;
|
||||||
this.accessTokenSecret = accessTokenSecret;
|
this.accessTokenSecret = accessTokenSecret;
|
||||||
this.objectMapper = new ObjectMapper();
|
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 {
|
try {
|
||||||
logger.info("Posting tweet: {}", tweetText);
|
logger.info("Posting tweet: {}", tweetText);
|
||||||
|
|
||||||
// Build query parameters
|
// Build JSON request body for Twitter API v2
|
||||||
Map<String, String> params = new TreeMap<>();
|
ObjectNode tweetData = objectMapper.createObjectNode();
|
||||||
params.put("status", tweetText);
|
tweetData.put("text", tweetText);
|
||||||
|
|
||||||
// Upload media if image path is provided
|
// Upload media if image path is provided
|
||||||
if (imagePath != null && !imagePath.isEmpty()) {
|
if (imagePath != null && !imagePath.isEmpty()) {
|
||||||
@@ -72,32 +72,35 @@ public class TwitterService {
|
|||||||
try {
|
try {
|
||||||
String mediaId = uploadMedia(imageFile);
|
String mediaId = uploadMedia(imageFile);
|
||||||
logger.info("Media uploaded successfully. Media ID: {}", mediaId);
|
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) {
|
} catch (Exception e) {
|
||||||
logger.error("Failed to upload media, posting tweet without image", e);
|
logger.error("Failed to upload media, posting tweet without image", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build URL with query parameters
|
String requestBody = objectMapper.writeValueAsString(tweetData);
|
||||||
StringBuilder urlBuilder = new StringBuilder(TWEET_CREATE_URL);
|
logger.debug("Request body: {}", requestBody);
|
||||||
urlBuilder.append("?");
|
|
||||||
for (Map.Entry<String, String> 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();
|
|
||||||
|
|
||||||
// Create OAuth header (include query params in signature)
|
// Create OAuth header (no query params for v2)
|
||||||
String authHeader = generateOAuthHeader("POST", TWEET_CREATE_URL, params);
|
String authHeader = generateOAuthHeader("POST", TWEET_CREATE_URL, new TreeMap<>());
|
||||||
|
|
||||||
// Make HTTP request
|
// Make HTTP request
|
||||||
HttpURLConnection connection = (HttpURLConnection) new URL(fullUrl).openConnection();
|
HttpURLConnection connection = (HttpURLConnection) new URL(TWEET_CREATE_URL).openConnection();
|
||||||
connection.setRequestMethod("POST");
|
connection.setRequestMethod("POST");
|
||||||
connection.setRequestProperty("Authorization", authHeader);
|
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
|
// Read response
|
||||||
int responseCode = connection.getResponseCode();
|
int responseCode = connection.getResponseCode();
|
||||||
@@ -116,14 +119,14 @@ public class TwitterService {
|
|||||||
}
|
}
|
||||||
reader.close();
|
reader.close();
|
||||||
|
|
||||||
if (responseCode != 200) {
|
if (responseCode != 200 && responseCode != 201) {
|
||||||
logger.error("Failed to post tweet. Status code: {}, Response: {}", responseCode, response.toString());
|
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());
|
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());
|
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("Tweet posted successfully. Tweet ID: {}", tweetId);
|
||||||
logger.info("View tweet at: https://twitter.com/user/status/{}", tweetId);
|
logger.info("View tweet at: https://twitter.com/user/status/{}", tweetId);
|
||||||
|
|||||||
Reference in New Issue
Block a user