@ -6,19 +6,21 @@ import signal
import sys
import sys
import fcntl
import fcntl
import os
import os
import time
from datetime import datetime , timedelta , timezone
from datetime import datetime , timedelta , timezone
from openai import OpenAI
from openai import OpenAI
from foodie_utils import post_tweet , AUTHORS , SUMMARY_MODEL , load_post_counts , save_post_counts
from foodie_utils import post_tweet , load_post_counts , save_post_counts
from foodie_config import X_API_CREDENTIALS , AUTHOR_BACKGROUNDS_FILE
from foodie_config import (
AUTHORS , SUMMARY_MODEL , X_API_CREDENTIALS , AUTHOR_BACKGROUNDS_FILE ,
PERSONA_CONFIGS , ENGAGEMENT_REFERENCE_DATE_FILE
)
from dotenv import load_dotenv
from dotenv import load_dotenv
from foodie_config import ENGAGEMENT_REFERENCE_DATE_FILE
load_dotenv ( )
load_dotenv ( )
REFERENCE_DATE_FILE = ENGAGEMENT_REFERENCE_DATE_FILE
REFERENCE_DATE_FILE = ENGAGEMENT_REFERENCE_DATE_FILE
LOCK_FILE = " /home/shane/foodie_automator/locks/foodie_engagement_tweet.lock "
LOCK_FILE = " /home/shane/foodie_automator/locks/foodie_engagement_tweet.lock "
LOG_FILE = " /home/shane/foodie_automator/logs/foodie_engagement_tweet.log "
LOG_FILE = " /home/shane/foodie_automator/logs/foodie_engagement_tweet.log "
REFERENCE_DATE_FILE = " /home/shane/foodie_automator/engagement_reference_date.json "
LOG_PRUNE_DAYS = 30
LOG_PRUNE_DAYS = 30
MAX_RETRIES = 3
MAX_RETRIES = 3
RETRY_BACKOFF = 2
RETRY_BACKOFF = 2
@ -116,7 +118,6 @@ def get_reference_date():
except ( json . JSONDecodeError , KeyError , ValueError ) as e :
except ( json . JSONDecodeError , KeyError , ValueError ) as e :
logging . error ( f " Failed to load reference date from { REFERENCE_DATE_FILE } : { e } . Initializing new date. " )
logging . error ( f " Failed to load reference date from { REFERENCE_DATE_FILE } : { e } . Initializing new date. " )
# Initialize with current date (start of day)
reference_date = datetime . now ( timezone . utc ) . replace ( hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
reference_date = datetime . now ( timezone . utc ) . replace ( hour = 0 , minute = 0 , second = 0 , microsecond = 0 )
try :
try :
with open ( REFERENCE_DATE_FILE , ' w ' ) as f :
with open ( REFERENCE_DATE_FILE , ' w ' ) as f :
@ -127,26 +128,35 @@ def get_reference_date():
return reference_date
return reference_date
def generate_engagement_tweet ( author ) :
def generate_engagement_tweet ( author ) :
""" Generate an engagement tweet using author background themes. """
""" Generate an engagement tweet using author background themes and persona. """
credentials = next ( ( cred for cred in X_API_CREDENTIALS if cred [ " username " ] == author [ " username " ] ) , None )
username = author [ " username " ]
credentials = X_API_CREDENTIALS . get ( username )
if not credentials :
if not credentials :
logging . error ( f " No X credentials found for { author [ ' username' ] } " )
logging . error ( f " No X credentials found for { username } " )
return None
return None
author_handle = credentials [ " x_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 " ] == author [ " username " ] ) , { } )
background = next ( ( bg for bg in AUTHOR_BACKGROUNDS if bg [ " username " ] == username ) , { } )
if not background or " engagement_themes " not in background :
if not background or " engagement_themes " not in background :
logging . warning ( f " No background or engagement themes found for { author [ ' username ' ] } " )
logging . warning ( f " No background or engagement themes found for { username } , using default theme " )
theme = " food trends "
theme = " food trends "
else :
else :
theme = random . choice ( background [ " engagement_themes " ] )
theme = random . choice ( background [ " engagement_themes " ] )
base_prompt = persona_config [ " x_prompt " ] . format (
description = persona_config [ " description " ] ,
tone = persona_config [ " tone " ]
)
prompt = (
prompt = (
f " Generate a concise tweet (under 280 characters) for { author_handle } . "
f " { base_prompt } \n \n "
f " Create an engaging question or statement about { theme } to spark interaction. "
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 " 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 " 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 " 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). "
f " Do not include emojis, hashtags, or reward-driven incentives (e.g., giveaways). "
f " Return only the tweet text. "
)
)
for attempt in range ( MAX_RETRIES ) :
for attempt in range ( MAX_RETRIES ) :
@ -163,23 +173,20 @@ def generate_engagement_tweet(author):
tweet = response . choices [ 0 ] . message . content . strip ( )
tweet = response . choices [ 0 ] . message . content . strip ( )
if len ( tweet ) > 280 :
if len ( tweet ) > 280 :
tweet = tweet [ : 277 ] + " ... "
tweet = tweet [ : 277 ] + " ... "
logging . debug ( f " Generated engagement tweet: { tweet } " )
logging . debug ( f " Generated engagement tweet for { username } : { tweet } " )
return tweet
return tweet
except Exception as e :
except Exception as e :
logging . warning ( f " Failed to generate engagement tweet for { author [ ' username' ] } (attempt { attempt + 1 } ): { e } " )
logging . warning ( f " Failed to generate engagement tweet for { username } (attempt { attempt + 1 } ): { e } " )
if attempt < MAX_RETRIES - 1 :
if attempt < MAX_RETRIES - 1 :
time . sleep ( RETRY_BACKOFF * ( 2 * * attempt ) )
time . sleep ( RETRY_BACKOFF * ( 2 * * attempt ) )
else :
else :
logging . error ( f " Failed to generate engagement tweet after { MAX_RETRIES } attempts " )
logging . error ( f " Failed to generate engagement tweet after { MAX_RETRIES } attempts " )
engagement_templates = [
fallback = (
f " What ' s the most mouthwatering { theme } you ' ve seen this week? Share below and follow { author_handle } for more on InsiderFoodie.com! Link: https://insiderfoodie.com " ,
f " What ' s the hottest { theme } you ' re into? Share and follow { author_handle } for more on InsiderFoodie.com! "
f " { theme . capitalize ( ) } lovers unite! What ' s your go-to pick? Tell us and like this tweet for more from { author_handle } on InsiderFoodie.com! Link: https://insiderfoodie.com " ,
f " Link: https://insiderfoodie.com "
f " Ever tried a { theme } that blew your mind? Share your favorites and follow { author_handle } for more on InsiderFoodie.com! Link: https://insiderfoodie.com " ,
)
f " What { theme } trend are you loving right now? Let us know and like this tweet to keep up with { author_handle } on InsiderFoodie.com! Link: https://insiderfoodie.com "
logging . info ( f " Using fallback engagement tweet: { fallback } " )
]
return fallback
template = random . choice ( engagement_templates )
logging . info ( f " Using fallback engagement tweet: { template } " )
return template
def post_engagement_tweet ( ) :
def post_engagement_tweet ( ) :
""" Post engagement tweets for authors every 2 days. """
""" Post engagement tweets for authors every 2 days. """
@ -187,52 +194,57 @@ def post_engagement_tweet():
logging . info ( " Starting foodie_engagement_tweet.py " )
logging . info ( " Starting foodie_engagement_tweet.py " )
print ( " Starting foodie_engagement_tweet.py " )
print ( " Starting foodie_engagement_tweet.py " )
# Get reference date
reference_date = get_reference_date ( )
reference_date = get_reference_date ( )
current_date = datetime . now ( timezone . utc )
current_date = datetime . now ( timezone . utc )
days_since_reference = ( current_date - reference_date ) . days
days_since_reference = ( current_date - reference_date ) . days
logging . info ( f " Days since reference date ( { reference_date . date ( ) } ): { days_since_reference } " )
logging . info ( f " Days since reference date ( { reference_date . date ( ) } ): { days_since_reference } " )
print ( f " Days since reference date ( { reference_date . date ( ) } ): { days_since_reference } " )
print ( f " Days since reference date ( { reference_date . date ( ) } ): { days_since_reference } " )
# Post only if the number of days since the reference date is divisible by 2
if days_since_reference % 2 == 0 :
if days_since_reference % 2 == 0 :
logging . info ( " Today is an engagement tweet day (every 2 days). Posting... " )
logging . info ( " Today is an engagement tweet day (every 2 days). Posting... " )
print ( " Today is an engagement tweet day (every 2 days). Posting... " )
print ( " Today is an engagement tweet day (every 2 days). Posting... " )
# Load post counts to check limits
post_counts = load_post_counts ( )
post_counts = load_post_counts ( )
for author in AUTHORS :
for author in AUTHORS :
username = author [ " username " ]
try :
try :
# Check post limits
author_count = next ( ( entry for entry in post_counts if entry [ " username " ] == username ) , None )
author_count = next ( ( entry for entry in post_counts if entry [ " username " ] == author [ " username " ] ) , None )
if not author_count :
if not author_count :
logging . error ( f " No post count entry for { author [ ' username ' ] } , skipping " )
logging . warning ( f " No post count entry for { username } , initializing new entry " )
continue
author_count = {
" username " : username ,
" month " : datetime . now ( timezone . utc ) . strftime ( " % Y- % m " ) ,
" monthly_count " : 0 ,
" day " : datetime . now ( timezone . utc ) . strftime ( " % Y- % m- %d " ) ,
" daily_count " : 0
}
post_counts . append ( author_count )
save_post_counts ( post_counts )
if author_count [ " monthly_count " ] > = 500 :
if author_count [ " monthly_count " ] > = 500 :
logging . warning ( f " Monthly post limit (500) reached for { author [ ' username ' ] } , skipping " )
logging . warning ( f " Monthly post limit (500) reached for { username } , skipping " )
continue
continue
if author_count [ " daily_count " ] > = 20 :
if author_count [ " daily_count " ] > = 20 :
logging . warning ( f " Daily post limit (20) reached for { author [ ' username ' ] } , skipping " )
logging . warning ( f " Daily post limit (20) reached for { username } , skipping " )
continue
continue
tweet = generate_engagement_tweet ( author )
tweet = generate_engagement_tweet ( author )
if not tweet :
if not tweet :
logging . error ( f " Failed to generate engagement tweet for { author [ ' username' ] } , skipping " )
logging . error ( f " Failed to generate engagement tweet for { username } , skipping " )
continue
continue
logging . info ( f " Posting engagement tweet for { author [ ' username' ] } : { tweet } " )
logging . info ( f " Posting engagement tweet for { username } : { tweet } " )
print ( f " Posting engagement tweet for { author [ ' username' ] } : { tweet } " )
print ( f " Posting engagement tweet for { username } : { tweet } " )
if post_tweet ( author , tweet ) :
if post_tweet ( author , tweet ) :
logging . info ( f " Successfully posted engagement tweet for { author [ ' username ' ] } " )
logging . info ( f " Successfully posted engagement tweet for { username } " )
# Update post counts
author_count [ " monthly_count " ] + = 1
author_count [ " monthly_count " ] + = 1
author_count [ " daily_count " ] + = 1
author_count [ " daily_count " ] + = 1
save_post_counts ( post_counts )
save_post_counts ( post_counts )
else :
else :
logging . warning ( f " Failed to post engagement tweet for { author [ ' username' ] } " )
logging . warning ( f " Failed to post engagement tweet for { username } " )
except Exception as e :
except Exception as e :
logging . error ( f " Error posting engagement tweet for { author [ ' username' ] } : { e } " , exc_info = True )
logging . error ( f " Error posting engagement tweet for { username } : { e } " , exc_info = True )
continue
continue
else :
else :
logging . info ( f " Today is not an engagement tweet day (every 2 days). Days since reference: { days_since_reference } . Skipping... " )
logging . info ( f " Today is not an engagement tweet day (every 2 days). Days since reference: { days_since_reference } . Skipping... " )