|
|
|
@ -222,6 +222,9 @@ def post_tweet(author, tweet, reply_to_id=None): |
|
|
|
import logging |
|
|
|
import logging |
|
|
|
import tweepy |
|
|
|
import tweepy |
|
|
|
from datetime import datetime, timezone |
|
|
|
from datetime import datetime, timezone |
|
|
|
|
|
|
|
import time |
|
|
|
|
|
|
|
import random |
|
|
|
|
|
|
|
from tweepy.errors import TooManyRequests |
|
|
|
|
|
|
|
|
|
|
|
username = author["username"] |
|
|
|
username = author["username"] |
|
|
|
if username not in X_API_CREDENTIALS: |
|
|
|
if username not in X_API_CREDENTIALS: |
|
|
|
@ -260,6 +263,9 @@ def post_tweet(author, tweet, reply_to_id=None): |
|
|
|
logging.warning(f"Daily post limit (20) reached for {username}") |
|
|
|
logging.warning(f"Daily post limit (20) reached for {username}") |
|
|
|
return False |
|
|
|
return False |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
max_retries = 3 |
|
|
|
|
|
|
|
retry_delay = 5 # Initial delay in seconds |
|
|
|
|
|
|
|
|
|
|
|
try: |
|
|
|
try: |
|
|
|
client = tweepy.Client( |
|
|
|
client = tweepy.Client( |
|
|
|
consumer_key=credentials["api_key"], |
|
|
|
consumer_key=credentials["api_key"], |
|
|
|
@ -267,24 +273,75 @@ def post_tweet(author, tweet, reply_to_id=None): |
|
|
|
access_token=credentials["access_token"], |
|
|
|
access_token=credentials["access_token"], |
|
|
|
access_token_secret=credentials["access_token_secret"] |
|
|
|
access_token_secret=credentials["access_token_secret"] |
|
|
|
) |
|
|
|
) |
|
|
|
response = client.create_tweet( |
|
|
|
|
|
|
|
text=tweet, |
|
|
|
for attempt in range(max_retries + 1): |
|
|
|
in_reply_to_tweet_id=reply_to_id |
|
|
|
try: |
|
|
|
) |
|
|
|
response = client.create_tweet( |
|
|
|
# Update post counts |
|
|
|
text=tweet, |
|
|
|
author_count["monthly_count"] += 1 |
|
|
|
in_reply_to_tweet_id=reply_to_id |
|
|
|
author_count["daily_count"] += 1 |
|
|
|
) |
|
|
|
save_post_counts(post_counts) |
|
|
|
# Log rate limit headers on success |
|
|
|
logging.info(f"Posted tweet for {username} (handle: {credentials['x_username']}): {tweet}") |
|
|
|
if hasattr(client, 'session') and client.session.last_response: |
|
|
|
logging.debug(f"Tweet ID: {response.data['id']}") |
|
|
|
rate_limit_headers = { |
|
|
|
return {"id": response.data["id"]} |
|
|
|
"x-rate-limit-limit": client.session.last_response.headers.get("x-rate-limit-limit"), |
|
|
|
except tweepy.TweepyException as e: |
|
|
|
"x-rate-limit-remaining": client.session.last_response.headers.get("x-rate-limit-remaining"), |
|
|
|
logging.error(f"Failed to post tweet for {username} (handle: {credentials['x_username']}): {e}") |
|
|
|
"x-rate-limit-reset": client.session.last_response.headers.get("x-rate-limit-reset"), |
|
|
|
if hasattr(e, 'response') and e.response: |
|
|
|
} |
|
|
|
logging.error(f"Twitter API response: {e.response.text}") |
|
|
|
logging.debug(f"Rate limit headers after posting for {username}: {rate_limit_headers}") |
|
|
|
if "forbidden" in str(e).lower(): |
|
|
|
else: |
|
|
|
logging.error(f"Possible causes: invalid credentials, insufficient permissions, or account restrictions for {credentials['x_username']}") |
|
|
|
logging.debug("No rate limit headers available in response") |
|
|
|
return False |
|
|
|
|
|
|
|
|
|
|
|
# Update post counts |
|
|
|
|
|
|
|
author_count["monthly_count"] += 1 |
|
|
|
|
|
|
|
author_count["daily_count"] += 1 |
|
|
|
|
|
|
|
save_post_counts(post_counts) |
|
|
|
|
|
|
|
logging.info(f"Posted tweet for {username} (handle: {credentials['x_username']}): {tweet}") |
|
|
|
|
|
|
|
logging.debug(f"Tweet ID: {response.data['id']}") |
|
|
|
|
|
|
|
return {"id": response.data["id"]} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
except TooManyRequests as e: |
|
|
|
|
|
|
|
if attempt == max_retries: |
|
|
|
|
|
|
|
logging.error(f"Failed to post tweet for {username} after {max_retries} retries due to rate limit") |
|
|
|
|
|
|
|
return False |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Log rate limit headers on failure |
|
|
|
|
|
|
|
rate_limit_headers = {} |
|
|
|
|
|
|
|
if hasattr(e, 'response') and e.response: |
|
|
|
|
|
|
|
rate_limit_headers = { |
|
|
|
|
|
|
|
"x-rate-limit-limit": e.response.headers.get("x-rate-limit-limit"), |
|
|
|
|
|
|
|
"x-rate-limit-remaining": e.response.headers.get("x-rate-limit-remaining"), |
|
|
|
|
|
|
|
"x-rate-limit-reset": e.response.headers.get("x-rate-limit-reset"), |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
logging.debug(f"Rate limit headers after failed post for {username}: {rate_limit_headers}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Determine wait time based on reset header or fallback to exponential backoff |
|
|
|
|
|
|
|
reset_time = rate_limit_headers.get("x-rate-limit-reset") |
|
|
|
|
|
|
|
if reset_time: |
|
|
|
|
|
|
|
wait_time = int(reset_time) - int(time.time()) + 1 |
|
|
|
|
|
|
|
wait_time = max(wait_time, 1) # Ensure we wait at least 1 second |
|
|
|
|
|
|
|
else: |
|
|
|
|
|
|
|
wait_time = retry_delay * (2 ** attempt) + random.uniform(0, 1) # Exponential backoff with jitter |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logging.warning( |
|
|
|
|
|
|
|
f"Rate limit exceeded for {username}. Attempt {attempt + 1}/{max_retries}. " |
|
|
|
|
|
|
|
f"Waiting {wait_time:.2f} seconds before retrying..." |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
time.sleep(wait_time) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
except tweepy.TweepyException as e: |
|
|
|
|
|
|
|
logging.error(f"Failed to post tweet for {username} (handle: {credentials['x_username']}): {e}") |
|
|
|
|
|
|
|
if hasattr(e, 'response') and e.response: |
|
|
|
|
|
|
|
logging.error(f"Twitter API response: {e.response.text}") |
|
|
|
|
|
|
|
rate_limit_headers = { |
|
|
|
|
|
|
|
"x-rate-limit-limit": e.response.headers.get("x-rate-limit-limit"), |
|
|
|
|
|
|
|
"x-rate-limit-remaining": e.response.headers.get("x-rate-limit-remaining"), |
|
|
|
|
|
|
|
"x-rate-limit-reset": e.response.headers.get("x-rate-limit-reset"), |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
logging.debug(f"Rate limit headers after failed post for {username}: {rate_limit_headers}") |
|
|
|
|
|
|
|
if "forbidden" in str(e).lower(): |
|
|
|
|
|
|
|
logging.error(f"Possible causes: invalid credentials, insufficient permissions, or account restrictions for {credentials['x_username']}") |
|
|
|
|
|
|
|
return False |
|
|
|
|
|
|
|
|
|
|
|
except Exception as e: |
|
|
|
except Exception as e: |
|
|
|
logging.error(f"Unexpected error posting tweet for {username} (handle: {credentials['x_username']}): {e}", exc_info=True) |
|
|
|
logging.error(f"Unexpected error posting tweet for {username} (handle: {credentials['x_username']}): {e}", exc_info=True) |
|
|
|
return False |
|
|
|
return False |
|
|
|
|