summary refs log tree commit diff
path: root/src/api/util/utility
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-12-17 11:00:51 +0100
committerRory& <root@rory.gay>2025-12-17 11:01:27 +0100
commit491c0de845a886954262e3ddb24959ba37a014b6 (patch)
treec0251f3779a5cd2842320e1278232b48fbea0390 /src/api/util/utility
parentPre-commit: use spaces for formatting, regenerate schemas if changed (diff)
downloadserver-ts-491c0de845a886954262e3ddb24959ba37a014b6.tar.xz
lintstagedrc: regenerate schemas/openapi if schemas changed
Diffstat (limited to 'src/api/util/utility')
-rw-r--r--src/api/util/utility/Base64.ts46
-rw-r--r--src/api/util/utility/EmbedHandlers.ts826
-rw-r--r--src/api/util/utility/RandomInviteID.ts52
-rw-r--r--src/api/util/utility/String.ts22
-rw-r--r--src/api/util/utility/captcha.ts60
-rw-r--r--src/api/util/utility/ipAddress.ts10
-rw-r--r--src/api/util/utility/passwordStrength.ts62
7 files changed, 539 insertions, 539 deletions
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; }