fedimbed: bluesky support
This commit is contained in:
		
							parent
							
								
									e3991b5be7
								
							
						
					
					
						commit
						ef9d4b4d0e
					
				
					 1 changed files with 212 additions and 14 deletions
				
			
		| 
						 | 
					@ -41,8 +41,11 @@ const PLATFORM_COLORS = {
 | 
				
			||||||
  birdsitelive: 0x1da1f2,
 | 
					  birdsitelive: 0x1da1f2,
 | 
				
			||||||
  iceshrimp: 0x8e82f9, // YCbCr interpolated as the accent color is a gradient
 | 
					  iceshrimp: 0x8e82f9, // YCbCr interpolated as the accent color is a gradient
 | 
				
			||||||
  cohost: 0x83254f,
 | 
					  cohost: 0x83254f,
 | 
				
			||||||
 | 
					  bluesky: 0x0085ff,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const BSKY_DOMAINS = ["bsky.app", "bskye.app", "boobsky.app", "vxbsky.app", "cbsky.app", "fxbsky.app"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const domainCache = new Map();
 | 
					const domainCache = new Map();
 | 
				
			||||||
domainCache.set("cohost.org", "cohost"); // no nodeinfo
 | 
					domainCache.set("cohost.org", "cohost"); // no nodeinfo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -106,7 +109,7 @@ async function signedFetch(url, options) {
 | 
				
			||||||
      key: privKey,
 | 
					      key: privKey,
 | 
				
			||||||
      headers: headerNames,
 | 
					      headers: headerNames,
 | 
				
			||||||
      authorizationHeaderName: "signature",
 | 
					      authorizationHeaderName: "signature",
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  options.headers = Object.assign(headers, options.headers ?? {});
 | 
					  options.headers = Object.assign(headers, options.headers ?? {});
 | 
				
			||||||
| 
						 | 
					@ -114,6 +117,199 @@ async function signedFetch(url, options) {
 | 
				
			||||||
  return await fetch(url, options);
 | 
					  return await fetch(url, options);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const BSKY_POST_REGEX = /^\/profile\/([a-z0-9][a-z0-9.\-]+[a-z0-9]*)\/post\/([a-z0-9]+)\/?$/i;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function blueskyQuoteEmbed(quote, videos) {
 | 
				
			||||||
 | 
					  const embeds = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const mainEmbed = {
 | 
				
			||||||
 | 
					    color: PLATFORM_COLORS.bluesky,
 | 
				
			||||||
 | 
					    url: `https://bsky.app/profile/${quote.author.handle}/post/${quote.uri.substring(quote.uri.lastIndexOf("/"))}`,
 | 
				
			||||||
 | 
					    author: {name: "\u2198 Quoted Post"},
 | 
				
			||||||
 | 
					    title: `${quote.author.display_name} (@${quote.author.handle})`,
 | 
				
			||||||
 | 
					    thumbnail: {
 | 
				
			||||||
 | 
					      url: quote.author.avatar,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    description: quote.value.text,
 | 
				
			||||||
 | 
					    footer: {
 | 
				
			||||||
 | 
					      text: "Bluesky",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    timestamp: quote.value.createdAt,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (quote.embeds?.[0]) {
 | 
				
			||||||
 | 
					    const embed = embeds[0];
 | 
				
			||||||
 | 
					    switch (embed.$type) {
 | 
				
			||||||
 | 
					      case "app.bsky.embed.images#view": {
 | 
				
			||||||
 | 
					        embeds.push(...embed.images.map((image) => ({...mainEmbed, image: {url: image.fullsize}})));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      case "app.bsky.embed.video#view": {
 | 
				
			||||||
 | 
					        const videoUrl = `https://bsky.social/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(quote.author.did)}&cid=${embed.cid}`;
 | 
				
			||||||
 | 
					        const contentType = await fetch(videoUrl, {
 | 
				
			||||||
 | 
					          method: "HEAD",
 | 
				
			||||||
 | 
					        }).then((res) => res.headers.get("Content-Type"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        videos.push({url: videoUrl, desc: embed.alt, type: contentType});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        embeds.push({...mainEmbed, fields: [{name: "\u200b", value: `[Video Link](${videoUrl})`}]});
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      default: {
 | 
				
			||||||
 | 
					        embeds.push(mainEmbed);
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    embeds.push(mainEmbed);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return embeds;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function bluesky(msg, url, spoiler = false) {
 | 
				
			||||||
 | 
					  const urlObj = new URL(url);
 | 
				
			||||||
 | 
					  urlObj.hostname = "bsky.app";
 | 
				
			||||||
 | 
					  url = urlObj.toString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const postMatch = urlObj.pathname.match(BSKY_POST_REGEX);
 | 
				
			||||||
 | 
					  if (!postMatch) return {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [_, user, postId] = postMatch;
 | 
				
			||||||
 | 
					  const postUri = `at://${user}/app.bsky.feed.post/${postId}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const res = await fetch(
 | 
				
			||||||
 | 
					    `https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${postUri}&depth=0&parentHeight`,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      headers: {
 | 
				
			||||||
 | 
					        "User-Agent": FRIENDLY_USERAGENT,
 | 
				
			||||||
 | 
					        Accept: "application/json",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!res.ok) throw new Error(`Got non-OK status: ${res.status}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const data = await res.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!data?.thread || !("$type" in data.thread) || data.thread.$type !== "app.bsky.feed.defs#threadViewPost")
 | 
				
			||||||
 | 
					    throw new Error(`Did not get a valid Bluesky thread`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const {post} = data.thread;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const videos = [];
 | 
				
			||||||
 | 
					  const embeds = [];
 | 
				
			||||||
 | 
					  let sendWait = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const mainEmbed = {
 | 
				
			||||||
 | 
					    color: PLATFORM_COLORS.bluesky,
 | 
				
			||||||
 | 
					    url,
 | 
				
			||||||
 | 
					    title: `${post.author.display_name} (@${post.author.handle})`,
 | 
				
			||||||
 | 
					    description: post.record.text,
 | 
				
			||||||
 | 
					    thumbnail: {
 | 
				
			||||||
 | 
					      url: post.author.avatar,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    footer: {
 | 
				
			||||||
 | 
					      text: "Bluesky",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    timestamp: post.record.createdAt,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (post.embed) {
 | 
				
			||||||
 | 
					    switch (post.embed.$type) {
 | 
				
			||||||
 | 
					      case "app.bsky.embed.images#view": {
 | 
				
			||||||
 | 
					        embeds.push(...post.embed.images.map((image) => ({...mainEmbed, image: {url: image.fullsize}})));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      case "app.bsky.embed.video#view": {
 | 
				
			||||||
 | 
					        const videoUrl = `https://bsky.social/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(post.author.did)}&cid=${post.embed.cid}`;
 | 
				
			||||||
 | 
					        const contentType = await fetch(videoUrl, {
 | 
				
			||||||
 | 
					          method: "HEAD",
 | 
				
			||||||
 | 
					        }).then((res) => res.headers.get("Content-Type"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        videos.push({url: videoUrl, desc: post.embed.alt, type: contentType});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        embeds.push({...mainEmbed, fields: [{name: "\u200b", value: `[Video Link](${videoUrl})`}]});
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      case "app.bsky.embed.record#view": {
 | 
				
			||||||
 | 
					        const quote = post.embed.record;
 | 
				
			||||||
 | 
					        embeds.push(mainEmbed, ...(await blueskyQuoteEmbed(quote, videos)));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      case "app.bsky.embed.recordWithMedia#view": {
 | 
				
			||||||
 | 
					        if (post.embed.media.$type === "app.bsky.embed.images#view") {
 | 
				
			||||||
 | 
					          embeds.push(...post.embed.media.images.map((image) => ({...mainEmbed, image: {url: image.fullsize}})));
 | 
				
			||||||
 | 
					        } else if (post.embed.media.$type === "app.bsky.embed.video#view") {
 | 
				
			||||||
 | 
					          const videoUrl = `https://bsky.social/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(post.author.did)}&cid=${post.embed.media.cid}`;
 | 
				
			||||||
 | 
					          const contentType = await fetch(videoUrl, {
 | 
				
			||||||
 | 
					            method: "HEAD",
 | 
				
			||||||
 | 
					          }).then((res) => res.headers.get("Content-Type"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          videos.push({url: videoUrl, desc: post.embed.alt, type: contentType});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          embeds.push({...mainEmbed, fields: [{name: "\u200b", value: `[Video Link](${videoUrl})`}]});
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        embeds.push(...(await blueskyQuoteEmbed(post.embed.record.record, videos)));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      default: {
 | 
				
			||||||
 | 
					        embeds.push(mainEmbed);
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    embeds.push(mainEmbed);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (videos.length > 0) {
 | 
				
			||||||
 | 
					    sendWait = true;
 | 
				
			||||||
 | 
					    if (msg instanceof Message) await msg.addReaction("\uD83D\uDCE4");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const guild = msg.channel?.guild ?? (msg.guildID ? hf.bot.guilds.get(msg.guildID) : false);
 | 
				
			||||||
 | 
					  const files = [];
 | 
				
			||||||
 | 
					  if (videos.length > 0) {
 | 
				
			||||||
 | 
					    for (const attachment of videos) {
 | 
				
			||||||
 | 
					      const size = await fetch(attachment.url, {
 | 
				
			||||||
 | 
					        method: "HEAD",
 | 
				
			||||||
 | 
					        headers: {
 | 
				
			||||||
 | 
					          "User-Agent": FRIENDLY_USERAGENT,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      }).then((res) => Number(res.headers.get("Content-Length")));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (size <= getUploadLimit(guild)) {
 | 
				
			||||||
 | 
					        const file = await fetch(attachment.url, {
 | 
				
			||||||
 | 
					          headers: {
 | 
				
			||||||
 | 
					            "User-Agent": FRIENDLY_USERAGENT,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					          .then((res) => res.arrayBuffer())
 | 
				
			||||||
 | 
					          .then((buf) => Buffer.from(buf));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        files.push({
 | 
				
			||||||
 | 
					          filename:
 | 
				
			||||||
 | 
					            (spoiler ? "SPOILER_" : "") +
 | 
				
			||||||
 | 
					            (attachment.type.indexOf("/") > -1
 | 
				
			||||||
 | 
					              ? attachment.type.replace("/", ".").replace("quicktime", "mov")
 | 
				
			||||||
 | 
					              : attachment.type + "." + (url.match(/\.([a-z0-9]{3,4})$/)?.[0] ?? "mp4")),
 | 
				
			||||||
 | 
					          file,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    response: {
 | 
				
			||||||
 | 
					      content: spoiler ? `|| ${url} ||` : "",
 | 
				
			||||||
 | 
					      embeds,
 | 
				
			||||||
 | 
					      attachments: files,
 | 
				
			||||||
 | 
					      allowedMentions: {
 | 
				
			||||||
 | 
					        repliedUser: false,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      messageReference: {
 | 
				
			||||||
 | 
					        messageID: msg.id,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    sendWait,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function processUrl(msg, url, spoiler = false) {
 | 
					async function processUrl(msg, url, spoiler = false) {
 | 
				
			||||||
  let invalidUrl = false;
 | 
					  let invalidUrl = false;
 | 
				
			||||||
  let urlObj;
 | 
					  let urlObj;
 | 
				
			||||||
| 
						 | 
					@ -125,6 +321,8 @@ async function processUrl(msg, url, spoiler = false) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (invalidUrl) return {};
 | 
					  if (invalidUrl) return {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (BSKY_DOMAINS.includes(urlObj.hostname)) return await bluesky(msg, url, spoiler);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // some lemmy instances have old reddit frontend subdomains
 | 
					  // some lemmy instances have old reddit frontend subdomains
 | 
				
			||||||
  // but these frontends are just frontends and dont actually expose the API
 | 
					  // but these frontends are just frontends and dont actually expose the API
 | 
				
			||||||
  if (urlObj.hostname.startsWith("old.")) {
 | 
					  if (urlObj.hostname.startsWith("old.")) {
 | 
				
			||||||
| 
						 | 
					@ -242,7 +440,7 @@ async function processUrl(msg, url, spoiler = false) {
 | 
				
			||||||
    if (redirUrl) {
 | 
					    if (redirUrl) {
 | 
				
			||||||
      logger.verbose(
 | 
					      logger.verbose(
 | 
				
			||||||
        "fedimbed",
 | 
					        "fedimbed",
 | 
				
			||||||
        `Redirecting "${url}" to "${redirUrl}": ${JSON.stringify(options)}, ${JSON.stringify(headers)}`
 | 
					        `Redirecting "${url}" to "${redirUrl}": ${JSON.stringify(options)}, ${JSON.stringify(headers)}`,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      let rawPostData2;
 | 
					      let rawPostData2;
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
| 
						 | 
					@ -252,7 +450,7 @@ async function processUrl(msg, url, spoiler = false) {
 | 
				
			||||||
            headers: Object.assign(headers, {
 | 
					            headers: Object.assign(headers, {
 | 
				
			||||||
              "User-Agent": FRIENDLY_USERAGENT,
 | 
					              "User-Agent": FRIENDLY_USERAGENT,
 | 
				
			||||||
            }),
 | 
					            }),
 | 
				
			||||||
          })
 | 
					          }),
 | 
				
			||||||
        ).then((res) => res.text());
 | 
					        ).then((res) => res.text());
 | 
				
			||||||
      } catch (err) {
 | 
					      } catch (err) {
 | 
				
			||||||
        logger.error("fedimbed", `Failed to signed fetch "${url}" via MastoAPI, retrying unsigned: ${err}`);
 | 
					        logger.error("fedimbed", `Failed to signed fetch "${url}" via MastoAPI, retrying unsigned: ${err}`);
 | 
				
			||||||
| 
						 | 
					@ -265,7 +463,7 @@ async function processUrl(msg, url, spoiler = false) {
 | 
				
			||||||
              headers: Object.assign(headers, {
 | 
					              headers: Object.assign(headers, {
 | 
				
			||||||
                "User-Agent": FRIENDLY_USERAGENT,
 | 
					                "User-Agent": FRIENDLY_USERAGENT,
 | 
				
			||||||
              }),
 | 
					              }),
 | 
				
			||||||
            })
 | 
					            }),
 | 
				
			||||||
          ).then((res) => res.text());
 | 
					          ).then((res) => res.text());
 | 
				
			||||||
        } catch (err) {
 | 
					        } catch (err) {
 | 
				
			||||||
          logger.error("fedimbed", `Failed to fetch "${url}" via MastoAPI: ${err}`);
 | 
					          logger.error("fedimbed", `Failed to fetch "${url}" via MastoAPI: ${err}`);
 | 
				
			||||||
| 
						 | 
					@ -284,7 +482,7 @@ async function processUrl(msg, url, spoiler = false) {
 | 
				
			||||||
      } else if (postData2.error) {
 | 
					      } else if (postData2.error) {
 | 
				
			||||||
        logger.error(
 | 
					        logger.error(
 | 
				
			||||||
          "fedimbed",
 | 
					          "fedimbed",
 | 
				
			||||||
          `Bailing trying to re-embed "${url}", MastoAPI gave us error: ${JSON.stringify(postData2.error)}`
 | 
					          `Bailing trying to re-embed "${url}", MastoAPI gave us error: ${JSON.stringify(postData2.error)}`,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        cw = postData2.spoiler_warning ?? postData2.spoiler_text ?? postData2.cw;
 | 
					        cw = postData2.spoiler_warning ?? postData2.spoiler_text ?? postData2.cw;
 | 
				
			||||||
| 
						 | 
					@ -339,15 +537,15 @@ async function processUrl(msg, url, spoiler = false) {
 | 
				
			||||||
              const type = attachment.type?.toLowerCase();
 | 
					              const type = attachment.type?.toLowerCase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              const fileType =
 | 
					              const fileType =
 | 
				
			||||||
                attachment.pleroma?.mime_type ?? type.indexOf("/") > -1
 | 
					                (attachment.pleroma?.mime_type ?? type.indexOf("/") > -1)
 | 
				
			||||||
                  ? type
 | 
					                  ? type
 | 
				
			||||||
                  : type +
 | 
					                  : type +
 | 
				
			||||||
                    "/" +
 | 
					                    "/" +
 | 
				
			||||||
                    (url.match(/\.([a-z0-9]{3,4})$/)?.[0] ?? type == "image"
 | 
					                    ((url.match(/\.([a-z0-9]{3,4})$/)?.[0] ?? type == "image")
 | 
				
			||||||
                      ? "png"
 | 
					                      ? "png"
 | 
				
			||||||
                      : type == "video"
 | 
					                      : type == "video"
 | 
				
			||||||
                      ? "mp4"
 | 
					                        ? "mp4"
 | 
				
			||||||
                      : "mpeg");
 | 
					                        : "mpeg");
 | 
				
			||||||
              if (type.startsWith("image")) {
 | 
					              if (type.startsWith("image")) {
 | 
				
			||||||
                images.push({
 | 
					                images.push({
 | 
				
			||||||
                  url: attachment.url,
 | 
					                  url: attachment.url,
 | 
				
			||||||
| 
						 | 
					@ -465,7 +663,7 @@ async function processUrl(msg, url, spoiler = false) {
 | 
				
			||||||
              ? type
 | 
					              ? type
 | 
				
			||||||
              : type +
 | 
					              : type +
 | 
				
			||||||
                "/" +
 | 
					                "/" +
 | 
				
			||||||
                (url.match(/\.([a-z0-9]{3,4})$/)?.[0] ?? type == "image" ? "png" : type == "video" ? "mp4" : "mpeg");
 | 
					                ((url.match(/\.([a-z0-9]{3,4})$/)?.[0] ?? type == "image") ? "png" : type == "video" ? "mp4" : "mpeg");
 | 
				
			||||||
          if (type.startsWith("image")) {
 | 
					          if (type.startsWith("image")) {
 | 
				
			||||||
            images.push({
 | 
					            images.push({
 | 
				
			||||||
              url: attachment.url,
 | 
					              url: attachment.url,
 | 
				
			||||||
| 
						 | 
					@ -678,7 +876,7 @@ async function processUrl(msg, url, spoiler = false) {
 | 
				
			||||||
            const bar = Math.round(percent * 30);
 | 
					            const bar = Math.round(percent * 30);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return `**${o.name}** (${o.count}, ${Math.round(percent * 100)}%)\n\`[${"=".repeat(bar)}${" ".repeat(
 | 
					            return `**${o.name}** (${o.count}, ${Math.round(percent * 100)}%)\n\`[${"=".repeat(bar)}${" ".repeat(
 | 
				
			||||||
              30 - bar
 | 
					              30 - bar,
 | 
				
			||||||
            )}]\``;
 | 
					            )}]\``;
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
          .join("\n\n") + `\n\n${poll.total} votes \u2022 Ends <t:${Math.floor(poll.end.getTime() / 1000)}:R>`,
 | 
					          .join("\n\n") + `\n\n${poll.total} votes \u2022 Ends <t:${Math.floor(poll.end.getTime() / 1000)}:R>`,
 | 
				
			||||||
| 
						 | 
					@ -873,8 +1071,8 @@ async function processUrl(msg, url, spoiler = false) {
 | 
				
			||||||
        cw != "" && (images.length > 0 || videos.length > 0 || audios.length > 0)
 | 
					        cw != "" && (images.length > 0 || videos.length > 0 || audios.length > 0)
 | 
				
			||||||
          ? `:warning: ${cw} || ${url} ||`
 | 
					          ? `:warning: ${cw} || ${url} ||`
 | 
				
			||||||
          : spoiler
 | 
					          : spoiler
 | 
				
			||||||
          ? `|| ${url} ||`
 | 
					            ? `|| ${url} ||`
 | 
				
			||||||
          : "",
 | 
					            : "",
 | 
				
			||||||
      embeds,
 | 
					      embeds,
 | 
				
			||||||
      attachments: files,
 | 
					      attachments: files,
 | 
				
			||||||
      allowedMentions: {
 | 
					      allowedMentions: {
 | 
				
			||||||
| 
						 | 
					@ -937,7 +1135,7 @@ events.add("messageCreate", "fedimbed", async function (msg) {
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const fedimbedCommand = new InteractionCommand("fedimbed");
 | 
					const fedimbedCommand = new InteractionCommand("fedimbed");
 | 
				
			||||||
fedimbedCommand.helpText = "Better embeds for fediverse (Mastodon, Pleroma, etc) posts";
 | 
					fedimbedCommand.helpText = "Better embeds for fediverse (Mastodon, Pleroma, etc) and Bluesky posts";
 | 
				
			||||||
fedimbedCommand.options.url = {
 | 
					fedimbedCommand.options.url = {
 | 
				
			||||||
  name: "url",
 | 
					  name: "url",
 | 
				
			||||||
  type: ApplicationCommandOptionTypes.STRING,
 | 
					  type: ApplicationCommandOptionTypes.STRING,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue