v0.3: dashboard local mode, openbridge tab reuse, tab activation fix

- dashboard.html: local dashboard.json mode (10s poll) alongside artifact db mode;
  clickable cards behind an http(s)-only safeUrl filter; meta charset; mode-aware
  freshness line
- openbridge.py: localhost shim exposing open_link so dashboard clicks reuse the
  platform's warm tab instead of spawning one per click
- cdp.py: Tab.activate(); get_or_open_tab activates reused tabs — a backgrounded
  tab never finishes rendering, which made YouTube's login check read a signed-in
  account as signed out
- README/MANIFEST: corrected CDP port (9336, was documented as 9222), removed the
  false claim that the server writes dashboard.json, documented the reboot/VPS
  requirements
- .gitignore: __pycache__/, *.pyc, state.json, Archived/; removed stale tracked
  pycache and state.json from index
This commit is contained in:
2026-09-11 10:18:38 +08:00
parent eaedf4f409
commit 1ac06c1bbe
20 changed files with 719 additions and 72 deletions
+103 -34
View File
@@ -1,39 +1,50 @@
# socialmag — Social Media MCP Server
**One tool, all platforms.** Post to Threads, Facebook, Instagram, TikTok (and YouTube when eligible) through a single MCP call. Built on CDP — no API keys, no rate limits, no approval process. Just a browser and a logged-in account.
**One tool, all platforms.** Post to Threads, Facebook, Instagram, TikTok, Twitter/X (and YouTube when eligible) through a single MCP call. Built on CDP — no API keys, no rate limits, no approval process. Just a browser and a logged-in account.
## What It Does
**Write**
- **`create_post`** — Post text/media to one or more platforms in one call
- **`reply_tweet`** — Reply to a tweet found by search
**Read**
- **`check_login`** — Verify which platforms are logged in
- **`get_status`** — Last post results + open browser tabs
- **`get_feed`** — Twitter timeline or search, with X operators (`from:`, `min_faves:`, …)
- **`get_notifications`** — Platform activity (Threads)
**Navigate**
- **`open_link`** — Send a platform's existing tab to a URL, instead of spawning a new one
## Architecture
```
Claude/MCP Client
↓ stdio (JSON-RPC)
socialmag server.py
Claude/MCP Client dashboard.html (browser)
↓ stdio (JSON-RPC) ↓ fetch (localhost)
socialmag server.py ←────────────── openbridge.py :8322
↓ CDP WebSocket
Brave Browser (dedicated instance, port 9336)
↓ real browser sessions
Threads · Facebook · Instagram · TikTok · YouTube
Threads · Facebook · Instagram · TikTok · YouTube · Twitter/X
```
- **Tab reuse** — attaches to existing tabs, no cold loads
- **Tab activation** — a reused tab is brought to the front, because a backgrounded tab is not guaranteed to have rendered (see *Journey*)
- **Per-platform modules** — each platform has its own flow + selectors
- **Sequential queue** — one platform at a time, no race conditions
- **Error screenshots** — failed flows capture the page state to `/tmp/socialmag/`
## Platform Status
| Platform | Text | Media | Tab Reuse | Status |
|----------|:----:|:-----:|:---------:|--------|
| Threads | ✅ | 🔲 | ✅ | **Live** — inline composer |
| Facebook | ✅ | 🔲 | ✅ | **Live** — dialog flow |
| Instagram | — | ✅ | ✅ | **Live** — media required |
| TikTok | — | ✅ | ✅ | **Live** — video/photo upload |
| YouTube | 🔲 | 🔲 | ✅ | **Parked** — needs 500 subs |
| Platform | Text | Media | Read | Status |
|----------|:----:|:-----:|:----:|--------|
| Threads | ✅ | 🔲 | ✅ | **Live** — inline composer + activity scraping |
| Facebook | ✅ | 🔲 | — | **Live** — dialog flow |
| Instagram | — | ✅ | — | **Live** — media required |
| TikTok | — | ✅ | — | **Live** — video/photo upload |
| Twitter/X | ✅ | 🔲 | ✅ | **Live** — post, reply, timeline + search |
| YouTube | 🔲 | 🔲 | — | **Parked** — needs 500 subs |
✅ = tested live · 🔲 = coded but untested · — = not applicable
@@ -56,33 +67,86 @@ brave --remote-debugging-port=9336 \
Log into each platform manually once. socialmag reuses those sessions.
## Future Roadmap
---
### v0.2 — Read Side
- [ ] `get_notifications(platform)` — pull notifications via CDP scraping
- [ ] `get_comments(platform, post_id?)` — read comments on recent posts
- [ ] `get_engagement(platform)` — likes, shares, reach per post
## Journey So Far
### v0.3 — Monitoring
- [ ] Cron/loop polling — periodic checks, dump to JSON
### v0.1 — 2026-09-10 · it posts
Five platforms coded, four live. Threads, Facebook, Instagram, TikTok posting
through a real logged-in browser. YouTube parked behind its 500-subscriber gate.
First automated post from the house went out that night.
### v0.2 — 2026-09-11 (early hours) · it reads
Twitter/X joined as a full module — post, reply, timeline, search.
`get_notifications` landed for Threads. `open_link` built for warm-tab reuse.
Pushed to a private GitHub repo.
**What the read side cost that the plan didn't predict:**
- The Threads scraper was believed to already return a post `link`. It did not —
it returned `{text}` only. Row detection had to be built from the live DOM. The
rule that worked: *a row is an element that links somewhere, carries a
relative-time marker, and contains no smaller element that does the same.*
- A tweet's permalink is **not** "the first `/status/` href" — an article carries
three (canonical, `/analytics`, `/photo/N`). It's the anchor wrapping `<time>`.
### v0.3 — 2026-09-11 (morning) · it shows, and it stops lying
**Dashboard, two modes.** Artifact mode reads four live `db` collections over
`onSnapshot`. Local mode falls back to `dashboard.json` beside the page, polling
every 10s, served off any plain HTTP server.
**Clicks stopped exploding.** `openbridge.py` exposes `open_link` over localhost so
a dashboard card navigates the platform's existing tab. Verified: 8 tabs before two
clicks, 8 after. It imports `tool_open_link` rather than reimplementing it.
**The YouTube false-negative.** `check_login` reported YouTube signed out while the
account was signed in. Not a race, not an account problem — a **backgrounded tab
never finishes rendering**. YouTube leaves its masthead as
`#masthead-skeleton-icons` with an empty `#buttons`, so `#avatar-btn` — the element
the check looks for — never mounts. Measured: still absent after 23 seconds, while
`ytcfg.get('LOGGED_IN')` was `true` the whole time. Fixed by activating a reused tab
(`Target.activateTarget`): `yt` now returns `True` 3/3, against `False` 3/3 before.
The general lesson, written down because it will bite again: **a DOM-render signal
needs the foreground; a JS-state signal does not.**
**What was lost.** The v2 dashboard files were written to `/tmp/socialmag/` and did
not survive a `/tmp` clear. Git held only the pre-v2 version. They were rebuilt from
the field handback. Everything since is written into the repo.
---
## Roadmap
### Done
- [x] `get_notifications(platform)` — Threads activity via CDP scraping *(v0.2)*
- [x] `get_feed` — Twitter timeline + search *(v0.2)*
- [x] `reply_tweet` *(v0.2)*
- [x] Web dashboard — one view for all platform activity *(v0.3)*
- [x] `open_link` + openbridge — tab reuse from the dashboard *(v0.3)*
### Next — monitoring
- [ ] **A writer for `dashboard.json`** — nothing writes it today; it is hand-maintained
- [ ] Periodic refresh (systemd timer over cron: journal + ordering + `Persistent=true`)
- [ ] Units so `:8321` and `:8322` survive reboot
- [ ] Unified notification feed across all platforms
- [ ] Alert on new comments/DMs
- [ ] `get_comments`, `get_engagement`
### v0.4 — Dashboard
- [ ] Web dashboard — one view for all platform activity
- [ ] Post scheduler — queue posts for specific times
- [ ] Analytics — engagement trends, best posting times
### v0.5 — Media Pipeline
- [ ] Threads media posting (image + video)
- [ ] Facebook media posting
- [ ] YouTube video upload (separate from community posts)
- [ ] Auto-resize/format media per platform requirements
⚠️ **The blocker isn't the code.** Interval refresh now steals browser focus by
design, since tab activation is what makes login checks truthful. And Brave is not a
service — after boot there is no CDP until it's opened by hand, so a refresher must
treat "no browser" as a waiting state, not a failure. See README → *Surviving
reboot*.
### Later
- [ ] Post scheduler + content calendar
- [ ] Analytics — engagement trends, best posting times
- [ ] Media pipeline — Threads/Facebook media, YouTube video upload, per-platform resize
- [ ] Multi-account support
- [ ] Platform API integration (where available + worthwhile)
- [ ] Content calendar
- [ ] VPS deployment — **a different product, not a port.** Needs a headless browser
with all six accounts logged in *there*, a display (unrendered pages give wrong
answers — see the YouTube finding), and it puts real accounts at risk of being
challenged from a datacentre IP
- [ ] AI-assisted caption generation
## File Structure
@@ -91,17 +155,22 @@ Log into each platform manually once. socialmag reuses those sessions.
socialmag/
├── __init__.py # package marker
├── __main__.py # python -m socialmag entry
├── server.py # MCP stdio server + tool handlers
├── cdp.py # CDP WebSocket client
├── server.py # MCP stdio server + 7 tool handlers
├── cdp.py # CDP WebSocket client (CDPClient + Tab, incl. activate)
├── selectors.py # all platform selectors in one file
├── queue.py # sequential post queue
├── openbridge.py # localhost HTTP shim — open_link for the dashboard
├── dashboard.html # status/timeline viewer (artifact db or local JSON)
├── dashboard.json # local-mode data — hand-maintained, nothing writes it
├── platforms/
│ ├── __init__.py # shared helpers (guard, require_media, etc.)
│ ├── threads.py
│ ├── facebook.py
│ ├── instagram.py
│ ├── tiktok.py
│ ├── twitter.py
│ └── youtube.py
├── README.md # how to run it, and what it costs to keep running
└── MANIFEST.md # this file
```