fedimbed: much needed cleanup and reply context
This commit is contained in:
parent
d39bf5f2a4
commit
9fba9d2c9e
1 changed files with 279 additions and 182 deletions
|
@ -122,6 +122,14 @@ async function resolvePlatform(url) {
|
|||
}
|
||||
}
|
||||
|
||||
function normalizePlatform(platform) {
|
||||
return platform
|
||||
.replace("gotosocial", "GoToSocial")
|
||||
.replace("birdsitelive", '"Twitter" (BirdsiteLive)')
|
||||
.replace(/^(.)/, (_, c) => c.toUpperCase())
|
||||
.replace("Cohost", "cohost");
|
||||
}
|
||||
|
||||
const keyId = "https://hf.c7.pm/actor#main-key";
|
||||
const privKey = fs.readFileSync(require.resolve("#root/priv/private.pem"));
|
||||
async function signedFetch(url, options) {
|
||||
|
@ -406,7 +414,7 @@ async function bluesky(msg, url, spoiler = false) {
|
|||
if (data.thread.parent) {
|
||||
const reply = data.thread.parent.post;
|
||||
mainEmbed.author = {
|
||||
name: `Replying to: ${reply.author.displayName} (${reply.author.handle})`,
|
||||
name: `Replying to: ${reply.author.displayName} (@${reply.author.handle})`,
|
||||
icon_url: "https://cdn.discordapp.com/emojis/1308640078825787412.png",
|
||||
url: `https://bsky.app/profile/${reply.author.handle}/post/${reply.uri.substring(
|
||||
reply.uri.lastIndexOf("/") + 1
|
||||
|
@ -570,104 +578,53 @@ async function bluesky(msg, url, spoiler = false) {
|
|||
};
|
||||
}
|
||||
|
||||
async function processUrl(msg, url, spoiler = false, command = false) {
|
||||
let canFedi = await hasFlag(msg.guildID, "fedimbed");
|
||||
let canBsky = await hasFlag(msg.guildID, "bskyEmbeds");
|
||||
async function fetchPost(url, platform, forceMastoAPI = false) {
|
||||
let urlObj = new URL(url);
|
||||
let postData;
|
||||
|
||||
if (command === true) {
|
||||
canFedi = true;
|
||||
canBsky = true;
|
||||
}
|
||||
|
||||
let invalidUrl = false;
|
||||
let urlObj;
|
||||
try {
|
||||
urlObj = new URL(url);
|
||||
} catch {
|
||||
invalidUrl = true;
|
||||
}
|
||||
|
||||
if (invalidUrl) return {};
|
||||
|
||||
if (BSKY_DOMAINS.includes(urlObj.hostname.toLowerCase())) {
|
||||
if (canBsky) {
|
||||
return await bluesky(msg, url, spoiler);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
if (!canFedi) return {};
|
||||
|
||||
// some lemmy instances have old reddit frontend subdomains
|
||||
// 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;
|
||||
}
|
||||
|
||||
let platform = (await resolvePlatform(url)) ?? "<no nodeinfo>";
|
||||
let color = PLATFORM_COLORS[platform];
|
||||
let platformName = platform
|
||||
.replace("gotosocial", "GoToSocial")
|
||||
.replace("birdsitelive", '"Twitter" (BirdsiteLive)')
|
||||
.replace(/^(.)/, (_, c) => c.toUpperCase())
|
||||
.replace("Cohost", "cohost");
|
||||
|
||||
const images = [];
|
||||
const videos = [];
|
||||
const audios = [];
|
||||
let content,
|
||||
cw,
|
||||
author,
|
||||
timestamp,
|
||||
title,
|
||||
poll,
|
||||
emotes = [],
|
||||
sensitive = false;
|
||||
|
||||
// Fetch post
|
||||
let rawPostData;
|
||||
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}`);
|
||||
}
|
||||
if (!rawPostData) {
|
||||
if (!forceMastoAPI) {
|
||||
let rawPostData;
|
||||
try {
|
||||
rawPostData = await fetch(url, {
|
||||
rawPostData = await signedFetch(url, {
|
||||
headers: {
|
||||
"User-Agent": FRIENDLY_USERAGENT,
|
||||
Accept: "application/activity+json",
|
||||
},
|
||||
}).then((res) => res.text());
|
||||
} catch (err) {
|
||||
logger.error("fedimbed", `Failed to fetch "${url}": ${err}`);
|
||||
logger.error("fedimbed", `Failed to signed fetch "${url}", retrying unsigned: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
let postData;
|
||||
if (rawPostData?.startsWith("{")) {
|
||||
try {
|
||||
postData = JSON.parse(rawPostData);
|
||||
} catch (err) {
|
||||
logger.error("fedimbed", `Failed to decode JSON for "${url}": ${err}\n "${rawPostData}"`);
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.warn("fedimbed", `Got non-JSON for "${url}": ${rawPostData}`);
|
||||
}
|
||||
|
||||
if (postData?.error) {
|
||||
logger.error("fedimbed", `Received error for "${url}": ${postData.error}`);
|
||||
if (rawPostData?.startsWith("{")) {
|
||||
try {
|
||||
postData = JSON.parse(rawPostData);
|
||||
} catch (err) {
|
||||
logger.error("fedimbed", `Failed to decode JSON for "${url}": ${err}\n "${rawPostData}"`);
|
||||
}
|
||||
} else {
|
||||
logger.warn("fedimbed", `Got non-JSON for "${url}": ${rawPostData}`);
|
||||
}
|
||||
|
||||
if (postData?.error) {
|
||||
logger.error("fedimbed", `Received error for "${url}": ${postData.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!postData) {
|
||||
// We failed to get post.
|
||||
// Assume it was due to AFM or forced HTTP signatures and use MastoAPI
|
||||
// We failed to get post. (or we forced ourselves to be here)
|
||||
// Assume it was due to AFM or some other issue and (try to) use MastoAPI (or equivalent)
|
||||
|
||||
// Follow redirect from /object since we need the ID from /notice
|
||||
if (PATH_REGEX.pleroma.test(urlObj.pathname)) {
|
||||
|
@ -754,119 +711,211 @@ async function processUrl(msg, url, spoiler = false, command = false) {
|
|||
|
||||
if (!postData2) {
|
||||
logger.warn("fedimbed", `Bailing trying to re-embed "${url}": Failed to get post from normal and MastoAPI.`);
|
||||
return null;
|
||||
} else if (postData2.error) {
|
||||
logger.error(
|
||||
"fedimbed",
|
||||
`Bailing trying to re-embed "${url}", MastoAPI gave us error: ${JSON.stringify(postData2.error)}`
|
||||
);
|
||||
} else {
|
||||
cw = postData2.spoiler_warning ?? postData2.spoiler_text ?? postData2.cw;
|
||||
content =
|
||||
postData2.akkoma?.source?.content ??
|
||||
postData2.pleroma?.content?.["text/plain"] ??
|
||||
postData2.text ??
|
||||
postData2.content;
|
||||
author = {
|
||||
name:
|
||||
postData2.account?.display_name ??
|
||||
postData2.account?.username ??
|
||||
postData2.user?.name ??
|
||||
postData2.user?.username,
|
||||
handle:
|
||||
postData2.account?.fqn ?? `${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,
|
||||
};
|
||||
timestamp = postData2.created_at ?? postData2.createdAt;
|
||||
emotes = postData2.emojis.filter((x) => !x.name.endsWith("#.")).map((x) => ({name: `:${x.name}:`, url: x.url}));
|
||||
sensitive = postData2.sensitive;
|
||||
return null;
|
||||
}
|
||||
|
||||
const attachments = postData2.media_attachments ?? postData2.files;
|
||||
if (attachments) {
|
||||
for (const attachment of attachments) {
|
||||
const contentType = await fetch(attachment.url, {
|
||||
method: "HEAD",
|
||||
}).then((res) => res.headers.get("Content-Type"));
|
||||
postData2._fedimbed_mastoapi = true;
|
||||
return postData2;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return postData;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
async function processUrl(msg, url, spoiler = false, command = false) {
|
||||
let canFedi = await hasFlag(msg.guildID, "fedimbed");
|
||||
let canBsky = await hasFlag(msg.guildID, "bskyEmbeds");
|
||||
|
||||
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 (command === true) {
|
||||
canFedi = true;
|
||||
canBsky = true;
|
||||
}
|
||||
|
||||
let invalidUrl = false;
|
||||
let urlObj;
|
||||
try {
|
||||
urlObj = new URL(url);
|
||||
} catch {
|
||||
invalidUrl = true;
|
||||
}
|
||||
|
||||
if (invalidUrl) return {};
|
||||
|
||||
if (BSKY_DOMAINS.includes(urlObj.hostname.toLowerCase())) {
|
||||
if (canBsky) {
|
||||
return await bluesky(msg, url, spoiler);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
if (!canFedi) return {};
|
||||
|
||||
// some lemmy instances have old reddit frontend subdomains
|
||||
// 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;
|
||||
}
|
||||
|
||||
let platform = (await resolvePlatform(url)) ?? "<no nodeinfo>";
|
||||
let color = PLATFORM_COLORS[platform];
|
||||
let platformName = normalizePlatform(platform);
|
||||
|
||||
const images = [];
|
||||
const videos = [];
|
||||
const audios = [];
|
||||
let content,
|
||||
cw,
|
||||
author,
|
||||
timestamp,
|
||||
title,
|
||||
poll,
|
||||
context,
|
||||
contextUrl,
|
||||
emotes = [],
|
||||
sensitive = false;
|
||||
|
||||
// Fetch post
|
||||
const postData = await fetchPost(url, platform);
|
||||
|
||||
if (postData._fedimbed_mastoapi) {
|
||||
if (postData.url) {
|
||||
const realUrlObj = new URL(postData.url);
|
||||
if (realUrlObj.origin != urlObj.origin) {
|
||||
platform = await resolvePlatform(postData.url);
|
||||
color = PLATFORM_COLORS[platform];
|
||||
platformName = normalizePlatform(platform);
|
||||
url = postData.url;
|
||||
urlObj = realUrlObj;
|
||||
}
|
||||
}
|
||||
|
||||
cw = postData.spoiler_warning ?? postData.spoiler_text ?? postData.cw;
|
||||
content =
|
||||
postData.akkoma?.source?.content ??
|
||||
postData.pleroma?.content?.["text/plain"] ??
|
||||
postData.text ??
|
||||
postData.content;
|
||||
author = {
|
||||
name:
|
||||
postData.account?.display_name ?? postData.account?.username ?? postData.user?.name ?? postData.user?.username,
|
||||
handle: postData.account?.fqn ?? `${postData.account?.username ?? postData.user?.username}@${urlObj.hostname}`,
|
||||
url: postData.account?.url ?? `${urlObj.origin}/@${postData.account?.username ?? postData.user?.username}`,
|
||||
avatar: postData.account?.avatar ?? postData.user?.avatarUrl,
|
||||
};
|
||||
timestamp = postData.created_at ?? postData.createdAt;
|
||||
emotes = postData.emojis.filter((x) => !x.name.endsWith("#.")).map((x) => ({name: `:${x.name}:`, url: x.url}));
|
||||
sensitive = postData.sensitive;
|
||||
|
||||
if (postData.in_reply_to_id) {
|
||||
// this url is a dummy and will failed if gone to normally
|
||||
const replyData = await fetchPost(
|
||||
`https://${urlObj.origin}/@fedimbed_reply_fake_user_sorry/${postData.in_reply_to_id}`,
|
||||
platform,
|
||||
true
|
||||
);
|
||||
if (replyData) {
|
||||
contextUrl = replyData.url;
|
||||
context = `Replying to: ${
|
||||
replyData.account?.display_name ??
|
||||
replyData.account?.username ??
|
||||
replyData.user?.name ??
|
||||
replyData.user?.username
|
||||
} (${
|
||||
replyData.account?.fqn ?? `${replyData.account?.username ?? replyData.user?.username}@${urlObj.hostname}`
|
||||
})`;
|
||||
}
|
||||
}
|
||||
|
||||
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"));
|
||||
|
||||
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 (!spoiler && postData2.sensitive && attachments.length > 0) {
|
||||
spoiler = true;
|
||||
}
|
||||
|
||||
if (postData2.poll) {
|
||||
poll = {
|
||||
end: new Date(postData2.poll.expires_at),
|
||||
total: postData2.poll.votes_count,
|
||||
options: postData2.poll.options.map((o) => ({
|
||||
name: o.title,
|
||||
count: o.votes_count,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!spoiler && postData.sensitive && attachments.length > 0) {
|
||||
spoiler = true;
|
||||
}
|
||||
|
||||
if (postData.poll) {
|
||||
poll = {
|
||||
end: new Date(postData.poll.expires_at),
|
||||
total: postData.poll.votes_count,
|
||||
options: postData.poll.options.map((o) => ({
|
||||
name: o.title,
|
||||
count: o.votes_count,
|
||||
})),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
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());
|
||||
platformName = normalizePlatform(platform);
|
||||
url = postData.id;
|
||||
urlObj = realUrlObj;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -882,6 +931,49 @@ async function processUrl(msg, url, spoiler = false, command = false) {
|
|||
emotes = tag.filter((x) => !!x.icon).map((x) => ({name: x.name, url: x.icon.url}));
|
||||
}
|
||||
|
||||
if (postData.inReplyTo) {
|
||||
contextUrl = postData.inReplyTo;
|
||||
context = "Replying to: ";
|
||||
|
||||
const replyData = await fetchPost(postData.inReplyTo, platform);
|
||||
if (replyData) {
|
||||
if (replyData._fedimbed_mastoapi) {
|
||||
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}`
|
||||
})`;
|
||||
} else {
|
||||
const authorData = await signedFetch(postData.actor ?? postData.attributedTo, {
|
||||
headers: {
|
||||
"User-Agent": FRIENDLY_USERAGENT,
|
||||
Accept: "application/activity+json",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((err) => {
|
||||
/*if (platform !== "cohost")*/ logger.error("fedimbed", `Failed to get author for "${url}": ${err}`);
|
||||
});
|
||||
|
||||
if (authorData) {
|
||||
const authorUrlObj = new URL(authorData.url ?? authorData.id);
|
||||
context += `${authorData.name} (${authorData.preferredUsername}@${authorUrlObj.hostname})`;
|
||||
} 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}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
context += "<failed to get reply author>";
|
||||
}
|
||||
}
|
||||
|
||||
// NB: gts doesnt send singular attachments as array
|
||||
const attachments = Array.isArray(postData.attachment) ? postData.attachment : [postData.attachment];
|
||||
for (const attachment of attachments) {
|
||||
|
@ -991,8 +1083,7 @@ async function processUrl(msg, url, spoiler = false, command = false) {
|
|||
})
|
||||
.then((res) => res.json())
|
||||
.catch((err) => {
|
||||
// only posts can be activity+json right now, reduce log spam
|
||||
if (platform !== "cohost") logger.error("fedimbed", `Failed to get author for "${url}": ${err}`);
|
||||
/*if (platform !== "cohost")*/ logger.error("fedimbed", `Failed to get author for "${url}": ${err}`);
|
||||
});
|
||||
|
||||
if (authorData) {
|
||||
|
@ -1004,7 +1095,7 @@ async function processUrl(msg, url, spoiler = false, command = false) {
|
|||
avatar: authorData.icon?.url,
|
||||
};
|
||||
} else {
|
||||
// bootleg author, mainly for cohost
|
||||
// bootleg author
|
||||
const authorUrl = postData.actor ?? postData.attributedTo;
|
||||
const authorUrlObj = new URL(authorUrl);
|
||||
const name = authorUrlObj.pathname.substring(authorUrlObj.pathname.lastIndexOf("/") + 1);
|
||||
|
@ -1083,6 +1174,12 @@ async function processUrl(msg, url, spoiler = false, command = false) {
|
|||
name: user,
|
||||
url: author.url,
|
||||
}
|
||||
: context
|
||||
? {
|
||||
name: context,
|
||||
url: contextUrl,
|
||||
icon_url: "https://cdn.discordapp.com/emojis/1308640078825787412.png",
|
||||
}
|
||||
: null,
|
||||
footer: {
|
||||
text: platformName,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue