rsm-roguchi commited on
Commit
f9e7cf4
·
1 Parent(s): 052fa72

threads up

Browse files
Files changed (6) hide show
  1. app.py +4 -0
  2. server/meta.py +16 -6
  3. server/threads.py +167 -0
  4. server/twitter.py +1 -1
  5. threads.ipynb +0 -0
  6. ui/threads.py +27 -0
app.py CHANGED
@@ -7,6 +7,7 @@ from ui import (
7
  blog,
8
  #general_blog,
9
  meta,
 
10
  twitter,
11
  price_matching,
12
  inventory,
@@ -17,6 +18,7 @@ from server import (
17
  blog as blog_srv,
18
  #general_blog as general_blog_srv,
19
  meta as meta_srv,
 
20
  twitter as twitter_srv,
21
  price_matching as price_matching_srv,
22
  inventory as inventory_srv,
@@ -29,6 +31,7 @@ ui = ui.page_fluid(
29
  blog.ui,
30
  #general_blog.ui,
31
  meta.ui,
 
32
  twitter.ui,
33
  price_matching.ui,
34
  inventory.ui,
@@ -44,6 +47,7 @@ def server(input, output, session):
44
  blog_srv.server(input, output, session)
45
  #general_blog_srv.server(input, output, session)
46
  meta_srv.server(input, output, session)
 
47
  twitter_srv.server(input, output, session)
48
  price_matching_srv.server(input, output, session)
49
  inventory_srv.server(input, output, session)
 
7
  blog,
8
  #general_blog,
9
  meta,
10
+ threads,
11
  twitter,
12
  price_matching,
13
  inventory,
 
18
  blog as blog_srv,
19
  #general_blog as general_blog_srv,
20
  meta as meta_srv,
21
+ threads as threads_srv,
22
  twitter as twitter_srv,
23
  price_matching as price_matching_srv,
24
  inventory as inventory_srv,
 
31
  blog.ui,
32
  #general_blog.ui,
33
  meta.ui,
34
+ threads.ui,
35
  twitter.ui,
36
  price_matching.ui,
37
  inventory.ui,
 
47
  blog_srv.server(input, output, session)
48
  #general_blog_srv.server(input, output, session)
49
  meta_srv.server(input, output, session)
50
+ threads_srv.server(input, output, session)
51
  twitter_srv.server(input, output, session)
52
  price_matching_srv.server(input, output, session)
53
  inventory_srv.server(input, output, session)
server/meta.py CHANGED
@@ -19,7 +19,7 @@ def scrape_shopify_blog(url: str) -> str:
19
  soup = BeautifulSoup(resp.content, 'html.parser')
20
  section = soup.find(
21
  'div',
22
- class_='article-template__content page-width page-width--narrow rte scroll-trigger animate--slide-in'
23
  )
24
  return section.get_text(strip=True, separator=' ') if section else ""
25
  except Exception as e:
@@ -95,12 +95,22 @@ def post_to_facebook(message: str) -> str:
95
  res.raise_for_status()
96
  data = res.json()
97
 
98
- if data["status"] == "success":
99
- return f"✅ Post successful! Facebook Post ID: {data['post_id']}"
100
- else:
101
- return f" Proxy error: {data.get('error', 'Unknown error')}"
 
 
 
 
 
 
 
 
 
 
102
  except Exception as e:
103
- return f"❌ Failed to reach proxy: {e}"
104
 
105
 
106
 
 
19
  soup = BeautifulSoup(resp.content, 'html.parser')
20
  section = soup.find(
21
  'div',
22
+ class_='w940 align-center size-content'
23
  )
24
  return section.get_text(strip=True, separator=' ') if section else ""
25
  except Exception as e:
 
95
  res.raise_for_status()
96
  data = res.json()
97
 
98
+ # Safely handle both proxy success and raw Facebook responses
99
+ if isinstance(data, dict):
100
+ if "status" in data and data["status"] == "success":
101
+ return f" Post successful! Facebook Post ID: {data.get('post_id', 'unknown')}"
102
+ elif "id" in data or "post_id" in data:
103
+ post_id = data.get("post_id") or data.get("id")
104
+ return f"✅ Post successful! Facebook Post ID: {post_id}"
105
+ elif "error" in data:
106
+ return f"❌ Facebook error: {data['error']}"
107
+
108
+ return f"⚠️ Unexpected response: {data}"
109
+
110
+ except requests.RequestException as e:
111
+ return f"❌ Network or proxy error: {e}"
112
  except Exception as e:
113
+ return f"❌ Unexpected error: {e}"
114
 
115
 
116
 
server/threads.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from shiny import reactive, render, ui
2
+ import os
3
+ import requests
4
+ from dotenv import load_dotenv
5
+ from bs4 import BeautifulSoup
6
+ from llm_connect import get_response
7
+
8
+ # Load env
9
+ load_dotenv()
10
+
11
+ # ===== Threads config =====
12
+ THREADS_TOKEN = os.getenv("THREADS_ACCESS_TOKEN") # <-- set this in your env
13
+
14
+ # Store latest generated post text
15
+ generated_threads_post = reactive.Value("") # keeping the same name so your UI doesn't change
16
+
17
+
18
+ # ---- Helpers ----
19
+ def scrape_shopify_blog(url: str) -> str:
20
+ try:
21
+ resp = requests.get(url, timeout=10)
22
+ resp.raise_for_status()
23
+ soup = BeautifulSoup(resp.content, 'html.parser')
24
+ # Adjust this selector to match your Shopify theme if needed
25
+ section = soup.find('div', class_='w940 align-center size-content')
26
+ return section.get_text(strip=True, separator=' ') if section else ""
27
+ except Exception as e:
28
+ return f"❌ Error scraping blog: {e}"
29
+
30
+
31
+ def generate_threads_post( # keeping the function name to avoid UI refactor
32
+ topic: str = "",
33
+ url: str = "",
34
+ min_len: int = 100,
35
+ max_len: int = 450
36
+ ) -> str:
37
+ """
38
+ Generates post text using your existing LLM helper.
39
+ (Text is suitable for Threads now; you can tweak tone if you want.)
40
+ """
41
+ if url:
42
+ scraped = scrape_shopify_blog(url)
43
+ if scraped.startswith("❌") or not scraped.strip():
44
+ return f"⚠️ Failed to extract blog content from URL: {url}"
45
+ prompt = (
46
+ "You are a social media manager for a hobby e-commerce company called 'Ultima Supply'.\n"
47
+ f"Write a detailed, engaging Threads post (min {min_len} chars, max {max_len} chars) summarizing the Shopify blog:\n\n"
48
+ f"{scraped}\n\n"
49
+ f"The post MUST include this exact URL: {url}\n"
50
+ "Use a casual, friendly tone with emojis.\n"
51
+ "VERY IMPORTANT: Include exactly 5 to 10 SEO-relevant hashtags grouped at the end (not inline).\n"
52
+ f"Keep everything under {max_len} characters total."
53
+ )
54
+ elif topic:
55
+ prompt = (
56
+ "You are a social media manager for a hobby e-commerce company called 'Ultima Supply'.\n"
57
+ f"Write a short, engaging Threads post (min {min_len} chars, max {max_len} chars) about: '{topic}'.\n"
58
+ "Use a casual, friendly tone with fun emojis.\n"
59
+ "VERY IMPORTANT: Include exactly 5 to 10 SEO-relevant hashtags grouped at the end (not inline).\n"
60
+ f"The entire post must be under {max_len} characters total."
61
+ )
62
+ else:
63
+ return "⚠️ Provide either a topic or a Shopify blog URL."
64
+
65
+ # Step 1: Generate
66
+ post = get_response(
67
+ input=prompt,
68
+ template=lambda x: x.strip(),
69
+ llm="gemini",
70
+ md=False,
71
+ temperature=0.9,
72
+ max_tokens=250
73
+ ).strip()
74
+
75
+ if len(post) <= max_len:
76
+ return post
77
+
78
+ # Step 2: Shorten if needed
79
+ shorten_prompt = (
80
+ f"Shorten this post to {max_len} characters or fewer. Keep the Shopify link ({url}) and all original hashtags:\n\n{post}"
81
+ if url else
82
+ f"Shorten this post to {max_len} characters or fewer, keeping the tone, emojis, and all original hashtags:\n\n{post}"
83
+ )
84
+
85
+ shortened = get_response(
86
+ input=shorten_prompt,
87
+ template=lambda x: x.strip(),
88
+ llm="gemini",
89
+ md=False,
90
+ temperature=0.7,
91
+ max_tokens=200
92
+ ).strip()
93
+
94
+ return shortened[:max_len]
95
+
96
+
97
+ # ---- Threads poster (replaces the proxy) ----
98
+ def post_to_threads(message: str) -> str:
99
+ """
100
+ One-shot TEXT post to Threads using your long-lived token.
101
+ Uses: POST https://graph.threads.net/me/threads with text params
102
+ """
103
+ if not THREADS_TOKEN:
104
+ return "❌ Missing THREADS_USER_ACCESS_TOKEN environment variable."
105
+
106
+ try:
107
+ r = requests.post(
108
+ "https://graph.threads.net/me/threads",
109
+ headers={"Authorization": f"Bearer {THREADS_TOKEN}"},
110
+ params={
111
+ "text": message,
112
+ "media_type": "TEXT",
113
+ "auto_publish_text": "true", # one-call publish for text
114
+ },
115
+ timeout=30,
116
+ )
117
+ # Prefer JSON, fall back to raw text for debugging
118
+ data = {}
119
+ try:
120
+ data = r.json()
121
+ except Exception:
122
+ data = {"raw": r.text}
123
+
124
+ if r.ok and isinstance(data, dict) and data.get("id"):
125
+ return f"✅ Post successful! Threads Post ID: {data['id']}"
126
+ return f"❌ Threads error ({r.status_code}): {data}"
127
+
128
+ except requests.RequestException as e:
129
+ return f"❌ Network error posting to Threads: {e}"
130
+ except Exception as e:
131
+ return f"❌ Unexpected error: {e}"
132
+
133
+
134
+ # ---- Shiny server wiring (IDs unchanged) ----
135
+ def server(input, output, session):
136
+ post_status = reactive.Value("")
137
+
138
+ @output
139
+ @render.ui
140
+ @reactive.event(input.gen_btn_threads)
141
+ def threads_post_draft():
142
+ topic = input.threads_topic().strip()
143
+ url = input.threads_url().strip()
144
+
145
+ if not topic and not url:
146
+ return ui.HTML("<p><strong>⚠️ Enter a topic or a blog URL.</strong></p>")
147
+
148
+ post = generate_threads_post(topic=topic, url=url)
149
+ if post.startswith(("⚠️", "❌")):
150
+ return ui.HTML(f"<p><strong>{post}</strong></p>")
151
+
152
+ generated_threads_post.set(post)
153
+ return ui.HTML(f"<p><strong>Generated Threads Post:</strong><br>{post}</p>")
154
+
155
+ @output
156
+ @render.text
157
+ def threads_post_status():
158
+ return post_status()
159
+
160
+ @reactive.effect
161
+ @reactive.event(input.post_btn_threads)
162
+ def _():
163
+ post = generated_threads_post()
164
+ if not post:
165
+ post_status.set("⚠️ No post generated yet.")
166
+ else:
167
+ post_status.set(post_to_threads(post))
server/twitter.py CHANGED
@@ -23,7 +23,7 @@ def scrape_shopify_blog(url: str) -> str:
23
  soup = BeautifulSoup(resp.content, 'html.parser')
24
  section = soup.find(
25
  'div',
26
- class_='article-template__content page-width page-width--narrow rte scroll-trigger animate--slide-in'
27
  )
28
  return section.get_text(strip=True, separator=' ') if section else ""
29
  except Exception as e:
 
23
  soup = BeautifulSoup(resp.content, 'html.parser')
24
  section = soup.find(
25
  'div',
26
+ class_='w940 align-center size-content'
27
  )
28
  return section.get_text(strip=True, separator=' ') if section else ""
29
  except Exception as e:
threads.ipynb ADDED
File without changes
ui/threads.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from shiny import ui
2
+
3
+ ui = ui.nav_panel(
4
+ "Threads Poster",
5
+
6
+ # Topic input
7
+ ui.input_text("threads_topic", "Enter Topic", placeholder="e.g. New Anime Drop This Friday"),
8
+
9
+ # Optional blog link
10
+ ui.input_text("threads_url", "Optional Blog Link", placeholder="https://yourshop.com/blogs/xyz"),
11
+
12
+ # Generate button
13
+ ui.div(
14
+ ui.input_action_button("gen_btn_threads", "Generate Threads Post", class_="btn-primary"),
15
+ class_="mt-3 mb-2"
16
+ ),
17
+
18
+ # Post output and button
19
+ ui.output_ui("threads_post_draft"),
20
+ ui.div(
21
+ ui.input_action_button("post_btn_threads", "Post to Threads", class_="btn-success"),
22
+ class_="my-3"
23
+ ),
24
+
25
+ # Status output
26
+ ui.output_text("threads_post_status")
27
+ )