diff --git a/foodie_engagement_tweet.py b/foodie_engagement_tweet.py index 8000c97..b2f3da8 100644 --- a/foodie_engagement_tweet.py +++ b/foodie_engagement_tweet.py @@ -8,6 +8,7 @@ import fcntl import os import time from datetime import datetime, timedelta, timezone +import tweepy from openai import OpenAI from foodie_utils import post_tweet, load_post_counts, save_post_counts from foodie_config import ( @@ -61,6 +62,7 @@ def setup_logging(): console_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) logging.getLogger().addHandler(console_handler) logging.getLogger("openai").setLevel(logging.WARNING) + logging.getLogger("tweepy").setLevel(logging.WARNING) logging.info("Logging initialized for foodie_engagement_tweet.py") except Exception as e: print(f"Failed to setup logging: {e}") @@ -101,10 +103,39 @@ except Exception as e: try: with open(AUTHOR_BACKGROUNDS_FILE, 'r') as f: AUTHOR_BACKGROUNDS = json.load(f) + logging.debug(f"Loaded author backgrounds: {[bg['username'] for bg in AUTHOR_BACKGROUNDS]}") except Exception as e: logging.error(f"Failed to load author_backgrounds.json: {e}", exc_info=True) + AUTHOR_BACKGROUNDS = [] sys.exit(1) +def validate_twitter_credentials(author): + """Validate Twitter API credentials for a specific author.""" + username = author["username"] + credentials = X_API_CREDENTIALS.get(username) + if not credentials: + logging.error(f"No X credentials found for {username}") + return False + for attempt in range(MAX_RETRIES): + try: + twitter_client = tweepy.Client( + consumer_key=credentials["api_key"], + consumer_secret=credentials["api_secret"], + access_token=credentials["access_token"], + access_token_secret=credentials["access_token_secret"] + ) + user = twitter_client.get_me() + logging.info(f"Credentials valid for {username} (handle: {credentials['x_username']})") + return True + except tweepy.TweepyException as e: + logging.warning(f"Failed to validate credentials for {username} (attempt {attempt + 1}): {e}") + if attempt < MAX_RETRIES - 1: + time.sleep(RETRY_BACKOFF * (2 ** attempt)) + else: + logging.error(f"Credentials invalid for {username} after {MAX_RETRIES} attempts") + return False + return False + def get_reference_date(): """Load or initialize the reference date for the 2-day interval.""" os.makedirs(os.path.dirname(REFERENCE_DATE_FILE), exist_ok=True) @@ -130,20 +161,26 @@ def get_reference_date(): def generate_engagement_tweet(author): """Generate an engagement tweet using author background themes and persona.""" username = author["username"] - credentials = X_API_CREDENTIALS.get(username) - if not credentials: - logging.error(f"No X credentials found for {username}") + if not validate_twitter_credentials(author): + logging.error(f"Skipping tweet generation for {username} due to invalid credentials") return None + + credentials = X_API_CREDENTIALS.get(username) author_handle = credentials["x_username"] persona = author["persona"] persona_config = PERSONA_CONFIGS.get(persona, PERSONA_CONFIGS["Visionary Editor"]) - background = next((bg for bg in AUTHOR_BACKGROUNDS if bg["username"] == username), {}) + # Case-insensitive lookup for background + background = next( + (bg for bg in AUTHOR_BACKGROUNDS if bg["username"].lower() == username.lower()), + {} + ) if not background or "engagement_themes" not in background: logging.warning(f"No background or engagement themes found for {username}, using default theme") theme = "food trends" else: theme = random.choice(background["engagement_themes"]) + logging.debug(f"Selected engagement theme '{theme}' for {username}") base_prompt = persona_config["x_prompt"].format( description=persona_config["description"], @@ -152,7 +189,8 @@ def generate_engagement_tweet(author): prompt = ( f"{base_prompt}\n\n" f"Generate an engagement tweet for {author_handle} asking a question about {theme} to engage the public. " - f"Keep it under 280 characters, using {persona_config['tone']}. " + f"Keep it under 230 characters to ensure room for the URL. " + f"Use {persona_config['tone']}. " f"Include a call to action to follow {author_handle} or like the tweet, and mention InsiderFoodie.com with a link to https://insiderfoodie.com. " f"Avoid using the word 'elevate'—use more humanized language like 'level up' or 'bring to life'. " f"Do not include emojis, hashtags, or reward-driven incentives (e.g., giveaways). " @@ -167,12 +205,14 @@ def generate_engagement_tweet(author): {"role": "system", "content": "You are a social media expert crafting engaging tweets."}, {"role": "user", "content": prompt} ], - max_tokens=100, + max_tokens=80, # Reduced to ensure shorter tweets temperature=0.7 ) tweet = response.choices[0].message.content.strip() - if len(tweet) > 280: - tweet = tweet[:277] + "..." + # Ensure tweet length is within limits (accounting for URL) + url_length = 23 # Twitter shortens URLs + if len(tweet) > (280 - url_length): + tweet = tweet[:(280 - url_length - 3)] + "..." logging.debug(f"Generated engagement tweet for {username}: {tweet}") return tweet except Exception as e: @@ -182,11 +222,14 @@ def generate_engagement_tweet(author): else: logging.error(f"Failed to generate engagement tweet after {MAX_RETRIES} attempts") fallback = ( - f"What's the hottest {theme} you're into? Share and follow {author_handle} for more on InsiderFoodie.com! " - f"Link: https://insiderfoodie.com" + f"What's the hottest {theme}? Share and follow {author_handle} for more on InsiderFoodie.com! " + f"https://insiderfoodie.com" ) + if len(fallback) > (280 - url_length): + fallback = fallback[:(280 - url_length - 3)] + "..." logging.info(f"Using fallback engagement tweet: {fallback}") return fallback + return None def post_engagement_tweet(): """Post engagement tweets for authors every 2 days."""