v0.2: twitter module, feed/notifications/reply/open_link tools, dashboard
This commit is contained in:
+70
-7
@@ -1,15 +1,13 @@
|
||||
"""Threads posting flow (2026-09 inline composer).
|
||||
"""Threads posting and notifications 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
|
||||
post — post via inline composer
|
||||
check_login — verify signed in
|
||||
get_notifications — scrape the Activity page
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from ..cdp import human_delay
|
||||
from ..cdp import human_delay, CDPError
|
||||
from . import guard, require_text, validate_media
|
||||
from ..selectors import THREADS as S
|
||||
|
||||
@@ -117,3 +115,68 @@ async def check_login(cdp_client):
|
||||
" || [...document.querySelectorAll('span,div')]"
|
||||
".find(e => e.innerText && e.innerText.trim() === 'What\\'s new?'))"
|
||||
)
|
||||
|
||||
|
||||
async def get_notifications(cdp_client, count=10):
|
||||
"""Scrape the Threads Activity page."""
|
||||
tab, reused = await cdp_client.get_or_open_tab(
|
||||
"https://www.threads.com/activity", "threads.com"
|
||||
)
|
||||
if reused:
|
||||
await tab.eval("window.onbeforeunload = null")
|
||||
await tab.navigate("https://www.threads.com/activity")
|
||||
|
||||
await asyncio.sleep(4)
|
||||
|
||||
count = min(count, 20)
|
||||
js = r"""
|
||||
(() => {
|
||||
const region = document.querySelector('[role="region"]');
|
||||
if (!region) return [];
|
||||
const ORIGIN = 'https://www.threads.com';
|
||||
const abs = (h) => !h ? null
|
||||
: (/^https?:/.test(h) ? h : ORIGIN + (h.startsWith('/') ? h : '/' + h));
|
||||
|
||||
// Row detection, from the live DOM (probed 2026-09-11): a
|
||||
// notification row is an element that links somewhere, carries a
|
||||
// relative-time marker ("3h", "6h", "2d"), and holds no smaller
|
||||
// element that does the same. The length floor rejects the bare
|
||||
// "6h" time chip, which is itself a link on some rows.
|
||||
const isRow = (el) => {
|
||||
const t = el.innerText ? el.innerText.trim() : '';
|
||||
return t.length >= 12
|
||||
&& /\b\d+[smhdw]\b/.test(t)
|
||||
&& !!el.querySelector('a[href]');
|
||||
};
|
||||
const cands = [...region.querySelectorAll('*')].filter(isRow);
|
||||
const rows = cands.filter(el => !cands.some(o => o !== el && el.contains(o)));
|
||||
|
||||
const results = [];
|
||||
for (const row of rows) {
|
||||
const text = row.innerText.trim().replace(/\n+/g, ' ');
|
||||
if (text.length < 5) continue;
|
||||
const hrefs = [...row.querySelectorAll('a[href]')]
|
||||
.map(x => x.getAttribute('href'));
|
||||
// Prefer the post permalink; otherwise whatever the row links to
|
||||
// (settings notices link to /settings/... — a real destination).
|
||||
const post = hrefs.find(h => /\/@[^\/]+\/post\//.test(h || ''));
|
||||
results.push({ text: text, url: abs(post || hrefs[0]) });
|
||||
}
|
||||
|
||||
// Fallback: no anchors at all (layout change, or a notification kind
|
||||
// that doesn't link anywhere). Keep the old text-split rather than
|
||||
// returning an empty list that would read as "no activity".
|
||||
if (!results.length) {
|
||||
const items = region.innerText.trim()
|
||||
.split(/(?<=\b\d+[hdm]\b)\s*\n\s*/);
|
||||
for (const p of items) {
|
||||
const t = p.trim().replace(/\n+/g, ' ');
|
||||
if (t.length < 5) continue;
|
||||
results.push({ text: t, url: null });
|
||||
}
|
||||
}
|
||||
return results.slice(0, COUNT);
|
||||
})()
|
||||
""".replace("COUNT", str(count))
|
||||
|
||||
return await tab.eval(js) or []
|
||||
|
||||
Reference in New Issue
Block a user