@sockethub/platform-metadata 1.0.1-alpha.17 → 1.0.1-alpha.18
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.d.ts +2 -0
- package/dist/index.js +135 -16
- package/dist/index.js.map +4 -4
- package/dist/resolvers.d.ts +30 -0
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -160,6 +160,8 @@ export default class Metadata implements PlatformInterface {
|
|
|
160
160
|
private userAgent;
|
|
161
161
|
private compatUserAgent;
|
|
162
162
|
fetch(job: ActivityStream, cb: PlatformCallback): void;
|
|
163
|
+
/** Fetch and validate YouTube's official preview metadata. */
|
|
164
|
+
private fetchYouTubeEmbed;
|
|
163
165
|
private fetchTweet;
|
|
164
166
|
private fetchReddit;
|
|
165
167
|
/**
|
package/dist/index.js
CHANGED
|
@@ -71813,7 +71813,7 @@ var import_undici2 = __toESM(require_undici(), 1);
|
|
|
71813
71813
|
// package.json
|
|
71814
71814
|
var package_default = {
|
|
71815
71815
|
name: "@sockethub/platform-metadata",
|
|
71816
|
-
version: "1.0.1-alpha.
|
|
71816
|
+
version: "1.0.1-alpha.18",
|
|
71817
71817
|
description: "Fetch metadata from a given URL",
|
|
71818
71818
|
author: "Nick Jennings <nick@silverbucket.net>",
|
|
71819
71819
|
license: "LGPL-3.0+",
|
|
@@ -71855,17 +71855,17 @@ var package_default = {
|
|
|
71855
71855
|
"clean:deps": "rm -rf node_modules"
|
|
71856
71856
|
},
|
|
71857
71857
|
dependencies: {
|
|
71858
|
-
"@sockethub/util": "^1.0.0-alpha.
|
|
71858
|
+
"@sockethub/util": "^1.0.0-alpha.11",
|
|
71859
71859
|
"open-graph-scraper": "^6.11.0",
|
|
71860
71860
|
undici: "^7.16.0"
|
|
71861
71861
|
},
|
|
71862
71862
|
devDependencies: {
|
|
71863
|
-
"@sockethub/schemas": "^3.0.0-alpha.
|
|
71863
|
+
"@sockethub/schemas": "^3.0.0-alpha.23"
|
|
71864
71864
|
},
|
|
71865
71865
|
peerDependencies: {
|
|
71866
|
-
"@sockethub/server": "^5.0.0-alpha.
|
|
71866
|
+
"@sockethub/server": "^5.0.0-alpha.23"
|
|
71867
71867
|
},
|
|
71868
|
-
gitHead: "
|
|
71868
|
+
gitHead: "a6d303097640e82c7d1a7cef6dea2e6dae5f2cac",
|
|
71869
71869
|
types: "./dist/index.d.ts"
|
|
71870
71870
|
};
|
|
71871
71871
|
|
|
@@ -71878,6 +71878,12 @@ var TWITTER_HOSTS = new Set([
|
|
|
71878
71878
|
"www.x.com",
|
|
71879
71879
|
"mobile.x.com"
|
|
71880
71880
|
]);
|
|
71881
|
+
var YOUTUBE_HOSTS = new Set([
|
|
71882
|
+
"youtube.com",
|
|
71883
|
+
"www.youtube.com",
|
|
71884
|
+
"m.youtube.com",
|
|
71885
|
+
"music.youtube.com"
|
|
71886
|
+
]);
|
|
71881
71887
|
var REDDIT_HOSTS = new Set([
|
|
71882
71888
|
"reddit.com",
|
|
71883
71889
|
"www.reddit.com",
|
|
@@ -71901,6 +71907,66 @@ function resolveTwitterStatus(url) {
|
|
|
71901
71907
|
}
|
|
71902
71908
|
return `https://api.fxtwitter.com/status/${match[1]}`;
|
|
71903
71909
|
}
|
|
71910
|
+
function resolveYouTubeOEmbed(url) {
|
|
71911
|
+
let parsed;
|
|
71912
|
+
try {
|
|
71913
|
+
parsed = new URL(url);
|
|
71914
|
+
} catch {
|
|
71915
|
+
return null;
|
|
71916
|
+
}
|
|
71917
|
+
const host = parsed.hostname.toLowerCase();
|
|
71918
|
+
let videoId;
|
|
71919
|
+
if (YOUTUBE_HOSTS.has(host)) {
|
|
71920
|
+
if (parsed.pathname === "/watch") {
|
|
71921
|
+
videoId = parsed.searchParams.get("v");
|
|
71922
|
+
} else {
|
|
71923
|
+
videoId = parsed.pathname.match(/^\/(?:shorts|live|embed)\/([A-Za-z0-9_-]{11})(?:\/|$)/)?.[1];
|
|
71924
|
+
}
|
|
71925
|
+
} else if (host === "youtu.be") {
|
|
71926
|
+
videoId = parsed.pathname.match(/^\/([A-Za-z0-9_-]{11})(?:\/|$)/)?.[1];
|
|
71927
|
+
}
|
|
71928
|
+
if (!videoId || !/^[A-Za-z0-9_-]{11}$/.test(videoId))
|
|
71929
|
+
return null;
|
|
71930
|
+
const canonical = new URL("https://www.youtube.com/watch");
|
|
71931
|
+
canonical.searchParams.set("v", videoId);
|
|
71932
|
+
const endpoint = new URL("https://www.youtube.com/oembed");
|
|
71933
|
+
endpoint.searchParams.set("url", canonical.href);
|
|
71934
|
+
endpoint.searchParams.set("format", "json");
|
|
71935
|
+
return endpoint.href;
|
|
71936
|
+
}
|
|
71937
|
+
function parseYouTubeOEmbed(value) {
|
|
71938
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
71939
|
+
return null;
|
|
71940
|
+
const embed = value;
|
|
71941
|
+
if (typeof embed.title !== "string" || !embed.title || typeof embed.thumbnail_url !== "string" || !embed.thumbnail_url) {
|
|
71942
|
+
return null;
|
|
71943
|
+
}
|
|
71944
|
+
if (embed.provider_name !== undefined && typeof embed.provider_name !== "string") {
|
|
71945
|
+
return null;
|
|
71946
|
+
}
|
|
71947
|
+
for (const key of ["thumbnail_width", "thumbnail_height"]) {
|
|
71948
|
+
if (embed[key] !== undefined && (typeof embed[key] !== "number" || !Number.isFinite(embed[key]) || embed[key] < 0)) {
|
|
71949
|
+
return null;
|
|
71950
|
+
}
|
|
71951
|
+
}
|
|
71952
|
+
try {
|
|
71953
|
+
const thumbnail = new URL(embed.thumbnail_url);
|
|
71954
|
+
if (!["http:", "https:"].includes(thumbnail.protocol))
|
|
71955
|
+
return null;
|
|
71956
|
+
} catch {
|
|
71957
|
+
return null;
|
|
71958
|
+
}
|
|
71959
|
+
return embed;
|
|
71960
|
+
}
|
|
71961
|
+
function youtubeOEmbedImage(embed) {
|
|
71962
|
+
return embed ? [
|
|
71963
|
+
{
|
|
71964
|
+
url: embed.thumbnail_url,
|
|
71965
|
+
width: embed.thumbnail_width,
|
|
71966
|
+
height: embed.thumbnail_height
|
|
71967
|
+
}
|
|
71968
|
+
] : undefined;
|
|
71969
|
+
}
|
|
71904
71970
|
function isRedditUrl(url) {
|
|
71905
71971
|
let parsed;
|
|
71906
71972
|
try {
|
|
@@ -72026,15 +72092,21 @@ function tweetToPageObject(status) {
|
|
|
72026
72092
|
return null;
|
|
72027
72093
|
}
|
|
72028
72094
|
const author = tweet.author ?? {};
|
|
72029
|
-
const
|
|
72095
|
+
const article = tweet.article;
|
|
72096
|
+
const articleBlocks = article?.content?.blocks;
|
|
72097
|
+
const articleBody = Array.isArray(articleBlocks) ? articleBlocks.map((block) => block?.type !== "atomic" && typeof block?.text === "string" ? block.text.trim() : "").filter(Boolean).join(`
|
|
72098
|
+
|
|
72099
|
+
`) : undefined;
|
|
72100
|
+
const title = typeof article?.title === "string" && article.title || (author.name && author.screen_name ? `${author.name} (@${author.screen_name}) on X` : author.name ?? "Post on X");
|
|
72030
72101
|
const photo = tweet.media?.photos?.[0];
|
|
72031
72102
|
const video = tweet.media?.videos?.[0];
|
|
72032
|
-
const
|
|
72103
|
+
const articleImage = article?.cover_media?.media_info;
|
|
72104
|
+
const imageUrl = articleImage?.original_img_url ?? photo?.url ?? video?.thumbnail_url;
|
|
72033
72105
|
const page = {
|
|
72034
72106
|
type: "page",
|
|
72035
72107
|
title,
|
|
72036
72108
|
name: "X (formerly Twitter)",
|
|
72037
|
-
description: normalizeDescription(tweet.text
|
|
72109
|
+
description: normalizeDescription(articleBody || (typeof article?.preview_text === "string" && article.preview_text.trim() ? article.preview_text : "") || tweet.text || ""),
|
|
72038
72110
|
url: tweet.url,
|
|
72039
72111
|
favicon: "https://x.com/favicon.ico"
|
|
72040
72112
|
};
|
|
@@ -72042,8 +72114,8 @@ function tweetToPageObject(status) {
|
|
|
72042
72114
|
page.image = [
|
|
72043
72115
|
{
|
|
72044
72116
|
url: imageUrl,
|
|
72045
|
-
width: photo?.width ?? video?.width,
|
|
72046
|
-
height: photo?.height ?? video?.height
|
|
72117
|
+
width: articleImage?.original_img_width ?? photo?.width ?? video?.width,
|
|
72118
|
+
height: articleImage?.original_img_height ?? photo?.height ?? video?.height
|
|
72047
72119
|
}
|
|
72048
72120
|
];
|
|
72049
72121
|
}
|
|
@@ -72153,6 +72225,7 @@ var COMPAT_USER_AGENT = "Mozilla/5.0 (compatible; Discordbot/2.0; +https://disco
|
|
|
72153
72225
|
var TWEET_API_TIMEOUT_MS = 1e4;
|
|
72154
72226
|
var REDDIT_OEMBED_URL = "https://www.reddit.com/oembed";
|
|
72155
72227
|
var REDDIT_OEMBED_TIMEOUT_MS = 4000;
|
|
72228
|
+
var YOUTUBE_OEMBED_TIMEOUT_MS = 4000;
|
|
72156
72229
|
var SCRAPE_TIMEOUT_MS = 5000;
|
|
72157
72230
|
var REDDIT_JSON_TIMEOUT_MS = 2500;
|
|
72158
72231
|
var REDDIT_JSON_MAX_BYTES = 1e6;
|
|
@@ -72210,8 +72283,36 @@ class Metadata {
|
|
|
72210
72283
|
this.fetchReddit(job, cb);
|
|
72211
72284
|
return;
|
|
72212
72285
|
}
|
|
72286
|
+
const youtubeOEmbedUrl = resolveYouTubeOEmbed(job.actor.id);
|
|
72287
|
+
if (youtubeOEmbedUrl) {
|
|
72288
|
+
const embed = withDeadline(this.fetchYouTubeEmbed(youtubeOEmbedUrl), YOUTUBE_OEMBED_TIMEOUT_MS).catch(() => {
|
|
72289
|
+
return;
|
|
72290
|
+
});
|
|
72291
|
+
this.scrape(job, cb, undefined, job.actor.id, embed);
|
|
72292
|
+
return;
|
|
72293
|
+
}
|
|
72213
72294
|
this.scrape(job, cb);
|
|
72214
72295
|
}
|
|
72296
|
+
async fetchYouTubeEmbed(embedUrl) {
|
|
72297
|
+
try {
|
|
72298
|
+
const res = await this.fetchImpl(embedUrl, {
|
|
72299
|
+
dispatcher: this.getDispatcher(),
|
|
72300
|
+
headers: { "user-agent": this.userAgent() },
|
|
72301
|
+
signal: AbortSignal.timeout(YOUTUBE_OEMBED_TIMEOUT_MS)
|
|
72302
|
+
});
|
|
72303
|
+
if (!res.ok) {
|
|
72304
|
+
throw new Error(`YouTube oEmbed returned HTTP ${res.status}`);
|
|
72305
|
+
}
|
|
72306
|
+
const embed = parseYouTubeOEmbed(await res.json());
|
|
72307
|
+
if (!embed) {
|
|
72308
|
+
throw new Error("YouTube oEmbed returned an invalid payload");
|
|
72309
|
+
}
|
|
72310
|
+
return embed;
|
|
72311
|
+
} catch (err) {
|
|
72312
|
+
this.log.debug(`youtube oEmbed fetch failed for ${embedUrl}: ${String(err)}; using scraped metadata`);
|
|
72313
|
+
return;
|
|
72314
|
+
}
|
|
72315
|
+
}
|
|
72215
72316
|
async fetchTweet(apiUrl, job, cb) {
|
|
72216
72317
|
try {
|
|
72217
72318
|
const res = await this.fetchImpl(apiUrl, {
|
|
@@ -72339,8 +72440,9 @@ class Metadata {
|
|
|
72339
72440
|
return;
|
|
72340
72441
|
}
|
|
72341
72442
|
}
|
|
72342
|
-
scrape(job, cb, redditEmbed, scrapeUrl = job.actor.id) {
|
|
72343
|
-
const
|
|
72443
|
+
scrape(job, cb, redditEmbed, scrapeUrl = job.actor.id, youtubeEmbed) {
|
|
72444
|
+
const useCompatUserAgent = isRedditUrl(job.actor.id) || Boolean(resolveYouTubeOEmbed(job.actor.id));
|
|
72445
|
+
const userAgent = useCompatUserAgent ? this.compatUserAgent() : this.userAgent();
|
|
72344
72446
|
const dispatcher = this.getDispatcher();
|
|
72345
72447
|
this.log.debug(`scrape started for ${job.actor.id} via ${scrapeUrl}`);
|
|
72346
72448
|
const options = {
|
|
@@ -72380,16 +72482,17 @@ class Metadata {
|
|
|
72380
72482
|
this.log.debug(`scrape completed for ${job.actor.id}`);
|
|
72381
72483
|
const reddit = isRedditUrl(job.actor.id);
|
|
72382
72484
|
const embed = reddit ? await redditEmbed : undefined;
|
|
72485
|
+
const youtube = await youtubeEmbed;
|
|
72383
72486
|
if (!reddit)
|
|
72384
72487
|
job.actor.id = result.ogUrl || job.actor.id;
|
|
72385
72488
|
job.actor.name = reddit ? embed?.provider_name ?? "reddit" : result.ogSiteName ?? job.actor.name ?? "";
|
|
72386
72489
|
job.object = {
|
|
72387
72490
|
type: "page",
|
|
72388
72491
|
language: result.ogLocale,
|
|
72389
|
-
title: embed?.title ?? result.ogTitle,
|
|
72390
|
-
name: embed?.provider_name ?? result.ogSiteName,
|
|
72492
|
+
title: embed?.title ?? youtube?.title ?? result.ogTitle,
|
|
72493
|
+
name: embed?.provider_name ?? youtube?.provider_name ?? result.ogSiteName,
|
|
72391
72494
|
description: normalizeDescription(result.ogDescription || ""),
|
|
72392
|
-
image: reddit ? redditPostImages(result.ogImage) ?? redditOEmbedImage(embed ?? {}) : result.ogImage,
|
|
72495
|
+
image: reddit ? redditPostImages(result.ogImage) ?? redditOEmbedImage(embed ?? {}) : result.ogImage?.length ? result.ogImage : youtubeOEmbedImage(youtube),
|
|
72393
72496
|
url: reddit ? job.actor.id : result.ogUrl,
|
|
72394
72497
|
favicon: result.favicon || "/favicon.ico",
|
|
72395
72498
|
charset: result.charset
|
|
@@ -72416,6 +72519,22 @@ class Metadata {
|
|
|
72416
72519
|
return;
|
|
72417
72520
|
}
|
|
72418
72521
|
}
|
|
72522
|
+
const youtube = await youtubeEmbed;
|
|
72523
|
+
if (youtube) {
|
|
72524
|
+
job.actor.name = youtube.provider_name || "YouTube";
|
|
72525
|
+
job.object = {
|
|
72526
|
+
type: "page",
|
|
72527
|
+
title: youtube.title,
|
|
72528
|
+
name: youtube.provider_name || "YouTube",
|
|
72529
|
+
description: "",
|
|
72530
|
+
image: youtubeOEmbedImage(youtube),
|
|
72531
|
+
url: job.actor.id,
|
|
72532
|
+
favicon: "/favicon.ico"
|
|
72533
|
+
};
|
|
72534
|
+
this.log.debug(`using youtube oEmbed fallback for ${job.actor.id}`);
|
|
72535
|
+
cb(null, job);
|
|
72536
|
+
return;
|
|
72537
|
+
}
|
|
72419
72538
|
cb(err);
|
|
72420
72539
|
});
|
|
72421
72540
|
}
|
|
@@ -72432,4 +72551,4 @@ export {
|
|
|
72432
72551
|
withDeadline
|
|
72433
72552
|
};
|
|
72434
72553
|
|
|
72435
|
-
//# debugId=
|
|
72554
|
+
//# debugId=98F3FC3874006CC764756E2164756E21
|