Compare commits
4
Commits
1.3.1
..
c08b628cce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c08b628cce
|
||
|
|
045a312f14
|
||
|
|
2782ba41b3
|
||
|
|
658639c0b6
|
@@ -0,0 +1,58 @@
|
||||
# AGENTS.md
|
||||
|
||||
Self-hosted page that renders a social post (X, Threads, Instagram, Facebook, TikTok, Bluesky, Reddit) without the app, by driving real headless Chromium logged-out. No auth; private-network use only. Keep deployment specifics (hosts, clusters, registries, manifests) out of this repo.
|
||||
|
||||
`AGENTS.md` (this file) is the source of truth for agent instructions. `CLAUDE.md` is supplemental detail (adapter internals, per-platform quirks, publishing/signing). `README.md` is user-facing. On conflict, this file wins; executable sources win over all prose.
|
||||
|
||||
## Commands
|
||||
|
||||
```sh
|
||||
npm ci
|
||||
npx playwright install chromium # once
|
||||
npm run dev # http://localhost:8080, runs src directly via type-stripping
|
||||
npm run resolve -- '<post url>' # ground truth: real browser, prints Post JSON or error
|
||||
npm run probe -- '<adapter url>' [waitMs] # what the page actually served (payload keys, DOM video/img, OG, challenge, filtered API traffic)
|
||||
npm test # node:test vs test/fixtures/, no network
|
||||
npm run typecheck # this IS the lint step — no ESLint
|
||||
npm run build # tsc -> dist/
|
||||
```
|
||||
|
||||
CI (`.gitea/workflows/ci.yml`) runs `typecheck -> test -> build` with `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1` (fixtures need no browser).
|
||||
|
||||
## "This link didn't work" workflow — do not skip step 1
|
||||
|
||||
1. `npm run resolve -- '<url>'` — ground truth.
|
||||
2. Classify before fixing: transient rate-limit (esp. TikTok — wait and retry) vs verification puzzle (expect redirect to `/challenge/<id>`) vs structured-payload miss (carousel count wrong / poster where video belongs = DOM fall-through) vs Facebook wrong-post (feed read instead of post — must become an error) vs Reddit post-ok-no-comments (`.json` refused, page fallback).
|
||||
3. `npm run probe -- '<url the adapter loads>'` — e.g. Instagram `/embed/captioned/`, not the post URL. Every adapter fix starts here.
|
||||
4. Fix adapter + capture a real-payload fixture in `test/fixtures/` + assert in `test/<platform>.test.ts`. Fix without fixture regresses.
|
||||
5. `npm test`, then re-resolve end to end.
|
||||
|
||||
## Architecture
|
||||
|
||||
Request → `src/routes/post.ts` → `src/platforms/index.ts` (prefix → adapter) → `src/resolve.ts` (cache/timeout/concurrency) → `withPage` (`src/browser/pool.ts`) → adapter → `Post` (`src/types.ts`) → `src/render/post.ts`.
|
||||
|
||||
Adding a platform: one file in `src/platforms/` + one row in `index.ts`. Nothing downstream changes.
|
||||
|
||||
## Rules agents get wrong
|
||||
|
||||
- Extraction order is load-bearing: in-flight API response → inline payload → rendered DOM → OG tags. Keep it.
|
||||
- `Post` is `Segment[]`, not one body. Bluesky/Threads emit author chains; `isAnchor` marks the linked post. `Segment.quoted` (X, Bluesky) keeps quoted author+text+media together — never lift quoted media into the quoter's media (false attribution).
|
||||
- `Post.comments` tree is Reddit-only. Renderer uses `<details>` per comment; nesting is what makes folding free.
|
||||
- Media is never linked to a CDN: register in `src/media/registry.ts`, serve `/m/<id>` with stored `Referer`/`Cookie`, forward `Range`. Exception: `direct: true` HLS (no playlist rewriting).
|
||||
- One Chromium, one persistent context — cookies/banners accumulate deliberately. Images/video/fonts are aborted at route layer (URLs/`src` attrs survive for DOM extraction).
|
||||
- `src/platforms/scan.ts` pulls balanced JSON by key, including escaped-inside-JS-string (Instagram). Payload present-but-empty usually means one more encoding level.
|
||||
- Facebook: narrow to the linked post via `partsOfPost` (id, or `permalink_url`/`wwwURL` for `pfbid`; unmatched id = failure, never read the shipped-along feed). Collect all claiming nodes (caption/author/files live in different blocks); post owner comes from `actors`/`owner`/`video_owner`/`owner_as_page`, never a bare `author` (that's a commenter).
|
||||
- X: decode pre-escaped text after slicing `display_text_range` (UTF-16 units into escaped text); expand `t.co` by matching shortlink text in `entities.urls`, not `indices`; rebuild quoted URL from handle + `id_str`.
|
||||
- Reddit: prefer `.json` (post + first comment page); cold profile gets a JS challenge — navigate once, retry, keep the cookie. `replies` is `""` when empty. Video with sound (`has_audio`) must use `hls_url` direct; silent uses proxied MP4; poster is `preview.images`, never `scrubber_media_url`. Gallery order comes only from `gallery_data`. Comment images are tokens resolved via the comment's own `media_metadata`, with Giphy-id → `i.giphy.com/media/<id>/giphy.gif` fallback; render Markdown via `src/render/markdown.ts` (escape-first), never `body_html`.
|
||||
- TikTok: find whichever `__UNIVERSAL_DATA_FOR_REHYDRATION__` scope holds `itemStruct` (key varies); CDN needs `Referer` + cookies; bare-path `vm.`/`vt.` codes rebuilt in `buildOriginalUrl`.
|
||||
- Threads/Instagram share the media schema (`meta-media.ts`); Threads follow-up = author replying to self (vs stranger reply with same `reply_to_author`); Bluesky app calls `getPostThreadV2`, page fallback matches V1 only.
|
||||
- Never a bare error page: failed resolves render a card with platform + original URL + copy button.
|
||||
- Video sizing comes from the poster (empty SVG stand-in when missing); iOS audio needs the `playback` session in `public/app.js`.
|
||||
|
||||
## Conventions
|
||||
|
||||
- TS 7, ESM, `.ts` import specifiers (tsc rewrites to `.js`); `node --experimental-strip-types` runs `src`/`test`/`bin` directly. `strict` + `noUnusedLocals` + `noUncheckedIndexedAccess` enforced by `typecheck`.
|
||||
- `public/` is served as-is: plain JS, not TS. `public/browsers.js` stays a separate module so tests can import it (`allowJs`).
|
||||
- Tests: `node:test`, fixture(payload) via `test/helpers.ts`, no network. Single-file run: `node --test --experimental-strip-types test/reddit.test.ts`.
|
||||
- All post text is untrusted: emit via `html` tagged template / `linkify` only, never string-concatenated markup. Comments explain *why*, not what the next line does.
|
||||
- Config is env-only with working defaults (`src/config.ts`); `PROFILE_DIR` persists browser state. Chromium needs >64Mi `/dev/shm`.
|
||||
@@ -1,5 +1,10 @@
|
||||
# antisocial — working notes
|
||||
|
||||
> `AGENTS.md` is the source of truth for agent instructions. This file is
|
||||
> supplemental detail (adapter internals, per-platform quirks,
|
||||
> publishing/signing). On conflict, `AGENTS.md` wins; executable sources win
|
||||
> over all prose.
|
||||
|
||||
A self-hosted page that shows a social post without the app. StopTheMadness rewrites
|
||||
links to X, Threads, Instagram, Facebook, TikTok, Bluesky and Reddit into
|
||||
`/<prefix>/<original path>`; this resolves the post by driving a real headless Chromium
|
||||
|
||||
@@ -254,6 +254,9 @@ link preview offers.
|
||||
|
||||
## Development
|
||||
|
||||
Agent instructions live in `AGENTS.md` (source of truth); `CLAUDE.md` holds
|
||||
adapter-internals detail.
|
||||
|
||||
Needs Node 22+. The container commands below use Apple `container`; `docker` takes the
|
||||
same arguments if that is what you have.
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "antisocial",
|
||||
"version": "1.2.5",
|
||||
"version": "1.3.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "antisocial",
|
||||
"version": "1.2.5",
|
||||
"version": "1.3.2",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@fastify/static": "10.1.3",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "antisocial",
|
||||
"version": "1.2.5",
|
||||
"version": "1.3.2",
|
||||
"private": true,
|
||||
"description": "Reads social posts back to you without the app.",
|
||||
"license": "UNLICENSED",
|
||||
|
||||
Reference in New Issue
Block a user