v0.2: twitter module, feed/notifications/reply/open_link tools, dashboard

This commit is contained in:
2026-09-11 02:20:13 +08:00
parent a4f33f456e
commit eaedf4f409
6 changed files with 1368 additions and 24 deletions
+70 -7
View File
@@ -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 []