@sockethub/platform-metadata 1.0.1-alpha.13 → 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 +6 -6
- package/dist/index.d.ts +12 -0
- package/dist/index.js +322 -29
- package/dist/index.js.map +6 -6
- package/dist/resolvers.d.ts +39 -0
- package/package.json +7 -6
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**
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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
|
|
66297
|
+
function isIP2(str, version = "") {
|
|
66298
66298
|
assertString(str);
|
|
66299
66299
|
version = String(version);
|
|
66300
66300
|
if (!version) {
|
|
66301
|
-
return
|
|
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 (!
|
|
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.
|
|
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.
|
|
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.
|
|
71861
|
+
"@sockethub/schemas": "^3.0.0-alpha.20"
|
|
71846
71862
|
},
|
|
71847
71863
|
peerDependencies: {
|
|
71848
|
-
"@sockethub/server": "^5.0.0-alpha.
|
|
71864
|
+
"@sockethub/server": "^5.0.0-alpha.20"
|
|
71849
71865
|
},
|
|
71850
|
-
gitHead: "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
72092
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
72114
|
-
|
|
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
|
-
|
|
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
|
|
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=
|
|
72433
|
+
//# debugId=119F74A1FCE6AC3B64756E2164756E21
|