From b73f36b22a0d2631678e8cb84173b73ec0d9daf4 Mon Sep 17 00:00:00 2001 From: David Merfield Date: Mon, 25 Nov 2024 14:25:18 +0100 Subject: [PATCH] Adds bluesky embed plugin (#812) * Adds bluesky embed plugin * Updates bluesky tests --- app/build/plugins/bluesky/index.js | 91 ++++++++++++++++++++++++++++++ app/build/plugins/bluesky/tests.js | 45 +++++++++++++++ app/build/plugins/index.js | 2 + 3 files changed, 138 insertions(+) create mode 100644 app/build/plugins/bluesky/index.js create mode 100644 app/build/plugins/bluesky/tests.js diff --git a/app/build/plugins/bluesky/index.js b/app/build/plugins/bluesky/index.js new file mode 100644 index 00000000000..dbd5d11bf71 --- /dev/null +++ b/app/build/plugins/bluesky/index.js @@ -0,0 +1,91 @@ +/* from bluesky docs: + +oEmbed Endpoint + +The official oEmbed endpoint for Bluesky posts is https://embed.bsky.app/oembed, which accepts the following HTTP GET query parameters: + + url (required): bsky.app or AT-URI pointing to a post + format (optional): json is the default and only supported format + maxwidth (optional, integer): range is 220 to 600; default is 600 + maxheight (optional, integer): part of oEmbed specification, but not used for Bluesky post embeds + +The rendered height of posts is not known until rendered, so the maxheight is ignored and the height field in the response JSON is always null. This follows the precedent of Twitter tweet embeds. + +The oEmebd response contains roughly the same HTML snippet as found at embed.bsky.app, with the same public content policy mentioned above. + +The HTTP URL patterns which the oEmbed endpoint supports are: + + https://bsky.app/profile/:user/post/:id: post embeds + +You can learn more about oEmbed at https://oembed.com. Bluesky is a registered provider, included in the JSON directory at https://oembed.com/providers.json. + +*/ + +const each = require("../eachEl"); +const Url = require("url"); +const fetch = require("node-fetch"); + +function render($, callback) { + console.log("bluesky plugin", "render", $.html()); + + each( + $, + "a", + function (el, next) { + var href, host, text, id; + + try { + href = $(el).attr("href"); + text = $(el).text(); + host = Url.parse(href).host; + } catch (e) { + return next(); + } + + // Ensure we managed to extract everything from the url + if (!href || !text || !host) return next(); + + // Look for bare links + if (href !== text) return next(); + + // which point to a post on bluesky + if (host !== "bsky.app") return next(); + + var params = { + url: href, + format: "json", + maxwidth: 600, + }; + + var oembedUrl = + "https://embed.bsky.app/oembed?" + + new URLSearchParams(params).toString(); + + console.log(oembedUrl); + + fetch(oembedUrl) + .then((res) => res.json()) + .then((data) => { + if (!data || !data.html) return next(); + + var html = data.html; + + $(el).replaceWith(html); + next(); + }) + .catch(() => { + return next(); + }); + }, + function () { + callback(); + } + ); +} + +module.exports = { + render: render, + category: "external", + title: "Bluesky", + description: "Embed posts from Bluesky URLs", +}; diff --git a/app/build/plugins/bluesky/tests.js b/app/build/plugins/bluesky/tests.js new file mode 100644 index 00000000000..bee0f6f92c6 --- /dev/null +++ b/app/build/plugins/bluesky/tests.js @@ -0,0 +1,45 @@ +describe("bluesky plugin", function () { + const replaceURLsWithEmbeds = require("./index.js").render; + const cheerio = require("cheerio"); + + it("works", function (done) { + // html bare link to a post on bluesky + const html = + 'https://bsky.app/profile/logicallyjc.bsky.social/post/3lbretguxqk2b'; + + const $ = cheerio.load(html); + + replaceURLsWithEmbeds($, function () { + + console.log('html:', $.html()); + expect($("a[href='https://bsky.app/profile/logicallyjc.bsky.social/post/3lbretguxqk2b']").length).toBe(0); + expect($("blockquote").length).toBe(1); + done(); + }); + }); + + it("does not error when there are no links", function (done) { + const html = "

hello

"; + + const $ = cheerio.load(html); + + replaceURLsWithEmbeds($, function () { + expect($("a").length).toBe(0); + expect($("blockquote").length).toBe(0); + done(); + }); + }); + + // if the bluesky link is invalid or poorly formatted, it should not be replaced + it("does not error when the link is invalid", function (done) { + const html = 'https://bsky.app'; + + const $ = cheerio.load(html); + + replaceURLsWithEmbeds($, function () { + expect($("a").length).toBe(1); + expect($("blockquote").length).toBe(0); + done(); + }); + }); +}); diff --git a/app/build/plugins/index.js b/app/build/plugins/index.js index 788568fe87e..0d89f3877db 100644 --- a/app/build/plugins/index.js +++ b/app/build/plugins/index.js @@ -8,6 +8,7 @@ var extend = require("helper/extend"); var deCamelize = require("helper/deCamelize"); var time = require("helper/time"); var async = require("async"); +const bluesky = require("./bluesky"); // Wait 10 minutes to go to next plugin var TIMEOUT = 10 * 60 * 1000; @@ -20,6 +21,7 @@ var defaultPlugins = {}; var loaded = loadPlugins({ analytics: require("./analytics"), autoImage: require("./autoImage"), + bluesky: require("./bluesky"), codeHighlighting: require("./codeHighlighting"), commento: require("./commento"), disqus: require("./disqus"),