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
+40
View File
@@ -0,0 +1,40 @@
import type { Challenge } from '../challenge/registry.ts';
import { html } from './html.ts';
import { badge, layout } from './layout.ts';
type Box = { x: number; y: number; width: number; height: number };
export function renderChallenge(challenge: Challenge, box: Box | undefined): string {
const body = html`<article class="post post--challenge" data-challenge="${challenge.id}">
<header class="post__head">
${badge(challenge.platform, challenge.platformLabel)}
<div class="who"><span class="who__handle">wants you to prove you are a person</span></div>
</header>
<p class="challenge__lede">
Solve it here and the post loads. Drag on the picture exactly as you would on
${challenge.platformLabel}.
</p>
<div
class="challenge__stage"
id="stage"
data-box="${box ? `${box.x},${box.y},${box.width},${box.height}` : ''}"
>
<img id="frame" alt="The verification puzzle" draggable="false">
<p class="challenge__status" id="status">Loading…</p>
</div>
<footer class="post__foot">
<div class="original">
<button type="button" class="copy" id="give-up">Give up and show me the link</button>
<a class="original__open" href="${challenge.originalUrl}" rel="noopener noreferrer nofollow" target="_blank">
Open on ${challenge.platformLabel}
</a>
<p class="original__url"><code>${challenge.originalUrl}</code></p>
</div>
</footer>
</article>`;
return layout(`Verify — ${challenge.platformLabel}`, body);
}
+30
View File
@@ -0,0 +1,30 @@
import { html } from './html.ts';
import { badge, layout, originalUrlBlock } from './layout.ts';
/**
* A resolve can always fail — someone else's markup changed, the post was
* deleted, a platform tightened a screw. Failing still has to leave the
* link one tap away, so this is a card rather than a status page.
*/
export function renderError(options: {
platform?: string;
platformLabel?: string;
originalUrl?: string;
heading: string;
detail?: string;
}): string {
const { platform, platformLabel = 'the original', originalUrl, heading, detail } = options;
const body = html`<article class="post post--error">
<header class="post__head">
${platform ? badge(platform, platformLabel) : ''}
</header>
<h1 class="error__heading">${heading}</h1>
${detail ? html`<p class="error__detail">${detail}</p>` : ''}
${originalUrl
? html`<footer class="post__foot">${originalUrlBlock(originalUrl, platformLabel)}</footer>`
: ''}
</article>`;
return layout(heading, body);
}
+46
View File
@@ -0,0 +1,46 @@
/** A string that is already safe to drop into the document. */
export class Raw {
readonly value: string;
constructor(value: string) {
this.value = value;
}
toString(): string {
return this.value;
}
}
export function raw(value: string): Raw {
return new Raw(value);
}
const ESCAPES: Record<string, string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
};
export function escapeHtml(value: string): string {
return value.replace(/[&<>"']/g, (char) => ESCAPES[char] ?? char);
}
function render(value: unknown): string {
if (value === null || value === undefined || value === false) return '';
if (value instanceof Raw) return value.value;
if (Array.isArray(value)) return value.map(render).join('');
return escapeHtml(String(value));
}
/**
* Tagged template that escapes every interpolation unless it is already
* `Raw`. Post text comes from strangers, so nothing reaches the document
* without passing through here.
*/
export function html(strings: TemplateStringsArray, ...values: unknown[]): Raw {
let out = strings[0] ?? '';
for (let i = 0; i < values.length; i += 1) {
out += render(values[i]) + (strings[i + 1] ?? '');
}
return new Raw(out);
}
+36
View File
@@ -0,0 +1,36 @@
import { config } from '../config.ts';
import type { PlatformSpec } from '../platforms/types.ts';
import { html } from './html.ts';
import { badge, layout } from './layout.ts';
/**
* There is no useful landing page for a redirect target, so this is the
* reference card instead: which prefixes exist, and the rewrite rules to
* paste into StopTheMadness.
*/
export function renderIndex(platforms: readonly PlatformSpec[]): string {
const origin = config.publicOrigin;
const body = html`<article class="post post--index">
<h1 class="index__title">antisocial</h1>
<p class="index__lede">
Point StopTheMadness at these and shared links open here instead of in an app.
</p>
<table class="index__table">
<thead>
<tr><th>Platform</th><th>Matches</th><th>Rewrites to</th></tr>
</thead>
<tbody>
${platforms.map(
(p) => html`<tr>
<td>${badge(p.id, p.label)}</td>
<td><code>${p.canonicalHost}</code></td>
<td><code>${origin}/${p.prefix}/…</code></td>
</tr>`,
)}
</tbody>
</table>
</article>`;
return layout('antisocial', body);
}
+55
View File
@@ -0,0 +1,55 @@
import { html, type Raw } from './html.ts';
export function layout(title: string, body: Raw): string {
return `<!doctype html>
${html`<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="robots" content="noindex, nofollow">
<meta name="referrer" content="no-referrer">
<meta name="color-scheme" content="light dark">
<title>${title}</title>
<link rel="stylesheet" href="/static/app.css">
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
</head>
<body>
<main>${body}</main>
<script src="/static/app.js" type="module"></script>
</body>
</html>`}`;
}
/**
* Indicative brand accents. The written label is what actually identifies
* the platform; the colour is only there to make it recognisable at a
* glance.
*/
const ACCENT: Record<string, { color: string; monogram: string }> = {
x: { color: '#0f1419', monogram: '✕' },
threads: { color: '#3b3b3b', monogram: '@' },
instagram: { color: '#c13584', monogram: 'IG' },
tiktok: { color: '#fe2c55', monogram: '♪' },
bluesky: { color: '#0085ff', monogram: 'B' },
};
export function badge(platform: string, label: string): Raw {
const accent = ACCENT[platform] ?? { color: '#666', monogram: '?' };
return html`<span class="badge" style="--accent: ${accent.color}">
<span class="badge__mark" aria-hidden="true">${accent.monogram}</span>
<span class="badge__label">${label}</span>
</span>`;
}
/** The copy control, plus the URL itself so it is always selectable even if
* the clipboard API is unavailable. */
export function originalUrlBlock(originalUrl: string, platformLabel: string): Raw {
return html`<div class="original">
<button type="button" class="copy" data-url="${originalUrl}">
<span class="copy__idle">Copy original link</span>
<span class="copy__done" hidden>Copied</span>
</button>
<a class="original__open" href="${originalUrl}" rel="noopener noreferrer nofollow" target="_blank">Open on ${platformLabel}</a>
<p class="original__url"><code>${originalUrl}</code></p>
</div>`;
}
+100
View File
@@ -0,0 +1,100 @@
import { proxyUrlFor } from '../media/registry.ts';
import type { Media, Post } from '../types.ts';
import { html, type Raw } from './html.ts';
import { badge, layout, originalUrlBlock } from './layout.ts';
import { linkify } from './text.ts';
function aspect(item: Media): string {
return item.width && item.height ? `aspect-ratio: ${item.width} / ${item.height};` : '';
}
function renderItem(item: Media, index: number): Raw {
const src = proxyUrlFor(item);
if (item.kind === 'video') {
const poster = item.poster ? proxyUrlFor(item.poster) : undefined;
return html`<figure class="item item--video" data-index="${index}" style="${aspect(item)}">
<video
controls
playsinline
preload="metadata"
${poster ? html`poster="${poster}"` : ''}
><source src="${src}"></video>
${item.hls
? html`<figcaption class="item__note">Streaming format — plays natively in Safari.</figcaption>`
: ''}
</figure>`;
}
return html`<figure class="item item--image" data-index="${index}" style="${aspect(item)}">
<img
src="${src}"
alt="${item.alt ?? ''}"
loading="${index === 0 ? 'eager' : 'lazy'}"
decoding="async"
>
</figure>`;
}
function renderMedia(media: Media[]): Raw {
if (media.length === 0) return html``;
const many = media.length > 1;
return html`<section class="media" data-count="${media.length}">
${many
? html`<div class="media__bar">
<span class="media__count"><span class="media__at">1</span> / ${media.length}</span>
<div class="media__views" role="group" aria-label="Layout">
<button type="button" class="media__view" data-view="swipe" aria-pressed="true">Swipe</button>
<button type="button" class="media__view" data-view="grid" aria-pressed="false">Grid</button>
</div>
</div>`
: ''}
<div class="media__rail">${media.map((item, index) => renderItem(item, index))}</div>
</section>`;
}
function renderText(post: Post): Raw {
if (!post.text) return html``;
return html`<div class="text">${linkify(post.text, post.platform)}</div>`;
}
function renderWhen(post: Post): Raw {
if (!post.postedAt) return html``;
const date = new Date(post.postedAt);
if (Number.isNaN(date.getTime())) return html``;
return html`<time datetime="${date.toISOString()}">${date.toLocaleString('en-CA', {
dateStyle: 'medium',
timeStyle: 'short',
})}</time>`;
}
export function renderPost(post: Post): string {
const avatar = post.author.avatar ? proxyUrlFor(post.author.avatar) : undefined;
const above = post.textPosition === 'above';
const body = html`<article class="post" data-platform="${post.platform}">
<header class="post__head">
${badge(post.platform, post.platformLabel)}
<div class="who">
${avatar ? html`<img class="who__avatar" src="${avatar}" alt="" loading="lazy">` : ''}
<div class="who__names">
${post.author.displayName ? html`<span class="who__name">${post.author.displayName}</span>` : ''}
<span class="who__handle">${post.author.handle}</span>
</div>
</div>
</header>
${above ? renderText(post) : ''}
${renderMedia(post.media)}
${above ? '' : renderText(post)}
<footer class="post__foot">
${renderWhen(post)}
${originalUrlBlock(post.originalUrl, post.platformLabel)}
</footer>
</article>`;
const who = post.author.displayName ?? post.author.handle;
return layout(`${who} on ${post.platformLabel}`, body);
}
+54
View File
@@ -0,0 +1,54 @@
import { escapeHtml, raw, type Raw } from './html.ts';
import type { PlatformId } from '../types.ts';
/** Where an @mention or #hashtag points on each platform. */
const PROFILE_BASE: Record<PlatformId, string> = {
x: 'https://x.com/',
threads: 'https://www.threads.com/@',
instagram: 'https://www.instagram.com/',
tiktok: 'https://www.tiktok.com/@',
bluesky: 'https://bsky.app/profile/',
};
const TAG_BASE: Partial<Record<PlatformId, string>> = {
x: 'https://x.com/hashtag/',
instagram: 'https://www.instagram.com/explore/tags/',
tiktok: 'https://www.tiktok.com/tag/',
};
const TOKEN = /(https?:\/\/[^\s<]+)|(^|\s)(@[\w.-]+)|(^|\s)(#[\w]+)/g;
function link(href: string, label: string): string {
return `<a href="${escapeHtml(href)}" rel="noopener noreferrer nofollow" target="_blank">${escapeHtml(label)}</a>`;
}
/**
* Turn post text into markup: escape everything, then make links, mentions
* and hashtags clickable back to the platform they came from.
*/
export function linkify(text: string, platform: PlatformId): Raw {
let out = '';
let cursor = 0;
for (const match of text.matchAll(TOKEN)) {
const index = match.index;
out += escapeHtml(text.slice(cursor, index));
const [whole, url, mentionLead, mention, tagLead, tag] = match;
if (url) {
out += link(url, url.replace(/^https?:\/\/(www\.)?/, ''));
} else if (mention) {
out += escapeHtml(mentionLead ?? '') + link(PROFILE_BASE[platform] + mention.slice(1), mention);
} else if (tag) {
const base = TAG_BASE[platform];
out += escapeHtml(tagLead ?? '') + (base ? link(base + tag.slice(1), tag) : escapeHtml(tag));
} else {
out += escapeHtml(whole);
}
cursor = index + whole.length;
}
out += escapeHtml(text.slice(cursor));
return raw(out.replace(/\n/g, '<br>'));
}