Chat+ link previews: a link in a chat shows its page's title, text and picture, YouTube playable in place
X's web chat shows a link as a bare card. An X tab on a chat page may now ask native code (link_preview, x-capture only, chat pages only) for one link's preview. Native code fetches it: https and public hosts only (addresses checked and pinned), never X's hosts, no cookies or referrer, redirects re-checked, page head <= 512 KB, images <= 1.5 MB by magic bytes as data:, 30 fetches a minute, memory-only cache. Only the link leaves the page. On by default (owner, 2026-10-10).
This commit is contained in:
parent
7530fb1de7
commit
6d1798038c
21 changed files with 1455 additions and 519 deletions
|
|
@ -61,11 +61,11 @@ const pace = (chat: FakeChat) => ({ sleep: (ms: number) => new Promise<void>(res
|
|||
const star = (row: El) => row.querySelector('.bt-chat-star')!;
|
||||
const group = (chat: FakeChat) => chat.conversations.get(GROUP)!.history;
|
||||
|
||||
test('Chat+: five features, each its own flag, all off by default, all allowed on X\'s chat pages', () => {
|
||||
assert.deepEqual(FEATURES.map(feature => feature.id), ['chat.bookmarks', 'chat.searchLoaded', 'chat.pins', 'chat.personFilter', 'chat.keywords']);
|
||||
test('Chat+: six features, each its own flag, off by default but link previews (owner, 2026-10-10), all allowed on X\'s chat pages', () => {
|
||||
assert.deepEqual(FEATURES.map(feature => feature.id), ['chat.bookmarks', 'chat.searchLoaded', 'chat.pins', 'chat.personFilter', 'chat.linkPreviews', 'chat.keywords']);
|
||||
assert.deepEqual(MODULES.map(module => module.id).sort(), FEATURES.map(feature => feature.id).sort());
|
||||
for (const feature of FEATURES) {
|
||||
assert.equal(feature.default, false, feature.id);
|
||||
assert.equal(feature.default, feature.id === 'chat.linkPreviews', feature.id);
|
||||
assert.equal(feature.heavy, true, `${feature.id} runs on the heavy chat pages`);
|
||||
assert.ok(feature.permissions && !feature.permissions.includes('network'), `${feature.id}: no network`);
|
||||
}
|
||||
|
|
@ -175,7 +175,7 @@ test('starred messages: stored in full, valid ones only, each once; the list fil
|
|||
});
|
||||
|
||||
test('Starred messages: a star on every rendered row; a click stores exactly that message, in full; off leaves nothing', () => {
|
||||
const { env, chat, stored } = page(`/i/chat/${GROUP}`, { 'chat.bookmarks': true });
|
||||
const { env, chat, stored } = page(`/i/chat/${GROUP}`, { 'chat.bookmarks': true, 'chat.linkPreviews': false });
|
||||
assert.deepEqual(env.registry.active(), ['chat.bookmarks']);
|
||||
const all = group(chat);
|
||||
const rows = chat.rendered();
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@ const DEFAULT_ON = [
|
|||
'logoBrush', 'twitterWording', 'singleSplitNav',
|
||||
'fixLikeFollow', 'fixScrollRestore', 'fixTimelineJump', 'fixHomeTab', 'fixNotificationBadge', 'fixNewPostsPill', 'fixMediaUpload', 'followingChronological', 'fixChatButton', 'composerRestorePrompt',
|
||||
'composerDrafts', 'hidePromotedPosts', 'mediaDownload', 'rememberVideoVolume', 'searchOperators', 'notifications.keywordAlerts',
|
||||
// On by default by the owner's choice (2026-10-10): Chat+ link previews.
|
||||
'chat.linkPreviews',
|
||||
];
|
||||
/** What Advanced adds: the calm features that came with the area plugins (they add something; none hides or changes X). */
|
||||
const ADVANCED_ADDS = [
|
||||
|
|
@ -166,7 +168,7 @@ test('modes: the exact flags of Basic, Advanced and Power User; an existing setu
|
|||
assert.deepEqual(on(defaults), [...DEFAULT_ON].sort());
|
||||
assert.deepEqual(running(null), [...DEFAULT_ON].sort());
|
||||
for (const [flag, value] of Object.entries(FEATURE_DEFAULTS)) assert.equal(defaults[flag], value, flag);
|
||||
for (const feature of X_FEATURES) if (!(feature.id in FEATURE_DEFAULTS) && feature.id !== 'notifications.keywordAlerts' && feature.backing !== 'plugin') assert.equal(defaults[feature.id], false, `${feature.id} is new: off by default`);
|
||||
for (const feature of X_FEATURES) if (!(feature.id in FEATURE_DEFAULTS) && feature.id !== 'notifications.keywordAlerts' && feature.id !== 'chat.linkPreviews' && feature.backing !== 'plugin') assert.equal(defaults[feature.id], false, `${feature.id} is new: off by default`);
|
||||
for (const row of supersededFiles(X_TABLE)) assert.equal(defaults[row.flag], false, `${row.flag}: what came from an opt-in plugin stays off`);
|
||||
|
||||
// Basic: every area off (so everything an area owns is off), no logo or wording; the fixes and split-view navigation stay.
|
||||
|
|
|
|||
48
apps/desktop/tests/chat-link-previews.test.ts
Normal file
48
apps/desktop/tests/chat-link-previews.test.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// Chat+ link previews (src/areas/chat/linkPreviews.ts; native side src-tauri/src/link_preview.rs):
|
||||
// which links are asked about, how a native answer is checked again before it becomes page text,
|
||||
// and which of X's cards a preview stands in for. Nothing here touches the network or x.com.
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import { asPreview, isBareCard, previewable } from '../src/areas/chat/linkPreviews.ts';
|
||||
|
||||
test('link previews: only plain https links to hosts that are not X\'s own', () => {
|
||||
assert.equal(previewable('https://www.youtube.com/watch?v=dQw4w9WgXcQ'), 'https://www.youtube.com/watch?v=dQw4w9WgXcQ');
|
||||
assert.equal(previewable('https://fishies.lol/'), 'https://fishies.lol/');
|
||||
for (const bad of [
|
||||
'http://example.com/', 'https://user:pw@example.com/', 'https://example.com:8443/', 'https://192.168.0.1/', 'https://[::1]/',
|
||||
'https://intranet/', 'https://x.com/a/status/1', 'https://t.co/abc', 'https://pbs.twimg.com/media/a.jpg', 'javascript:alert(1)', '/i/chat/1-2', '', null,
|
||||
]) assert.equal(previewable(bad), null, String(bad));
|
||||
});
|
||||
|
||||
test('link previews: a native answer is checked again: plain fields, capped, data: images of four types, an 11-character video id', () => {
|
||||
const good = asPreview({ url: 'https://fishies.lol/', site: 'Fishies', title: 'The social aquarium', description: 'x'.repeat(400), image: 'data:image/png;base64,iVBORw0KGgo=' });
|
||||
assert.ok(good);
|
||||
assert.equal(good.description.length, 300);
|
||||
assert.equal(good.image, 'data:image/png;base64,iVBORw0KGgo=');
|
||||
assert.equal(asPreview({ url: 'https://a.example/', title: 't', image: 'data:image/svg+xml;base64,PHN2Zz4=' })?.image, undefined, 'no SVG');
|
||||
assert.equal(asPreview({ url: 'https://a.example/', title: 't', image: 'https://tracker.example/p.gif' })?.image, undefined, 'no remote image');
|
||||
assert.equal(asPreview({ url: 'https://youtu.be/x', title: 't', youtube: 'dQw4w9WgXcQ' })?.youtube, 'dQw4w9WgXcQ');
|
||||
assert.equal(asPreview({ url: 'https://youtu.be/x', title: 't', youtube: 'a"onload="x' })?.youtube, undefined);
|
||||
assert.equal(asPreview({ url: 'javascript:alert(1)', title: 't' }), null, 'the link itself is re-checked');
|
||||
assert.equal(asPreview({ url: 'https://a.example/', title: '' }), null, 'nothing to show, no card');
|
||||
assert.equal(asPreview('nope'), null);
|
||||
});
|
||||
|
||||
test('link previews: X\'s bare card (a host name and an icon) is replaced; a card with its own picture or a sentence is not', () => {
|
||||
const anchor = (text: string, media = false) => ({ textContent: text, querySelector: () => (media ? {} : null) }) as unknown as Element;
|
||||
assert.equal(isBareCard(anchor('www.youtube.com')), true);
|
||||
assert.equal(isBareCard(anchor('fishies.lol')), true);
|
||||
assert.equal(isBareCard(anchor('www.youtube.com', true)), false);
|
||||
assert.equal(isBareCard(anchor('watch this video')), false);
|
||||
assert.equal(isBareCard(anchor('www.youtube.com/watch?v=jNQXAC9IVRw')), false, 'a link in the text (live 2026-10-10) stays');
|
||||
assert.equal(isBareCard(anchor('')), false);
|
||||
});
|
||||
|
||||
test('link previews: page text only through textContent, the player only on YouTube\'s host X allows, no request from the page', () => {
|
||||
const source = readFileSync(new URL('../src/areas/chat/linkPreviews.ts', import.meta.url), 'utf8');
|
||||
assert.doesNotMatch(source, /innerHTML|insertAdjacentHTML|outerHTML|document\.write/);
|
||||
assert.doesNotMatch(source, /\bfetch\(|XMLHttpRequest|WebSocket|sendBeacon/);
|
||||
for (const match of source.matchAll(/https:\/\/([a-z0-9.-]+)\//g)) assert.ok(['i.ytimg.com', 'www.youtube.com'].includes(match[1]!), match[1]);
|
||||
assert.match(source, /rel = 'noopener noreferrer'/);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue