@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 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.5",
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.