socialmag v0.1 — 4/5 platforms live (Threads, FB, IG, TikTok)
This commit is contained in:
+165
@@ -0,0 +1,165 @@
|
||||
"""Every platform selector, in one file.
|
||||
|
||||
When a platform ships a UI change, this is the only file that should need
|
||||
touching (proposal, Risks table). Nothing here is guessed silently — anything
|
||||
that came from a doc rather than a live FAFO session is marked, and every
|
||||
selector a flow depends on is validated live before we trust it.
|
||||
|
||||
Convention:
|
||||
*_SEL CSS selector string
|
||||
*_JS a JS expression returning an element (for click_js)
|
||||
"""
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Threads
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
THREADS = {
|
||||
"url": "https://www.threads.com/",
|
||||
# New inline composer (2026-09 redesign) — no dialog.
|
||||
"composer_input": '[placeholder="What\'s new?"]',
|
||||
"composer_trigger_js": (
|
||||
"document.querySelector('[placeholder=\"What\\'s new?\"]')"
|
||||
" || [...document.querySelectorAll('span,div')]"
|
||||
".find(e => e.innerText && e.innerText.trim() === 'What\\'s new?')"
|
||||
),
|
||||
"post_button_js": (
|
||||
"[...document.querySelectorAll('button, [role=\"button\"]')]"
|
||||
".find(b => b.innerText.trim() === 'Post')"
|
||||
),
|
||||
# Legacy dialog selectors (kept for fallback)
|
||||
"dialog": '[role="dialog"]',
|
||||
"dialog_textbox": '[role="dialog"] [role="textbox"]',
|
||||
"attach_media": '[role="dialog"] [aria-label="Attach media"]',
|
||||
"file_input": 'input[type="file"]',
|
||||
"login_marker": 'input[name="username"], a[href*="/login"]',
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Facebook
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
FACEBOOK = {
|
||||
"url": "https://www.facebook.com/",
|
||||
# ⚠️ Mouse-clicking the visible "What's on your mind" span opens Messenger.
|
||||
# Walk up to the enclosing [role=button] and .click() it instead.
|
||||
"composer_trigger_js": (
|
||||
"(() => { const span = [...document.querySelectorAll('span')]"
|
||||
".find(s => s.innerText && s.innerText.toLowerCase().includes('on your mind'));"
|
||||
" return span ? span.closest('[role=\"button\"]') : null; })()"
|
||||
),
|
||||
"dialog": '[role="dialog"]',
|
||||
"dialog_textbox": '[role="dialog"] [role="textbox"][contenteditable="true"]',
|
||||
# TODO: VALIDATE LIVE — the Photo/video button inside the create-post dialog.
|
||||
"photo_video_js": (
|
||||
"[...document.querySelectorAll('[role=\"dialog\"] [role=\"button\"]')]"
|
||||
".find(b => /photo\\/video/i.test(b.innerText || ''))"
|
||||
),
|
||||
"file_input": '[role="dialog"] input[type="file"][accept*="image"]',
|
||||
# TODO: VALIDATE LIVE — audience/privacy selector inside the dialog.
|
||||
"privacy_button_js": (
|
||||
"[...document.querySelectorAll('[role=\"dialog\"] [role=\"button\"]')]"
|
||||
".find(b => /public|friends|audience/i.test(b.innerText || ''))"
|
||||
),
|
||||
"post_button": '[role="dialog"] [aria-label="Post"]',
|
||||
"post_button_js": (
|
||||
"[...document.querySelectorAll('[role=\"dialog\"] [role=\"button\"], "
|
||||
"[role=\"dialog\"] button')].find(b => b.innerText.trim() === 'Post')"
|
||||
),
|
||||
"create_post_text": "Create post",
|
||||
"login_marker": 'input[name="email"][type="text"], form[action*="login"]',
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Instagram
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
INSTAGRAM = {
|
||||
"url": "https://www.instagram.com/",
|
||||
"new_post_svg": 'svg[aria-label="New post"]',
|
||||
# JS click — more reliable than mouse on sidebar SVG icons
|
||||
"new_post_js": (
|
||||
"(() => { const svg = document.querySelector('svg[aria-label=\"New post\"]');"
|
||||
" if (svg) { const btn = svg.closest('a, button, [role=\"button\"], div[role=\"button\"]')"
|
||||
" || svg.parentElement; btn.click(); return true; } return false; })()"
|
||||
),
|
||||
"dialog": '[role="dialog"]',
|
||||
"create_dialog_text": "Create new post",
|
||||
"file_input": '[role="dialog"] input[type="file"]',
|
||||
"next_button_js": (
|
||||
"[...document.querySelectorAll('[role=\"dialog\"] [role=\"button\"], "
|
||||
"[role=\"dialog\"] button')].find(b => b.innerText.trim() === 'Next')"
|
||||
),
|
||||
"caption_box": '[role="dialog"] textarea, [role="dialog"] [contenteditable="true"]',
|
||||
"share_button_js": (
|
||||
"[...document.querySelectorAll('[role=\"dialog\"] [role=\"button\"], "
|
||||
"[role=\"dialog\"] button')].find(b => b.innerText.trim() === 'Share')"
|
||||
),
|
||||
"shared_text": "Your post has been shared",
|
||||
"done_button_js": (
|
||||
"[...document.querySelectorAll('[role=\"dialog\"] [role=\"button\"], "
|
||||
"[role=\"dialog\"] button')].find(b => b.innerText.trim() === 'Done')"
|
||||
),
|
||||
"login_marker": 'input[name="username"]',
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# YouTube — community post
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
YOUTUBE = {
|
||||
"url": "https://www.youtube.com/",
|
||||
"create_button": 'button[aria-label="Create"]',
|
||||
# TODO: VALIDATE LIVE — the dropdown item is a tp-yt-paper-item with text.
|
||||
"create_post_item_js": (
|
||||
"[...document.querySelectorAll('tp-yt-paper-item, ytd-compact-link-renderer, "
|
||||
"yt-formatted-string')].find(e => e.innerText && "
|
||||
"e.innerText.trim().toLowerCase() === 'create post')"
|
||||
),
|
||||
"editor": "#contenteditable-root",
|
||||
"post_button": 'button[aria-label="Post"]',
|
||||
# TODO: VALIDATE LIVE — YouTube also renders Post as ytd-button-renderer text.
|
||||
"post_button_js": (
|
||||
"[...document.querySelectorAll('button, tp-yt-paper-button')]"
|
||||
".find(b => b.innerText.trim().toLowerCase() === 'post')"
|
||||
),
|
||||
"login_marker": 'a[href*="accounts.google.com/ServiceLogin"]',
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# TikTok
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
TIKTOK = {
|
||||
"url": "https://www.tiktok.com/tiktokstudio/upload?from=upload",
|
||||
"video_file_input": 'input[type="file"][accept*="video"]',
|
||||
"photo_file_input": 'input[type="file"][accept*="image"]',
|
||||
# TODO: VALIDATE LIVE — the Photos/Video tab switch on the upload page.
|
||||
"photos_tab_js": (
|
||||
"[...document.querySelectorAll('div, span, button')]"
|
||||
".find(e => e.innerText && e.innerText.trim() === 'Photos')"
|
||||
),
|
||||
# TikTok's caption box is a DraftJS contenteditable.
|
||||
"caption_box": 'div[contenteditable="true"]',
|
||||
# TODO: VALIDATE LIVE — upload progress marker; used to know processing is done.
|
||||
"uploaded_text": "Uploaded",
|
||||
"post_button_js": (
|
||||
"[...document.querySelectorAll('button')]"
|
||||
".find(b => ['post','publish'].includes(b.innerText.trim().toLowerCase()) "
|
||||
"&& !b.disabled)"
|
||||
),
|
||||
"success_text": "Your video is being uploaded",
|
||||
"continue_post_js": (
|
||||
"[...document.querySelectorAll('button, [role=\"button\"]')]"
|
||||
".find(b => /continue to post|post now/i.test(b.innerText.trim()))"
|
||||
),
|
||||
"login_marker": 'a[href*="/login"]',
|
||||
}
|
||||
|
||||
PLATFORMS = {
|
||||
"threads": THREADS,
|
||||
"fb": FACEBOOK,
|
||||
"ig": INSTAGRAM,
|
||||
"yt": YOUTUBE,
|
||||
"tiktok": TIKTOK,
|
||||
}
|
||||
Reference in New Issue
Block a user