socialmag v0.1 — 4/5 platforms live (Threads, FB, IG, TikTok)
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""Facebook posting flow.
|
||||
|
||||
1. navigate facebook.com
|
||||
2. JS-click the composer: find the "on your mind" span, walk up to the
|
||||
enclosing [role=button], .click() it
|
||||
⚠️ A real mouse click on the span opens Messenger instead.
|
||||
3. wait for the "Create post" dialog
|
||||
4. type into the dialog textbox
|
||||
5. optional: Photo/video -> setFileInputFiles
|
||||
6. optional: change audience when visibility != the current one
|
||||
7. click [aria-label="Post"]
|
||||
8. dialog closes = success
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from ..cdp import CDPError, human_delay
|
||||
from . import guard, require_text, validate_media
|
||||
from ..selectors import FACEBOOK as S
|
||||
|
||||
PLATFORM = "fb"
|
||||
VALID_VISIBILITY = ("public", "friends")
|
||||
|
||||
|
||||
async def post(cdp_client, text, media_path=None, options=None):
|
||||
options = options or {}
|
||||
require_text(PLATFORM, text)
|
||||
validate_media(media_path)
|
||||
|
||||
visibility = options.get("visibility")
|
||||
if visibility is not None and visibility not in VALID_VISIBILITY:
|
||||
raise CDPError(
|
||||
f"fb visibility must be one of {VALID_VISIBILITY}, got {visibility!r}"
|
||||
)
|
||||
|
||||
tab, reused = await cdp_client.get_or_open_tab(S["url"], "facebook.com")
|
||||
if reused:
|
||||
# Dismiss any leftover dialog (e.g. unsaved Create Post from a failed run)
|
||||
await tab.eval("window.onbeforeunload = null")
|
||||
await tab.navigate(S["url"])
|
||||
return await guard(PLATFORM, tab, _flow(tab, text, media_path, visibility))
|
||||
|
||||
|
||||
async def _flow(tab, text, media_path, visibility):
|
||||
await asyncio.sleep(5)
|
||||
await tab.click_js(S["composer_trigger_js"])
|
||||
|
||||
await tab.wait_for(S["dialog_textbox"], timeout=30.0)
|
||||
await tab.type_text(S["dialog_textbox"], text)
|
||||
|
||||
if media_path:
|
||||
# TODO: VALIDATE LIVE — whether the hidden file input is already in the
|
||||
# DOM before the Photo/video button is clicked.
|
||||
if not await tab.exists(S["file_input"]):
|
||||
await tab.click_js(S["photo_video_js"])
|
||||
await tab.wait_for(S["file_input"], visible=False)
|
||||
await tab.set_file_input(S["file_input"], media_path)
|
||||
# TODO: VALIDATE LIVE — upload/preview settle time.
|
||||
await human_delay(2.0, 3.5)
|
||||
|
||||
if visibility:
|
||||
# TODO: VALIDATE LIVE — the whole audience sub-dialog. We only open the
|
||||
# picker here; the option list itself is unmapped, so we refuse rather
|
||||
# than click something we haven't seen.
|
||||
raise CDPError(
|
||||
"fb visibility switching is not implemented: the audience sub-dialog "
|
||||
"has not been mapped live yet. Post without `visibility` to use the "
|
||||
"account's current default."
|
||||
)
|
||||
|
||||
# Try aria-label first, fall back to text-match JS click
|
||||
if await tab.exists(S["post_button"]):
|
||||
await tab.click(S["post_button"])
|
||||
else:
|
||||
await tab.click_js(S["post_button_js"])
|
||||
# FB keeps hidden [role="dialog"] elements in the DOM after the composer
|
||||
# closes. Instead of waiting for all dialogs to vanish, check that the
|
||||
# composer textbox is gone — that's the real "posted" signal.
|
||||
await tab.wait_for_gone(S["dialog_textbox"], timeout=60.0)
|
||||
|
||||
|
||||
async def check_login(cdp_client):
|
||||
tab, reused = await cdp_client.get_or_open_tab(S["url"], "facebook.com")
|
||||
if reused:
|
||||
# Dismiss any leftover dialog (e.g. unsaved Create Post from a failed run)
|
||||
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.querySelectorAll('span')].find(s => s.innerText"
|
||||
" && s.innerText.toLowerCase().includes('on your mind')))()"
|
||||
)
|
||||
Reference in New Issue
Block a user