Initial commit: read social posts back without the app
CI / Typecheck, test, build (push) Successful in 28s
Publish / Build and push (push) Failing after 2m24s

antisocial is the other half of a StopTheMadness redirect rule. Links to X,
Threads, Instagram, TikTok and Bluesky get rewritten to /<prefix>/<original
path>, and this resolves the post and shows the media and the words, with a
badge saying where it came from and a button to copy the original URL.

Every request drives a real headless Chromium, logged out, from a residential
IP. One code path, and it survives markup changes better than parsing from the
outside would. Extraction is layered, most structured first: the platform's own
API response caught in flight, then an inline payload, then the rendered DOM,
then Open Graph tags.

Media is never linked straight at a CDN. Instagram and TikTok reject requests
without a matching Referer and cookies, and proxying keeps the viewer's browser
from talking to the platform at all. Range is forwarded so the native video
scrubber can seek. HLS is the exception, since proxying it would mean rewriting
playlists.

TikTok sometimes answers with a slider puzzle. Rather than reporting that as a
failure, the page is parked and the viewer is handed the puzzle: screenshots
stream out, pointer events are replayed back. Solving it leaves the cookie in
the shared browser context, so the retry is an ordinary request.

A failed resolve is never a blank error page. The card carries the platform, the
original URL and the copy button, so a broken adapter still leaves the link one
tap away.

Verified end to end against real shared links on all five platforms, in the
container, including multi-image carousels, reels, TikTok short links and photo
posts. 49 tests run the adapters against captured payloads with no network.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BGkRmLfiWuJHx6tQ12EELY
This commit is contained in:
2026-08-26 11:32:25 -03:00
co-authored by Claude Opus 5
commit b5f9483615
66 changed files with 7881 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
name: CI
# Typecheck, test and build on every pull request, and on main so a direct
# push cannot leave the branch red without anyone noticing.
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
jobs:
check:
name: Typecheck, test, build
runs-on: ubuntu-latest
container:
image: node:22
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }}
restore-keys: npm-
# Nothing here drives a browser: the adapter tests run against
# captured payloads. Skipping the download keeps the job to seconds.
- name: Install
run: npm ci
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
- name: Typecheck
run: npm run typecheck
- name: Test
run: npm test
- name: Build
run: npm run build
+83
View File
@@ -0,0 +1,83 @@
name: Publish
# Builds the application image and pushes it to the Gitea container registry
# as git.unsupervised.ca/unsupervised/antisocial.
#
# Runs on main, and on demand. Pull requests build without pushing, so a
# broken Dockerfile is caught before it can move a published tag.
on:
push:
branches:
- main
pull_request:
paths:
- 'Dockerfile'
- 'package.json'
- 'package-lock.json'
- '.gitea/workflows/publish.yml'
workflow_dispatch:
env:
REGISTRY: git.unsupervised.ca
IMAGE: unsupervised/antisocial
jobs:
build:
name: Build and push
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Images are built natively, so each carries the architecture of the
# runner that built it. Every runner in the pool is arm64, which is
# also what Kallone is. A runner of a different architecture joining
# would overwrite these tags with its own arch, at which point this
# needs buildx and a multi-arch manifest.
- name: Check Docker is available
run: |
if ! docker info >/dev/null 2>&1; then
echo "No usable Docker daemon in the job container." >&2
echo "act_runner needs container.docker_host set, or left empty to autodetect." >&2
exit 1
fi
docker version --format 'client {{.Client.Version}} / server {{.Server.Version}} / arch {{.Server.Arch}}'
- name: Work out the tags
id: tags
run: echo "sha=sha-$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
# REGISTRY_TOKEN is an organisation secret: a personal access token
# with the package scope, Read and Write. Gitea's Actions task token is
# rejected by the container registry (go-gitea/gitea#23642), so this
# cannot fall back to secrets.GITHUB_TOKEN. The login user must own the
# token; set the REGISTRY_USER variable if it is not github.actor.
- name: Log in to the container registry
if: github.event_name != 'pull_request'
run: |
if [ -z "${{ secrets.REGISTRY_TOKEN }}" ]; then
echo "REGISTRY_TOKEN is not set. The Actions task token cannot push packages." >&2
exit 1
fi
echo "${{ secrets.REGISTRY_TOKEN }}" \
| docker login "${REGISTRY}" -u "${{ vars.REGISTRY_USER || github.actor }}" --password-stdin
- name: Build
run: |
docker build \
--pull \
--tag "${REGISTRY}/${IMAGE}:${{ steps.tags.outputs.sha }}" \
--tag "${REGISTRY}/${IMAGE}:latest" \
--file Dockerfile \
.
- name: Push
if: github.event_name != 'pull_request'
run: |
docker push "${REGISTRY}/${IMAGE}:${{ steps.tags.outputs.sha }}"
docker push "${REGISTRY}/${IMAGE}:latest"
echo "Published ${REGISTRY}/${IMAGE}:${{ steps.tags.outputs.sha }}"
- name: Log out
if: always() && github.event_name != 'pull_request'
run: docker logout "${REGISTRY}" || true