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