@sockethub/platform-metadata 1.0.1-alpha.14 → 1.0.1-alpha.15

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/README.md CHANGED
@@ -96,11 +96,11 @@ so those URLs are resolved through preview services built for this purpose:
96
96
  thumbnail), and the canonical `x.com` URL. If the API is unavailable or the
97
97
  post can't be resolved, the platform falls back to the regular scrape
98
98
  (which still yields the post text).
99
- * **Reddit URLs** (`reddit.com` hosts and `redd.it` short links) are scraped
100
- directly, but with a *compatibility user agent* — Reddit serves its Open
101
- Graph data (including the post's real preview image) only to recognized
102
- embed crawlers, and returns a page with no OG tags (or a 403) to anything
103
- else. See `compatUserAgent` below.
99
+ * **Reddit post URLs** use `old.reddit.com`'s structured JSON response, avoiding
100
+ Reddit's unreliable HTML and Open Graph crawler behavior. The post title,
101
+ self-text, and direct `i.redd.it` image are mapped into the preview; text posts
102
+ intentionally have no image. oEmbed remains a title-only failure fallback.
103
+ See `compatUserAgent` below.
104
104
  * **Facebook** remains best-effort: post text usually comes through, but
105
105
  media requires authentication and no public preview service exists.
106
106
 
@@ -124,7 +124,7 @@ default UA. Override per deployment via `packageConfig`:
124
124
 
125
125
  ### `compatUserAgent`
126
126
 
127
- Sites that only serve Open Graph data to *recognized* embed crawlers (Reddit)
127
+ Sites that only serve metadata to *recognized* embed crawlers (Reddit)
128
128
  are fetched with a link-preview crawler user agent instead — the established
129
129
  practice for self-hosted preview fetchers. Defaults to
130
130
  `Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)`;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import type { ActivityStream, PlatformCallback, PlatformConfig, PlatformInterface, PlatformSession } from "@sockethub/schemas";
2
+ /** Enforce a deadline independently of a dependency's AbortSignal handling. */
3
+ export declare function withDeadline<T>(promise: Promise<T>, timeoutMs: number): Promise<T>;
2
4
  export default class Metadata implements PlatformInterface {
3
5
  private readonly log;
6
+ private readonly fetchImpl;
4
7
  private dispatcher?;
5
8
  config: PlatformConfig;
6
9
  constructor(session: PlatformSession);
@@ -158,6 +161,15 @@ export default class Metadata implements PlatformInterface {
158
161
  private compatUserAgent;
159
162
  fetch(job: ActivityStream, cb: PlatformCallback): void;
160
163
  private fetchTweet;
164
+ private fetchReddit;
165
+ /**
166
+ * Fetch Reddit's fixed-host JSON endpoint without the Undici dispatcher.
167
+ * The platform child runtime has exhibited stuck Undici requests even when
168
+ * AbortSignal fires. node:https gives us an independently enforced socket
169
+ * timeout; the outer withDeadline still guarantees callback completion.
170
+ */
171
+ private fetchRedditJson;
172
+ private fetchRedditEmbed;
161
173
  private scrape;
162
174
  cleanup(cb: PlatformCallback): void;
163
175
  }
package/dist/index.js CHANGED
@@ -66294,11 +66294,11 @@ var require_isUrl = __commonJS((exports) => {
66294
66294
  var IPv4AddressRegExp = new RegExp(`^${IPv4AddressFormat}$`);
66295
66295
  var IPv6SegmentFormat = "(?:[0-9a-fA-F]{1,4})";
66296
66296
  var IPv6AddressRegExp = new RegExp("^(" + `(?:${IPv6SegmentFormat}:){7}(?:${IPv6SegmentFormat}|:)|` + `(?:${IPv6SegmentFormat}:){6}(?:${IPv4AddressFormat}|:${IPv6SegmentFormat}|:)|` + `(?:${IPv6SegmentFormat}:){5}(?::${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,2}|:)|` + `(?:${IPv6SegmentFormat}:){4}(?:(:${IPv6SegmentFormat}){0,1}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,3}|:)|` + `(?:${IPv6SegmentFormat}:){3}(?:(:${IPv6SegmentFormat}){0,2}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,4}|:)|` + `(?:${IPv6SegmentFormat}:){2}(?:(:${IPv6SegmentFormat}){0,3}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,5}|:)|` + `(?:${IPv6SegmentFormat}:){1}(?:(:${IPv6SegmentFormat}){0,4}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,6}|:)|` + `(?::((?::${IPv6SegmentFormat}){0,5}:${IPv4AddressFormat}|(?::${IPv6SegmentFormat}){1,7}|:))` + ")(%[0-9a-zA-Z-.:]{1,})?$");
66297
- function isIP(str, version = "") {
66297
+ function isIP2(str, version = "") {
66298
66298
  assertString(str);
66299
66299
  version = String(version);
66300
66300
  if (!version) {
66301
- return isIP(str, 4) || isIP(str, 6);
66301
+ return isIP2(str, 4) || isIP2(str, 6);
66302
66302
  }
66303
66303
  if (version === "4") {
66304
66304
  return IPv4AddressRegExp.test(str);
@@ -66433,7 +66433,7 @@ var require_isUrl = __commonJS((exports) => {
66433
66433
  if (host === "" && !options.require_host) {
66434
66434
  return true;
66435
66435
  }
66436
- if (!isIP(host) && !isFQDN(host, options) && (!ipv6 || !isIP(ipv6, 6))) {
66436
+ if (!isIP2(host) && !isFQDN(host, options) && (!ipv6 || !isIP2(ipv6, 6))) {
66437
66437
  return false;
66438
66438
  }
66439
66439
  host = host || ipv6;
@@ -71643,6 +71643,9 @@ var require_esm = __commonJS((exports, module) => {
71643
71643
  module.exports = run;
71644
71644
  });
71645
71645
 
71646
+ // src/index.ts
71647
+ import { request as httpsRequest } from "node:https";
71648
+
71646
71649
  // ../util/dist/error/index.js
71647
71650
  function toError(value) {
71648
71651
  if (value instanceof Error) {
@@ -71660,6 +71663,7 @@ function errorMessage(value) {
71660
71663
  // ../util/dist/net/index.js
71661
71664
  var import_undici = __toESM(require_undici(), 1);
71662
71665
  import { lookup as dnsLookup } from "node:dns";
71666
+ import { isIP } from "node:net";
71663
71667
  function isBlockedAddress(ip) {
71664
71668
  const addr = ip.trim().toLowerCase();
71665
71669
  if (addr.includes(":")) {
@@ -71778,6 +71782,18 @@ function createGuardedLookup(allowPrivateAddresses) {
71778
71782
  });
71779
71783
  };
71780
71784
  }
71785
+ function createGuardedConnector(allowPrivateAddresses, connector = import_undici.buildConnector({
71786
+ lookup: createGuardedLookup(allowPrivateAddresses)
71787
+ })) {
71788
+ return (options, callback) => {
71789
+ const hostname = options.hostname.replace(/^\[|\]$/g, "");
71790
+ if (!allowPrivateAddresses && isIP(hostname) !== 0 && isBlockedAddress(hostname)) {
71791
+ callback(new Error(`blocked non-public address ${hostname}`), null);
71792
+ return;
71793
+ }
71794
+ connector(options, callback);
71795
+ };
71796
+ }
71781
71797
  function createGuardedDispatcher(options = {}) {
71782
71798
  const {
71783
71799
  allowPrivateAddresses = false,
@@ -71785,18 +71801,17 @@ function createGuardedDispatcher(options = {}) {
71785
71801
  } = options;
71786
71802
  return new import_undici.Agent({
71787
71803
  maxResponseSize: maxResponseBytes,
71788
- connect: {
71789
- lookup: createGuardedLookup(allowPrivateAddresses)
71790
- }
71804
+ connect: createGuardedConnector(allowPrivateAddresses)
71791
71805
  });
71792
71806
  }
71793
71807
 
71794
71808
  // src/index.ts
71795
71809
  var import_open_graph_scraper = __toESM(require_esm(), 1);
71810
+ var import_undici2 = __toESM(require_undici(), 1);
71796
71811
  // package.json
71797
71812
  var package_default = {
71798
71813
  name: "@sockethub/platform-metadata",
71799
- version: "1.0.1-alpha.14",
71814
+ version: "1.0.1-alpha.15",
71800
71815
  description: "Fetch metadata from a given URL",
71801
71816
  author: "Nick Jennings <nick@silverbucket.net>",
71802
71817
  license: "LGPL-3.0+",
@@ -71838,16 +71853,17 @@ var package_default = {
71838
71853
  "clean:deps": "rm -rf node_modules"
71839
71854
  },
71840
71855
  dependencies: {
71841
- "@sockethub/util": "^1.0.0-alpha.7",
71842
- "open-graph-scraper": "^6.11.0"
71856
+ "@sockethub/util": "^1.0.0-alpha.8",
71857
+ "open-graph-scraper": "^6.11.0",
71858
+ undici: "^7.16.0"
71843
71859
  },
71844
71860
  devDependencies: {
71845
- "@sockethub/schemas": "^3.0.0-alpha.19"
71861
+ "@sockethub/schemas": "^3.0.0-alpha.20"
71846
71862
  },
71847
71863
  peerDependencies: {
71848
- "@sockethub/server": "^5.0.0-alpha.19"
71864
+ "@sockethub/server": "^5.0.0-alpha.20"
71849
71865
  },
71850
- gitHead: "9dc63d790aabfbfe894b1d1b0754013e40fd25ec",
71866
+ gitHead: "194422de2ffe1a95ffff45063bc988da8594756a",
71851
71867
  types: "./dist/index.d.ts"
71852
71868
  };
71853
71869
 
@@ -71893,6 +71909,115 @@ function isRedditUrl(url) {
71893
71909
  const host = parsed.hostname.toLowerCase();
71894
71910
  return REDDIT_HOSTS.has(host) || host === "redd.it";
71895
71911
  }
71912
+ function resolveRedditJson(url) {
71913
+ let parsed;
71914
+ try {
71915
+ parsed = new URL(url);
71916
+ } catch {
71917
+ return null;
71918
+ }
71919
+ if (!REDDIT_HOSTS.has(parsed.hostname.toLowerCase()))
71920
+ return null;
71921
+ const match = parsed.pathname.match(/^\/(?:r|user)\/[^/]+\/comments\/[^/]+(?:\/[^/]*)?\/?$/);
71922
+ if (!match)
71923
+ return null;
71924
+ const json = new URL(`${parsed.pathname.replace(/\/$/, "")}.json`, "https://old.reddit.com");
71925
+ json.searchParams.set("raw_json", "1");
71926
+ return json.href;
71927
+ }
71928
+ function parseRedditPost(value) {
71929
+ if (!Array.isArray(value))
71930
+ return null;
71931
+ const post = value[0]?.data?.children?.[0]?.data;
71932
+ if (!post || typeof post !== "object" || Array.isArray(post))
71933
+ return null;
71934
+ const data = post;
71935
+ if (typeof data.title !== "string" || !data.title)
71936
+ return null;
71937
+ for (const key of ["selftext", "url", "url_overridden_by_dest"]) {
71938
+ if (data[key] !== undefined && typeof data[key] !== "string")
71939
+ return null;
71940
+ }
71941
+ return data;
71942
+ }
71943
+ function redditPostImage(post) {
71944
+ for (const candidate of [post.url_overridden_by_dest, post.url]) {
71945
+ if (!candidate)
71946
+ continue;
71947
+ try {
71948
+ const url = new URL(candidate);
71949
+ if (["i.redd.it", "preview.redd.it"].includes(url.hostname.toLowerCase())) {
71950
+ return [{ url: url.href }];
71951
+ }
71952
+ } catch {}
71953
+ }
71954
+ return;
71955
+ }
71956
+ function parseRedditOEmbed(value) {
71957
+ if (!value || typeof value !== "object" || Array.isArray(value))
71958
+ return null;
71959
+ const embed = value;
71960
+ for (const key of [
71961
+ "title",
71962
+ "author_name",
71963
+ "provider_name",
71964
+ "thumbnail_url"
71965
+ ]) {
71966
+ if (embed[key] !== undefined && typeof embed[key] !== "string") {
71967
+ return null;
71968
+ }
71969
+ }
71970
+ for (const key of ["thumbnail_width", "thumbnail_height"]) {
71971
+ if (embed[key] !== undefined && (typeof embed[key] !== "number" || !Number.isFinite(embed[key]) || embed[key] < 0)) {
71972
+ return null;
71973
+ }
71974
+ }
71975
+ return embed;
71976
+ }
71977
+ function redditOEmbedImage(embed) {
71978
+ if (!embed?.thumbnail_url) {
71979
+ return;
71980
+ }
71981
+ let url;
71982
+ try {
71983
+ url = new URL(embed.thumbnail_url);
71984
+ } catch {
71985
+ return;
71986
+ }
71987
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
71988
+ return;
71989
+ }
71990
+ return [
71991
+ {
71992
+ url: url.href,
71993
+ width: embed.thumbnail_width,
71994
+ height: embed.thumbnail_height
71995
+ }
71996
+ ];
71997
+ }
71998
+ function redditPostImages(images) {
71999
+ const allowedHosts = new Set([
72000
+ "i.redd.it",
72001
+ "preview.redd.it",
72002
+ "external-preview.redd.it"
72003
+ ]);
72004
+ const filtered = images?.filter((image) => {
72005
+ try {
72006
+ return allowedHosts.has(new URL(image.url).hostname.toLowerCase());
72007
+ } catch {
72008
+ return false;
72009
+ }
72010
+ });
72011
+ return filtered?.length ? filtered : undefined;
72012
+ }
72013
+ function normalizeDescription(value) {
72014
+ return value.replace(/\r\n?/g, `
72015
+ `).replace(/\u00a0/g, " ").split(`
72016
+ `).map((line) => line.replace(/[\t\p{Zs}]+/gu, " ").trim()).join(`
72017
+ `).replace(/^\n+|\n+$/g, "").replace(/\n{3,}/g, `
72018
+
72019
+ `);
72020
+ }
71896
72021
  function tweetToPageObject(status) {
71897
72022
  const tweet = status?.tweet;
71898
72023
  if (status?.code !== 200 || !tweet) {
@@ -71907,7 +72032,7 @@ function tweetToPageObject(status) {
71907
72032
  type: "page",
71908
72033
  title,
71909
72034
  name: "X (formerly Twitter)",
71910
- description: tweet.text ?? "",
72035
+ description: normalizeDescription(tweet.text ?? ""),
71911
72036
  url: tweet.url,
71912
72037
  favicon: "https://x.com/favicon.ico"
71913
72038
  };
@@ -72024,9 +72149,27 @@ var PlatformMetadataSchema = {
72024
72149
  var DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; SockethubBot/${package_default.version}; +https://sockethub.org)`;
72025
72150
  var COMPAT_USER_AGENT = "Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)";
72026
72151
  var TWEET_API_TIMEOUT_MS = 1e4;
72152
+ var REDDIT_OEMBED_URL = "https://www.reddit.com/oembed";
72153
+ var REDDIT_OEMBED_TIMEOUT_MS = 4000;
72154
+ var SCRAPE_TIMEOUT_MS = 5000;
72155
+ var REDDIT_JSON_TIMEOUT_MS = 2500;
72156
+ var REDDIT_JSON_MAX_BYTES = 1e6;
72157
+ function withDeadline(promise, timeoutMs) {
72158
+ return new Promise((resolve, reject) => {
72159
+ const timer = setTimeout(() => reject(new Error(`metadata scrape timed out after ${timeoutMs}ms`)), timeoutMs);
72160
+ promise.then((value) => {
72161
+ clearTimeout(timer);
72162
+ resolve(value);
72163
+ }, (err) => {
72164
+ clearTimeout(timer);
72165
+ reject(err);
72166
+ });
72167
+ });
72168
+ }
72027
72169
 
72028
72170
  class Metadata {
72029
72171
  log;
72172
+ fetchImpl = import_undici2.fetch;
72030
72173
  dispatcher;
72031
72174
  config = {
72032
72175
  persist: false
@@ -72061,11 +72204,15 @@ class Metadata {
72061
72204
  this.fetchTweet(tweetApiUrl, job, cb);
72062
72205
  return;
72063
72206
  }
72207
+ if (isRedditUrl(job.actor.id)) {
72208
+ this.fetchReddit(job, cb);
72209
+ return;
72210
+ }
72064
72211
  this.scrape(job, cb);
72065
72212
  }
72066
72213
  async fetchTweet(apiUrl, job, cb) {
72067
72214
  try {
72068
- const res = await globalThis.fetch(apiUrl, {
72215
+ const res = await this.fetchImpl(apiUrl, {
72069
72216
  dispatcher: this.getDispatcher(),
72070
72217
  headers: { "user-agent": this.userAgent() },
72071
72218
  signal: AbortSignal.timeout(TWEET_API_TIMEOUT_MS)
@@ -72085,11 +72232,118 @@ class Metadata {
72085
72232
  }
72086
72233
  this.scrape(job, cb);
72087
72234
  }
72088
- scrape(job, cb) {
72235
+ async fetchReddit(job, cb) {
72236
+ const jsonUrl = resolveRedditJson(job.actor.id);
72237
+ const embedPromise = withDeadline(this.fetchRedditEmbed(job.actor.id), REDDIT_JSON_TIMEOUT_MS).catch(() => {
72238
+ return;
72239
+ });
72240
+ if (jsonUrl) {
72241
+ try {
72242
+ this.log.debug(`reddit JSON started for ${job.actor.id}`);
72243
+ const post = parseRedditPost(await withDeadline(this.fetchRedditJson(jsonUrl), REDDIT_JSON_TIMEOUT_MS));
72244
+ if (!post)
72245
+ throw new Error("Reddit JSON returned an invalid payload");
72246
+ job.actor.name = "reddit";
72247
+ job.object = {
72248
+ type: "page",
72249
+ title: post.title,
72250
+ name: "reddit",
72251
+ description: normalizeDescription(post.selftext ?? ""),
72252
+ image: redditPostImage(post),
72253
+ url: job.actor.id,
72254
+ favicon: "/favicon.ico"
72255
+ };
72256
+ this.log.debug(`reddit JSON completed for ${job.actor.id}`);
72257
+ cb(null, job);
72258
+ return;
72259
+ } catch (err) {
72260
+ this.log.debug(`reddit JSON failed for ${job.actor.id}: ${String(err)}; falling back to oEmbed`);
72261
+ }
72262
+ }
72263
+ const embed = await embedPromise;
72264
+ if (embed?.title) {
72265
+ job.actor.name = embed.provider_name || "reddit";
72266
+ job.object = {
72267
+ type: "page",
72268
+ title: embed.title,
72269
+ name: embed.provider_name || "reddit",
72270
+ description: "",
72271
+ image: redditOEmbedImage(embed),
72272
+ url: job.actor.id,
72273
+ favicon: "/favicon.ico"
72274
+ };
72275
+ cb(null, job);
72276
+ return;
72277
+ }
72278
+ cb(new Error(`No Reddit metadata available for ${job.actor.id}`));
72279
+ }
72280
+ fetchRedditJson(url) {
72281
+ return new Promise((resolve, reject) => {
72282
+ const req = httpsRequest(url, {
72283
+ headers: { "user-agent": this.compatUserAgent() }
72284
+ }, (res) => {
72285
+ if (res.statusCode !== 200) {
72286
+ res.resume();
72287
+ reject(new Error(`Reddit JSON returned HTTP ${res.statusCode}`));
72288
+ return;
72289
+ }
72290
+ let bytes = 0;
72291
+ const chunks = [];
72292
+ res.on("data", (chunk) => {
72293
+ bytes += chunk.length;
72294
+ if (bytes > REDDIT_JSON_MAX_BYTES) {
72295
+ req.destroy(new Error("Reddit JSON response too large"));
72296
+ return;
72297
+ }
72298
+ chunks.push(chunk);
72299
+ });
72300
+ res.on("end", () => {
72301
+ try {
72302
+ resolve(JSON.parse(Buffer.concat(chunks).toString()));
72303
+ } catch (err) {
72304
+ reject(err);
72305
+ }
72306
+ });
72307
+ res.on("error", reject);
72308
+ });
72309
+ req.setTimeout(REDDIT_JSON_TIMEOUT_MS, () => {
72310
+ req.destroy(new Error("Reddit JSON request timed out"));
72311
+ });
72312
+ req.on("error", reject);
72313
+ req.end();
72314
+ });
72315
+ }
72316
+ async fetchRedditEmbed(postUrl) {
72317
+ try {
72318
+ const apiUrl = new URL(REDDIT_OEMBED_URL);
72319
+ apiUrl.searchParams.set("url", postUrl);
72320
+ this.log.debug(`reddit oEmbed started for ${postUrl}`);
72321
+ const res = await this.fetchImpl(apiUrl, {
72322
+ dispatcher: this.getDispatcher(),
72323
+ headers: { "user-agent": this.userAgent() },
72324
+ signal: AbortSignal.timeout(REDDIT_OEMBED_TIMEOUT_MS)
72325
+ });
72326
+ if (!res.ok) {
72327
+ throw new Error(`Reddit oEmbed returned HTTP ${res.status}`);
72328
+ }
72329
+ const embed = parseRedditOEmbed(await res.json());
72330
+ if (!embed)
72331
+ throw new Error("Reddit oEmbed returned an invalid payload");
72332
+ const image = redditOEmbedImage(embed);
72333
+ this.log.debug(`reddit oEmbed completed for ${postUrl} (${image ? "thumbnail" : "no thumbnail"})`);
72334
+ return embed;
72335
+ } catch (err) {
72336
+ this.log.debug(`reddit oEmbed fetch failed for ${postUrl}: ${String(err)}; returning a text-only scrape`);
72337
+ return;
72338
+ }
72339
+ }
72340
+ scrape(job, cb, redditEmbed, scrapeUrl = job.actor.id) {
72089
72341
  const userAgent = isRedditUrl(job.actor.id) ? this.compatUserAgent() : this.userAgent();
72090
72342
  const dispatcher = this.getDispatcher();
72091
- import_open_graph_scraper.default({
72092
- url: job.actor.id,
72343
+ this.log.debug(`scrape started for ${job.actor.id} via ${scrapeUrl}`);
72344
+ const options = {
72345
+ url: scrapeUrl,
72346
+ timeout: SCRAPE_TIMEOUT_MS / 1000,
72093
72347
  urlValidatorSettings: {
72094
72348
  allow_fragments: true,
72095
72349
  allow_protocol_relative_urls: false,
@@ -72108,33 +72362,72 @@ class Metadata {
72108
72362
  dispatcher,
72109
72363
  headers: { "user-agent": userAgent }
72110
72364
  }
72111
- }).then((data) => {
72365
+ };
72366
+ const scrape = isRedditUrl(job.actor.id) ? this.fetchImpl(scrapeUrl, {
72367
+ dispatcher,
72368
+ headers: { "user-agent": userAgent },
72369
+ signal: AbortSignal.timeout(SCRAPE_TIMEOUT_MS)
72370
+ }).then(async (res) => {
72371
+ if (!res.ok) {
72372
+ throw new Error(`metadata scrape returned HTTP ${res.status}`);
72373
+ }
72374
+ return import_open_graph_scraper.default({ html: await res.text() });
72375
+ }) : import_open_graph_scraper.default(options);
72376
+ withDeadline(scrape, SCRAPE_TIMEOUT_MS).then(async (data) => {
72112
72377
  const { result } = data;
72113
- job.actor.id = result.ogUrl || job.actor.id;
72114
- job.actor.name = result.ogSiteName || job.actor.name || "";
72378
+ this.log.debug(`scrape completed for ${job.actor.id}`);
72379
+ const reddit = isRedditUrl(job.actor.id);
72380
+ const embed = reddit ? await redditEmbed : undefined;
72381
+ if (!reddit)
72382
+ job.actor.id = result.ogUrl || job.actor.id;
72383
+ job.actor.name = reddit ? embed?.provider_name ?? "reddit" : result.ogSiteName ?? job.actor.name ?? "";
72115
72384
  job.object = {
72116
72385
  type: "page",
72117
72386
  language: result.ogLocale,
72118
- title: result.ogTitle,
72119
- name: result.ogSiteName,
72120
- description: result.ogDescription || "",
72121
- image: result.ogImage,
72122
- url: result.ogUrl,
72387
+ title: embed?.title ?? result.ogTitle,
72388
+ name: embed?.provider_name ?? result.ogSiteName,
72389
+ description: normalizeDescription(result.ogDescription || ""),
72390
+ image: reddit ? redditPostImages(result.ogImage) ?? redditOEmbedImage(embed ?? {}) : result.ogImage,
72391
+ url: reddit ? job.actor.id : result.ogUrl,
72123
72392
  favicon: result.favicon || "/favicon.ico",
72124
72393
  charset: result.charset
72125
72394
  };
72126
72395
  cb(null, job);
72127
- }).catch((data) => {
72128
- cb(toError(data?.result?.error ?? data));
72396
+ }).catch(async (data) => {
72397
+ const err = toError(data?.result?.error ?? data);
72398
+ this.log.debug(`scrape failed for ${job.actor.id}: ${String(err)}`);
72399
+ if (isRedditUrl(job.actor.id)) {
72400
+ const embed = await redditEmbed;
72401
+ if (embed?.title) {
72402
+ job.actor.name = embed.provider_name || "reddit";
72403
+ job.object = {
72404
+ type: "page",
72405
+ title: embed.title,
72406
+ name: embed.provider_name || "reddit",
72407
+ description: "",
72408
+ image: redditOEmbedImage(embed),
72409
+ url: job.actor.id,
72410
+ favicon: "/favicon.ico"
72411
+ };
72412
+ this.log.debug(`using reddit oEmbed fallback for ${job.actor.id}`);
72413
+ cb(null, job);
72414
+ return;
72415
+ }
72416
+ }
72417
+ cb(err);
72129
72418
  });
72130
72419
  }
72131
72420
  cleanup(cb) {
72132
- this.dispatcher?.close().catch(() => {});
72421
+ const close = this.dispatcher?.close;
72422
+ if (typeof close === "function") {
72423
+ close.call(this.dispatcher).catch(() => {});
72424
+ }
72133
72425
  cb();
72134
72426
  }
72135
72427
  }
72136
72428
  export {
72429
+ withDeadline,
72137
72430
  Metadata as default
72138
72431
  };
72139
72432
 
72140
- //# debugId=A271557344D4528064756E2164756E21
72433
+ //# debugId=119F74A1FCE6AC3B64756E2164756E21