From 3c1b2c2ec284694228572b5f8b2b74929e521eac Mon Sep 17 00:00:00 2001 From: Cynthia Foxwell Date: Fri, 18 Apr 2025 14:28:16 -0600 Subject: [PATCH] fedimbed.bluesky: finish quotes --- src/modules/fedimbed.js | 396 ++++++++++++++-------------------------- 1 file changed, 139 insertions(+), 257 deletions(-) diff --git a/src/modules/fedimbed.js b/src/modules/fedimbed.js index 53e15e3..9f5ab96 100644 --- a/src/modules/fedimbed.js +++ b/src/modules/fedimbed.js @@ -317,7 +317,7 @@ function getStatsBluesky(post) { } async function blueskyQuoteEmbed(quote) { - const embeds = []; + const images = []; const videos = []; let hidden = false; @@ -341,76 +341,36 @@ async function blueskyQuoteEmbed(quote) { } } - const mainEmbed = { - color: PLATFORM_COLORS.bluesky, - url: `https://bsky.app/profile/${quote.author.handle}/post/${quote.uri.substring(quote.uri.lastIndexOf("/") + 1)}`, - author: {name: "Quoted Post", icon_url: "https://cdn.discordapp.com/emojis/1308640087759654922.png"}, - title: `${quote.author.displayName} (@${quote.author.handle})`, - thumbnail: { - url: quote.author.avatar, - }, - description: quote.value.text, - footer: { - text: `${getStatsBluesky(quote)}\nBluesky`, - icon_url: "https://bsky.app/static/apple-touch-icon.png", - }, - timestamp: quote.value.createdAt, - }; + const components = []; - /*if (quote.author.handle.endsWith(".ap.brid.gy")) { - const handle = quote.author.handle.replace(".ap.brid.gy", ""); - const split = handle.split("."); - - const username = split.shift(); - const domain = split.join("."); - const authorUrl = `https://${domain}/users/${username}`; - - const authorData = await signedFetch(authorUrl, { - headers: { - "User-Agent": FRIENDLY_USERAGENT, - Accept: "application/activity+json", + const header = { + type: 9, + components: [ + { + type: 10, + content: `### <:i:1308640087759654922> Quoted Post\n## ${quote.author.displayName}\n[@${ + quote.author.handle + }](https://bsky.app/profile/${quote.author.did})\n${ + quote.value.facets?.length ?? 0 > 0 + ? processBlueskyFacets(quote.value.text, quote.value.facets) + : quote.value.text + }`.trim(), }, - }) - .then((res) => res.json()) - .catch((err) => { - logger.error("fedimbed", `Failed to get author for "${authorUrl}": ${err}`); - }); - - if (authorData) { - const platform = (await resolvePlatform(authorUrl)) ?? ""; - let color = PLATFORM_COLORS[platform.toLowerCase()]; - let platformName = normalizePlatform(platform); - - const crawled = await getCrawledData(authorUrl, color, platformName); - if (!color && crawled?.color) { - color = crawled.color; - } - if (crawled?.platformName) { - platformName = crawled.platformName; - } - platformName += " [via Bluesky via Bridgy]"; - - mainEmbed.footer = { - text: platformName, - icon_url: crawled?.icon, - }; - mainEmbed.title = `${authorData.name} (${authorData.preferredUsername}@${domain})`; - mainEmbed.color = color; - mainEmbed.thumbnail.url = authorData?.icon?.url ?? quote.author.avatar; - } - }*/ - - if (quote.value.facets?.length > 0) { - mainEmbed.description = processBlueskyFacets(mainEmbed.description, quote.value.facets); - } + ], + accessory: { + type: 11, + media: { + url: quote.author.avatar, + }, + }, + }; + components.push(header); if (quote.embeds?.[0]) { const embed = quote.embeds[0]; switch (embed.$type) { case "app.bsky.embed.images#view": { - embeds.push( - ...embed.images.map((image) => ({...mainEmbed, image: {url: image.fullsize, description: image.alt}})) - ); + images.push(...embed.images.map((image) => ({media: {url: image.fullsize}, description: image.alt}))); break; } case "app.bsky.embed.video#view": { @@ -418,32 +378,18 @@ async function blueskyQuoteEmbed(quote) { const domain = lookup.service.find((service) => service.id === "#atproto_pds").serviceEndpoint; const videoUrl = `${domain}/xrpc/com.atproto.sync.getBlob?did=${quote.author.did}&cid=${embed.cid}`; - const contentType = await fetch(videoUrl, { - method: "HEAD", - }).then((res) => res.headers.get("Content-Type")); - - videos.push({url: videoUrl, desc: embed.alt, type: contentType}); - - embeds.push({...mainEmbed, fields: [{name: "\u200b", value: `[Video Link](${videoUrl})`}]}); + videos.push({media: {url: videoUrl}, description: embed.alt}); break; } case "app.bsky.embed.recordWithMedia#view": { if (embed.media.$type === "app.bsky.embed.images#view") { - embeds.push( - ...embed.media.images.map((image) => ({...mainEmbed, image: {url: image.fullsize, description: image.alt}})) - ); + images.push(...embed.media.images.map((image) => ({media: {url: image.fullsize}, description: image.alt}))); } else if (embed.media.$type === "app.bsky.embed.video#view") { const lookup = await fetch(`https://plc.directory/${quote.author.did}`).then((res) => res.json()); const domain = lookup.service.find((service) => service.id === "#atproto_pds").serviceEndpoint; const videoUrl = `${domain}/xrpc/com.atproto.sync.getBlob?did=${quote.author.did}&cid=${embed.media.cid}`; - const contentType = await fetch(videoUrl, { - method: "HEAD", - }).then((res) => res.headers.get("Content-Type")); - - videos.push({url: videoUrl, desc: embed.alt, type: contentType}); - - embeds.push({...mainEmbed, fields: [{name: "\u200b", value: `[Video Link](${videoUrl})`}]}); + videos.push({media: {url: videoUrl}, description: embed.alt}); } break; } @@ -452,22 +398,37 @@ async function blueskyQuoteEmbed(quote) { const url = new URL(embed.external.uri); url.searchParams.delete("hh"); url.searchParams.delete("ww"); - embeds.push({...mainEmbed, image: {url: url.toString()}}); - } else { - embeds.push(mainEmbed); + images.push({media: {url: url.toString()}}); } break; } - default: { - embeds.push(mainEmbed); - break; - } } - } else { - embeds.push(mainEmbed); } - return {embeds, videos, adult, hidden, spoiler, tags}; + const footer = { + type: 10, + content: `${getStatsBluesky(quote)}\n`, + }; + + if (images.length > 0) { + components.push({ + type: 12, + items: images, + }); + } + for (const video of videos) { + components.push({ + type: 12, + items: [video], + }); + } + components.push(footer); + + const url = `https://bsky.app/profile/${quote.author.did}/post/${quote.uri.substring( + quote.uri.lastIndexOf("/") + 1 + )}`; + + return {components, url, adult, hidden, spoiler, tags}; } async function bluesky(msg, url, spoiler = false) { @@ -529,6 +490,76 @@ async function bluesky(msg, url, spoiler = false) { } } + const videos = []; + const images = []; + let quoteData; + + if (post.embed) { + switch (post.embed.$type) { + case "app.bsky.embed.images#view": { + images.push(post.embed.images.map((image) => ({media: {url: image.fullsize}, description: image.alt}))); + break; + } + case "app.bsky.embed.video#view": { + const lookup = await fetch(`https://plc.directory/${post.author.did}`).then((res) => res.json()); + const domain = lookup.service.find((service) => service.id === "#atproto_pds").serviceEndpoint; + const videoUrl = `${domain}/xrpc/com.atproto.sync.getBlob?did=${post.author.did}&cid=${post.embed.cid}`; + + videos.push({media: {url: videoUrl}, description: post.embed.alt}); + break; + } + case "app.bsky.embed.record#view": { + hasQuote = true; + const quote = post.embed.record; + quoteData = await blueskyQuoteEmbed(quote); + + if (quoteData.adult) adult = true; + if (quoteData.hidden) hidden = true; + if (quoteData.spoiler) spoiler = true; + if (quoteData.tags.length > 0) tags.push(...quoteData.tags); + + break; + } + case "app.bsky.embed.recordWithMedia#view": { + hasQuote = true; + + if (post.embed.media.$type === "app.bsky.embed.images#view") { + images.push(post.embed.media.images.map((image) => ({media: {url: image.fullsize}, description: image.alt}))); + } else if (post.embed.media.$type === "app.bsky.embed.video#view") { + const lookup = await fetch(`https://plc.directory/${post.author.did}`).then((res) => res.json()); + const domain = lookup.service.find((service) => service.id === "#atproto_pds").serviceEndpoint; + const videoUrl = `${domain}/xrpc/com.atproto.sync.getBlob?did=${post.author.did}&cid=${post.embed.media.cid}`; + + videos.push({media: {url: videoUrl}, description: post.embed.alt}); + } else if (post.embed.media.$type === "app.bsky.embed.external#view") { + if (post.embed.media.external.uri.includes("tenor.com")) { + const url = new URL(post.embed.media.external.uri); + url.searchParams.delete("hh"); + url.searchParams.delete("ww"); + } + } + + const quoteData = await blueskyQuoteEmbed(post.embed.record.record); + + if (quoteData.adult) adult = true; + if (quoteData.hidden) hidden = true; + if (quoteData.spoiler) spoiler = true; + if (quoteData.tags.length > 0) tags.push(...quoteData.tags); + + break; + } + case "app.bsky.embed.external#view": { + if (post.embed.external.uri.includes("tenor.com")) { + const url = new URL(post.embed.external.uri); + url.searchParams.delete("hh"); + url.searchParams.delete("ww"); + images.push({media: {url: url.toString()}}); + } + break; + } + } + } + const warnings = []; if (hidden) { warnings.push(":warning: Post marked as hidden"); @@ -545,11 +576,6 @@ async function bluesky(msg, url, spoiler = false) { } } - const videos = []; - const images = []; - //const embeds = []; - //let sendWait = false; - const warningText = { type: 10, content: `## ${warnings.join("\n## ")}`, @@ -595,169 +621,9 @@ async function bluesky(msg, url, spoiler = false) { const footer = { type: 10, - content: `${getStatsBluesky(post)}\n-# Bluesky \u2022 `, + content: `${getStatsBluesky(post)}\n`, }; - /*if (post.author.handle.endsWith(".ap.brid.gy")) { - const handle = post.author.handle.replace(".ap.brid.gy", ""); - const split = handle.split("."); - - const username = split.shift(); - const domain = split.join("."); - const authorUrl = `https://${domain}/@${username}`; - - const authorData = await signedFetch(authorUrl, { - headers: { - "User-Agent": FRIENDLY_USERAGENT, - Accept: "application/activity+json", - }, - }) - .then((res) => res.json()) - .catch((err) => { - logger.error("fedimbed", `Failed to get author for "${authorUrl}": ${err}`); - }); - - if (authorData) { - const platform = (await resolvePlatform(authorUrl)) ?? ""; - let color = PLATFORM_COLORS[platform]; - let platformName = normalizePlatform(platform); - - const crawled = await getCrawledData(authorUrl, color, platformName); - if (!color && crawled?.color) { - color = crawled.color; - } - if (crawled?.platformName) { - platformName = crawled.platformName; - } - platformName += " [via Bluesky via Bridgy]"; - - mainEmbed.footer = { - text: platformName, - icon_url: crawled?.icon, - }; - mainEmbed.title = `${authorData.name} (${authorData.preferredUsername}@${domain})`; - mainEmbed.color = color; - mainEmbed.thumbnail.url = authorData?.icon?.url ?? post.author.avatar; - } - }*/ - - if (post.embed) { - switch (post.embed.$type) { - case "app.bsky.embed.images#view": { - images.push(post.embed.images.map((image) => ({media: {url: image.fullsize}, description: image.alt}))); - break; - } - case "app.bsky.embed.video#view": { - const lookup = await fetch(`https://plc.directory/${post.author.did}`).then((res) => res.json()); - const domain = lookup.service.find((service) => service.id === "#atproto_pds").serviceEndpoint; - const videoUrl = `${domain}/xrpc/com.atproto.sync.getBlob?did=${post.author.did}&cid=${post.embed.cid}`; - - videos.push({media: {url: videoUrl}, description: post.embed.alt}); - break; - } - case "app.bsky.embed.record#view": { - hasQuote = true; - const quote = post.embed.record; - const quoteData = await blueskyQuoteEmbed(quote); - - if (quoteData.videos.length > 0) videos.push(...quoteData.videos); - - //embeds.push(mainEmbed, ...quoteData.embeds); - - if (quoteData.adult) adult = true; - if (quoteData.hidden) hidden = true; - if (quoteData.spoiler) spoiler = true; - if (quoteData.tags.length > 0) tags.push(...quoteData.tags); - - break; - } - case "app.bsky.embed.recordWithMedia#view": { - //hasQuote = true; - - if (post.embed.media.$type === "app.bsky.embed.images#view") { - images.push(post.embed.media.images.map((image) => ({media: {url: image.fullsize}, description: image.alt}))); - } else if (post.embed.media.$type === "app.bsky.embed.video#view") { - const lookup = await fetch(`https://plc.directory/${post.author.did}`).then((res) => res.json()); - const domain = lookup.service.find((service) => service.id === "#atproto_pds").serviceEndpoint; - const videoUrl = `${domain}/xrpc/com.atproto.sync.getBlob?did=${post.author.did}&cid=${post.embed.media.cid}`; - - videos.push({media: {url: videoUrl}, description: post.embed.alt}); - - //embeds.push({...mainEmbed, fields: [{name: "\u200b", value: `[Video Link](${videoUrl})`}]}); - } else if (post.embed.media.$type === "app.bsky.embed.external#view") { - if (post.embed.media.external.uri.includes("tenor.com")) { - const url = new URL(post.embed.media.external.uri); - url.searchParams.delete("hh"); - url.searchParams.delete("ww"); - //embeds.push({...mainEmbed, image: {url: url.toString()}}); - } - } - - const quoteData = await blueskyQuoteEmbed(post.embed.record.record); - if (quoteData.videos.length > 0) videos.push(...quoteData.videos); - //embeds.push(...quoteData.embeds); - - if (quoteData.adult) adult = true; - if (quoteData.hidden) hidden = true; - if (quoteData.spoiler) spoiler = true; - if (quoteData.tags.length > 0) tags.push(...quoteData.tags); - - break; - } - case "app.bsky.embed.external#view": { - if (post.embed.external.uri.includes("tenor.com")) { - const url = new URL(post.embed.external.uri); - url.searchParams.delete("hh"); - url.searchParams.delete("ww"); - images.push({media: {url: url.toString()}}); - } - break; - } - } - } - - /*if (videos.length > 0) { - sendWait = true; - if (msg instanceof Message) await msg.addReaction("\uD83D\uDCE4"); - } - - const files = []; - if (videos.length > 0) { - for (const attachment of videos) { - const size = await fetch(attachment.url, { - method: "HEAD", - headers: { - "User-Agent": FRIENDLY_USERAGENT, - }, - }).then((res) => Number(res.headers.get("Content-Length"))); - - let limit = getUploadLimit(guild); - if (msg.attachmentSizeLimit != null) limit = msg.attachmentSizeLimit; - - if (size <= limit) { - const file = await fetch(attachment.url, { - headers: { - "User-Agent": FRIENDLY_USERAGENT, - }, - }) - .then((res) => res.arrayBuffer()) - .then((buf) => Buffer.from(buf)); - - files.push({ - filename: - (spoiler ? "SPOILER_" : "") + - (attachment.type.indexOf("/") > -1 - ? attachment.type.replace("/", ".").replace("quicktime", "mov") - : attachment.type + "." + (url.match(/\.([a-z0-9]{3,4})$/)?.[0] ?? "mp4")), - file, - description: attachment.desc, - }); - } - } - }*/ - if (images.length > 0) { container.components.push({ type: 12, @@ -774,6 +640,22 @@ async function bluesky(msg, url, spoiler = false) { if (quoteOnly && !hasQuote) return {}; + if (quoteData) container.components.push({type: 14}, ...quoteData.components); + + const buttons = { + type: 1, + components: [ + { + type: 2, + style: 5, + label: "Post", + url, + }, + ], + }; + if (quoteData?.url) buttons.components.push({type: 2, style: 5, label: "Quoted Post", url: quoteData.url}); + container.components.push(buttons); + return { response: { flags: 1 << 15,