try
This commit is contained in:
+37
-33
@@ -1204,10 +1204,9 @@ def get_next_author_round_robin():
|
|||||||
def get_x_rate_limit_status(author):
|
def get_x_rate_limit_status(author):
|
||||||
"""
|
"""
|
||||||
Check the X API Free tier rate limit by posting a test tweet.
|
Check the X API Free tier rate limit by posting a test tweet.
|
||||||
Returns (remaining, reset) based on app-level headers (x-rate-limit-remaining, x-rate-limit-reset).
|
Returns (remaining, reset) based on app-level or user-level 24-hour headers.
|
||||||
Returns (None, None) if the check fails.
|
Returns (None, None) if the check fails.
|
||||||
"""
|
"""
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
username = author['username']
|
username = author['username']
|
||||||
credentials = X_API_CREDENTIALS.get(username)
|
credentials = X_API_CREDENTIALS.get(username)
|
||||||
if not credentials:
|
if not credentials:
|
||||||
@@ -1223,18 +1222,40 @@ def get_x_rate_limit_status(author):
|
|||||||
url = 'https://api.x.com/2/tweets'
|
url = 'https://api.x.com/2/tweets'
|
||||||
payload = {'text': f'Test tweet to check rate limits for {username} - please ignore {int(time.time())}'}
|
payload = {'text': f'Test tweet to check rate limits for {username} - please ignore {int(time.time())}'}
|
||||||
|
|
||||||
|
# Add delay to avoid IP-based rate limiting
|
||||||
|
logger.info(f"Waiting 5 seconds before attempting to post for {username}")
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(url, json=payload, auth=oauth)
|
response = requests.post(url, json=payload, auth=oauth)
|
||||||
headers = response.headers
|
headers = response.headers
|
||||||
logger.debug(f"Rate limit headers for {username}: {headers}")
|
logger.debug(f"Rate limit headers for {username}: {headers}")
|
||||||
|
|
||||||
# Extract app-level rate limit info from headers
|
# Initialize defaults
|
||||||
remaining_str = headers.get('x-rate-limit-remaining')
|
remaining = None
|
||||||
reset_str = headers.get('x-rate-limit-reset')
|
reset = None
|
||||||
if remaining_str is None or reset_str is None:
|
current_time = int(time.time())
|
||||||
logger.error(f"Rate limit headers missing for {username}: {headers}")
|
|
||||||
|
if response.status_code == 201:
|
||||||
|
# Extract app-level 24-hour limits
|
||||||
|
remaining_str = headers.get('x-app-limit-24hour-remaining')
|
||||||
|
reset_str = headers.get('x-app-limit-24hour-reset')
|
||||||
|
if remaining_str is None or reset_str is None:
|
||||||
|
logger.error(f"App 24-hour limit headers missing for {username}: {headers}")
|
||||||
|
return None, None
|
||||||
|
elif response.status_code == 429:
|
||||||
|
# Extract user-level 24-hour limits for rate limit exceeded
|
||||||
|
remaining_str = headers.get('x-user-limit-24hour-remaining')
|
||||||
|
reset_str = headers.get('x-user-limit-24hour-reset')
|
||||||
|
if remaining_str is None or reset_str is None:
|
||||||
|
logger.error(f"User 24-hour limit headers missing for {username}: {headers}")
|
||||||
|
return None, None
|
||||||
|
logger.info(f"Rate limit exceeded for {username}")
|
||||||
|
else:
|
||||||
|
logger.error(f"Unexpected response for {username}: {response.status_code} - {response.text}")
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
# Parse headers
|
||||||
try:
|
try:
|
||||||
remaining = int(remaining_str)
|
remaining = int(remaining_str)
|
||||||
reset = int(reset_str)
|
reset = int(reset_str)
|
||||||
@@ -1248,7 +1269,6 @@ def get_x_rate_limit_status(author):
|
|||||||
remaining = min(remaining, 17)
|
remaining = min(remaining, 17)
|
||||||
|
|
||||||
# Ensure reset is in the future
|
# Ensure reset is in the future
|
||||||
current_time = int(time.time())
|
|
||||||
if reset <= current_time or reset > current_time + 2 * 86400: # Allow up to 48 hours
|
if reset <= current_time or reset > current_time + 2 * 86400: # Allow up to 48 hours
|
||||||
logger.warning(f"Invalid reset time {reset} ({datetime.fromtimestamp(reset, tz=timezone.utc)}) for {username}. Setting to 24 hours from now.")
|
logger.warning(f"Invalid reset time {reset} ({datetime.fromtimestamp(reset, tz=timezone.utc)}) for {username}. Setting to 24 hours from now.")
|
||||||
reset = current_time + 86400 # 24 hours
|
reset = current_time + 86400 # 24 hours
|
||||||
@@ -1263,28 +1283,20 @@ def get_x_rate_limit_status(author):
|
|||||||
logger.info(f"Successfully deleted test tweet {tweet_id} for {username}")
|
logger.info(f"Successfully deleted test tweet {tweet_id} for {username}")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Failed to delete test tweet {tweet_id} for {username}: {delete_response.status_code} - {delete_response.text}")
|
logger.warning(f"Failed to delete test tweet {tweet_id} for {username}: {delete_response.status_code} - {delete_response.text}")
|
||||||
logger.info(f"Rate limit for {username}: {remaining} remaining, reset at {datetime.fromtimestamp(reset, tz=timezone.utc)}")
|
|
||||||
return remaining, reset
|
logger.info(f"Rate limit for {username}: {remaining} remaining, reset at {datetime.fromtimestamp(reset, tz=timezone.utc)}")
|
||||||
elif response.status_code == 429:
|
return remaining, reset
|
||||||
logger.info(f"Rate limit exceeded for {username}: {remaining} remaining, reset at {datetime.fromtimestamp(reset, tz=timezone.utc)}")
|
|
||||||
return remaining, reset
|
|
||||||
elif response.status_code == 403:
|
|
||||||
logger.warning(f"403 Forbidden for {username}: {response.text}, rate limit info: {remaining} remaining, reset at {datetime.fromtimestamp(reset, tz=timezone.utc)}")
|
|
||||||
return remaining, reset
|
|
||||||
else:
|
|
||||||
logger.error(f"Unexpected response for {username}: {response.status_code} - {response.text}")
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Unexpected error fetching X rate limit for {username}: {e}", exc_info=True)
|
logger.error(f"Unexpected error fetching X rate limit for {username}: {e}", exc_info=True)
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
def check_author_rate_limit(author, max_tweets=17, tweet_window_seconds=86400):
|
def check_author_rate_limit(author, max_tweets=17, tweet_window_seconds=86400):
|
||||||
"""
|
"""
|
||||||
Check if an author can post based on their X API Free tier quota (17 tweets per 24 hours per app).
|
Check if an author can post based on their X API Free tier quota (17 tweets per 24 hours per user).
|
||||||
Posts a test tweet only on script restart or for new authors, then tracks tweets in rate_limit_info.json.
|
Posts a test tweet only on script restart or for new authors, then tracks tweets in rate_limit_info.json.
|
||||||
Returns (can_post, remaining, reset_timestamp) where can_post is True if tweets are available.
|
Returns (can_post, remaining, reset_timestamp) where can_post is True if tweets are available.
|
||||||
"""
|
"""
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
rate_limit_file = '/home/shane/foodie_automator/rate_limit_info.json'
|
rate_limit_file = '/home/shane/foodie_automator/rate_limit_info.json'
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
|
|
||||||
@@ -1336,22 +1348,14 @@ def check_author_rate_limit(author, max_tweets=17, tweet_window_seconds=86400):
|
|||||||
reset = current_time + tweet_window_seconds
|
reset = current_time + tweet_window_seconds
|
||||||
logger.warning(f"Test tweet failed for {username}, resetting quota to {max_tweets}")
|
logger.warning(f"Test tweet failed for {username}, resetting quota to {max_tweets}")
|
||||||
else:
|
else:
|
||||||
# Only update remaining if the API reset time is newer and valid
|
remaining = min(remaining, max_tweets) # Ensure within Free tier limit
|
||||||
if api_reset > author_info.get('tweet_reset', 0) and api_reset > current_time:
|
reset = api_reset
|
||||||
logger.info(f"Updating quota for {username} from API: {remaining} remaining, reset at {datetime.fromtimestamp(api_reset, tz=timezone.utc)}")
|
|
||||||
author_info['tweet_remaining'] = remaining
|
|
||||||
author_info['tweet_reset'] = api_reset
|
|
||||||
else:
|
|
||||||
logger.info(f"Retaining previous quota for {username}: {author_info['tweet_remaining']} remaining")
|
|
||||||
remaining = author_info['tweet_remaining']
|
|
||||||
# Keep the existing reset time (from quota reset or previous state)
|
|
||||||
reset = author_info['tweet_reset']
|
|
||||||
|
|
||||||
# Update author info
|
# Update author info
|
||||||
author_info['tweets_posted_in_run'] = 0
|
|
||||||
author_info['script_run_id'] = check_author_rate_limit.script_run_id
|
|
||||||
author_info['tweet_remaining'] = remaining
|
author_info['tweet_remaining'] = remaining
|
||||||
author_info['tweet_reset'] = reset
|
author_info['tweet_reset'] = reset
|
||||||
|
author_info['tweets_posted_in_run'] = 0
|
||||||
|
author_info['script_run_id'] = check_author_rate_limit.script_run_id
|
||||||
rate_limit_info[username] = author_info
|
rate_limit_info[username] = author_info
|
||||||
save_json_file(rate_limit_file, rate_limit_info)
|
save_json_file(rate_limit_file, rate_limit_info)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user