@sockethub/platform-metadata 1.0.1-beta.0 → 1.0.1-beta.1
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 +13 -4
- package/dist/index.d.ts +6 -0
- package/dist/index.js +76 -8
- package/dist/index.js.map +5 -5
- package/dist/resolvers.d.ts +24 -2
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -101,8 +101,12 @@ so those URLs are resolved through preview services built for this purpose:
|
|
|
101
101
|
self-text, and direct `i.redd.it` image are mapped into the preview; text posts
|
|
102
102
|
intentionally have no image. oEmbed remains a title-only failure fallback.
|
|
103
103
|
See `compatUserAgent` below.
|
|
104
|
-
* **Facebook** remains best-effort: post text usually comes through
|
|
105
|
-
|
|
104
|
+
* **Facebook** remains best-effort: post text usually comes through. The
|
|
105
|
+
video or photo thumbnail is taken from the post's Open Graph image, then
|
|
106
|
+
its Twitter card image, and finally the image rendered by Facebook's public
|
|
107
|
+
embed plugin (`/plugins/video.php` or `/plugins/post.php`), which Facebook
|
|
108
|
+
serves to third parties when it withholds Open Graph media from a
|
|
109
|
+
datacenter address. Full media still requires authentication.
|
|
106
110
|
|
|
107
111
|
### `userAgent`
|
|
108
112
|
|
|
@@ -151,9 +155,14 @@ Because the server fetches whatever URL a client supplies as `actor.id`, metadat
|
|
|
151
155
|
requests are hardened against server-side request forgery (SSRF) and resource
|
|
152
156
|
exhaustion. The fetch runs through a guarded connection layer that:
|
|
153
157
|
|
|
158
|
+
* rejects URL schemes other than `http` and `https`;
|
|
154
159
|
* refuses to connect to loopback, private, link-local, carrier-grade NAT, or
|
|
155
|
-
cloud-metadata addresses (e.g. `169.254.169.254`),
|
|
156
|
-
|
|
160
|
+
cloud-metadata addresses (e.g. `169.254.169.254`), validated at connection
|
|
161
|
+
time so it also covers redirect hops. IPv4-mapped, IPv4-compatible, and
|
|
162
|
+
well-known NAT64 (`64:ff9b::/96`) embeddings are judged as the embedded IPv4
|
|
163
|
+
address. The local-use NAT64 prefix `64:ff9b:1::/48` is refused entirely.
|
|
164
|
+
The ranges are listed under
|
|
165
|
+
[Outbound URL guard](../../docs/configuration.md#outbound-url-guard);
|
|
157
166
|
* caps the response body size.
|
|
158
167
|
|
|
159
168
|
### `allowPrivateAddresses`
|
package/dist/index.d.ts
CHANGED
|
@@ -168,6 +168,12 @@ export default class Metadata implements PlatformInterface {
|
|
|
168
168
|
private fetchDirectImage;
|
|
169
169
|
/** Fetch and validate YouTube's official preview metadata. */
|
|
170
170
|
private fetchYouTubeEmbed;
|
|
171
|
+
/**
|
|
172
|
+
* Last-resort thumbnail for a Facebook post whose page carried no
|
|
173
|
+
* Open Graph image: the media image rendered by Facebook's public embed
|
|
174
|
+
* plugin, which is served to third parties without a login wall.
|
|
175
|
+
*/
|
|
176
|
+
private fetchFacebookEmbedImage;
|
|
171
177
|
private fetchTweet;
|
|
172
178
|
private fetchReddit;
|
|
173
179
|
/**
|
package/dist/index.js
CHANGED
|
@@ -74222,7 +74222,7 @@ var import_undici3 = __toESM(require_undici(), 1);
|
|
|
74222
74222
|
// package.json
|
|
74223
74223
|
var package_default = {
|
|
74224
74224
|
name: "@sockethub/platform-metadata",
|
|
74225
|
-
version: "1.0.1-beta.
|
|
74225
|
+
version: "1.0.1-beta.1",
|
|
74226
74226
|
description: "Fetch metadata from a given URL",
|
|
74227
74227
|
author: "Nick Jennings <nick@silverbucket.net>",
|
|
74228
74228
|
license: "LGPL-3.0+",
|
|
@@ -74264,17 +74264,17 @@ var package_default = {
|
|
|
74264
74264
|
"clean:deps": "rm -rf node_modules"
|
|
74265
74265
|
},
|
|
74266
74266
|
dependencies: {
|
|
74267
|
-
"@sockethub/util": "^1.0.0-beta.
|
|
74267
|
+
"@sockethub/util": "^1.0.0-beta.1",
|
|
74268
74268
|
"open-graph-scraper": "^6.11.0",
|
|
74269
74269
|
undici: "^7.30.0"
|
|
74270
74270
|
},
|
|
74271
74271
|
devDependencies: {
|
|
74272
|
-
"@sockethub/schemas": "^3.0.0-beta.
|
|
74272
|
+
"@sockethub/schemas": "^3.0.0-beta.1"
|
|
74273
74273
|
},
|
|
74274
74274
|
peerDependencies: {
|
|
74275
|
-
"@sockethub/server": "^5.0.0-beta.
|
|
74275
|
+
"@sockethub/server": "^5.0.0-beta.1"
|
|
74276
74276
|
},
|
|
74277
|
-
gitHead: "
|
|
74277
|
+
gitHead: "06c9062f1cb5f0c0599fd979e6e91da79f08eda0",
|
|
74278
74278
|
types: "./dist/index.d.ts"
|
|
74279
74279
|
};
|
|
74280
74280
|
|
|
@@ -74447,7 +74447,35 @@ function isFacebookUrl(url) {
|
|
|
74447
74447
|
return FACEBOOK_HOSTS.has(host) || host === "fb.watch";
|
|
74448
74448
|
}
|
|
74449
74449
|
function stripFacebookEngagement(title) {
|
|
74450
|
-
|
|
74450
|
+
const bar = title?.indexOf("|") ?? -1;
|
|
74451
|
+
if (title === undefined || bar === -1) {
|
|
74452
|
+
return title;
|
|
74453
|
+
}
|
|
74454
|
+
const segment = title.slice(0, bar);
|
|
74455
|
+
const dot = segment.lastIndexOf("·");
|
|
74456
|
+
const isStats = dot !== -1 && /\p{Nd}/u.test(segment.slice(0, dot)) || /^\s*(?:\p{Nd}+[.,]\p{Nd}+(?:[.,\s]\p{Nd}+)*\p{L}{0,2}|\p{Nd}{1,3}\p{L}{1,2}|\p{Nd}{4,})(?:\s+\p{L}+\.)?\s+\p{L}+\s*$/u.test(segment);
|
|
74457
|
+
return isStats ? title.slice(bar + 1).trimStart() : title;
|
|
74458
|
+
}
|
|
74459
|
+
function resolveFacebookEmbed(url, ogType) {
|
|
74460
|
+
if (!isFacebookUrl(url)) {
|
|
74461
|
+
return null;
|
|
74462
|
+
}
|
|
74463
|
+
const isVideo = ogType?.startsWith("video") || /^\/(?:reel|watch)(?:\/|$)|\/videos\//i.test(new URL(url).pathname);
|
|
74464
|
+
const plugin = isVideo ? "video.php" : "post.php";
|
|
74465
|
+
return `https://www.facebook.com/plugins/${plugin}?href=${encodeURIComponent(url)}`;
|
|
74466
|
+
}
|
|
74467
|
+
var FACEBOOK_EMBED_MAX_CHARS = 500000;
|
|
74468
|
+
function extractFacebookEmbedImage(html) {
|
|
74469
|
+
const match = html.slice(0, FACEBOOK_EMBED_MAX_CHARS).match(/<img\b[^>]*\bsrc="(https:\/\/[^"]*\.fbcdn\.net\/[^"]*)"/i);
|
|
74470
|
+
if (!match) {
|
|
74471
|
+
return;
|
|
74472
|
+
}
|
|
74473
|
+
const url = match[1].replace(/&/g, "&");
|
|
74474
|
+
try {
|
|
74475
|
+
return new URL(url).hostname.endsWith(".fbcdn.net") ? url : undefined;
|
|
74476
|
+
} catch {
|
|
74477
|
+
return;
|
|
74478
|
+
}
|
|
74451
74479
|
}
|
|
74452
74480
|
function isRedditUrl(url) {
|
|
74453
74481
|
let parsed;
|
|
@@ -74753,6 +74781,7 @@ var TWEET_API_TIMEOUT_MS = 1e4;
|
|
|
74753
74781
|
var REDDIT_OEMBED_URL = "https://www.reddit.com/oembed";
|
|
74754
74782
|
var REDDIT_OEMBED_TIMEOUT_MS = 4000;
|
|
74755
74783
|
var YOUTUBE_OEMBED_TIMEOUT_MS = 4000;
|
|
74784
|
+
var FACEBOOK_EMBED_TIMEOUT_MS = 4000;
|
|
74756
74785
|
var SCRAPE_TIMEOUT_MS = 5000;
|
|
74757
74786
|
var REDDIT_JSON_TIMEOUT_MS = 2500;
|
|
74758
74787
|
var REDDIT_JSON_MAX_BYTES = 1e6;
|
|
@@ -74939,6 +74968,29 @@ class Metadata {
|
|
|
74939
74968
|
return;
|
|
74940
74969
|
}
|
|
74941
74970
|
}
|
|
74971
|
+
async fetchFacebookEmbedImage(embedUrl) {
|
|
74972
|
+
try {
|
|
74973
|
+
const res = await this.fetchImpl(embedUrl, {
|
|
74974
|
+
dispatcher: this.getDispatcher(),
|
|
74975
|
+
headers: { "user-agent": this.userAgent() },
|
|
74976
|
+
signal: AbortSignal.timeout(FACEBOOK_EMBED_TIMEOUT_MS)
|
|
74977
|
+
});
|
|
74978
|
+
if (!res.ok) {
|
|
74979
|
+
await res.body?.cancel().catch(() => {
|
|
74980
|
+
return;
|
|
74981
|
+
});
|
|
74982
|
+
throw new Error(`Facebook embed returned HTTP ${res.status}`);
|
|
74983
|
+
}
|
|
74984
|
+
const image = extractFacebookEmbedImage(await res.text());
|
|
74985
|
+
if (!image) {
|
|
74986
|
+
throw new Error("Facebook embed contained no CDN image");
|
|
74987
|
+
}
|
|
74988
|
+
return image;
|
|
74989
|
+
} catch (err) {
|
|
74990
|
+
this.log.debug(`facebook embed fetch failed for ${embedUrl}: ${String(err)}; using scraped metadata`);
|
|
74991
|
+
return;
|
|
74992
|
+
}
|
|
74993
|
+
}
|
|
74942
74994
|
async fetchTweet(apiUrl, job, cb) {
|
|
74943
74995
|
try {
|
|
74944
74996
|
const res = await this.fetchImpl(apiUrl, {
|
|
@@ -75158,6 +75210,22 @@ class Metadata {
|
|
|
75158
75210
|
this.log.debug(`facebook canonical scrape failed for ${result.ogUrl}: ${String(err)}; using share metadata`);
|
|
75159
75211
|
}
|
|
75160
75212
|
}
|
|
75213
|
+
if (facebook && !result.ogImage?.length) {
|
|
75214
|
+
if (result.twitterImage?.length) {
|
|
75215
|
+
result = { ...result, ogImage: result.twitterImage };
|
|
75216
|
+
} else {
|
|
75217
|
+
const embedUrl = resolveFacebookEmbed(result.ogUrl || job.actor.id, result.ogType);
|
|
75218
|
+
const embedImage = embedUrl ? await withDeadline(this.fetchFacebookEmbedImage(embedUrl), FACEBOOK_EMBED_TIMEOUT_MS).catch(() => {
|
|
75219
|
+
return;
|
|
75220
|
+
}) : undefined;
|
|
75221
|
+
if (embedImage) {
|
|
75222
|
+
result = {
|
|
75223
|
+
...result,
|
|
75224
|
+
ogImage: [{ url: embedImage }]
|
|
75225
|
+
};
|
|
75226
|
+
}
|
|
75227
|
+
}
|
|
75228
|
+
}
|
|
75161
75229
|
const embed = reddit ? await redditEmbed : undefined;
|
|
75162
75230
|
const youtube = await youtubeEmbed;
|
|
75163
75231
|
if (!reddit)
|
|
@@ -75175,7 +75243,7 @@ class Metadata {
|
|
|
75175
75243
|
name: embed?.provider_name ?? youtube?.provider_name ?? siteName,
|
|
75176
75244
|
description: normalizeDescription(youtubeDescription || result.ogDescription || ""),
|
|
75177
75245
|
image: reddit ? redditPostImages(result.ogImage) ?? redditOEmbedImage(embed ?? {}) : scrapedImages?.length ? scrapedImages : youtubeOEmbedImage(youtube),
|
|
75178
|
-
url:
|
|
75246
|
+
url: job.actor.id,
|
|
75179
75247
|
favicon: result.favicon || "/favicon.ico",
|
|
75180
75248
|
charset: result.charset
|
|
75181
75249
|
};
|
|
@@ -75233,4 +75301,4 @@ export {
|
|
|
75233
75301
|
withDeadline
|
|
75234
75302
|
};
|
|
75235
75303
|
|
|
75236
|
-
//# debugId=
|
|
75304
|
+
//# debugId=E636DFFA5AC4B45D64756E2164756E21
|