update posting to X authors
This commit is contained in:
+26
-17
@@ -1,51 +1,60 @@
|
|||||||
# foodie_config.py
|
# foodie_config.py
|
||||||
# Constants shared across all automator scripts
|
# Constants shared across all automator scripts
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
|
||||||
OPENAI_API_KEY = "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"
|
load_dotenv()
|
||||||
PIXABAY_API_KEY = "14836528-999c19a033d77d463113b1fb8"
|
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||||||
|
PIXABAY_API_KEY = os.getenv("PIXABAY_API_KEY")
|
||||||
|
|
||||||
AUTHORS = [
|
AUTHORS = [
|
||||||
{
|
{
|
||||||
"url": "https://insiderfoodie.com",
|
"url": "https://insiderfoodie.com",
|
||||||
"username": "owenjohnson",
|
"username": "owenjohnson",
|
||||||
"password": "hESy E2qO grdB KiKS X6SW oM05",
|
"password": os.getenv("OWENJOHNSON_PASSWORD"),
|
||||||
"persona": "Visionary Editor",
|
"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",
|
"url": "https://insiderfoodie.com",
|
||||||
"username": "javiermorales",
|
"username": "javiermorales",
|
||||||
"password": "r46q z0JX QL1q ztbH Tifk Cn28",
|
"password": os.getenv("JAVIERMORALES_PASSWORD"),
|
||||||
"persona": "Foodie Critic",
|
"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",
|
"url": "https://insiderfoodie.com",
|
||||||
"username": "aishapatel",
|
"username": "aishapatel",
|
||||||
"password": "NyCa SOXd 5EVf bVvW KIoz wC0C",
|
"password": os.getenv("AISHAPATEL_PASSWORD"),
|
||||||
"persona": "Trend Scout",
|
"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",
|
"url": "https://insiderfoodie.com",
|
||||||
"username": "trangnguyen",
|
"username": "trangnguyen",
|
||||||
"password": "A53T Nn8i CCEI HMq8 a9Ps Uhyg",
|
"password": os.getenv("TRANGNGUYEN_PASSWORD"),
|
||||||
"persona": "Culture Connoisseur",
|
"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",
|
"url": "https://insiderfoodie.com",
|
||||||
"username": "keishareid",
|
"username": "keishareid",
|
||||||
"password": "BOGQ pjT8 rdTv JyOJ 3IjB Apww",
|
"password": os.getenv("KEISHAREID_PASSWORD"),
|
||||||
"persona": "African-American Soul Food Sage",
|
"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",
|
"url": "https://insiderfoodie.com",
|
||||||
"username": "lilamoreau",
|
"username": "lilamoreau",
|
||||||
"password": "e3nv Vsg4 L9wv RgL6 dHkm T3UD",
|
"password": os.getenv("LILAMOREAU_PASSWORD"),
|
||||||
"persona": "Global Street Food Nomad",
|
"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_ID = os.getenv("REDDIT_CLIENT_ID")
|
||||||
REDDIT_CLIENT_SECRET = "YGTx69ZzvMn329pZj2qiEEXW82aeSA"
|
REDDIT_CLIENT_SECRET = os.getenv("REDDIT_CLIENT_SECRET")
|
||||||
REDDIT_USER_AGENT = "foodie_trends_bot by /u/AskShaneHill"
|
REDDIT_USER_AGENT = os.getenv("REDDIT_USER_AGENT")
|
||||||
REDDIT_SUBREDDITS = [
|
REDDIT_SUBREDDITS = [
|
||||||
"food",
|
"food",
|
||||||
"FoodPorn",
|
"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_id = post_info["id"]
|
||||||
post_url = post_info["link"]
|
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})")
|
logging.info(f"Posted/Updated by {author['username']}: {post_data['title']} (ID: {post_id})")
|
||||||
return post_id, post_url
|
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)
|
category = generate_category_from_summary(final_summary)
|
||||||
|
|
||||||
return post_data, author, category, image_url, image_source, uploader, page_url
|
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()
|
||||||
+3
-1
@@ -1,10 +1,12 @@
|
|||||||
requests==2.32.3
|
requests==2.32.3
|
||||||
selenium==4.29.0
|
selenium==4.29.0
|
||||||
duckduckgo_search==7.5.4
|
duckduckgo_search==7.5.4
|
||||||
openai==1.75.0
|
openai==1.35.3
|
||||||
praw==7.8.1
|
praw==7.8.1
|
||||||
beautifulsoup4==4.13.3
|
beautifulsoup4==4.13.3
|
||||||
Pillow==11.1.0
|
Pillow==11.1.0
|
||||||
pytesseract==0.3.13
|
pytesseract==0.3.13
|
||||||
feedparser==6.0.11
|
feedparser==6.0.11
|
||||||
webdriver-manager==4.0.2
|
webdriver-manager==4.0.2
|
||||||
|
tweepy==4.14.0
|
||||||
|
python-dotenv==1.0.1
|
||||||
Reference in New Issue
Block a user