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;
}
|