diff --git a/.rgignore b/.rgignore deleted file mode 100644 index 8fce603..0000000 --- a/.rgignore +++ /dev/null @@ -1 +0,0 @@ -data/ diff --git a/src/index.js b/src/index.js index 9e85c14..c6ca6de 100644 --- a/src/index.js +++ b/src/index.js @@ -56,7 +56,6 @@ global.hf = { events, timer, database, - event_stats: {}, }; const {formatUsername} = require("#util/misc.js"); @@ -70,7 +69,7 @@ for (const file of fs.readdirSync(resolve(__dirname, "modules"), {withFileTypes: require(`#modules/${file.name}`); logger.info("hf:modules", `Loaded module: "${file.name}"`); } catch (err) { - logger.error("hf:modules", `Failed to load "${file.name}": ${err.stack}`); + logger.error("hf:modules", `Failed to load "${file.name}": ${err}`); } } @@ -90,7 +89,9 @@ bot.on("messageCreate", async (msg) => { await CommandDispatcher(msg); } catch (err) { - logger.error("hf:main", `Failed to dispatch command: ${err.stack}`); + const stack = (err?.stack ?? err.message).split("\n"); + const error = stack.shift(); + logger.error("hf:main", `Failed to dispatch command: ${error}\n\t${stack.join("\n\t")}`); } }); bot.on("messageUpdate", async (msg, oldMsg) => { @@ -100,7 +101,9 @@ bot.on("messageUpdate", async (msg, oldMsg) => { await CommandDispatcher(msg); } } catch (err) { - logger.error("hf:main", `Failed to dispatch command update: ${err.stack}`); + const stack = (err?.stack ?? err.message).split("\n"); + const error = stack.shift(); + logger.error("hf:main", `Failed to dispatch command update: ${error}\n\t${stack.join("\n\t")}`); } }); bot.on("messageReactionAdd", async (msg, reaction, reactor) => { @@ -130,14 +133,18 @@ bot.on("messageReactionAdd", async (msg, reaction, reactor) => { await msg.delete("Command sender requested output deletion."); } catch (err) { - logger.error("hf:main", `Failed to self-delete message: ${err.stack}`); + const stack = (err?.stack ?? err.message).split("\n"); + const error = stack.shift(); + logger.error("hf:main", `Failed to self-delete message: ${error}\n\t${stack.join("\n\t")}`); } }); bot.on("interactionCreate", async (interaction) => { try { await InteractionDispatcher(interaction); } catch (err) { - logger.error("hf:main", `Failed to dispatch interaction command: ${err.stack}`); + const stack = (err?.stack ?? err.message).split("\n"); + const error = stack.shift(); + logger.error("hf:main", `Failed to dispatch interaction command: ${error}\n\t${stack.join("\n\t")}`); } }); @@ -153,7 +160,7 @@ bot.once("ready", async () => { }); } } catch (err) { - logger.error("hf:main", `Failed to send startup message, API probably broken currently.\n${err.stack}`); + logger.error("hf:main", `Failed to send startup message, API probably broken currently.\n${err}`); } bot.on("ready", () => { logger.info("hf:main", "Reconnected to Discord."); @@ -202,23 +209,23 @@ bot.once("ready", async () => { try { await bot.requestHandler.request("PUT", APIEndpoints.COMMANDS(bot.application.id), true, commands); } catch (err) { - logger.error("hf:main", `Failed to update interaction commands, API probably broken currently.\n${err.stack}`); + logger.error("hf:main", `Failed to update interaction commands, API probably broken currently.\n${err}`); } } }); bot.on("error", (err) => { - logger.error("hf:main", `Catching error: ${err.stack}`); + logger.error("hf:main", "Catching error: " + err); }); bot.on("warn", (err) => { - logger.warn("hf:main", `Catching warn: ${err}`); + logger.warn("hf:main", "Catching warn: " + err); }); bot.on("shardDisconnect", (err, id) => { - logger.verbose("hf:shard", `Disconnecting from shard ${id}: ${err?.stack ?? err ?? "no error???"}`); + logger.verbose("hf:shard", `Disconnecting from shard ${id}: ${err}`); }); bot.on("shardResume", (id) => { - logger.verbose("hf:shard", `Resuming on shard ${id}`); + logger.verbose("hf:shard", "Resuming on shard " + id); }); bot.on("shardPreReady", (id) => { logger.verbose("hf:shard", `Shard ${id} getting ready`); @@ -227,14 +234,7 @@ bot.on("shardReady", (id) => { logger.verbose("hf:shard", `Shard ${id} ready`); }); bot.on("unknown", (packet, id) => { - logger.verbose("hf:main", `Shard ${id} caught unknown packet:\n ${JSON.stringify(packet)}`); -}); - -bot.on("rawWS", (packet) => { - if (packet.op === 0 && packet.t != null) { - if (!hf.event_stats[packet.t]) hf.event_stats[packet.t] = 0; - hf.event_stats[packet.t]++; - } + logger.verbose("hf:main", `Shard ${id} caught unknown packet: ${JSON.stringify(packet)}`); }); instead("spawn", bot.shards, function (args, orig) { diff --git a/src/modules/bot.js b/src/modules/bot.js index 765a16c..7352bc5 100644 --- a/src/modules/bot.js +++ b/src/modules/bot.js @@ -221,17 +221,3 @@ settings.callback = async function (msg, line, [cmd, key, value]) { } }; hf.registerCommand(settings); - -const socketstats = new Command("socketstats"); -socketstats.category = CATEGORY; -socketstats.helpText = "List the counts of socket events this session"; -socketstats.callback = function () { - let out = "```c\n"; - for (const [event, count] of Object.entries(hf.event_stats)) { - out += `"${event}": ${count}\n`; - } - out += "```"; - - return out; -}; -hf.registerCommand(socketstats); diff --git a/src/modules/codePreviews.js b/src/modules/codePreviews.js index d96961e..49e9c80 100644 --- a/src/modules/codePreviews.js +++ b/src/modules/codePreviews.js @@ -60,12 +60,12 @@ async function processFile(link, originalLink, spoiler = false, linkFile = false fileName = `[${fileName}](<${originalLink}>)`; } - const lineStr = urlObj.hash.match(/#L\d+(C\d+)?(-L?\d+)?(C\d+)?/)?.[0]; + const lineStr = urlObj.hash.match(/#L\d+(-L?\d+)?/)?.[0]; let startLine, endLine; let entireFile = false; if (lineStr) { - const [start, end] = lineStr.match(/(?<=[L-]{1,2})\d+/g); + const [start, end] = lineStr.match(/\d+/g); if (!end) { startLine = endLine = start; } else { diff --git a/src/modules/fedimbed.js b/src/modules/fedimbed.js index 173a9e2..fcd4c3c 100644 --- a/src/modules/fedimbed.js +++ b/src/modules/fedimbed.js @@ -21,8 +21,8 @@ const BSKY_POST_REGEX = /^\/profile\/(did:plc:[a-z0-9]+|(did:web:)?[a-z0-9][a-z0-9.-]+[a-z0-9]*)\/post\/([a-z0-9]+)\/?$/i; const PATH_REGEX = { - mastodon: /^\/@(.+?)\/([a-z0-9]+?)\/?/i, - mastodon2: /^\/(.+?)\/statuses\/([a-z0-9]+?)\/?/i, + mastodon: /^\/@(.+?)\/(\d+)\/?/, + mastodon2: /^\/(.+?)\/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]+\/?/, @@ -30,12 +30,12 @@ const PATH_REGEX = { lemmy: /^\/post\/\d+\/?/, honk: /^\/u\/(.+?)\/h\/(.+?)\/?/, pixelfed: /^\/p\/(.+?)\/(.+?)\/?/, - //cohost: /^\/[A-Za-z0-9]+\/post\/\d+-[A-Za-z0-9-]+\/?/, + cohost: /^\/[A-Za-z0-9]+\/post\/\d+-[A-Za-z0-9-]+\/?/, bluesky: BSKY_POST_REGEX, }; const PLATFORM_COLORS = { - mastodon: 0x6363ff, + mastodon: 0x2791da, pleroma: 0xfba457, akkoma: 0x593196, misskey: 0x99c203, @@ -46,9 +46,8 @@ const PLATFORM_COLORS = { birdsitelive: 0x1da1f2, iceshrimp: 0x8e82f9, // YCbCr interpolated as the accent color is a gradient pixelfed: 0x10c5f8, - //cohost: 0x83254f, + cohost: 0x83254f, bluesky: 0x0085ff, - twitter: 0xff6c60, // Nitter accent color }; const BSKY_DOMAINS = [ @@ -68,7 +67,6 @@ const BSKY_DOMAINS = [ /*const TW_DOMAINS = [ "tw.c7.pm", "tw.counter-strike.gay", - "xcancel.com", "twitter.com", "x.com", "fxtwitter.com", @@ -86,49 +84,36 @@ async function resolvePlatform(url) { const urlObj = new URL(url); if (domainCache.has(urlObj.hostname)) return domainCache.get(urlObj.hostname); - try { - const res = await fetch(urlObj.origin + "/.well-known/nodeinfo", { - headers: {"User-Agent": FRIENDLY_USERAGENT}, - }).then((res) => res.text()); + const res = await fetch(urlObj.origin + "/.well-known/nodeinfo", { + headers: {"User-Agent": FRIENDLY_USERAGENT}, + }).then((res) => res.text()); - if (!res.startsWith("{")) { - logger.error("fedimbed", `No nodeinfo for "${urlObj.hostname}"???`); - domainCache.set(urlObj.hostname, null); - return null; - } - - const probe = JSON.parse(res); - - 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; - } catch (err) { - logger.error("fedimbed", `Failed to get nodeinfo for "${url}": ${err}`); - return ""; + if (!res.startsWith("{")) { + logger.error("fedimbed", `No nodeinfo for "${urlObj.hostname}"???`); + domainCache.set(urlObj.hostname, null); + return null; } -} -function normalizePlatform(platform) { - return platform - .replace("gotosocial", "GoToSocial") - .replace("birdsitelive", '"Twitter" (BirdsiteLive)') - .replace(/^(.)/, (_, c) => c.toUpperCase()) - .replace("Cohost", "cohost"); + const probe = JSON.parse(res); + + 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; } const keyId = "https://hf.c7.pm/actor#main-key"; @@ -415,7 +400,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 @@ -476,17 +461,6 @@ async function bluesky(msg, url, spoiler = false) { videos.push({url: videoUrl, desc: post.embed.alt, type: contentType}); embeds.push({...mainEmbed, fields: [{name: "\u200b", value: `[Video Link](${videoUrl})`}]}); - } else if (post.embed.media.$type === "app.bsky.embed.external#view") { - if (post.embed.media.external.uri.includes("tenor.com")) { - const url = new URL(post.embed.media.external.uri); - url.searchParams.delete("hh"); - url.searchParams.delete("ww"); - embeds.push({...mainEmbed, image: {url: url.toString()}}); - } else { - embeds.push(mainEmbed); - } - } else { - embeds.push(mainEmbed); } const quoteData = await blueskyQuoteEmbed(post.embed.record.record); @@ -590,53 +564,104 @@ async function bluesky(msg, url, spoiler = false) { }; } -async function fetchPost(url, platform, forceMastoAPI = false) { - let urlObj = new URL(url); - let postData; +async function processUrl(msg, url, spoiler = false, command = false) { + let canFedi = await hasFlag(msg.guildID, "fedimbed"); + let canBsky = await hasFlag(msg.guildID, "bskyEmbeds"); - if (!forceMastoAPI) { - let rawPostData; + 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)) ?? ""; + 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) { try { - rawPostData = await signedFetch(url, { + rawPostData = await fetch(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) { - 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}`); - } - } - - 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}`); + logger.error("fedimbed", `Failed to fetch "${url}": ${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}"`); + } + } 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. (or we forced ourselves to be here) - // Assume it was due to AFM or some other issue and (try to) use MastoAPI (or equivalent) + // We failed to get post. + // Assume it was due to AFM or forced HTTP signatures and use MastoAPI // Follow redirect from /object since we need the ID from /notice if (PATH_REGEX.pleroma.test(urlObj.pathname)) { @@ -673,17 +698,10 @@ async function fetchPost(url, platform, forceMastoAPI = false) { noteId = noteId.split("#")[0]; } logger.verbose("fedimbed", "Misskey post ID: " + noteId); - - // iceshrimp has an entire .NET rewrite and only supports MastoAPI it seems - // *should* be fine for older iceshrimp-js instances - if (platform == "iceshrimp") { - redirUrl = urlObj.origin + "/api/v1/statuses/" + noteId; - } else { - redirUrl = urlObj.origin + "/api/notes/show/"; - options.method = "POST"; - options.body = JSON.stringify({noteId}); - headers["Content-Type"] = "application/json"; - } + redirUrl = urlObj.origin + "/api/notes/show/"; + options.method = "POST"; + options.body = JSON.stringify({noteId}); + headers["Content-Type"] = "application/json"; } else { logger.error("fedimbed", `Missing MastoAPI replacement for "${platform}"`); } @@ -730,205 +748,110 @@ async function fetchPost(url, platform, forceMastoAPI = 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)}` ); - return null; - } + } 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; - postData2._fedimbed_mastoapi = true; - return postData2; - } else { - return null; - } - } else { - return postData; - } -} + 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")); -async function processUrl(msg, url, spoiler = false, command = false) { - let canFedi = await hasFlag(msg.guildID, "fedimbed"); - let canBsky = await hasFlag(msg.guildID, "bskyEmbeds"); + 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(); - 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)) ?? ""; - 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) { - return {}; - } else 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 ?? x.shortcode)?.endsWith("#.")) - .map((x) => ({name: `:${x.name ?? x.shortcode}:`, 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, - }); + 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 && postData.sensitive && attachments.length > 0) { - spoiler = true; - } + if (!spoiler && postData2.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, - })), - }; + 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, + })), + }; + } + } } } else { if (postData.id) { @@ -936,9 +859,8 @@ async function processUrl(msg, url, spoiler = false, command = false) { if (realUrlObj.origin != urlObj.origin) { platform = await resolvePlatform(postData.id); color = PLATFORM_COLORS[platform]; - platformName = normalizePlatform(platform); + platformName = platform.replace("gotosocial", "GoToSocial").replace(/^(.)/, (_, c) => c.toUpperCase()); url = postData.id; - urlObj = realUrlObj; } } @@ -954,137 +876,92 @@ 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(replyData.actor ?? replyData.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}`; - } + // NB: gts doesnt send singular attachments as array + 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 { - context += ""; - } - } + } else if (attachment.url) { + const contentType = await fetch(attachment.url, { + method: "HEAD", + }).then((res) => res.headers.get("Content-Type")); - if (postData.attachment != null) { - // NB: gts doesnt send singular attachments as array - 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/")) { + if (contentType) { + if (contentType.startsWith("image/")) { images.push({ url: attachment.url, desc: attachment.name ?? attachment.description ?? attachment.comment, - type: attachment.mediaType, + type: contentType, }); - } else if (attachment.mediaType.startsWith("audio/")) { + } else if (contentType.startsWith("video/")) { + videos.push({ + url: attachment.url, + desc: attachment.name ?? attachment.description ?? attachment.comment, + type: contentType, + }); + } else if (contentType.startsWith("audio/")) { audios.push({ url: attachment.url, desc: attachment.name ?? attachment.description ?? attachment.comment, - type: attachment.mediaType, + type: contentType, }); } - } else if (attachment.url) { - 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.name ?? attachment.description ?? attachment.comment, - type: contentType, - }); - } else if (contentType.startsWith("video/")) { - videos.push({ - url: attachment.url, - desc: attachment.name ?? attachment.description ?? attachment.comment, - type: contentType, - }); - } else if (contentType.startsWith("audio/")) { - audios.push({ - url: attachment.url, - 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: attachment.url, - desc: attachment.name ?? attachment.description ?? attachment.comment, - type: fileType, - }); - } else if (type.startsWith("video")) { - videos.push({ - url: attachment.url, - desc: attachment.name ?? attachment.description ?? attachment.comment, - type: fileType, - }); - } else if (type.startsWith("audio")) { - audios.push({ - url: attachment.url, - desc: attachment.name ?? attachment.description ?? attachment.comment, - type: fileType, - }); - } - } } else { - logger.warn("fedimbed", `Unhandled attachment structure! ${JSON.stringify(attachment)}`); - } - } + const type = attachment.type?.toLowerCase(); - if (!spoiler && postData.sensitive && attachments.length > 0) { - spoiler = true; + 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: attachment.url, + desc: attachment.name ?? attachment.description ?? attachment.comment, + type: fileType, + }); + } else if (type.startsWith("video")) { + videos.push({ + url: attachment.url, + desc: attachment.name ?? attachment.description ?? attachment.comment, + type: fileType, + }); + } else if (type.startsWith("audio")) { + audios.push({ + url: attachment.url, + desc: attachment.name ?? attachment.description ?? attachment.comment, + type: fileType, + }); + } + } + } else { + logger.warn("fedimbed", `Unhandled attachment structure! ${JSON.stringify(attachment)}`); } } + if (!spoiler && postData.sensitive && attachments.length > 0) { + spoiler = true; + } + if (postData.image?.url) { const imageUrl = new URL(postData.image.url); const contentType = await fetch(postData.image.url, { @@ -1108,7 +985,8 @@ 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}`); + // 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 (authorData) { @@ -1120,7 +998,7 @@ async function processUrl(msg, url, spoiler = false, command = false) { avatar: authorData.icon?.url, }; } else { - // bootleg author + // bootleg author, mainly for cohost const authorUrl = postData.actor ?? postData.attributedTo; const authorUrlObj = new URL(authorUrl); const name = authorUrlObj.pathname.substring(authorUrlObj.pathname.lastIndexOf("/") + 1); @@ -1199,12 +1077,6 @@ 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, @@ -1512,7 +1384,6 @@ events.add("messageCreate", "fedimbed", async function (msg) { logger.verbose("fedimbed", `Hit "${service}" for "${url}", processing now.`); try { const {response, sendWait} = await processUrl(msg, url, hasSpoiler); - if (!response) return; await msg.channel.createMessage(response).then(() => { if (sendWait) { msg.removeReaction("\uD83D\uDCE4"); @@ -1523,7 +1394,7 @@ events.add("messageCreate", "fedimbed", async function (msg) { } }); } catch (err) { - logger.error("fedimbed", `Error processing "${url}":\n${err.stack}`); + logger.error("fedimbed", `Error processing "${url}":\n` + err.stack); } break; } diff --git a/src/modules/foxwells.js b/src/modules/foxwells.js index 7907663..62ad126 100644 --- a/src/modules/foxwells.js +++ b/src/modules/foxwells.js @@ -263,33 +263,3 @@ async function processReaction(_msg, reaction, user) { events.add("messageReactionAdd", "vinboard", processReaction); events.add("messageReactionRemove", "vinboard", processReaction); - -/* join request logger */ -/*const STAFF_CHANNEL_ID = "662007759738372096"; - -function processJoinRequest(data) { - if (data.status !== "SUBMITTED") return; - if (!data.request) return; - if (data.request.guild_id !== FOXWELLS_GUILD_ID) return; - - const channel = hf.bot.guilds.get(FOXWELLS_GUILD_ID).channels.get(STAFF_CHANNEL_ID); - const userId = data.request.user_id; - - channel.createMessage({ - embeds: [ - { - title: "New join request", - description: `<@${userId}> (\`${userId}\`)`, - fields: data.request.form_responses - .filter((field) => field.field_type !== "TERMS") - .map((field) => ({name: field.label, value: `\`\`\`\n${field.response}\n\`\`\``})), - }, - ], - }); -} - -events.add("unknown", "foxwells_joinrequest", (packet) => { - if (packet.t === "GUILD_JOIN_REQUEST_UPDATE") { - processJoinRequest(packet.d); - } -});*/ diff --git a/src/modules/utility/guildinfo.js b/src/modules/utility/guildinfo.js index 0572e22..6c57b5d 100644 --- a/src/modules/utility/guildinfo.js +++ b/src/modules/utility/guildinfo.js @@ -601,7 +601,7 @@ guildinfo.callback = async function (msg, line, args, {nolocal, debug}) { invite = await hf.bot.requestHandler.request( "GET", `/invites/${guild.instant_invite.replace( - /(https?:\/\/)?(canary\.|ptb\.)?discord(\.gg|(app)?.com\/invite)\//, + /(https?:\/\/)?discord(\.gg|(app)?.com\/invite)\//, "" )}?with_counts=true&with_expiration=true` ); diff --git a/src/modules/utility/lookupinvite.js b/src/modules/utility/lookupinvite.js index d6a41d3..bdc5acb 100644 --- a/src/modules/utility/lookupinvite.js +++ b/src/modules/utility/lookupinvite.js @@ -23,7 +23,7 @@ lookupinvite.addAlias("ii"); lookupinvite.callback = async function (msg, line) { if (!line || line == "") return "Arguments required."; - line = line.replace(/(https?:\/\/)?(canary\.|ptb\.)?discord(\.gg|(app)?.com\/invite)\//, ""); + line = line.replace(/(https?:\/\/)?discord(\.gg|(app)?.com\/invite)\//, ""); if (decodeURIComponent(line).indexOf("../") > -1) return "nuh uh"; diff --git a/src/util/html.js b/src/util/html.js index be61121..85bb5ec 100644 --- a/src/util/html.js +++ b/src/util/html.js @@ -32,14 +32,14 @@ function parseHtmlEntities(str) { function htmlToMarkdown(str, images = true, embed = true) { str = str.replaceAll("\\", "\\\\"); - str = str.replace(/]+)?>(.|\n)*?<\/style>/gi, ""); - str = str.replace(/]+)?href="([^"]+?)"(\s*[^>]+)?>(.+?)<\/a>/gi, (_, __, url, ___, text) => { + str = str.replace(/]+)?>(.|\n)*?<\/style>/gi, ""); + str = str.replace(/]+)?href="([^"]+?)"(\s+[^>]+)?>(.+?)<\/a>/gi, (_, __, url, ___, text) => { url = url.replace(/^\/\//, "https://").replace("\\#", "#"); return url == text ? url : `[${text}](${embed ? "" : "<"}${url}${embed ? "" : ">"})`; }); if (images) str = str.replace( - /]+)?src="([^"]+?)"(\s*[^>]+)?(alt|title)="([^"]+?)"(\s*[^>]+)?\/>/gi, + /]+)?src="([^"]+?)"(\s+[^>]+)?(alt|title)="([^"]+?)"(\s+[^>]+)?\/>/gi, `[$5](${embed ? "" : "<"}$2${embed ? "" : ">"})` ); str = str.replace(/<\/?\s*br\s*\/?>/gi, "\n"); @@ -49,7 +49,7 @@ function htmlToMarkdown(str, images = true, embed = true) { ); str = str.replace(/<\/?p>/gi, "\n"); str = str.replace(/
((.|\n)*?)<\/dd>/gi, (_, inner) => "\u3000\u3000" + inner.split("\n").join("\n\u3000\u3000")); - str = str.replace(/]+)?>((.|\n)*?)<\/ol>/gi, (_, __, inner) => { + str = str.replace(/]+)?>((.|\n)*?)<\/ol>/gi, (_, __, inner) => { let index = 0; return inner .replace(/
  • /gi, () => { @@ -59,7 +59,7 @@ function htmlToMarkdown(str, images = true, embed = true) { .replace(/<\/li>/gi, "\n") .replaceAll("\n\n", "\n"); }); - str = str.replace(/]+)?>((.|\n)*?)<\/ul>/gi, (_, __, inner) => { + str = str.replace(/]+)?>((.|\n)*?)<\/ul>/gi, (_, __, inner) => { let index = 0; return inner .replace(/
  • /gi, () => { @@ -69,18 +69,17 @@ function htmlToMarkdown(str, images = true, embed = true) { .replace(/<\/li>/gi, "\n") .replaceAll("\n\n", "\n"); }); - str = str.replace(/<\/?span(\s*[^>]+)?>/gi, ""); - str = str.replace(/<\/?code(\s*[^>]+)?>/gi, "`"); - str = str.replace(/<\/?em(\s*[^>]+)?>/gi, "_"); - str = str.replace(/<\/?i(\s*[^>]+)?>/gi, "_"); - str = str.replace(/<\/?b(\s*[^>]+)?>/gi, "**"); - str = str.replace(/<\/?u(\s*[^>]+)?>/gi, "__"); - str = str.replace(/<\/?s(\s*[^>]+)?>/gi, "~~"); - str = str.replace(/]+)?>/gi, "# "); - str = str.replace(/]+)?>/gi, "## "); - str = str.replace(/]+)?>/gi, "### "); - str = str.replace(/<\/?h4(\s*[^>]+)?>/gi, "**"); - str = str.replace(/<(math|noscript)(\s*[^>]+)?>((.|\n)*?)<\/(math|noscript)>/gi, ""); + str = str.replace(/<\/?code(\s+[^>]+)?>/gi, "`"); + str = str.replace(/<\/?em(\s+[^>]+)?>/gi, "_"); + str = str.replace(/<\/?i(\s+[^>]+)?>/gi, "_"); + str = str.replace(/<\/?b(\s+[^>]+)?>/gi, "**"); + str = str.replace(/<\/?u(\s+[^>]+)?>/gi, "__"); + str = str.replace(/<\/?s(\s+[^>]+)?>/gi, "~~"); + str = str.replace(/]+)?>/gi, "# "); + str = str.replace(/]+)?>/gi, "## "); + str = str.replace(/]+)?>/gi, "### "); + str = str.replace(/<\/?h4(\s+[^>]+)?>/gi, "**"); + str = str.replace(/<(math|noscript)(\s+[^>]+)?>((.|\n)*?)<\/(math|noscript)>/gi, ""); str = str.replace(/<[^>]+?>/gi, ""); str = parseHtmlEntities(str); // whyyyyyyyyyyyy