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("

⚠️ Enter a topic or a blog URL.

") post = generate_threads_post(topic=topic, url=url) if post.startswith(("⚠️", "❌")): return ui.HTML(f"

{post}

") generated_threads_post.set(post) return ui.HTML(f"

Generated Threads Post:
{post}

") @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))