summary refs log tree commit diff
path: root/src/api/util
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
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')
-rw-r--r--src/api/util/handlers/Instance.ts54
-rw-r--r--src/api/util/handlers/Message.ts932
-rw-r--r--src/api/util/handlers/Voice.ts44
-rw-r--r--src/api/util/handlers/Webhook.ts192
-rw-r--r--src/api/util/handlers/route.ts170
-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
12 files changed, 1235 insertions, 1235 deletions
diff --git a/src/api/util/handlers/Instance.ts b/src/api/util/handlers/Instance.ts

index 86dda74b..e71c3eda 100644 --- a/src/api/util/handlers/Instance.ts +++ b/src/api/util/handlers/Instance.ts
@@ -21,34 +21,34 @@ import { Like } from "typeorm"; import { setInterval } from "timers"; export async function initInstance() { - // TODO: clean up database and delete tombstone data - // TODO: set first user as instance administrator/or generate one if none exists and output it in the terminal + // TODO: clean up database and delete tombstone data + // TODO: set first user as instance administrator/or generate one if none exists and output it in the terminal - // create default guild and add it to auto join - // TODO: check if any current user is not part of autoJoinGuilds - // const { autoJoin } = Config.get().guild; + // create default guild and add it to auto join + // TODO: check if any current user is not part of autoJoinGuilds + // const { autoJoin } = Config.get().guild; - // if (autoJoin.enabled && !autoJoin.guilds?.length) { - // const guild = await Guild.findOne({ where: {}, select: ["id"] }); - // if (guild) { - // await Config.set({ guild: { autoJoin: { guilds: [guild.id] } } }); - // } - // } + // if (autoJoin.enabled && !autoJoin.guilds?.length) { + // const guild = await Guild.findOne({ where: {}, select: ["id"] }); + // if (guild) { + // await Config.set({ guild: { autoJoin: { guilds: [guild.id] } } }); + // } + // } - // TODO: do no clear sessions for instance cluster - // await Session.clear(); // This is now used as part of authentication... - // ... but we can still expire temporary sessions for legacy tokens - setInterval( - async () => { - for await (const session of await Session.createQueryBuilder("session").where("last_seen = '1970/01/01'").select().stream()) { - // session object has all fields prefixed with `session_`... thanks typeorm - if (TimeSpan.fromDates((session.session_created_at as Date).getTime(), new Date().getTime()).totalHours > 1) { - console.log(`[API/Instance.ts] Deleting unused session ${session.session_session_id} created at ${session.session_created_at}`); - await Session.delete({ session_id: session.session_session_id }); - } - } - }, - 1000 * 60 * 5, - ); - // await Session.delete({ session_id: Like("TEMP_%") }); + // TODO: do no clear sessions for instance cluster + // await Session.clear(); // This is now used as part of authentication... + // ... but we can still expire temporary sessions for legacy tokens + setInterval( + async () => { + for await (const session of await Session.createQueryBuilder("session").where("last_seen = '1970/01/01'").select().stream()) { + // session object has all fields prefixed with `session_`... thanks typeorm + if (TimeSpan.fromDates((session.session_created_at as Date).getTime(), new Date().getTime()).totalHours > 1) { + console.log(`[API/Instance.ts] Deleting unused session ${session.session_session_id} created at ${session.session_created_at}`); + await Session.delete({ session_id: session.session_session_id }); + } + } + }, + 1000 * 60 * 5, + ); + // await Session.delete({ session_id: Like("TEMP_%") }); } diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts
index 06a3e081..65583f53 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts
@@ -18,36 +18,36 @@ import { EmbedHandlers } from "@spacebar/api"; import { - Application, - Attachment, - Channel, - Config, - EmbedCache, - emitEvent, - EVERYONE_MENTION, - getPermission, - getRights, - Guild, - HERE_MENTION, - Message, - MessageCreateEvent, - MessageUpdateEvent, - Role, - ROLE_MENTION, - Sticker, - User, - //CHANNEL_MENTION, - USER_MENTION, - Webhook, - handleFile, - Permissions, - normalizeUrl, - DiscordApiErrors, - CloudAttachment, - ReadState, - Member, - Session, - MessageFlags, + Application, + Attachment, + Channel, + Config, + EmbedCache, + emitEvent, + EVERYONE_MENTION, + getPermission, + getRights, + Guild, + HERE_MENTION, + Message, + MessageCreateEvent, + MessageUpdateEvent, + Role, + ROLE_MENTION, + Sticker, + User, + //CHANNEL_MENTION, + USER_MENTION, + Webhook, + handleFile, + Permissions, + normalizeUrl, + DiscordApiErrors, + CloudAttachment, + ReadState, + Member, + Session, + MessageFlags, } from "@spacebar/util"; import { HTTPError } from "lambert-server"; import { In, Or, Equal, IsNull } from "typeorm"; @@ -59,515 +59,515 @@ const allow_empty = false; const LINK_REGEX = /<?https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)>?/g; export async function handleMessage(opts: MessageOptions): Promise<Message> { - const channel = await Channel.findOneOrFail({ - where: { id: opts.channel_id }, - relations: ["recipients"], - }); - if (!channel || !opts.channel_id) throw new HTTPError("Channel not found", 404); + const channel = await Channel.findOneOrFail({ + where: { id: opts.channel_id }, + relations: ["recipients"], + }); + if (!channel || !opts.channel_id) throw new HTTPError("Channel not found", 404); - let permission: undefined | Permissions; - const limit = channel.rate_limit_per_user; + let permission: undefined | Permissions; + const limit = channel.rate_limit_per_user; - if (limit) { - const lastMsgTime = (await Message.findOne({ where: { channel_id: channel.id, author_id: opts.author_id }, select: { timestamp: true }, order: { timestamp: "DESC" } })) - ?.timestamp; - if (lastMsgTime && Date.now() - limit * 1000 < +lastMsgTime) { - permission ||= await getPermission(opts.author_id, channel.guild_id, channel); - //FIXME MANAGE_MESSAGES and MANAGE_CHANNELS will need to be removed once they're gone as checks - if (!permission.has("MANAGE_MESSAGES") && !permission.has("MANAGE_CHANNELS") && !permission.has("BYPASS_SLOWMODE")) { - throw DiscordApiErrors.SLOWMODE_RATE_LIMIT; - } - } - } + if (limit) { + const lastMsgTime = (await Message.findOne({ where: { channel_id: channel.id, author_id: opts.author_id }, select: { timestamp: true }, order: { timestamp: "DESC" } })) + ?.timestamp; + if (lastMsgTime && Date.now() - limit * 1000 < +lastMsgTime) { + permission ||= await getPermission(opts.author_id, channel.guild_id, channel); + //FIXME MANAGE_MESSAGES and MANAGE_CHANNELS will need to be removed once they're gone as checks + if (!permission.has("MANAGE_MESSAGES") && !permission.has("MANAGE_CHANNELS") && !permission.has("BYPASS_SLOWMODE")) { + throw DiscordApiErrors.SLOWMODE_RATE_LIMIT; + } + } + } - const stickers = opts.sticker_ids ? await Sticker.find({ where: { id: In(opts.sticker_ids) } }) : undefined; - // cloud attachments with indexes - const cloudAttachments = opts.attachments?.reduce( - (acc, att, index) => { - if ("uploaded_filename" in att) { - acc.push({ attachment: att, index }); - } - return acc; - }, - [] as { attachment: MessageCreateCloudAttachment; index: number }[], - ); + const stickers = opts.sticker_ids ? await Sticker.find({ where: { id: In(opts.sticker_ids) } }) : undefined; + // cloud attachments with indexes + const cloudAttachments = opts.attachments?.reduce( + (acc, att, index) => { + if ("uploaded_filename" in att) { + acc.push({ attachment: att, index }); + } + return acc; + }, + [] as { attachment: MessageCreateCloudAttachment; index: number }[], + ); - const message = Message.create({ - ...opts, - poll: opts.poll, - sticker_items: stickers, - guild_id: channel.guild_id, - channel_id: opts.channel_id, - attachments: opts.attachments || [], - embeds: opts.embeds || [], - reactions: opts.reactions || [], - type: opts.type ?? 0, - mentions: [], - components: opts.components ?? undefined, // Fix Discord-Go? - }); - const ephermal = (message.flags & (1 << 6)) !== 0; + const message = Message.create({ + ...opts, + poll: opts.poll, + sticker_items: stickers, + guild_id: channel.guild_id, + channel_id: opts.channel_id, + attachments: opts.attachments || [], + embeds: opts.embeds || [], + reactions: opts.reactions || [], + type: opts.type ?? 0, + mentions: [], + components: opts.components ?? undefined, // Fix Discord-Go? + }); + const ephermal = (message.flags & (1 << 6)) !== 0; - if (cloudAttachments && cloudAttachments.length > 0) { - console.log("[Message] Processing attachments for message", message.id, ":", message.attachments); - const uploadedAttachments = await Promise.all( - cloudAttachments.map(async (att) => { - const cAtt = att.attachment; - const attEnt = await CloudAttachment.findOneOrFail({ - where: { - uploadFilename: cAtt.uploaded_filename, - }, - }); + if (cloudAttachments && cloudAttachments.length > 0) { + console.log("[Message] Processing attachments for message", message.id, ":", message.attachments); + const uploadedAttachments = await Promise.all( + cloudAttachments.map(async (att) => { + const cAtt = att.attachment; + const attEnt = await CloudAttachment.findOneOrFail({ + where: { + uploadFilename: cAtt.uploaded_filename, + }, + }); - const cloneResponse = await fetch(`${Config.get().cdn.endpointPrivate}/attachments/${attEnt.uploadFilename}/clone_to_message/${message.id}`, { - method: "POST", - headers: { - signature: Config.get().security.requestSignature || "", - }, - }); + const cloneResponse = await fetch(`${Config.get().cdn.endpointPrivate}/attachments/${attEnt.uploadFilename}/clone_to_message/${message.id}`, { + method: "POST", + headers: { + signature: Config.get().security.requestSignature || "", + }, + }); - if (!cloneResponse.ok) { - console.error(`[Message] Failed to clone attachment ${attEnt.userFilename} to message ${message.id}`); - throw new HTTPError("Failed to process attachment: " + (await cloneResponse.text()), 500); - } + if (!cloneResponse.ok) { + console.error(`[Message] Failed to clone attachment ${attEnt.userFilename} to message ${message.id}`); + throw new HTTPError("Failed to process attachment: " + (await cloneResponse.text()), 500); + } - const cloneRespBody = (await cloneResponse.json()) as { success: boolean; new_path: string }; + const cloneRespBody = (await cloneResponse.json()) as { success: boolean; new_path: string }; - const realAtt = Attachment.create({ - filename: attEnt.userFilename, - url: `${Config.get().cdn.endpointPublic}/${cloneRespBody.new_path}`, - proxy_url: `${Config.get().cdn.endpointPublic}/${cloneRespBody.new_path}`, - size: attEnt.size, - height: attEnt.height, - width: attEnt.width, - content_type: attEnt.contentType || attEnt.userOriginalContentType, - }); - await realAtt.save(); - return { attachment: realAtt, index: att.index }; - }), - ); - console.log("[Message] Processed attachments for message", message.id, ":", message.attachments); + const realAtt = Attachment.create({ + filename: attEnt.userFilename, + url: `${Config.get().cdn.endpointPublic}/${cloneRespBody.new_path}`, + proxy_url: `${Config.get().cdn.endpointPublic}/${cloneRespBody.new_path}`, + size: attEnt.size, + height: attEnt.height, + width: attEnt.width, + content_type: attEnt.contentType || attEnt.userOriginalContentType, + }); + await realAtt.save(); + return { attachment: realAtt, index: att.index }; + }), + ); + console.log("[Message] Processed attachments for message", message.id, ":", message.attachments); - for (const att of uploadedAttachments) { - message.attachments![att.index] = att.attachment; - } - } - // else console.log("[Message] No cloud attachments to process for message", message.id, ":", message.attachments); + for (const att of uploadedAttachments) { + message.attachments![att.index] = att.attachment; + } + } + // else console.log("[Message] No cloud attachments to process for message", message.id, ":", message.attachments); - if (message.content && message.content.length > Config.get().limits.message.maxCharacters) { - throw new HTTPError("Content length over max character limit"); - } + if (message.content && message.content.length > Config.get().limits.message.maxCharacters) { + throw new HTTPError("Content length over max character limit"); + } - if (opts.author_id) { - message.author = await User.getPublicUser(opts.author_id); - const rights = await getRights(opts.author_id); - rights.hasThrow("SEND_MESSAGES"); - } - if (opts.application_id) { - message.application = await Application.findOneOrFail({ - where: { id: opts.application_id }, - }); - } + if (opts.author_id) { + message.author = await User.getPublicUser(opts.author_id); + const rights = await getRights(opts.author_id); + rights.hasThrow("SEND_MESSAGES"); + } + if (opts.application_id) { + message.application = await Application.findOneOrFail({ + where: { id: opts.application_id }, + }); + } - if (opts.webhook_id) { - message.webhook = await Webhook.findOneOrFail({ - where: { id: opts.webhook_id }, - }); + if (opts.webhook_id) { + message.webhook = await Webhook.findOneOrFail({ + where: { id: opts.webhook_id }, + }); - message.author = - (await User.findOne({ - where: { id: opts.webhook_id }, - })) || undefined; + message.author = + (await User.findOne({ + where: { id: opts.webhook_id }, + })) || undefined; - if (!message.author) { - message.author = User.create({ - id: opts.webhook_id, - username: message.webhook.name, - discriminator: "0000", - avatar: message.webhook.avatar, - public_flags: 0, - premium: false, - premium_type: 0, - bot: true, - created_at: new Date(), - verified: true, - rights: "0", - data: { - valid_tokens_since: new Date(), - }, - }); + if (!message.author) { + message.author = User.create({ + id: opts.webhook_id, + username: message.webhook.name, + discriminator: "0000", + avatar: message.webhook.avatar, + public_flags: 0, + premium: false, + premium_type: 0, + bot: true, + created_at: new Date(), + verified: true, + rights: "0", + data: { + valid_tokens_since: new Date(), + }, + }); - await message.author.save(); - } + await message.author.save(); + } - if (opts.username) { - message.username = opts.username; - message.author.username = message.username; - } - if (opts.avatar_url) { - const avatarData = await fetch(opts.avatar_url); - const base64 = await avatarData.arrayBuffer().then((x) => Buffer.from(x).toString("base64")); + if (opts.username) { + message.username = opts.username; + message.author.username = message.username; + } + if (opts.avatar_url) { + const avatarData = await fetch(opts.avatar_url); + const base64 = await avatarData.arrayBuffer().then((x) => Buffer.from(x).toString("base64")); - const dataUri = "data:" + avatarData.headers.get("content-type") + ";base64," + base64; + const dataUri = "data:" + avatarData.headers.get("content-type") + ";base64," + base64; - message.avatar = await handleFile(`/avatars/${opts.webhook_id}`, dataUri as string); - message.author.avatar = message.avatar; - } - } else { - permission ||= await getPermission(opts.author_id, channel.guild_id, channel); - permission.hasThrow("SEND_MESSAGES"); - if (permission.cache.member) { - message.member = permission.cache.member; - } + message.avatar = await handleFile(`/avatars/${opts.webhook_id}`, dataUri as string); + message.author.avatar = message.avatar; + } + } else { + permission ||= await getPermission(opts.author_id, channel.guild_id, channel); + permission.hasThrow("SEND_MESSAGES"); + if (permission.cache.member) { + message.member = permission.cache.member; + } - if (opts.tts) permission.hasThrow("SEND_TTS_MESSAGES"); - if (opts.message_reference) { - permission.hasThrow("READ_MESSAGE_HISTORY"); - // code below has to be redone when we add custom message routing - if (message.guild_id !== null) { - const guild = await Guild.findOneOrFail({ - where: { id: channel.guild_id }, - }); - if (!opts.message_reference.guild_id) opts.message_reference.guild_id = channel.guild_id; - if (!opts.message_reference.channel_id) opts.message_reference.channel_id = opts.channel_id; + if (opts.tts) permission.hasThrow("SEND_TTS_MESSAGES"); + if (opts.message_reference) { + permission.hasThrow("READ_MESSAGE_HISTORY"); + // code below has to be redone when we add custom message routing + if (message.guild_id !== null) { + const guild = await Guild.findOneOrFail({ + where: { id: channel.guild_id }, + }); + if (!opts.message_reference.guild_id) opts.message_reference.guild_id = channel.guild_id; + if (!opts.message_reference.channel_id) opts.message_reference.channel_id = opts.channel_id; - if (!guild.features.includes("CROSS_CHANNEL_REPLIES")) { - if (opts.message_reference.guild_id !== channel.guild_id) throw new HTTPError("You can only reference messages from this guild"); - if (opts.message_reference.channel_id !== opts.channel_id) throw new HTTPError("You can only reference messages from this channel"); - } + if (!guild.features.includes("CROSS_CHANNEL_REPLIES")) { + if (opts.message_reference.guild_id !== channel.guild_id) throw new HTTPError("You can only reference messages from this guild"); + if (opts.message_reference.channel_id !== opts.channel_id) throw new HTTPError("You can only reference messages from this channel"); + } - message.message_reference = opts.message_reference; - message.referenced_message = await Message.findOneOrFail({ - where: { - id: opts.message_reference.message_id, - }, - relations: ["author", "webhook", "application", "mentions", "mention_roles", "mention_channels", "sticker_items", "attachments"], - }); + message.message_reference = opts.message_reference; + message.referenced_message = await Message.findOneOrFail({ + where: { + id: opts.message_reference.message_id, + }, + relations: ["author", "webhook", "application", "mentions", "mention_roles", "mention_channels", "sticker_items", "attachments"], + }); - if (message.referenced_message.channel_id && message.referenced_message.channel_id !== opts.message_reference.channel_id) - throw new HTTPError("Referenced message not found in the specified channel", 404); - if (message.referenced_message.guild_id && message.referenced_message.guild_id !== opts.message_reference.guild_id) - throw new HTTPError("Referenced message not found in the specified channel", 404); - } - /** Q: should be checked if the referenced message exists? ANSWER: NO + if (message.referenced_message.channel_id && message.referenced_message.channel_id !== opts.message_reference.channel_id) + throw new HTTPError("Referenced message not found in the specified channel", 404); + if (message.referenced_message.guild_id && message.referenced_message.guild_id !== opts.message_reference.guild_id) + throw new HTTPError("Referenced message not found in the specified channel", 404); + } + /** Q: should be checked if the referenced message exists? ANSWER: NO otherwise backfilling won't work **/ - message.type = MessageType.REPLY; - } - } + message.type = MessageType.REPLY; + } + } - // TODO: stickers/activity - if (!allow_empty && !opts.content && !opts.embeds?.length && !opts.attachments?.length && !opts.sticker_ids?.length && !opts.poll && !opts.components?.length) { - console.log("[Message] Rejecting empty message:", opts, message); - throw new HTTPError("Empty messages are not allowed", 50006); - } + // TODO: stickers/activity + if (!allow_empty && !opts.content && !opts.embeds?.length && !opts.attachments?.length && !opts.sticker_ids?.length && !opts.poll && !opts.components?.length) { + console.log("[Message] Rejecting empty message:", opts, message); + throw new HTTPError("Empty messages are not allowed", 50006); + } - let content = opts.content; + let content = opts.content; - // root@Rory - 20/02/2023 - This breaks channel mentions in test client. We're not sure this was used in older clients. - //const mention_channel_ids = [] as string[]; - const mention_role_ids = [] as string[]; - const mention_user_ids = [] as string[]; - let mention_everyone = false; + // root@Rory - 20/02/2023 - This breaks channel mentions in test client. We're not sure this was used in older clients. + //const mention_channel_ids = [] as string[]; + const mention_role_ids = [] as string[]; + const mention_user_ids = [] as string[]; + let mention_everyone = false; - if (content) { - // TODO: explicit-only mentions - message.content = content.trim(); - content = content.replace(/ *`[^)]*` */g, ""); // remove codeblocks - // root@Rory - 20/02/2023 - This breaks channel mentions in test client. We're not sure this was used in older clients. - /*for (const [, mention] of content.matchAll(CHANNEL_MENTION)) { + if (content) { + // TODO: explicit-only mentions + message.content = content.trim(); + content = content.replace(/ *`[^)]*` */g, ""); // remove codeblocks + // root@Rory - 20/02/2023 - This breaks channel mentions in test client. We're not sure this was used in older clients. + /*for (const [, mention] of content.matchAll(CHANNEL_MENTION)) { if (!mention_channel_ids.includes(mention)) mention_channel_ids.push(mention); }*/ - for (const [, mention] of content.matchAll(USER_MENTION)) { - if (!mention_user_ids.includes(mention)) mention_user_ids.push(mention); - } + for (const [, mention] of content.matchAll(USER_MENTION)) { + if (!mention_user_ids.includes(mention)) mention_user_ids.push(mention); + } - await Promise.all( - Array.from(content.matchAll(ROLE_MENTION)).map(async ([, mention]) => { - const role = await Role.findOneOrFail({ - where: { id: mention, guild_id: channel.guild_id }, - }); - if (role.mentionable || opts.webhook_id || permission?.has("MANAGE_ROLES")) { - mention_role_ids.push(mention); - } - }), - ); + await Promise.all( + Array.from(content.matchAll(ROLE_MENTION)).map(async ([, mention]) => { + const role = await Role.findOneOrFail({ + where: { id: mention, guild_id: channel.guild_id }, + }); + if (role.mentionable || opts.webhook_id || permission?.has("MANAGE_ROLES")) { + mention_role_ids.push(mention); + } + }), + ); - if (opts.webhook_id || permission?.has("MENTION_EVERYONE")) { - mention_everyone = !!content.match(EVERYONE_MENTION) || !!content.match(HERE_MENTION); - } - } + if (opts.webhook_id || permission?.has("MENTION_EVERYONE")) { + mention_everyone = !!content.match(EVERYONE_MENTION) || !!content.match(HERE_MENTION); + } + } - if (message.message_reference?.message_id) { - const referencedMessage = await Message.findOne({ - where: { - id: message.message_reference.message_id, - channel_id: message.channel_id, - }, - }); - if (referencedMessage && referencedMessage.author_id !== message.author_id) { - message.mentions.push( - User.create({ - id: referencedMessage.author_id, - }), - ); - } - } + if (message.message_reference?.message_id) { + const referencedMessage = await Message.findOne({ + where: { + id: message.message_reference.message_id, + channel_id: message.channel_id, + }, + }); + if (referencedMessage && referencedMessage.author_id !== message.author_id) { + message.mentions.push( + User.create({ + id: referencedMessage.author_id, + }), + ); + } + } - // root@Rory - 20/02/2023 - This breaks channel mentions in test client. We're not sure this was used in older clients. - /*message.mention_channels = mention_channel_ids.map((x) => + // root@Rory - 20/02/2023 - This breaks channel mentions in test client. We're not sure this was used in older clients. + /*message.mention_channels = mention_channel_ids.map((x) => Channel.create({ id: x }), );*/ - message.mention_roles = ( - await Promise.all( - mention_role_ids.map((x) => { - return Role.findOne({ where: { id: x } }); - }), - ) - ).filter((role) => role !== null); + message.mention_roles = ( + await Promise.all( + mention_role_ids.map((x) => { + return Role.findOne({ where: { id: x } }); + }), + ) + ).filter((role) => role !== null); - message.mentions = [ - ...message.mentions, - ...( - await Promise.all( - mention_user_ids.map((x) => { - return User.findOne({ where: { id: x } }); - }), - ) - ).filter((user) => user !== null), - ]; + message.mentions = [ + ...message.mentions, + ...( + await Promise.all( + mention_user_ids.map((x) => { + return User.findOne({ where: { id: x } }); + }), + ) + ).filter((user) => user !== null), + ]; - message.mention_everyone = mention_everyone; - async function fillInMissingIDs(ids: string[]) { - const states = await ReadState.findBy({ - user_id: Or(...ids.map((id) => Equal(id))), - channel_id: channel.id, - }); - const users = new Set(ids); - states.forEach((state) => users.delete(state.user_id)); - if (!users.size) { - return; - } - return Promise.all( - [...users].map((user_id) => { - return ReadState.create({ user_id, channel_id: channel.id }).save(); - }), - ); - } - if (ephermal) { - const id = message.interaction_metadata?.user_id; - if (id) { - let pinged = mention_everyone || channel.type === ChannelType.DM || channel.type === ChannelType.GROUP_DM; - if (!pinged) pinged = !!message.mentions.find((user) => user.id === id); - if (!pinged) pinged = !!(await Member.find({ where: { id, roles: Or(...message.mention_roles.map(({ id }) => Equal(id))) } })); - if (pinged) { - //stuff - } - } - } else if ((!!message.content?.match(EVERYONE_MENTION) && permission?.has("MENTION_EVERYONE")) || channel.type === ChannelType.DM || channel.type === ChannelType.GROUP_DM) { - if (channel.type === ChannelType.DM || channel.type === ChannelType.GROUP_DM) { - if (channel.recipients) { - await fillInMissingIDs(channel.recipients.map(({ user_id }) => user_id)); - } - } else { - console.log(channel.guild_id); - await fillInMissingIDs((await Member.find({ where: { guild_id: channel.guild_id } })).map(({ id }) => id)); - } - const repository = ReadState.getRepository(); - const condition = { channel_id: channel.id }; - await repository.update({ ...condition, mention_count: IsNull() }, { mention_count: 0 }); - await repository.increment(condition, "mention_count", 1); - } else { - const users = new Set<string>([ - ...(message.mention_roles.length - ? await Member.find({ - where: [ - ...message.mention_roles.map((role) => { - return { roles: { id: role.id } }; - }), - ], - }) - : [] - ).map((member) => member.id), - ...message.mentions.map((user) => user.id), - ]); - if (!!message.content?.match(HERE_MENTION) && permission?.has("MENTION_EVERYONE")) { - const ids = (await Member.find({ where: { guild_id: channel.guild_id } })).map(({ id }) => id); - (await Session.find({ where: { user_id: Or(...ids.map((id) => Equal(id))) } })).forEach(({ user_id }) => users.add(user_id)); - } - if (users.size) { - const repository = ReadState.getRepository(); - const condition = { user_id: Or(...[...users].map((id) => Equal(id))), channel_id: channel.id }; + message.mention_everyone = mention_everyone; + async function fillInMissingIDs(ids: string[]) { + const states = await ReadState.findBy({ + user_id: Or(...ids.map((id) => Equal(id))), + channel_id: channel.id, + }); + const users = new Set(ids); + states.forEach((state) => users.delete(state.user_id)); + if (!users.size) { + return; + } + return Promise.all( + [...users].map((user_id) => { + return ReadState.create({ user_id, channel_id: channel.id }).save(); + }), + ); + } + if (ephermal) { + const id = message.interaction_metadata?.user_id; + if (id) { + let pinged = mention_everyone || channel.type === ChannelType.DM || channel.type === ChannelType.GROUP_DM; + if (!pinged) pinged = !!message.mentions.find((user) => user.id === id); + if (!pinged) pinged = !!(await Member.find({ where: { id, roles: Or(...message.mention_roles.map(({ id }) => Equal(id))) } })); + if (pinged) { + //stuff + } + } + } else if ((!!message.content?.match(EVERYONE_MENTION) && permission?.has("MENTION_EVERYONE")) || channel.type === ChannelType.DM || channel.type === ChannelType.GROUP_DM) { + if (channel.type === ChannelType.DM || channel.type === ChannelType.GROUP_DM) { + if (channel.recipients) { + await fillInMissingIDs(channel.recipients.map(({ user_id }) => user_id)); + } + } else { + console.log(channel.guild_id); + await fillInMissingIDs((await Member.find({ where: { guild_id: channel.guild_id } })).map(({ id }) => id)); + } + const repository = ReadState.getRepository(); + const condition = { channel_id: channel.id }; + await repository.update({ ...condition, mention_count: IsNull() }, { mention_count: 0 }); + await repository.increment(condition, "mention_count", 1); + } else { + const users = new Set<string>([ + ...(message.mention_roles.length + ? await Member.find({ + where: [ + ...message.mention_roles.map((role) => { + return { roles: { id: role.id } }; + }), + ], + }) + : [] + ).map((member) => member.id), + ...message.mentions.map((user) => user.id), + ]); + if (!!message.content?.match(HERE_MENTION) && permission?.has("MENTION_EVERYONE")) { + const ids = (await Member.find({ where: { guild_id: channel.guild_id } })).map(({ id }) => id); + (await Session.find({ where: { user_id: Or(...ids.map((id) => Equal(id))) } })).forEach(({ user_id }) => users.add(user_id)); + } + if (users.size) { + const repository = ReadState.getRepository(); + const condition = { user_id: Or(...[...users].map((id) => Equal(id))), channel_id: channel.id }; - await fillInMissingIDs([...users]); + await fillInMissingIDs([...users]); - await repository.update({ ...condition, mention_count: IsNull() }, { mention_count: 0 }); - await repository.increment(condition, "mention_count", 1); - } - } + await repository.update({ ...condition, mention_count: IsNull() }, { mention_count: 0 }); + await repository.increment(condition, "mention_count", 1); + } + } - // TODO: check and put it all in the body + // TODO: check and put it all in the body - return message; + return message; } // TODO: cache link result in db export async function postHandleMessage(message: Message) { - const content = message.content?.replace(/ *`[^)]*` */g, ""); // remove markdown + const content = message.content?.replace(/ *`[^)]*` */g, ""); // remove markdown - const linkMatches = content?.match(LINK_REGEX) || []; + const linkMatches = content?.match(LINK_REGEX) || []; - const data = { ...message }; + const data = { ...message }; - const currentNormalizedUrls = new Set<string>(); - for (const link of linkMatches) { - // Don't process links in <> - if (link.startsWith("<") && link.endsWith(">")) { - continue; - } - try { - const normalized = normalizeUrl(link); - currentNormalizedUrls.add(normalized); - } catch (e) { - continue; - } - } + const currentNormalizedUrls = new Set<string>(); + for (const link of linkMatches) { + // Don't process links in <> + if (link.startsWith("<") && link.endsWith(">")) { + continue; + } + try { + const normalized = normalizeUrl(link); + currentNormalizedUrls.add(normalized); + } catch (e) { + continue; + } + } - data.embeds.forEach((embed) => { - if (!embed.type) { - embed.type = EmbedType.rich; - } - }); - // Filter out embeds that could be links, start from scratch - data.embeds = data.embeds.filter((embed) => embed.type === "rich"); + data.embeds.forEach((embed) => { + if (!embed.type) { + embed.type = EmbedType.rich; + } + }); + // Filter out embeds that could be links, start from scratch + data.embeds = data.embeds.filter((embed) => embed.type === "rich"); - const seenNormalizedUrls = new Set<string>(); - const uniqueLinks: string[] = []; + const seenNormalizedUrls = new Set<string>(); + const uniqueLinks: string[] = []; - for (const link of linkMatches.slice(0, 20)) { - // embed max 20 links - TODO: make this configurable with instance policies - // Don't embed links in <> - if (link.startsWith("<") && link.endsWith(">")) continue; + for (const link of linkMatches.slice(0, 20)) { + // embed max 20 links - TODO: make this configurable with instance policies + // Don't embed links in <> + if (link.startsWith("<") && link.endsWith(">")) continue; - try { - const normalized = normalizeUrl(link); + try { + const normalized = normalizeUrl(link); - if (!seenNormalizedUrls.has(normalized)) { - seenNormalizedUrls.add(normalized); - uniqueLinks.push(link); - } - } catch (e) { - // Invalid URL, skip - continue; - } - } + if (!seenNormalizedUrls.has(normalized)) { + seenNormalizedUrls.add(normalized); + uniqueLinks.push(link); + } + } catch (e) { + // Invalid URL, skip + continue; + } + } - if (uniqueLinks.length === 0) { - // No valid unique links found, update message to remove old embeds - data.embeds = data.embeds.filter((embed) => embed.type === "rich"); - const author = data.author?.toPublicUser(); - const event = { - event: "MESSAGE_UPDATE", - channel_id: message.channel_id, - data: { - ...data, - author, - }, - } as MessageUpdateEvent; - await Promise.all([emitEvent(event), Message.update({ id: message.id, channel_id: message.channel_id }, { embeds: data.embeds })]); - return; - } + if (uniqueLinks.length === 0) { + // No valid unique links found, update message to remove old embeds + data.embeds = data.embeds.filter((embed) => embed.type === "rich"); + const author = data.author?.toPublicUser(); + const event = { + event: "MESSAGE_UPDATE", + channel_id: message.channel_id, + data: { + ...data, + author, + }, + } as MessageUpdateEvent; + await Promise.all([emitEvent(event), Message.update({ id: message.id, channel_id: message.channel_id }, { embeds: data.embeds })]); + return; + } - const cachePromises = []; + const cachePromises = []; - for (const link of uniqueLinks) { - let url: URL; - try { - url = new URL(link); - } catch (e) { - // Skip invalid URLs - continue; - } + for (const link of uniqueLinks) { + let url: URL; + try { + url = new URL(link); + } catch (e) { + // Skip invalid URLs + continue; + } - const normalizedUrl = normalizeUrl(link); + const normalizedUrl = normalizeUrl(link); - // Check cache using normalized URL - const cached = await EmbedCache.findOne({ - where: { url: normalizedUrl }, - }); + // Check cache using normalized URL + const cached = await EmbedCache.findOne({ + where: { url: normalizedUrl }, + }); - if (cached) { - data.embeds.push(cached.embed); - continue; - } + if (cached) { + data.embeds.push(cached.embed); + continue; + } - // bit gross, but whatever! - const endpointPublic = Config.get().cdn.endpointPublic; // lol - const handler = url.hostname === new URL(endpointPublic!).hostname ? EmbedHandlers["self"] : EmbedHandlers[url.hostname] || EmbedHandlers["default"]; + // bit gross, but whatever! + const endpointPublic = Config.get().cdn.endpointPublic; // lol + const handler = url.hostname === new URL(endpointPublic!).hostname ? EmbedHandlers["self"] : EmbedHandlers[url.hostname] || EmbedHandlers["default"]; - try { - let res = await handler(url); - if (!res) continue; - // tried to use shorthand but types didn't like me L - if (!Array.isArray(res)) res = [res]; + try { + let res = await handler(url); + if (!res) continue; + // tried to use shorthand but types didn't like me L + if (!Array.isArray(res)) res = [res]; - for (const embed of res) { - // Cache with normalized URL - const cache = EmbedCache.create({ - url: normalizedUrl, - embed: embed, - }); - cachePromises.push(cache.save()); - data.embeds.push(embed); - } - } catch (e) { - console.error(`[Embeds] Error while generating embed for ${link}`, e); - } - } + for (const embed of res) { + // Cache with normalized URL + const cache = EmbedCache.create({ + url: normalizedUrl, + embed: embed, + }); + cachePromises.push(cache.save()); + data.embeds.push(embed); + } + } catch (e) { + console.error(`[Embeds] Error while generating embed for ${link}`, e); + } + } - await Promise.all([ - emitEvent({ - event: "MESSAGE_UPDATE", - channel_id: message.channel_id, - data, - } as MessageUpdateEvent), - Message.update({ id: message.id, channel_id: message.channel_id }, { embeds: data.embeds }), - ...cachePromises, - ]); + await Promise.all([ + emitEvent({ + event: "MESSAGE_UPDATE", + channel_id: message.channel_id, + data, + } as MessageUpdateEvent), + Message.update({ id: message.id, channel_id: message.channel_id }, { embeds: data.embeds }), + ...cachePromises, + ]); } export async function sendMessage(opts: MessageOptions) { - const message = await handleMessage({ ...opts, timestamp: new Date() }); + const message = await handleMessage({ ...opts, timestamp: new Date() }); - const ephemeral = (message.flags & Number(MessageFlags.FLAGS.EPHEMERAL)) !== 0; - await Promise.all([ - Message.insert(message), - emitEvent({ - event: "MESSAGE_CREATE", - ...(ephemeral ? { user_id: message.interaction_metadata?.user_id } : { channel_id: message.channel_id }), - data: message.toJSON(), - } as MessageCreateEvent), - ]); + const ephemeral = (message.flags & Number(MessageFlags.FLAGS.EPHEMERAL)) !== 0; + await Promise.all([ + Message.insert(message), + emitEvent({ + event: "MESSAGE_CREATE", + ...(ephemeral ? { user_id: message.interaction_metadata?.user_id } : { channel_id: message.channel_id }), + data: message.toJSON(), + } as MessageCreateEvent), + ]); - // no await as it should catch error non-blockingly - postHandleMessage(message).catch((e) => console.error("[Message] post-message handler failed", e)); + // no await as it should catch error non-blockingly + postHandleMessage(message).catch((e) => console.error("[Message] post-message handler failed", e)); - return message; + return message; } interface MessageOptions extends MessageCreateSchema { - id?: string; - type?: MessageType; - pinned?: boolean; - author_id?: string; - webhook_id?: string; - application_id?: string; - embeds?: Embed[]; - reactions?: Reaction[]; - channel_id?: string; - attachments?: (MessageCreateAttachment | MessageCreateCloudAttachment | Attachment)[]; // why are we masking this? - edited_timestamp?: Date; - timestamp?: Date; - username?: string; - avatar_url?: string; + id?: string; + type?: MessageType; + pinned?: boolean; + author_id?: string; + webhook_id?: string; + application_id?: string; + embeds?: Embed[]; + reactions?: Reaction[]; + channel_id?: string; + attachments?: (MessageCreateAttachment | MessageCreateCloudAttachment | Attachment)[]; // why are we masking this? + edited_timestamp?: Date; + timestamp?: Date; + username?: string; + avatar_url?: string; } diff --git a/src/api/util/handlers/Voice.ts b/src/api/util/handlers/Voice.ts
index e34b6794..409338f7 100644 --- a/src/api/util/handlers/Voice.ts +++ b/src/api/util/handlers/Voice.ts
@@ -20,31 +20,31 @@ import { Config, IpDataClient } from "@spacebar/util"; import { distanceBetweenLocations } from "../utility/ipAddress"; export async function getVoiceRegions(ipAddress: string, vip: boolean) { - const regions = Config.get().regions; - const availableRegions = regions.available.filter((ar) => (vip ? true : !ar.vip)); - let optimalId = regions.default; + const regions = Config.get().regions; + const availableRegions = regions.available.filter((ar) => (vip ? true : !ar.vip)); + let optimalId = regions.default; - if (!regions.useDefaultAsOptimal) { - const clientIpAnalysis = await IpDataClient.getIpInfo(ipAddress); + if (!regions.useDefaultAsOptimal) { + const clientIpAnalysis = await IpDataClient.getIpInfo(ipAddress); - let min = Number.POSITIVE_INFINITY; + let min = Number.POSITIVE_INFINITY; - for (const ar of availableRegions) { - //TODO the endpoint location should be saved in the database if not already present to prevent IPAnalysis call - const dist = distanceBetweenLocations(clientIpAnalysis!, ar.location || (await IpDataClient.getIpInfo(ar.endpoint))!); + for (const ar of availableRegions) { + //TODO the endpoint location should be saved in the database if not already present to prevent IPAnalysis call + const dist = distanceBetweenLocations(clientIpAnalysis!, ar.location || (await IpDataClient.getIpInfo(ar.endpoint))!); - if (dist < min) { - min = dist; - optimalId = ar.id; - } - } - } + if (dist < min) { + min = dist; + optimalId = ar.id; + } + } + } - return availableRegions.map((ar) => ({ - id: ar.id, - name: ar.name, - custom: ar.custom, - deprecated: ar.deprecated, - optimal: ar.id === optimalId, - })); + return availableRegions.map((ar) => ({ + id: ar.id, + name: ar.name, + custom: ar.custom, + deprecated: ar.deprecated, + optimal: ar.id === optimalId, + })); } diff --git a/src/api/util/handlers/Webhook.ts b/src/api/util/handlers/Webhook.ts
index 54ef26ec..cd0f2f6e 100644 --- a/src/api/util/handlers/Webhook.ts +++ b/src/api/util/handlers/Webhook.ts
@@ -6,118 +6,118 @@ import { MoreThan } from "typeorm"; import { WebhookExecuteSchema } from "@spacebar/schemas"; export const executeWebhook = async (req: Request, res: Response) => { - const body = req.body as WebhookExecuteSchema; + const body = req.body as WebhookExecuteSchema; - const { webhook_id, token } = req.params; + const { webhook_id, token } = req.params; - const webhook = await Webhook.findOne({ - where: { - id: webhook_id, - }, - relations: ["channel", "guild", "application"], - }); + const webhook = await Webhook.findOne({ + where: { + id: webhook_id, + }, + relations: ["channel", "guild", "application"], + }); - if (!webhook) { - throw DiscordApiErrors.UNKNOWN_WEBHOOK; - } + if (!webhook) { + throw DiscordApiErrors.UNKNOWN_WEBHOOK; + } - if (webhook.token !== token) { - throw DiscordApiErrors.INVALID_WEBHOOK_TOKEN_PROVIDED; - } + if (webhook.token !== token) { + throw DiscordApiErrors.INVALID_WEBHOOK_TOKEN_PROVIDED; + } - if (body.username) { - ValidateName(body.username); - } + if (body.username) { + ValidateName(body.username); + } - // ensure one of content, embeds, components, or file is present - if (!body.content && !body.embeds && !body.components && !body.file && !body.attachments) { - throw DiscordApiErrors.CANNOT_SEND_EMPTY_MESSAGE; - } + // ensure one of content, embeds, components, or file is present + if (!body.content && !body.embeds && !body.components && !body.file && !body.attachments) { + throw DiscordApiErrors.CANNOT_SEND_EMPTY_MESSAGE; + } - const wait = req.query.wait === "true"; + const wait = req.query.wait === "true"; - if (!wait) { - res.status(204).send(); - } + if (!wait) { + res.status(204).send(); + } - const attachments: Attachment[] = []; + const attachments: Attachment[] = []; - if (!webhook.channel.isWritable()) { - if (wait) { - throw new HTTPError(`Cannot send messages to channel of type ${webhook.channel.type}`, 400); - } else { - return; - } - } + if (!webhook.channel.isWritable()) { + if (wait) { + throw new HTTPError(`Cannot send messages to channel of type ${webhook.channel.type}`, 400); + } else { + return; + } + } - // TODO: creating messages by users checks if the user can bypass rate limits, we cant do that on webhooks, but maybe we could check the application if there is one? - const limits = Config.get().limits; - if (limits.absoluteRate.register.enabled) { - const count = await Message.count({ - where: { - channel_id: webhook.channel_id, - timestamp: MoreThan(new Date(Date.now() - limits.absoluteRate.sendMessage.window)), - }, - }); + // TODO: creating messages by users checks if the user can bypass rate limits, we cant do that on webhooks, but maybe we could check the application if there is one? + const limits = Config.get().limits; + if (limits.absoluteRate.register.enabled) { + const count = await Message.count({ + where: { + channel_id: webhook.channel_id, + timestamp: MoreThan(new Date(Date.now() - limits.absoluteRate.sendMessage.window)), + }, + }); - if (count >= limits.absoluteRate.sendMessage.limit) - if (wait) { - throw FieldErrors({ - channel_id: { - code: "TOO_MANY_MESSAGES", - message: req.t("common:toomany.MESSAGE"), - }, - }); - } else { - return; - } - } + if (count >= limits.absoluteRate.sendMessage.limit) + if (wait) { + throw FieldErrors({ + channel_id: { + code: "TOO_MANY_MESSAGES", + message: req.t("common:toomany.MESSAGE"), + }, + }); + } else { + return; + } + } - const files = (req.files as Express.Multer.File[]) ?? []; - for (const currFile of files) { - try { - const file = await uploadFile(`/attachments/${webhook.channel.id}`, currFile); - attachments.push(Attachment.create({ ...file, proxy_url: file.url })); - } catch (error) { - if (wait) res.status(400).json({ message: error?.toString() }); - return; - } - } + const files = (req.files as Express.Multer.File[]) ?? []; + for (const currFile of files) { + try { + const file = await uploadFile(`/attachments/${webhook.channel.id}`, currFile); + attachments.push(Attachment.create({ ...file, proxy_url: file.url })); + } catch (error) { + if (wait) res.status(400).json({ message: error?.toString() }); + return; + } + } - const embeds = body.embeds || []; - const message = await handleMessage({ - ...body, - username: body.username || webhook.name, - avatar_url: body.avatar_url || webhook.avatar, - type: 0, - pinned: false, - webhook_id: webhook.id, - application_id: webhook.application?.id, - embeds, - // TODO: Support thread_id/thread_name once threads are implemented - channel_id: webhook.channel_id, - attachments, - timestamp: new Date(), - }); + const embeds = body.embeds || []; + const message = await handleMessage({ + ...body, + username: body.username || webhook.name, + avatar_url: body.avatar_url || webhook.avatar, + type: 0, + pinned: false, + webhook_id: webhook.id, + application_id: webhook.application?.id, + embeds, + // TODO: Support thread_id/thread_name once threads are implemented + channel_id: webhook.channel_id, + attachments, + timestamp: new Date(), + }); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore dont care2 - message.edited_timestamp = null; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore dont care2 + message.edited_timestamp = null; - webhook.channel.last_message_id = message.id; + webhook.channel.last_message_id = message.id; - await Promise.all([ - message.save(), - webhook.channel.save(), - emitEvent({ - event: "MESSAGE_CREATE", - channel_id: webhook.channel_id, - data: message, - } as MessageCreateEvent), - ]); + await Promise.all([ + message.save(), + webhook.channel.save(), + emitEvent({ + event: "MESSAGE_CREATE", + channel_id: webhook.channel_id, + data: message, + } as MessageCreateEvent), + ]); - // no await as it shouldnt block the message send function and silently catch error - postHandleMessage(message).catch((e) => console.error("[Message] post-message handler failed", e)); - if (wait) res.json(message); - return; + // no await as it shouldnt block the message send function and silently catch error + postHandleMessage(message).catch((e) => console.error("[Message] post-message handler failed", e)); + if (wait) res.json(message); + return; }; diff --git a/src/api/util/handlers/route.ts b/src/api/util/handlers/route.ts
index bb38c530..7ae00a55 100644 --- a/src/api/util/handlers/route.ts +++ b/src/api/util/handlers/route.ts
@@ -22,107 +22,107 @@ import { NextFunction, Request, Response } from "express"; import { ajv } from "@spacebar/schemas"; const ignoredRequestSchemas = [ - // skip validation for settings proto JSON updates - TODO: figure out if this even possible to fix? - "SettingsProtoUpdateJsonSchema", + // skip validation for settings proto JSON updates - TODO: figure out if this even possible to fix? + "SettingsProtoUpdateJsonSchema", ]; declare global { - // TODO: fix this - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace Express { - interface Request { - permission?: Permissions; - } - } + // TODO: fix this + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Express { + interface Request { + permission?: Permissions; + } + } } export type RouteResponse = { - status?: number; - body?: `${string}Response`; - headers?: Record<string, string>; + status?: number; + body?: `${string}Response`; + headers?: Record<string, string>; }; export interface RouteOptions { - permission?: PermissionResolvable; - right?: RightResolvable; - requestBody?: `${string}Schema`; // typescript interface name - responses?: { - [status: number]: { - // body?: `${string}Response`; - body?: string; - }; - }; - event?: EVENT | EVENT[]; - summary?: string; - description?: string; - query?: { - [key: string]: { - type: string; - required?: boolean; - description?: string; - values?: string[]; - }; - }; - deprecated?: boolean; - // test?: { - // response?: RouteResponse; - // body?: unknown; - // path?: string; - // event?: EVENT | EVENT[]; - // headers?: Record<string, string>; - // }; + permission?: PermissionResolvable; + right?: RightResolvable; + requestBody?: `${string}Schema`; // typescript interface name + responses?: { + [status: number]: { + // body?: `${string}Response`; + body?: string; + }; + }; + event?: EVENT | EVENT[]; + summary?: string; + description?: string; + query?: { + [key: string]: { + type: string; + required?: boolean; + description?: string; + values?: string[]; + }; + }; + deprecated?: boolean; + // test?: { + // response?: RouteResponse; + // body?: unknown; + // path?: string; + // event?: EVENT | EVENT[]; + // headers?: Record<string, string>; + // }; } export function route(opts: RouteOptions) { - let validate: AnyValidateFunction | undefined; - if (opts.requestBody) { - try { - validate = ajv.getSchema(opts.requestBody); - } catch (e) { - console.error("AJV getSchema failed!"); - throw e; - } + let validate: AnyValidateFunction | undefined; + if (opts.requestBody) { + try { + validate = ajv.getSchema(opts.requestBody); + } catch (e) { + console.error("AJV getSchema failed!"); + throw e; + } - if (!validate) throw new Error(`Body schema ${opts.requestBody} not found`); - } + if (!validate) throw new Error(`Body schema ${opts.requestBody} not found`); + } - return async (req: Request, res: Response, next: NextFunction) => { - if (opts.permission) { - req.permission = await getPermission(req.user_id, req.params.guild_id, req.params.channel_id); + return async (req: Request, res: Response, next: NextFunction) => { + if (opts.permission) { + req.permission = await getPermission(req.user_id, req.params.guild_id, req.params.channel_id); - const requiredPerms = Array.isArray(opts.permission) ? opts.permission : [opts.permission]; - requiredPerms.forEach((perm) => { - // bitfield comparison: check if user lacks certain permission - if (!req.permission!.has(new Permissions(perm))) { - throw DiscordApiErrors.MISSING_PERMISSIONS.withParams(perm as string); - } - }); - } + const requiredPerms = Array.isArray(opts.permission) ? opts.permission : [opts.permission]; + requiredPerms.forEach((perm) => { + // bitfield comparison: check if user lacks certain permission + if (!req.permission!.has(new Permissions(perm))) { + throw DiscordApiErrors.MISSING_PERMISSIONS.withParams(perm as string); + } + }); + } - if (opts.right) { - const required = new Rights(opts.right); - req.rights = await getRights(req.user_id); + if (opts.right) { + const required = new Rights(opts.right); + req.rights = await getRights(req.user_id); - if (!req.rights || !req.rights.has(required)) { - throw SpacebarApiErrors.MISSING_RIGHTS.withParams(opts.right as string); - } - } + if (!req.rights || !req.rights.has(required)) { + throw SpacebarApiErrors.MISSING_RIGHTS.withParams(opts.right as string); + } + } - if (validate && !ignoredRequestSchemas.includes(opts.requestBody!)) { - const valid = validate(req.body); - if (!valid) { - const fields: Record<string, { code?: string; message: string }> = {}; - validate.errors?.forEach( - (x) => - (fields[x.instancePath.slice(1)] = { - code: x.keyword, - message: x.message || "", - }), - ); - if (process.env.LOG_VALIDATION_ERRORS) console.log(`[VALIDATION ERROR] ${req.method} ${req.originalUrl} - SCHEMA='${opts.requestBody}' -`, validate?.errors); - throw FieldErrors(fields, validate.errors!); - } - } - next(); - }; + if (validate && !ignoredRequestSchemas.includes(opts.requestBody!)) { + const valid = validate(req.body); + if (!valid) { + const fields: Record<string, { code?: string; message: string }> = {}; + validate.errors?.forEach( + (x) => + (fields[x.instancePath.slice(1)] = { + code: x.keyword, + message: x.message || "", + }), + ); + if (process.env.LOG_VALIDATION_ERRORS) console.log(`[VALIDATION ERROR] ${req.method} ${req.originalUrl} - SCHEMA='${opts.requestBody}' -`, validate?.errors); + throw FieldErrors(fields, validate.errors!); + } + } + next(); + }; } 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; }