UNPKG

@indiekit/syndicator-mastodon

Version:

Mastodon syndicator for Indiekit

140 lines (120 loc) 4.15 kB
import path from "node:path"; import { IndiekitError } from "@indiekit/error"; import brevity from "brevity"; import { htmlToText } from "html-to-text"; /** * Get status parameters from given JF2 properties * @param {object} properties - JF2 properties * @param {object} [options] - Options * @param {number} [options.characterLimit] - Character limit * @param {boolean} [options.includePermalink] - Include permalink in status * @param {Array} [options.mediaIds] - Mastodon media IDs * @param {string} [options.serverUrl] - Server URL * @returns {object} Status parameters */ export const createStatus = (properties, options = {}) => { const { characterLimit, includePermalink, mediaIds, serverUrl } = options; const parameters = {}; let status; let statusText; if (properties.content && properties.content.html) { statusText = htmlToStatusText(properties.content.html, serverUrl); } if (statusText && properties["repost-of"]) { // If repost of Mastodon URL with content, create a reblog status = `${statusText} ${properties["repost-of"]}`; } else if (properties.name && properties.name !== "") { // Else, if post has a non-empty title, show title with a link to post status = `${properties.name} ${properties.url}`; } else if (statusText) { // Else, post content (converted to plain text) status = statusText; } // Truncate status if longer than 500 characters if (status) { const statusText = brevity.shorten( status, properties.url, includePermalink // https://indieweb.org/permashortlink ? properties.url : false, false, // https://indieweb.org/permashortcitation characterLimit, ); // Show permalink below status, not within brackets parameters.status = statusText.replace( `(${properties.url})`, `\n\n${properties.url}`, ); } // Add media IDs if (mediaIds) { parameters.mediaIds = mediaIds; } // If post is in reply to a status, add respective parameter if (properties["in-reply-to"]) { const inReplyTo = properties["in-reply-to"]; const inReplyToHostname = new URL(inReplyTo).hostname; const serverHostname = new URL(serverUrl).hostname; if (inReplyToHostname === serverHostname) { // Reply to status const statusId = getStatusIdFromUrl(inReplyTo); parameters.inReplyToId = statusId; } else { throw IndiekitError.badRequest("Not a reply to a URL at this target"); } } // If post visibility set, use the same setting when sharing to Mastodon if (properties.visibility) { parameters.visibility = properties.visibility; } return parameters; }; /** * Get status ID from Mastodon status URL * @param {string} url - Mastodon status URL * @returns {string} Status ID */ export const getStatusIdFromUrl = (url) => { const parsedUrl = new URL(url); const statusId = path.basename(parsedUrl.pathname); return statusId; }; /** * Convert HTML to plain text, appending last link href if present * @param {string} html - HTML * @param {string} serverUrl - Server URL, i.e. https://mastodon.social * @returns {string} Text */ export const htmlToStatusText = (html, serverUrl) => { // Get all the link references let hrefs = [...html.matchAll(/href="(https?:\/\/.+?)"/g)]; // Remove any links to Mastodon server // HTML may contain Mastodon usernames or hashtag links hrefs = hrefs.filter((href) => { const hrefHostname = new URL(href[1]).hostname; const serverHostname = new URL(serverUrl).hostname; return hrefHostname !== serverHostname; }); // Get the last link mentioned, or return false const lastHref = hrefs.length > 0 ? hrefs.at(-1)[1] : false; // Convert HTML to plain text, removing any links const text = htmlToText(html, { selectors: [ { selector: "a", options: { ignoreHref: true, }, }, { selector: "img", format: "skip", }, ], wordwrap: false, }); // Append the last link if present const statusText = lastHref ? `${text} ${lastHref}` : text; return statusText; };