From d22e46d593074b51e99b580f2b05968577ab8152 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 25 Nov 2025 22:58:31 -0600 Subject: [PATCH 01/27] remove first/last --- src/gateway/events/Close.ts | 19 +---- src/gateway/opcodes/LazyRequest.ts | 96 ++++++-------------------- src/util/util/extensions/Array.test.ts | 42 ++++++----- src/util/util/extensions/Array.ts | 15 +--- 4 files changed, 46 insertions(+), 126 deletions(-) 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..979735237 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 } 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") @@ -118,9 +94,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] = members.partition((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 +104,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 +127,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 +154,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 +169,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 +177,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 +209,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 +219,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 +232,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) { @@ -317,9 +265,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 +273,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/util/extensions/Array.test.ts b/src/util/util/extensions/Array.test.ts index d41442c99..45c010384 100644 --- a/src/util/util/extensions/Array.test.ts +++ b/src/util/util/extensions/Array.test.ts @@ -1,11 +1,10 @@ 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])); @@ -27,8 +26,14 @@ describe("Array extensions", () => { 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.strictEqual( + arr.single((n) => n === 3), + 3, + ); + assert.strictEqual( + arr.single((n) => n === 6), + null, + ); assert.throws(() => arr.single((n) => n > 2)); }); @@ -55,18 +60,6 @@ describe("Array extensions", () => { 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]); @@ -75,8 +68,14 @@ describe("Array extensions", () => { 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), []); + assert.deepEqual( + arr.distinctBy((x) => x.id), + [{ id: 1 }, { id: 2 }, { id: 3 }], + ); + assert.deepEqual( + [].distinctBy((x) => x), + [], + ); }); it("intersect", () => { @@ -98,5 +97,4 @@ describe("Array extensions", () => { // @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..95ad0f3bf 100644 --- a/src/util/util/extensions/Array.ts +++ b/src/util/util/extensions/Array.ts @@ -24,8 +24,6 @@ declare global { 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[]; @@ -126,21 +124,14 @@ 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)); + return arrayDistinctBy.call(this, key as (elem: unknown) => unknown); }; if (!Array.prototype.intersect) Array.prototype.intersect = function (this: T[], other: T[]) { @@ -149,4 +140,4 @@ if (!Array.prototype.intersect) if (!Array.prototype.except) Array.prototype.except = function (this: T[], other: T[]) { return arrayExcept.call(this, other); - }; \ No newline at end of file + }; From 685552579b13873f70202a3388bc4981f0c750a9 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 25 Nov 2025 23:01:52 -0600 Subject: [PATCH 02/27] get rid of single --- src/util/entities/User.ts | 4 ++-- src/util/util/extensions/Array.test.ts | 13 ------------- src/util/util/extensions/Array.ts | 13 +------------ 3 files changed, 3 insertions(+), 27 deletions(-) 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/extensions/Array.test.ts b/src/util/util/extensions/Array.test.ts index 45c010384..454197c1f 100644 --- a/src/util/util/extensions/Array.test.ts +++ b/src/util/util/extensions/Array.test.ts @@ -24,19 +24,6 @@ describe("Array extensions", () => { 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; diff --git a/src/util/util/extensions/Array.ts b/src/util/util/extensions/Array.ts index 95ad0f3bf..d061edf96 100644 --- a/src/util/util/extensions/Array.ts +++ b/src/util/util/extensions/Array.ts @@ -20,7 +20,6 @@ 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; @@ -43,13 +42,6 @@ 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)); } @@ -108,10 +100,7 @@ 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); From 10147dda080d317ec8b4e4baee24581859f47c6d Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 25 Nov 2025 23:28:02 -0600 Subject: [PATCH 03/27] remove except (it's probally faster this way too) --- src/util/util/extensions/Array.test.ts | 10 ---------- src/util/util/extensions/Array.ts | 9 --------- src/util/util/lambert-server/check.ts | 18 +++++++----------- 3 files changed, 7 insertions(+), 30 deletions(-) diff --git a/src/util/util/extensions/Array.test.ts b/src/util/util/extensions/Array.test.ts index 454197c1f..faabfdeab 100644 --- a/src/util/util/extensions/Array.test.ts +++ b/src/util/util/extensions/Array.test.ts @@ -74,14 +74,4 @@ describe("Array extensions", () => { // @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), []); - }); }); diff --git a/src/util/util/extensions/Array.ts b/src/util/util/extensions/Array.ts index d061edf96..11d939207 100644 --- a/src/util/util/extensions/Array.ts +++ b/src/util/util/extensions/Array.ts @@ -26,7 +26,6 @@ declare global { distinct(): T[]; distinctBy(key: (elem: T) => K): T[]; intersect(other: T[]): T[]; - except(other: T[]): T[]; } } @@ -87,10 +86,6 @@ 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[]) { @@ -126,7 +121,3 @@ 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); - }; diff --git a/src/util/util/lambert-server/check.ts b/src/util/util/lambert-server/check.ts index 4dabc5cea..c60a70f7c 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}`; From b3de3ddd9cc4e99081628d71482f7d6fffbf2601 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 25 Nov 2025 23:28:56 -0600 Subject: [PATCH 04/27] remove unused intersect --- src/util/util/extensions/Array.test.ts | 10 ---------- src/util/util/extensions/Array.ts | 5 ----- 2 files changed, 15 deletions(-) diff --git a/src/util/util/extensions/Array.test.ts b/src/util/util/extensions/Array.test.ts index faabfdeab..432e9a590 100644 --- a/src/util/util/extensions/Array.test.ts +++ b/src/util/util/extensions/Array.test.ts @@ -64,14 +64,4 @@ describe("Array extensions", () => { [], ); }); - - 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), []); - }); }); diff --git a/src/util/util/extensions/Array.ts b/src/util/util/extensions/Array.ts index 11d939207..eec76a0fb 100644 --- a/src/util/util/extensions/Array.ts +++ b/src/util/util/extensions/Array.ts @@ -25,7 +25,6 @@ declare global { remove(item: T): void; distinct(): T[]; distinctBy(key: (elem: T) => K): T[]; - intersect(other: T[]): T[]; } } @@ -117,7 +116,3 @@ 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); - }; From 9871ce00282ef9ac230cf764b97f66b3d4e0a544 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 25 Nov 2025 23:34:20 -0600 Subject: [PATCH 05/27] rid of contains all --- src/util/entities/Channel.ts | 6 +++--- src/util/util/extensions/Array.test.ts | 12 ------------ src/util/util/extensions/Array.ts | 9 --------- 3 files changed, 3 insertions(+), 24 deletions(-) diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts index 252fb8f9e..39ab04e4b 100644 --- a/src/util/entities/Channel.ts +++ b/src/util/entities/Channel.ts @@ -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/util/extensions/Array.test.ts b/src/util/util/extensions/Array.test.ts index 432e9a590..1dea2cf43 100644 --- a/src/util/util/extensions/Array.test.ts +++ b/src/util/util/extensions/Array.test.ts @@ -5,18 +5,6 @@ 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); diff --git a/src/util/util/extensions/Array.ts b/src/util/util/extensions/Array.ts index eec76a0fb..f8ca31894 100644 --- a/src/util/util/extensions/Array.ts +++ b/src/util/util/extensions/Array.ts @@ -18,7 +18,6 @@ declare global { interface Array { - containsAll(target: T[]): boolean; partition(filter: (elem: T) => boolean): [T[], T[]]; forEachAsync(callback: (elem: T, index: number, array: T[]) => Promise): Promise; filterAsync(callback: (elem: T, index: number, array: T[]) => Promise): Promise; @@ -28,10 +27,6 @@ declare global { } } -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[] = [], @@ -86,10 +81,6 @@ export function arrayIntersect(this: T[], other: T[]): T[] { } // 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); From 319b420fc099bf5cb0d2a0a2756e248fc9fed763 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 25 Nov 2025 23:37:20 -0600 Subject: [PATCH 06/27] remove unused forEach --- src/util/util/extensions/Object.test.ts | 13 ------------- src/util/util/extensions/Object.ts | 18 ------------------ 2 files changed, 31 deletions(-) diff --git a/src/util/util/extensions/Object.test.ts b/src/util/util/extensions/Object.test.ts index 570b80249..1bcd487e8 100644 --- a/src/util/util/extensions/Object.test.ts +++ b/src/util/util/extensions/Object.test.ts @@ -5,19 +5,6 @@ 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}`); diff --git a/src/util/util/extensions/Object.ts b/src/util/util/extensions/Object.ts index bda4ccf21..894fbba31 100644 --- a/src/util/util/extensions/Object.ts +++ b/src/util/util/extensions/Object.ts @@ -1,16 +1,9 @@ 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 } = {}; @@ -20,17 +13,6 @@ export function objectMap(srcObj: { [index: string]: SV }, callback: (va 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 From 54540a8b14c463aca83430c2a8a5f8d7322a2d66 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 25 Nov 2025 23:49:03 -0600 Subject: [PATCH 07/27] Delete Object.map --- src/api/routes/users/@me/settings.ts | 13 +++++-------- src/util/util/FieldError.ts | 6 +++--- src/util/util/extensions/Object.test.ts | 13 ------------- src/util/util/extensions/Object.ts | 25 ------------------------- src/util/util/extensions/index.ts | 3 +-- 5 files changed, 9 insertions(+), 51 deletions(-) delete mode 100644 src/util/util/extensions/Object.test.ts delete mode 100644 src/util/util/extensions/Object.ts diff --git a/src/api/routes/users/@me/settings.ts b/src/api/routes/users/@me/settings.ts index beef662bb..3a9724242 100644 --- a/src/api/routes/users/@me/settings.ts +++ b/src/api/routes/users/@me/settings.ts @@ -19,7 +19,7 @@ import { route } from "@spacebar/api"; import { User, UserSettings } from "@spacebar/util"; import { Request, Response, Router } from "express"; -import { UserSettingsUpdateSchema, UserSettingsSchema } from "@spacebar/schemas" +import { UserSettingsUpdateSchema, UserSettingsSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); @@ -36,7 +36,7 @@ router.get( }, }), async (req: Request, res: Response) => { - const settings = await UserSettings.getOrDefault(req.user_id) + const settings = await UserSettings.getOrDefault(req.user_id); return res.json(settings); }, ); @@ -67,13 +67,10 @@ 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; + if (body.guild_folders) user.settings.guild_folders = body.guild_folders; await user.settings.save(); await user.save(); 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 { - 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 894fbba31..000000000 --- a/src/util/util/extensions/Object.ts +++ /dev/null @@ -1,25 +0,0 @@ -declare global { - interface Object { - map(callback: (value: SV, key: string, object: { [index: string]: SV }) => TV): { [index: string]: TV }; - } -} - -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.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/index.ts b/src/util/util/extensions/index.ts index e006591f8..6d9ed8de1 100644 --- a/src/util/util/extensions/index.ts +++ b/src/util/util/extensions/index.ts @@ -1,5 +1,4 @@ export * from "./Array"; export * from "./Math"; export * from "./Url"; -export * from "./Object"; -export * from "./String"; \ No newline at end of file +export * from "./String"; From bbd75f1e6c74341b0a5b0f43d7a948136b8438db Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 25 Nov 2025 23:50:45 -0600 Subject: [PATCH 08/27] actually delete the functions --- src/util/util/extensions/Array.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/util/util/extensions/Array.ts b/src/util/util/extensions/Array.ts index f8ca31894..a5cda5711 100644 --- a/src/util/util/extensions/Array.ts +++ b/src/util/util/extensions/Array.ts @@ -51,14 +51,6 @@ export function arrayRemove(this: T[], item: T): void { } } -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)); } From bcef42fe1fda66bf789646849be580f419a6f114 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 25 Nov 2025 23:54:33 -0600 Subject: [PATCH 09/27] remove distinct by --- src/util/util/extensions/Array.test.ts | 12 ------------ src/util/util/extensions/Array.ts | 22 ---------------------- 2 files changed, 34 deletions(-) diff --git a/src/util/util/extensions/Array.test.ts b/src/util/util/extensions/Array.test.ts index 1dea2cf43..effa38e4f 100644 --- a/src/util/util/extensions/Array.test.ts +++ b/src/util/util/extensions/Array.test.ts @@ -40,16 +40,4 @@ describe("Array extensions", () => { 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), - [], - ); - }); }); diff --git a/src/util/util/extensions/Array.ts b/src/util/util/extensions/Array.ts index a5cda5711..2b80b0bc9 100644 --- a/src/util/util/extensions/Array.ts +++ b/src/util/util/extensions/Array.ts @@ -23,7 +23,6 @@ declare global { filterAsync(callback: (elem: T, index: number, array: T[]) => Promise): Promise; remove(item: T): void; distinct(): T[]; - distinctBy(key: (elem: T) => K): T[]; } } @@ -55,23 +54,6 @@ 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)); -} - // register extensions if (!Array.prototype.partition) Array.prototype.partition = function (this: T[], filter: (elem: T) => boolean) { @@ -95,7 +77,3 @@ 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); - }; From 763f4b41f2d1639f585e6554b3693d10f478adf3 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 25 Nov 2025 23:54:41 -0600 Subject: [PATCH 10/27] actually include fix --- src/gateway/opcodes/LazyRequest.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/gateway/opcodes/LazyRequest.ts b/src/gateway/opcodes/LazyRequest.ts index 979735237..e7490ff3a 100644 --- a/src/gateway/opcodes/LazyRequest.ts +++ b/src/gateway/opcodes/LazyRequest.ts @@ -80,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), From 1613ba82ae9c6762c637353e88e74887b7af9e44 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 25 Nov 2025 23:55:50 -0600 Subject: [PATCH 11/27] remove unused filter async --- src/util/util/extensions/Array.test.ts | 6 ------ src/util/util/extensions/Array.ts | 11 +---------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/util/util/extensions/Array.test.ts b/src/util/util/extensions/Array.test.ts index effa38e4f..0e0a04123 100644 --- a/src/util/util/extensions/Array.test.ts +++ b/src/util/util/extensions/Array.test.ts @@ -21,12 +21,6 @@ describe("Array extensions", () => { 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); diff --git a/src/util/util/extensions/Array.ts b/src/util/util/extensions/Array.ts index 2b80b0bc9..a4b1a8996 100644 --- a/src/util/util/extensions/Array.ts +++ b/src/util/util/extensions/Array.ts @@ -20,7 +20,6 @@ declare global { interface Array { partition(filter: (elem: T) => boolean): [T[], T[]]; forEachAsync(callback: (elem: T, index: number, array: T[]) => Promise): Promise; - filterAsync(callback: (elem: T, index: number, array: T[]) => Promise): Promise; remove(item: T): void; distinct(): T[]; } @@ -38,11 +37,6 @@ export async function arrayForEachAsync(array: T[], callback: (elem: T, index 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); if (index > -1) { @@ -64,10 +58,7 @@ 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); From 5a3965ab229b65dc01cabb448b0f5cfa2a48d5ef Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 25 Nov 2025 23:57:56 -0600 Subject: [PATCH 12/27] rid of Math.clamp --- src/api/routes/users/#user_id/messages.ts | 4 +-- src/util/util/extensions/Math.test.ts | 19 -------------- src/util/util/extensions/Math.ts | 31 ----------------------- src/util/util/extensions/index.ts | 1 - 4 files changed, 2 insertions(+), 53 deletions(-) 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/util/util/extensions/Math.test.ts b/src/util/util/extensions/Math.test.ts index 5f112dc0a..e69de29bb 100644 --- a/src/util/util/extensions/Math.test.ts +++ b/src/util/util/extensions/Math.test.ts @@ -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 index a5bd80c30..e69de29bb 100644 --- a/src/util/util/extensions/Math.ts +++ b/src/util/util/extensions/Math.ts @@ -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/index.ts b/src/util/util/extensions/index.ts index 6d9ed8de1..afd6c0b35 100644 --- a/src/util/util/extensions/index.ts +++ b/src/util/util/extensions/index.ts @@ -1,4 +1,3 @@ export * from "./Array"; -export * from "./Math"; export * from "./Url"; export * from "./String"; From 3d8450ff1a4253020e32ab3117a4f6f0aad92730 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Tue, 25 Nov 2025 23:58:28 -0600 Subject: [PATCH 13/27] remove the empty files --- src/util/util/extensions/Math.test.ts | 0 src/util/util/extensions/Math.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/util/util/extensions/Math.test.ts delete mode 100644 src/util/util/extensions/Math.ts diff --git a/src/util/util/extensions/Math.test.ts b/src/util/util/extensions/Math.test.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/util/util/extensions/Math.ts b/src/util/util/extensions/Math.ts deleted file mode 100644 index e69de29bb..000000000 From 960752a685ee95742fe565479d4bcca4abbc6fb7 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 26 Nov 2025 00:01:51 -0600 Subject: [PATCH 14/27] get rid of sleep() --- src/util/util/KittyLogo.ts | 19 ++++++------------- src/util/util/extensions/Global.test.ts | 16 ---------------- src/util/util/extensions/Global.ts | 12 ------------ 3 files changed, 6 insertions(+), 41 deletions(-) delete mode 100644 src/util/util/extensions/Global.test.ts delete mode 100644 src/util/util/extensions/Global.ts diff --git a/src/util/util/KittyLogo.ts b/src/util/util/KittyLogo.ts index 2af0357e3..5251227dc 100644 --- a/src/util/util/KittyLogo.ts +++ b/src/util/util/KittyLogo.ts @@ -15,12 +15,9 @@ export class KittyLogo { public static async initialise() { this.isSupported = await this.checkSupport(); if (this.isSupported) - this.iconCache = readFileSync( - __dirname + "/../../../assets/icon.png", - { - encoding: "base64", - }, - ); + this.iconCache = readFileSync(__dirname + "/../../../assets/icon.png", { + encoding: "base64", + }); } public static printLogo(): void { @@ -77,11 +74,9 @@ export class KittyLogo { if (resp.startsWith("\x1B_Gi=31;OK")) resolve(true); else resolve(false); }); - process.stdout.write( - "\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\\x1b[c", - ); + process.stdout.write("\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\\x1b[c"); - await sleep(5000); + await new Promise((res) => 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/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); - }; From b1fc5dc305442a4e8a4dcf1403f09e72e1eb93a0 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 26 Nov 2025 00:06:05 -0600 Subject: [PATCH 15/27] make normalize URL normal --- src/util/util/{extensions => }/Url.ts | 19 -------------- src/util/util/extensions/Url.test.ts | 37 --------------------------- src/util/util/extensions/index.ts | 1 - src/util/util/index.ts | 3 ++- 4 files changed, 2 insertions(+), 58 deletions(-) rename src/util/util/{extensions => }/Url.ts (79%) delete mode 100644 src/util/util/extensions/Url.test.ts 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/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 afd6c0b35..b7dcf4d53 100644 --- a/src/util/util/extensions/index.ts +++ b/src/util/util/extensions/index.ts @@ -1,3 +1,2 @@ export * from "./Array"; -export * from "./Url"; export * from "./String"; 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"; From 3080445768d7fa710fa8371934e8b09110269f0b Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 26 Nov 2025 00:11:22 -0600 Subject: [PATCH 16/27] rid of forEachAsync --- .../channels/#channel_id/messages/index.ts | 34 +++++++++++-------- src/api/routes/guilds/#guild_id/invites.ts | 12 ++++--- src/util/util/extensions/Array.ts | 10 ------ 3 files changed, 26 insertions(+), 30 deletions(-) diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts index d0d445b9d..99523dab8 100644 --- a/src/api/routes/channels/#channel_id/messages/index.ts +++ b/src/api/routes/channels/#channel_id/messages/index.ts @@ -244,24 +244,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); }, 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/util/util/extensions/Array.ts b/src/util/util/extensions/Array.ts index a4b1a8996..74cceb769 100644 --- a/src/util/util/extensions/Array.ts +++ b/src/util/util/extensions/Array.ts @@ -19,7 +19,6 @@ declare global { interface Array { partition(filter: (elem: T) => boolean): [T[], T[]]; - forEachAsync(callback: (elem: T, index: number, array: T[]) => Promise): Promise; remove(item: T): void; distinct(): T[]; } @@ -33,10 +32,6 @@ export function arrayPartition(array: T[], filter: (elem: T) => boolean): [T[ return [pass, fail]; } -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); if (index > -1) { @@ -54,11 +49,6 @@ if (!Array.prototype.partition) return arrayPartition(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); From 2b9802200d1c0e0a73226821fdb867b760df9468 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 26 Nov 2025 00:11:47 -0600 Subject: [PATCH 17/27] save file --- src/util/util/extensions/Array.test.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/util/util/extensions/Array.test.ts b/src/util/util/extensions/Array.test.ts index 0e0a04123..916192517 100644 --- a/src/util/util/extensions/Array.test.ts +++ b/src/util/util/extensions/Array.test.ts @@ -12,15 +12,6 @@ describe("Array extensions", () => { assert.deepEqual(odd, [1, 3, 5]); }); - 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); From 781abf950a549161a5cb5d4e7541600aa494dde7 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 26 Nov 2025 00:23:41 -0600 Subject: [PATCH 18/27] Fix backwards condition --- src/util/util/lambert-server/check.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/util/lambert-server/check.ts b/src/util/util/lambert-server/check.ts index c60a70f7c..bca65bf41 100644 --- a/src/util/util/lambert-server/check.ts +++ b/src/util/util/lambert-server/check.ts @@ -96,7 +96,7 @@ export function instanceOf(type: any, value: any, { path = "", optional = false if (typeof value !== "object") throw `${path} must be a object`; 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(_)); + const diff = Object.keys(value).filter((_) => !filterset.has(_)); if (diff.length) throw `Unknown key ${diff}`; From 12763d1c593daabe170cf10e48fb460f86e9bed7 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 26 Nov 2025 14:45:19 -0600 Subject: [PATCH 19/27] remove distinct --- .../routes/channels/#channel_id/recipients.ts | 32 +++---------------- src/gateway/opcodes/LazyRequest.ts | 5 +-- src/util/entities/Channel.ts | 2 +- src/util/util/extensions/Array.test.ts | 6 ---- src/util/util/extensions/Array.ts | 10 ------ 5 files changed, 7 insertions(+), 48 deletions(-) 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/gateway/opcodes/LazyRequest.ts b/src/gateway/opcodes/LazyRequest.ts index e7490ff3a..bd22bdb09 100644 --- a/src/gateway/opcodes/LazyRequest.ts +++ b/src/gateway/opcodes/LazyRequest.ts @@ -254,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, diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts index 39ab04e4b..ff3de280a 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.distinct().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 diff --git a/src/util/util/extensions/Array.test.ts b/src/util/util/extensions/Array.test.ts index 916192517..3550654ab 100644 --- a/src/util/util/extensions/Array.test.ts +++ b/src/util/util/extensions/Array.test.ts @@ -19,10 +19,4 @@ describe("Array extensions", () => { arr.remove(6); assert.deepEqual(arr, [1, 2, 4, 5]); }); - - it("distinct", () => { - const arr = [1, 2, 2, 3, 3, 3]; - assert.deepEqual(arr.distinct(), [1, 2, 3]); - assert.deepEqual([].distinct(), []); - }); }); diff --git a/src/util/util/extensions/Array.ts b/src/util/util/extensions/Array.ts index 74cceb769..58ec0a1b7 100644 --- a/src/util/util/extensions/Array.ts +++ b/src/util/util/extensions/Array.ts @@ -20,7 +20,6 @@ declare global { interface Array { partition(filter: (elem: T) => boolean): [T[], T[]]; remove(item: T): void; - distinct(): T[]; } } @@ -39,10 +38,6 @@ export function arrayRemove(this: T[], item: T): void { } } -export function arrayDistinct(this: T[]): T[] { - return Array.from(new Set(this)); -} - // register extensions if (!Array.prototype.partition) Array.prototype.partition = function (this: T[], filter: (elem: T) => boolean) { @@ -53,8 +48,3 @@ if (!Array.prototype.remove) Array.prototype.remove = function (this: T[], item: T) { return arrayRemove.call(this, item); }; - -if (!Array.prototype.distinct) - Array.prototype.distinct = function (this: T[]) { - return arrayDistinct.call(this); - }; From 8f2efff694daace68724f2d56c66e33a3a5a94d3 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 26 Nov 2025 14:48:03 -0600 Subject: [PATCH 20/27] change partition --- .../#guild_id/roles/#role_id/members.ts | 47 +++++++------------ src/gateway/opcodes/LazyRequest.ts | 4 +- src/util/util/extensions/Array.ts | 11 ----- 3 files changed, 18 insertions(+), 44 deletions(-) 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/gateway/opcodes/LazyRequest.ts b/src/gateway/opcodes/LazyRequest.ts index bd22bdb09..c3c74990e 100644 --- a/src/gateway/opcodes/LazyRequest.ts +++ b/src/gateway/opcodes/LazyRequest.ts @@ -16,7 +16,7 @@ along with this program. If not, see . */ -import { getDatabase, getPermission, listenEvent, Member, Role, Session, User, Presence, Channel, Permissions } from "@spacebar/util"; +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"; @@ -98,7 +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, diff --git a/src/util/util/extensions/Array.ts b/src/util/util/extensions/Array.ts index 58ec0a1b7..7ed1d1f0d 100644 --- a/src/util/util/extensions/Array.ts +++ b/src/util/util/extensions/Array.ts @@ -18,18 +18,11 @@ declare global { interface Array { - partition(filter: (elem: T) => boolean): [T[], T[]]; remove(item: T): void; } } /* https://stackoverflow.com/a/50636286 */ -export function arrayPartition(array: T[], filter: (elem: T) => boolean): [T[], T[]] { - const pass: T[] = [], - fail: T[] = []; - array.forEach((e) => (filter(e) ? pass : fail).push(e)); - return [pass, fail]; -} export function arrayRemove(this: T[], item: T): void { const index = this.indexOf(item); @@ -39,10 +32,6 @@ export function arrayRemove(this: T[], item: T): void { } // register extensions -if (!Array.prototype.partition) - Array.prototype.partition = function (this: T[], filter: (elem: T) => boolean) { - return arrayPartition(this, filter); - }; if (!Array.prototype.remove) Array.prototype.remove = function (this: T[], item: T) { From aa40eee0975782771994f83ed14cfc9ee9e4c3ab Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 26 Nov 2025 14:48:15 -0600 Subject: [PATCH 21/27] remove test, oops --- src/util/util/extensions/Array.test.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/util/util/extensions/Array.test.ts b/src/util/util/extensions/Array.test.ts index 3550654ab..6b15f5d55 100644 --- a/src/util/util/extensions/Array.test.ts +++ b/src/util/util/extensions/Array.test.ts @@ -5,13 +5,6 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; describe("Array extensions", () => { - 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("remove", () => { const arr = [1, 2, 3, 4, 5]; arr.remove(3); From 8c9e2877198def8b6652dce5182111be07c1d360 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 26 Nov 2025 14:50:44 -0600 Subject: [PATCH 22/27] re-add partion function --- src/util/util/extensions/Array.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/util/util/extensions/Array.ts b/src/util/util/extensions/Array.ts index 7ed1d1f0d..b58b1c03a 100644 --- a/src/util/util/extensions/Array.ts +++ b/src/util/util/extensions/Array.ts @@ -23,6 +23,12 @@ declare global { } /* https://stackoverflow.com/a/50636286 */ +export function arrayPartition(array: T[], filter: (elem: T) => boolean): [T[], T[]] { + const pass: T[] = [], + fail: T[] = []; + array.forEach((e) => (filter(e) ? pass : fail).push(e)); + return [pass, fail]; +} export function arrayRemove(this: T[], item: T): void { const index = this.indexOf(item); From 9f9ed88299ce8552c9a7868197adf0e11f9cc4b2 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 26 Nov 2025 14:53:38 -0600 Subject: [PATCH 23/27] fix distinct --- src/util/entities/Channel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts index ff3de280a..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 = [...new Set(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 From e92e26ac7a2443ac40de4f56c5df82a7f23cc91b Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 26 Nov 2025 15:24:54 -0600 Subject: [PATCH 24/27] get distinct working better --- scripts/openapi.js | 53 +++++++++++++--------------------------------- 1 file changed, 15 insertions(+), 38 deletions(-) diff --git a/scripts/openapi.js b/scripts/openapi.js index 6d4d63667..bec7ef1c2 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 openapiPath = path.join(__dirname, "..", "assets", "openapi.json"); @@ -88,8 +86,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; @@ -121,7 +118,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("|"); @@ -134,8 +131,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); }) ) { @@ -176,9 +172,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 { @@ -213,7 +207,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"] = [ @@ -224,45 +218,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(); From f09a6976b27743322b6cf760c677340ddd428ec0 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 26 Nov 2025 15:55:47 -0600 Subject: [PATCH 25/27] remove remove --- .../messages/#message_id/reactions.ts | 7 ++++--- src/util/entities/Guild.ts | 4 ++-- src/util/util/extensions/Array.test.ts | 8 +------- src/util/util/extensions/Array.ts | 17 +++++++---------- 4 files changed, 14 insertions(+), 22 deletions(-) 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/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/util/extensions/Array.test.ts b/src/util/util/extensions/Array.test.ts index 6b15f5d55..8952c9016 100644 --- a/src/util/util/extensions/Array.test.ts +++ b/src/util/util/extensions/Array.test.ts @@ -5,11 +5,5 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; describe("Array extensions", () => { - 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]); - }); + // }); diff --git a/src/util/util/extensions/Array.ts b/src/util/util/extensions/Array.ts index b58b1c03a..bbd861643 100644 --- a/src/util/util/extensions/Array.ts +++ b/src/util/util/extensions/Array.ts @@ -18,10 +18,12 @@ declare global { interface Array { - remove(item: T): void; + /** + * @deprecated never use, idk why but I can't get rid of this without errors + */ + remove(h: T): never; } } - /* https://stackoverflow.com/a/50636286 */ export function arrayPartition(array: T[], filter: (elem: T) => boolean): [T[], T[]] { const pass: T[] = [], @@ -30,16 +32,11 @@ export function arrayPartition(array: T[], filter: (elem: T) => boolean): [T[ return [pass, fail]; } -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); } } // register extensions - -if (!Array.prototype.remove) - Array.prototype.remove = function (this: T[], item: T) { - return arrayRemove.call(this, item); - }; From 10649cdbc6a232ada7da0ee0d1177a3279173347 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 26 Nov 2025 17:17:15 -0600 Subject: [PATCH 26/27] make mentions not use the pollyfills --- src/api/routes/users/@me/mentions.ts | 100 +++++++-------------------- 1 file changed, 26 insertions(+), 74 deletions(-) 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 { From d18cdd2d28202119bc306a99389f5f60d255c878 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Wed, 26 Nov 2025 17:20:22 -0600 Subject: [PATCH 27/27] remove string proto fake polyfill --- .../channels/#channel_id/messages/index.ts | 5 ++- src/util/util/String.ts | 8 +++- src/util/util/extensions/String.test.ts | 15 -------- src/util/util/extensions/String.ts | 37 ------------------- src/util/util/extensions/index.ts | 1 - 5 files changed, 10 insertions(+), 56 deletions(-) delete mode 100644 src/util/util/extensions/String.test.ts delete mode 100644 src/util/util/extensions/String.ts diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts index 99523dab8..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"; @@ -453,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/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/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/index.ts b/src/util/util/extensions/index.ts index b7dcf4d53..e1946bb38 100644 --- a/src/util/util/extensions/index.ts +++ b/src/util/util/extensions/index.ts @@ -1,2 +1 @@ export * from "./Array"; -export * from "./String";