diff --git a/src/api/util/utility/Base64.ts b/src/api/util/utility/Base64.ts
index 7426d22b..78a56f63 100644
--- a/src/api/util/utility/Base64.ts
+++ b/src/api/util/utility/Base64.ts
@@ -25,41 +25,41 @@ const b2s = alphabet.split("");
// 123 == 'z'.charCodeAt(0) + 1
const s2b = new Array(123);
for (let i = 0; i < alphabet.length; i++) {
- s2b[alphabet.charCodeAt(i)] = i;
+ s2b[alphabet.charCodeAt(i)] = i;
}
// number to base64
export const ntob = (n: number): string => {
- if (n < 0) return `-${ntob(-n)}`;
+ if (n < 0) return `-${ntob(-n)}`;
- let lo = n >>> 0;
- let hi = (n / 4294967296) >>> 0;
+ let lo = n >>> 0;
+ let hi = (n / 4294967296) >>> 0;
- let right = "";
- while (hi > 0) {
- right = b2s[0x3f & lo] + right;
- lo >>>= 6;
- lo |= (0x3f & hi) << 26;
- hi >>>= 6;
- }
+ let right = "";
+ while (hi > 0) {
+ right = b2s[0x3f & lo] + right;
+ lo >>>= 6;
+ lo |= (0x3f & hi) << 26;
+ hi >>>= 6;
+ }
- let left = "";
- do {
- left = b2s[0x3f & lo] + left;
- lo >>>= 6;
- } while (lo > 0);
+ let left = "";
+ do {
+ left = b2s[0x3f & lo] + left;
+ lo >>>= 6;
+ } while (lo > 0);
- return left + right;
+ return left + right;
};
// base64 to number
export const bton = (base64: string) => {
- let number = 0;
- const sign = base64.charAt(0) === "-" ? 1 : 0;
+ let number = 0;
+ const sign = base64.charAt(0) === "-" ? 1 : 0;
- for (let i = sign; i < base64.length; i++) {
- number = number * 64 + s2b[base64.charCodeAt(i)];
- }
+ for (let i = sign; i < base64.length; i++) {
+ number = number * 64 + s2b[base64.charCodeAt(i)];
+ }
- return sign ? -number : number;
+ return sign ? -number : number;
};
diff --git a/src/api/util/utility/EmbedHandlers.ts b/src/api/util/utility/EmbedHandlers.ts
index 8bbd6283..2e50c536 100644
--- a/src/api/util/utility/EmbedHandlers.ts
+++ b/src/api/util/utility/EmbedHandlers.ts
@@ -24,494 +24,494 @@ import { yellow } from "picocolors";
import probe from "probe-image-size";
export const DEFAULT_FETCH_OPTIONS: RequestInit = {
- redirect: "follow",
- headers: {
- "user-agent": "Mozilla/5.0 (compatible; Spacebar/1.0; +https://github.com/spacebarchat/server)",
- },
- // size: 1024 * 1024 * 5, // grabbed from config later
- method: "GET",
+ redirect: "follow",
+ headers: {
+ "user-agent": "Mozilla/5.0 (compatible; Spacebar/1.0; +https://github.com/spacebarchat/server)",
+ },
+ // size: 1024 * 1024 * 5, // grabbed from config later
+ method: "GET",
};
const makeEmbedImage = (url: string | undefined, width: number | undefined, height: number | undefined): Required<EmbedImage> | undefined => {
- if (!url || !width || !height) return undefined;
- return {
- url,
- width,
- height,
- proxy_url: getProxyUrl(new URL(url), width, height),
- };
+ if (!url || !width || !height) return undefined;
+ return {
+ url,
+ width,
+ height,
+ proxy_url: getProxyUrl(new URL(url), width, height),
+ };
};
let hasWarnedAboutImagor = false;
export const getProxyUrl = (url: URL, width: number, height: number): string => {
- const { resizeWidthMax, resizeHeightMax, imagorServerUrl } = Config.get().cdn;
- const secret = Config.get().security.requestSignature;
- width = Math.min(width || 500, resizeWidthMax || width);
- height = Math.min(height || 500, resizeHeightMax || width);
+ const { resizeWidthMax, resizeHeightMax, imagorServerUrl } = Config.get().cdn;
+ const secret = Config.get().security.requestSignature;
+ width = Math.min(width || 500, resizeWidthMax || width);
+ height = Math.min(height || 500, resizeHeightMax || width);
- // Imagor
- if (imagorServerUrl) {
- const path = `${width}x${height}/${url.host}${url.pathname}`;
+ // Imagor
+ if (imagorServerUrl) {
+ const path = `${width}x${height}/${url.host}${url.pathname}`;
- const hash = crypto.createHmac("sha1", secret).update(path).digest("base64").replace(/\+/g, "-").replace(/\//g, "_");
+ const hash = crypto.createHmac("sha1", secret).update(path).digest("base64").replace(/\+/g, "-").replace(/\//g, "_");
- return `${imagorServerUrl}/${hash}/${path}`;
- }
+ return `${imagorServerUrl}/${hash}/${path}`;
+ }
- if (!hasWarnedAboutImagor) {
- hasWarnedAboutImagor = true;
- console.log("[Embeds]", yellow("Imagor has not been set up correctly. https://docs.spacebar.chat/setup/server/configuration/imagor/"));
- }
+ if (!hasWarnedAboutImagor) {
+ hasWarnedAboutImagor = true;
+ console.log("[Embeds]", yellow("Imagor has not been set up correctly. https://docs.spacebar.chat/setup/server/configuration/imagor/"));
+ }
- return url.toString();
+ return url.toString();
};
const getMeta = ($: cheerio.CheerioAPI, name: string): string | undefined => {
- let elem = $(`meta[property="${name}"]`);
- if (!elem.length) elem = $(`meta[name="${name}"]`);
- const ret = elem.attr("content") || elem.text();
- return ret.trim().length == 0 ? undefined : ret;
+ let elem = $(`meta[property="${name}"]`);
+ if (!elem.length) elem = $(`meta[name="${name}"]`);
+ const ret = elem.attr("content") || elem.text();
+ return ret.trim().length == 0 ? undefined : ret;
};
const tryParseInt = (str: string | undefined) => {
- if (!str) return undefined;
- try {
- return parseInt(str);
- } catch (e) {
- return undefined;
- }
+ if (!str) return undefined;
+ try {
+ return parseInt(str);
+ } catch (e) {
+ return undefined;
+ }
};
export const getMetaDescriptions = (text: string) => {
- const $ = cheerio.load(text);
+ const $ = cheerio.load(text);
- return {
- type: getMeta($, "og:type"),
- title: getMeta($, "og:title") || $("title").first().text(),
- provider_name: getMeta($, "og:site_name"),
- author: getMeta($, "article:author"),
- description: getMeta($, "og:description") || getMeta($, "description"),
- image: getMeta($, "og:image") || getMeta($, "twitter:image"),
- image_fallback: $(`image`).attr("src"),
- video_fallback: $(`video`).attr("src"),
- width: tryParseInt(getMeta($, "og:image:width")),
- height: tryParseInt(getMeta($, "og:image:height")),
- url: getMeta($, "og:url"),
- youtube_embed: getMeta($, "og:video:secure_url"),
- site_name: getMeta($, "og:site_name"),
+ return {
+ type: getMeta($, "og:type"),
+ title: getMeta($, "og:title") || $("title").first().text(),
+ provider_name: getMeta($, "og:site_name"),
+ author: getMeta($, "article:author"),
+ description: getMeta($, "og:description") || getMeta($, "description"),
+ image: getMeta($, "og:image") || getMeta($, "twitter:image"),
+ image_fallback: $(`image`).attr("src"),
+ video_fallback: $(`video`).attr("src"),
+ width: tryParseInt(getMeta($, "og:image:width")),
+ height: tryParseInt(getMeta($, "og:image:height")),
+ url: getMeta($, "og:url"),
+ youtube_embed: getMeta($, "og:video:secure_url"),
+ site_name: getMeta($, "og:site_name"),
- $,
- };
+ $,
+ };
};
const doFetch = async (url: URL) => {
- try {
- const res = await fetch(url, {
- ...DEFAULT_FETCH_OPTIONS,
- });
- if (res.headers.get("content-length")) {
- const contentLength = parseInt(res.headers.get("content-length")!);
- if (Config.get().limits.message.maxEmbedDownloadSize && contentLength > Config.get().limits.message.maxEmbedDownloadSize) {
- return null;
- }
- }
- return res;
- } catch (e) {
- return null;
- }
+ try {
+ const res = await fetch(url, {
+ ...DEFAULT_FETCH_OPTIONS,
+ });
+ if (res.headers.get("content-length")) {
+ const contentLength = parseInt(res.headers.get("content-length")!);
+ if (Config.get().limits.message.maxEmbedDownloadSize && contentLength > Config.get().limits.message.maxEmbedDownloadSize) {
+ return null;
+ }
+ }
+ return res;
+ } catch (e) {
+ return null;
+ }
};
const genericImageHandler = async (url: URL): Promise<Embed | null> => {
- const type = await fetch(url, {
- ...DEFAULT_FETCH_OPTIONS,
- method: "HEAD",
- });
+ const type = await fetch(url, {
+ ...DEFAULT_FETCH_OPTIONS,
+ method: "HEAD",
+ });
- let image;
+ let image;
- if (type.headers.get("content-type")?.indexOf("image") !== -1) {
- const result = await probe(url.href);
- image = makeEmbedImage(url.href, result.width, result.height);
- } else if (type.headers.get("content-type")?.indexOf("video") !== -1) {
- // TODO
- return null;
- } else {
- // have to download the page, unfortunately
- const response = await doFetch(url);
- if (!response) return null;
- const metas = getMetaDescriptions(await response.text());
- image = makeEmbedImage(metas.image || metas.image_fallback, metas.width, metas.height);
- }
+ if (type.headers.get("content-type")?.indexOf("image") !== -1) {
+ const result = await probe(url.href);
+ image = makeEmbedImage(url.href, result.width, result.height);
+ } else if (type.headers.get("content-type")?.indexOf("video") !== -1) {
+ // TODO
+ return null;
+ } else {
+ // have to download the page, unfortunately
+ const response = await doFetch(url);
+ if (!response) return null;
+ const metas = getMetaDescriptions(await response.text());
+ image = makeEmbedImage(metas.image || metas.image_fallback, metas.width, metas.height);
+ }
- if (!image) return null;
+ if (!image) return null;
- return {
- url: url.href,
- type: EmbedType.image,
- thumbnail: image,
- };
+ return {
+ url: url.href,
+ type: EmbedType.image,
+ thumbnail: image,
+ };
};
export const EmbedHandlers: {
- [key: string]: (url: URL) => Promise<Embed | Embed[] | null>;
+ [key: string]: (url: URL) => Promise<Embed | Embed[] | null>;
} = {
- // the url does not have a special handler
- default: async (url: URL) => {
- const type = await fetch(url, {
- ...DEFAULT_FETCH_OPTIONS,
- method: "HEAD",
- });
- if (type.headers.get("content-type")?.indexOf("image") !== -1) return await genericImageHandler(url);
+ // the url does not have a special handler
+ default: async (url: URL) => {
+ const type = await fetch(url, {
+ ...DEFAULT_FETCH_OPTIONS,
+ method: "HEAD",
+ });
+ if (type.headers.get("content-type")?.indexOf("image") !== -1) return await genericImageHandler(url);
- const response = await doFetch(url);
- if (!response) return null;
+ const response = await doFetch(url);
+ if (!response) return null;
- const text = await response.text();
- const metas = getMetaDescriptions(text);
+ const text = await response.text();
+ const metas = getMetaDescriptions(text);
- // TODO: handle video
+ // TODO: handle video
- if (!metas.image) metas.image = metas.image_fallback;
+ if (!metas.image) metas.image = metas.image_fallback;
- if (metas.image && (!metas.width || !metas.height)) {
- metas.image = new URL(metas.image, url).toString();
- const result = await probe(metas.image);
- metas.width = result.width;
- metas.height = result.height;
- }
+ if (metas.image && (!metas.width || !metas.height)) {
+ metas.image = new URL(metas.image, url).toString();
+ const result = await probe(metas.image);
+ metas.width = result.width;
+ metas.height = result.height;
+ }
- if (!metas.image && (!metas.title || !metas.description)) {
- // we don't have any content to display
- return null;
- }
+ if (!metas.image && (!metas.title || !metas.description)) {
+ // we don't have any content to display
+ return null;
+ }
- let embedType = EmbedType.link;
- if (metas.type == "article") embedType = EmbedType.article;
- if (metas.type == "object") embedType = EmbedType.article; // github
- if (metas.type == "rich") embedType = EmbedType.rich;
+ let embedType = EmbedType.link;
+ if (metas.type == "article") embedType = EmbedType.article;
+ if (metas.type == "object") embedType = EmbedType.article; // github
+ if (metas.type == "rich") embedType = EmbedType.rich;
- return {
- url: url.href,
- type: embedType,
- title: metas.title,
- thumbnail: makeEmbedImage(metas.image, metas.width, metas.height),
- description: metas.description,
- provider: metas.site_name
- ? {
- name: metas.site_name,
- url: url.origin,
- }
- : undefined,
- };
- },
+ return {
+ url: url.href,
+ type: embedType,
+ title: metas.title,
+ thumbnail: makeEmbedImage(metas.image, metas.width, metas.height),
+ description: metas.description,
+ provider: metas.site_name
+ ? {
+ name: metas.site_name,
+ url: url.origin,
+ }
+ : undefined,
+ };
+ },
- "giphy.com": genericImageHandler,
- "media4.giphy.com": genericImageHandler,
- "tenor.com": genericImageHandler,
- "c.tenor.com": genericImageHandler,
- "media.tenor.com": genericImageHandler,
+ "giphy.com": genericImageHandler,
+ "media4.giphy.com": genericImageHandler,
+ "tenor.com": genericImageHandler,
+ "c.tenor.com": genericImageHandler,
+ "media.tenor.com": genericImageHandler,
- "facebook.com": (url) => EmbedHandlers["www.facebook.com"](url),
- "www.facebook.com": async (url: URL) => {
- const response = await doFetch(url);
- if (!response) return null;
- const metas = getMetaDescriptions(await response.text());
+ "facebook.com": (url) => EmbedHandlers["www.facebook.com"](url),
+ "www.facebook.com": async (url: URL) => {
+ const response = await doFetch(url);
+ if (!response) return null;
+ const metas = getMetaDescriptions(await response.text());
- return {
- url: url.href,
- type: EmbedType.link,
- title: metas.title,
- description: metas.description,
- thumbnail: makeEmbedImage(metas.image, 640, 640),
- color: 16777215,
- };
- },
+ return {
+ url: url.href,
+ type: EmbedType.link,
+ title: metas.title,
+ description: metas.description,
+ thumbnail: makeEmbedImage(metas.image, 640, 640),
+ color: 16777215,
+ };
+ },
- "twitter.com": (url) => EmbedHandlers["www.twitter.com"](url),
- "www.twitter.com": async (url: URL) => {
- const token = Config.get().external.twitter;
- if (!token) return null;
+ "twitter.com": (url) => EmbedHandlers["www.twitter.com"](url),
+ "www.twitter.com": async (url: URL) => {
+ const token = Config.get().external.twitter;
+ if (!token) return null;
- if (!url.href.includes("/status/")) return null; // TODO;
- const id = url.pathname.split("/")[3]; // super bad lol
- if (!parseInt(id)) return null;
- const endpointUrl =
- `https://api.twitter.com/2/tweets/${id}` +
- `?expansions=author_id,attachments.media_keys` +
- `&media.fields=url,width,height` +
- `&tweet.fields=created_at,public_metrics` +
- `&user.fields=profile_image_url`;
+ if (!url.href.includes("/status/")) return null; // TODO;
+ const id = url.pathname.split("/")[3]; // super bad lol
+ if (!parseInt(id)) return null;
+ const endpointUrl =
+ `https://api.twitter.com/2/tweets/${id}` +
+ `?expansions=author_id,attachments.media_keys` +
+ `&media.fields=url,width,height` +
+ `&tweet.fields=created_at,public_metrics` +
+ `&user.fields=profile_image_url`;
- const response = await fetch(endpointUrl, {
- ...DEFAULT_FETCH_OPTIONS,
- headers: {
- authorization: `Bearer ${token}`,
- },
- });
- const json = (await response.json()) as {
- errors?: never[];
- includes: {
- users: {
- profile_image_url: string;
- username: string;
- name: string;
- }[];
- media: {
- type: string;
- width: number;
- height: number;
- url: string;
- }[];
- };
- data: {
- text: string;
- created_at: string;
- public_metrics: { like_count: number; retweet_count: number };
- };
- };
- if (json.errors) return null;
- const author = json.includes.users[0];
- const text = json.data.text;
- const created_at = new Date(json.data.created_at);
- const metrics = json.data.public_metrics;
- const media = json.includes.media?.filter((x: { type: string }) => x.type == "photo");
+ const response = await fetch(endpointUrl, {
+ ...DEFAULT_FETCH_OPTIONS,
+ headers: {
+ authorization: `Bearer ${token}`,
+ },
+ });
+ const json = (await response.json()) as {
+ errors?: never[];
+ includes: {
+ users: {
+ profile_image_url: string;
+ username: string;
+ name: string;
+ }[];
+ media: {
+ type: string;
+ width: number;
+ height: number;
+ url: string;
+ }[];
+ };
+ data: {
+ text: string;
+ created_at: string;
+ public_metrics: { like_count: number; retweet_count: number };
+ };
+ };
+ if (json.errors) return null;
+ const author = json.includes.users[0];
+ const text = json.data.text;
+ const created_at = new Date(json.data.created_at);
+ const metrics = json.data.public_metrics;
+ const media = json.includes.media?.filter((x: { type: string }) => x.type == "photo");
- const embed: Embed = {
- type: EmbedType.rich,
- url: `${url.origin}${url.pathname}`,
- description: text,
- author: {
- url: `https://twitter.com/${author.username}`,
- name: `${author.name} (@${author.username})`,
- proxy_icon_url: getProxyUrl(new URL(author.profile_image_url), 400, 400),
- icon_url: author.profile_image_url,
- },
- timestamp: created_at,
- fields: [
- {
- inline: true,
- name: "Likes",
- value: metrics.like_count.toString(),
- },
- {
- inline: true,
- name: "Retweet",
- value: metrics.retweet_count.toString(),
- },
- ],
- color: 1942002,
- footer: {
- text: "Twitter",
- proxy_icon_url: getProxyUrl(new URL("https://abs.twimg.com/icons/apple-touch-icon-192x192.png"), 192, 192),
- icon_url: "https://abs.twimg.com/icons/apple-touch-icon-192x192.png",
- },
- // Discord doesn't send this?
- // provider: {
- // name: "Twitter",
- // url: "https://twitter.com"
- // },
- };
+ const embed: Embed = {
+ type: EmbedType.rich,
+ url: `${url.origin}${url.pathname}`,
+ description: text,
+ author: {
+ url: `https://twitter.com/${author.username}`,
+ name: `${author.name} (@${author.username})`,
+ proxy_icon_url: getProxyUrl(new URL(author.profile_image_url), 400, 400),
+ icon_url: author.profile_image_url,
+ },
+ timestamp: created_at,
+ fields: [
+ {
+ inline: true,
+ name: "Likes",
+ value: metrics.like_count.toString(),
+ },
+ {
+ inline: true,
+ name: "Retweet",
+ value: metrics.retweet_count.toString(),
+ },
+ ],
+ color: 1942002,
+ footer: {
+ text: "Twitter",
+ proxy_icon_url: getProxyUrl(new URL("https://abs.twimg.com/icons/apple-touch-icon-192x192.png"), 192, 192),
+ icon_url: "https://abs.twimg.com/icons/apple-touch-icon-192x192.png",
+ },
+ // Discord doesn't send this?
+ // provider: {
+ // name: "Twitter",
+ // url: "https://twitter.com"
+ // },
+ };
- if (media && media.length > 0) {
- embed.image = {
- width: media[0].width,
- height: media[0].height,
- url: media[0].url,
- proxy_url: getProxyUrl(new URL(media[0].url), media[0].width, media[0].height),
- };
- media.shift();
- }
+ if (media && media.length > 0) {
+ embed.image = {
+ width: media[0].width,
+ height: media[0].height,
+ url: media[0].url,
+ proxy_url: getProxyUrl(new URL(media[0].url), media[0].width, media[0].height),
+ };
+ media.shift();
+ }
- return embed;
+ return embed;
- // TODO: Client won't merge these into a single embed, for some reason.
- // return [embed, ...media.map((x: any) => ({
- // // generate new embeds for each additional attachment
- // type: EmbedType.rich,
- // url: url.href,
- // image: {
- // width: x.width,
- // height: x.height,
- // url: x.url,
- // proxy_url: getProxyUrl(new URL(x.url), x.width, x.height)
- // }
- // }))];
- },
+ // TODO: Client won't merge these into a single embed, for some reason.
+ // return [embed, ...media.map((x: any) => ({
+ // // generate new embeds for each additional attachment
+ // type: EmbedType.rich,
+ // url: url.href,
+ // image: {
+ // width: x.width,
+ // height: x.height,
+ // url: x.url,
+ // proxy_url: getProxyUrl(new URL(x.url), x.width, x.height)
+ // }
+ // }))];
+ },
- "open.spotify.com": async (url: URL) => {
- const response = await doFetch(url);
- if (!response) return null;
- const metas = getMetaDescriptions(await response.text());
+ "open.spotify.com": async (url: URL) => {
+ const response = await doFetch(url);
+ if (!response) return null;
+ const metas = getMetaDescriptions(await response.text());
- return {
- url: url.href,
- type: EmbedType.link,
- title: metas.title,
- description: metas.description,
- thumbnail: makeEmbedImage(metas.image, 640, 640),
- provider: {
- url: "https://spotify.com",
- name: "Spotify",
- },
- };
- },
+ return {
+ url: url.href,
+ type: EmbedType.link,
+ title: metas.title,
+ description: metas.description,
+ thumbnail: makeEmbedImage(metas.image, 640, 640),
+ provider: {
+ url: "https://spotify.com",
+ name: "Spotify",
+ },
+ };
+ },
- // TODO: docs: Pixiv won't work without Imagor
- "pixiv.net": (url) => EmbedHandlers["www.pixiv.net"](url),
- "www.pixiv.net": async (url: URL) => {
- const response = await doFetch(url);
- if (!response) return null;
- const metas = getMetaDescriptions(await response.text());
+ // TODO: docs: Pixiv won't work without Imagor
+ "pixiv.net": (url) => EmbedHandlers["www.pixiv.net"](url),
+ "www.pixiv.net": async (url: URL) => {
+ const response = await doFetch(url);
+ if (!response) return null;
+ const metas = getMetaDescriptions(await response.text());
- if (!metas.image) return null;
+ if (!metas.image) return null;
- return {
- url: url.href,
- type: EmbedType.image,
- title: metas.title,
- description: metas.description,
- image: makeEmbedImage(metas.image || metas.image_fallback, metas.width, metas.height),
- provider: {
- url: "https://pixiv.net",
- name: "Pixiv",
- },
- };
- },
+ return {
+ url: url.href,
+ type: EmbedType.image,
+ title: metas.title,
+ description: metas.description,
+ image: makeEmbedImage(metas.image || metas.image_fallback, metas.width, metas.height),
+ provider: {
+ url: "https://pixiv.net",
+ name: "Pixiv",
+ },
+ };
+ },
- "store.steampowered.com": async (url: URL) => {
- const response = await doFetch(url);
- if (!response) return null;
- const metas = getMetaDescriptions(await response.text());
- const numReviews = metas.$("#review_summary_num_reviews").val() as string | undefined;
- const price = metas.$(".game_purchase_price.price").data("price-final") as number | undefined;
- const releaseDate = metas.$(".release_date").find("div.date").text().trim();
- const isReleased = new Date(releaseDate) < new Date();
+ "store.steampowered.com": async (url: URL) => {
+ const response = await doFetch(url);
+ if (!response) return null;
+ const metas = getMetaDescriptions(await response.text());
+ const numReviews = metas.$("#review_summary_num_reviews").val() as string | undefined;
+ const price = metas.$(".game_purchase_price.price").data("price-final") as number | undefined;
+ const releaseDate = metas.$(".release_date").find("div.date").text().trim();
+ const isReleased = new Date(releaseDate) < new Date();
- const fields: Embed["fields"] = [];
+ const fields: Embed["fields"] = [];
- if (numReviews)
- fields.push({
- name: "Reviews",
- value: numReviews,
- inline: true,
- });
+ if (numReviews)
+ fields.push({
+ name: "Reviews",
+ value: numReviews,
+ inline: true,
+ });
- if (price)
- fields.push({
- name: "Price",
- value: `$${price / 100}`,
- inline: true,
- });
+ if (price)
+ fields.push({
+ name: "Price",
+ value: `$${price / 100}`,
+ inline: true,
+ });
- // if the release date is in the past, it's already out
- if (releaseDate && !isReleased)
- fields.push({
- name: "Release Date",
- value: releaseDate,
- inline: true,
- });
+ // if the release date is in the past, it's already out
+ if (releaseDate && !isReleased)
+ fields.push({
+ name: "Release Date",
+ value: releaseDate,
+ inline: true,
+ });
- return {
- url: url.href,
- type: EmbedType.rich,
- title: metas.title,
- description: metas.description,
- image: {
- // TODO: meant to be thumbnail.
- // isn't this standard across all of steam?
- width: 460,
- height: 215,
- url: metas.image,
- proxy_url: metas.image ? getProxyUrl(new URL(metas.image), 460, 215) : undefined,
- },
- provider: {
- url: "https://store.steampowered.com",
- name: "Steam",
- },
- fields,
- // TODO: Video
- };
- },
+ return {
+ url: url.href,
+ type: EmbedType.rich,
+ title: metas.title,
+ description: metas.description,
+ image: {
+ // TODO: meant to be thumbnail.
+ // isn't this standard across all of steam?
+ width: 460,
+ height: 215,
+ url: metas.image,
+ proxy_url: metas.image ? getProxyUrl(new URL(metas.image), 460, 215) : undefined,
+ },
+ provider: {
+ url: "https://store.steampowered.com",
+ name: "Steam",
+ },
+ fields,
+ // TODO: Video
+ };
+ },
- "reddit.com": (url) => EmbedHandlers["www.reddit.com"](url),
- "www.reddit.com": async (url: URL) => {
- const res = await EmbedHandlers["default"](url);
- return {
- ...res,
- color: 16777215,
- provider: {
- name: "reddit",
- },
- };
- },
+ "reddit.com": (url) => EmbedHandlers["www.reddit.com"](url),
+ "www.reddit.com": async (url: URL) => {
+ const res = await EmbedHandlers["default"](url);
+ return {
+ ...res,
+ color: 16777215,
+ provider: {
+ name: "reddit",
+ },
+ };
+ },
- "youtu.be": (url) => EmbedHandlers["www.youtube.com"](url),
- "youtube.com": (url) => EmbedHandlers["www.youtube.com"](url),
- "www.youtube.com": async (url: URL): Promise<Embed | null> => {
- const response = await doFetch(url);
- if (!response) return null;
- const metas = getMetaDescriptions(await response.text());
+ "youtu.be": (url) => EmbedHandlers["www.youtube.com"](url),
+ "youtube.com": (url) => EmbedHandlers["www.youtube.com"](url),
+ "www.youtube.com": async (url: URL): Promise<Embed | null> => {
+ const response = await doFetch(url);
+ if (!response) return null;
+ const metas = getMetaDescriptions(await response.text());
- return {
- video: makeEmbedImage(metas.youtube_embed, metas.width, metas.height),
- url: url.href,
- type: metas.youtube_embed ? EmbedType.video : EmbedType.link,
- title: metas.title,
- thumbnail: makeEmbedImage(metas.image || metas.image_fallback, metas.width, metas.height),
- provider: {
- url: "https://www.youtube.com",
- name: "YouTube",
- },
- description: metas.description,
- color: 16711680,
- author: metas.author
- ? {
- name: metas.author,
- // TODO: author channel url
- }
- : undefined,
- };
- },
+ return {
+ video: makeEmbedImage(metas.youtube_embed, metas.width, metas.height),
+ url: url.href,
+ type: metas.youtube_embed ? EmbedType.video : EmbedType.link,
+ title: metas.title,
+ thumbnail: makeEmbedImage(metas.image || metas.image_fallback, metas.width, metas.height),
+ provider: {
+ url: "https://www.youtube.com",
+ name: "YouTube",
+ },
+ description: metas.description,
+ color: 16711680,
+ author: metas.author
+ ? {
+ name: metas.author,
+ // TODO: author channel url
+ }
+ : undefined,
+ };
+ },
- "www.xkcd.com": (url) => EmbedHandlers["xkcd.com"](url),
- "xkcd.com": async (url) => {
- const response = await doFetch(url);
- if (!response) return null;
+ "www.xkcd.com": (url) => EmbedHandlers["xkcd.com"](url),
+ "xkcd.com": async (url) => {
+ const response = await doFetch(url);
+ if (!response) return null;
- const metas = getMetaDescriptions(await response.text());
- const hoverText = metas.$("#comic img").attr("title");
+ const metas = getMetaDescriptions(await response.text());
+ const hoverText = metas.$("#comic img").attr("title");
- if (!metas.image) return null;
+ if (!metas.image) return null;
- const { width, height } = await probe(metas.image);
+ const { width, height } = await probe(metas.image);
- return {
- url: url.href,
- type: EmbedType.rich,
- title: `xkcd: ${metas.title}`,
- image: makeEmbedImage(metas.image, width, height),
- footer: hoverText
- ? {
- text: hoverText,
- }
- : undefined,
- };
- },
+ return {
+ url: url.href,
+ type: EmbedType.rich,
+ title: `xkcd: ${metas.title}`,
+ image: makeEmbedImage(metas.image, width, height),
+ footer: hoverText
+ ? {
+ text: hoverText,
+ }
+ : undefined,
+ };
+ },
- // the url is an image from this instance
- self: async (url: URL): Promise<Embed | null> => {
- const result = await probe(url.href);
+ // the url is an image from this instance
+ self: async (url: URL): Promise<Embed | null> => {
+ const result = await probe(url.href);
- return {
- url: url.href,
- type: EmbedType.image,
- thumbnail: {
- width: result.width,
- height: result.height,
- url: url.href,
- proxy_url: url.href,
- },
- };
- },
+ return {
+ url: url.href,
+ type: EmbedType.image,
+ thumbnail: {
+ width: result.width,
+ height: result.height,
+ url: url.href,
+ proxy_url: url.href,
+ },
+ };
+ },
};
diff --git a/src/api/util/utility/RandomInviteID.ts b/src/api/util/utility/RandomInviteID.ts
index 3850df54..0718a736 100644
--- a/src/api/util/utility/RandomInviteID.ts
+++ b/src/api/util/utility/RandomInviteID.ts
@@ -23,42 +23,42 @@ import crypto from "crypto";
// And why is this even here? Just use cryto.randomBytes?
export function randomString(length = 6) {
- // Declare all characters
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ // Declare all characters
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
- // Pick characers randomly
- let str = "";
- for (let i = 0; i < length; i++) {
- str += chars.charAt(Math.floor(crypto.randomInt(chars.length)));
- }
+ // Pick characers randomly
+ let str = "";
+ for (let i = 0; i < length; i++) {
+ str += chars.charAt(Math.floor(crypto.randomInt(chars.length)));
+ }
- return str;
+ return str;
}
export function snowflakeBasedInvite() {
- // Declare all characters
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
- const base = BigInt(chars.length);
- let snowflake = Snowflake.generateWorkerProcess();
+ // Declare all characters
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ const base = BigInt(chars.length);
+ let snowflake = Snowflake.generateWorkerProcess();
- // snowflakes hold ~10.75 characters worth of entropy;
- // safe to generate a 8-char invite out of them
- const str = "";
- for (let i = 0; i < 10; i++) {
- str.concat(chars.charAt(Number(snowflake % base)));
- snowflake = snowflake / base;
- }
+ // snowflakes hold ~10.75 characters worth of entropy;
+ // safe to generate a 8-char invite out of them
+ const str = "";
+ for (let i = 0; i < 10; i++) {
+ str.concat(chars.charAt(Number(snowflake % base)));
+ snowflake = snowflake / base;
+ }
- return str.substr(3, 8).split("").reverse().join("");
+ return str.substr(3, 8).split("").reverse().join("");
}
export function randomUpperString(length: number = 10) {
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
- let result = "";
- for (let i = 0; i < length; i++) {
- result += chars.charAt(Math.floor(Math.random() * chars.length));
- }
+ let result = "";
+ for (let i = 0; i < length; i++) {
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
- return result;
+ return result;
}
diff --git a/src/api/util/utility/String.ts b/src/api/util/utility/String.ts
index e5c1279a..e33613a3 100644
--- a/src/api/util/utility/String.ts
+++ b/src/api/util/utility/String.ts
@@ -21,18 +21,18 @@ import { ntob } from "./Base64";
import { FieldErrors, Random } from "@spacebar/util";
export function checkLength(str: string, min: number, max: number, key: string, req: Request) {
- if (str.length < min || str.length > max) {
- throw FieldErrors({
- [key]: {
- code: "BASE_TYPE_BAD_LENGTH",
- message: req.t("common:field.BASE_TYPE_BAD_LENGTH", {
- length: `${min} - ${max}`,
- }),
- },
- });
- }
+ if (str.length < min || str.length > max) {
+ throw FieldErrors({
+ [key]: {
+ code: "BASE_TYPE_BAD_LENGTH",
+ message: req.t("common:field.BASE_TYPE_BAD_LENGTH", {
+ length: `${min} - ${max}`,
+ }),
+ },
+ });
+ }
}
export function generateCode() {
- return ntob(Date.now() + Random.nextInt(0, 10000));
+ return ntob(Date.now() + Random.nextInt(0, 10000));
}
diff --git a/src/api/util/utility/captcha.ts b/src/api/util/utility/captcha.ts
index f6d4d0e1..c4d8c9a3 100644
--- a/src/api/util/utility/captcha.ts
+++ b/src/api/util/utility/captcha.ts
@@ -19,46 +19,46 @@
import { Config } from "@spacebar/util";
export interface hcaptchaResponse {
- success: boolean;
- challenge_ts: string;
- hostname: string;
- credit: boolean;
- "error-codes": string[];
- score: number; // enterprise only
- score_reason: string[]; // enterprise only
+ success: boolean;
+ challenge_ts: string;
+ hostname: string;
+ credit: boolean;
+ "error-codes": string[];
+ score: number; // enterprise only
+ score_reason: string[]; // enterprise only
}
export interface recaptchaResponse {
- success: boolean;
- score: number; // between 0 - 1
- action: string;
- challenge_ts: string;
- hostname: string;
- "error-codes"?: string[];
+ success: boolean;
+ score: number; // between 0 - 1
+ action: string;
+ challenge_ts: string;
+ hostname: string;
+ "error-codes"?: string[];
}
const verifyEndpoints = {
- hcaptcha: "https://hcaptcha.com/siteverify",
- recaptcha: "https://www.google.com/recaptcha/api/siteverify",
+ hcaptcha: "https://hcaptcha.com/siteverify",
+ recaptcha: "https://www.google.com/recaptcha/api/siteverify",
};
export async function verifyCaptcha(response: string, ip?: string) {
- const { security } = Config.get();
- const { service, secret, sitekey } = security.captcha;
+ const { security } = Config.get();
+ const { service, secret, sitekey } = security.captcha;
- if (!service || !secret || !sitekey) throw new Error("CAPTCHA is not configured correctly. https://docs.spacebar.chat/setup/server/security/captcha/");
+ if (!service || !secret || !sitekey) throw new Error("CAPTCHA is not configured correctly. https://docs.spacebar.chat/setup/server/security/captcha/");
- const res = await fetch(verifyEndpoints[service], {
- method: "POST",
- headers: {
- "Content-Type": "application/x-www-form-urlencoded",
- },
- body:
- `response=${encodeURIComponent(response)}` +
- `&secret=${encodeURIComponent(secret)}` +
- `&sitekey=${encodeURIComponent(sitekey)}` +
- (ip ? `&remoteip=${encodeURIComponent(ip)}` : ""),
- });
+ const res = await fetch(verifyEndpoints[service], {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body:
+ `response=${encodeURIComponent(response)}` +
+ `&secret=${encodeURIComponent(secret)}` +
+ `&sitekey=${encodeURIComponent(sitekey)}` +
+ (ip ? `&remoteip=${encodeURIComponent(ip)}` : ""),
+ });
- return (await res.json()) as hcaptchaResponse | recaptchaResponse;
+ return (await res.json()) as hcaptchaResponse | recaptchaResponse;
}
diff --git a/src/api/util/utility/ipAddress.ts b/src/api/util/utility/ipAddress.ts
index 19408253..399edb67 100644
--- a/src/api/util/utility/ipAddress.ts
+++ b/src/api/util/utility/ipAddress.ts
@@ -18,14 +18,14 @@
type Location = { latitude: number; longitude: number };
export function distanceBetweenLocations(loc1: Location, loc2: Location): number {
- return distanceBetweenCoords(loc1.latitude, loc1.longitude, loc2.latitude, loc2.longitude);
+ return distanceBetweenCoords(loc1.latitude, loc1.longitude, loc2.latitude, loc2.longitude);
}
//Haversine function
function distanceBetweenCoords(lat1: number, lon1: number, lat2: number, lon2: number) {
- const p = 0.017453292519943295; // Math.PI / 180
- const c = Math.cos;
- const a = 0.5 - c((lat2 - lat1) * p) / 2 + (c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p))) / 2;
+ const p = 0.017453292519943295; // Math.PI / 180
+ const c = Math.cos;
+ const a = 0.5 - c((lat2 - lat1) * p) / 2 + (c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p))) / 2;
- return 12742 * Math.asin(Math.sqrt(a)); // 2 * R; R = 6371 km
+ return 12742 * Math.asin(Math.sqrt(a)); // 2 * R; R = 6371 km
}
diff --git a/src/api/util/utility/passwordStrength.ts b/src/api/util/utility/passwordStrength.ts
index beb277b0..7a9d4c5f 100644
--- a/src/api/util/utility/passwordStrength.ts
+++ b/src/api/util/utility/passwordStrength.ts
@@ -35,43 +35,43 @@ const reSYMBOLS = /[A-Za-z0-9]/g;
* Returns: 0 > pw > 1
*/
export function checkPassword(password: string): number {
- const { minLength, minNumbers, minUpperCase, minSymbols } = Config.get().register.password;
- let strength = 0;
+ const { minLength, minNumbers, minUpperCase, minSymbols } = Config.get().register.password;
+ let strength = 0;
- // checks for total password len
- if (password.length >= minLength - 1) {
- strength += 0.05;
- }
+ // checks for total password len
+ if (password.length >= minLength - 1) {
+ strength += 0.05;
+ }
- // checks for amount of Numbers
- if (password.match(reNUMBER)?.length ?? 0 >= minNumbers - 1) {
- strength += 0.05;
- }
+ // checks for amount of Numbers
+ if (password.match(reNUMBER)?.length ?? 0 >= minNumbers - 1) {
+ strength += 0.05;
+ }
- // checks for amount of Uppercase Letters
- if (password.match(reUPPERCASELETTER)?.length ?? 0 >= minUpperCase - 1) {
- strength += 0.05;
- }
+ // checks for amount of Uppercase Letters
+ if (password.match(reUPPERCASELETTER)?.length ?? 0 >= minUpperCase - 1) {
+ strength += 0.05;
+ }
- // checks for amount of symbols
- if (password.replace(reSYMBOLS, "").length >= minSymbols - 1) {
- strength += 0.05;
- }
+ // checks for amount of symbols
+ if (password.replace(reSYMBOLS, "").length >= minSymbols - 1) {
+ strength += 0.05;
+ }
- // checks if password only consists of numbers or only consists of chars
- if (password.length == password.match(reNUMBER)?.length || password.length === password.match(reUPPERCASELETTER)?.length) {
- strength = 0;
- }
+ // checks if password only consists of numbers or only consists of chars
+ if (password.length == password.match(reNUMBER)?.length || password.length === password.match(reUPPERCASELETTER)?.length) {
+ strength = 0;
+ }
- const entropyMap: { [key: string]: number } = {};
- for (let i = 0; i < password.length; i++) {
- if (entropyMap[password[i]]) entropyMap[password[i]]++;
- else entropyMap[password[i]] = 1;
- }
+ const entropyMap: { [key: string]: number } = {};
+ for (let i = 0; i < password.length; i++) {
+ if (entropyMap[password[i]]) entropyMap[password[i]]++;
+ else entropyMap[password[i]] = 1;
+ }
- const entropies = Object.values(entropyMap);
+ const entropies = Object.values(entropyMap);
- entropies.map((x) => x / entropyMap.length);
- strength += entropies.reduceRight((a: number, x: number) => a - x * Math.log2(x)) / Math.log2(password.length);
- return strength;
+ entropies.map((x) => x / entropyMap.length);
+ strength += entropies.reduceRight((a: number, x: number) => a - x * Math.log2(x)) / Math.log2(password.length);
+ return strength;
}
|