socialmag v0.1 — 4/5 platforms live (Threads, FB, IG, TikTok)
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
"""Threads posting flow (2026-09 inline composer).
|
||||
|
||||
1. navigate threads.com
|
||||
2. click the inline "What's new?" composer
|
||||
3. type text
|
||||
4. click Post button
|
||||
5. text clearing / post appearing = success
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from ..cdp import human_delay
|
||||
from . import guard, require_text, validate_media
|
||||
from ..selectors import THREADS as S
|
||||
|
||||
PLATFORM = "threads"
|
||||
|
||||
|
||||
async def post(cdp_client, text, media_path=None, options=None):
|
||||
options = options or {}
|
||||
require_text(PLATFORM, text)
|
||||
validate_media(media_path)
|
||||
|
||||
tab, reused = await cdp_client.get_or_open_tab(S["url"], "threads.com")
|
||||
if reused:
|
||||
await tab.eval("window.onbeforeunload = null")
|
||||
await tab.navigate(S["url"])
|
||||
return await guard(PLATFORM, tab, _flow(tab, text, media_path))
|
||||
|
||||
|
||||
async def _flow(tab, text, media_path):
|
||||
# Wait for the inline composer to appear
|
||||
await _wait_for_composer(tab)
|
||||
await human_delay()
|
||||
|
||||
# Click the composer input area
|
||||
await tab.click_js(S["composer_trigger_js"])
|
||||
await human_delay(0.5, 1.0)
|
||||
|
||||
# Type into whichever input is now focused — could be a contenteditable
|
||||
# or a textbox that appeared after clicking
|
||||
textbox = await _find_active_input(tab)
|
||||
await tab.type_text(textbox, text)
|
||||
|
||||
if media_path:
|
||||
if not await tab.exists(S["file_input"]):
|
||||
# Look for an attach media button near the composer
|
||||
await human_delay()
|
||||
if await tab.exists(S["file_input"]):
|
||||
await tab.set_file_input(S["file_input"], media_path)
|
||||
await human_delay(1.5, 3.0)
|
||||
|
||||
await human_delay(0.5, 1.0)
|
||||
await tab.click_js(S["post_button_js"])
|
||||
|
||||
# Success = the composer returns to its empty placeholder state
|
||||
await _wait_for_post_complete(tab)
|
||||
|
||||
|
||||
async def _wait_for_composer(tab, timeout=20.0):
|
||||
deadline = asyncio.get_running_loop().time() + timeout
|
||||
while asyncio.get_running_loop().time() < deadline:
|
||||
has_input = await tab.eval(
|
||||
"!!(document.querySelector('[placeholder=\"What\\'s new?\"]')"
|
||||
" || [...document.querySelectorAll('span,div')]"
|
||||
".find(e => e.innerText && e.innerText.trim() === 'What\\'s new?'))"
|
||||
)
|
||||
if has_input:
|
||||
return
|
||||
await asyncio.sleep(0.5)
|
||||
from ..cdp import CDPError
|
||||
raise CDPError("threads: composer ('What's new?') not found after 20s")
|
||||
|
||||
|
||||
async def _find_active_input(tab):
|
||||
"""After clicking the composer, find the actual text input element."""
|
||||
# Check for contenteditable first (Threads often uses these)
|
||||
for sel in ('[contenteditable="true"]', '[role="textbox"]', 'textarea',
|
||||
'[placeholder="What\'s new?"]'):
|
||||
if await tab.exists(sel):
|
||||
return sel
|
||||
from ..cdp import CDPError
|
||||
raise CDPError("threads: no text input found after clicking composer")
|
||||
|
||||
|
||||
async def _wait_for_post_complete(tab, timeout=30.0):
|
||||
"""Wait for the post to go through — composer empties or a toast appears."""
|
||||
deadline = asyncio.get_running_loop().time() + timeout
|
||||
while asyncio.get_running_loop().time() < deadline:
|
||||
# Check if the composer is back to empty placeholder state
|
||||
empty = await tab.eval(
|
||||
"(() => {"
|
||||
" const ce = document.querySelector('[contenteditable=\"true\"]');"
|
||||
" if (ce && ce.innerText.trim() === '') return true;"
|
||||
" const tb = document.querySelector('[role=\"textbox\"]');"
|
||||
" if (tb && tb.innerText.trim() === '') return true;"
|
||||
" return false;"
|
||||
"})()"
|
||||
)
|
||||
if empty:
|
||||
return
|
||||
await asyncio.sleep(0.5)
|
||||
from ..cdp import CDPError
|
||||
raise CDPError("threads: composer still has text 30s after clicking Post")
|
||||
|
||||
|
||||
async def check_login(cdp_client):
|
||||
tab, reused = await cdp_client.get_or_open_tab(S["url"], "threads.com")
|
||||
if reused:
|
||||
await tab.eval("window.onbeforeunload = null")
|
||||
await tab.navigate(S["url"])
|
||||
await asyncio.sleep(4)
|
||||
if await tab.exists(S["login_marker"]):
|
||||
return False
|
||||
return await tab.eval(
|
||||
"!!(document.querySelector('[placeholder=\"What\\'s new?\"]')"
|
||||
" || [...document.querySelectorAll('span,div')]"
|
||||
".find(e => e.innerText && e.innerText.trim() === 'What\\'s new?'))"
|
||||
)
|
||||
Reference in New Issue
Block a user