diff --git a/foodie_engagement_tweet.py b/foodie_engagement_tweet.py index 04ef41f..073e0f3 100644 --- a/foodie_engagement_tweet.py +++ b/foodie_engagement_tweet.py @@ -414,6 +414,10 @@ def post_engagement_tweet(): save_post_counts(post_counts) else: logging.warning(f"Failed to post engagement tweet for {username}") + + # Add a 5-second delay between posts to avoid rate limits + time.sleep(5) + except Exception as e: logging.error(f"Error posting engagement tweet for {username}: {e}", exc_info=True) continue diff --git a/foodie_utils.py b/foodie_utils.py index 3042632..2e3d000 100644 --- a/foodie_utils.py +++ b/foodie_utils.py @@ -222,6 +222,9 @@ def post_tweet(author, tweet, reply_to_id=None): import logging import tweepy from datetime import datetime, timezone + import time + import random + from tweepy.errors import TooManyRequests username = author["username"] 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}") return False + max_retries = 3 + retry_delay = 5 # Initial delay in seconds + try: client = tweepy.Client( 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_secret=credentials["access_token_secret"] ) - response = client.create_tweet( - text=tweet, - in_reply_to_tweet_id=reply_to_id - ) - # 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 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}") - if "forbidden" in str(e).lower(): - logging.error(f"Possible causes: invalid credentials, insufficient permissions, or account restrictions for {credentials['x_username']}") - return False + + for attempt in range(max_retries + 1): + try: + response = client.create_tweet( + text=tweet, + in_reply_to_tweet_id=reply_to_id + ) + # Log rate limit headers on success + if hasattr(client, 'session') and client.session.last_response: + rate_limit_headers = { + "x-rate-limit-limit": client.session.last_response.headers.get("x-rate-limit-limit"), + "x-rate-limit-remaining": client.session.last_response.headers.get("x-rate-limit-remaining"), + "x-rate-limit-reset": client.session.last_response.headers.get("x-rate-limit-reset"), + } + logging.debug(f"Rate limit headers after posting for {username}: {rate_limit_headers}") + else: + logging.debug("No rate limit headers available in response") + + # 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: logging.error(f"Unexpected error posting tweet for {username} (handle: {credentials['x_username']}): {e}", exc_info=True) return False