update posting to X authors

my-fix-branch
Shane 7 months ago
parent 36829eaeb8
commit 94ef0294bf
  1. 43
      foodie_config.py
  2. 40
      foodie_utils.py
  3. 99
      foodie_x_config.py
  4. 223
      foodie_x_poster.py
  5. 55
      generate_backgrounds.py
  6. 4
      requirements.txt

@ -1,51 +1,60 @@
# foodie_config.py
# Constants shared across all automator scripts
from dotenv import load_dotenv
import os
OPENAI_API_KEY = "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"
PIXABAY_API_KEY = "14836528-999c19a033d77d463113b1fb8"
load_dotenv()
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
PIXABAY_API_KEY = os.getenv("PIXABAY_API_KEY")
AUTHORS = [
{
"url": "https://insiderfoodie.com",
"username": "owenjohnson",
"password": "hESy E2qO grdB KiKS X6SW oM05",
"password": os.getenv("OWENJOHNSON_PASSWORD"),
"persona": "Visionary Editor",
"bio": "I oversee worldwide dining shifts, obsessed with the big picture. My edits deliver precise takes—charting the future of food with confidence."
"bio": "I oversee worldwide dining shifts, obsessed with the big picture. My edits deliver precise takes—charting the future of food with confidence.",
"dob": "1990-04-26"
},
{
"url": "https://insiderfoodie.com",
"username": "javiermorales",
"password": "r46q z0JX QL1q ztbH Tifk Cn28",
"password": os.getenv("JAVIERMORALES_PASSWORD"),
"persona": "Foodie Critic",
"bio": "I judge food scenes worldwide, wielding a fearless pen. My takes expose what shines and what flops—no compromise, just truth."
"bio": "I judge food scenes worldwide, wielding a fearless pen. My takes expose what shines and what flops—no compromise, just truth.",
"dob": "1996-07-08"
},
{
"url": "https://insiderfoodie.com",
"username": "aishapatel",
"password": "NyCa SOXd 5EVf bVvW KIoz wC0C",
"password": os.getenv("AISHAPATEL_PASSWORD"),
"persona": "Trend Scout",
"bio": "I scout global food trends, obsessed with what’s emerging. My sharp predictions map the industry’s path—always one step ahead."
"bio": "I scout global food trends, obsessed with what’s emerging. My sharp predictions map the industry’s path—always one step ahead.",
"dob": "1999-03-15"
},
{
"url": "https://insiderfoodie.com",
"username": "trangnguyen",
"password": "A53T Nn8i CCEI HMq8 a9Ps Uhyg",
"password": os.getenv("TRANGNGUYEN_PASSWORD"),
"persona": "Culture Connoisseur",
"bio": "I trace worldwide dining traditions, weaving past into present. My words uncover the soul of flavor—connecting cultures bite by bite."
"bio": "I trace worldwide dining traditions, weaving past into present. My words uncover the soul of flavor—connecting cultures bite by bite.",
"dob": "2002-08-22"
},
{
"url": "https://insiderfoodie.com",
"username": "keishareid",
"password": "BOGQ pjT8 rdTv JyOJ 3IjB Apww",
"password": os.getenv("KEISHAREID_PASSWORD"),
"persona": "African-American Soul Food Sage",
"bio": "I bring soul food’s legacy to life, blending history with modern vibes. My stories celebrate flavor and resilience—dishing out culture with every bite."
"bio": "I bring soul food’s legacy to life, blending history with modern vibes. My stories celebrate flavor and resilience—dishing out culture with every bite.",
"dob": "1994-06-10"
},
{
"url": "https://insiderfoodie.com",
"username": "lilamoreau",
"password": "e3nv Vsg4 L9wv RgL6 dHkm T3UD",
"password": os.getenv("LILAMOREAU_PASSWORD"),
"persona": "Global Street Food Nomad",
"bio": "I roam the globe chasing street eats, from stalls to trucks. My tales uncover bold flavors and gritty trends shaping food on the go."
"bio": "I roam the globe chasing street eats, from stalls to trucks. My tales uncover bold flavors and gritty trends shaping food on the go.",
"dob": "1993-02-14"
}
]
@ -133,9 +142,9 @@ SUMMARY_PERSONA_PROMPTS = {
)
}
REDDIT_CLIENT_ID = "GtoZmrM8VyrxMvb7gBLrLg"
REDDIT_CLIENT_SECRET = "YGTx69ZzvMn329pZj2qiEEXW82aeSA"
REDDIT_USER_AGENT = "foodie_trends_bot by /u/AskShaneHill"
REDDIT_CLIENT_ID = os.getenv("REDDIT_CLIENT_ID")
REDDIT_CLIENT_SECRET = os.getenv("REDDIT_CLIENT_SECRET")
REDDIT_USER_AGENT = os.getenv("REDDIT_USER_AGENT")
REDDIT_SUBREDDITS = [
"food",
"FoodPorn",

@ -762,6 +762,13 @@ def post_to_wp(post_data, category, link, author, image_url, original_source, im
post_id = post_info["id"]
post_url = post_info["link"]
# Save to recent_posts.json
timestamp = datetime.now(timezone.utc).isoformat()
save_post_to_recent(post_data["title"], post_url, author["username"], timestamp)
logging.info(f"Posted/Updated by {author['username']}: {post_data['title']} (ID: {post_id})")
return post_id, post_url
logging.info(f"Posted/Updated by {author['username']}: {post_data['title']} (ID: {post_id})")
return post_id, post_url
@ -953,3 +960,36 @@ def prepare_post_data(final_summary, original_title, context_info=""):
category = generate_category_from_summary(final_summary)
return post_data, author, category, image_url, image_source, uploader, page_url
def save_post_to_recent(post_title, post_url, author_username, timestamp):
"""Save post details to recent_posts.json."""
try:
recent_posts = load_json_file('/home/shane/foodie_automator/recent_posts.json')
entry = {
"title": post_title,
"url": post_url,
"author_username": author_username,
"timestamp": timestamp
}
recent_posts.append(entry)
with open('/home/shane/foodie_automator/recent_posts.json', 'w') as f:
for item in recent_posts:
json.dump(item, f)
f.write('\n')
logging.info(f"Saved post '{post_title}' to recent_posts.json")
except Exception as e:
logging.error(f"Failed to save post to recent_posts.json: {e}")
def prune_recent_posts():
"""Prune recent_posts.json to keep only entries from the last 24 hours."""
try:
cutoff = (datetime.now(timezone.utc) - timedelta(hours=24)).isoformat()
recent_posts = load_json_file('/home/shane/foodie_automator/recent_posts.json')
recent_posts = [entry for entry in recent_posts if entry["timestamp"] > cutoff]
with open('/home/shane/foodie_automator/recent_posts.json', 'w') as f:
for item in recent_posts:
json.dump(item, f)
f.write('\n')
logging.info(f"Pruned recent_posts.json to {len(recent_posts)} entries")
except Exception as e:
logging.error(f"Failed to prune recent_posts.json: {e}")

@ -0,0 +1,99 @@
# foodie_x_config.py
from dotenv import load_dotenv
import os
load_dotenv()
X_API_CREDENTIALS = [
{
"username": "owenjohnson",
"x_username": "@insiderfoodieowen",
"api_key": os.getenv("OWENJOHNSON_X_API_KEY"),
"api_secret": os.getenv("OWENJOHNSON_X_API_SECRET"),
"access_token": os.getenv("OWENJOHNSON_X_ACCESS_TOKEN"),
"access_token_secret": os.getenv("OWENJOHNSON_X_ACCESS_TOKEN_SECRET"),
"client_secret": os.getenv("OWENJOHNSON_X_CLIENT_SECRET")
},
{
"username": "javiermorales",
"x_username": "@insiderfoodiejavier",
"api_key": os.getenv("JAVIERMORALES_X_API_KEY"),
"api_secret": os.getenv("JAVIERMORALES_X_API_SECRET"),
"access_token": os.getenv("JAVIERMORALES_X_ACCESS_TOKEN"),
"access_token_secret": os.getenv("JAVIERMORALES_X_ACCESS_TOKEN_SECRET"),
"client_secret": os.getenv("JAVIERMORALES_X_CLIENT_SECRET")
},
{
"username": "aishapatel",
"x_username": "@insiderfoodieaisha",
"api_key": os.getenv("AISHAPATEL_X_API_KEY"),
"api_secret": os.getenv("AISHAPATEL_X_API_SECRET"),
"access_token": os.getenv("AISHAPATEL_X_ACCESS_TOKEN"),
"access_token_secret": os.getenv("AISHAPATEL_X_ACCESS_TOKEN_SECRET"),
"client_secret": os.getenv("AISHAPATEL_X_CLIENT_SECRET")
},
{
"username": "trangnguyen",
"x_username": "@insiderfoodietrang",
"api_key": os.getenv("TRANGNGUYEN_X_API_KEY"),
"api_secret": os.getenv("TRANGNGUYEN_X_API_SECRET"),
"access_token": os.getenv("TRANGNGUYEN_X_ACCESS_TOKEN"),
"access_token_secret": os.getenv("TRANGNGUYEN_X_ACCESS_TOKEN_SECRET"),
"client_secret": os.getenv("TRANGNGUYEN_X_CLIENT_SECRET")
},
{
"username": "keishareid",
"x_username": "@insiderfoodiekeisha",
"api_key": os.getenv("KEISHAREID_X_API_KEY"),
"api_secret": os.getenv("KEISHAREID_X_API_SECRET"),
"access_token": os.getenv("KEISHAREID_X_ACCESS_TOKEN"),
"access_token_secret": os.getenv("KEISHAREID_X_ACCESS_TOKEN_SECRET"),
"client_secret": os.getenv("KEISHAREID_X_CLIENT_SECRET")
},
{
"username": "lilamoreau",
"x_username": "@insiderfoodielila",
"api_key": os.getenv("LILAMOREAU_X_API_KEY"),
"api_secret": os.getenv("LILAMOREAU_X_API_SECRET"),
"access_token": os.getenv("LILAMOREAU_X_ACCESS_TOKEN"),
"access_token_secret": os.getenv("LILAMOREAU_X_ACCESS_TOKEN_SECRET"),
"client_secret": os.getenv("LILAMOREAU_X_CLIENT_SECRET")
}
]
X_PERSONA_PROMPTS = {
"Visionary Editor": (
"Craft a tweet as a commanding food editor with a bold, global view. Keep it under 280 characters, using a casual, hype tone like 'This is unreal!'. "
"For article tweets, include the article title, a quirky hook, and the URL. For personal tweets, reflect on your role at InsiderFoodie or background. "
"Avoid emojis and clichés like 'game-changer'. Return only the tweet text."
),
"Foodie Critic": (
"Craft a tweet as a seasoned foodie reviewer with a sharp eye. Keep it under 280 characters, using a pro yet lively tone like 'This bangs!'. "
"For article tweets, include the article title, a quirky hook, and the URL. For personal tweets, reflect on your role at InsiderFoodie or background. "
"Avoid emojis and clichés like 'game-changer'. Return only the tweet text."
),
"Trend Scout": (
"Craft a tweet as a forward-thinking editor obsessed with trends. Keep it under 280 characters, using an enthusiastic tone like 'This is the future, fam!'. "
"For article tweets, include the article title, a quirky hook, and the URL. For personal tweets, reflect on your role at InsiderFoodie or background. "
"Avoid emojis and clichés like 'game-changer'. Return only the tweet text."
),
"Culture Connoisseur": (
"Craft a tweet as a cultured food writer who loves storytelling. Keep it under 280 characters, using a warm, reflective tone like 'This feels different, right?'. "
"For article tweets, include the article title, a quirky hook, and the URL. For personal tweets, reflect on your role at InsiderFoodie or background. "
"Avoid emojis and clichés like 'game-changer'. Return only the tweet text."
),
"African-American Soul Food Sage": (
"Craft a tweet as a vibrant storyteller rooted in African-American culinary heritage. Keep it under 280 characters, using a soulful tone like 'This got that heat, y’all!'. "
"For article tweets, include the article title, a quirky hook, and the URL. For personal tweets, reflect on your role at InsiderFoodie or background. "
"Avoid emojis and clichés like 'game-changer'. Return only the tweet text."
),
"Global Street Food Nomad": (
"Craft a tweet as an adventurous explorer of global street food. Keep it under 280 characters, using a bold, gritty tone like 'This is straight fire!'. "
"For article tweets, include the article title, a quirky hook, and the URL. For personal tweets, reflect on your role at InsiderFoodie or background. "
"Avoid emojis and clichés like 'game-changer'. Return only the tweet text."
)
}
AUTHOR_BACKGROUNDS_FILE = '/home/shane/foodie_automator/author_backgrounds.json'
X_POST_COUNTS_FILE = '/home/shane/foodie_automator/x_post_counts.json'
RECENT_POSTS_FILE = '/home/shane/foodie_automator/recent_posts.json'

@ -0,0 +1,223 @@
import json
import logging
import random
import time
import sys
import signal
import os
from datetime import datetime, timedelta, timezone
from openai import OpenAI
import tweepy
from foodie_config import OPENAI_API_KEY, AUTHORS, LIGHT_TASK_MODEL
from foodie_utils import load_json_file
from foodie_x_config import X_API_CREDENTIALS, X_PERSONA_PROMPTS, AUTHOR_BACKGROUNDS_FILE, X_POST_COUNTS_FILE, RECENT_POSTS_FILE
from dotenv import load_dotenv
load_dotenv()
LOG_FILE = "/home/shane/foodie_automator/foodie_x_poster.log"
LOG_PRUNE_DAYS = 30
def setup_logging():
if os.path.exists(LOG_FILE):
with open(LOG_FILE, 'r') as f:
lines = f.readlines()
cutoff = datetime.now(timezone.utc) - timedelta(days=LOG_PRUNE_DAYS)
pruned_lines = [line for line in lines if datetime.strptime(line[:19], '%Y-%m-%d %H:%M:%S').replace(tzinfo=timezone.utc) > cutoff]
with open(LOG_FILE, 'w') as f:
f.writelines(pruned_lines)
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.info("Logging initialized for foodie_x_poster.py")
setup_logging()
client = OpenAI(api_key=OPENAI_API_KEY)
try:
with open(AUTHOR_BACKGROUNDS_FILE, 'r') as f:
AUTHOR_BACKGROUNDS = json.load(f)
except Exception as e:
logging.error(f"Failed to load author_backgrounds.json: {e}")
sys.exit(1)
def load_post_counts():
counts = load_json_file(X_POST_COUNTS_FILE)
if not counts:
counts = [{"username": author["username"], "count": 0, "month": datetime.now(timezone.utc).strftime("%Y-%m")} for author in AUTHORS]
current_month = datetime.now(timezone.utc).strftime("%Y-%m")
for entry in counts:
if entry["month"] != current_month:
entry["count"] = 0
entry["month"] = current_month
return counts
def save_post_counts(counts):
with open(X_POST_COUNTS_FILE, 'w') as f:
for item in counts:
json.dump(item, f)
f.write('\n')
logging.info(f"Saved post counts to {X_POST_COUNTS_FILE}")
is_posting = False
def signal_handler(sig, frame):
logging.info("Received termination signal, checking if safe to exit...")
if is_posting:
logging.info("Currently posting, will exit after completion.")
else:
logging.info("Safe to exit immediately.")
sys.exit(0)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
def get_recent_posts_for_author(username):
posts = load_json_file(RECENT_POSTS_FILE)
return [post for post in posts if post["author_username"] == username]
def delete_used_post(post_title):
posts = load_json_file(RECENT_POSTS_FILE)
posts = [post for post in posts if post["title"] != post_title]
with open(RECENT_POSTS_FILE, 'w') as f:
for item in posts:
json.dump(item, f)
f.write('\n')
logging.info(f"Deleted post '{post_title}' from recent_posts.json")
def generate_article_tweet(author, post, persona):
prompt = X_PERSONA_PROMPTS[persona].replace(
"For article tweets, include the article title, a quirky hook, and the URL.",
f"Generate an article tweet including the title '{post['title']}', a quirky hook, and the URL '{post['url']}'."
)
try:
response = client.chat.completions.create(
model=LIGHT_TASK_MODEL,
messages=[
{"role": "system", "content": prompt},
{"role": "user", "content": f"Generate tweet for {post['title']}."}
],
max_tokens=100,
temperature=0.9
)
tweet = response.choices[0].message.content.strip()
if len(tweet) > 280:
tweet = tweet[:277] + "..."
logging.info(f"Generated article tweet for {author['username']}: {tweet}")
return tweet
except Exception as e:
logging.error(f"Failed to generate article tweet for {author['username']}: {e}")
return f"This trend is fire! Check out {post['title']} at {post['url']} #Foodie"
def generate_personal_tweet(author, persona):
background = next((bg for bg in AUTHOR_BACKGROUNDS if bg["username"] == author["username"]), {})
if not background:
logging.warning(f"No background found for {author['username']}")
return f"Loving my gig at InsiderFoodie, dishing out food trends! #FoodieLife"
# Get DOB and calculate age
dob = author.get('dob', '1980-01-01')
current_year = datetime.now().year
birth_year = int(dob.split('-')[0])
age = current_year - birth_year
is_role_reflection = random.choice([True, False])
if is_role_reflection:
content = f"Reflect on your role at InsiderFoodie as {author['persona']}. Mention you're {age} years old."
else:
content = (
f"Share a personal story about your background, considering you were born on {dob} and are {age} years old. "
f"Hometown: {background['hometown']}, Cultural influences: {background['cultural_influences']}, "
f"Early memory: {background['early_memory']}, Career path: {background['career_path']}."
)
prompt = X_PERSONA_PROMPTS[persona].replace(
"For personal tweets, reflect on your role at InsiderFoodie or background.",
content
)
try:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": prompt},
{"role": "user", "content": f"Generate personal tweet for {author['username']}."}
],
max_tokens=100,
temperature=0.9
)
tweet = response.choices[0].message.content.strip()
if len(tweet) > 280:
tweet = tweet[:277] + "..."
logging.info(f"Generated personal tweet for {author['username']}: {tweet}")
return tweet
except Exception as e:
logging.error(f"Failed to generate personal tweet for {author['username']}: {e}")
return f"Loving my gig at InsiderFoodie, dishing out food trends! #FoodieLife"
def post_tweet(author, tweet):
global is_posting
credentials = next((cred for cred in X_API_CREDENTIALS if cred["username"] == author["username"]), None)
if not credentials:
logging.error(f"No X credentials found for {author['username']}")
return False
post_counts = load_post_counts()
author_count = next((entry for entry in post_counts if entry["username"] == author["username"]), None)
if author_count["count"] >= 450:
logging.warning(f"Post limit reached for {author['username']} this month")
return False
try:
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"]
)
is_posting = True
response = client.create_tweet(text=tweet)
is_posting = False
author_count["count"] += 1
save_post_counts(post_counts)
logging.info(f"Posted tweet for {author['username']}: {tweet}")
return True
except Exception as e:
is_posting = False
logging.error(f"Failed to post tweet for {author['username']}: {e}")
return False
def main():
logging.info("***** X Poster Launched *****")
for author in AUTHORS:
posts = get_recent_posts_for_author(author["username"])
if not posts:
logging.info(f"No recent posts for {author['username']}, skipping")
continue
article_tweets = 0
for post in posts[:2]:
tweet = generate_article_tweet(author, post, author["persona"])
if post_tweet(author, tweet):
delete_used_post(post["title"])
article_tweets += 1
time.sleep(random.uniform(3600, 7200))
if article_tweets >= 2:
break
tweet = generate_personal_tweet(author, author["persona"])
post_tweet(author, tweet)
logging.info("X posting completed")
return random.randint(600, 1800)
if __name__ == "__main__":
sleep_time = main()
logging.info(f"Sleeping for {sleep_time} seconds")
time.sleep(sleep_time)

@ -0,0 +1,55 @@
import json
import logging
from openai import OpenAI
from foodie_config import OPENAI_API_KEY, AUTHORS, LIGHT_TASK_MODEL
from datetime import datetime, timezone
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
client = OpenAI(api_key=OPENAI_API_KEY)
def generate_background(author):
# Include the DOB in the prompt to contextualize the timeline
dob = author.get('dob', '1980-01-01') # Fallback DOB if not provided
current_year = datetime.now().year
birth_year = int(dob.split('-')[0])
age = current_year - birth_year # Calculate approximate age
prompt = (
f"Generate a fictional background for a food writer named {author['username']} with the persona '{author['persona']}'. "
f"They were born on {dob} and are currently {age} years old. Use this to create a realistic timeline for their life events. "
"Include: hometown, cultural influences, an early food memory (from their childhood, appropriate for their birth year), "
"and how they became a food writer (considering their age and career timeline). "
f"Match the tone of their bio: '{author['bio']}'. Keep it concise (100-150 words). "
"Return as JSON: {'username': '...', 'hometown': '...', 'cultural_influences': '...', 'early_memory': '...', 'career_path': '...'}"
)
try:
response = client.chat.completions.create(
model=LIGHT_TASK_MODEL,
messages=[
{"role": "system", "content": prompt},
{"role": "user", "content": f"Generate background for {author['username']}."}
],
max_tokens=200
)
raw_result = response.choices[0].message.content.strip()
cleaned_result = raw_result.replace('```json', '').replace('```', '').strip()
return json.loads(cleaned_result)
except Exception as e:
logging.error(f"Failed to generate background for {author['username']}: {e}")
return None
def main():
backgrounds = []
for author in AUTHORS:
background = generate_background(author)
if background:
backgrounds.append(background)
logging.info(f"Generated background for {author['username']}")
with open('/home/shane/foodie_automator/author_backgrounds.json', 'w') as f:
json.dump(backgrounds, f, indent=2)
logging.info("Saved backgrounds to author_backgrounds.json")
if __name__ == "__main__":
main()

@ -1,10 +1,12 @@
requests==2.32.3
selenium==4.29.0
duckduckgo_search==7.5.4
openai==1.75.0
openai==1.35.3
praw==7.8.1
beautifulsoup4==4.13.3
Pillow==11.1.0
pytesseract==0.3.13
feedparser==6.0.11
webdriver-manager==4.0.2
tweepy==4.14.0
python-dotenv==1.0.1
Loading…
Cancel
Save