diff --git a/src/modules/fedimbed.js b/src/modules/fedimbed.js index 74fd914..ccae30d 100644 --- a/src/modules/fedimbed.js +++ b/src/modules/fedimbed.js @@ -1,5 +1,3 @@ -const {Message} = require("@projectdysnomia/dysnomia"); - const fs = require("node:fs"); const httpSignature = require("@peertube/http-signature"); const {XMLParser} = require("fast-xml-parser"); @@ -11,7 +9,6 @@ const InteractionCommand = require("#lib/interactionCommand.js"); const {Icons} = require("#util/constants.js"); const {MessageFlags, ApplicationCommandOptionTypes, Permissions} = require("#util/dconstants.js"); -const {getUploadLimit} = require("#util/misc.js"); const {htmlToMarkdown} = require("#util/html.js"); const FRIENDLY_USERAGENT = @@ -715,7 +712,7 @@ async function bluesky(msg, url, spoiler = false) { return { response: { flags: 1 << 15, - components: [warnings.length > 0 ? warningText : false, container].filter((x) => !!x), + components: [warnings.length > 0 && warningText, container].filter((x) => !!x), allowedMentions: { repliedUser: false, }, @@ -910,18 +907,20 @@ async function getStatsAS(post) { } const stats = []; - if (replyCount > 0) stats.push(`\u21a9 ${statsFormatter.format(replyCount)}`); - if (post.shares?.totalItems ?? 0 > 0) stats.push(`\ud83d\udd01 ${statsFormatter.format(post.shares.totalItems)}`); - if (post.likes?.totalItems ?? 0 > 0) stats.push(`\u2665 ${statsFormatter.format(post.likes.totalItems)}`); + if (replyCount > 0) stats.push(`${Icons.fedimbed.reply} ${statsFormatter.format(replyCount)}`); + if (post.shares?.totalItems ?? 0 > 0) + stats.push(`${Icons.fedimbed.repost} ${statsFormatter.format(post.shares.totalItems)}`); + if (post.likes?.totalItems ?? 0 > 0) + stats.push(`${Icons.fedimbed.like} ${statsFormatter.format(post.likes.totalItems)}`); return stats.join("\u3000"); } function getStatsMasto(post) { const stats = []; - if (post.replies_count > 0) stats.push(`\u21a9 ${statsFormatter.format(post.replies_count)}`); - if (post.reblogs_count > 0) stats.push(`\ud83d\udd01 ${statsFormatter.format(post.reblogs_count)}`); - if (post.favourites_count > 0) stats.push(`\u2665 ${statsFormatter.format(post.favourites_count)}`); + if (post.replies_count > 0) stats.push(`${Icons.fedimbed.reply} ${statsFormatter.format(post.replies_count)}`); + if (post.reblogs_count > 0) stats.push(`${Icons.fedimbed.repost} ${statsFormatter.format(post.reblogs_count)}`); + if (post.favourites_count > 0) stats.push(`${Icons.fedimbed.like} ${statsFormatter.format(post.favourites_count)}`); return stats.join("\u3000"); } @@ -1061,58 +1060,23 @@ async function processUrl(msg, url, spoiler = false, command = false) { const attachments = postData.media_attachments ?? postData.files; if (attachments) { for (const attachment of attachments) { - const contentType = await fetch(attachment.url, { - method: "HEAD", - }).then((res) => res.headers.get("Content-Type")); + const type = attachment.type?.toLowerCase(); - if (contentType) { - if (contentType.startsWith("image/")) { - images.push({ - url: attachment.url, - desc: attachment.description ?? attachment.comment, - type: contentType, - }); - } else if (contentType.startsWith("video/")) { - videos.push({ - url: attachment.url, - desc: attachment.description ?? attachment.comment, - type: contentType, - }); - } else if (contentType.startsWith("audio/")) { - audios.push({ - url: attachment.url, - desc: attachment.description ?? attachment.comment, - type: contentType, - }); - } - } else { - const type = attachment.type?.toLowerCase(); - - const fileType = - attachment.pleroma?.mime_type ?? type.indexOf("/") > -1 - ? type - : type + - "/" + - (url.match(/\.([a-z0-9]{3,4})$/)?.[0] ?? type == "image" ? "png" : type == "video" ? "mp4" : "mpeg"); - if (type.startsWith("image")) { - images.push({ - url: attachment.url, - desc: attachment.description ?? attachment.comment, - type: fileType, - }); - } else if (type.startsWith("video")) { - videos.push({ - url: attachment.url, - desc: attachment.description ?? attachment.comment, - type: fileType, - }); - } else if (type.startsWith("audio")) { - audios.push({ - url: attachment.url, - desc: attachment.description ?? attachment.comment, - type: fileType, - }); - } + if (type.startsWith("image")) { + images.push({ + media: {url: attachment.url}, + description: attachment.description ?? attachment.comment, + }); + } else if (type.startsWith("video")) { + videos.push({ + media: {url: attachment.url}, + description: attachment.description ?? attachment.comment, + }); + } else if (type.startsWith("audio")) { + audios.push({ + url: attachment.url, + description: attachment.description ?? attachment.comment, + }); } } } @@ -1156,19 +1120,19 @@ async function processUrl(msg, url, spoiler = false, command = false) { if (postData.inReplyTo) { contextUrl = postData.inReplyTo; - context = "Replying to: "; + context = `-# ${Icons.fedimbed.reply} Replying to: `; const replyData = await fetchPost(postData.inReplyTo, platform); if (replyData) { if (replyData._fedimbed_mastoapi) { - context += `${ + context += `[${ replyData.account?.display_name ?? replyData.account?.username ?? replyData.user?.name ?? replyData.user?.username } (@${ replyData.account?.fqn ?? `${replyData.account?.username ?? replyData.user?.username}@${urlObj.hostname}` - })`; + })](${contextUrl})`; } else { const authorData = await signedFetch(replyData.actor ?? replyData.attributedTo, { headers: { @@ -1183,13 +1147,13 @@ async function processUrl(msg, url, spoiler = false, command = false) { if (authorData) { const authorUrlObj = new URL(authorData.url ?? authorData.id); - context += `${authorData.name} (${authorData.preferredUsername}@${authorUrlObj.hostname})`; + context += `[${authorData.name} (${authorData.preferredUsername}@${authorUrlObj.hostname})](${contextUrl})`; } else { // bootleg author const authorUrl = replyData.actor ?? replyData.attributedTo; const authorUrlObj = new URL(authorUrl); const name = authorUrlObj.pathname.substring(authorUrlObj.pathname.lastIndexOf("/") + 1); - context += `${name}@${authorUrlObj.hostname}`; + context += `[${name}@${authorUrlObj.hostname}](${authorUrl})`; } } } else { @@ -1201,36 +1165,18 @@ async function processUrl(msg, url, spoiler = false, command = false) { // NB: everyone else except gts doesnt follow the spec (they should be using attachments) const attachments = Array.isArray(postData.attachment) ? postData.attachment : [postData.attachment]; for (const attachment of attachments) { - if (attachment.mediaType) { - if (attachment.mediaType.startsWith("video/")) { - videos.push({ - url: attachment.url, - desc: attachment.name ?? attachment.description ?? attachment.comment, - type: attachment.mediaType, - }); - } else if (attachment.mediaType.startsWith("image/")) { - images.push({ - url: attachment.url, - desc: attachment.name ?? attachment.description ?? attachment.comment, - type: attachment.mediaType, - }); - } else if (attachment.mediaType.startsWith("audio/")) { - audios.push({ - url: attachment.url, - desc: attachment.name ?? attachment.description ?? attachment.comment, - type: attachment.mediaType, - }); - } - } else if (attachment.url) { + if (attachment.url) { + const type = attachment.type?.toLowerCase(); + let attUrl = attachment.url; if (Array.isArray(attachment.url)) { - switch (attachment.type) { - case "Image": { + switch (type) { + case "image": { const newUrl = attachment.url.find((a) => a.mediaType?.startsWith("image/"))?.href; if (newUrl) attUrl = newUrl; break; } - case "Video": { + case "video": { const newUrl = attachment.url.find((a) => a.mediaType?.startsWith("video/"))?.href; if (newUrl) attUrl = newUrl; break; @@ -1240,60 +1186,21 @@ async function processUrl(msg, url, spoiler = false, command = false) { } } - let contentType; - if (attUrl != null) - contentType = await fetch(attUrl, { - method: "HEAD", - }).then((res) => res.headers.get("Content-Type")); - - if (contentType) { - if (contentType.startsWith("image/")) { - images.push({ - url: attUrl, - desc: attachment.name ?? attachment.description ?? attachment.comment, - type: contentType, - }); - } else if (contentType.startsWith("video/")) { - videos.push({ - url: attUrl, - desc: attachment.name ?? attachment.description ?? attachment.comment, - type: contentType, - }); - } else if (contentType.startsWith("audio/")) { - audios.push({ - url: attUrl, - desc: attachment.name ?? attachment.description ?? attachment.comment, - type: contentType, - }); - } - } else { - const type = attachment.type?.toLowerCase(); - - const fileType = - type.indexOf("/") > -1 - ? type - : type + - "/" + - (url.match(/\.([a-z0-9]{3,4})$/)?.[0] ?? type == "image" ? "png" : type == "video" ? "mp4" : "mpeg"); - if (type.startsWith("image")) { - images.push({ - url: attUrl, - desc: attachment.name ?? attachment.description ?? attachment.comment, - type: fileType, - }); - } else if (type.startsWith("video")) { - videos.push({ - url: attUrl, - desc: attachment.name ?? attachment.description ?? attachment.comment, - type: fileType, - }); - } else if (type.startsWith("audio")) { - audios.push({ - url: attUrl, - desc: attachment.name ?? attachment.description ?? attachment.comment, - type: fileType, - }); - } + if (type.startsWith("image")) { + images.push({ + media: {url: attUrl}, + description: attachment.name ?? attachment.description ?? attachment.comment, + }); + } else if (type.startsWith("video")) { + videos.push({ + media: {url: attUrl}, + description: attachment.name ?? attachment.description ?? attachment.comment, + }); + } else if (type.startsWith("audio")) { + audios.push({ + url: attUrl, + description: attachment.name ?? attachment.description ?? attachment.comment, + }); } } else { logger.warn("fedimbed", `Unhandled attachment structure! ${JSON.stringify(attachment)}`); @@ -1306,14 +1213,8 @@ async function processUrl(msg, url, spoiler = false, command = false) { } if (postData.image?.url) { - const imageUrl = new URL(postData.image.url); - const contentType = await fetch(postData.image.url, { - method: "HEAD", - }).then((res) => res.headers.get("Content-Type")); images.push({ - url: postData.image.url, - desc: "", - type: contentType ?? "image/" + imageUrl.pathname.substring(imageUrl.pathname.lastIndexOf(".") + 1), + media: {url: postData.image.url}, }); } @@ -1328,7 +1229,7 @@ async function processUrl(msg, url, spoiler = false, command = false) { }) .then((res) => res.json()) .catch((err) => { - /*if (platform !== "cohost")*/ logger.error("fedimbed", `Failed to get author for "${url}": ${err}`); + logger.error("fedimbed", `Failed to get author for "${url}": ${err}`); }); if (authorData) { @@ -1394,31 +1295,15 @@ async function processUrl(msg, url, spoiler = false, command = false) { let desc = ""; let MAX_LENGTH = 3999; - if ((cw != "" || sensitive) && images.length == 0 && videos.length == 0 && audios.length == 0) { + if (cw != "" || sensitive) { const ors = content.split("||"); desc += `||${content.replaceAll("||", "|\u200b|")}||`; MAX_LENGTH -= ors.length - 1; MAX_LENGTH -= 4; - - if (cw != "") { - desc = "\u26a0 " + cw + "\n\n" + desc; - MAX_LENGTH -= 4 - cw.length; - } } else { desc = content; } - if (desc.length > MAX_LENGTH) { - if (desc.endsWith("||")) { - desc = desc.substring(0, MAX_LENGTH - 2); - desc += "\u2026||"; - } else { - desc = desc.substring(0, MAX_LENGTH) + "\u2026"; - } - } - - let user = author.name ? `${author.name} (${author.handle})` : author.handle; - const crawled = await getCrawledData(url, color, platformName); if (!color && crawled?.color) { color = crawled.color; @@ -1429,328 +1314,184 @@ async function processUrl(msg, url, spoiler = false, command = false) { if (platformName.includes("Nitter") && platformName.includes(" \u2022 ")) { const [nn, ns] = platformName.split(" \u2022 "); - stats = ns; + stats = ns + .replace("\u21a9", Icons.fedimbed.reply) + .replace("\ud83d\udd01", Icons.fedimbed.repost) + .replace("\u2198", Icons.fedimbed.quote) + .replace("\u2665", Icons.fedimbed.like) + .replace("\ud83d\udd16", Icons.fedimbed.bookmark) + .replace("\ud83d\udc41", Icons.fedimbed.views) + .replaceAll(/ <:i:/g, "\u3000<:i:"); platformName = nn; } if (platformName == "Nitter") { const newHandle = author.handle.split("@")[0]; - user = `${author.name} (@${newHandle})`; + author.handle = "@" + newHandle; if (context) { const contextHandle = context.match(/\(([^@]+?@.+?)\)/)?.[1]; if (contextHandle) { - const newContextHandle = "@" + contextHandle.split("@")[0]; - context = context.replace(contextHandle, newContextHandle); + const newContextHandle = contextHandle.split("@")[0]; + context = context.replace(contextHandle, "@" + newContextHandle); + context = context.replace(/: (.+?)$/, `: [$1](${contextUrl})`); } } } - const baseEmbed = { - color, - url, - timestamp, - description: desc, - title: title ?? user, - author: title - ? { - name: user, - url: author.url, - } - : context - ? { - name: context, - url: contextUrl, - icon_url: "https://cdn.discordapp.com/emojis/1308640078825787412.png", - } - : null, - footer: { - icon_url: crawled?.icon, - text: (stats != null && stats != "" ? stats + "\n" : "") + platformName, - }, - thumbnail: { - url: author.avatar, - }, - fields: [], + const warningText = { + type: 10, + content: `## ${cw}`, }; - if (images.length > 0) { - if (images.length > 1) { - const links = images.map((attachment, index) => `[Image ${index + 1}](${attachment.url})`).join("\u3000\u3000"); - if (links.length <= 1024) - baseEmbed.fields.push({ - name: "Images", - value: links, - inline: true, - }); + const container = { + type: 17, + accent_color: color, + components: [], + spoiler, + }; + + let headerContent = `${author.name ? `## ${author.name}\n` : ""}-# [${author.handle}](${author.url})`; + + if (title) headerContent += "\n### " + title; + + MAX_LENGTH -= headerContent.length + 1; + + if (desc.length > MAX_LENGTH) { + if (desc.endsWith("||")) { + desc = desc.substring(0, MAX_LENGTH - 2); + desc += "\u2026||"; } else { - baseEmbed.fields.push({ - name: "Image", - value: `[Click for image](${images[0].url})`, - inline: true, - }); - } - } - if (videos.length > 0) { - if (videos.length > 1) { - baseEmbed.fields.push({ - name: "Videos", - value: videos.map((attachment, index) => `[Video ${index + 1}](${attachment.url})`).join("\u3000\u3000"), - inline: true, - }); - } else { - baseEmbed.fields.push({ - name: "Video", - value: `[Click for video](${videos[0].url})`, - inline: true, - }); - } - } - if (audios.length > 0) { - if (audios.length > 1) { - baseEmbed.fields.push({ - name: "Audios", - value: audios.map((attachment, index) => `[Audio ${index + 1}](${attachment.url})`).join("\u3000\u3000"), - inline: true, - }); - } else { - baseEmbed.fields.push({ - name: "Audio", - value: `[Click for audio](${audios[0].url})`, - inline: true, - }); + desc = desc.substring(0, MAX_LENGTH) + "\u2026"; } } + headerContent += "\n" + desc; + + const header = [ + context && { + type: 10, + content: context, + }, + { + type: 9, + components: [ + { + type: 10, + content: headerContent, + }, + ], + accessory: { + type: 11, + media: { + url: author.avatar, + }, + }, + }, + ].filter((x) => !!x); + container.components.push(...header); if (poll) { const pollTime = poll.end.getTime(); const now = Date.now(); - baseEmbed.fields.push({ - name: "Poll", - value: + container.components.push({ + type: 10, + content: poll.options .map((o) => { const percent = o.count / poll.total; const bar = Math.round(percent * 32); - return `**${o.name}** (${numberFormatter.format(o.count)}, ${Math.round( + return `> **${o.name}** (${numberFormatter.format(o.count)}, ${Math.round( percent * 100 - )}%)\n\`${"\u2588".repeat(bar)}${" ".repeat(32 - bar)}\``; + )}%)> \n\`${"\u2588".repeat(bar)}${" ".repeat(32 - bar)}\``; }) - .join("\n\n") + - `\n\n${poll.total} votes \u2022 End${pollTime > now ? "s" : "ed"} `, + .join("> \n> \n") + + `> \n> \n${poll.total} votes \u2022 End${pollTime > now ? "s" : "ed"} `, }); } - let sendWait = false; - if (videos.length > 0 || audios.length > 0 || images.length > 4) { - sendWait = true; - if (msg instanceof Message) await msg.addReaction("\uD83D\uDCE4"); + const footer = { + type: 9, + components: [ + { + type: 10, + content: `-# ${stats}\n${platformName} \u2022 `, + }, + ], + accessory: { + type: 2, + style: 5, + label: "View Post", + url, + }, + }; + + for (const image in images) { + if (image.description?.length === 0) image.description = null; } - const embeds = []; - const files = []; - - const guild = msg.channel?.guild ?? (msg.guildID ? hf.bot.guilds.get(msg.guildID) : false); - - let limit = getUploadLimit(guild); - if (msg.attachmentSizeLimit != null) limit = msg.attachmentSizeLimit; - if (images.length > 0) { - if (images.length <= 4) { - for (const attachment of images) { - const embed = Object.assign({}, baseEmbed); - embed.image = { - url: attachment.url, - description: attachment.desc, - }; - embeds.push(embed); - } - } else if (images.length > 4 && images.length <= 10) { - for (const attachment of images) { - const size = await fetch(attachment.url, { - method: "HEAD", - headers: { - "User-Agent": FRIENDLY_USERAGENT, - }, - }).then((res) => Number(res.headers.get("Content-Length"))); - - 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: - (cw != "" || spoiler ? "SPOILER_" : "") + - (attachment.type.indexOf("/") > -1 - ? attachment.type.replace("/", ".") - : attachment.type + "." + (url.match(/\.([a-z0-9]{3,4})$/)?.[0] ?? "png")), - file, - description: attachment.desc, - }); - } - } - embeds.push(baseEmbed); - } else { - const ten = images.slice(0, 10); - - for (const attachment of ten) { - const size = await fetch(attachment.url, { - method: "HEAD", - headers: { - "User-Agent": FRIENDLY_USERAGENT, - }, - }).then((res) => Number(res.headers.get("Content-Length"))); - - 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: - (cw != "" || spoiler ? "SPOILER_" : "") + - (attachment.type.indexOf("/") > -1 - ? attachment.type.replace("/", ".") - : attachment.type + "." + (url.match(/\.([a-z0-9]{3,4})$/)?.[0] ?? "png")), - file, - description: attachment.desc, - }); - } - } - - if (images.length <= 14) { - const fourteen = images.slice(10, 14); - - for (const attachment of fourteen) { - const embed = Object.assign({}, baseEmbed); - embed.image = { - url: attachment.url, - description: attachment.desc, - }; - embeds.push(embed); - } - } else if (images.length <= 18) { - const fourteen = images.slice(10, 14); - - for (const attachment of fourteen) { - const embed = Object.assign({}, baseEmbed); - embed.image = { - url: attachment.url, - description: attachment.desc, - }; - embeds.push(embed); - } - - const eighteen = images.slice(14, 18); - const _embed = { - color: baseEmbed.color, - url: baseEmbed.url + "?_", - title: "Additional Images", - }; - - for (const attachment of eighteen) { - const embed = Object.assign({}, _embed); - embed.image = { - url: attachment.url, - description: attachment.desc, - }; - embeds.push(embed); - } - } - } - } else { - embeds.push(baseEmbed); - } - - 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"))); - - 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: - (cw != "" || 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 > 10) { + while (images.length > 10) { + container.components.push({ + type: 12, + items: images.splice(0, 10), }); } } + + container.components.push({ + type: 12, + items: images, + }); } + for (const video of videos) { + if (video.description?.length === 0) video.description = null; + container.components.push({ + type: 12, + items: [video], + }); + } + if (audios.length > 0) { - for (const attachment of audios) { - const size = await fetch(attachment.url, { - method: "HEAD", - headers: { - "User-Agent": FRIENDLY_USERAGENT, - }, - }).then((res) => Number(res.headers.get("Content-Length"))); - - 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: - (cw != "" || spoiler ? "SPOILER_" : "") + - (attachment.type.indexOf("/") > -1 - ? attachment.type.replace("/", ".").replace("mpeg", "mp3").replace("vnd.wave", "wav").replace("x-", "") - : attachment.type + "." + (url.match(/\.([a-z0-9]{3,4})$/)?.[0] ?? "mp3")), - file, - description: attachment.desc, - }); + container.components.push( + { + type: 10, + content: "### Audio Attachments", + }, + { + type: 1, + components: audios.map((a) => ({ + type: 2, + style: 5, + emoji: {name: "silk_sound", id: "1273119755376529418", animated: false}, + label: + a.description?.length > 0 + ? `"${a.description.length > 78 ? a.description.substring(0, 77) + "\u2026" : a.description}"` + : "Audio", + url: a.url, + })), } - } + ); } + container.components.push(footer); + if (quoteRes) { - if (!sendWait && quoteRes.sendWait) sendWait = true; - files.push(...quoteRes.response.attachments); - const quoteEmbed = quoteRes.response.embeds[0]; - quoteEmbed.author = {name: "Quoted Post", icon_url: "https://cdn.discordapp.com/emojis/1308640087759654922.png"}; - embeds.push(quoteEmbed); + const quoteComponents = quoteRes.response.components[0].components; + const quoteContext = `-# ${Icons.fedimbed.quote} Quoted Post`; + if (quoteComponents[0].type == 10) { + quoteComponents[0].content = quoteContext + "\n" + quoteComponents[0].content; + } else { + quoteComponents.splice(0, 0, {type: 10, content: quoteContext}); + } + container.components.push({type: 14}, ...quoteComponents); } return { response: { - content: - cw != "" && (images.length > 0 || videos.length > 0 || audios.length > 0) - ? `:warning: ${cw} || ${url} ||` - : spoiler - ? `|| ${url} ||` - : "", - embeds, - attachments: files, + flags: 1 << 15, + components: [cw.length > 0 ? warningText : false, container].filter((x) => !!x), allowedMentions: { repliedUser: false, }, @@ -1758,7 +1499,6 @@ async function processUrl(msg, url, spoiler = false, command = false) { messageID: msg.id, }, }, - sendWait, }; }