diff --git a/assets/openapi.json b/assets/openapi.json index c5e927640..82b3ff3a3 100644 Binary files a/assets/openapi.json and b/assets/openapi.json differ diff --git a/assets/schemas.json b/assets/schemas.json index e53b7b527..e4a034d2f 100644 Binary files a/assets/schemas.json and b/assets/schemas.json differ diff --git a/scripts/openapi.js b/scripts/openapi.js index 589614cc2..a1188c541 100644 --- a/scripts/openapi.js +++ b/scripts/openapi.js @@ -20,9 +20,7 @@ require("module-alias/register"); const getRouteDescriptions = require("./util/getRouteDescriptions"); const path = require("path"); const fs = require("fs"); -const { - NO_AUTHORIZATION_ROUTES, -} = require("../dist/api/middlewares/Authentication"); +const { NO_AUTHORIZATION_ROUTES } = require("../dist/api/middlewares/Authentication"); require("../dist/util/util/extensions"); const { bgRedBright } = require("picocolors"); @@ -89,8 +87,7 @@ function combineSchemas(schemas) { continue; } specification.components = specification.components || {}; - specification.components.schemas = - specification.components.schemas || {}; + specification.components.schemas = specification.components.schemas || {}; specification.components.schemas[key] = definitions[key]; delete definitions[key].additionalProperties; delete definitions[key].$schema; @@ -122,7 +119,7 @@ function apiRoutes(missingRoutes) { const tags = Array.from(routes.keys()) .map((x) => getTag(x)) .sort((a, b) => a.localeCompare(b)); - specification.tags = tags.distinct().map((x) => ({ name: x })); + specification.tags = [...new Set(tags)].map((x) => ({ name: x })); routes.forEach((route, pathAndMethod) => { const [p, method] = pathAndMethod.split("|"); @@ -135,8 +132,7 @@ function apiRoutes(missingRoutes) { if ( !NO_AUTHORIZATION_ROUTES.some((x) => { - if (typeof x === "string") - return (method.toUpperCase() + " " + path).startsWith(x); + if (typeof x === "string") return (method.toUpperCase() + " " + path).startsWith(x); return x.test(method.toUpperCase() + " " + path); }) ) { @@ -177,9 +173,7 @@ function apiRoutes(missingRoutes) { }; else obj.responses[k] = { - description: - obj?.responses?.[k]?.description || - "No description available", + description: obj?.responses?.[k]?.description || "No description available", }; } } else { @@ -214,7 +208,7 @@ function apiRoutes(missingRoutes) { obj.parameters = [...(obj.parameters || []), ...query]; } - obj.tags = [...(obj.tags || []), getTag(p)].distinct(); + obj.tags = [...new Set([...(obj.tags || []), getTag(p)])]; if (missingRoutes.additional.includes(path.replace(/\/$/, ""))) { obj["x-badges"] = [ @@ -225,45 +219,28 @@ function apiRoutes(missingRoutes) { ]; } - specification.paths[path] = Object.assign( - specification.paths[path] || {}, - { - [method]: obj, - }, - ); + specification.paths[path] = Object.assign(specification.paths[path] || {}, { + [method]: obj, + }); }); } async function main() { console.log("Generating OpenAPI Specification..."); - const routesRes = await fetch( - "https://github.com/spacebarchat/missing-routes/raw/main/missing.json", - { - headers: { - Accept: "application/json", - }, + const routesRes = await fetch("https://github.com/spacebarchat/missing-routes/raw/main/missing.json", { + headers: { + Accept: "application/json", }, - ); + }); const missingRoutes = await routesRes.json(); combineSchemas(schemas); apiRoutes(missingRoutes); - fs.writeFileSync( - openapiPath, - JSON.stringify(specification, null, 4) - .replaceAll("#/definitions", "#/components/schemas") - .replaceAll("bigint", "number"), - ); + fs.writeFileSync(openapiPath, JSON.stringify(specification, null, 4).replaceAll("#/definitions", "#/components/schemas").replaceAll("bigint", "number")); console.log("Wrote OpenAPI specification to", openapiPath); - console.log( - "Specification contains", - Object.keys(specification.paths).length, - "paths and", - Object.keys(specification.components.schemas).length, - "schemas.", - ); + console.log("Specification contains", Object.keys(specification.paths).length, "paths and", Object.keys(specification.components.schemas).length, "schemas."); } main(); diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts index fe2a25098..63c41cd7e 100644 --- a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts @@ -29,6 +29,7 @@ import { MessageReactionRemoveEmojiEvent, MessageReactionRemoveEvent, User, + arrayRemove, } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; @@ -112,7 +113,7 @@ router.delete( const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name); if (!already_added) throw new HTTPError("Reaction not found", 404); - message.reactions.remove(already_added); + arrayRemove(message.reactions, already_added); await Promise.all([ message.save(), @@ -283,7 +284,7 @@ router.delete( already_added.count--; - if (already_added.count <= 0) message.reactions.remove(already_added); + if (already_added.count <= 0) arrayRemove(message.reactions, already_added); else already_added.user_ids.splice(already_added.user_ids.indexOf(user_id), 1); await message.save(); @@ -340,7 +341,7 @@ router.delete( already_added.count--; - if (already_added.count <= 0) message.reactions.remove(already_added); + if (already_added.count <= 0) arrayRemove(message.reactions, already_added); else already_added.user_ids.splice(already_added.user_ids.indexOf(user_id), 1); await message.save(); diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts index d0d445b9d..d399e53af 100644 --- a/src/api/routes/channels/#channel_id/messages/index.ts +++ b/src/api/routes/channels/#channel_id/messages/index.ts @@ -41,6 +41,7 @@ import { Snowflake, uploadFile, User, + stringGlobToRegexp, } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; @@ -244,24 +245,28 @@ router.get( return x; }); - await ret - .filter((x: MessageCreateSchema) => x.interaction_metadata && !x.interaction_metadata.user) - .forEachAsync(async (x: MessageCreateSchema) => { - x.interaction_metadata!.user = x.interaction!.user = await User.findOneOrFail({ where: { id: (x as Message).interaction_metadata!.user_id } }); - }); + await Promise.all( + ret + .filter((x: MessageCreateSchema) => x.interaction_metadata && !x.interaction_metadata.user) + .map(async (x: MessageCreateSchema) => { + x.interaction_metadata!.user = x.interaction!.user = await User.findOneOrFail({ where: { id: (x as Message).interaction_metadata!.user_id } }); + }), + ); // polyfill message references for old messages - await ret - .filter((msg) => msg.message_reference && !msg.referenced_message?.id) - .forEachAsync(async (msg) => { - const whereOptions: { id: string; guild_id?: string; channel_id?: string } = { - id: msg.message_reference!.message_id, - }; - if (msg.message_reference!.guild_id) whereOptions.guild_id = msg.message_reference!.guild_id; - if (msg.message_reference!.channel_id) whereOptions.channel_id = msg.message_reference!.channel_id; + await Promise.all( + ret + .filter((msg) => msg.message_reference && !msg.referenced_message?.id) + .map(async (msg) => { + const whereOptions: { id: string; guild_id?: string; channel_id?: string } = { + id: msg.message_reference!.message_id, + }; + if (msg.message_reference!.guild_id) whereOptions.guild_id = msg.message_reference!.guild_id; + if (msg.message_reference!.channel_id) whereOptions.channel_id = msg.message_reference!.channel_id; - msg.referenced_message = await Message.findOne({ where: whereOptions, relations: ["author", "mentions", "mention_roles", "mention_channels"] }); - }); + msg.referenced_message = await Message.findOne({ where: whereOptions, relations: ["author", "mentions", "mention_roles", "mention_channels"] }); + }), + ); return res.json(ret); }, @@ -449,8 +454,8 @@ router.post( if (rule.trigger_type == AutomodTriggerTypes.CUSTOM_WORDS) { const triggerMeta = rule.trigger_metadata as AutomodCustomWordsRule; - const regexes = triggerMeta.regex_patterns.map((x) => new RegExp(x, "i")).concat(triggerMeta.keyword_filter.map((k) => k.globToRegexp("i"))); - const allowedRegexes = triggerMeta.allow_list.map((k) => k.globToRegexp("i")); + const regexes = triggerMeta.regex_patterns.map((x) => new RegExp(x, "i")).concat(triggerMeta.keyword_filter.map((k) => stringGlobToRegexp(k, "i"))); + const allowedRegexes = triggerMeta.allow_list.map((k) => stringGlobToRegexp(k, "i")); const matches = regexes .map((r) => message.content!.match(r)) diff --git a/src/api/routes/channels/#channel_id/recipients.ts b/src/api/routes/channels/#channel_id/recipients.ts index 65160274d..6388cfe21 100644 --- a/src/api/routes/channels/#channel_id/recipients.ts +++ b/src/api/routes/channels/#channel_id/recipients.ts @@ -17,15 +17,7 @@ */ import { route } from "@spacebar/api"; -import { - Channel, - ChannelRecipientAddEvent, - DiscordApiErrors, - DmChannelDTO, - emitEvent, - Recipient, - User, -} from "@spacebar/util"; +import { Channel, ChannelRecipientAddEvent, DiscordApiErrors, DmChannelDTO, emitEvent, Recipient, User } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { ChannelType, PublicUserProjection } from "@spacebar/schemas"; @@ -47,24 +39,16 @@ router.put( }); if (channel.type !== ChannelType.GROUP_DM) { - const recipients = [ - ...(channel.recipients?.map((r) => r.user_id) || []), - user_id, - ].distinct(); + const recipients = [...new Set([...(channel.recipients?.map((r) => r.user_id) || []), user_id])]; - const new_channel = await Channel.createDMChannel( - recipients, - req.user_id, - ); + const new_channel = await Channel.createDMChannel(recipients, req.user_id); return res.status(201).json(new_channel); } else { if (channel.recipients?.map((r) => r.user_id).includes(user_id)) { throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error? } - channel.recipients?.push( - Recipient.create({ channel_id: channel_id, user_id: user_id }), - ); + channel.recipients?.push(Recipient.create({ channel_id: channel_id, user_id: user_id })); await channel.save(); await emitEvent({ @@ -103,13 +87,7 @@ router.delete( where: { id: channel_id }, relations: ["recipients"], }); - if ( - !( - channel.type === ChannelType.GROUP_DM && - (channel.owner_id === req.user_id || user_id === req.user_id) - ) - ) - throw DiscordApiErrors.MISSING_PERMISSIONS; + if (!(channel.type === ChannelType.GROUP_DM && (channel.owner_id === req.user_id || user_id === req.user_id))) throw DiscordApiErrors.MISSING_PERMISSIONS; if (!channel.recipients?.map((r) => r.user_id).includes(user_id)) { throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error? diff --git a/src/api/routes/guilds/#guild_id/invites.ts b/src/api/routes/guilds/#guild_id/invites.ts index ec3116c95..9372651ae 100644 --- a/src/api/routes/guilds/#guild_id/invites.ts +++ b/src/api/routes/guilds/#guild_id/invites.ts @@ -40,11 +40,13 @@ router.get( relations: PublicInviteRelation, }); - await invites - .filter((i) => i.isExpired()) - .forEachAsync(async (i) => { - await Invite.delete({ code: i.code }); - }); + await Promise.all( + invites + .filter((i) => i.isExpired()) + .map(async (i) => { + await Invite.delete({ code: i.code }); + }), + ); return res.json(invites.filter((i) => !i.isExpired())); }, diff --git a/src/api/routes/guilds/#guild_id/roles/#role_id/members.ts b/src/api/routes/guilds/#guild_id/roles/#role_id/members.ts index 4cfe21b95..d22d14347 100644 --- a/src/api/routes/guilds/#guild_id/roles/#role_id/members.ts +++ b/src/api/routes/guilds/#guild_id/roles/#role_id/members.ts @@ -17,45 +17,30 @@ */ import { Router, Request, Response } from "express"; -import { DiscordApiErrors, Member } from "@spacebar/util"; +import { DiscordApiErrors, Member, arrayPartition } from "@spacebar/util"; import { route } from "@spacebar/api"; const router = Router({ mergeParams: true }); -router.patch( - "/", - route({ permission: "MANAGE_ROLES" }), - async (req: Request, res: Response) => { - // Payload is JSON containing a list of member_ids, the new list of members to have the role - const { guild_id, role_id } = req.params; - const { member_ids } = req.body; +router.patch("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { + // Payload is JSON containing a list of member_ids, the new list of members to have the role + const { guild_id, role_id } = req.params; + const { member_ids } = req.body; - // don't mess with @everyone - if (role_id == guild_id) throw DiscordApiErrors.INVALID_ROLE; + // don't mess with @everyone + if (role_id == guild_id) throw DiscordApiErrors.INVALID_ROLE; - const members = await Member.find({ - where: { guild_id }, - relations: ["roles"], - }); + const members = await Member.find({ + where: { guild_id }, + relations: ["roles"], + }); - const [add, remove] = members.partition( - (member) => - member_ids.includes(member.id) && - !member.roles.map((role) => role.id).includes(role_id), - ); + const [add, remove] = arrayPartition(members, (member) => member_ids.includes(member.id) && !member.roles.map((role) => role.id).includes(role_id)); - // TODO (erkin): have a bulk add/remove function that adds the roles in a single txn - await Promise.all([ - ...add.map((member) => - Member.addRole(member.id, guild_id, role_id), - ), - ...remove.map((member) => - Member.removeRole(member.id, guild_id, role_id), - ), - ]); + // TODO (erkin): have a bulk add/remove function that adds the roles in a single txn + await Promise.all([...add.map((member) => Member.addRole(member.id, guild_id, role_id)), ...remove.map((member) => Member.removeRole(member.id, guild_id, role_id))]); - res.sendStatus(204); - }, -); + res.sendStatus(204); +}); export default router; diff --git a/src/api/routes/users/#user_id/messages.ts b/src/api/routes/users/#user_id/messages.ts index 9ce0b3699..717bca96d 100644 --- a/src/api/routes/users/#user_id/messages.ts +++ b/src/api/routes/users/#user_id/messages.ts @@ -19,7 +19,7 @@ import { route } from "@spacebar/api"; import { Config, Message, User } from "@spacebar/util"; import { Request, Response, Router } from "express"; -import { DmMessagesResponseSchema } from "@spacebar/schemas" +import { DmMessagesResponseSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); router.get( @@ -42,7 +42,7 @@ router.get( await Message.find({ where: { channel_id: channel?.id }, order: { timestamp: "DESC" }, - take: Math.clamp(req.query.limit ? Number(req.query.limit) : 50, 1, Config.get().limits.message.maxPreloadCount), + take: Math.min(Math.max(req.query.limit ? Number(req.query.limit) : 50, 1), Config.get().limits.message.maxPreloadCount), }) ).filter((x) => x !== null) as Message[]; diff --git a/src/api/routes/users/@me/mentions.ts b/src/api/routes/users/@me/mentions.ts index cb2aac33e..37a7abe16 100644 --- a/src/api/routes/users/@me/mentions.ts +++ b/src/api/routes/users/@me/mentions.ts @@ -19,7 +19,7 @@ import { route } from "@spacebar/api"; import { Snowflake, User, Message, Member, Channel, Permissions, timePromise, NewUrlUserSignatureData, Stopwatch, Attachment } from "@spacebar/util"; import { Request, Response, Router } from "express"; -import { In, LessThan } from "typeorm"; +import { In, LessThan, FindOptionsWhere } from "typeorm"; const router: Router = Router({ mergeParams: true }); @@ -70,7 +70,7 @@ router.get( const channels = await Channel.find({ where: { - guild_id: In(memberships.map((m) => m.guild_id).distinct()), + guild_id: In(memberships.map((m) => m.guild_id)), }, select: { id: true, guild_id: true, permission_overwrites: true }, }); @@ -78,7 +78,7 @@ router.get( const visibleChannels = channels.filter((c) => { const member = memberships.find((m) => m.guild_id === c.guild_id)!; return Permissions.finalPermission({ - user: { id: member.id, roles: member.roles.map((r) => r.id).distinct(), communication_disabled_until: member.communication_disabled_until, flags: 0 }, + user: { id: member.id, roles: member.roles.map((r) => r.id), communication_disabled_until: member.communication_disabled_until, flags: 0 }, guild: { id: member.guild.id, owner_id: member.guild.owner_id!, roles: member.roles }, channel: c, }).has("VIEW_CHANNEL"); @@ -90,81 +90,32 @@ router.get( return acc; }, [] as Snowflake[]); - const [ - { result: userMentions, elapsed: userMentionQueryTime }, - { result: roleMentions, elapsed: roleMentionQueryTime }, - { result: everyoneMentions, elapsed: everyoneMentionQueryTime }, - ] = await Promise.all([ - await timePromise(() => - Message.find({ - where: { - channel_id: In(visibleChannelIds), - mentions: { id: user.id }, - ...(before === undefined ? {} : { id: LessThan(before) }), - }, - select: { - id: true, - timestamp: true, - }, - order: { - timestamp: "DESC", - }, - take: limit, - }), - ), - await timePromise(() => - !roles - ? Promise.resolve([]) - : Message.find({ - where: { - channel_id: In(visibleChannelIds), - mention_roles: { id: In(ownedMentionableRoleIds) }, - ...(before === undefined ? {} : { id: LessThan(before) }), - }, - select: { - id: true, - timestamp: true, - }, - order: { - timestamp: "DESC", - }, - take: limit, - }), - ), - await timePromise(() => - !everyone - ? Promise.resolve([]) - : Message.find({ - where: { - channel_id: In(visibleChannelIds), - mention_everyone: true, - ...(before === undefined ? {} : { id: LessThan(before) }), - }, - select: { - id: true, - timestamp: true, - }, - order: { - timestamp: "DESC", - }, - take: limit, - }), - ), - ]); - - const allMentions = [...userMentions, ...roleMentions, ...everyoneMentions]; - console.log( - `[Inbox/mentions] User ${user.id} query results: totalRecs=${allMentions.length} | user=${userMentions.length} (took ${userMentionQueryTime.totalMilliseconds}ms), role=${roleMentions.length} (took ${roleMentionQueryTime.totalMilliseconds}ms), everyone=${everyoneMentions.length} (took ${everyoneMentionQueryTime.totalMilliseconds}ms)`, - ); - const messageIdsToReturn = allMentions - .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) - .distinctBy((m) => m.id) - .slice(0, limit); + const whereQuery: FindOptionsWhere[] = [ + { + channel_id: In(visibleChannelIds), + mentions: { id: user.id }, + id: before ? LessThan(before) : undefined, + }, + ]; + if (everyone) { + whereQuery.push({ + channel_id: In(visibleChannelIds), + mention_everyone: true, + id: before ? LessThan(before) : undefined, + }); + } + if (roles) { + whereQuery.push({ + channel_id: In(visibleChannelIds), + mention_roles: { id: In(ownedMentionableRoleIds) }, + id: before ? LessThan(before) : undefined, + }); + } const sw = Stopwatch.startNew(); const finalMessages = ( await Message.find({ - where: { id: In(messageIdsToReturn.map((m) => m.id)) }, + where: whereQuery, order: { timestamp: "DESC" }, relations: [ "author", @@ -185,6 +136,7 @@ router.get( "referenced_message.sticker_items", "referenced_message.attachments", ], + take: limit, }) ).map((m) => { return { diff --git a/src/api/routes/users/@me/settings.ts b/src/api/routes/users/@me/settings.ts index b265bc5f6..54a6539e0 100644 --- a/src/api/routes/users/@me/settings.ts +++ b/src/api/routes/users/@me/settings.ts @@ -67,7 +67,7 @@ router.patch( relations: ["settings"], }); - if (!user.settings) user.settings = UserSettings.create(body as UserSettingsUpdateSchema); + if (!user.settings) user.settings = UserSettings.create(body); else user.settings.assign(body); if (body.guild_folders) user.settings.guild_folders = body.guild_folders; diff --git a/src/gateway/events/Close.ts b/src/gateway/events/Close.ts index dbbc41d8a..909ede968 100644 --- a/src/gateway/events/Close.ts +++ b/src/gateway/events/Close.ts @@ -17,16 +17,7 @@ */ import { WebSocket } from "@spacebar/gateway"; -import { - emitEvent, - PresenceUpdateEvent, - PrivateSessionProjection, - Session, - SessionsReplace, - User, - VoiceState, - VoiceStateUpdateEvent, -} from "@spacebar/util"; +import { emitEvent, PresenceUpdateEvent, PrivateSessionProjection, Session, SessionsReplace, User, VoiceState, VoiceStateUpdateEvent } from "@spacebar/util"; export async function Close(this: WebSocket, code: number, reason: Buffer) { console.log("[WebSocket] closed", code, reason.toString()); @@ -44,11 +35,7 @@ export async function Close(this: WebSocket, code: number, reason: Buffer) { }); // clear the voice state for this session if user was in voice channel - if ( - voiceState && - voiceState.session_id === this.session_id && - voiceState.channel_id - ) { + if (voiceState && voiceState.session_id === this.session_id && voiceState.channel_id) { const prevGuildId = voiceState.guild_id; const prevChannelId = voiceState.channel_id; @@ -83,7 +70,7 @@ export async function Close(this: WebSocket, code: number, reason: Buffer) { user_id: this.user_id, data: sessions, } as SessionsReplace); - const session = sessions.first() || { + const session = sessions[0] || { activities: [], client_status: {}, status: "offline", diff --git a/src/gateway/opcodes/LazyRequest.ts b/src/gateway/opcodes/LazyRequest.ts index 94dc7517b..c3c74990e 100644 --- a/src/gateway/opcodes/LazyRequest.ts +++ b/src/gateway/opcodes/LazyRequest.ts @@ -16,28 +16,11 @@ along with this program. If not, see . */ -import { - getDatabase, - getPermission, - listenEvent, - Member, - Role, - Session, - User, - Presence, - Channel, - Permissions, -} from "@spacebar/util"; -import { - WebSocket, - Payload, - handlePresenceUpdate, - OPCODES, - Send, -} from "@spacebar/gateway"; +import { getDatabase, getPermission, listenEvent, Member, Role, Session, User, Presence, Channel, Permissions, arrayPartition } from "@spacebar/util"; +import { WebSocket, Payload, handlePresenceUpdate, OPCODES, Send } from "@spacebar/gateway"; import murmur from "murmurhash-js/murmurhash3_gc"; import { check } from "./instanceOf"; -import { LazyRequestSchema } from "@spacebar/schemas" +import { LazyRequestSchema } from "@spacebar/schemas"; // TODO: only show roles/members that have access to this channel // TODO: config: to list all members (even those who are offline) sorted by role, or just those who are online @@ -53,14 +36,10 @@ const getMostRelevantSession = (sessions: Session[]) => { }; // sort sessions by relevance sessions = sessions.sort((a, b) => { - return ( - statusMap[a.status] - - statusMap[b.status] + - ((a.activities?.length ?? 0) - (b.activities?.length ?? 0)) * 2 - ); + return statusMap[a.status] - statusMap[b.status] + ((a.activities?.length ?? 0) - (b.activities?.length ?? 0)) * 2; }); - return sessions.first(); + return sessions[0]; }; async function getMembers(guild_id: string, range: [number, number]) { @@ -79,10 +58,7 @@ async function getMembers(guild_id: string, range: [number, number]) { .leftJoinAndSelect("member.user", "user") .leftJoinAndSelect("user.sessions", "session") .addSelect("user.settings") - .addSelect( - "CASE WHEN session.status IS NULL OR session.status = 'offline' OR session.status = 'invisible' THEN 0 ELSE 1 END", - "_status", - ) + .addSelect("CASE WHEN session.status IS NULL OR session.status = 'offline' OR session.status = 'invisible' THEN 0 ELSE 1 END", "_status") .orderBy("_status", "DESC") .addOrderBy("role.position", "DESC") .addOrderBy("user.username", "ASC") @@ -104,10 +80,14 @@ async function getMembers(guild_id: string, range: [number, number]) { const groups = []; const items = []; - const member_roles = members - .map((m) => m.roles) - .flat() - .distinctBy((r: Role) => r.id); + const member_roles = [ + ...new Map( + members + .map((m) => m.roles) + .flat() + .map((role) => [role.id, role] as [string, Role]), + ).values(), + ]; member_roles.push( member_roles.splice( member_roles.findIndex((x) => x.id === x.guild_id), @@ -118,9 +98,7 @@ async function getMembers(guild_id: string, range: [number, number]) { const offlineItems = []; for (const role of member_roles) { - const [role_members, other_members] = members.partition( - (m: Member) => !!m.roles.find((r) => r.id === role.id), - ); + const [role_members, other_members] = arrayPartition(members, (m: Member) => !!m.roles.find((r) => r.id === role.id)); const group = { count: role_members.length, id: role.id === guild_id ? "online" : role.id, @@ -130,13 +108,9 @@ async function getMembers(guild_id: string, range: [number, number]) { groups.push(group); for (const member of role_members) { - const roles = member.roles - .filter((x: Role) => x.id !== guild_id) - .map((x: Role) => x.id); + const roles = member.roles.filter((x: Role) => x.id !== guild_id).map((x: Role) => x.id); - const session: Session | undefined = getMostRelevantSession( - member.user.sessions, - ); + const session: Session | undefined = getMostRelevantSession(member.user.sessions); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -157,11 +131,7 @@ async function getMembers(guild_id: string, range: [number, number]) { }, }; - if ( - !session || - session.status == "invisible" || - session.status == "offline" - ) { + if (!session || session.status == "invisible" || session.status == "offline") { item.member.presence.status = "offline"; offlineItems.push(item); group.count--; @@ -188,24 +158,14 @@ async function getMembers(guild_id: string, range: [number, number]) { items, groups, range, - members: items - .map((x) => - "member" in x - ? { ...x.member, settings: undefined } - : undefined, - ) - .filter((x) => !!x), + members: items.map((x) => ("member" in x ? { ...x.member, settings: undefined } : undefined)).filter((x) => !!x), }; } async function subscribeToMemberEvents(this: WebSocket, user_id: string) { if (this.events[user_id]) return false; // already subscribed as friend if (this.member_events[user_id]) return false; // already subscribed in member list - this.member_events[user_id] = await listenEvent( - user_id, - handlePresenceUpdate.bind(this), - this.listen_options, - ); + this.member_events[user_id] = await listenEvent(user_id, handlePresenceUpdate.bind(this), this.listen_options); return true; } @@ -213,8 +173,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) { const startTime = Date.now(); // TODO: check data check.call(this, LazyRequestSchema, d); - const { guild_id, typing, channels, activities, members } = - d as LazyRequestSchema; + const { guild_id, typing, channels, activities, members } = d as LazyRequestSchema; if (members) { // Client has requested a PRESENCE_UPDATE for specific member @@ -222,10 +181,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) { await Promise.all([ members.map(async (x) => { if (!x) return; - const didSubscribe = await subscribeToMemberEvents.call( - this, - x, - ); + const didSubscribe = await subscribeToMemberEvents.call(this, x); if (!didSubscribe) return; // if we didn't subscribe just now, this is a new subscription @@ -257,7 +213,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) { if (!channels) throw new Error("Must provide channel ranges"); - const channel_id = Object.keys(channels || {}).first(); + const channel_id = Object.keys(channels || {})[0]; if (!channel_id) return; const permissions = await getPermission(this.user_id, guild_id, channel_id); @@ -267,9 +223,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) { if (!Array.isArray(ranges)) throw new Error("Not a valid Array"); const member_count = await Member.count({ where: { guild_id } }); - const ops = await Promise.all( - ranges.map((x) => getMembers(guild_id, x as [number, number])), - ); + const ops = await Promise.all(ranges.map((x) => getMembers(guild_id, x as [number, number]))); let list_id = "everyone"; @@ -282,10 +236,8 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) { channel.permission_overwrites.forEach((overwrite) => { const { id, allow, deny } = overwrite; - if (BigInt(allow) & Permissions.FLAGS.VIEW_CHANNEL) - perms.push(`allow:${id}`); - else if (BigInt(deny) & Permissions.FLAGS.VIEW_CHANNEL) - perms.push(`deny:${id}`); + if (BigInt(allow) & Permissions.FLAGS.VIEW_CHANNEL) perms.push(`allow:${id}`); + else if (BigInt(deny) & Permissions.FLAGS.VIEW_CHANNEL) perms.push(`deny:${id}`); }); if (perms.length > 0) { @@ -302,10 +254,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) { }); }); - const groups = ops - .map((x) => x.groups) - .flat() - .distinct(); + const groups = [...new Set(ops.map((x) => x.groups).flat())]; await Send(this, { op: OPCODES.Dispatch, @@ -317,9 +266,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) { op: "SYNC", range: x.range, })), - online_count: - member_count - - (groups.find((x) => x.id == "offline")?.count ?? 0), + online_count: member_count - (groups.find((x) => x.id == "offline")?.count ?? 0), member_count, id: list_id, guild_id, @@ -327,7 +274,5 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) { }, }); - console.log( - `[Gateway] LAZY_REQUEST ${guild_id} ${channel_id} took ${Date.now() - startTime}ms`, - ); + console.log(`[Gateway] LAZY_REQUEST ${guild_id} ${channel_id} took ${Date.now() - startTime}ms`); } diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts index 252fb8f9e..782420447 100644 --- a/src/util/entities/Channel.ts +++ b/src/util/entities/Channel.ts @@ -255,7 +255,7 @@ export class Channel extends BaseClass { } static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) { - recipients = recipients.distinct().filter((x) => x !== creator_user_id); + recipients = [...new Set(recipients)].filter((x) => x !== creator_user_id); // TODO: check config for max number of recipients /** if you want to disallow note to self channels, uncomment the conditional below @@ -280,7 +280,7 @@ export class Channel extends BaseClass { if (!ur.channel.recipients) continue; const re = ur.channel.recipients.map((r) => r.user_id); if (re.length === channelRecipients.length) { - if (re.containsAll(channelRecipients)) { + if (channelRecipients.every((_) => re.includes(_))) { if (channel == null) { channel = ur.channel; await ur.assign({ closed: false }).save(); @@ -429,7 +429,7 @@ export class Channel extends BaseClass { } async getUserPermissions(opts: { user_id?: string; user?: User; member?: Member; guild?: Guild }): Promise { - if(this.isDm()) this.owner_id == (opts.user_id ?? opts.user?.id) ? Permissions.ALL : Permissions.DEFAULT_DM_PERMISSIONS; + if (this.isDm()) return this.owner_id == (opts.user_id ?? opts.user?.id) ? Permissions.ALL : Permissions.DEFAULT_DM_PERMISSIONS; let guild = opts.guild; if (!guild) { if (this.guild) guild = this.guild; @@ -470,7 +470,7 @@ export class Channel extends BaseClass { position: true, }, }, - loadEagerRelations: false + loadEagerRelations: false, }) ).roles ).sort((a, b) => a.position - b.position); // ascending by position diff --git a/src/util/entities/Guild.ts b/src/util/entities/Guild.ts index 0b09f8594..92d4925a3 100644 --- a/src/util/entities/Guild.ts +++ b/src/util/entities/Guild.ts @@ -30,7 +30,7 @@ import { Template } from "./Template"; import { User } from "./User"; import { VoiceState } from "./VoiceState"; import { Webhook } from "./Webhook"; - +import { arrayRemove } from "@spacebar/util"; // TODO: application_command_count, application_command_counts: {1: 0, 2: 0, 3: 0} // TODO: guild_scheduled_events // TODO: stage_instances @@ -420,7 +420,7 @@ export class Guild extends BaseClass { if (typeof insertPoint == "string") position = guild.channel_ordering.indexOf(insertPoint) + 1; else position = insertPoint; - guild.channel_ordering.remove(channel_id); + arrayRemove(guild.channel_ordering, channel_id); guild.channel_ordering.splice(position, 0, channel_id); await Guild.update({ id: guild_id }, { channel_ordering: guild.channel_ordering }); diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index 4343aaf4a..afa7c00e4 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts @@ -354,9 +354,9 @@ export class User extends BaseClass { for (const channel of qry) { console.warn(JSON.stringify(channel)); } + throw new Error("Array contains more than one matching element"); } - // throw if multiple - return qry.single((_) => true); + return qry[0]; } } diff --git a/src/util/util/FieldError.ts b/src/util/util/FieldError.ts index a99a04860..83d6051ce 100644 --- a/src/util/util/FieldError.ts +++ b/src/util/util/FieldError.ts @@ -31,7 +31,7 @@ export function FieldErrors(fields: Record(({ message, code }) => ({ + Object.values(fields).map(({ message, code }) => ({ _errors: [ { message, @@ -39,7 +39,7 @@ export function FieldErrors(fields: Record setTimeout(res, 5000)); resolve(false); })(); }); @@ -111,9 +106,7 @@ export class KittyLogo { while (pngData.length > 0) { const dataSize = Math.min(pngData.length, chunkSize); - process.stdout.write( - header + `,m=${dataSize == chunkSize ? 1 : 0};`, - ); + process.stdout.write(header + `,m=${dataSize == chunkSize ? 1 : 0};`); process.stdout.write(pngData.slice(0, chunkSize)); pngData = pngData.slice(chunkSize); process.stdout.write("\x1b\\"); diff --git a/src/util/util/String.ts b/src/util/util/String.ts index 2d2e132a7..f79e73f20 100644 --- a/src/util/util/String.ts +++ b/src/util/util/String.ts @@ -37,4 +37,10 @@ export function centerString(str: string, len: number): string { const pad = len - str.length; const padLeft = Math.floor(pad / 2) + str.length; return str.padStart(padLeft).padEnd(len); -} \ No newline at end of file +} + +export function stringGlobToRegexp(str: string, flags?: string): RegExp { + // Convert simple wildcard patterns to regex + const escaped = str.replace(".", "\\.").replace("?", ".").replace("*", ".*"); + return new RegExp(escaped, flags); +} diff --git a/src/util/util/extensions/Url.ts b/src/util/util/Url.ts similarity index 79% rename from src/util/util/extensions/Url.ts rename to src/util/util/Url.ts index c0f07b374..808bdeaa9 100644 --- a/src/util/util/extensions/Url.ts +++ b/src/util/util/Url.ts @@ -16,19 +16,6 @@ along with this program. If not, see . */ -declare module "url" { - interface URL { - normalize(): string; - } -} - -/** - * Normalize a URL by: - * - Removing trailing slashes (except root path) - * - Sorting query params alphabetically - * - Removing empty query strings - * - Removing fragments - */ export function normalizeUrl(input: string): string { try { const u = new URL(input); @@ -52,9 +39,3 @@ export function normalizeUrl(input: string): string { return input; } } - -// register extensions -if (!URL.prototype.normalize) - URL.prototype.normalize = function () { - return normalizeUrl(this.toString()); - }; diff --git a/src/util/util/extensions/Array.test.ts b/src/util/util/extensions/Array.test.ts index d41442c99..8952c9016 100644 --- a/src/util/util/extensions/Array.test.ts +++ b/src/util/util/extensions/Array.test.ts @@ -1,102 +1,9 @@ import moduleAlias from "module-alias"; moduleAlias(); -import './Array'; -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; +import "./Array"; +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; describe("Array extensions", () => { - - it("containsAll", () => { - const arr = [1, 2, 3, 4, 5]; - assert(arr.containsAll([1, 2])); - assert(!arr.containsAll([1, 6])); - assert(arr.containsAll([])); - assert([].containsAll([])); - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - assert(![].containsAll([1])); - }); - - it("partition", () => { - const arr = [1, 2, 3, 4, 5]; - const [even, odd] = arr.partition((n) => n % 2 === 0); - assert.deepEqual(even, [2, 4]); - assert.deepEqual(odd, [1, 3, 5]); - }); - - it("single", () => { - const arr = [1, 2, 3, 4, 5]; - assert.strictEqual(arr.single((n) => n === 3), 3); - assert.strictEqual(arr.single((n) => n === 6), null); - assert.throws(() => arr.single((n) => n > 2)); - }); - - it("forEachAsync", async () => { - const arr = [1, 2, 3]; - let sum = 0; - await arr.forEachAsync(async (n) => { - sum += n; - }); - assert.strictEqual(sum, 6); - }); - - it("filterAsync", async () => { - const arr = [1, 2, 3, 4, 5]; - const even = await arr.filterAsync(async (n) => n % 2 === 0); - assert.deepEqual(even, [2, 4]); - }); - - it("remove", () => { - const arr = [1, 2, 3, 4, 5]; - arr.remove(3); - assert.deepEqual(arr, [1, 2, 4, 5]); - arr.remove(6); - assert.deepEqual(arr, [1, 2, 4, 5]); - }); - - it("first", () => { - const arr = [1, 2, 3]; - assert.strictEqual(arr.first(), 1); - assert.strictEqual([].first(), undefined); - }); - - it("last", () => { - const arr = [1, 2, 3]; - assert.strictEqual(arr.last(), 3); - assert.strictEqual([].last(), undefined); - }); - - it("distinct", () => { - const arr = [1, 2, 2, 3, 3, 3]; - assert.deepEqual(arr.distinct(), [1, 2, 3]); - assert.deepEqual([].distinct(), []); - }); - - it("distinctBy", () => { - const arr = [{ id: 1 }, { id: 2 }, { id: 1 }, { id: 3 }]; - assert.deepEqual(arr.distinctBy((x) => x.id), [{ id: 1 }, { id: 2 }, { id: 3 }]); - assert.deepEqual([].distinctBy((x) => x), []); - }); - - it("intersect", () => { - const arr1 = [1, 2, 3, 4]; - const arr2 = [3, 4, 5, 6]; - assert.deepEqual(arr1.intersect(arr2), [3, 4]); - assert.deepEqual(arr1.intersect([]), []); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - assert.deepEqual([].intersect(arr2), []); - }); - - it("except", () => { - const arr1 = [1, 2, 3, 4]; - const arr2 = [3, 4, 5, 6]; - assert.deepEqual(arr1.except(arr2), [1, 2]); - assert.deepEqual(arr1.except([]), [1, 2, 3, 4]); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - assert.deepEqual([].except(arr2), []); - }); - -}); \ No newline at end of file + // +}); diff --git a/src/util/util/extensions/Array.ts b/src/util/util/extensions/Array.ts index c32b86ce8..bbd861643 100644 --- a/src/util/util/extensions/Array.ts +++ b/src/util/util/extensions/Array.ts @@ -18,25 +18,12 @@ declare global { interface Array { - containsAll(target: T[]): boolean; - partition(filter: (elem: T) => boolean): [T[], T[]]; - single(filter: (elem: T) => boolean): T | null; - forEachAsync(callback: (elem: T, index: number, array: T[]) => Promise): Promise; - filterAsync(callback: (elem: T, index: number, array: T[]) => Promise): Promise; - remove(item: T): void; - first(): T | undefined; - last(): T | undefined; - distinct(): T[]; - distinctBy(key: (elem: T) => K): T[]; - intersect(other: T[]): T[]; - except(other: T[]): T[]; + /** + * @deprecated never use, idk why but I can't get rid of this without errors + */ + remove(h: T): never; } } - -export function arrayContainsAll(arr: T[], target: T[]) { - return target.every((v) => arr.includes(v)); -} - /* https://stackoverflow.com/a/50636286 */ export function arrayPartition(array: T[], filter: (elem: T) => boolean): [T[], T[]] { const pass: T[] = [], @@ -45,108 +32,11 @@ export function arrayPartition(array: T[], filter: (elem: T) => boolean): [T[ return [pass, fail]; } -export function arraySingle(array: T[], filter: (elem: T) => boolean): T | null { - const results = array.filter(filter); - if (results.length > 1) throw new Error("Array contains more than one matching element"); - if (results.length === 0) return null; - return results[0]; -} - -export async function arrayForEachAsync(array: T[], callback: (elem: T, index: number, array: T[]) => Promise): Promise { - await Promise.all(array.map(callback)); -} - -export async function arrayFilterAsync(array: T[], callback: (elem: T, index: number, array: T[]) => Promise): Promise { - const results = await Promise.all(array.map(callback)); - return array.filter((_, index) => results[index]); -} - -export function arrayRemove(this: T[], item: T): void { - const index = this.indexOf(item); +export function arrayRemove(array: T[], item: T): void { + const index = array.indexOf(item); if (index > -1) { - this.splice(index, 1); + array.splice(index, 1); } } -export function arrayFirst(this: T[]): T | undefined { - return this[0]; -} - -export function arrayLast(this: T[]): T | undefined { - return this[this.length - 1]; -} - -export function arrayDistinct(this: T[]): T[] { - return Array.from(new Set(this)); -} - -export function arrayDistinctBy(this: T[], key: (elem: T) => K): T[] { - const seen = new Set(); - return this.filter((item) => { - const k = key(item); - if (seen.has(k)) { - return false; - } else { - seen.add(k); - return true; - } - }); -} - -export function arrayIntersect(this: T[], other: T[]): T[] { - return this.filter((value) => other.includes(value)); -} - -export function arrayExcept(this: T[], other: T[]): T[] { - return this.filter((value) => !other.includes(value)); -} - // register extensions -if (!Array.prototype.containsAll) - Array.prototype.containsAll = function (this: T[], target: T[]) { - return arrayContainsAll(this, target); - }; -if (!Array.prototype.partition) - Array.prototype.partition = function (this: T[], filter: (elem: T) => boolean) { - return arrayPartition(this, filter); - }; -if (!Array.prototype.single) - Array.prototype.single = function (this: T[], filter: (elem: T) => boolean) { - return arraySingle(this, filter); - }; -if (!Array.prototype.forEachAsync) - Array.prototype.forEachAsync = function (this: T[], callback: (elem: T, index: number, array: T[]) => Promise) { - return arrayForEachAsync(this, callback); - }; -if (!Array.prototype.filterAsync) - Array.prototype.filterAsync = function (this: T[], callback: (elem: T, index: number, array: T[]) => Promise) { - return arrayFilterAsync(this, callback); - }; -if (!Array.prototype.remove) - Array.prototype.remove = function (this: T[], item: T) { - return arrayRemove.call(this, item); - }; -if (!Array.prototype.first) - Array.prototype.first = function (this: T[]) { - return arrayFirst.call(this); - }; -if (!Array.prototype.last) - Array.prototype.last = function (this: T[]) { - return arrayLast.call(this); - }; -if (!Array.prototype.distinct) - Array.prototype.distinct = function (this: T[]) { - return arrayDistinct.call(this); - }; -if (!Array.prototype.distinctBy) - Array.prototype.distinctBy = function (this: T[], key: (elem: T) => K) { - return arrayDistinctBy.call(this, key as ((elem: unknown) => unknown)); - }; -if (!Array.prototype.intersect) - Array.prototype.intersect = function (this: T[], other: T[]) { - return arrayIntersect.call(this, other); - }; -if (!Array.prototype.except) - Array.prototype.except = function (this: T[], other: T[]) { - return arrayExcept.call(this, other); - }; \ No newline at end of file diff --git a/src/util/util/extensions/Global.test.ts b/src/util/util/extensions/Global.test.ts deleted file mode 100644 index 0c6a93dc7..000000000 --- a/src/util/util/extensions/Global.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import moduleAlias from "module-alias"; -moduleAlias(); -import './Global'; -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; - -describe("Global extensions", () => { - - it("sleep", async () => { - const start = Date.now(); - await sleep(100); - const duration = Date.now() - start; - assert(duration >= 100, `Sleep duration was less than expected: ${duration}ms`); - }); - -}); \ No newline at end of file diff --git a/src/util/util/extensions/Global.ts b/src/util/util/extensions/Global.ts deleted file mode 100644 index 4634573de..000000000 --- a/src/util/util/extensions/Global.ts +++ /dev/null @@ -1,12 +0,0 @@ -declare global { - function sleep(ms: number): Promise; -} - -export function globalSleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -if (!globalThis.sleep) - globalThis.sleep = function (ms: number): Promise { - return globalSleep(ms); - }; diff --git a/src/util/util/extensions/Math.test.ts b/src/util/util/extensions/Math.test.ts deleted file mode 100644 index 5f112dc0a..000000000 --- a/src/util/util/extensions/Math.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import moduleAlias from "module-alias"; -moduleAlias(); -import './Math'; -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; - -describe("Math extensions", () => { - - it("clamp", async () => { - assert.strictEqual(Math.clamp(5, 1, 10), 5); - assert.strictEqual(Math.clamp(0, 1, 10), 1); - assert.strictEqual(Math.clamp(15, 1, 10), 10); - assert.strictEqual(Math.clamp(-5, -10, -1), -5); - assert.strictEqual(Math.clamp(-15, -10, -1), -10); - assert.strictEqual(Math.clamp(-0.5, -1, 0), -0.5); - assert.strictEqual(Math.clamp(1.5, 1, 2), 1.5); - }); - -}); \ No newline at end of file diff --git a/src/util/util/extensions/Math.ts b/src/util/util/extensions/Math.ts deleted file mode 100644 index a5bd80c30..000000000 --- a/src/util/util/extensions/Math.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2025 Spacebar and Spacebar Contributors - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -declare global { - interface Math { - clamp(value: number, min: number, max: number): number; - } -} - -export function mathClamp(value: number, min: number, max: number): number { - return Math.min(Math.max(value, min), max); -} - -// register extensions -if (!Math.clamp) - Math.clamp = mathClamp; \ No newline at end of file diff --git a/src/util/util/extensions/Object.test.ts b/src/util/util/extensions/Object.test.ts deleted file mode 100644 index 570b80249..000000000 --- a/src/util/util/extensions/Object.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import moduleAlias from "module-alias"; -moduleAlias(); -import "./Object"; -import { describe, it } from "node:test"; -import assert from "node:assert/strict"; - -describe("Object extensions", () => { - it("forEach", async () => { - const obj: { [index:string]: number } = { a: 1, b: 2, c: 3 }; - const keys: string[] = []; - const values: number[] = []; - obj.forEach((value, key, _) => { - keys.push(key); - values.push(value); - }); - console.log(keys, values); - assert.deepEqual(keys, ["a", "b", "c"]); - assert.deepEqual(values, [1, 2, 3]); - }); - - it("map", async () => { - const obj = { a: 1, b: 2, c: 3 }; - const result = obj.map((value, key) => `${key}:${value}`); - assert.deepEqual(result, { a: "a:1", b: "b:2", c: "c:3" }); - }); -}); diff --git a/src/util/util/extensions/Object.ts b/src/util/util/extensions/Object.ts deleted file mode 100644 index bda4ccf21..000000000 --- a/src/util/util/extensions/Object.ts +++ /dev/null @@ -1,43 +0,0 @@ -declare global { - interface Object { - forEach(callback: (value: T, key: string, object: { [index: string]: T }) => void): void; - map(callback: (value: SV, key: string, object: { [index: string]: SV }) => TV): { [index: string]: TV }; - } -} - -export function objectForEach(obj: { [index: string]: T }, callback: (value: T, key: string, object: { [index: string]: T }) => void): void { - Object.keys(obj).forEach((key) => { - callback(obj[key], key, obj); - }); -} - -export function objectMap(srcObj: { [index: string]: SV }, callback: (value: SV, key: string, object: { [index: string]: SV }) => TV): { [index: string]: TV } { - if (typeof callback !== "function") throw new TypeError(`${callback} is not a function`); - const obj: { [index: string]: TV } = {}; - Object.keys(srcObj).forEach((key) => { - obj[key] = callback(srcObj[key], key, srcObj); - }); - return obj; -} - -if (!Object.prototype.forEach) - Object.defineProperty(Object.prototype, "forEach", { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - value: function (cb) { - return objectForEach(this, cb); - }, - enumerable: false, - writable: true, - }); - -if (!Object.prototype.map) - Object.defineProperty(Object.prototype, "map", { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - value: function (cb) { - return objectMap(this, cb); - }, - enumerable: false, - writable: true, - }); diff --git a/src/util/util/extensions/String.test.ts b/src/util/util/extensions/String.test.ts deleted file mode 100644 index d5cc9292e..000000000 --- a/src/util/util/extensions/String.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import moduleAlias from "module-alias"; -moduleAlias(); -import './String'; -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; - -describe("String extensions", () => { - - it("globToRegexp", () => { - const pattern = "file-*.txt"; - const regex = pattern.globToRegexp(); - assert.ok(regex.test("file-123.txt")); - }); - -}); \ No newline at end of file diff --git a/src/util/util/extensions/String.ts b/src/util/util/extensions/String.ts deleted file mode 100644 index ce9b1a513..000000000 --- a/src/util/util/extensions/String.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2025 Spacebar and Spacebar Contributors - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -declare global { - interface String { - globToRegexp(flags?: string): RegExp; - } -} - -export function stringGlobToRegexp(str: string, flags?: string): RegExp { - // Convert simple wildcard patterns to regex - const escaped = str.replace(".", "\\.") - .replace("?", ".") - .replace("*", ".*") - return new RegExp(escaped, flags); -} - -// Register extensions -if (!String.prototype.globToRegexp) - String.prototype.globToRegexp = function (str: string, flags?: string) { - return stringGlobToRegexp.call(null, str, flags); - }; \ No newline at end of file diff --git a/src/util/util/extensions/Url.test.ts b/src/util/util/extensions/Url.test.ts deleted file mode 100644 index 37afd0a28..000000000 --- a/src/util/util/extensions/Url.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import moduleAlias from "module-alias"; -moduleAlias(); -import './Url'; -import { describe, it } from 'node:test'; -import assert from 'node:assert/strict'; - -describe("URL extensions", () => { - - it("normalize", async () => { - const tests: [string, string][] = [ - ["http://example.com", "http://example.com/"], - ["http://example.com/", "http://example.com/"], - ["http://example.com/path/", "http://example.com/path"], - ["http://example.com/path//", "http://example.com/path/"], - ["http://example.com/path?b=2&a=1", "http://example.com/path?a=1&b=2"], - ["http://example.com/path?b=2&a=1&", "http://example.com/path?a=1&b=2"], - ["http://example.com/path?", "http://example.com/path"], - ["http://example.com/path#fragment", "http://example.com/path"], - ["http://example.com/path/?b=2&a=1#fragment", "http://example.com/path?a=1&b=2"], - ["ftp://example.com/resource/", "ftp://example.com/resource"], - ["https://example.com/resource?z=3&y=2&x=1", "https://example.com/resource?x=1&y=2&z=3"], - ["https://example.com/resource?z=3&y=2&x=1#", "https://example.com/resource?x=1&y=2&z=3"], - ["https://example.com/resource?z=3&y=2&x=1#section", "https://example.com/resource?x=1&y=2&z=3"], - ["https://example.com/resource/?z=3&y=2&x=1#section", "https://example.com/resource?x=1&y=2&z=3"], - ["https://example.com/resource//?z=3&y=2&x=1#section", "https://example.com/resource/?x=1&y=2&z=3"], - ["https://example.com/", "https://example.com/"], - ["https://example.com", "https://example.com/"], - ]; - for (const [input, expected] of tests) { - assert.doesNotThrow(() => new URL(input), `URL("${input}") should not throw`); - const url = new URL(input); - const normalized = url.normalize(); - assert.strictEqual(normalized, expected, `normalize("${input}") = "${normalized}", expected "${expected}"`); - } - }); - -}); \ No newline at end of file diff --git a/src/util/util/extensions/index.ts b/src/util/util/extensions/index.ts index e006591f8..e1946bb38 100644 --- a/src/util/util/extensions/index.ts +++ b/src/util/util/extensions/index.ts @@ -1,5 +1 @@ export * from "./Array"; -export * from "./Math"; -export * from "./Url"; -export * from "./Object"; -export * from "./String"; \ No newline at end of file diff --git a/src/util/util/index.ts b/src/util/util/index.ts index 11c4899cc..920c330db 100644 --- a/src/util/util/index.ts +++ b/src/util/util/index.ts @@ -48,4 +48,5 @@ export * from "./Application"; export * from "./NameValidation"; export * from "../../schemas/HelperTypes"; export * from "./extensions"; -export * from "./Random"; \ No newline at end of file +export * from "./Random"; +export * from "./Url"; diff --git a/src/util/util/lambert-server/check.ts b/src/util/util/lambert-server/check.ts index 4dabc5cea..bca65bf41 100644 --- a/src/util/util/lambert-server/check.ts +++ b/src/util/util/lambert-server/check.ts @@ -2,8 +2,7 @@ import { NextFunction, Request, Response } from "express"; import { HTTPError } from "."; const OPTIONAL_PREFIX = "$"; -const EMAIL_REGEX = - /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; +const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; export function check(schema: any) { return (req: Request, res: Response, next: NextFunction) => { @@ -31,11 +30,7 @@ export class Email { } } -export function instanceOf( - type: any, - value: any, - { path = "", optional = false }: { path?: string; optional?: boolean } = {} -): Boolean { +export function instanceOf(type: any, value: any, { path = "", optional = false }: { path?: string; optional?: boolean } = {}): boolean { if (!type) return true; // no type was specified if (value == null) { @@ -55,7 +50,9 @@ export function instanceOf( try { value = BigInt(value); if (typeof value === "bigint") return true; - } catch (error) {} + } catch (error) { + //Ignore BigInt error + } throw `${path} must be a bigint`; case Boolean: if (value == "true") value = true; @@ -98,9 +95,8 @@ export function instanceOf( } if (typeof value !== "object") throw `${path} must be a object`; - const diff = Object.keys(value).except( - Object.keys(type).map((x) => (x.startsWith(OPTIONAL_PREFIX) ? x.slice(OPTIONAL_PREFIX.length) : x)) - ); + const filterset = new Set(Object.keys(type).map((x) => (x.startsWith(OPTIONAL_PREFIX) ? x.slice(OPTIONAL_PREFIX.length) : x))); + const diff = Object.keys(value).filter((_) => !filterset.has(_)); if (diff.length) throw `Unknown key ${diff}`;