Add Reddit with its comment threads, and show quoted posts whole
CI / Typecheck, test, build (pull_request) Successful in 9s
CI / Typecheck, test, build (pull_request) Successful in 9s
Two changes. They share the `Segment` model, which is why they arrive
together.
## Reddit
A new adapter under /reddit, plus the thread beneath the post -- on Reddit the
conversation is usually the reason the link was shared, so a viewer that showed
only the post would be showing the wrong half.
The `.json` twin of a post URL is the post and the whole first page of comments
in one response, far better than anything the page gives up, so it is the only
layer that normally runs. Reddit refuses it to a browser it has never seen and
answers with a JavaScript challenge, which any ordinary navigation solves by
itself; the adapter navigates once and retries, and the cookie left behind
serves every later post. Below that, `shreddit-comment` elements are read from
the rendered page -- flat, each carrying its own `depth`, so `treeFromDepths`
rebuilds the nesting.
`Post.comments` is a tree rather than a flat list with depths, because folding
a comment has to take everything under it along and nesting is what makes that
free. Each comment renders as a `<details open>`, so collapsing works with the
stylesheet off and from the keyboard, and a collapsed one says how many replies
it is hiding. "Collapse all" is the only part that needs the script, so it
ships hidden and appears once the script has run. What sits behind a "load
more" is not fetched -- that is a second page and often a third -- but it is
counted and said out loud rather than quietly dropped.
Four things the payloads got wrong on the first try, each now with a fixture:
`fallback_url` is the video track alone whenever `has_audio` is true, so a post
with sound has to use `hls_url` and only a silent one gets the proxied MP4;
`scrubber_media_url` looks like a poster and is a second MP4 for the timeline
thumbnails, while the still is in `preview.images`; a gallery's pictures live
in `media_metadata` keyed and unordered, with their order only in
`gallery_data`; and `replies` is the string "" rather than an object when there
are none. Comment bodies are Markdown, rendered by a new render/markdown.ts
that escapes first and then puts back only the constructs we chose to support
-- never Reddit's own `body_html`, which would mean trusting markup a stranger
caused to be generated.
An app share link (/r/<sub>/s/<code>) is a plain 301, so one request told not
to follow it is enough. The permalink it resolves to is what the copy button
hands back, since an opaque share code is a tracking parameter by another name.
## Quoted posts
Both X and Bluesky lifted the quoted post's media out and showed it as the
quoter's own, dropping the quoted words and the quoted author entirely. A quote
of a photo post therefore rendered as somebody else's picture under the wrong
name with nothing to say so, and a quote that had a picture of its own dropped
the quoted one instead -- the two could never both appear. Half the quote posts
people share are someone answering a stranger and the other half are someone
continuing a thought from an earlier post; neither reads with only one side of
it on the page.
`Segment.quoted` now carries the whole thing -- author, words, pictures, time
and a link to it -- and renders as a post inside the post. Neither payload
carries a usable address for it: X has no permalink and it is rebuilt from the
handle and `id_str`, Bluesky has an `at://` URI nobody can open and it is
rebuilt from the handle and the record key. On Bluesky the record sits at
`embed.record` for a plain quote and at `embed.record.record` when the quoting
post has media of its own, and a quote can also point at a feed, a list or a
post since deleted, which arrive in the same slot under a different `$type` --
only `app.bsky.embed.record#viewRecord` is taken.
Two things about X's text, both visible on any post and not only a quote. It
arrives pre-escaped, so an ampersand someone typed was reaching the page as the
literal `&`; it is decoded in the adapter, where the encoding comes from,
leaving the escape-on-the-way-out rule alone. And every link is a `t.co`, which
tells the reader nothing and routes them through X's click tracker to find out
-- `entities.urls` carries the real address alongside, so it is put back. The
shortlink X staples onto the end of a quote post is dropped rather than
expanded, since the post it points at is already on the page;
`display_text_range` is where that boundary is and it keeps a link the author
put there deliberately. Its indices are UTF-16 units into the escaped text, so
the slice happens before decoding and before expanding, and splitting to
codepoints first overshoots past an emoji -- checked against a post carrying
one.
A quote is context, and context that fills the screen has stopped being
context, so a segment carrying one gives up the window-filling cap on its own
media.
## Along the way
setupMedia only ever wired `document.querySelector('.media')`, the first rail
on the page. That was already wrong for a Bluesky or Threads chain with media
in more than one post, and a quoted carousel would have hit it too. It now runs
per rail.
## Verified
108 tests, typecheck and build clean. Resolved end to end against the live
platforms: Reddit self, gallery, video, link and share-link posts; X quotes
with no media, with media on the quoted side, and with media on both; Bluesky
quotes in both embed shapes, including a ten-post chain where every post quotes
a different account and all nine quotes come back under the right name.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017nMQ2eDKnqALYhAibpTKTu
This commit is contained in:
+44
-9
@@ -1,6 +1,6 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { mediaFromEmbed, selfThread, toPost } from '../src/platforms/bluesky.ts';
|
||||
import { mediaFromEmbed, quotedFrom, selfThread, toPost } from '../src/platforms/bluesky.ts';
|
||||
import { fixture } from './helpers.ts';
|
||||
|
||||
const URL_ = 'https://bsky.app/profile/example/post/abc';
|
||||
@@ -45,14 +45,49 @@ test('a quote post with media reaches through the nested embed', () => {
|
||||
assert.deepEqual(media, [{ kind: 'image', url: 'https://example/i.jpg' }]);
|
||||
});
|
||||
|
||||
test('a quote post shows the media of the post it quotes', () => {
|
||||
const media = mediaFromEmbed({
|
||||
$type: 'app.bsky.embed.record#view',
|
||||
record: {
|
||||
embeds: [{ $type: 'app.bsky.embed.images#view', images: [{ fullsize: 'https://cdn/q.jpg' }] }],
|
||||
},
|
||||
});
|
||||
assert.deepEqual(media, [{ kind: 'image', url: 'https://cdn/q.jpg' }]);
|
||||
test('a quote post carries the post it quotes, whose it is included', () => {
|
||||
const segment = toPost(fixture('bluesky/quote.json'), URL_).segments[0];
|
||||
const quoted = segment?.quoted;
|
||||
|
||||
assert.ok(quoted, 'the quoted post must survive');
|
||||
assert.equal(quoted?.author.handle, '@cpsc.gov');
|
||||
assert.equal(quoted?.author.displayName, 'U.S. Consumer Product Safety Commission');
|
||||
assert.match(quoted?.text ?? '', /Longer videos is great/);
|
||||
assert.ok(quoted?.postedAt);
|
||||
// `at://` is not an address anyone can open, so the web one is rebuilt.
|
||||
assert.equal(quoted?.url, 'https://bsky.app/profile/cpsc.gov/post/3mtzkm3hmo222');
|
||||
});
|
||||
|
||||
test('the quoted post keeps its own media instead of lending it to the quoter', () => {
|
||||
const segment = toPost(fixture('bluesky/quote.json'), URL_).segments[0];
|
||||
|
||||
assert.deepEqual(segment?.media, [], 'the quoter attached nothing');
|
||||
assert.equal(segment?.quoted?.media.length, 1);
|
||||
assert.equal(segment?.quoted?.media[0]?.kind, 'video');
|
||||
});
|
||||
|
||||
test('a quote with media of its own nests the quoted record one level deeper', () => {
|
||||
// `recordWithMedia` puts it at `embed.record.record`; a plain quote puts it
|
||||
// at `embed.record`. Reading only the first shape finds nothing here.
|
||||
const quoted = toPost(fixture('bluesky/quote-with-media.json'), URL_).segments[0]?.quoted;
|
||||
assert.equal(quoted?.author.handle, '@quillmatiq.com');
|
||||
assert.match(quoted?.text ?? '', /huge value add/);
|
||||
});
|
||||
|
||||
test('a quote pointing at something that is not a post is left out', () => {
|
||||
// A quote can point at a feed, a list, or a post since deleted or blocked.
|
||||
// They arrive in the same slot under a different type, carrying none of
|
||||
// what a post has.
|
||||
for (const $type of [
|
||||
'app.bsky.embed.record#viewNotFound',
|
||||
'app.bsky.embed.record#viewBlocked',
|
||||
'app.bsky.embed.record#viewDetached',
|
||||
'app.bsky.feed.defs#generatorView',
|
||||
]) {
|
||||
assert.equal(quotedFrom({ $type: 'app.bsky.embed.record#view', record: { $type } }), undefined);
|
||||
}
|
||||
assert.equal(quotedFrom(undefined), undefined);
|
||||
assert.equal(quotedFrom({ $type: 'app.bsky.embed.images#view' }), undefined);
|
||||
});
|
||||
|
||||
// The chain fixture is a real thread. Other people's replies were trimmed to
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"post": {
|
||||
"uri": "at://did:plc:4hodhjl2kposuchzvpiviwps/app.bsky.feed.post/3mrvmbtqn2c24",
|
||||
"author": {
|
||||
"handle": "brittanyellich.com",
|
||||
"displayName": "Brittany Ellich",
|
||||
"avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:4hodhjl2kposuchzvpiviwps/bafkreidaibgqtw5rstbp6vmjvyv3zata5whjmhw2qerrbeqaq3gy7cfyzq"
|
||||
},
|
||||
"record": {
|
||||
"text": "Thanks to this suggestion, you can now schedule updates to @attie.ai pages to build your own personal newsletter about whatever it is you care about ❤️\n\nattie.ai/@brittanyell...",
|
||||
"createdAt": "2026-07-30T23:37:54.005Z"
|
||||
},
|
||||
"embed": {
|
||||
"$type": "app.bsky.embed.recordWithMedia#view",
|
||||
"media": {
|
||||
"$type": "app.bsky.embed.external#view"
|
||||
},
|
||||
"record": {
|
||||
"record": {
|
||||
"$type": "app.bsky.embed.record#viewRecord",
|
||||
"uri": "at://did:plc:xgvzy7ni6ig6ievcbls5jaxe/app.bsky.feed.post/3mrdeskslm222",
|
||||
"author": {
|
||||
"handle": "quillmatiq.com",
|
||||
"displayName": "Anuj Ahooja",
|
||||
"avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:xgvzy7ni6ig6ievcbls5jaxe/bafkreiaumjzoigdvnldguuxu3ytpzwglqgy3dckzg5cxwqcp5yjychrf4e"
|
||||
},
|
||||
"value": {
|
||||
"text": "I think a huge value add here would be to have these pages periodically query for new info and create new versions of the pages, along with a notification (maybe on Bluesky or over email for visibility). A manual refresh button would be a good start!\n\ncc @jay.bsky.team @pfrazee.com",
|
||||
"createdAt": "2026-07-23T17:36:09.832Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"post": {
|
||||
"uri": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3mtzvbrnbds2q",
|
||||
"author": {
|
||||
"handle": "bsky.app",
|
||||
"displayName": "Bluesky",
|
||||
"avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkreihwihm6kpd6zuwhhlro75p5qks5qtrcu55jp3gddbfjsieiv7wuka"
|
||||
},
|
||||
"record": {
|
||||
"text": "NOW PLAYING: \"Beats To Relax / Be Safe To,\" by @cpsc.gov",
|
||||
"createdAt": "2026-08-27T03:19:57.683Z"
|
||||
},
|
||||
"embed": {
|
||||
"$type": "app.bsky.embed.record#view",
|
||||
"record": {
|
||||
"$type": "app.bsky.embed.record#viewRecord",
|
||||
"uri": "at://did:plc:wbmw5r5a2j7txv72ggne2uoe/app.bsky.feed.post/3mtzkm3hmo222",
|
||||
"author": {
|
||||
"handle": "cpsc.gov",
|
||||
"displayName": "U.S. Consumer Product Safety Commission",
|
||||
"avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:wbmw5r5a2j7txv72ggne2uoe/bafkreic44z27sfbvclf2h2xjmsfbglhyqiobn2kigoayodampyvbiml3m4"
|
||||
},
|
||||
"value": {
|
||||
"text": "Longer videos is great.",
|
||||
"createdAt": "2026-08-27T00:08:52.368Z"
|
||||
},
|
||||
"embeds": [
|
||||
{
|
||||
"$type": "app.bsky.embed.video#view",
|
||||
"playlist": "https://video.bsky.app/watch/did%3Aplc%3Awbmw5r5a2j7txv72ggne2uoe/bafkreia7e6rm7lq6k74ttdlbyx25g37eng3llqm7pex4fheah5voznais4/playlist.m3u8",
|
||||
"thumbnail": "https://video.bsky.app/watch/did%3Aplc%3Awbmw5r5a2j7txv72ggne2uoe/bafkreia7e6rm7lq6k74ttdlbyx25g37eng3llqm7pex4fheah5voznais4/thumbnail.jpg",
|
||||
"aspectRatio": {
|
||||
"height": 1080,
|
||||
"width": 1920
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+541
@@ -0,0 +1,541 @@
|
||||
[
|
||||
{
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"title": "Spay day kinda nervy",
|
||||
"author": "Background_Round_853",
|
||||
"subreddit_name_prefixed": "r/aww",
|
||||
"created_utc": 1786959785,
|
||||
"selftext": "",
|
||||
"permalink": "/r/aww/comments/1vqnp2e/spay_day_kinda_nervy/",
|
||||
"url": "https://www.reddit.com/gallery/1vqnp2e",
|
||||
"url_overridden_by_dest": "https://www.reddit.com/gallery/1vqnp2e",
|
||||
"domain": "reddit.com",
|
||||
"is_self": false,
|
||||
"is_video": false,
|
||||
"is_gallery": true,
|
||||
"over_18": false,
|
||||
"spoiler": false,
|
||||
"num_comments": 443,
|
||||
"score": 40774,
|
||||
"gallery_data": {
|
||||
"items": [
|
||||
{
|
||||
"is_deleted": false,
|
||||
"media_id": "6xfm6s4wowjh1",
|
||||
"id": 1020169008
|
||||
},
|
||||
{
|
||||
"is_deleted": false,
|
||||
"media_id": "x1o3cr4wowjh1",
|
||||
"id": 1020169009
|
||||
},
|
||||
{
|
||||
"is_deleted": false,
|
||||
"media_id": "kury9t4wowjh1",
|
||||
"id": 1020169010
|
||||
},
|
||||
{
|
||||
"is_deleted": false,
|
||||
"media_id": "v07ubs4wowjh1",
|
||||
"id": 1020169011
|
||||
}
|
||||
]
|
||||
},
|
||||
"media_metadata": {
|
||||
"v07ubs4wowjh1": {
|
||||
"status": "valid",
|
||||
"e": "Image",
|
||||
"m": "image/jpg",
|
||||
"p": [
|
||||
{
|
||||
"y": 155,
|
||||
"x": 108,
|
||||
"u": "https://preview.redd.it/v07ubs4wowjh1.jpg?width=108&crop=smart&auto=webp&s=aa2a1ab3c453178f9ba6b8a06800c0fa923ad570"
|
||||
},
|
||||
{
|
||||
"y": 311,
|
||||
"x": 216,
|
||||
"u": "https://preview.redd.it/v07ubs4wowjh1.jpg?width=216&crop=smart&auto=webp&s=ec1e4d46b41511ffb3c27e2e331d23469c6be827"
|
||||
},
|
||||
{
|
||||
"y": 461,
|
||||
"x": 320,
|
||||
"u": "https://preview.redd.it/v07ubs4wowjh1.jpg?width=320&crop=smart&auto=webp&s=22812e3829549a461bdbcb81232a8edf752cac10"
|
||||
},
|
||||
{
|
||||
"y": 922,
|
||||
"x": 640,
|
||||
"u": "https://preview.redd.it/v07ubs4wowjh1.jpg?width=640&crop=smart&auto=webp&s=56315dc4cf19d12e6f19401792a059f70314ef04"
|
||||
},
|
||||
{
|
||||
"y": 1383,
|
||||
"x": 960,
|
||||
"u": "https://preview.redd.it/v07ubs4wowjh1.jpg?width=960&crop=smart&auto=webp&s=b706a7d2164462251f869651679c3635e15b6d25"
|
||||
},
|
||||
{
|
||||
"y": 1555,
|
||||
"x": 1080,
|
||||
"u": "https://preview.redd.it/v07ubs4wowjh1.jpg?width=1080&crop=smart&auto=webp&s=e5af0cc235a7b2583d8e8a87f7799607e41f1f23"
|
||||
}
|
||||
],
|
||||
"s": {
|
||||
"y": 1736,
|
||||
"x": 1205,
|
||||
"u": "https://preview.redd.it/v07ubs4wowjh1.jpg?width=1205&format=pjpg&auto=webp&s=d5f2f69da610bf9a9b5b7cb99f94c21d21d4e75e"
|
||||
},
|
||||
"id": "v07ubs4wowjh1"
|
||||
},
|
||||
"6xfm6s4wowjh1": {
|
||||
"status": "valid",
|
||||
"e": "Image",
|
||||
"m": "image/jpg",
|
||||
"p": [
|
||||
{
|
||||
"y": 140,
|
||||
"x": 108,
|
||||
"u": "https://preview.redd.it/6xfm6s4wowjh1.jpg?width=108&crop=smart&auto=webp&s=231c0253d9ffcadcfa085c55264f968e1be7cd38"
|
||||
},
|
||||
{
|
||||
"y": 281,
|
||||
"x": 216,
|
||||
"u": "https://preview.redd.it/6xfm6s4wowjh1.jpg?width=216&crop=smart&auto=webp&s=efb67b41fa2afbde45b4fbf3ff389cb7c98cbe62"
|
||||
},
|
||||
{
|
||||
"y": 417,
|
||||
"x": 320,
|
||||
"u": "https://preview.redd.it/6xfm6s4wowjh1.jpg?width=320&crop=smart&auto=webp&s=4709db023af013805c8a0e4d64fdde867900df2c"
|
||||
},
|
||||
{
|
||||
"y": 834,
|
||||
"x": 640,
|
||||
"u": "https://preview.redd.it/6xfm6s4wowjh1.jpg?width=640&crop=smart&auto=webp&s=2a43ebc507dc45cf578f7e74b62b6d37861d4f18"
|
||||
},
|
||||
{
|
||||
"y": 1252,
|
||||
"x": 960,
|
||||
"u": "https://preview.redd.it/6xfm6s4wowjh1.jpg?width=960&crop=smart&auto=webp&s=f8f82d7392e30f1fb0d449489972ee2cd2e77d1f"
|
||||
},
|
||||
{
|
||||
"y": 1408,
|
||||
"x": 1080,
|
||||
"u": "https://preview.redd.it/6xfm6s4wowjh1.jpg?width=1080&crop=smart&auto=webp&s=d438c9c25573da6b99ec89d26b373cf676d0a727"
|
||||
}
|
||||
],
|
||||
"s": {
|
||||
"y": 1572,
|
||||
"x": 1205,
|
||||
"u": "https://preview.redd.it/6xfm6s4wowjh1.jpg?width=1205&format=pjpg&auto=webp&s=ce8cff5c5c1d347a25c3c4431290fa76637f85b1"
|
||||
},
|
||||
"id": "6xfm6s4wowjh1"
|
||||
},
|
||||
"kury9t4wowjh1": {
|
||||
"status": "valid",
|
||||
"e": "Image",
|
||||
"m": "image/jpg",
|
||||
"p": [
|
||||
{
|
||||
"y": 141,
|
||||
"x": 108,
|
||||
"u": "https://preview.redd.it/kury9t4wowjh1.jpg?width=108&crop=smart&auto=webp&s=cca8d53fa39835bc14cfcb0b70f2409d2c0b41fd"
|
||||
},
|
||||
{
|
||||
"y": 283,
|
||||
"x": 216,
|
||||
"u": "https://preview.redd.it/kury9t4wowjh1.jpg?width=216&crop=smart&auto=webp&s=5c196d8c1c5395b4388548729d779a74d011a6a7"
|
||||
},
|
||||
{
|
||||
"y": 420,
|
||||
"x": 320,
|
||||
"u": "https://preview.redd.it/kury9t4wowjh1.jpg?width=320&crop=smart&auto=webp&s=6760b50b63edbf681e13739bbccc17551dbcacba"
|
||||
},
|
||||
{
|
||||
"y": 840,
|
||||
"x": 640,
|
||||
"u": "https://preview.redd.it/kury9t4wowjh1.jpg?width=640&crop=smart&auto=webp&s=4cf3bb5bd6c53e6d9a8fff6560258da7fb9de4e1"
|
||||
},
|
||||
{
|
||||
"y": 1261,
|
||||
"x": 960,
|
||||
"u": "https://preview.redd.it/kury9t4wowjh1.jpg?width=960&crop=smart&auto=webp&s=7c214c8d1a7a951b891b08d43cf445c28cb6ac20"
|
||||
},
|
||||
{
|
||||
"y": 1418,
|
||||
"x": 1080,
|
||||
"u": "https://preview.redd.it/kury9t4wowjh1.jpg?width=1080&crop=smart&auto=webp&s=4eca14b69a68d05f78a5c6319867b4102fa3abfb"
|
||||
}
|
||||
],
|
||||
"s": {
|
||||
"y": 1583,
|
||||
"x": 1205,
|
||||
"u": "https://preview.redd.it/kury9t4wowjh1.jpg?width=1205&format=pjpg&auto=webp&s=24c97654ddd1d3bee61788a965223a9e40d92d6e"
|
||||
},
|
||||
"id": "kury9t4wowjh1"
|
||||
},
|
||||
"x1o3cr4wowjh1": {
|
||||
"status": "valid",
|
||||
"e": "Image",
|
||||
"m": "image/jpg",
|
||||
"p": [
|
||||
{
|
||||
"y": 80,
|
||||
"x": 108,
|
||||
"u": "https://preview.redd.it/x1o3cr4wowjh1.jpg?width=108&crop=smart&auto=webp&s=8a3211796cdf827ccdc39d76ff9aab299c4cbe2f"
|
||||
},
|
||||
{
|
||||
"y": 160,
|
||||
"x": 216,
|
||||
"u": "https://preview.redd.it/x1o3cr4wowjh1.jpg?width=216&crop=smart&auto=webp&s=52c19a8c35965fccd08e4aaa7bb22f9aae22549e"
|
||||
},
|
||||
{
|
||||
"y": 237,
|
||||
"x": 320,
|
||||
"u": "https://preview.redd.it/x1o3cr4wowjh1.jpg?width=320&crop=smart&auto=webp&s=c54e14e1989b0c1a67708fda3fe611da15f52414"
|
||||
},
|
||||
{
|
||||
"y": 474,
|
||||
"x": 640,
|
||||
"u": "https://preview.redd.it/x1o3cr4wowjh1.jpg?width=640&crop=smart&auto=webp&s=4b7d4b60bb388c485024efe867466f6e5aba6fad"
|
||||
},
|
||||
{
|
||||
"y": 711,
|
||||
"x": 960,
|
||||
"u": "https://preview.redd.it/x1o3cr4wowjh1.jpg?width=960&crop=smart&auto=webp&s=b0e46c04be74d31c5b00c0a704638888a4ca6781"
|
||||
},
|
||||
{
|
||||
"y": 800,
|
||||
"x": 1080,
|
||||
"u": "https://preview.redd.it/x1o3cr4wowjh1.jpg?width=1080&crop=smart&auto=webp&s=c9bb5e091d769c8ec1360a83b6164117406641c8"
|
||||
}
|
||||
],
|
||||
"s": {
|
||||
"y": 893,
|
||||
"x": 1205,
|
||||
"u": "https://preview.redd.it/x1o3cr4wowjh1.jpg?width=1205&format=pjpg&auto=webp&s=20fbd865e429feb527bddb0b7f4bed3fc3281075"
|
||||
},
|
||||
"id": "x1o3cr4wowjh1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Background_Round_853",
|
||||
"body": "https://preview.redd.it/d1m4kf45pwjh1.jpeg?width=1206&format=pjpg&auto=webp&s=6ccd095185d203c6569514e4b0bf93ed9de3ed2c\n\nDone ✅✅ she looks DEVASTATED",
|
||||
"created_utc": 1786959888,
|
||||
"score": 6583,
|
||||
"score_hidden": false,
|
||||
"is_submitter": true,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Fritzkreig",
|
||||
"body": "Ah, may your lady and you share mutual joy for the years to come!",
|
||||
"created_utc": 1786960145,
|
||||
"score": 943,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "malk600",
|
||||
"body": "She's coming off anaesthesia, idk what the SOC is in your country, but kitteh would typically get dexmedetomidine + opioid + something like ketamine. Vets will typically revert dexmedetomidine w/ atipamezol, which is about as mild and pleasant as being woken up by a swift kick in the nuts. The cat is just tired, confused, restless (it's possible she's going to be woozy and tired as hell but won't be able to sleep) and overall feels like a person would feel after doing a heavy night of things that are supposed to stay in Vegas.\n\nTake good care of her!",
|
||||
"created_utc": 1786961915,
|
||||
"score": 1298,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "teadazed",
|
||||
"body": "Yes, I showed up to collect my two (brother and sister cats) and commented on their weirdass pupils. The nurse said fondly \"yeah... that's from all the drugs we've given them\".\nThey were both batting invisible butterflies the rest of the evening.",
|
||||
"created_utc": 1786966670,
|
||||
"score": 449,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "rocket_randall",
|
||||
"body": "It's been a while since we had ours fixed, but we opted for a slow release pain killer that would keep them comfy for ~3 days after surgery while they healed. Of course our lunatics decided that once they were home and feeling no pain that it was time to lose their minds and nothing on earth could stop them running around and crashing into things. There was a bit of a scare after our little guy crashed into something and our daughter saw what looked like an eye injury, so off we went to the emergency vet just to be told that there was no injury, it was just the lubricant that was put on his eyes before surgery breaking up and he was fine. As a small consolation the vet said he was a huge endorphin hit for the staff at that time of night.",
|
||||
"created_utc": 1786983276,
|
||||
"score": 28,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "teadazed",
|
||||
"body": "Oh of course they must have eye drops during surgery, their eyes stay open don't they? It's the right way round to take them in for something that turns out to be nothing.\n\nIt's funny, I also have a collie who unlike the cats did not enjoy the aftermath of her neutering drugs At All, she was a haunted-looking cuddlebug until she could move properly again.",
|
||||
"created_utc": 1786984556,
|
||||
"score": 18,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "more",
|
||||
"data": {
|
||||
"count": 1,
|
||||
"id": "p4dtjha"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Ashkir",
|
||||
"body": "Some of the funniest things my dog has ever done was after they were waking up form anesthesia. She decided on a new place to potty that day too and to this day, years later, I can’t train that spot out of her too 👀😭😂",
|
||||
"created_utc": 1786975001,
|
||||
"score": 112,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Winter_Fall_7066",
|
||||
"body": "My dog was given a small sedative so the vet could witness a limp (he was excited and all over the place despite being in pain). Got home, ex asked “is the dog fucked up?” Dog immediately walks over to Christmas tree and pees on it. ",
|
||||
"created_utc": 1786986392,
|
||||
"score": 73,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Constant_Function238",
|
||||
"body": "In his brain”thanks family for bringing my potty inside.”",
|
||||
"created_utc": 1786990338,
|
||||
"score": 28,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Pr0digy_",
|
||||
"body": "My friend years ago dropped off his mastiff to get neutered and I was the one to pick him up because my friend was working late, he was dopey and groggy but really happy to see me. I took him home and watched him until my friend came home as soon as my friend entered the house the dog growled (nothing crazy) and went and hid behind me it was hilarious. Everything was forgiven with some treats and pets. ",
|
||||
"created_utc": 1787001797,
|
||||
"score": 19,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "werewooferer",
|
||||
"body": "the dog knew who took him, and knew who saved him 🤣",
|
||||
"created_utc": 1787167461,
|
||||
"score": 3,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "more",
|
||||
"data": {
|
||||
"count": 1,
|
||||
"id": "p4c9qw2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "chickens_for_laughs",
|
||||
"body": "I remember that vet telling me to not let her jump onto furniture. Like I could get her to stop. She healed fine and lived to 20!",
|
||||
"created_utc": 1786979935,
|
||||
"score": 22,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "cAt_S0fa",
|
||||
"body": "Lol yes the vet said the same thing to my mother when she got my childhood cat spayed. That night the cat was climbing the curtains...\n",
|
||||
"created_utc": 1787000485,
|
||||
"score": 3,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "more",
|
||||
"data": {
|
||||
"count": 21,
|
||||
"id": "p49jamx"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "more",
|
||||
"data": {
|
||||
"count": 24,
|
||||
"id": "p4cqjj6"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Shailo",
|
||||
"body": "What a precious baby 🥺 those eyes omg. Wishing her a speedy recovery and lots of cuddles and treats in the meantime 💖",
|
||||
"created_utc": 1786960262,
|
||||
"score": 667,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Trenzane",
|
||||
"body": "I misread it as Spa day and was like ❤️ \n\nThen my brain worked and went ☹️",
|
||||
"created_utc": 1786962348,
|
||||
"score": 875,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "klgall1",
|
||||
"body": "Glad I'm not the only one! I was reading the comments and saw the discussions about all the drugs and was very confused.\n\nI am not awake yet haha.",
|
||||
"created_utc": 1786969784,
|
||||
"score": 89,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Cyber_Mermaid",
|
||||
"body": "I still thought this said \"spa day\" until I read this comment lol",
|
||||
"created_utc": 1786985184,
|
||||
"score": 23,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "more",
|
||||
"data": {
|
||||
"count": 8,
|
||||
"id": "p48kfhn"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "more",
|
||||
"data": {
|
||||
"count": 195,
|
||||
"id": "p4c6cph"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
Vendored
+102
@@ -0,0 +1,102 @@
|
||||
[
|
||||
{
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"title": "Ratko Mladic, Murderous ‘Butcher of Bosnia,’ Dies at 83",
|
||||
"author": "Knightro829",
|
||||
"subreddit_name_prefixed": "r/news",
|
||||
"created_utc": 1787836039,
|
||||
"selftext": "",
|
||||
"permalink": "/r/news/comments/1vzthbh/ratko_mladic_murderous_butcher_of_bosnia_dies_at/",
|
||||
"url": "https://www.nytimes.com/2026/08/27/world/europe/ratko-mladic-dead.html?unlocked_article_code=1.8lA.Wgnd.qCJl7Bw9IdHl&smid=nytcore-ios-share",
|
||||
"url_overridden_by_dest": "https://www.nytimes.com/2026/08/27/world/europe/ratko-mladic-dead.html?unlocked_article_code=1.8lA.Wgnd.qCJl7Bw9IdHl&smid=nytcore-ios-share",
|
||||
"domain": "nytimes.com",
|
||||
"is_self": false,
|
||||
"is_video": false,
|
||||
"over_18": false,
|
||||
"spoiler": false,
|
||||
"num_comments": 32,
|
||||
"score": 187
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Knightro829",
|
||||
"body": "Well, at least Death is getting closer to the mark after the past couple of days…. \n \nEdit: If you have several hours on your hands, go watch a.) The Death of Yugoslavia, a six-part documentary that aired on BBC in 1995-1996 (all episodes available on YouTube), and b.) PBS Frontline’s “The Trial of Ratko Mladic”, also available on YouTube.",
|
||||
"created_utc": 1787836109,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": true,
|
||||
"replies": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "HungryCurrency8481",
|
||||
"body": "Dolly Parton, Tim Curry, Ratko Mladic - what a trio ",
|
||||
"created_utc": 1787836074,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "kataiga",
|
||||
"body": "Unfortunately death also got Yayoi Kusama last night…",
|
||||
"created_utc": 1787836540,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "RhoOfFeh",
|
||||
"body": "He wasn't 80. I am holding out for completion of this deal.",
|
||||
"created_utc": 1787836843,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "SheikhMahdeek",
|
||||
"body": "See? Bad people live to ripe old age too",
|
||||
"created_utc": 1787836434,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
Vendored
+351
@@ -0,0 +1,351 @@
|
||||
[
|
||||
{
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"title": "What's a healthy food that pleases the taste buds too?",
|
||||
"author": "Weary_Discipline_178",
|
||||
"subreddit_name_prefixed": "r/AskReddit",
|
||||
"created_utc": 1787833594,
|
||||
"selftext": "",
|
||||
"permalink": "/r/AskReddit/comments/1vzsjee/whats_a_healthy_food_that_pleases_the_taste_buds/",
|
||||
"url": "https://www.reddit.com/r/AskReddit/comments/1vzsjee/whats_a_healthy_food_that_pleases_the_taste_buds/",
|
||||
"domain": "self.AskReddit",
|
||||
"is_self": true,
|
||||
"is_video": false,
|
||||
"over_18": false,
|
||||
"spoiler": false,
|
||||
"num_comments": 127,
|
||||
"score": 94
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "manny221122",
|
||||
"body": "greek yogurt with berries, honey and a little granola 😭 it tastes suspiciously close to dessert and somehow i get to pretend i have my life together while eating it",
|
||||
"created_utc": 1787835955,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "more",
|
||||
"data": {
|
||||
"count": 1,
|
||||
"id": "p67it4u"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "CoconutxKitten",
|
||||
"body": "The vast majority of fruit ",
|
||||
"created_utc": 1787833703,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Technical_Buy_8198",
|
||||
"body": "Currently cant get enough of a crunchy green grape. Almost ate an entire pack but had to stop myself. So good",
|
||||
"created_utc": 1787833833,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "CoconutxKitten",
|
||||
"body": "I love when they’re crunchy & slightly tart ",
|
||||
"created_utc": 1787834158,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Ok_Reference_4860",
|
||||
"body": "Yeppp and so delicious ",
|
||||
"created_utc": 1787835229,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "deerhuntingdude",
|
||||
"body": "Grapes are pretty low in fiber but high in sugar. They're good for you, but definitely only in moderation ",
|
||||
"created_utc": 1787834561,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "random314",
|
||||
"body": "Mangoes specifically.\n\nAlso watermelon.",
|
||||
"created_utc": 1787834693,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "CoconutxKitten",
|
||||
"body": "Watermelon is unhealthy specifically for me because I’m allergic 😂",
|
||||
"created_utc": 1787834743,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "more",
|
||||
"data": {
|
||||
"count": 2,
|
||||
"id": "p67dyiw"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "AngryBarista",
|
||||
"body": "Chili",
|
||||
"created_utc": 1787833749,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "VillagePlayful416",
|
||||
"body": "good chili is basically a cheat code, you can dump a ton of veg in there and it still tastes like a proper meal\n\n \ni slow cook mine with black beans, capsicum, shredded carrot and sometimes a bit of sweet potato, the smoked paprika does all the heavy lifting\n\n \nmake a huge batch sunday and you're set for the week",
|
||||
"created_utc": 1787833937,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "NOT_Frank_or_Joe",
|
||||
"body": "A winter staple for me. I also add beets, cut them up small and cook fully on the side then in to simmer with the rest. \n\nEdit: forgot to add, replace the beef with ground turkey as well.",
|
||||
"created_utc": 1787834097,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "AngryBarista",
|
||||
"body": "Every time I make it, i look at my wife and say \"this has no business being as good as it is for how healthy it is. Just packed with protein and fiber and can have minimal fat if you use a lean protein",
|
||||
"created_utc": 1787834149,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Klutzy-Client",
|
||||
"body": "But triple the amount of beans in it and we are talking",
|
||||
"created_utc": 1787836492,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "kitty_swankiss",
|
||||
"body": "Everything Nuts in moderation 🥜🌰",
|
||||
"created_utc": 1787833760,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "jefe_gonna_jefe",
|
||||
"body": "Not me.",
|
||||
"created_utc": 1787833889,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "BlueEyedMalachi",
|
||||
"body": "Nice",
|
||||
"created_utc": 1787833961,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Lettuce_Milk",
|
||||
"body": "Either you got all the nut or none at all 😎",
|
||||
"created_utc": 1787836058,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "more",
|
||||
"data": {
|
||||
"count": 2,
|
||||
"id": "p67a17t"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Chicken-picante",
|
||||
"body": "What about odyssey?\n\nOdyssey deez nutz",
|
||||
"created_utc": 1787834927,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Perfectenschlag_",
|
||||
"body": "Boooooo",
|
||||
"created_utc": 1787836540,
|
||||
"score": 1,
|
||||
"score_hidden": true,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "more",
|
||||
"data": {
|
||||
"count": 10,
|
||||
"id": "p677sc7"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
Vendored
+451
@@ -0,0 +1,451 @@
|
||||
[
|
||||
{
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"title": "1957 Ford Fairlane",
|
||||
"author": "icleanjaxfl",
|
||||
"subreddit_name_prefixed": "r/oddlysatisfying",
|
||||
"created_utc": 1787830536,
|
||||
"selftext": "",
|
||||
"permalink": "/r/oddlysatisfying/comments/1vzrfkw/1957_ford_fairlane/",
|
||||
"url": "https://v.redd.it/74hudhx2mwlh1",
|
||||
"url_overridden_by_dest": "https://v.redd.it/74hudhx2mwlh1",
|
||||
"domain": "v.redd.it",
|
||||
"post_hint": "hosted:video",
|
||||
"is_self": false,
|
||||
"is_video": true,
|
||||
"over_18": false,
|
||||
"spoiler": false,
|
||||
"num_comments": 109,
|
||||
"score": 3477,
|
||||
"secure_media": {
|
||||
"reddit_video": {
|
||||
"bitrate_kbps": 2400,
|
||||
"fallback_url": "https://v.redd.it/74hudhx2mwlh1/CMAF_720.mp4?source=fallback",
|
||||
"has_audio": false,
|
||||
"height": 1280,
|
||||
"width": 720,
|
||||
"scrubber_media_url": "https://v.redd.it/74hudhx2mwlh1/CMAF_96.mp4",
|
||||
"dash_url": "https://v.redd.it/74hudhx2mwlh1/DASHPlaylist.mpd?a=1790429193%2CNjQwMTBmMDMxODhjMGViOGUyMjU2ZDE2NGFkNmI2MDU2ZDQ2NmVjNmEzNzg0MTQzNGVkMTU5OGI2ZTgyYTU3ZQ%3D%3D&v=1&f=sd",
|
||||
"duration": 25,
|
||||
"hls_url": "https://v.redd.it/74hudhx2mwlh1/HLSPlaylist.m3u8?a=1790429193%2CZjkwZmQwODJiYmRhNGY2NDkyYTMxMDZlYmVhMzQwYWVkYTg0MzMwOWEwYjAyMGZhMzY0MWJmYTIyMmE0OTQ3Zg%3D%3D&v=1&f=sd",
|
||||
"is_gif": false,
|
||||
"transcoding_status": "completed"
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"reddit_video": {
|
||||
"bitrate_kbps": 2400,
|
||||
"fallback_url": "https://v.redd.it/74hudhx2mwlh1/CMAF_720.mp4?source=fallback",
|
||||
"has_audio": false,
|
||||
"height": 1280,
|
||||
"width": 720,
|
||||
"scrubber_media_url": "https://v.redd.it/74hudhx2mwlh1/CMAF_96.mp4",
|
||||
"dash_url": "https://v.redd.it/74hudhx2mwlh1/DASHPlaylist.mpd?a=1790429193%2CNjQwMTBmMDMxODhjMGViOGUyMjU2ZDE2NGFkNmI2MDU2ZDQ2NmVjNmEzNzg0MTQzNGVkMTU5OGI2ZTgyYTU3ZQ%3D%3D&v=1&f=sd",
|
||||
"duration": 25,
|
||||
"hls_url": "https://v.redd.it/74hudhx2mwlh1/HLSPlaylist.m3u8?a=1790429193%2CZjkwZmQwODJiYmRhNGY2NDkyYTMxMDZlYmVhMzQwYWVkYTg0MzMwOWEwYjAyMGZhMzY0MWJmYTIyMmE0OTQ3Zg%3D%3D&v=1&f=sd",
|
||||
"is_gif": false,
|
||||
"transcoding_status": "completed"
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
"images": [
|
||||
{
|
||||
"source": {
|
||||
"url": "https://external-preview.redd.it/N245aGFidjJtd2xoMRGGAvg1khFB1AQL8vVunwC_Mb5qnXHCQGxm0j4C5bdR.png?format=pjpg&auto=webp&s=cd60e451633a907b172389c0cd1c09e174ac9f16",
|
||||
"width": 405,
|
||||
"height": 720
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Head-State-",
|
||||
"body": "That retractable hardtop transformer mechanism is pure engineering wizardry.",
|
||||
"created_utc": 1787830965,
|
||||
"score": 328,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Pretend-Internet-625",
|
||||
"body": "ya until you have to go and fix it and make it work properly. ",
|
||||
"created_utc": 1787832356,
|
||||
"score": 150,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "bigalindahouse",
|
||||
"body": "Would much rather work on this than today's vehicles",
|
||||
"created_utc": 1787833724,
|
||||
"score": 63,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Pretend-Internet-625",
|
||||
"body": "Well ya I get that but these were a real pain in the ",
|
||||
"created_utc": 1787833961,
|
||||
"score": 6,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Brasticus",
|
||||
"body": "r/redditsniper claims another",
|
||||
"created_utc": 1787834765,
|
||||
"score": 21,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Andy_B_Goode",
|
||||
"body": "Yeah, it's neat, but it also totally looks like one of those things that would break in the first two or three years and most people wouldn't bother fixing",
|
||||
"created_utc": 1787835341,
|
||||
"score": 6,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "utukore",
|
||||
"body": "Same setup was used for the e93 roof. Other than the squeeks and rattles or the slow open/ close time they worked well.",
|
||||
"created_utc": 1787836411,
|
||||
"score": 3,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "kss1089",
|
||||
"body": "Everything is a 4 bar mechanism. if it's super complicated, it's multiple 4 bar mechanisms. ",
|
||||
"created_utc": 1787833936,
|
||||
"score": 12,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Tony_Penny",
|
||||
"body": "Ford Fairlane, rock and roll detective. ",
|
||||
"created_utc": 1787831095,
|
||||
"score": 29,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "perflubon",
|
||||
"body": "One of the best \"worst\" movies ever.",
|
||||
"created_utc": 1787832767,
|
||||
"score": 7,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Tony_Penny",
|
||||
"body": "I absolutely loved that movie as a kid. ",
|
||||
"created_utc": 1787833553,
|
||||
"score": 3,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "imamakebaddecisions",
|
||||
"body": "Booty time, booty time, across the USA, booty time, booty time, hey hey hey!",
|
||||
"created_utc": 1787833566,
|
||||
"score": 5,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "[deleted]",
|
||||
"body": "[removed]",
|
||||
"created_utc": 1787833134,
|
||||
"score": 145,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Bongressman",
|
||||
"body": "And covering it for the winter... then uncovering it in the Spring. Getting that \"unwrapping a new gift\" endorphin rush all over again every year.",
|
||||
"created_utc": 1787834518,
|
||||
"score": 8,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "zytukin",
|
||||
"body": "You'd leave it outside over winter???",
|
||||
"created_utc": 1787834574,
|
||||
"score": 4,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Bongressman",
|
||||
"body": "I live in Seattle, so snow isnt a factor. Streets are dotted with covered cars in the winter. I guess it depends on location!",
|
||||
"created_utc": 1787834801,
|
||||
"score": 1,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Flyingdutchman2305",
|
||||
"body": "?? Isnt there lots of snow in seattle",
|
||||
"created_utc": 1787835019,
|
||||
"score": 1,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Bongressman",
|
||||
"body": "No. It doesn't snow in Seattle. We don't even have a snow removal budget in the city.\n\nWinter bottoms out at like 40-45 fahrenheit. It just mists, drizzles, gets overcast and grey for six months.",
|
||||
"created_utc": 1787835128,
|
||||
"score": 2,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Flyingdutchman2305",
|
||||
"body": "Huh, my american Geography when it comes to latitude is apparently off\n\n",
|
||||
"created_utc": 1787835286,
|
||||
"score": 1,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Bongressman",
|
||||
"body": "It snows in Washington state, just not Seattle. We are surrounded by the Cascades and Olympic Mountains. Kind of boxed in and protected.\n\nOur weather closely mirrors London's, but with even less snowfall.",
|
||||
"created_utc": 1787835622,
|
||||
"score": 1,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": {
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "Flyingdutchman2305",
|
||||
"body": "Never knew, always thought it was one of those typical snowy winter cities",
|
||||
"created_utc": 1787835875,
|
||||
"score": 1,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "zytukin",
|
||||
"body": "It's not due to latitude, Seattle is more north than Wyoming, Kansas, etc which get a lot of snow.\n\nI think it has to do with being near the west coast so it gets hit with warm air coming up from the tropics.",
|
||||
"created_utc": 1787835832,
|
||||
"score": 1,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t1",
|
||||
"data": {
|
||||
"author": "its_Raze_7",
|
||||
"body": "bot",
|
||||
"created_utc": 1787836203,
|
||||
"score": 2,
|
||||
"score_hidden": false,
|
||||
"is_submitter": false,
|
||||
"replies": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "more",
|
||||
"data": {
|
||||
"count": 1,
|
||||
"id": "p676rzl"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
Vendored
+128
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"id_str": "2086902906171605466",
|
||||
"text": "NEW: Joey McGuire tells @Brett_McMurphy Texas Tech can't find future games, with Texas A&M, USC, NC State and Mississippi State saying no or canceling series❌\n\n\"We’re talking to everybody and anybody. So far, everybody’s told us no.\"\n\nhttps://t.co/rEGx9JN5pJ https://t.co/QdJhOVu4En",
|
||||
"display_text_range": [
|
||||
0,
|
||||
262
|
||||
],
|
||||
"entities": {
|
||||
"urls": [
|
||||
{
|
||||
"url": "https://t.co/rEGx9JN5pJ",
|
||||
"expanded_url": "https://www.on3.com/news/joey-mcguire-on-texas-techs-villain-era-were-the-most-hated-team-in-college-football/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"created_at": "2026-08-10T19:50:00.000Z",
|
||||
"user": {
|
||||
"screen_name": "On3",
|
||||
"name": "On3",
|
||||
"profile_image_url_https": "https://pbs.twimg.com/profile_images/1414989749203390470/D2B25n0r_normal.jpg"
|
||||
},
|
||||
"mediaDetails": [
|
||||
{
|
||||
"type": "photo",
|
||||
"media_url_https": "https://pbs.twimg.com/media/HPYnDNPW0AAJd6h.jpg",
|
||||
"original_info": {
|
||||
"focus_rects": [
|
||||
{
|
||||
"h": 605,
|
||||
"w": 1080,
|
||||
"x": 0,
|
||||
"y": 609
|
||||
},
|
||||
{
|
||||
"h": 1080,
|
||||
"w": 1080,
|
||||
"x": 0,
|
||||
"y": 270
|
||||
},
|
||||
{
|
||||
"h": 1231,
|
||||
"w": 1080,
|
||||
"x": 0,
|
||||
"y": 119
|
||||
},
|
||||
{
|
||||
"h": 1350,
|
||||
"w": 675,
|
||||
"x": 169,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"h": 1350,
|
||||
"w": 1080,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
],
|
||||
"height": 1350,
|
||||
"width": 1080
|
||||
}
|
||||
}
|
||||
],
|
||||
"quoted_tweet": {
|
||||
"id_str": "2086832880676716740",
|
||||
"text": "NEW: Texas Tech's Joey McGuire tells @Brett_McMurphy the Red Raiders are embracing the villain role🔥\n\n\"We’re the most hated team in college football. And I absolutely love it,\" McGuire told his team in a meeting.\n\nStory: https://t.co/rEGx9JN5pJ https://t.co/q7m39MOMNo",
|
||||
"display_text_range": [
|
||||
0,
|
||||
245
|
||||
],
|
||||
"entities": {
|
||||
"urls": [
|
||||
{
|
||||
"url": "https://t.co/rEGx9JN5pJ",
|
||||
"expanded_url": "https://www.on3.com/news/joey-mcguire-on-texas-techs-villain-era-were-the-most-hated-team-in-college-football/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"created_at": "2026-08-10T15:11:44.000Z",
|
||||
"user": {
|
||||
"screen_name": "On3",
|
||||
"name": "On3",
|
||||
"profile_image_url_https": "https://pbs.twimg.com/profile_images/1414989749203390470/D2B25n0r_normal.jpg"
|
||||
},
|
||||
"mediaDetails": [
|
||||
{
|
||||
"type": "photo",
|
||||
"media_url_https": "https://pbs.twimg.com/media/HPXqebKWkAA5gvA.jpg",
|
||||
"original_info": {
|
||||
"focus_rects": [
|
||||
{
|
||||
"h": 630,
|
||||
"w": 1125,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"h": 630,
|
||||
"w": 630,
|
||||
"x": 75,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"h": 630,
|
||||
"w": 553,
|
||||
"x": 114,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"h": 630,
|
||||
"w": 315,
|
||||
"x": 233,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"h": 630,
|
||||
"w": 1200,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
],
|
||||
"height": 630,
|
||||
"width": 1200
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Vendored
+70
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"id_str": "2092384141488620017",
|
||||
"text": "The SEC, as expected, adopts a ban on professional players returning to college, which also includes basketball.",
|
||||
"display_text_range": [
|
||||
0,
|
||||
112
|
||||
],
|
||||
"created_at": "2026-08-25T22:50:28.000Z",
|
||||
"user": {
|
||||
"screen_name": "RossDellenger",
|
||||
"name": "Ross Dellenger",
|
||||
"profile_image_url_https": "https://pbs.twimg.com/profile_images/1174672001958649861/m2N_Dbim_normal.jpg"
|
||||
},
|
||||
"quoted_tweet": {
|
||||
"id_str": "2092383884872765473",
|
||||
"text": "Following meetings with the Presidents, Chancellors, and Athletics Directors of the Southeastern Conference, the SEC will enforce the following policy: https://t.co/LohWnb7G1J",
|
||||
"display_text_range": [
|
||||
0,
|
||||
151
|
||||
],
|
||||
"created_at": "2026-08-25T22:49:27.000Z",
|
||||
"user": {
|
||||
"screen_name": "SEC",
|
||||
"name": "Southeastern Conference",
|
||||
"profile_image_url_https": "https://pbs.twimg.com/profile_images/1807664314687299584/qhj7cnXy_normal.jpg"
|
||||
},
|
||||
"mediaDetails": [
|
||||
{
|
||||
"type": "photo",
|
||||
"media_url_https": "https://pbs.twimg.com/media/HQmj_lxW8AAlFmc.jpg",
|
||||
"original_info": {
|
||||
"focus_rects": [
|
||||
{
|
||||
"h": 605,
|
||||
"w": 1080,
|
||||
"x": 0,
|
||||
"y": 742
|
||||
},
|
||||
{
|
||||
"h": 1080,
|
||||
"w": 1080,
|
||||
"x": 0,
|
||||
"y": 360
|
||||
},
|
||||
{
|
||||
"h": 1231,
|
||||
"w": 1080,
|
||||
"x": 0,
|
||||
"y": 209
|
||||
},
|
||||
{
|
||||
"h": 1440,
|
||||
"w": 720,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
{
|
||||
"h": 1440,
|
||||
"w": 1080,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
],
|
||||
"height": 1440,
|
||||
"width": 1080
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"id_str": "2092435281869738141",
|
||||
"text": "Deacon Kevin Barron of the Catholic Community of Buffalo North led the prayer service on Tuesday for Mayich. In an interview with @azcentral, Barron said that Maywich suffered a “cardiac event” and is currently in a coma.",
|
||||
"display_text_range": [
|
||||
0,
|
||||
221
|
||||
],
|
||||
"created_at": "2026-08-26T02:13:41.000Z",
|
||||
"user": {
|
||||
"screen_name": "MikeMcMahonCHN",
|
||||
"name": "Mike McMahon",
|
||||
"profile_image_url_https": "https://pbs.twimg.com/profile_images/2068376149747449856/oqDq3qs4_normal.jpg"
|
||||
},
|
||||
"quoted_tweet": {
|
||||
"id_str": "2092384930034548928",
|
||||
"text": "Matthew Mayich, a sophomore defenseman, is the player hospitalized. There was a virtual prayer service held today, shared on social media, in which Mayich’s cousin spoke. I didn’t want to share the player’s name before we were sure the player’s family was OK with it being shared.",
|
||||
"display_text_range": [
|
||||
0,
|
||||
280
|
||||
],
|
||||
"created_at": "2026-08-25T22:53:36.000Z",
|
||||
"user": {
|
||||
"screen_name": "MikeMcMahonCHN",
|
||||
"name": "Mike McMahon",
|
||||
"profile_image_url_https": "https://pbs.twimg.com/profile_images/2068376149747449856/oqDq3qs4_normal.jpg"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { renderMarkdown } from '../src/render/markdown.ts';
|
||||
|
||||
const md = (text: string): string => String(renderMarkdown(text));
|
||||
|
||||
test('markup a commenter typed is text, not markup', () => {
|
||||
const out = md('<script>alert(1)</script> & "quoted"');
|
||||
assert.ok(!out.includes('<script>'));
|
||||
assert.match(out, /<script>/);
|
||||
assert.match(out, /&/);
|
||||
});
|
||||
|
||||
test('only http and https become links', () => {
|
||||
assert.match(md('[go](https://example.com/a)'), /<a href="https:\/\/example\.com\/a"/);
|
||||
// The label is still shown; it is only the link that is refused.
|
||||
const dangerous = md('[go](javascript:alert(1))');
|
||||
assert.ok(!dangerous.includes('<a '));
|
||||
assert.match(dangerous, /\[go\]/);
|
||||
});
|
||||
|
||||
test('a bare URL is linked without swallowing the sentence it ends', () => {
|
||||
const out = md('see https://example.com/x.');
|
||||
assert.match(out, /href="https:\/\/example\.com\/x"/);
|
||||
assert.ok(out.endsWith('.</p>'), `trailing full stop should stay outside the link: ${out}`);
|
||||
});
|
||||
|
||||
test('an ampersand in a link target survives as one', () => {
|
||||
const out = md('[x](https://example.com/?a=1&b=2)');
|
||||
assert.match(out, /href="https:\/\/example\.com\/\?a=1&b=2"/);
|
||||
});
|
||||
|
||||
test('a quote is a block of its own, so the reply is not read as part of it', () => {
|
||||
const out = md('> they said this\n\nand I disagree');
|
||||
assert.match(out, /<blockquote><p>they said this<\/p><\/blockquote><p>and I disagree<\/p>/);
|
||||
});
|
||||
|
||||
test('quoting a quote keeps both levels', () => {
|
||||
assert.match(md('> > deep\n> shallow'), /<blockquote><blockquote>/);
|
||||
});
|
||||
|
||||
test('emphasis does not fire inside a word', () => {
|
||||
// `snake_case_names` are ordinary in the subreddits this will be pointed at.
|
||||
assert.equal(md('some_variable_name'), '<p>some_variable_name</p>');
|
||||
assert.match(md('_yes_'), /<em>yes<\/em>/);
|
||||
});
|
||||
|
||||
test('code is left exactly as typed', () => {
|
||||
assert.match(md('`a < b && c`'), /<code>a < b && c<\/code>/);
|
||||
assert.match(md('```\n<b>not bold</b>\n```'), /<pre><code><b>not bold<\/b><\/code><\/pre>/);
|
||||
});
|
||||
|
||||
test('subreddit and user references link back to reddit', () => {
|
||||
assert.match(md('over in r/aww'), /href="https:\/\/www\.reddit\.com\/r\/aww"/);
|
||||
assert.match(md('ask u/someone'), /href="https:\/\/www\.reddit\.com\/u\/someone"/);
|
||||
});
|
||||
|
||||
test('lists survive, both kinds', () => {
|
||||
assert.match(md('- one\n- two'), /<ul><li>one<\/li><li>two<\/li><\/ul>/);
|
||||
assert.match(md('1. one\n2. two'), /<ol><li>one<\/li><li>two<\/li><\/ol>/);
|
||||
});
|
||||
|
||||
test('a single newline inside a paragraph is a line break, a blank line is a new one', () => {
|
||||
assert.equal(md('one\ntwo'), '<p>one<br>two</p>');
|
||||
assert.equal(md('one\n\ntwo'), '<p>one</p><p>two</p>');
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { commentsFrom, mediaFromLink, toPost, treeFromDepths } from '../src/platforms/reddit.ts';
|
||||
import { reddit } from '../src/platforms/reddit.ts';
|
||||
import { originalUrlFor } from '../src/platforms/index.ts';
|
||||
import { fixture } from './helpers.ts';
|
||||
|
||||
const URL_ = 'https://www.reddit.com/r/aww/comments/abc123/a_post/';
|
||||
|
||||
test('a text post is its title, and carries no media', () => {
|
||||
const post = toPost(fixture('reddit/self.json'), URL_);
|
||||
assert.equal(post.platform, 'reddit');
|
||||
assert.equal(post.author.handle, 'r/AskReddit');
|
||||
assert.match(post.author.displayName ?? '', /^u\//);
|
||||
assert.equal(post.segments[0]?.title, "What's a healthy food that pleases the taste buds too?");
|
||||
assert.deepEqual(post.segments[0]?.media, []);
|
||||
assert.ok(post.segments[0]?.postedAt);
|
||||
});
|
||||
|
||||
test('a gallery keeps every picture, in the order the post arranged them', () => {
|
||||
const link = fixture<[{ data: { children: Array<{ data: Record<string, unknown> }> } }]>(
|
||||
'reddit/gallery.json',
|
||||
)[0].data.children[0]?.data as Parameters<typeof mediaFromLink>[0];
|
||||
const media = mediaFromLink(link);
|
||||
const ids = (link.gallery_data?.items ?? []).map((item) => item.media_id);
|
||||
|
||||
assert.equal(media.length, ids.length);
|
||||
assert.ok(media.every((m) => m.kind === 'image'));
|
||||
// The pictures live in `media_metadata`, keyed and unordered; the order is
|
||||
// only in `gallery_data`, so joining the two is the whole job.
|
||||
media.forEach((item, index) => assert.ok(item.url.includes(String(ids[index]))));
|
||||
assert.ok(media.every((m) => m.width && m.height));
|
||||
});
|
||||
|
||||
test('a silent video is served as the plain MP4, with a real image for a poster', () => {
|
||||
const media = toPost(fixture('reddit/video.json'), URL_).segments[0]?.media ?? [];
|
||||
assert.equal(media.length, 1);
|
||||
const video = media[0];
|
||||
assert.equal(video?.kind, 'video');
|
||||
assert.match(video?.url ?? '', /\.mp4/);
|
||||
assert.ok(video?.kind === 'video' && video.poster);
|
||||
// Not the scrubber file, which is itself an MP4 and would render nothing.
|
||||
assert.ok(video?.kind === 'video' && !video.poster?.url.endsWith('.mp4'));
|
||||
assert.equal(video?.kind === 'video' ? video.hls : undefined, undefined);
|
||||
});
|
||||
|
||||
test('a video with sound is the HLS playlist, because the MP4 has no audio track', () => {
|
||||
const media = mediaFromLink({
|
||||
secure_media: {
|
||||
reddit_video: {
|
||||
fallback_url: 'https://v.redd.it/abc/CMAF_720.mp4?source=fallback',
|
||||
hls_url: 'https://v.redd.it/abc/HLSPlaylist.m3u8',
|
||||
has_audio: true,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
duration: 42,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(media, [
|
||||
{
|
||||
kind: 'video',
|
||||
url: 'https://v.redd.it/abc/HLSPlaylist.m3u8',
|
||||
hls: true,
|
||||
direct: true,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
durationSec: 42,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('a link post keeps the destination, since it is the whole content of the post', () => {
|
||||
const post = toPost(fixture('reddit/link.json'), URL_);
|
||||
assert.match(post.segments[0]?.text ?? '', /^https:\/\/www\.nytimes\.com\//);
|
||||
});
|
||||
|
||||
test('a crosspost shows what it is crossposting', () => {
|
||||
const media = mediaFromLink({
|
||||
is_self: false,
|
||||
crosspost_parent_list: [
|
||||
{ url_overridden_by_dest: 'https://i.redd.it/inner.jpg', post_hint: 'image' },
|
||||
],
|
||||
});
|
||||
assert.deepEqual(media, [{ kind: 'image', url: 'https://i.redd.it/inner.jpg' }]);
|
||||
});
|
||||
|
||||
test('comments come back as a tree, with the counts of what is missing', () => {
|
||||
const post = toPost(fixture('reddit/gallery.json'), URL_);
|
||||
|
||||
assert.equal(post.comments?.length, 3);
|
||||
assert.ok((post.moreComments ?? 0) > 0, 'the "more comments" node should be counted, not dropped');
|
||||
assert.equal(post.commentCount, 443);
|
||||
|
||||
const first = post.comments?.[0];
|
||||
assert.equal(first?.author, 'u/Background_Round_853');
|
||||
assert.equal(first?.isAuthor, true, 'the poster replying under their own post');
|
||||
assert.equal(first?.score, 6583);
|
||||
assert.ok((first?.replies.length ?? 0) > 0);
|
||||
assert.ok(first?.replies.every((reply) => Array.isArray(reply.replies)));
|
||||
});
|
||||
|
||||
test('a score the platform is still hiding is left off rather than guessed at', () => {
|
||||
const { comments } = commentsFrom({
|
||||
data: {
|
||||
children: [
|
||||
{ kind: 't1', data: { author: 'a', body: 'new', score: 1, score_hidden: true, replies: '' } },
|
||||
{ kind: 't1', data: { author: 'b', body: 'older', score: 42, score_hidden: false, replies: '' } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(comments[0]?.score, undefined);
|
||||
assert.equal(comments[1]?.score, 42);
|
||||
});
|
||||
|
||||
test('a deleted commenter keeps the platform\'s own word for it', () => {
|
||||
const { comments } = commentsFrom({
|
||||
data: { children: [{ kind: 't1', data: { author: '[deleted]', body: '[removed]', replies: '' } }] },
|
||||
});
|
||||
assert.equal(comments[0]?.author, '[deleted]');
|
||||
});
|
||||
|
||||
test('an empty reply listing is the string "", not an object', () => {
|
||||
// Reddit says "no replies" with an empty string, which is the shape most
|
||||
// likely to be read as a listing and crash the walk.
|
||||
const { comments } = commentsFrom({
|
||||
data: { children: [{ kind: 't1', data: { author: 'a', body: 'x', replies: '' } }] },
|
||||
});
|
||||
assert.deepEqual(comments[0]?.replies, []);
|
||||
});
|
||||
|
||||
test('a redd.it share code is rebuilt, since the rewrite drops the host', () => {
|
||||
assert.equal(originalUrlFor(reddit, 'abc123', ''), 'https://redd.it/abc123');
|
||||
assert.equal(
|
||||
originalUrlFor(reddit, 'r/aww/comments/abc123/a_post/', '?share_id=xyz&utm_source=share'),
|
||||
'https://www.reddit.com/r/aww/comments/abc123/a_post/',
|
||||
);
|
||||
});
|
||||
|
||||
test('reddit links route to the adapter, and its media hosts do not', () => {
|
||||
assert.ok(reddit.matchesHost('www.reddit.com'));
|
||||
assert.ok(reddit.matchesHost('old.reddit.com'));
|
||||
assert.ok(reddit.matchesHost('redd.it'));
|
||||
assert.ok(!reddit.matchesHost('bsky.app'));
|
||||
});
|
||||
|
||||
test('the page fallback rebuilds nesting from the depth on each comment', () => {
|
||||
// Reddit renders the tree flat, so depth is the only thing saying what
|
||||
// replies to what.
|
||||
const tree = treeFromDepths([
|
||||
{ depth: 0, author: 'a', score: 5, created: '', text: 'first' },
|
||||
{ depth: 1, author: 'b', score: 4, created: '', text: 'under first' },
|
||||
{ depth: 2, author: 'c', score: 3, created: '', text: 'under b' },
|
||||
// Back up two levels: this belongs to `first`, not to `c`.
|
||||
{ depth: 1, author: 'd', score: 2, created: '', text: 'also under first' },
|
||||
{ depth: 0, author: 'e', score: 1, created: '', text: 'second' },
|
||||
]);
|
||||
|
||||
assert.equal(tree.length, 2);
|
||||
assert.equal(tree[0]?.replies.length, 2);
|
||||
assert.equal(tree[0]?.replies[0]?.replies[0]?.text, 'under b');
|
||||
assert.equal(tree[0]?.replies[1]?.text, 'also under first');
|
||||
assert.deepEqual(tree[1]?.replies, []);
|
||||
});
|
||||
|
||||
test('a comment the page gave no text for is dropped rather than shown empty', () => {
|
||||
assert.deepEqual(treeFromDepths([{ depth: 0, author: 'a', score: 1, created: '', text: '' }]), []);
|
||||
});
|
||||
@@ -108,3 +108,132 @@ test('a failure still hands the link back', () => {
|
||||
assert.ok(page.includes('Open on TikTok'));
|
||||
assert.ok(page.includes('verification puzzle'));
|
||||
});
|
||||
|
||||
function redditPost(overrides: Partial<Post> = {}): Post {
|
||||
return post({
|
||||
platform: 'reddit',
|
||||
platformLabel: 'Reddit',
|
||||
originalUrl: 'https://www.reddit.com/r/aww/comments/abc/a/',
|
||||
author: { handle: 'r/aww' },
|
||||
textPosition: 'above',
|
||||
segments: oneSegment({ title: 'A headline', media: [] }),
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
test('a post with no comments renders no comment section at all', () => {
|
||||
assert.ok(!renderPost(redditPost()).includes('class="thread"'));
|
||||
assert.ok(!renderPost(post()).includes('class="thread"'));
|
||||
});
|
||||
|
||||
test('comments nest, so collapsing one takes its replies with it', () => {
|
||||
const page = renderPost(redditPost({
|
||||
comments: [
|
||||
{
|
||||
author: 'u/a',
|
||||
text: 'top',
|
||||
replies: [{ author: 'u/b', text: 'nested', replies: [] }],
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
// The reply is inside the parent's <details>, not a sibling of it.
|
||||
const parent = page.slice(page.indexOf('<details class="c"'));
|
||||
const closing = parent.indexOf('</details>');
|
||||
assert.ok(parent.slice(0, closing).includes('nested'), 'a reply must live inside its parent');
|
||||
assert.ok(page.includes('<details class="c" open'), 'threads start expanded');
|
||||
});
|
||||
|
||||
test('a comment says how much a fold would hide', () => {
|
||||
const page = renderPost(redditPost({
|
||||
comments: [
|
||||
{
|
||||
author: 'u/a',
|
||||
text: 'top',
|
||||
replies: [
|
||||
{ author: 'u/b', text: 'one', replies: [{ author: 'u/c', text: 'two', replies: [] }] },
|
||||
],
|
||||
},
|
||||
],
|
||||
}));
|
||||
assert.ok(page.includes('+2 replies'), 'the whole subtree is counted, not just direct replies');
|
||||
});
|
||||
|
||||
test('comment text is escaped, and its markdown is rendered', () => {
|
||||
const page = renderPost(redditPost({
|
||||
comments: [{ author: 'u/a', text: '**bold** <img src=x onerror=alert(1)>', replies: [] }],
|
||||
}));
|
||||
assert.ok(!page.includes('<img src=x'), 'markup in a comment must not survive');
|
||||
assert.ok(page.includes('<strong>bold</strong>'));
|
||||
});
|
||||
|
||||
test('what the first page did not carry is counted rather than pretended away', () => {
|
||||
const page = renderPost(redditPost({
|
||||
comments: [{ author: 'u/a', text: 'x', replies: [], moreReplies: 3 }],
|
||||
moreComments: 40,
|
||||
commentCount: 443,
|
||||
}));
|
||||
assert.ok(page.includes('3 more replies, on Reddit'));
|
||||
assert.ok(page.includes('40 more, behind'));
|
||||
assert.ok(page.includes('1 of 443'));
|
||||
});
|
||||
|
||||
function quoting(quoted: Post['segments'][number]['quoted']): Post {
|
||||
return post({
|
||||
platform: 'x',
|
||||
platformLabel: 'X',
|
||||
textPosition: 'above',
|
||||
segments: oneSegment({ text: 'look at this', media: [], quoted }),
|
||||
});
|
||||
}
|
||||
|
||||
test('a quoted post is drawn with its own author, not folded under the quoter', () => {
|
||||
const page = quoting({
|
||||
author: { handle: '@other', displayName: 'Someone Else' },
|
||||
text: 'the original words',
|
||||
media: [],
|
||||
url: 'https://x.com/other/status/9',
|
||||
});
|
||||
const html_ = renderPost(page);
|
||||
|
||||
assert.ok(html_.includes('class="quote"'));
|
||||
assert.ok(html_.includes('@other'), 'the quoted handle must be on the page');
|
||||
assert.ok(html_.includes('Someone Else'));
|
||||
assert.ok(html_.includes('the original words'));
|
||||
assert.ok(html_.includes('href="https://x.com/other/status/9"'));
|
||||
});
|
||||
|
||||
test('a quoted post with no quote renders no block', () => {
|
||||
assert.ok(!renderPost(quoting(undefined)).includes('class="quote"'));
|
||||
});
|
||||
|
||||
test("the quoted post's media is proxied like any other", () => {
|
||||
const html_ = renderPost(quoting({
|
||||
author: { handle: '@other' },
|
||||
media: [{ kind: 'image', url: 'https://pbs.twimg.com/q.jpg' }],
|
||||
}));
|
||||
assert.ok(!html_.includes('https://pbs.twimg.com/q.jpg'), 'upstream URLs must not reach the page');
|
||||
assert.ok(html_.includes('src="/m/'));
|
||||
});
|
||||
|
||||
test('a quoted post is separated from the words that quote it', () => {
|
||||
const html_ = renderPost(quoting({
|
||||
author: { handle: '@other' },
|
||||
text: 'quoted words',
|
||||
media: [],
|
||||
}));
|
||||
// The quoter's text closes before the block opens, so the two can never
|
||||
// read as one paragraph by the same person.
|
||||
assert.ok(html_.indexOf('look at this') < html_.indexOf('class="quote"'));
|
||||
assert.ok(html_.indexOf('class="quote"') < html_.indexOf('quoted words'));
|
||||
});
|
||||
|
||||
test('markup in a quoted post is escaped like any other stranger\'s text', () => {
|
||||
const html_ = renderPost(quoting({
|
||||
author: { handle: '<img src=x onerror=alert(1)>' },
|
||||
text: '<script>alert(1)</script>',
|
||||
media: [],
|
||||
}));
|
||||
assert.ok(!html_.includes('<script>alert(1)</script>'));
|
||||
assert.ok(!html_.includes('<img src=x'));
|
||||
});
|
||||
|
||||
+110
-11
@@ -1,6 +1,6 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { mediaFromDetails, toPost } from '../src/platforms/x.ts';
|
||||
import { mediaFromDetails, quotedFrom, toPost } from '../src/platforms/x.ts';
|
||||
import { fixture } from './helpers.ts';
|
||||
|
||||
const URL_ = 'https://x.com/example/status/1';
|
||||
@@ -50,20 +50,119 @@ test('a video picks the highest-bitrate mp4 and ignores the streaming variants',
|
||||
]);
|
||||
});
|
||||
|
||||
test('a quote post shows the media of the post it quotes', () => {
|
||||
// The quote itself carries none; without this the page is text and nothing
|
||||
// else, which is exactly the thing the reader wanted to see.
|
||||
test('a quote post carries the post it quotes, whose it is included', () => {
|
||||
// Someone continuing their own thought from an earlier post. Without the
|
||||
// quoted half on the page the remaining half says nothing.
|
||||
const post = toPost(fixture('x/quote-text.json'), URL_);
|
||||
const quoted = post.segments[0]?.quoted;
|
||||
|
||||
assert.ok(quoted, 'the quoted post must survive');
|
||||
assert.equal(quoted?.author.handle, '@MikeMcMahonCHN');
|
||||
assert.ok(quoted?.author.displayName);
|
||||
assert.match(quoted?.text ?? '', /Matthew Mayich/);
|
||||
assert.ok(quoted?.postedAt);
|
||||
// No permalink in the payload, so it is rebuilt from the handle and the id.
|
||||
assert.equal(quoted?.url, 'https://x.com/MikeMcMahonCHN/status/2092384930034548928');
|
||||
});
|
||||
|
||||
test('the quoted post keeps its own pictures instead of lending them to the quoter', () => {
|
||||
const post = toPost(fixture('x/quote-photo.json'), URL_);
|
||||
|
||||
assert.deepEqual(post.segments[0]?.media, [], 'the quoter attached nothing');
|
||||
assert.equal(post.segments[0]?.quoted?.media.length, 1);
|
||||
assert.equal(post.segments[0]?.quoted?.media[0]?.kind, 'image');
|
||||
});
|
||||
|
||||
test('media on both sides stays on the side it came from', () => {
|
||||
// This is the case the old either-or could not represent at all: it showed
|
||||
// the quoter's picture and silently dropped the one being talked about.
|
||||
const segment = toPost(fixture('x/quote-both.json'), URL_).segments[0];
|
||||
|
||||
assert.equal(segment?.media.length, 1);
|
||||
assert.equal(segment?.quoted?.media.length, 1);
|
||||
assert.notEqual(segment?.media[0]?.url, segment?.quoted?.media[0]?.url);
|
||||
});
|
||||
|
||||
test('a post quoting nothing has no quoted post', () => {
|
||||
assert.equal(toPost(fixture('x/photo.json'), URL_).segments[0]?.quoted, undefined);
|
||||
assert.equal(quotedFrom(undefined), undefined);
|
||||
// A quoted post X will not describe is nothing to show.
|
||||
assert.equal(quotedFrom({ text: 'orphaned' }), undefined);
|
||||
});
|
||||
|
||||
test('the shortlink X staples to a quote post is not shown', () => {
|
||||
// The quoted post is right there on the page; a t.co pointing at it is
|
||||
// noise, which is why X hides it too. `display_text_range` says where it
|
||||
// starts, and keeps a link the author put there deliberately.
|
||||
const both = toPost(fixture('x/quote-both.json'), URL_).segments[0];
|
||||
// Two links arrive: one the author wrote, one X appended pointing at the
|
||||
// quoted post. Exactly the second goes.
|
||||
assert.equal((both?.text?.match(/https?:\/\//g) ?? []).length, 1, "the author's own link stays");
|
||||
assert.match(both?.text ?? '', /on3\.com/);
|
||||
assert.ok(!both?.text?.includes('QdJhOVu4En'));
|
||||
|
||||
const photo = toPost(fixture('x/quote-photo.json'), URL_).segments[0];
|
||||
assert.ok(!photo?.quoted?.text?.includes('t.co'), 'and the same on the quoted post');
|
||||
});
|
||||
|
||||
test('a t.co stands aside for the address it stands in for', () => {
|
||||
// Left alone the page says `t.co/rEGx9JN5pJ`, which tells the reader
|
||||
// nothing and sends them through X's click tracker to find out.
|
||||
const segment = toPost(fixture('x/quote-both.json'), URL_).segments[0];
|
||||
assert.ok(!segment?.text?.includes('t.co/'), `still shortened: ${segment?.text}`);
|
||||
assert.match(segment?.text ?? '', /on3\.com/);
|
||||
assert.match(segment?.quoted?.text ?? '', /on3\.com/);
|
||||
});
|
||||
|
||||
test('a shortlink the payload does not explain is left as it is', () => {
|
||||
const post = toPost(
|
||||
{
|
||||
text: 'look at this',
|
||||
text: 'see https://t.co/unknown1 and https://t.co/known123',
|
||||
user: { screen_name: 'someone' },
|
||||
mediaDetails: [],
|
||||
quoted_tweet: {
|
||||
mediaDetails: [{ type: 'photo', media_url_https: 'https://pbs.twimg.com/q.jpg' }],
|
||||
},
|
||||
entities: { urls: [{ url: 'https://t.co/known123', expanded_url: 'https://example.com/real' }] },
|
||||
},
|
||||
URL_,
|
||||
);
|
||||
assert.equal(post.segments[0]?.media.length, 1);
|
||||
assert.equal(post.segments[0]?.media[0]?.url, 'https://pbs.twimg.com/q.jpg?name=orig');
|
||||
assert.equal(post.segments[0]?.text, 'see https://t.co/unknown1 and https://example.com/real');
|
||||
});
|
||||
|
||||
test('an expansion that is not an http address is refused', () => {
|
||||
const post = toPost(
|
||||
{
|
||||
text: 'https://t.co/abc',
|
||||
user: { screen_name: 'someone' },
|
||||
entities: { urls: [{ url: 'https://t.co/abc', expanded_url: 'javascript:alert(1)' }] },
|
||||
},
|
||||
URL_,
|
||||
);
|
||||
assert.equal(post.segments[0]?.text, 'https://t.co/abc');
|
||||
});
|
||||
|
||||
test('the visible range is applied before the entities are decoded', () => {
|
||||
// The indices are into the escaped text, where `&` is five characters.
|
||||
// Decoding first shifts everything after it and truncates the tail.
|
||||
const post = toPost(
|
||||
{
|
||||
text: 'Tom & Jerry https://t.co/xxx',
|
||||
display_text_range: [0, 15],
|
||||
user: { screen_name: 'someone' },
|
||||
},
|
||||
URL_,
|
||||
);
|
||||
assert.equal(post.segments[0]?.text, 'Tom & Jerry');
|
||||
});
|
||||
|
||||
test('escaped characters X hands back are decoded, not shown as entities', () => {
|
||||
// The syndication payload arrives pre-escaped; everything downstream
|
||||
// escapes again on the way out, so leaving these puts `&` on screen.
|
||||
const post = toPost(
|
||||
{
|
||||
text: 'Tom & Jerry <3 "quoted"',
|
||||
user: { screen_name: 'someone' },
|
||||
quoted_tweet: { text: 'me & you', user: { screen_name: 'other' }, id_str: '9' },
|
||||
},
|
||||
URL_,
|
||||
);
|
||||
assert.equal(post.segments[0]?.text, 'Tom & Jerry <3 "quoted"');
|
||||
assert.equal(post.segments[0]?.quoted?.text, 'me & you');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user