update posting to X authors
This commit is contained in:
+26
-17
@@ -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",
|
||||
|
||||
+41
-1
@@ -761,6 +761,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
|
||||
@@ -952,4 +959,37 @@ def prepare_post_data(final_summary, original_title, context_info=""):
|
||||
author = {"username": "owenjohnson", "password": "rfjk xhn6 2RPy FuQ9 cGlU K8mC"}
|
||||
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
|
||||
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
|
||||
Reference in New Issue
Block a user