You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

324 lines
15 KiB

# foodie_engagement_tweet.py
import json
import logging
import random
import signal
import sys
import fcntl
import os
import time
from datetime import datetime, timedelta, timezone
from openai import OpenAI
from foodie_utils import (
post_tweet,
AUTHORS,
SUMMARY_MODEL,
check_author_rate_limit,
load_json_file,
save_json_file, # Add this
update_system_activity,
get_next_author_round_robin
)
from foodie_config import X_API_CREDENTIALS, AUTHOR_BACKGROUNDS_FILE
from dotenv import load_dotenv
print("Loading environment variables")
load_dotenv()
print(f"Environment variables loaded: OPENAI_API_KEY={bool(os.getenv('OPENAI_API_KEY'))}")
SCRIPT_NAME = "foodie_engagement_tweet"
LOCK_FILE = "/home/shane/foodie_automator/locks/foodie_engagement_tweet.lock"
LOG_FILE = "/home/shane/foodie_automator/logs/foodie_engagement_tweet.log"
LOG_PRUNE_DAYS = 30
MAX_RETRIES = 3
RETRY_BACKOFF = 2
def setup_logging():
"""Initialize logging with pruning of old logs."""
print("Entering setup_logging")
try:
log_dir = os.path.dirname(LOG_FILE)
print(f"Ensuring log directory exists: {log_dir}")
os.makedirs(log_dir, exist_ok=True)
print(f"Log directory permissions: {os.stat(log_dir).st_mode & 0o777}, owner: {os.stat(log_dir).st_uid}")
if os.path.exists(LOG_FILE):
print(f"Pruning old logs in {LOG_FILE}")
with open(LOG_FILE, 'r') as f:
lines = f.readlines()
cutoff = datetime.now(timezone.utc) - timedelta(days=LOG_PRUNE_DAYS)
pruned_lines = []
malformed_count = 0
for line in lines:
if len(line) < 19 or not line[:19].replace('-', '').replace(':', '').replace(' ', '').isdigit():
malformed_count += 1
continue
try:
timestamp = datetime.strptime(line[:19], '%Y-%m-%d %H:%M:%S').replace(tzinfo=timezone.utc)
if timestamp > cutoff:
pruned_lines.append(line)
except ValueError:
malformed_count += 1
continue
print(f"Skipped {malformed_count} malformed log lines during pruning")
with open(LOG_FILE, 'w') as f:
f.writelines(pruned_lines)
print(f"Log file pruned, new size: {os.path.getsize(LOG_FILE)} bytes")
print(f"Configuring logging to {LOG_FILE}")
logging.basicConfig(
filename=LOG_FILE,
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
logging.getLogger().addHandler(console_handler)
logging.getLogger("openai").setLevel(logging.WARNING)
logging.info("Logging initialized for foodie_engagement_tweet.py")
print("Logging setup complete")
except Exception as e:
print(f"Failed to setup logging: {e}")
sys.exit(1)
def acquire_lock():
"""Acquire a lock to prevent concurrent runs."""
print("Entering acquire_lock")
try:
lock_dir = os.path.dirname(LOCK_FILE)
print(f"Ensuring lock directory exists: {lock_dir}")
os.makedirs(lock_dir, exist_ok=True)
print(f"Opening lock file: {LOCK_FILE}")
lock_fd = open(LOCK_FILE, 'w')
print(f"Attempting to acquire lock on {LOCK_FILE}")
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
lock_fd.write(str(os.getpid()))
lock_fd.flush()
print(f"Lock acquired, PID: {os.getpid()}")
return lock_fd
except IOError as e:
print(f"Failed to acquire lock, another instance is running: {e}")
logging.info("Another instance of foodie_engagement_tweet.py is running")
sys.exit(0)
except Exception as e:
print(f"Unexpected error in acquire_lock: {e}")
sys.exit(1)
def signal_handler(sig, frame):
"""Handle termination signals gracefully."""
print(f"Received signal: {sig}")
logging.info("Received termination signal, marking script as stopped...")
update_system_activity(SCRIPT_NAME, "stopped")
sys.exit(0)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
# Initialize OpenAI client
print("Initializing OpenAI client")
try:
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
if not os.getenv("OPENAI_API_KEY"):
print("OPENAI_API_KEY is not set")
logging.error("OPENAI_API_KEY is not set in environment variables")
raise ValueError("OPENAI_API_KEY is required")
print("OpenAI client initialized")
except Exception as e:
print(f"Failed to initialize OpenAI client: {e}")
logging.error(f"Failed to initialize OpenAI client: {e}", exc_info=True)
sys.exit(1)
# Load author backgrounds
print(f"Loading author backgrounds from {AUTHOR_BACKGROUNDS_FILE}")
try:
with open(AUTHOR_BACKGROUNDS_FILE, 'r') as f:
AUTHOR_BACKGROUNDS = json.load(f)
print(f"Author backgrounds loaded: {len(AUTHOR_BACKGROUNDS)} entries")
except Exception as e:
print(f"Failed to load author_backgrounds.json: {e}")
logging.error(f"Failed to load author_backgrounds.json: {e}", exc_info=True)
sys.exit(1)
def generate_engagement_tweet(author):
"""Generate an engagement tweet using author background themes and persona."""
print(f"Generating tweet for author: {author['username']}")
try:
credentials = X_API_CREDENTIALS.get(author["username"])
if not credentials:
print(f"No X credentials found for {author['username']}")
logging.error(f"No X credentials found for {author['username']}")
return None
author_handle = credentials["x_username"]
print(f"Author handle: {author_handle}")
background = next((bg for bg in AUTHOR_BACKGROUNDS if bg["username"] == author["username"]), {})
if not background or "engagement_themes" not in background:
print(f"No background or themes for {author['username']}, using default theme")
logging.warning(f"No background or engagement themes found for {author['username']}")
theme = "food trends"
else:
theme = random.choice(background["engagement_themes"])
print(f"Selected theme: {theme}")
# Get the author's persona from AUTHORS
persona = next((a["persona"] for a in AUTHORS if a["username"] == author["username"]), "Unknown")
prompt = (
f"Generate a concise tweet (under 230 characters) for {author_handle} as a {persona}. "
f"Create an engaging, specific question about {theme} to spark interaction (e.g., 'What's your go-to sushi spot in Tokyo?'). "
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)."
)
print(f"OpenAI prompt: {prompt}")
for attempt in range(MAX_RETRIES):
print(f"Attempt {attempt + 1} to generate tweet")
try:
response = client.chat.completions.create(
model=SUMMARY_MODEL,
messages=[
{"role": "system", "content": "You are a social media expert crafting engaging tweets."},
{"role": "user", "content": prompt}
],
max_tokens=100,
temperature=0.7
)
tweet = response.choices[0].message.content.strip()
if len(tweet) > 280:
tweet = tweet[:277] + "..."
print(f"Generated tweet: {tweet}")
logging.debug(f"Generated engagement tweet: {tweet}")
return tweet
except Exception as e:
print(f"Failed to generate tweet (attempt {attempt + 1}): {e}")
logging.warning(f"Failed to generate engagement tweet for {author['username']} (attempt {attempt + 1}): {e}")
if attempt < MAX_RETRIES - 1:
time.sleep(RETRY_BACKOFF * (2 ** attempt))
else:
print(f"Exhausted retries for {author['username']}")
logging.error(f"Failed to generate engagement tweet after {MAX_RETRIES} attempts")
engagement_templates = [
f"What's your favorite {theme} dish? Share below and follow {author_handle} for more on InsiderFoodie.com! Link: https://insiderfoodie.com",
f"Which {theme} spot is a must-visit? Tell us and like this tweet for more from {author_handle} on InsiderFoodie.com! Link: https://insiderfoodie.com",
f"Got a {theme} hidden gem? Share it and follow {author_handle} for more on InsiderFoodie.com! Link: https://insiderfoodie.com",
f"What's the best {theme} you've tried? Let us know and like this tweet to keep up with {author_handle} on InsiderFoodie.com! Link: https://insiderfoodie.com"
]
template = random.choice(engagement_templates)
print(f"Using fallback tweet: {template}")
logging.info(f"Using fallback engagement tweet: {template}")
return template
except Exception as e:
print(f"Error in generate_engagement_tweet for {author['username']}: {e}")
logging.error(f"Error in generate_engagement_tweet for {author['username']}: {e}", exc_info=True)
return None
def post_engagement_tweet():
"""Post engagement tweets for all authors with a delay between posts."""
print("Entering post_engagement_tweet")
try:
logging.info("Starting foodie_engagement_tweet.py")
posted = False
state_file = '/home/shane/foodie_automator/author_state.json'
state = load_json_file(state_file, default={'last_author_index': -1})
delay_seconds = 30 # Delay between posts to avoid rate limits and spread engagement
# Iterate through all authors
for index, author in enumerate(AUTHORS):
username = author['username']
print(f"Processing author: {username}")
logging.info(f"Processing author: {username}")
try:
print("Checking rate limit")
if not check_author_rate_limit(author):
print(f"Rate limit exceeded for {username}, skipping")
logging.info(f"Rate limit exceeded for {username}, skipping")
continue
print("Generating tweet")
tweet = generate_engagement_tweet(author)
if not tweet:
print(f"Failed to generate tweet for {username}, skipping")
logging.error(f"Failed to generate engagement tweet for {username}, skipping")
continue
print(f"Posting tweet: {tweet}")
logging.info(f"Posting engagement tweet for {username}: {tweet}")
if post_tweet(author, tweet):
print(f"Successfully posted tweet for {username}")
logging.info(f"Successfully posted engagement tweet for {username}")
posted = True
# Update last_author_index to maintain round-robin consistency
state['last_author_index'] = index
save_json_file(state_file, state)
else:
print(f"Failed to post tweet for {username}")
logging.warning(f"Failed to post tweet for {username}")
# Add delay between posts (except for the last author)
if index < len(AUTHORS) - 1:
print(f"Waiting {delay_seconds} seconds before next post")
logging.info(f"Waiting {delay_seconds} seconds before next post")
time.sleep(delay_seconds)
except Exception as e:
print(f"Error posting tweet for {username}: {e}")
logging.error(f"Error posting tweet for {username}: {e}", exc_info=True)
continue
print("Completed post_engagement_tweet")
logging.info("Completed foodie_engagement_tweet.py")
sleep_time = 86400 # 1 day for cron
return posted, sleep_time
except Exception as e:
print(f"Unexpected error in post_engagement_tweet: {e}")
logging.error(f"Unexpected error in post_engagement_tweet: {e}", exc_info=True)
sleep_time = 86400 # 1 day
return False, sleep_time
def main():
"""Main function to run the script."""
print("Starting main")
lock_fd = None
try:
print("Acquiring lock")
lock_fd = acquire_lock()
print("Setting up logging")
setup_logging()
print("Updating system activity to running")
update_system_activity(SCRIPT_NAME, "running", os.getpid())
print("Checking author state file")
author_state_file = "/home/shane/foodie_automator/author_state.json"
if not os.path.exists(author_state_file):
print(f"Author state file not found: {author_state_file}")
logging.error(f"Author state file not found: {author_state_file}")
raise FileNotFoundError(f"Author state file not found: {author_state_file}")
print(f"Author state file exists: {author_state_file}")
print("Posting engagement tweet")
posted, sleep_time = post_engagement_tweet()
print("Updating system activity to stopped")
update_system_activity(SCRIPT_NAME, "stopped")
print(f"Run completed, posted: {posted}, sleep_time: {sleep_time}")
logging.info(f"Run completed, posted: {posted}, sleep_time: {sleep_time} seconds")
return posted, sleep_time
except Exception as e:
print(f"Exception in main: {e}")
logging.error(f"Fatal error in main: {e}", exc_info=True)
print(f"Fatal error: {e}")
update_system_activity(SCRIPT_NAME, "stopped")
sleep_time = 86400 # 1 day for cron
print(f"Run completed, sleep_time: {sleep_time}")
logging.info(f"Run completed, sleep_time: {sleep_time} seconds")
return False, sleep_time
finally:
if lock_fd:
print("Releasing lock")
fcntl.flock(lock_fd, fcntl.LOCK_UN)
lock_fd.close()
os.remove(LOCK_FILE) if os.path.exists(LOCK_FILE) else None
print(f"Lock file removed: {LOCK_FILE}")
if __name__ == "__main__":
posted, sleep_time = main()