2023-01-22 04:45:57 +00:00
|
|
|
const {MessageFlags} = require("@projectdysnomia/dysnomia").Constants;
|
2023-07-26 03:21:29 +00:00
|
|
|
const fs = require("node:fs");
|
|
|
|
const path = require("node:path");
|
|
|
|
const httpSignature = require("@peertube/http-signature");
|
2022-12-06 03:35:48 +00:00
|
|
|
|
2022-12-06 02:50:22 +00:00
|
|
|
const events = require("../lib/events.js");
|
|
|
|
const logger = require("../lib/logger.js");
|
|
|
|
const {hasFlag} = require("../lib/guildSettings.js");
|
2022-12-06 17:55:57 +00:00
|
|
|
const {parseHtmlEntities, getUploadLimit} = require("../lib/utils.js");
|
2022-12-06 02:50:22 +00:00
|
|
|
|
|
|
|
const FRIENDLY_USERAGENT =
|
2022-12-31 20:32:48 +00:00
|
|
|
"HiddenPhox/fedimbed (https://gitdab.com/Cynosphere/HiddenPhox)";
|
2022-12-06 02:50:22 +00:00
|
|
|
|
2023-01-17 19:25:05 +00:00
|
|
|
const URLS_REGEX =
|
|
|
|
/(?:\s|^)(\|\|\s*)?(https?:\/\/[^\s<]+[^<.,:;"'\]\s])(\s+\|\|)?/g;
|
2023-11-08 17:05:51 +00:00
|
|
|
const SPOILER_REGEX = /(?:\s|^)\|\|([\s\S]+?)\|\|/;
|
2022-12-06 02:50:22 +00:00
|
|
|
|
|
|
|
const PATH_REGEX = {
|
2022-12-06 04:25:26 +00:00
|
|
|
mastodon: /^\/@(.+?)\/(\d+)\/?/,
|
2022-12-06 02:54:25 +00:00
|
|
|
mastodon2: /^\/(.+?)\/statuses\/\d+\/?/,
|
2022-12-06 02:50:22 +00:00
|
|
|
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]+\/?/,
|
2023-09-23 02:50:19 +00:00
|
|
|
lemmy: /^\/post\/\d+\/?/,
|
2022-12-06 02:50:22 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const PLATFORM_COLORS = {
|
|
|
|
mastodon: 0x2791da,
|
|
|
|
pleroma: 0xfba457,
|
|
|
|
akkoma: 0x593196,
|
|
|
|
misskey: 0x99c203,
|
|
|
|
calckey: 0x31748f,
|
2023-07-25 00:32:50 +00:00
|
|
|
firefish: 0xf07a5b, // YCbCr interpolated color from the two logo colors
|
2022-12-06 02:50:22 +00:00
|
|
|
gotosocial: 0xff853e,
|
2023-09-23 02:50:19 +00:00
|
|
|
lemmy: 0x14854f,
|
2023-11-13 18:03:05 +00:00
|
|
|
birdsitelive: 0x1da1f2,
|
2022-12-06 02:50:22 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const domainCache = new Map();
|
|
|
|
async function resolvePlatform(url) {
|
2022-12-06 03:09:13 +00:00
|
|
|
const urlObj = new URL(url);
|
2022-12-06 02:50:22 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-07-26 03:26:40 +00:00
|
|
|
const keyId = "https://hf.c7.pm/actor#main-key";
|
2023-07-26 03:24:24 +00:00
|
|
|
const privKey = fs.readFileSync(
|
|
|
|
path.resolve(__dirname, "../../priv/private.pem")
|
|
|
|
);
|
2023-07-26 03:21:29 +00:00
|
|
|
async function signedFetch(url, options) {
|
|
|
|
const urlObj = new URL(url);
|
|
|
|
|
|
|
|
const headers = {
|
|
|
|
host: urlObj.host,
|
|
|
|
date: new Date().toUTCString(),
|
|
|
|
};
|
|
|
|
|
|
|
|
const headerNames = ["(request-target)", "host", "date"];
|
|
|
|
|
|
|
|
httpSignature.sign(
|
|
|
|
{
|
|
|
|
getHeader: (name) => headers[name.toLowerCase()],
|
|
|
|
setHeader: (name, value) => (headers[name] = value),
|
|
|
|
method: options.method ?? "GET",
|
|
|
|
path: urlObj.pathname,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
keyId,
|
|
|
|
key: privKey,
|
|
|
|
headers: headerNames,
|
|
|
|
authorizationHeaderName: "signature",
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
options.headers = Object.assign(headers, options.headers ?? {});
|
|
|
|
|
|
|
|
return await fetch(url, options);
|
|
|
|
}
|
|
|
|
|
2023-01-09 02:09:34 +00:00
|
|
|
async function processUrl(msg, url, spoiler = false) {
|
2022-12-06 03:09:13 +00:00
|
|
|
let urlObj = new URL(url);
|
2023-09-23 02:58:42 +00:00
|
|
|
|
2023-09-23 03:32:07 +00:00
|
|
|
// some lemmy instances have old reddit frontend subdomains
|
2023-09-23 02:58:42 +00:00
|
|
|
// but these frontends are just frontends and dont actually expose the API
|
|
|
|
if (urlObj.hostname.startsWith("old.")) {
|
|
|
|
urlObj.hostname = urlObj.hostname.replace("old.", "");
|
|
|
|
url = urlObj.href;
|
|
|
|
}
|
|
|
|
|
2022-12-07 04:17:12 +00:00
|
|
|
let platform = await resolvePlatform(url);
|
|
|
|
let color = PLATFORM_COLORS[platform];
|
|
|
|
let platformName = platform
|
2022-12-06 02:50:22 +00:00
|
|
|
.replace("gotosocial", "GoToSocial")
|
2023-11-13 18:03:05 +00:00
|
|
|
.replace("birdsitelive", '"Twitter" (BirdsiteLive)')
|
2022-12-06 02:50:22 +00:00
|
|
|
.replace(/^(.)/, (_, c) => c.toUpperCase());
|
|
|
|
|
2022-12-06 17:39:24 +00:00
|
|
|
const images = [];
|
|
|
|
const videos = [];
|
|
|
|
const audios = [];
|
2023-08-15 04:42:12 +00:00
|
|
|
let content,
|
|
|
|
cw,
|
|
|
|
author,
|
|
|
|
timestamp,
|
2023-09-23 02:50:19 +00:00
|
|
|
title,
|
2023-08-15 04:42:12 +00:00
|
|
|
emotes = [];
|
2022-12-06 02:50:22 +00:00
|
|
|
|
|
|
|
// Fetch post
|
2023-07-28 03:14:14 +00:00
|
|
|
let rawPostData;
|
2023-07-28 03:16:15 +00:00
|
|
|
try {
|
|
|
|
rawPostData = await signedFetch(url, {
|
|
|
|
headers: {
|
|
|
|
"User-Agent": FRIENDLY_USERAGENT,
|
|
|
|
Accept: "application/activity+json",
|
|
|
|
},
|
|
|
|
}).then((res) => res.text());
|
|
|
|
} catch (err) {
|
|
|
|
logger.error(
|
|
|
|
"fedimbed",
|
|
|
|
`Failed to signed fetch "${url}", retrying unsigned: ${err}`
|
|
|
|
);
|
2023-07-28 03:19:24 +00:00
|
|
|
}
|
|
|
|
if (!rawPostData) {
|
|
|
|
try {
|
|
|
|
rawPostData = await fetch(url, {
|
|
|
|
headers: {
|
|
|
|
"User-Agent": FRIENDLY_USERAGENT,
|
|
|
|
Accept: "application/activity+json",
|
|
|
|
},
|
|
|
|
}).then((res) => res.text());
|
|
|
|
} catch (err) {
|
|
|
|
logger.error("fedimbed", `Failed to fetch "${url}": ${err}`);
|
|
|
|
}
|
2023-07-28 03:16:15 +00:00
|
|
|
}
|
2022-12-06 02:50:22 +00:00
|
|
|
|
2022-12-06 02:59:43 +00:00
|
|
|
let postData;
|
2023-07-28 03:12:14 +00:00
|
|
|
if (rawPostData?.startsWith("{")) {
|
2022-12-06 02:59:43 +00:00
|
|
|
postData = JSON.parse(rawPostData);
|
|
|
|
} else {
|
2022-12-06 03:11:43 +00:00
|
|
|
logger.warn("fedimbed", `Got non-JSON for "${url}": ${rawPostData}`);
|
2022-12-06 02:59:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (postData?.error) {
|
2022-12-06 02:50:22 +00:00
|
|
|
logger.error("fedimbed", `Received error for "${url}": ${postData.error}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!postData) {
|
|
|
|
// We failed to get post.
|
2022-12-31 19:55:26 +00:00
|
|
|
// Assume it was due to AFM or forced HTTP signatures and use MastoAPI
|
2022-12-06 04:16:37 +00:00
|
|
|
|
|
|
|
// Follow redirect from /object since we need the ID from /notice
|
2022-12-06 02:50:22 +00:00
|
|
|
if (PATH_REGEX.pleroma.test(urlObj.pathname)) {
|
2023-07-26 03:21:29 +00:00
|
|
|
url = await signedFetch(url, {
|
2022-12-06 02:50:22 +00:00
|
|
|
method: "HEAD",
|
|
|
|
headers: {
|
|
|
|
"User-Agent": FRIENDLY_USERAGENT,
|
|
|
|
},
|
|
|
|
redirect: "manual",
|
|
|
|
}).then((res) => res.headers.get("location"));
|
2022-12-06 03:05:13 +00:00
|
|
|
if (url.startsWith("/")) {
|
|
|
|
url = urlObj.origin + url;
|
|
|
|
}
|
2022-12-06 03:08:23 +00:00
|
|
|
urlObj = new URL(url);
|
2022-12-06 02:50:22 +00:00
|
|
|
}
|
|
|
|
|
2022-12-06 04:10:15 +00:00
|
|
|
let redirUrl;
|
2022-12-31 19:47:29 +00:00
|
|
|
const options = {};
|
2022-12-31 19:59:21 +00:00
|
|
|
const headers = {};
|
2022-12-06 02:50:22 +00:00
|
|
|
if (PATH_REGEX.pleroma2.test(urlObj.pathname)) {
|
2022-12-06 04:10:15 +00:00
|
|
|
redirUrl = url.replace("notice", "api/v1/statuses");
|
2022-12-06 04:13:47 +00:00
|
|
|
} else if (PATH_REGEX.mastodon.test(urlObj.pathname)) {
|
2022-12-06 04:25:26 +00:00
|
|
|
const postId = urlObj.pathname.match(PATH_REGEX.mastodon)?.[2];
|
|
|
|
redirUrl = urlObj.origin + "/api/v1/statuses/" + postId;
|
2022-12-06 04:16:37 +00:00
|
|
|
} else if (PATH_REGEX.mastodon2.test(urlObj.pathname)) {
|
|
|
|
redirUrl = url.replace(/^\/(.+?)\/statuses/, "/api/v1/statuses");
|
2022-12-31 19:47:29 +00:00
|
|
|
} else if (PATH_REGEX.misskey.test(urlObj.pathname)) {
|
2022-12-31 20:11:09 +00:00
|
|
|
let noteId = url.split("/notes/")[1];
|
2022-12-31 20:05:57 +00:00
|
|
|
if (noteId.indexOf("/") > -1) {
|
|
|
|
noteId = noteId.split("/")[0];
|
|
|
|
} else if (noteId.indexOf("?") > -1) {
|
|
|
|
noteId = noteId.split("?")[0];
|
|
|
|
} else if (noteId.indexOf("#") > -1) {
|
|
|
|
noteId = noteId.split("#")[0];
|
|
|
|
}
|
2022-12-31 20:09:32 +00:00
|
|
|
logger.verbose("fedimbed", "Misskey post ID: " + noteId);
|
2022-12-31 19:47:29 +00:00
|
|
|
redirUrl = urlObj.origin + "/api/notes/show/";
|
|
|
|
options.method = "POST";
|
|
|
|
options.body = JSON.stringify({noteId});
|
2022-12-31 19:59:21 +00:00
|
|
|
headers["Content-Type"] = "application/json";
|
2022-12-06 04:10:15 +00:00
|
|
|
} else {
|
|
|
|
logger.error(
|
|
|
|
"fedimbed",
|
|
|
|
`Missing MastoAPI replacement for "${platform}"`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (redirUrl) {
|
2022-12-31 20:15:02 +00:00
|
|
|
logger.verbose(
|
|
|
|
"fedimbed",
|
|
|
|
`Redirecting "${url}" to "${redirUrl}": ${JSON.stringify(
|
|
|
|
options
|
|
|
|
)}, ${JSON.stringify(headers)}`
|
|
|
|
);
|
2023-07-28 03:14:14 +00:00
|
|
|
let rawPostData2;
|
2023-07-28 03:16:15 +00:00
|
|
|
try {
|
|
|
|
rawPostData2 = await signedFetch(
|
|
|
|
redirUrl,
|
|
|
|
Object.assign(options, {
|
|
|
|
headers: Object.assign(headers, {
|
|
|
|
"User-Agent": FRIENDLY_USERAGENT,
|
|
|
|
}),
|
|
|
|
})
|
|
|
|
).then((res) => res.text());
|
|
|
|
} catch (err) {
|
|
|
|
logger.error(
|
|
|
|
"fedimbed",
|
|
|
|
`Failed to signed fetch "${url}" via MastoAPI, retrying unsigned: ${err}`
|
|
|
|
);
|
2023-07-28 03:19:24 +00:00
|
|
|
}
|
|
|
|
if (!rawPostData2) {
|
|
|
|
try {
|
|
|
|
rawPostData2 = await signedFetch(
|
|
|
|
redirUrl,
|
|
|
|
Object.assign(options, {
|
|
|
|
headers: Object.assign(headers, {
|
|
|
|
"User-Agent": FRIENDLY_USERAGENT,
|
|
|
|
}),
|
|
|
|
})
|
|
|
|
).then((res) => res.text());
|
|
|
|
} catch (err) {
|
|
|
|
logger.error(
|
|
|
|
"fedimbed",
|
|
|
|
`Failed to fetch "${url}" via MastoAPI: ${err}`
|
|
|
|
);
|
|
|
|
}
|
2023-07-28 03:16:15 +00:00
|
|
|
}
|
2022-12-06 02:50:22 +00:00
|
|
|
|
2022-12-06 04:18:28 +00:00
|
|
|
let postData2;
|
2023-07-28 03:12:14 +00:00
|
|
|
if (rawPostData2?.startsWith("{")) {
|
2022-12-06 04:18:28 +00:00
|
|
|
postData2 = JSON.parse(rawPostData2);
|
|
|
|
} else {
|
2022-12-06 04:46:03 +00:00
|
|
|
logger.warn(
|
|
|
|
"fedimbed",
|
2023-07-28 03:12:14 +00:00
|
|
|
`Got non-JSON for "${url}" via MastoAPI: ${rawPostData2}`
|
2022-12-06 04:46:03 +00:00
|
|
|
);
|
2022-12-06 04:18:28 +00:00
|
|
|
}
|
|
|
|
|
2022-12-06 02:50:22 +00:00
|
|
|
if (!postData2) {
|
|
|
|
logger.warn(
|
|
|
|
"fedimbed",
|
2023-07-28 03:12:14 +00:00
|
|
|
`Bailing trying to re-embed "${url}": Failed to get post from normal and MastoAPI.`
|
2022-12-06 02:50:22 +00:00
|
|
|
);
|
2022-12-06 03:46:07 +00:00
|
|
|
} else if (postData2.error) {
|
2022-12-06 04:10:15 +00:00
|
|
|
logger.error(
|
|
|
|
"fedimbed",
|
2022-12-31 19:59:21 +00:00
|
|
|
`Bailing trying to re-embed "${url}", MastoAPI gave us error: ${JSON.stringify(
|
|
|
|
postData2.error
|
|
|
|
)}`
|
2022-12-06 04:10:15 +00:00
|
|
|
);
|
2022-12-06 02:50:22 +00:00
|
|
|
} else {
|
2022-12-31 19:47:29 +00:00
|
|
|
cw =
|
|
|
|
postData2.spoiler_warning ?? postData2.spoiler_text ?? postData2.cw;
|
2022-12-06 02:50:22 +00:00
|
|
|
content =
|
|
|
|
postData2.akkoma?.source?.content ??
|
|
|
|
postData2.pleroma?.content?.["text/plain"] ??
|
2022-12-31 19:47:29 +00:00
|
|
|
postData2.text ??
|
2022-12-06 02:50:22 +00:00
|
|
|
postData2.content;
|
|
|
|
author = {
|
2022-12-31 20:15:02 +00:00
|
|
|
name:
|
|
|
|
postData2.account?.display_name ??
|
|
|
|
postData2.account?.username ??
|
|
|
|
postData2.user?.name ??
|
|
|
|
postData2.user?.username,
|
2022-12-06 17:39:24 +00:00
|
|
|
handle:
|
2022-12-31 20:12:26 +00:00
|
|
|
postData2.account?.fqn ??
|
2022-12-31 19:47:29 +00:00
|
|
|
`${postData2.account?.username ?? postData2.user?.username}@${
|
|
|
|
urlObj.hostname
|
|
|
|
}`,
|
|
|
|
url:
|
|
|
|
postData2.account?.url ??
|
|
|
|
`${urlObj.origin}/@${
|
|
|
|
postData2.account?.username ?? postData2.user?.username
|
|
|
|
}`,
|
|
|
|
avatar: postData2.account?.avatar ?? postData2.user?.avatarUrl,
|
2022-12-06 02:50:22 +00:00
|
|
|
};
|
2022-12-31 20:12:26 +00:00
|
|
|
timestamp = postData2.created_at ?? postData2.createdAt;
|
2023-07-28 16:34:15 +00:00
|
|
|
if (!spoiler && postData2.sensitive) {
|
|
|
|
spoiler = true;
|
|
|
|
}
|
2023-08-15 04:42:12 +00:00
|
|
|
emotes = postData2.emojis
|
|
|
|
.filter((x) => !x.name.endsWith("@."))
|
|
|
|
.map((x) => ({name: `:${x.name}:`, url: x.url}));
|
2023-07-28 16:34:15 +00:00
|
|
|
|
2022-12-31 19:47:29 +00:00
|
|
|
const attachments = postData2.media_attachments ?? postData2.files;
|
|
|
|
if (attachments) {
|
|
|
|
for (const attachment of attachments) {
|
|
|
|
const fileType =
|
|
|
|
attachment.pleroma?.mime_type ?? attachment.type.indexOf("/") > -1
|
|
|
|
? attachment.type
|
|
|
|
: attachment.type +
|
2023-01-06 18:23:22 +00:00
|
|
|
"/" +
|
2022-12-31 19:47:29 +00:00
|
|
|
(url.match(/\.([a-z0-9]{3,4})$/)?.[0] ??
|
|
|
|
attachment.type == "image"
|
|
|
|
? "png"
|
|
|
|
: attachment.type == "video"
|
|
|
|
? "mp4"
|
|
|
|
: "mpeg");
|
|
|
|
if (attachment.type.startsWith("image")) {
|
|
|
|
images.push({
|
|
|
|
url: attachment.url,
|
|
|
|
desc: attachment.description ?? attachment.comment,
|
|
|
|
type: fileType,
|
|
|
|
});
|
|
|
|
} else if (attachment.type.startsWith("video")) {
|
|
|
|
videos.push({
|
|
|
|
url: attachment.url,
|
|
|
|
desc: attachment.description ?? attachment.comment,
|
|
|
|
type: fileType,
|
|
|
|
});
|
|
|
|
} else if (attachment.type.startsWith("audio")) {
|
|
|
|
audios.push({
|
|
|
|
url: attachment.url,
|
|
|
|
desc: attachment.description ?? attachment.comment,
|
|
|
|
type: fileType,
|
|
|
|
});
|
|
|
|
}
|
2022-12-07 00:01:44 +00:00
|
|
|
}
|
2022-12-06 02:50:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2022-12-07 04:17:12 +00:00
|
|
|
if (postData.id) {
|
|
|
|
const realUrlObj = new URL(postData.id);
|
|
|
|
if (realUrlObj.origin != urlObj.origin) {
|
|
|
|
platform = await resolvePlatform(postData.id);
|
|
|
|
color = PLATFORM_COLORS[platform];
|
|
|
|
platformName = platform
|
|
|
|
.replace("gotosocial", "GoToSocial")
|
|
|
|
.replace(/^(.)/, (_, c) => c.toUpperCase());
|
2022-12-07 04:18:54 +00:00
|
|
|
url = postData.id;
|
2022-12-07 04:17:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-06 18:02:46 +00:00
|
|
|
content =
|
|
|
|
postData._misskey_content ?? postData.source?.content ?? postData.content;
|
2022-12-06 02:50:22 +00:00
|
|
|
cw = postData.summary;
|
2022-12-06 03:39:20 +00:00
|
|
|
timestamp = postData.published;
|
2023-07-28 16:34:15 +00:00
|
|
|
if (!spoiler && postData.sensitive) {
|
|
|
|
spoiler = true;
|
|
|
|
}
|
2023-09-23 02:58:42 +00:00
|
|
|
|
|
|
|
if (postData.tag)
|
|
|
|
emotes = postData.tag
|
|
|
|
.filter((x) => !!x.icon)
|
|
|
|
.map((x) => ({name: x.name, url: x.icon.url}));
|
2023-07-28 06:09:34 +00:00
|
|
|
|
|
|
|
// NB: gts doesnt send singular attachments as areay
|
|
|
|
const attachments = Array.isArray(postData.attachment)
|
|
|
|
? postData.attachment
|
|
|
|
: [postData.attachment];
|
|
|
|
for (const attachment of attachments) {
|
2023-09-23 02:50:19 +00:00
|
|
|
if (attachment.mediaType) {
|
|
|
|
if (attachment.mediaType.startsWith("video/")) {
|
|
|
|
videos.push({
|
|
|
|
url: attachment.url,
|
|
|
|
desc: attachment.name,
|
|
|
|
type: attachment.mediaType,
|
|
|
|
});
|
|
|
|
} else if (attachment.mediaType.startsWith("image/")) {
|
|
|
|
images.push({
|
|
|
|
url: attachment.url,
|
|
|
|
desc: attachment.name,
|
|
|
|
type: attachment.mediaType,
|
|
|
|
});
|
|
|
|
} else if (attachment.mediaType.startsWith("audio/")) {
|
|
|
|
audios.push({
|
|
|
|
url: attachment.url,
|
|
|
|
desc: attachment.name,
|
|
|
|
type: attachment.mediaType,
|
|
|
|
});
|
|
|
|
}
|
2022-12-06 17:39:24 +00:00
|
|
|
}
|
2022-12-06 02:50:22 +00:00
|
|
|
}
|
|
|
|
|
2023-09-23 02:50:19 +00:00
|
|
|
if (postData.image?.url) {
|
|
|
|
const imageUrl = new URL(postData.image?.url);
|
|
|
|
images.push({
|
|
|
|
url: postData.image?.url,
|
|
|
|
desc: "",
|
|
|
|
type:
|
|
|
|
"image/" +
|
|
|
|
imageUrl.pathname.substring(imageUrl.pathname.lastIndexOf(".") + 1),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (postData.name) title = postData.name;
|
|
|
|
|
2022-12-06 02:50:22 +00:00
|
|
|
// Author data is not sent with the post with AS2
|
2023-07-26 03:21:29 +00:00
|
|
|
const authorData = await signedFetch(
|
|
|
|
postData.actor ?? postData.attributedTo,
|
|
|
|
{
|
|
|
|
headers: {
|
|
|
|
"User-Agent": FRIENDLY_USERAGENT,
|
|
|
|
Accept: "application/activity+json",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
)
|
2022-12-06 02:50:22 +00:00
|
|
|
.then((res) => res.json())
|
|
|
|
.catch((err) => {
|
|
|
|
logger.error("fedimbed", `Failed to get author for "${url}": ${err}`);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (authorData) {
|
2023-09-23 03:03:22 +00:00
|
|
|
const authorUrlObj = new URL(authorData.url ?? authorData.id);
|
2022-12-06 02:50:22 +00:00
|
|
|
author = {
|
|
|
|
name: authorData.name,
|
2022-12-07 04:17:12 +00:00
|
|
|
handle: `${authorData.preferredUsername}@${authorUrlObj.hostname}`,
|
2022-12-06 02:50:22 +00:00
|
|
|
url: authorData.url,
|
2023-09-23 03:06:20 +00:00
|
|
|
avatar: authorData.icon?.url,
|
2022-12-06 02:50:22 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// We could just continue without author but it'd look ugly and be confusing.
|
|
|
|
if (!author) {
|
|
|
|
logger.warn(
|
2022-12-06 03:03:02 +00:00
|
|
|
"fedimbed",
|
|
|
|
`Bailing trying to re-embed "${url}": Failed to get author.`
|
2022-12-06 02:50:22 +00:00
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Start constructing embed
|
|
|
|
content = content ?? "";
|
|
|
|
cw = cw ?? "";
|
|
|
|
|
|
|
|
// TODO: convert certain HTML tags back to markdown
|
2022-12-20 01:20:30 +00:00
|
|
|
content = content.replace(/<\/?\s*br\s*\/?>/g, "\n");
|
2022-12-09 19:17:51 +00:00
|
|
|
content = content.replace(/<\/p><p>/g, "\n\n");
|
2023-10-21 23:14:52 +00:00
|
|
|
content = content.replace(/<ol>/g, "\n");
|
2023-10-21 23:12:31 +00:00
|
|
|
content = content.replace(/<li>/g, "- ");
|
|
|
|
content = content.replace(/<\/li>/g, "\n");
|
|
|
|
content = content.replace(
|
|
|
|
/<a .*?href="([^"]+?)".*?>([^<]+?)<\/a>/g,
|
|
|
|
"[$2]($1)"
|
|
|
|
);
|
|
|
|
content = content.replace(/<\/?code>/g, "`");
|
|
|
|
content = content.replace(/<\/?em>/g, "*");
|
|
|
|
content = content.replace(/<\/?u>/g, "__");
|
|
|
|
content = content.replace(/<\/?s>/g, "~~");
|
2022-12-06 03:01:25 +00:00
|
|
|
content = content.replace(/(<([^>]+)>)/gi, "");
|
2022-12-06 02:50:22 +00:00
|
|
|
content = parseHtmlEntities(content);
|
|
|
|
|
2023-08-15 04:42:12 +00:00
|
|
|
for (const emote of emotes) {
|
|
|
|
content = content.replaceAll(emote.name, `[${emote.name}](${emote.url})`);
|
|
|
|
}
|
|
|
|
|
2022-12-06 03:01:25 +00:00
|
|
|
cw = cw.replace(/(<([^>]+)>)/gi, "");
|
2022-12-06 02:50:22 +00:00
|
|
|
cw = parseHtmlEntities(cw);
|
|
|
|
|
|
|
|
let desc = "";
|
|
|
|
let MAX_LENGTH = 3999;
|
2022-12-07 00:01:44 +00:00
|
|
|
if (
|
|
|
|
cw != "" &&
|
|
|
|
images.length == 0 &&
|
|
|
|
videos.length == 0 &&
|
|
|
|
audios.length == 0
|
|
|
|
) {
|
2022-12-06 02:50:22 +00:00
|
|
|
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";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-23 02:50:19 +00:00
|
|
|
const user = author.name
|
|
|
|
? `${author.name} (${author.handle})`
|
|
|
|
: author.handle;
|
|
|
|
|
2022-12-06 02:50:22 +00:00
|
|
|
const baseEmbed = {
|
|
|
|
color,
|
|
|
|
url,
|
2022-12-06 03:39:20 +00:00
|
|
|
timestamp,
|
2022-12-06 02:50:22 +00:00
|
|
|
description: desc,
|
2023-09-23 02:50:19 +00:00
|
|
|
title: title ?? user,
|
|
|
|
author: title
|
|
|
|
? {
|
|
|
|
name: user,
|
|
|
|
url: author.url,
|
|
|
|
}
|
|
|
|
: null,
|
2022-12-06 03:39:20 +00:00
|
|
|
footer: {
|
|
|
|
text: platformName,
|
2022-12-06 02:50:22 +00:00
|
|
|
},
|
|
|
|
thumbnail: {
|
2022-12-06 03:14:16 +00:00
|
|
|
url: author.avatar,
|
2022-12-06 02:50:22 +00:00
|
|
|
},
|
|
|
|
fields: [],
|
|
|
|
};
|
2022-12-06 17:39:24 +00:00
|
|
|
if (images.length > 0) {
|
|
|
|
if (images.length > 1) {
|
2022-12-06 02:50:22 +00:00
|
|
|
baseEmbed.fields.push({
|
|
|
|
name: "Images",
|
2022-12-06 17:39:24 +00:00
|
|
|
value: images
|
2022-12-06 02:50:22 +00:00
|
|
|
.map((attachment, index) => `[Image ${index + 1}](${attachment.url})`)
|
|
|
|
.join(" | "),
|
2022-12-06 17:39:24 +00:00
|
|
|
inline: true,
|
2022-12-06 02:50:22 +00:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
baseEmbed.fields.push({
|
|
|
|
name: "Image",
|
2022-12-06 17:39:24 +00:00
|
|
|
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(" | "),
|
|
|
|
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(" | "),
|
|
|
|
inline: true,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
baseEmbed.fields.push({
|
|
|
|
name: "Audio",
|
|
|
|
value: `[Click for audio](${audios[0].url})`,
|
|
|
|
inline: true,
|
2022-12-06 02:50:22 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const embeds = [];
|
|
|
|
|
2022-12-06 17:39:24 +00:00
|
|
|
if (images.length > 0) {
|
|
|
|
for (const attachment of images) {
|
2022-12-06 04:46:03 +00:00
|
|
|
const embed = Object.assign({}, baseEmbed);
|
|
|
|
embed.image = {
|
|
|
|
url: attachment.url,
|
|
|
|
};
|
|
|
|
embeds.push(embed);
|
|
|
|
}
|
2022-12-06 04:37:29 +00:00
|
|
|
} else {
|
|
|
|
embeds.push(baseEmbed);
|
|
|
|
}
|
2022-12-06 02:50:22 +00:00
|
|
|
|
2022-12-06 17:55:57 +00:00
|
|
|
const files = [];
|
|
|
|
|
|
|
|
if (videos.length > 0) {
|
|
|
|
for (const attachment of videos) {
|
|
|
|
const size = await fetch(attachment.url, {
|
|
|
|
method: "HEAD",
|
|
|
|
headers: {
|
|
|
|
"User-Agent": FRIENDLY_USERAGENT,
|
|
|
|
},
|
2023-01-23 00:42:52 +00:00
|
|
|
}).then((res) => Number(res.headers.get("Content-Length")));
|
2022-12-06 17:55:57 +00:00
|
|
|
|
|
|
|
if (size <= getUploadLimit(msg.channel.guild)) {
|
|
|
|
const file = await fetch(attachment.url, {
|
|
|
|
headers: {
|
|
|
|
"User-Agent": FRIENDLY_USERAGENT,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
.then((res) => res.arrayBuffer())
|
|
|
|
.then((buf) => Buffer.from(buf));
|
|
|
|
|
|
|
|
files.push({
|
2023-01-25 19:36:42 +00:00
|
|
|
filename:
|
2023-01-17 19:27:55 +00:00
|
|
|
(cw != "" || spoiler ? "SPOILER_" : "") +
|
|
|
|
(attachment.type.indexOf("/") > -1
|
2023-01-06 18:21:32 +00:00
|
|
|
? attachment.type.replace("/", ".")
|
|
|
|
: attachment.type +
|
2023-01-06 18:23:22 +00:00
|
|
|
"." +
|
2023-01-17 19:27:55 +00:00
|
|
|
(url.match(/\.([a-z0-9]{3,4})$/)?.[0] ?? "mp4")),
|
2023-01-25 19:36:42 +00:00
|
|
|
file,
|
2022-12-06 17:58:57 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (audios.length > 0) {
|
|
|
|
for (const attachment of audios) {
|
|
|
|
const size = await fetch(attachment.url, {
|
|
|
|
method: "HEAD",
|
|
|
|
headers: {
|
|
|
|
"User-Agent": FRIENDLY_USERAGENT,
|
|
|
|
},
|
2023-01-23 00:42:52 +00:00
|
|
|
}).then((res) => Number(res.headers.get("Content-Length")));
|
2022-12-06 17:58:57 +00:00
|
|
|
|
|
|
|
if (size <= getUploadLimit(msg.channel.guild)) {
|
|
|
|
const file = await fetch(attachment.url, {
|
|
|
|
headers: {
|
|
|
|
"User-Agent": FRIENDLY_USERAGENT,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
.then((res) => res.arrayBuffer())
|
|
|
|
.then((buf) => Buffer.from(buf));
|
|
|
|
|
|
|
|
files.push({
|
2023-01-25 19:36:42 +00:00
|
|
|
filename:
|
2023-01-17 19:27:55 +00:00
|
|
|
(cw != "" || spoiler ? "SPOILER_" : "") +
|
|
|
|
(attachment.type.indexOf("/") > -1
|
2023-01-06 18:21:32 +00:00
|
|
|
? attachment.type
|
|
|
|
.replace("/", ".")
|
|
|
|
.replace("mpeg", "mp3")
|
|
|
|
.replace("vnd.wave", "wav")
|
2023-09-23 02:50:19 +00:00
|
|
|
.replace("x-", "")
|
2023-01-06 18:21:32 +00:00
|
|
|
: attachment.type +
|
2023-01-06 18:23:22 +00:00
|
|
|
"." +
|
2023-01-17 19:27:55 +00:00
|
|
|
(url.match(/\.([a-z0-9]{3,4})$/)?.[0] ?? "mp3")),
|
2023-01-25 19:36:42 +00:00
|
|
|
file,
|
2022-12-06 17:55:57 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-11 23:15:52 +00:00
|
|
|
let sendWait = false;
|
|
|
|
if (files.length > 0) {
|
|
|
|
sendWait = true;
|
2023-01-22 05:02:53 +00:00
|
|
|
await msg.addReaction("\uD83D\uDCE4");
|
2023-01-11 23:15:52 +00:00
|
|
|
}
|
|
|
|
|
2023-01-06 04:20:31 +00:00
|
|
|
await msg.channel
|
|
|
|
.createMessage({
|
|
|
|
content:
|
2023-01-17 19:25:05 +00:00
|
|
|
cw != "" &&
|
|
|
|
(images.length > 0 || videos.length > 0 || audios.length > 0)
|
2023-01-06 04:20:31 +00:00
|
|
|
? `:warning: ${cw} || ${url} ||`
|
2023-01-17 19:25:05 +00:00
|
|
|
: spoiler
|
|
|
|
? `|| ${url} ||`
|
|
|
|
: "",
|
2023-01-06 04:20:31 +00:00
|
|
|
embeds,
|
2023-01-25 19:36:42 +00:00
|
|
|
attachments: files,
|
2023-01-06 04:20:31 +00:00
|
|
|
allowedMentions: {
|
|
|
|
repliedUser: false,
|
|
|
|
},
|
|
|
|
messageReference: {
|
|
|
|
messageID: msg.id,
|
2022-12-06 03:35:48 +00:00
|
|
|
},
|
|
|
|
})
|
2023-01-06 04:20:31 +00:00
|
|
|
.then(() => {
|
2023-01-11 23:15:52 +00:00
|
|
|
if (sendWait) {
|
2023-01-25 19:30:16 +00:00
|
|
|
msg.removeReaction("\uD83D\uDCE4");
|
2023-01-11 23:15:52 +00:00
|
|
|
}
|
|
|
|
|
2023-01-06 04:20:31 +00:00
|
|
|
if ((msg.flags & MessageFlags.SUPPRESS_EMBEDS) === 0) {
|
2023-01-22 04:45:57 +00:00
|
|
|
msg.edit({flags: MessageFlags.SUPPRESS_EMBEDS}).catch(() => {});
|
2023-01-06 04:20:31 +00:00
|
|
|
}
|
|
|
|
});
|
2022-12-06 02:50:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
events.add("messageCreate", "fedimbed", async function (msg) {
|
2022-12-06 04:48:39 +00:00
|
|
|
if (msg.author.id == hf.bot.user.id) return;
|
2022-12-06 02:50:22 +00:00
|
|
|
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);
|
2022-12-29 18:18:22 +00:00
|
|
|
for (let url of urls) {
|
2023-01-09 02:13:56 +00:00
|
|
|
const hasSpoiler = SPOILER_REGEX.test(url);
|
2023-01-17 19:25:05 +00:00
|
|
|
url = url
|
|
|
|
.replace(/\|/g, "")
|
|
|
|
.trim()
|
|
|
|
.replace("@\u200b", "@")
|
|
|
|
.replace("@%E2%80%8B", "@");
|
2022-12-06 02:50:22 +00:00
|
|
|
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.`
|
|
|
|
);
|
2023-01-09 02:09:34 +00:00
|
|
|
await processUrl(msg, url, hasSpoiler).catch((err) => {
|
2023-09-23 03:02:00 +00:00
|
|
|
logger.error(
|
|
|
|
"fedimbed",
|
|
|
|
`Error processing "${url}":\n` + err.stack
|
|
|
|
);
|
2022-12-06 02:50:22 +00:00
|
|
|
});
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|