ultima_seo / server /threads.py
rsm-roguchi
update new files
6b7bfdd
from shiny import reactive, render, ui
import os
import requests
from dotenv import load_dotenv
from bs4 import BeautifulSoup
import time
from llm_connect import get_response
# Load env
load_dotenv()
# ===== Threads config =====
THREADS_TOKEN = os.getenv("THREADS_ACCESS_TOKEN") # <-- set this in your env
# Store latest generated post text
generated_threads_post = reactive.Value("") # keeping the same name so your UI doesn't change
# ---- Helpers ----
def scrape_shopify_blog(url: str) -> str:
try:
resp = requests.get(url, timeout=10)
resp.raise_for_status()
soup = BeautifulSoup(resp.content, 'html.parser')
# Adjust this selector to match your Shopify theme if needed
section = soup.find('div', class_='w940 align-center size-content')
return section.get_text(strip=True, separator=' ') if section else ""
except Exception as e:
return f"❌ Error scraping blog: {e}"
def generate_threads_post( # keeping the function name to avoid UI refactor
topic: str = "",
url: str = "",
min_len: int = 100,
max_len: int = 450
) -> str:
"""
Generates post text using your existing LLM helper.
(Text is suitable for Threads now; you can tweak tone if you want.)
"""
if url:
scraped = scrape_shopify_blog(url)
if scraped.startswith("❌") or not scraped.strip():
return f"⚠️ Failed to extract blog content from URL: {url}"
prompt = (
"You are a social media manager for a hobby e-commerce company called 'Ultima Supply'.\n"
f"Write a detailed, engaging Threads post (min {min_len} chars, max {max_len} chars) summarizing the Shopify blog:\n\n"
f"{scraped}\n\n"
f"The post MUST include this exact URL: {url}\n"
"Use a casual, friendly tone with emojis.\n"
"VERY IMPORTANT: Include exactly 5 to 10 SEO-relevant hashtags grouped at the end (not inline).\n"
f"Keep everything under {max_len} characters total."
)
elif topic:
prompt = (
"You are a social media manager for a hobby e-commerce company called 'Ultima Supply'.\n"
f"Write a short, engaging Threads post (min {min_len} chars, max {max_len} chars) about: '{topic}'.\n"
"Use a casual, friendly tone with fun emojis.\n"
"VERY IMPORTANT: Include exactly 5 to 10 SEO-relevant hashtags grouped at the end (not inline).\n"
f"The entire post must be under {max_len} characters total."
)
else:
return "⚠️ Provide either a topic or a Shopify blog URL."
# Step 1: Generate
post = get_response(
input=prompt,
template=lambda x: x.strip(),
llm="gemini",
md=False,
temperature=0.9,
max_tokens=250
).strip()
if len(post) <= max_len:
return post
time.sleep(5)
# Step 2: Shorten if needed
shorten_prompt = (
f"Shorten this post to {max_len} characters or fewer. Keep the Shopify link ({url}) and all original hashtags:\n\n{post}"
if url else
f"Shorten this post to {max_len} characters or fewer, keeping the tone, emojis, and all original hashtags:\n\n{post}"
)
shortened = get_response(
input=shorten_prompt,
template=lambda x: x.strip(),
llm="gemini",
md=False,
temperature=0.7,
max_tokens=200
).strip()
return shortened[:max_len]
# ---- Threads poster (replaces the proxy) ----
def post_to_threads(message: str) -> str:
"""
One-shot TEXT post to Threads using your long-lived token.
Uses: POST https://graph.threads.net/me/threads with text params
"""
if not THREADS_TOKEN:
return "❌ Missing THREADS_USER_ACCESS_TOKEN environment variable."
try:
r = requests.post(
"https://graph.threads.net/me/threads",
headers={"Authorization": f"Bearer {THREADS_TOKEN}"},
params={
"text": message,
"media_type": "TEXT",
"auto_publish_text": "true", # one-call publish for text
},
timeout=30,
)
# Prefer JSON, fall back to raw text for debugging
data = {}
try:
data = r.json()
except Exception:
data = {"raw": r.text}
if r.ok and isinstance(data, dict) and data.get("id"):
return f"βœ… Post successful! Threads Post ID: {data['id']}"
return f"❌ Threads error ({r.status_code}): {data}"
except requests.RequestException as e:
return f"❌ Network error posting to Threads: {e}"
except Exception as e:
return f"❌ Unexpected error: {e}"
# ---- Shiny server wiring (IDs unchanged) ----
def server(input, output, session):
post_status = reactive.Value("")
@output
@render.ui
@reactive.event(input.gen_btn_threads)
def threads_post_draft():
topic = input.threads_topic().strip()
url = input.threads_url().strip()
if not topic and not url:
return ui.HTML("<p><strong>⚠️ Enter a topic or a blog URL.</strong></p>")
post = generate_threads_post(topic=topic, url=url)
if post.startswith(("⚠️", "❌")):
return ui.HTML(f"<p><strong>{post}</strong></p>")
generated_threads_post.set(post)
return ui.HTML(f"<p><strong>Generated Threads Post:</strong><br>{post}</p>")
@output
@render.text
def threads_post_status():
return post_status()
@reactive.effect
@reactive.event(input.post_btn_threads)
def _():
post = generated_threads_post()
if not post:
post_status.set("⚠️ No post generated yet.")
else:
post_status.set(post_to_threads(post))