socialmag v0.1 — 4/5 platforms live (Threads, FB, IG, TikTok)

This commit is contained in:
2026-09-10 21:10:55 +08:00
commit a4f33f456e
28 changed files with 2053 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
"""YouTube community post flow.
1. navigate youtube.com
2. click button[aria-label="Create"]
3. click "Create post" in the dropdown
4. wait for the redirect to the channel posts page
5. type into #contenteditable-root
6. click Post
7. the editor emptying / the post appearing = success
Community posts require a channel that has them enabled. Video upload
(post_youtube_video in the proposal) is NOT built here — the brief scoped this
module to the community post.
"""
import asyncio
from ..cdp import CDPError, human_delay
from . import guard, require_text, validate_media
from ..selectors import YOUTUBE as S
PLATFORM = "yt"
async def post(cdp_client, text, media_path=None, options=None):
options = options or {}
require_text(PLATFORM, text)
validate_media(media_path)
if media_path:
# Community posts do take images, but that flow is unmapped. Refuse
# rather than drop the media on the floor.
raise CDPError(
"yt: media on community posts is not implemented (the image-attach "
"flow has not been mapped live). Post text-only, or use a platform "
"whose media flow is built."
)
tab, reused = await cdp_client.get_or_open_tab(S["url"], "youtube.com")
if reused:
await tab.eval("window.onbeforeunload = null")
await tab.navigate(S["url"])
return await guard(PLATFORM, tab, _flow(tab, text))
async def _flow(tab, text):
await tab.wait_for(S["create_button"])
await human_delay()
await tab.click(S["create_button"])
# TODO: VALIDATE LIVE — the dropdown item text and element type.
await _wait_for_js(tab, S["create_post_item_js"], "the 'Create post' menu item")
await tab.click_js(S["create_post_item_js"])
# Step 4: YouTube navigates to the channel's posts page.
await tab.wait_for_load(timeout=30.0)
await tab.wait_for(S["editor"], timeout=30.0)
# #contenteditable-root is a YouTube custom editor: it listens to real key
# events, so type per-char rather than insertText.
# TODO: VALIDATE LIVE — whether insertText alone enables the Post button.
await tab.type_text(S["editor"], text, per_char=True)
if await tab.exists(S["post_button"]):
await tab.click(S["post_button"])
else:
await tab.click_js(S["post_button_js"])
# TODO: VALIDATE LIVE — success marker. The editor going empty is the
# cheapest observable signal; the proposal says "post appears".
await _wait_for_editor_empty(tab)
async def _wait_for_js(tab, expression, what, timeout=20.0):
deadline = asyncio.get_running_loop().time() + timeout
while asyncio.get_running_loop().time() < deadline:
if await tab.eval("!!(%s)" % expression):
return
await asyncio.sleep(0.25)
raise CDPError(f"yt: {what} did not appear within {timeout}s")
async def _wait_for_editor_empty(tab, timeout=30.0):
deadline = asyncio.get_running_loop().time() + timeout
while asyncio.get_running_loop().time() < deadline:
text = await tab.eval(
"(() => { const e = document.querySelector('%s');"
" return e ? e.innerText.trim() : null; })()" % S["editor"]
)
if text == "":
return
await asyncio.sleep(0.5)
raise CDPError("yt: editor still holds text 30s after clicking Post")
async def check_login(cdp_client):
tab, reused = await cdp_client.get_or_open_tab(S["url"], "youtube.com")
if reused:
await tab.eval("window.onbeforeunload = null")
await tab.navigate(S["url"])
await asyncio.sleep(5)
if await tab.exists(S["login_marker"]):
return False
return await tab.eval(
"!!document.querySelector('#avatar-btn, button#avatar-btn,"
" ytd-topbar-menu-button-renderer #avatar-btn')"
)