initial test of fedimbed
This commit is contained in:
parent
da72bf17c1
commit
3f39f840ff
2 changed files with 287 additions and 0 deletions
|
@ -3,6 +3,7 @@ const {getGuildData, setGuildData} = require("./guildData.js");
|
||||||
const flags = Object.freeze({
|
const flags = Object.freeze({
|
||||||
codePreviews: 1 << 0,
|
codePreviews: 1 << 0,
|
||||||
tweetUnrolling: 1 << 1,
|
tweetUnrolling: 1 << 1,
|
||||||
|
fedimbed: 1 << 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getFlags(guildId) {
|
async function getFlags(guildId) {
|
||||||
|
|
286
src/modules/fedimbed.js
Normal file
286
src/modules/fedimbed.js
Normal file
|
@ -0,0 +1,286 @@
|
||||||
|
const events = require("../lib/events.js");
|
||||||
|
const logger = require("../lib/logger.js");
|
||||||
|
const {hasFlag} = require("../lib/guildSettings.js");
|
||||||
|
const {parseHtmlEntities} = require("../lib/utils.js");
|
||||||
|
|
||||||
|
const FRIENDLY_USERAGENT =
|
||||||
|
"HiddenPhox/fedimbed (https://gitlab.com/Cynosphere/HiddenPhox)";
|
||||||
|
|
||||||
|
const URLS_REGEX = /(?:\s|^)(https?:\/\/[^\s<]+[^<.,:;"'\]\s])/g;
|
||||||
|
|
||||||
|
const PATH_REGEX = {
|
||||||
|
mastodon: /^\/@?(.+?)\/(statuses\/)?\d+\/?/,
|
||||||
|
pleroma:
|
||||||
|
/^\/objects\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/?/,
|
||||||
|
pleroma2: /^\/notice\/[A-Za-z0-9]+\/?/,
|
||||||
|
misskey: /^\/notes\/[a-z0-9]+\/?/,
|
||||||
|
gotosocial: /^\/@(.+?)\/statuses\/[0-9A-Z]+\/?/,
|
||||||
|
};
|
||||||
|
|
||||||
|
const PLATFORM_COLORS = {
|
||||||
|
mastodon: 0x2791da,
|
||||||
|
pleroma: 0xfba457,
|
||||||
|
akkoma: 0x593196,
|
||||||
|
misskey: 0x99c203,
|
||||||
|
calckey: 0x31748f,
|
||||||
|
gotosocial: 0xff853e,
|
||||||
|
};
|
||||||
|
|
||||||
|
const domainCache = new Map();
|
||||||
|
async function resolvePlatform(url) {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
if (domainCache.has(urlObj.hostname)) return domainCache.get(urlObj.hostname);
|
||||||
|
|
||||||
|
const probe = await fetch(urlObj.origin + "/.well-known/nodeinfo", {
|
||||||
|
headers: {"User-Agent": FRIENDLY_USERAGENT},
|
||||||
|
}).then((res) => res.json());
|
||||||
|
|
||||||
|
if (!probe?.links) {
|
||||||
|
logger.error("fedimbed", `No nodeinfo for "${urlObj.hostname}"???`);
|
||||||
|
domainCache.set(urlObj.hostname, null);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeinfo = await fetch(probe.links[probe.links.length - 1].href, {
|
||||||
|
headers: {"User-Agent": FRIENDLY_USERAGENT},
|
||||||
|
}).then((res) => res.json());
|
||||||
|
|
||||||
|
if (!nodeinfo?.software?.name) {
|
||||||
|
logger.error(
|
||||||
|
"fedimbed",
|
||||||
|
`Got nodeinfo for "${urlObj.hostname}", but missing software name.`
|
||||||
|
);
|
||||||
|
domainCache.set(urlObj.hostname, null);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
domainCache.set(urlObj.hostname, nodeinfo.software.name);
|
||||||
|
return nodeinfo.software.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processUrl(msg, url) {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
const platform = await resolvePlatform(url);
|
||||||
|
const color = PLATFORM_COLORS[platform];
|
||||||
|
const platformName = platform
|
||||||
|
.replace("gotosocial", "GoToSocial")
|
||||||
|
.replace(/^(.)/, (_, c) => c.toUpperCase());
|
||||||
|
|
||||||
|
const attachments = [];
|
||||||
|
let content, cw, author;
|
||||||
|
|
||||||
|
// Fetch post
|
||||||
|
const postData = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
"User-Agent": FRIENDLY_USERAGENT,
|
||||||
|
Accept: "application/activity+json",
|
||||||
|
"Content-Type": "application/activity+json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error("fedimbed", `Failed to fetch "${url}" as AS2: ${err}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (postData.error) {
|
||||||
|
logger.error("fedimbed", `Received error for "${url}": ${postData.error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!postData) {
|
||||||
|
// We failed to get post.
|
||||||
|
// If we're fetching from Pleroma, assume it was due to AMF and use MastoAPI
|
||||||
|
if (PATH_REGEX.pleroma.test(urlObj.pathname)) {
|
||||||
|
url = await fetch(url, {
|
||||||
|
method: "HEAD",
|
||||||
|
headers: {
|
||||||
|
"User-Agent": FRIENDLY_USERAGENT,
|
||||||
|
},
|
||||||
|
redirect: "manual",
|
||||||
|
}).then((res) => res.headers.get("location"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PATH_REGEX.pleroma2.test(urlObj.pathname)) {
|
||||||
|
const postData2 = await fetch(url.replace("notice", "api/v1/statuses"), {
|
||||||
|
headers: {
|
||||||
|
"User-Agent": FRIENDLY_USERAGENT,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error(
|
||||||
|
"fedimbed",
|
||||||
|
`Failed to fetch "${url}" as MastoAPI: ${err}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!postData2) {
|
||||||
|
logger.warn(
|
||||||
|
"fedimbed",
|
||||||
|
`Bailing trying to re-embed "${url}": Failed to get post from both AS2 and MastoAPI.`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
cw = postData2.spoiler_warning;
|
||||||
|
content =
|
||||||
|
postData2.akkoma?.source?.content ??
|
||||||
|
postData2.pleroma?.content?.["text/plain"] ??
|
||||||
|
postData2.content;
|
||||||
|
author = {
|
||||||
|
name: postData2.account.display_name,
|
||||||
|
handle: postData2.account.fqn,
|
||||||
|
url: postData2.account.url,
|
||||||
|
avatar: postData2.account.avatar,
|
||||||
|
};
|
||||||
|
for (const attachment of postData.media_attachments) {
|
||||||
|
attachments.push({
|
||||||
|
url: attachment.url,
|
||||||
|
desc: attachment.description,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content = postData.content;
|
||||||
|
cw = postData.summary;
|
||||||
|
for (const attachment of postData.attachment) {
|
||||||
|
attachments.push({
|
||||||
|
url: attachment.url,
|
||||||
|
desc: attachment.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Author data is not sent with the post with AS2
|
||||||
|
const authorData = await fetch(postData.actor, {
|
||||||
|
headers: {
|
||||||
|
"User-Agent": FRIENDLY_USERAGENT,
|
||||||
|
Accept: "application/activity+json",
|
||||||
|
"Content-Type": "application/activity+json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error("fedimbed", `Failed to get author for "${url}": ${err}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authorData) {
|
||||||
|
author = {
|
||||||
|
name: authorData.name,
|
||||||
|
handle: `${authorData.preferredUsername}@${urlObj.hostname}`,
|
||||||
|
url: authorData.url,
|
||||||
|
avatar: authorData.icon.url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We could just continue without author but it'd look ugly and be confusing.
|
||||||
|
if (!author) {
|
||||||
|
logger.warn(
|
||||||
|
"fedimbed"`Bailing trying to re-embed "${url}": Failed to get author.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start constructing embed
|
||||||
|
content = content ?? "";
|
||||||
|
cw = cw ?? "";
|
||||||
|
|
||||||
|
// TODO: convert certain HTML tags back to markdown
|
||||||
|
content = content.replace(/(<([^>]+)>)/gi,"");
|
||||||
|
content = parseHtmlEntities(content);
|
||||||
|
|
||||||
|
cw = cw.replace(/(<([^>]+)>)/gi,"");
|
||||||
|
cw = parseHtmlEntities(cw);
|
||||||
|
|
||||||
|
let desc = "";
|
||||||
|
let MAX_LENGTH = 3999;
|
||||||
|
if (cw != "") {
|
||||||
|
desc += "\u26a0 " + cw + "\n\n||" + content + "||";
|
||||||
|
MAX_LENGTH -= 8 - 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseEmbed = {
|
||||||
|
color,
|
||||||
|
url,
|
||||||
|
description: desc,
|
||||||
|
title: `${author.name} (${author.handle})`,
|
||||||
|
author: {
|
||||||
|
name: platformName,
|
||||||
|
},
|
||||||
|
thumbnail: {
|
||||||
|
url: author.url,
|
||||||
|
},
|
||||||
|
fields: [],
|
||||||
|
};
|
||||||
|
if (attachments.length > 0) {
|
||||||
|
if (attachments.length > 1) {
|
||||||
|
baseEmbed.fields.push({
|
||||||
|
name: "Images",
|
||||||
|
value: attachments
|
||||||
|
.map((attachment, index) => `[Image ${index + 1}](${attachment.url})`)
|
||||||
|
.join(" | "),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
baseEmbed.fields.push({
|
||||||
|
name: "Image",
|
||||||
|
value: `[Click for image](${attachments[0].url})`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const embeds = [];
|
||||||
|
|
||||||
|
for (const attachment of attachments) {
|
||||||
|
const embed = Object.assign({}, baseEmbed);
|
||||||
|
embed.image = {
|
||||||
|
url: attachment.url,
|
||||||
|
};
|
||||||
|
embeds.push(embed);
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.channel.createMessage({
|
||||||
|
embeds,
|
||||||
|
allowedMentions: {
|
||||||
|
repliedUser: false,
|
||||||
|
},
|
||||||
|
messageReference: {
|
||||||
|
messageID: msg.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
events.add("messageCreate", "fedimbed", async function (msg) {
|
||||||
|
if (!msg.guildID) return;
|
||||||
|
if (!(await hasFlag(msg.guildID, "fedimbed"))) return;
|
||||||
|
if (!msg.content || msg.content == "") return;
|
||||||
|
|
||||||
|
if (URLS_REGEX.test(msg.content)) {
|
||||||
|
const urls = msg.content.match(URLS_REGEX);
|
||||||
|
for (const url of urls) {
|
||||||
|
for (const service of Object.keys(PATH_REGEX)) {
|
||||||
|
const regex = PATH_REGEX[service];
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
if (regex.test(urlObj.pathname)) {
|
||||||
|
logger.verbose(
|
||||||
|
"fedimbed",
|
||||||
|
`Hit "${service}" for "${url}", processing now.`
|
||||||
|
);
|
||||||
|
await processUrl(msg, url).catch((err) => {
|
||||||
|
logger.error("fedimbed", `Error processing "${url}": ${err}`);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
Loading…
Reference in a new issue