diff --git a/assets/openapi.json b/assets/openapi.json index 1caad631..ec0dfc74 100644 Binary files a/assets/openapi.json and b/assets/openapi.json differ diff --git a/assets/schemas.json b/assets/schemas.json index 17c18c43..269d8ab6 100644 Binary files a/assets/schemas.json and b/assets/schemas.json differ diff --git a/package-lock.json b/package-lock.json index 044cfda2..909f510c 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/package.json b/package.json index 79a2416c..1a20cf0c 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.936.0", + "@spacebarchat/medooze-webrtc": "^1.0.8", "@toondepauw/node-zstd": "^1.2.0", "@types/web-push": "^3.6.4", "ajv": "^8.17.1", diff --git a/scripts/openapi.js b/scripts/openapi.js index 6d4d6366..761bc394 100644 --- a/scripts/openapi.js +++ b/scripts/openapi.js @@ -121,7 +121,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("|"); @@ -213,7 +213,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"] = [ 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 fe2a2509..e8794a2a 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 @@ -18,6 +18,7 @@ import { route } from "@spacebar/api"; import { + arrayRemove, Channel, emitEvent, Emoji, @@ -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 d0d445b9..30a44dd5 100644 --- a/src/api/routes/channels/#channel_id/messages/index.ts +++ b/src/api/routes/channels/#channel_id/messages/index.ts @@ -39,6 +39,7 @@ import { Relationship, Rights, Snowflake, + stringGlobToRegexp, uploadFile, User, } from "@spacebar/util"; @@ -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 65160274..44abeda1 100644 --- a/src/api/routes/channels/#channel_id/recipients.ts +++ b/src/api/routes/channels/#channel_id/recipients.ts @@ -47,10 +47,7 @@ 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, diff --git a/src/api/routes/guilds/#guild_id/invites.ts b/src/api/routes/guilds/#guild_id/invites.ts index ec3116c9..9372651a 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 4cfe21b9..f0b44789 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,7 +17,7 @@ */ 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 }); @@ -38,11 +38,7 @@ router.patch( 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([ diff --git a/src/api/routes/users/#user_id/messages.ts b/src/api/routes/users/#user_id/messages.ts index 9ce0b369..271c6cf9 100644 --- a/src/api/routes/users/#user_id/messages.ts +++ b/src/api/routes/users/#user_id/messages.ts @@ -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/settings.ts b/src/api/routes/users/@me/settings.ts index beef662b..f5ede5ab 100644 --- a/src/api/routes/users/@me/settings.ts +++ b/src/api/routes/users/@me/settings.ts @@ -67,10 +67,8 @@ router.patch( relations: ["settings"], }); - if (!user.settings) - user.settings = UserSettings.create(body as UserSettingsUpdateSchema); - else - user.settings.assign(body); + 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 dbbc41d8..68ad0d93 100644 --- a/src/gateway/events/Close.ts +++ b/src/gateway/events/Close.ts @@ -83,7 +83,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 94dc7517..63b3f55c 100644 --- a/src/gateway/opcodes/LazyRequest.ts +++ b/src/gateway/opcodes/LazyRequest.ts @@ -27,6 +27,7 @@ import { Presence, Channel, Permissions, + arrayPartition, } from "@spacebar/util"; import { WebSocket, @@ -51,16 +52,13 @@ const getMostRelevantSession = (sessions: Session[]) => { invisible: 3, offline: 4, }; + // 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]) { @@ -104,23 +102,19 @@ 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); - member_roles.push( - member_roles.splice( - member_roles.findIndex((x) => x.id === x.guild_id), - 1, - )[0], - ); + const member_roles = [ + ...new Map( + members + .map((m) => m.roles) + .flat() + .map((role) => [role.id, role] as [string, Role]), + ).values(), + ]; 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, @@ -257,7 +251,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); @@ -302,10 +296,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, diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts index c15b2ca2..492bf440 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(); @@ -474,7 +474,11 @@ export class Channel extends BaseClass { userPerms = new Permissions(userPerms.remove(overwrite.deny).add(overwrite.allow)); // member overwrite, throws if somehow we have multiple overwrites for the same member - const memberOverwrite = this.permission_overwrites.single((o) => o.type === ChannelPermissionOverwriteType.member && o.id === member?.id); + const memberOverwrite = this.permission_overwrites.find( + (o) => + o.type === ChannelPermissionOverwriteType.member && + o.id === member?.id + ); if (memberOverwrite) userPerms = new Permissions(userPerms.remove(memberOverwrite.deny).add(memberOverwrite.allow)); } diff --git a/src/util/entities/Guild.ts b/src/util/entities/Guild.ts index 0b09f859..64014d20 100644 --- a/src/util/entities/Guild.ts +++ b/src/util/entities/Guild.ts @@ -30,6 +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 @@ -420,7 +421,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 5ee63352..539d8fdb 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 a99a0486..83d6051c 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 2d2e132a..f79e73f2 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 c0f07b37..808bdeaa 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 6395ddcf..8952c901 100644 --- a/src/util/util/extensions/Array.test.ts +++ b/src/util/util/extensions/Array.test.ts @@ -1,96 +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("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 611a8437..bbd86164 100644 --- a/src/util/util/extensions/Array.ts +++ b/src/util/util/extensions/Array.ts @@ -18,24 +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; - 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[] = [], @@ -44,99 +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 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.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 0c6a93dc..00000000 --- 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 4634573d..00000000 --- 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 5f112dc0..00000000 --- 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 a5bd80c3..00000000 --- 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 570b8024..00000000 --- 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 bda4ccf2..00000000 --- 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 d5cc9292..00000000 --- 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 ce9b1a51..00000000 --- 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 37afd0a2..00000000 --- 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 e006591f..ac3789f3 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 +export * from "./Array"; \ No newline at end of file diff --git a/src/util/util/index.ts b/src/util/util/index.ts index 7e429fd0..d49894b7 100644 --- a/src/util/util/index.ts +++ b/src/util/util/index.ts @@ -50,3 +50,4 @@ export * from "../../schemas/HelperTypes"; export * from "./extensions"; export * from "./Random"; export * from "./WebPush"; +export * from "./Url"; \ No newline at end of file diff --git a/src/util/util/lambert-server/check.ts b/src/util/util/lambert-server/check.ts index 4dabc5ce..bca65bf4 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}`;