@postfetch/core 0.13.5 → 0.13.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +81 -0
- package/package.json +1 -1
- package/src/reddit.ts +77 -0
package/dist/index.js
CHANGED
|
@@ -1880,6 +1880,10 @@ async function redditComments(input, token, id) {
|
|
|
1880
1880
|
try {
|
|
1881
1881
|
const headers = { authorization: `Bearer ${token}`, "user-agent": browserUserAgent() };
|
|
1882
1882
|
const response = await input.net(`https://oauth.reddit.com/comments/${id}?raw_json=1&limit=${Math.min(limit, 100)}&depth=1&sort=top`, { headers }, 1);
|
|
1883
|
+
if (response.status === 403) {
|
|
1884
|
+
await response.body?.cancel();
|
|
1885
|
+
return await graphqlComments(input.net, headers, id, limit);
|
|
1886
|
+
}
|
|
1883
1887
|
if (!response.ok)
|
|
1884
1888
|
throw new Error("Reddit comments failed");
|
|
1885
1889
|
const payload = await response.json();
|
|
@@ -1922,6 +1926,10 @@ async function redditComments(input, token, id) {
|
|
|
1922
1926
|
const url = new URL("https://oauth.reddit.com/api/morechildren");
|
|
1923
1927
|
url.search = new URLSearchParams({ api_type: "json", raw_json: "1", link_id: `t3_${id}`, children: children.join(","), sort: "top", depth: "1", limit_children: "true" }).toString();
|
|
1924
1928
|
const more = await input.net(url.href, { headers }, 1);
|
|
1929
|
+
if (more.status === 403) {
|
|
1930
|
+
await more.body?.cancel();
|
|
1931
|
+
return await graphqlComments(input.net, headers, id, limit);
|
|
1932
|
+
}
|
|
1925
1933
|
if (!more.ok)
|
|
1926
1934
|
throw new Error("Reddit more comments failed");
|
|
1927
1935
|
const result = await more.json();
|
|
@@ -1936,6 +1944,79 @@ async function redditComments(input, token, id) {
|
|
|
1936
1944
|
return [];
|
|
1937
1945
|
}
|
|
1938
1946
|
}
|
|
1947
|
+
var postCommentsHash = "314f29c2494fd78c7844ef2c6b09acb4bef5b37ab50b4ddc01691d6d5c8433f8";
|
|
1948
|
+
async function graphqlComments(net, headers, id, limit) {
|
|
1949
|
+
const comments = [];
|
|
1950
|
+
const seen = new Set;
|
|
1951
|
+
const cursors = new Set;
|
|
1952
|
+
let after;
|
|
1953
|
+
do {
|
|
1954
|
+
const response = await net("https://gql-fed.reddit.com/", {
|
|
1955
|
+
method: "POST",
|
|
1956
|
+
headers: { ...headers, "content-type": "application/json" },
|
|
1957
|
+
body: JSON.stringify({
|
|
1958
|
+
operationName: "PostComments",
|
|
1959
|
+
variables: {
|
|
1960
|
+
id: `t3_${id}`,
|
|
1961
|
+
sortType: "TOP",
|
|
1962
|
+
count: Math.min(200, limit - comments.length),
|
|
1963
|
+
maxDepth: 1,
|
|
1964
|
+
includeConsistencyToken: false,
|
|
1965
|
+
includePremiumAvatarTreatment: false,
|
|
1966
|
+
...after && { after }
|
|
1967
|
+
},
|
|
1968
|
+
extensions: { persistedQuery: { version: 1, sha256Hash: postCommentsHash } }
|
|
1969
|
+
})
|
|
1970
|
+
}, 1);
|
|
1971
|
+
if (!response.ok)
|
|
1972
|
+
throw new Error(`Reddit GraphQL comments failed: ${response.status}`);
|
|
1973
|
+
const payload = await response.json();
|
|
1974
|
+
const data = object(payload) && object(payload.data) ? payload.data : null;
|
|
1975
|
+
const post = data && object(data.postInfoById) ? data.postInfoById : null;
|
|
1976
|
+
const forest = post && object(post.commentForest) ? post.commentForest : null;
|
|
1977
|
+
if (!forest || !Array.isArray(forest.trees))
|
|
1978
|
+
throw new Error("Invalid Reddit GraphQL comments");
|
|
1979
|
+
after = undefined;
|
|
1980
|
+
for (const tree of forest.trees) {
|
|
1981
|
+
if (!object(tree) || tree.depth !== 0 || tree.parentId != null && tree.parentId !== `t3_${id}`)
|
|
1982
|
+
continue;
|
|
1983
|
+
if (object(tree.more)) {
|
|
1984
|
+
after = string(tree.more.cursor) ?? undefined;
|
|
1985
|
+
continue;
|
|
1986
|
+
}
|
|
1987
|
+
if (!object(tree.node) || comments.length >= limit)
|
|
1988
|
+
continue;
|
|
1989
|
+
const node = tree.node;
|
|
1990
|
+
const fullname = string(node.id);
|
|
1991
|
+
const content = object(node.content) ? node.content : null;
|
|
1992
|
+
const body = content && string(content.markdown);
|
|
1993
|
+
if (!fullname || !/^t1_[a-z0-9]+$/i.test(fullname) || seen.has(fullname) || !body || body === "[deleted]" || body === "[removed]" || node.__typename === "DeletedComment" || node.isRemoved === true || node.isAdminTakedown === true)
|
|
1994
|
+
continue;
|
|
1995
|
+
seen.add(fullname);
|
|
1996
|
+
const metadata = {};
|
|
1997
|
+
const media = Array.isArray(content.richtextMedia) ? content.richtextMedia.filter(object) : [];
|
|
1998
|
+
for (const asset of media) {
|
|
1999
|
+
const mediaId2 = string(asset.id);
|
|
2000
|
+
const url = string(asset.url);
|
|
2001
|
+
if (!mediaId2 || !url || asset.status && asset.status !== "VALID")
|
|
2002
|
+
continue;
|
|
2003
|
+
const mime = string(asset.mimetype);
|
|
2004
|
+
metadata[mediaId2] = { m: mime, s: mime === "video/mp4" ? { mp4: url } : { u: url } };
|
|
2005
|
+
}
|
|
2006
|
+
const createdAt = string(node.createdAt)?.replace(/(\.\d{3})\d+/, "$1").replace(/([+-]\d{2})(\d{2})$/, "$1:$2");
|
|
2007
|
+
comments.push(redditComment({
|
|
2008
|
+
author: object(node.authorInfo) ? node.authorInfo.name : undefined,
|
|
2009
|
+
created_utc: createdAt ? Math.floor(Date.parse(createdAt) / 1000) : undefined,
|
|
2010
|
+
ups: node.score,
|
|
2011
|
+
media_metadata: metadata
|
|
2012
|
+
}, fullname.slice(3), id, body));
|
|
2013
|
+
}
|
|
2014
|
+
if (!after || cursors.has(after))
|
|
2015
|
+
break;
|
|
2016
|
+
cursors.add(after);
|
|
2017
|
+
} while (comments.length < limit);
|
|
2018
|
+
return comments;
|
|
2019
|
+
}
|
|
1939
2020
|
function redditComment(data, id, postId2, body) {
|
|
1940
2021
|
const metadata = object(data.media_metadata) ? data.media_metadata : {};
|
|
1941
2022
|
const mediaIds = Object.keys(metadata).filter((key) => object(metadata[key])).sort((left, right) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postfetch/core",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.6",
|
|
4
4
|
"description": "Zero-dependency typed core that turns Facebook, Instagram, LinkedIn, Pinterest, Reddit, SoundCloud, Spotify, TikTok, X and YouTube post URLs into media files.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/reddit.ts
CHANGED
|
@@ -329,6 +329,10 @@ async function redditComments(input: ResolveContext, token: string, id: string):
|
|
|
329
329
|
try {
|
|
330
330
|
const headers = { authorization: `Bearer ${token}`, "user-agent": browserUserAgent() };
|
|
331
331
|
const response = await input.net(`https://oauth.reddit.com/comments/${id}?raw_json=1&limit=${Math.min(limit, 100)}&depth=1&sort=top`, { headers }, 1);
|
|
332
|
+
if (response.status === 403) {
|
|
333
|
+
await response.body?.cancel();
|
|
334
|
+
return await graphqlComments(input.net, headers, id, limit);
|
|
335
|
+
}
|
|
332
336
|
if (!response.ok) throw new Error("Reddit comments failed");
|
|
333
337
|
const payload: unknown = await response.json();
|
|
334
338
|
const listing = Array.isArray(payload) ? payload[1] : null;
|
|
@@ -364,6 +368,10 @@ async function redditComments(input: ResolveContext, token: string, id: string):
|
|
|
364
368
|
const url = new URL("https://oauth.reddit.com/api/morechildren");
|
|
365
369
|
url.search = new URLSearchParams({ api_type: "json", raw_json: "1", link_id: `t3_${id}`, children: children.join(","), sort: "top", depth: "1", limit_children: "true" }).toString();
|
|
366
370
|
const more = await input.net(url.href, { headers }, 1);
|
|
371
|
+
if (more.status === 403) {
|
|
372
|
+
await more.body?.cancel();
|
|
373
|
+
return await graphqlComments(input.net, headers, id, limit);
|
|
374
|
+
}
|
|
367
375
|
if (!more.ok) throw new Error("Reddit more comments failed");
|
|
368
376
|
const result: unknown = await more.json();
|
|
369
377
|
const json = object(result) && object(result.json) ? result.json : null;
|
|
@@ -377,6 +385,75 @@ async function redditComments(input: ResolveContext, token: string, id: string):
|
|
|
377
385
|
}
|
|
378
386
|
}
|
|
379
387
|
|
|
388
|
+
// Reddit blocks anonymous OAuth comment routes while its mobile GraphQL API
|
|
389
|
+
// still accepts the same token. This is the app's registered PostComments query;
|
|
390
|
+
// gql-fed rejects arbitrary query text. Keep the JSON API as the first choice.
|
|
391
|
+
const postCommentsHash = "314f29c2494fd78c7844ef2c6b09acb4bef5b37ab50b4ddc01691d6d5c8433f8";
|
|
392
|
+
|
|
393
|
+
async function graphqlComments(net: Net, headers: Record<string, string>, id: string, limit: number): Promise<PostComment[]> {
|
|
394
|
+
const comments: PostComment[] = [];
|
|
395
|
+
const seen = new Set<string>();
|
|
396
|
+
const cursors = new Set<string>();
|
|
397
|
+
let after: string | undefined;
|
|
398
|
+
do {
|
|
399
|
+
const response = await net("https://gql-fed.reddit.com/", {
|
|
400
|
+
method: "POST",
|
|
401
|
+
headers: { ...headers, "content-type": "application/json" },
|
|
402
|
+
body: JSON.stringify({
|
|
403
|
+
operationName: "PostComments",
|
|
404
|
+
variables: {
|
|
405
|
+
id: `t3_${id}`, sortType: "TOP", count: Math.min(200, limit - comments.length), maxDepth: 1,
|
|
406
|
+
includeConsistencyToken: false, includePremiumAvatarTreatment: false,
|
|
407
|
+
...(after && { after }),
|
|
408
|
+
},
|
|
409
|
+
extensions: { persistedQuery: { version: 1, sha256Hash: postCommentsHash } },
|
|
410
|
+
}),
|
|
411
|
+
}, 1);
|
|
412
|
+
if (!response.ok) throw new Error(`Reddit GraphQL comments failed: ${response.status}`);
|
|
413
|
+
const payload: unknown = await response.json();
|
|
414
|
+
const data = object(payload) && object(payload.data) ? payload.data : null;
|
|
415
|
+
const post = data && object(data.postInfoById) ? data.postInfoById : null;
|
|
416
|
+
const forest = post && object(post.commentForest) ? post.commentForest : null;
|
|
417
|
+
if (!forest || !Array.isArray(forest.trees)) throw new Error("Invalid Reddit GraphQL comments");
|
|
418
|
+
after = undefined;
|
|
419
|
+
for (const tree of forest.trees) {
|
|
420
|
+
if (!object(tree) || tree.depth !== 0 || (tree.parentId != null && tree.parentId !== `t3_${id}`)) continue;
|
|
421
|
+
if (object(tree.more)) {
|
|
422
|
+
after = string(tree.more.cursor) ?? undefined;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (!object(tree.node) || comments.length >= limit) continue;
|
|
426
|
+
const node = tree.node;
|
|
427
|
+
const fullname = string(node.id);
|
|
428
|
+
const content = object(node.content) ? node.content : null;
|
|
429
|
+
const body = content && string(content.markdown);
|
|
430
|
+
if (!fullname || !/^t1_[a-z0-9]+$/i.test(fullname) || seen.has(fullname) || !body
|
|
431
|
+
|| body === "[deleted]" || body === "[removed]" || node.__typename === "DeletedComment" || node.isRemoved === true || node.isAdminTakedown === true) continue;
|
|
432
|
+
seen.add(fullname);
|
|
433
|
+
const metadata: Json = {};
|
|
434
|
+
const media = Array.isArray(content.richtextMedia) ? content.richtextMedia.filter(object) : [];
|
|
435
|
+
for (const asset of media) {
|
|
436
|
+
const mediaId = string(asset.id);
|
|
437
|
+
const url = string(asset.url);
|
|
438
|
+
if (!mediaId || !url || (asset.status && asset.status !== "VALID")) continue;
|
|
439
|
+
const mime = string(asset.mimetype);
|
|
440
|
+
metadata[mediaId] = { m: mime, s: mime === "video/mp4" ? { mp4: url } : { u: url } };
|
|
441
|
+
}
|
|
442
|
+
// The API uses microseconds and offsets like +0000; normalize for JS dates.
|
|
443
|
+
const createdAt = string(node.createdAt)?.replace(/(\.\d{3})\d+/, "$1").replace(/([+-]\d{2})(\d{2})$/, "$1:$2");
|
|
444
|
+
comments.push(redditComment({
|
|
445
|
+
author: object(node.authorInfo) ? node.authorInfo.name : undefined,
|
|
446
|
+
created_utc: createdAt ? Math.floor(Date.parse(createdAt) / 1000) : undefined,
|
|
447
|
+
ups: node.score,
|
|
448
|
+
media_metadata: metadata,
|
|
449
|
+
}, fullname.slice(3), id, body));
|
|
450
|
+
}
|
|
451
|
+
if (!after || cursors.has(after)) break;
|
|
452
|
+
cursors.add(after);
|
|
453
|
+
} while (comments.length < limit);
|
|
454
|
+
return comments;
|
|
455
|
+
}
|
|
456
|
+
|
|
380
457
|
function redditComment(data: Json, id: string, postId: string, body: string): PostComment {
|
|
381
458
|
const metadata = object(data.media_metadata) ? data.media_metadata : {};
|
|
382
459
|
// Comment images/GIFs use the same rendition metadata as Reddit galleries.
|