diff --git a/src/util/util/DateBuilder.test.ts b/src/util/util/DateBuilder.test.ts
deleted file mode 100644
index 74de4dcc..00000000
--- a/src/util/util/DateBuilder.test.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-import { describe, test } from "node:test";
-import assert from "node:assert/strict";
-import { DateBuilder } from "./DateBuilder";
-
-describe("DateBuilder", () => {
- test("should be able to be initialised", () => {
- const db = new DateBuilder();
- assert.equal(db instanceof DateBuilder, true);
- });
-
- test("should be able to build current date", () => {
- const now = new Date();
- const db = new DateBuilder();
- const built = db.build();
- assert.equal(built.getFullYear(), now.getFullYear());
- assert.equal(built.getMonth(), now.getMonth());
- assert.equal(built.getDate(), now.getDate());
- assert.equal(built.getHours(), now.getHours());
- assert.equal(built.getMinutes(), now.getMinutes());
- assert.equal(built.getSeconds(), now.getSeconds());
- });
-
- test("should be able to build timestamp", () => {
- const now = new Date();
- const db = new DateBuilder();
- const built = db.buildTimestamp();
- assert.equal(built, now.getTime());
- });
-
- test("should be able to add days", () => {
- const db = new DateBuilder(new Date(2024, 0, 1)); // Jan 1, 2024
- db.addDays(30);
- const built = db.build();
- assert.equal(built.getFullYear(), 2024);
- assert.equal(built.getMonth(), 0); // January
- assert.equal(built.getDate(), 31); // January has 31 days
- });
-
- test("should be able to add months", () => {
- const db = new DateBuilder(new Date(2024, 0, 1)); // Jan 31, 2024
- db.addMonths(1);
- const built = db.build();
- assert.equal(built.getFullYear(), 2024);
- assert.equal(built.getMonth(), 1); // February
- });
-
- test("should be able to add years", () => {
- const db = new DateBuilder(new Date(2020, 1, 1));
- db.addYears(1);
- const built = db.build();
- assert.equal(built.getFullYear(), 2021);
- assert.equal(built.getMonth(), 1); // February
- assert.equal(built.getDate(), 1);
- });
-
- test("should be able to set date", () => {
- const db = new DateBuilder();
- db.withDate(2022, 12, 25); // Dec 25, 2022
- const built = db.build();
- assert.equal(built.getFullYear(), 2022);
- assert.equal(built.getMonth(), 11); // December
- assert.equal(built.getDate(), 25);
- });
-
- test("should be able to set time", () => {
- const db = new DateBuilder();
- db.withTime(15, 30, 45, 123); // 15:30:45.123
- const built = db.build();
- assert.equal(built.getHours(), 15);
- assert.equal(built.getMinutes(), 30);
- assert.equal(built.getSeconds(), 45);
- assert.equal(built.getMilliseconds(), 123);
- });
-
- test("should be able to set start of day", () => {
- const db = new DateBuilder(new Date(2024, 5, 15, 10, 20, 30, 456)); // June 15, 2024, 10:20:30.456
- db.atStartOfDay();
- const built = db.build();
- assert.equal(built.getFullYear(), 2024);
- assert.equal(built.getMonth(), 5); // June
- assert.equal(built.getDate(), 15);
- assert.equal(built.getHours(), 0);
- assert.equal(built.getMinutes(), 0);
- assert.equal(built.getSeconds(), 0);
- assert.equal(built.getMilliseconds(), 0);
- });
-
- test("should be able to set end of day", () => {
- const db = new DateBuilder(new Date(2024, 5, 15, 10, 20, 30, 456)); // June 15, 2024, 10:20:30.456
- db.atEndOfDay();
- const built = db.build();
- assert.equal(built.getFullYear(), 2024);
- assert.equal(built.getMonth(), 5); // June
- assert.equal(built.getDate(), 15);
- assert.equal(built.getHours(), 23);
- assert.equal(built.getMinutes(), 59);
- assert.equal(built.getSeconds(), 59);
- assert.equal(built.getMilliseconds(), 999);
- });
-
- test("should be able to chain methods", () => {
- const db = new DateBuilder(new Date(2024, 0, 1)); // Jan 1, 2024
- db.addDays(1).addMonths(1).addYears(1).withTime(12, 0, 0).atEndOfDay();
- const built = db.build();
- assert.equal(built.getFullYear(), 2025);
- assert.equal(built.getMonth(), 1); // March
- assert.equal(built.getDate(), 2);
- assert.equal(built.getHours(), 23);
- assert.equal(built.getMinutes(), 59);
- assert.equal(built.getSeconds(), 59);
- assert.equal(built.getMilliseconds(), 999);
- });
-
- test("should not mutate original date", () => {
- const original = new Date(2024, 0, 1); // Jan 1, 2024
- const db = new DateBuilder(original);
- db.addDays(10);
- const built = db.build();
- assert.equal(original.getFullYear(), 2024);
- assert.equal(original.getMonth(), 0); // January
- assert.equal(original.getDate(), 1); // Original date should remain unchanged
- assert.equal(built.getFullYear(), 2024);
- assert.equal(built.getMonth(), 0); // January
- assert.equal(built.getDate(), 11); // New date should be Jan 11, 2024
- });
-
- test("should handle leap years correctly", () => {
- const db = new DateBuilder(new Date(2020, 1, 29)); // Feb 29, 2020 (leap year)
- db.addYears(1);
- const built = db.build();
- assert.equal(built.getFullYear(), 2021);
- assert.equal(built.getMonth(), 2); // March
- assert.equal(built.getDate(), 1); // March 1, 2021 (not a leap year)
- });
-
- test("should handle month overflow correctly", () => {
- const db = new DateBuilder(new Date(2024, 0, 31)); // Jan 31, 2024
- db.addDays(1);
- const built = db.build();
- assert.equal(built.getFullYear(), 2024);
- assert.equal(built.getMonth(), 1); // February
- assert.equal(built.getDate(), 1); // Feb 29, 2024 (leap year)
- });
-});
diff --git a/src/util/util/DateBuilder.ts b/src/util/util/DateBuilder.ts
deleted file mode 100644
index 58588c8b..00000000
--- a/src/util/util/DateBuilder.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
-*/
-
-export class DateBuilder {
- private date: Date;
- // constructors
- constructor(date: Date = new Date()) {
- this.date = new Date(date.getTime()); // Create a copy to avoid mutating the original date
- }
-
- // methods
- addYears(years: number) {
- this.date.setFullYear(this.date.getFullYear() + years);
- return this;
- }
-
- addMonths(months: number) {
- this.date.setMonth(this.date.getMonth() + months);
- return this;
- }
-
- addDays(days: number) {
- this.date.setDate(this.date.getDate() + days);
- return this;
- }
-
- addHours(hours: number) {
- this.date.setHours(this.date.getHours() + hours);
- return this;
- }
-
- addMinutes(minutes: number) {
- this.date.setMinutes(this.date.getMinutes() + minutes);
- return this;
- }
-
- addSeconds(seconds: number) {
- this.date.setSeconds(this.date.getSeconds() + seconds);
- return this;
- }
-
- addMillis(millis: number) {
- this.date.setTime(this.date.getTime() + millis);
- return this;
- }
-
- withDate(year: number, month: number, day: number | undefined) {
- this.date.setFullYear(year, month - 1, day); // month is 0-based
- return this;
- }
-
- withTime(hour: number, minute = 0, second = 0, millisecond = 0) {
- this.date.setHours(hour, minute, second, millisecond);
- return this;
- }
-
- atStartOfDay() {
- this.date.setHours(0, 0, 0, 0);
- return this;
- }
-
- atEndOfDay() {
- this.date.setHours(23, 59, 59, 999);
- return this;
- }
-
- build() {
- return new Date(this.date.getTime()); // Return a copy to avoid external mutation
- }
-
- buildTimestamp() {
- return this.date.getTime();
- }
-}
diff --git a/src/util/util/ElapsedTime.test.ts b/src/util/util/ElapsedTime.test.ts
deleted file mode 100644
index b8cbbdce..00000000
--- a/src/util/util/ElapsedTime.test.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import { describe, test } from "node:test";
-import assert from "node:assert/strict";
-import { ElapsedTime } from "./ElapsedTime";
-
-describe("ElapsedTime", () => {
- test("should be able to be initialised", () => {
- const db = new ElapsedTime(0n);
- assert.equal(db != null, true);
- });
-
- test("should return correct total nanoseconds", () => {
- const db = new ElapsedTime(1234567890n);
- assert.equal(db.totalNanoseconds, 1234567890n);
- });
-
- test("should return correct total microseconds", () => {
- const db = new ElapsedTime(1234567890n);
- assert.equal(db.totalMicroseconds, 1234567);
- });
-
- test("should return correct total milliseconds", () => {
- const db = new ElapsedTime(1234567890n);
- assert.equal(db.totalMilliseconds, 1234);
- });
-
- test("should return correct total seconds", () => {
- const db = new ElapsedTime(5000000000n);
- assert.equal(db.totalSeconds, 5);
- });
-
- test("should return correct total minutes", () => {
- const db = new ElapsedTime(300000000000n); // 5 minutes
- assert.equal(db.totalMinutes, 5);
- });
-
- test("should return correct total hours", () => {
- const db = new ElapsedTime(7200000000000n); // 2 hours
- assert.equal(db.totalHours, 2);
- });
-
- test("should return correct total days", () => {
- const db = new ElapsedTime(172800000000000n); // 2 days
- assert.equal(db.totalDays, 2);
- });
-
- test("should return correct nanoseconds", () => {
- const db = new ElapsedTime(1234567890n);
- assert.equal(db.nanoseconds, 890);
- });
-
- test("should return correct microseconds", () => {
- const db = new ElapsedTime(1234567890n);
- assert.equal(db.microseconds, 567);
- });
-
- test("should return correct milliseconds", () => {
- const db = new ElapsedTime(1234567890n);
- assert.equal(db.milliseconds, 234);
- });
-
- test("should return correct seconds", () => {
- const db = new ElapsedTime(5000000000n);
- assert.equal(db.seconds, 5);
- });
-
- test("should return correct minutes", () => {
- const db = new ElapsedTime(300000000000n); // 5 minutes
- assert.equal(db.minutes, 5);
- });
-
- test("should return correct hours", () => {
- const db = new ElapsedTime(7200000000000n); // 2 hours
- assert.equal(db.hours, 2);
- });
-
- test("should return correct days", () => {
- const db = new ElapsedTime(172800000000000n); // 2 days
- assert.equal(db.days, 2);
- });
-});
diff --git a/src/util/util/ElapsedTime.ts b/src/util/util/ElapsedTime.ts
deleted file mode 100644
index cce91280..00000000
--- a/src/util/util/ElapsedTime.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
-*/
-
-// Inspired by the dotnet Stopwatch class
-// Provides a simple interface to get elapsed time in high resolution
-
-export class ElapsedTime {
- private readonly timeNanos: bigint;
-
- constructor(timeNanos: bigint) {
- this.timeNanos = timeNanos;
- }
-
- get totalNanoseconds(): bigint {
- return this.timeNanos;
- }
- get totalMicroseconds(): number {
- return Number(this.timeNanos / 1_000n);
- }
- get totalMilliseconds(): number {
- return Number(this.timeNanos / 1_000_000n);
- }
- get totalSeconds(): number {
- return Number(this.timeNanos / 1_000_000_000n);
- }
- get totalMinutes(): number {
- return this.totalSeconds / 60;
- }
- get totalHours(): number {
- return this.totalMinutes / 60;
- }
- get totalDays(): number {
- return this.totalHours / 24;
- }
- get nanoseconds(): number {
- return Number(this.timeNanos % 1_000n);
- }
- get microseconds(): number {
- return Number(this.timeNanos / 1_000n) % 1000;
- }
- get milliseconds(): number {
- return Number(this.timeNanos / 1_000_000n) % 1000;
- }
- get seconds(): number {
- return Number(this.timeNanos / 1_000_000_000n) % 60;
- }
- get minutes(): number {
- return this.totalMinutes % 60;
- }
- get hours(): number {
- return this.totalHours % 24;
- }
- get days(): number {
- return this.totalDays;
- }
-
- toString(): string {
- // Format: "DD.HH:MM:SS.mmmuuuNNN", with days being optional
- const daysPart = Math.floor(this.days) > 0 ? `${Math.floor(this.days)}.` : "";
- const hoursPart = Math.floor(this.hours).toString().padStart(2, "0");
- const minutesPart = Math.floor(this.minutes).toString().padStart(2, "0");
- const secondsPart = Math.floor(this.seconds).toString().padStart(2, "0");
- const millisecondsPart = Math.floor(this.milliseconds).toString().padStart(3, "0");
- const microsecondsPart = Math.floor(this.microseconds).toString().padStart(3, "0");
- const nanosecondsPart = Math.floor(this.nanoseconds).toString().padStart(3, "0");
-
- return `${daysPart}${hoursPart}:${minutesPart}:${secondsPart}.${millisecondsPart}${microsecondsPart}${nanosecondsPart}`;
- }
-}
diff --git a/src/util/util/Random.ts b/src/util/util/Random.ts
deleted file mode 100644
index b81e17cd..00000000
--- a/src/util/util/Random.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-// Inspired by dotnet: https://learn.microsoft.com/en-us/dotnet/api/system.random?view=net-9.0#methods
-export class Random {
- public static nextInt(min?: number, max?: number): number {
- if (min === undefined && max === undefined) {
- // Next()
- return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
- } else if (max === undefined) {
- // Next(Int32)
- if (min! <= 0) throw new RangeError("min must be greater than 0");
- return Math.floor(Math.random() * min!);
- } else {
- // Next(Int32, Int32)
- if (min! >= max!) throw new RangeError("min must be less than max");
- return Math.floor(Math.random() * (max! - min!)) + min!;
- }
- }
-
- public static nextDouble(min?: number, max?: number): number {
- if (min === undefined && max === undefined) {
- // NextDouble()
- return Math.random();
- } else if (max === undefined) {
- // NextDouble(Double)
- if (min! <= 0) throw new RangeError("min must be greater than 0");
- return Math.random() * min!;
- } else {
- // NextDouble(Double, Double)
- if (min! >= max!) throw new RangeError("min must be less than max");
- return Math.random() * (max! - min!) + min!;
- }
- }
-
- public static nextBytes(count: number): Uint8Array {
- if (count <= 0) throw new RangeError("count must be greater than 0");
- const arr = new Uint8Array(count);
- for (let i = 0; i < count; i++) {
- arr[i] = Math.floor(Math.random() * 256);
- }
- return arr;
- }
-
- public static nextBytesArray(count: number) {
- if (count <= 0) throw new RangeError("count must be greater than 0");
- const arr = [];
- for (let i = 0; i < count; i++) {
- arr.push(Math.floor(Math.random() * 256));
- }
- return arr;
- }
-
- public static getItems<T>(items: T[], count: number): T[] {
- if (count <= 0) throw new RangeError("count must be greater than 0");
- if (count >= items.length) return this.shuffle(items);
- const usedIndices = new Set<number>();
- const result: T[] = [];
- while (result.length < count && usedIndices.size < items.length) {
- const index = Math.floor(Math.random() * items.length);
- if (!usedIndices.has(index)) {
- usedIndices.add(index);
- result.push(items[index]);
- }
- }
- return result;
- }
-
- public static shuffle<T>(items: T[]): T[] {
- const array = [...items];
- for (let i = array.length - 1; i > 0; i--) {
- const j = Math.floor(Math.random() * (i + 1));
- [array[i], array[j]] = [array[j], array[i]];
- }
- return array;
- }
-}
diff --git a/src/util/util/Stopwatch.test.ts b/src/util/util/Stopwatch.test.ts
deleted file mode 100644
index 35618314..00000000
--- a/src/util/util/Stopwatch.test.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { describe, test } from "node:test";
-import assert from "node:assert/strict";
-import { Stopwatch, timePromise } from "./Stopwatch";
-
-describe("Stopwatch", () => {
- test("should be able to be initialised", () => {
- const sw = new Stopwatch();
- assert.equal(sw != null, true);
- });
-
- test("should measure elapsed time", async () => {
- const sw = Stopwatch.startNew();
- await new Promise((resolve) => void setTimeout(resolve, 101));
- sw.stop();
- const elapsed = sw.elapsed();
- assert(elapsed.totalMilliseconds >= 100, `Elapsed time was ${elapsed.totalMilliseconds} ms`);
- });
-
- test("should reset correctly", async () => {
- const sw = Stopwatch.startNew();
- await new Promise((resolve) => void setTimeout(resolve, 101));
- sw.stop();
- let elapsed = sw.elapsed();
- assert(elapsed.totalMilliseconds >= 100, `Elapsed time was ${elapsed.totalMilliseconds} ms`);
-
- sw.reset();
- await new Promise((resolve) => void setTimeout(resolve, 50));
- sw.stop();
- elapsed = sw.elapsed();
- assert(elapsed.totalMilliseconds >= 49 && elapsed.totalMilliseconds < 100, `Elapsed time after reset was ${elapsed.totalMilliseconds} ms`);
- });
-
- test("getElapsedAndReset should work correctly", async () => {
- const sw = Stopwatch.startNew();
- await new Promise((resolve) => void setTimeout(resolve, 101));
- sw.stop();
- let elapsed = sw.getElapsedAndReset();
- assert(elapsed.totalMilliseconds >= 100, `Elapsed time was ${elapsed.totalMilliseconds} ms`);
-
- await new Promise((resolve) => void setTimeout(resolve, 50));
- sw.stop();
- elapsed = sw.elapsed();
- assert(elapsed.totalMilliseconds >= 50 && elapsed.totalMilliseconds < 100, `Elapsed time after getElapsedAndReset was ${elapsed.totalMilliseconds} ms`);
- });
-
- test("timePromise should measure promise execution time", async () => {
- const { result, elapsed } = await timePromise(async () => {
- await new Promise((resolve) => void setTimeout(resolve, 101));
- return 42;
- });
- assert.equal(result, 42);
- assert(elapsed.totalMilliseconds >= 100, `Elapsed time was ${elapsed.totalMilliseconds} ms`);
- });
-});
diff --git a/src/util/util/Stopwatch.ts b/src/util/util/Stopwatch.ts
deleted file mode 100644
index 1f83c847..00000000
--- a/src/util/util/Stopwatch.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
-*/
-
-// Inspired by the dotnet Stopwatch class
-// Provides a simple interface to get elapsed time in high resolution
-import { ElapsedTime } from "./ElapsedTime";
-
-export class Stopwatch {
- private startTime: bigint;
- private endTime: bigint | null = null;
-
- static startNew(): Stopwatch {
- const stopwatch = new Stopwatch();
- stopwatch.start();
- return stopwatch;
- }
-
- start(): void {
- this.startTime = process.hrtime.bigint();
- this.endTime = null;
- }
-
- reset(): void {
- this.startTime = process.hrtime.bigint();
- this.endTime = null;
- }
-
- stop(): void {
- this.endTime = process.hrtime.bigint();
- }
-
- elapsed(): ElapsedTime {
- return new ElapsedTime((this.endTime ?? process.hrtime.bigint()) - this.startTime);
- }
-
- getElapsedAndReset(): ElapsedTime {
- const elapsed = this.elapsed();
- this.reset();
- return elapsed;
- }
-}
-
-export async function timePromise<T>(fn: () => Promise<T>): Promise<{ result: T; elapsed: ElapsedTime }> {
- const stopwatch = Stopwatch.startNew();
- const result = await fn();
- const elapsed = stopwatch.elapsed();
- return { result, elapsed };
-}
-
-export function timeFunction<T>(fn: () => T): { result: T; elapsed: ElapsedTime } {
- const stopwatch = Stopwatch.startNew();
- const result = fn();
- const elapsed = stopwatch.elapsed();
- return { result, elapsed };
-}
diff --git a/src/util/util/String.ts b/src/util/util/String.ts
deleted file mode 100644
index d0e75700..00000000
--- a/src/util/util/String.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
- Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
-*/
-
-import { SPECIAL_CHAR } from "./Regex";
-
-export function trimSpecial(str?: string): string {
- if (!str) return "";
- return str.replace(SPECIAL_CHAR, "").trim();
-}
-
-/**
- * Capitalizes the first letter of a string.
- * @param str The string to capitalize.
- * @returns The capitalized string.
- */
-export function capitalize(str: string): string {
- if (!str) return "";
- return str.charAt(0).toUpperCase() + str.slice(1);
-}
-
-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);
-}
-
-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/Timespan.test.ts b/src/util/util/Timespan.test.ts
deleted file mode 100644
index 02195506..00000000
--- a/src/util/util/Timespan.test.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-import { describe, test } from "node:test";
-import assert from "node:assert/strict";
-import { TimeSpan } from "./Timespan";
-
-describe("TimeSpan", () => {
- test("should be able to be initialised", () => {
- const db = new TimeSpan();
- assert.equal(db != null, true);
- });
-
- test("should be able to be initialised with start and end", () => {
- const now = Date.now();
- const later = now + 5000;
- const ts = new TimeSpan(now, later);
- assert.equal(ts.start, now);
- assert.equal(ts.end, later);
- });
-
- test("should be able to be initialised with start and end (fromDates static method)", () => {
- const now = Date.now();
- const later = now + 5000;
- const ts = TimeSpan.fromDates(now, later);
- assert.equal(ts.start, now);
- assert.equal(ts.end, later);
- });
-
- test("should throw error if start is greater than end", () => {
- assert.throws(() => {
- new TimeSpan(2000, 1000);
- }, /Start time must be less than or equal to end time./);
- });
-
- test("should be able to return zero", () => {
- const ts = new TimeSpan();
- assert.equal(ts.totalMillis, 0);
- });
-
- test("should be able to return timespan from milliseconds", () => {
- const ts = TimeSpan.fromMillis(1000);
- assert.equal(ts.totalMillis, 1000);
- assert.equal(ts.totalSeconds, 1);
- });
-
- test("should be able to return timespan from seconds", () => {
- const ts = TimeSpan.fromSeconds(60);
- assert.equal(ts.totalMillis, 60000);
- assert.equal(ts.totalSeconds, 60);
- assert.equal(ts.totalMinutes, 1);
- assert.equal(ts.minutes, 1);
- assert.equal(ts.hours, 0);
- assert.equal(ts.days, 0);
- });
-
- test("should be pure", () => {
- const count = 10;
- const timestamps = [];
- for (let i = 0; i < count; i++) {
- timestamps.push(TimeSpan.fromMillis(8972347984));
- for (const ts2 of timestamps) {
- assert.equal(ts2.totalMillis, 8972347984);
- assert.equal(ts2.totalSeconds, 8972347);
- assert.equal(ts2.totalMinutes, 149539);
- assert.equal(ts2.totalHours, 2492);
- assert.equal(ts2.totalDays, 103);
- assert.equal(ts2.totalWeeks, 14);
- assert.equal(ts2.totalMonths, 3);
- assert.equal(ts2.totalYears, 0);
-
- assert.equal(ts2.millis, 984);
- assert.equal(ts2.seconds, 7);
- assert.equal(ts2.minutes, 19);
- assert.equal(ts2.hours, 20);
- assert.equal(ts2.days, 12);
- assert.equal(ts2.weekDays, 5);
- assert.equal(ts2.weeks, 1);
- assert.equal(ts2.months, 3);
- assert.equal(ts2.years, 0);
- }
- }
- });
-
- test("should be able to stringify", () => {
- const ts = TimeSpan.fromMillis(8972347984);
- assert.equal(ts.toString(), "3 months, 1 weeks, 5 days, 20 hours, 19 minutes, 7 seconds, 984 milliseconds");
- assert.equal(ts.toString(true), "3 months, 1 weeks, 5 days, 20 hours, 19 minutes, 7 seconds, 984 milliseconds");
- assert.equal(ts.toString(true, false), "3 months, 1 weeks, 5 days, 20 hours, 19 minutes, 7 seconds");
- assert.equal(ts.toString(false), "3 months, 12 days, 20 hours, 19 minutes, 7 seconds, 984 milliseconds");
- assert.equal(ts.toString(false, false), "3 months, 12 days, 20 hours, 19 minutes, 7 seconds");
- });
-
- test("should be able to shortStringify", () => {
- const ts = TimeSpan.fromMillis(8972347984);
- assert.equal(ts.toShortString(), "3mo1w5d20h19m7s984ms");
- assert.equal(ts.toShortString(true), "3mo1w5d20h19m7s984ms");
- assert.equal(ts.toShortString(true, false), "3mo1w5d20h19m7s");
- assert.equal(ts.toShortString(false), "3mo12d20h19m7s984ms");
- assert.equal(ts.toShortString(false, false), "3mo12d20h19m7s");
- });
-
- test("should be able to shortStringify with spaces", () => {
- const ts = TimeSpan.fromMillis(8972347984);
- assert.equal(ts.toShortString(undefined, undefined, true), "3mo 1w 5d 20h 19m 7s 984ms");
- assert.equal(ts.toShortString(true, undefined, true), "3mo 1w 5d 20h 19m 7s 984ms");
- assert.equal(ts.toShortString(true, false, true), "3mo 1w 5d 20h 19m 7s");
- assert.equal(ts.toShortString(false, undefined, true), "3mo 12d 20h 19m 7s 984ms");
- assert.equal(ts.toShortString(false, false, true), "3mo 12d 20h 19m 7s");
- });
-
- test("should be able to return start date", () => {
- const now = Date.now();
- const later = now + 5000;
- const ts = new TimeSpan(now, later);
- assert.equal(ts.startDate.getTime(), now);
- });
-
- test("should be able to return end date", () => {
- const now = Date.now();
- const later = now + 5000;
- const ts = new TimeSpan(now, later);
- assert.equal(ts.endDate.getTime(), later);
- });
-});
diff --git a/src/util/util/Timespan.ts b/src/util/util/Timespan.ts
deleted file mode 100644
index ad0c1d10..00000000
--- a/src/util/util/Timespan.ts
+++ /dev/null
@@ -1,130 +0,0 @@
-/**
- * Represents a timespan with a start and end time.
- */
-export class TimeSpan {
- public readonly start: number;
- public readonly end: number;
- // constructors
- constructor(start = Date.now(), end = Date.now()) {
- if (start > end) {
- throw new Error("Start time must be less than or equal to end time.");
- }
- this.start = start;
- this.end = end;
- }
-
- static fromDates(startDate: number, endDate: number) {
- return new TimeSpan(startDate, endDate);
- }
-
- static fromMillis(millis: number) {
- return new TimeSpan(0, millis);
- }
-
- static fromSeconds(seconds: number) {
- return TimeSpan.fromMillis(seconds * 1000);
- }
-
- // methods
- get totalMillis() {
- return this.end - this.start;
- }
-
- get millis() {
- return Math.floor(this.totalMillis % 1000);
- }
-
- get totalSeconds() {
- return Math.floor(this.totalMillis / 1000);
- }
-
- get seconds() {
- return Math.floor((this.totalMillis / 1000) % 60);
- }
-
- get totalMinutes() {
- return Math.floor(this.totalMillis / 1000 / 60);
- }
-
- get minutes() {
- return Math.floor((this.totalMillis / 1000 / 60) % 60);
- }
-
- get totalHours() {
- return Math.floor(this.totalMillis / 1000 / 60 / 60);
- }
-
- get hours() {
- return Math.floor((this.totalMillis / 1000 / 60 / 60) % 24);
- }
-
- get totalDays() {
- return Math.floor(this.totalMillis / 1000 / 60 / 60 / 24);
- }
-
- get days() {
- return Math.floor((this.totalMillis / 1000 / 60 / 60 / 24) % 30.44); // Average days in a month
- }
-
- get weekDays() {
- return Math.floor((this.totalMillis / 1000 / 60 / 60 / 24) % 7);
- }
-
- get totalWeeks() {
- return Math.floor(this.totalMillis / 1000 / 60 / 60 / 24 / 7);
- }
-
- get weeks() {
- return Math.floor((this.totalMillis / 1000 / 60 / 60 / 24 / 7) % 4.345); // Average weeks in a month
- }
-
- get totalMonths() {
- return Math.floor(this.totalMillis / 1000 / 60 / 60 / 24 / 30.44); // Average days in a month
- }
-
- get months() {
- return Math.floor((this.totalMillis / 1000 / 60 / 60 / 24 / 30.44) % 12); // Average days in a month
- }
-
- get totalYears() {
- return Math.floor(this.totalMillis / 1000 / 60 / 60 / 24 / 365.25); // Average days in a year
- }
-
- get years() {
- return Math.floor(this.totalMillis / 1000 / 60 / 60 / 24 / 365.25); // Average days in a year
- }
-
- toString(includeWeeks = true, includeMillis = true) {
- const parts = [];
- if (this.totalYears >= 1) parts.push(`${this.totalYears} years`);
- if (this.totalMonths >= 1) parts.push(`${this.months} months`);
- if (includeWeeks && this.totalWeeks >= 1) parts.push(`${this.weeks} weeks`);
- if (this.totalDays >= 1) parts.push(`${includeWeeks ? this.weekDays : this.days} days`);
- if (this.totalHours >= 1) parts.push(`${this.hours} hours`);
- if (this.totalMinutes >= 1) parts.push(`${this.minutes} minutes`);
- if (this.totalSeconds >= 1) parts.push(`${this.seconds} seconds`);
- if (includeMillis) parts.push(`${this.millis} milliseconds`);
- return parts.join(", ");
- }
-
- toShortString(includeWeeks = true, includeMillis = true, withSpaces = false) {
- const parts = [];
- if (this.totalYears >= 1) parts.push(`${this.totalYears}y`);
- if (this.totalMonths >= 1) parts.push(`${this.months}mo`);
- if (includeWeeks && this.totalWeeks >= 1) parts.push(`${this.weeks}w`);
- if (this.totalDays >= 1) parts.push(`${includeWeeks ? this.weekDays : this.days}d`);
- if (this.totalHours >= 1) parts.push(`${this.hours}h`);
- if (this.totalMinutes >= 1) parts.push(`${this.minutes}m`);
- if (this.totalSeconds >= 1) parts.push(`${this.seconds}s`);
- if (includeMillis) parts.push(`${this.millis}ms`);
- return parts.join(withSpaces ? " " : "");
- }
-
- get startDate() {
- return new Date(this.start);
- }
-
- get endDate() {
- return new Date(this.end);
- }
-}
diff --git a/src/util/util/Token.ts b/src/util/util/Token.ts
index 10bd5e2c..e8cd9ecb 100644
--- a/src/util/util/Token.ts
+++ b/src/util/util/Token.ts
@@ -25,7 +25,7 @@ import { existsSync } from "node:fs";
// TODO: dont use deprecated APIs lol
import { FindOptionsRelationByString, FindOptionsSelectByString } from "typeorm";
import { randomUpperString } from "@spacebar/api";
-import { TimeSpan } from "./Timespan";
+import { TimeSpan } from "../../extensions/Timespan";
import { HTTPError } from "lambert-server/HTTPError";
import path from "node:path";
diff --git a/src/util/util/Url.ts b/src/util/util/Url.ts
deleted file mode 100644
index 7fa681bf..00000000
--- a/src/util/util/Url.ts
+++ /dev/null
@@ -1,41 +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 <https://www.gnu.org/licenses/>.
-*/
-
-export function normalizeUrl(input: string): string {
- try {
- const u = new URL(input);
- // Remove fragment
- u.hash = "";
- // Normalize pathname - remove trailing slash except for root "/"
- if (u.pathname !== "/" && u.pathname.endsWith("/")) {
- u.pathname = u.pathname.slice(0, -1);
- }
- // Normalize query params: sort by key
- if (u.search) {
- const params = Array.from(u.searchParams.entries());
- params.sort(([a], [b]) => a.localeCompare(b));
- u.search = params.length ? "?" + params.map(([k, v]) => `${k}=${v}`).join("&") : "";
- } else {
- // Ensure no empty search string
- u.search = "";
- }
- return u.toString();
- } catch (e) {
- return input;
- }
-}
diff --git a/src/util/util/index.ts b/src/util/util/index.ts
index d4f21d61..fb7c209c 100644
--- a/src/util/util/index.ts
+++ b/src/util/util/index.ts
@@ -22,9 +22,7 @@ export * from "./BitField";
export * from "./cdn";
export * from "./Config";
export * from "./Constants";
-export * from "./DateBuilder";
export * from "./email";
-export * from "./ElapsedTime";
export * from "./ipc/Event";
export * from "./FieldError";
export * from "./Intents";
@@ -39,9 +37,6 @@ export * from "./ipc/RabbitMQ";
export * from "./Regex";
export * from "./Rights";
export * from "./Snowflake";
-export * from "./Stopwatch";
-export * from "./String";
-export * from "./Timespan";
export * from "./Token";
export * from "./TraverseDirectory";
export * from "./WebAuthn";
@@ -49,7 +44,5 @@ export * from "./ChannelFlags";
export * from "./Gifs";
export * from "./Application";
export * from "./NameValidation";
-export * from "./Random";
-export * from "./Url";
export * from "./Version";
export * from "./Presence";
diff --git a/src/util/util/ipc/writer/UnixSocketWriter.ts b/src/util/util/ipc/writer/UnixSocketWriter.ts
index b50f53dc..e8b810a8 100644
--- a/src/util/util/ipc/writer/UnixSocketWriter.ts
+++ b/src/util/util/ipc/writer/UnixSocketWriter.ts
@@ -20,11 +20,12 @@ import net, { Socket } from "node:net";
import fs, { FSWatcher } from "node:fs";
import path from "node:path";
import { red } from "picocolors";
-import { BaseEventWriter } from "./BaseEventWriter";
-import { Event, Stopwatch } from "@spacebar/util";
-import { ProcessLifecycle } from "../../ProcessLifecycle";
-import { Monitoring } from "../../../monitoring/Monitoring";
import { Gauge } from "prom-client";
+import { Stopwatch } from "@spacebar/extensions";
+import { Event } from "@spacebar/util";
+import { Monitoring } from "../../../monitoring/Monitoring";
+import { ProcessLifecycle } from "../../ProcessLifecycle";
+import { BaseEventWriter } from "./BaseEventWriter";
export class UnixSocketWriter extends BaseEventWriter {
private static openConnectionsMetric: Gauge;
diff --git a/src/util/util/json/JsonSerializer.test.ts b/src/util/util/json/JsonSerializer.test.ts
index 5adf6c1e..ff5bb744 100644
--- a/src/util/util/json/JsonSerializer.test.ts
+++ b/src/util/util/json/JsonSerializer.test.ts
@@ -2,7 +2,7 @@ import { JsonSerializer } from "./JsonSerializer";
import { describe, it } from "node:test";
import { strict as assert } from "node:assert";
import fs from "node:fs/promises";
-import { Stopwatch } from "../Stopwatch";
+import { Stopwatch } from "../../../extensions/Stopwatch";
import { JsonValue } from "@protobuf-ts/runtime";
describe("JsonSerializer", () => {
diff --git a/src/util/util/networking/abuseipdb/AbuseIpDbClient.ts b/src/util/util/networking/abuseipdb/AbuseIpDbClient.ts
index b001fc75..327d052d 100644
--- a/src/util/util/networking/abuseipdb/AbuseIpDbClient.ts
+++ b/src/util/util/networking/abuseipdb/AbuseIpDbClient.ts
@@ -1,4 +1,23 @@
-import { Config, DateBuilder } from "@spacebar/util";
+/*
+ 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 <https://www.gnu.org/licenses/>.
+*/
+
+import { DateBuilder } from "@spacebar/extensions";
+import { Config } from "@spacebar/util";
import { AbuseIpDbBlacklistResponse, AbuseIpDbCheckResponse } from "./AbuseIpDbSampleResponses";
export class AbuseIpDbClient {
diff --git a/src/util/util/networking/ipdata/IpDataClient.ts b/src/util/util/networking/ipdata/IpDataClient.ts
index b1551389..f717e330 100644
--- a/src/util/util/networking/ipdata/IpDataClient.ts
+++ b/src/util/util/networking/ipdata/IpDataClient.ts
@@ -1,4 +1,23 @@
-import { Config, DateBuilder } from "@spacebar/util";
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2026 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 <https://www.gnu.org/licenses/>.
+*/
+
+import { DateBuilder } from "@spacebar/extensions";
+import { Config } from "@spacebar/util";
import { IpDataIpLookupResponse } from "./IpDataSampleResponses";
export class IpDataClient {
diff --git a/src/util/util/networking/stopforumspam/StopForumSpamClient.ts b/src/util/util/networking/stopforumspam/StopForumSpamClient.ts
index 7767915a..c64d6dc3 100644
--- a/src/util/util/networking/stopforumspam/StopForumSpamClient.ts
+++ b/src/util/util/networking/stopforumspam/StopForumSpamClient.ts
@@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { DateBuilder } from "@spacebar/util";
+import { DateBuilder } from "@spacebar/extensions";
// https://www.stopforumspam.com/usage
export class StopForumSpamClient {
diff --git a/src/util/util/workers/bcrypt/BcryptWorkerPool.ts b/src/util/util/workers/bcrypt/BcryptWorkerPool.ts
index 1d1705ef..6316adac 100644
--- a/src/util/util/workers/bcrypt/BcryptWorkerPool.ts
+++ b/src/util/util/workers/bcrypt/BcryptWorkerPool.ts
@@ -1,6 +1,6 @@
import { isMainThread, parentPort, Worker } from "node:worker_threads";
import { ProcessLifecycle } from "../../ProcessLifecycle";
-import { Stopwatch } from "../../Stopwatch";
+import { Stopwatch } from "../../../../extensions/Stopwatch";
import bcrypt from "bcrypt";
export class BcryptWorkerPool {
|