remove useless extensions (commits from spacebar)

This commit is contained in:
murdle 2025-12-08 01:35:46 +02:00
parent c8b738f28d
commit 4eb674be85
35 changed files with 106 additions and 560 deletions

Binary file not shown.

Binary file not shown.

BIN
package-lock.json generated

Binary file not shown.

View File

@ -74,6 +74,7 @@
},
"dependencies": {
"@aws-sdk/client-s3": "^3.936.0",
"@spacebarchat/medooze-webrtc": "^1.0.8",
"@toondepauw/node-zstd": "^1.2.0",
"@types/web-push": "^3.6.4",
"ajv": "^8.17.1",

View File

@ -121,7 +121,7 @@ function apiRoutes(missingRoutes) {
const tags = Array.from(routes.keys())
.map((x) => getTag(x))
.sort((a, b) => a.localeCompare(b));
specification.tags = tags.distinct().map((x) => ({ name: x }));
specification.tags = [...new Set(tags)].map((x) => ({ name: x }));
routes.forEach((route, pathAndMethod) => {
const [p, method] = pathAndMethod.split("|");
@ -213,7 +213,7 @@ function apiRoutes(missingRoutes) {
obj.parameters = [...(obj.parameters || []), ...query];
}
obj.tags = [...(obj.tags || []), getTag(p)].distinct();
obj.tags = [...new Set([...(obj.tags || []), getTag(p)])];
if (missingRoutes.additional.includes(path.replace(/\/$/, ""))) {
obj["x-badges"] = [

View File

@ -18,6 +18,7 @@
import { route } from "@spacebar/api";
import {
arrayRemove,
Channel,
emitEvent,
Emoji,
@ -112,7 +113,7 @@ router.delete(
const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name);
if (!already_added) throw new HTTPError("Reaction not found", 404);
message.reactions.remove(already_added);
arrayRemove(message.reactions, already_added);
await Promise.all([
message.save(),
@ -283,7 +284,7 @@ router.delete(
already_added.count--;
if (already_added.count <= 0) message.reactions.remove(already_added);
if (already_added.count <= 0) arrayRemove(message.reactions, already_added);
else already_added.user_ids.splice(already_added.user_ids.indexOf(user_id), 1);
await message.save();
@ -340,7 +341,7 @@ router.delete(
already_added.count--;
if (already_added.count <= 0) message.reactions.remove(already_added);
if (already_added.count <= 0) arrayRemove(message.reactions, already_added);
else already_added.user_ids.splice(already_added.user_ids.indexOf(user_id), 1);
await message.save();

View File

@ -39,6 +39,7 @@ import {
Relationship,
Rights,
Snowflake,
stringGlobToRegexp,
uploadFile,
User,
} from "@spacebar/util";
@ -244,24 +245,28 @@ router.get(
return x;
});
await ret
.filter((x: MessageCreateSchema) => x.interaction_metadata && !x.interaction_metadata.user)
.forEachAsync(async (x: MessageCreateSchema) => {
x.interaction_metadata!.user = x.interaction!.user = await User.findOneOrFail({ where: { id: (x as Message).interaction_metadata!.user_id } });
});
await Promise.all(
ret
.filter((x: MessageCreateSchema) => x.interaction_metadata && !x.interaction_metadata.user)
.map(async (x: MessageCreateSchema) => {
x.interaction_metadata!.user = x.interaction!.user = await User.findOneOrFail({ where: { id: (x as Message).interaction_metadata!.user_id } });
}),
);
// polyfill message references for old messages
await ret
.filter((msg) => msg.message_reference && !msg.referenced_message?.id)
.forEachAsync(async (msg) => {
const whereOptions: { id: string; guild_id?: string; channel_id?: string } = {
id: msg.message_reference!.message_id,
};
if (msg.message_reference!.guild_id) whereOptions.guild_id = msg.message_reference!.guild_id;
if (msg.message_reference!.channel_id) whereOptions.channel_id = msg.message_reference!.channel_id;
await Promise.all(
ret
.filter((msg) => msg.message_reference && !msg.referenced_message?.id)
.map(async (msg) => {
const whereOptions: { id: string; guild_id?: string; channel_id?: string } = {
id: msg.message_reference!.message_id,
};
if (msg.message_reference!.guild_id) whereOptions.guild_id = msg.message_reference!.guild_id;
if (msg.message_reference!.channel_id) whereOptions.channel_id = msg.message_reference!.channel_id;
msg.referenced_message = await Message.findOne({ where: whereOptions, relations: ["author", "mentions", "mention_roles", "mention_channels"] });
});
msg.referenced_message = await Message.findOne({ where: whereOptions, relations: ["author", "mentions", "mention_roles", "mention_channels"] });
}),
);
return res.json(ret);
},
@ -449,8 +454,8 @@ router.post(
if (rule.trigger_type == AutomodTriggerTypes.CUSTOM_WORDS) {
const triggerMeta = rule.trigger_metadata as AutomodCustomWordsRule;
const regexes = triggerMeta.regex_patterns.map((x) => new RegExp(x, "i")).concat(triggerMeta.keyword_filter.map((k) => k.globToRegexp("i")));
const allowedRegexes = triggerMeta.allow_list.map((k) => k.globToRegexp("i"));
const regexes = triggerMeta.regex_patterns.map((x) => new RegExp(x, "i")).concat(triggerMeta.keyword_filter.map((k) => stringGlobToRegexp(k, "i")));
const allowedRegexes = triggerMeta.allow_list.map((k) => stringGlobToRegexp(k, "i"));
const matches = regexes
.map((r) => message.content!.match(r))

View File

@ -47,10 +47,7 @@ router.put(
});
if (channel.type !== ChannelType.GROUP_DM) {
const recipients = [
...(channel.recipients?.map((r) => r.user_id) || []),
user_id,
].distinct();
const recipients = [...new Set([...(channel.recipients?.map((r) => r.user_id) || []), user_id])];
const new_channel = await Channel.createDMChannel(
recipients,

View File

@ -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()));
},

View File

@ -17,7 +17,7 @@
*/
import { Router, Request, Response } from "express";
import { DiscordApiErrors, Member } from "@spacebar/util";
import { DiscordApiErrors, Member, arrayPartition } from "@spacebar/util";
import { route } from "@spacebar/api";
const router = Router({ mergeParams: true });
@ -38,11 +38,7 @@ router.patch(
relations: ["roles"],
});
const [add, remove] = members.partition(
(member) =>
member_ids.includes(member.id) &&
!member.roles.map((role) => role.id).includes(role_id),
);
const [add, remove] = arrayPartition(members, (member) => member_ids.includes(member.id) && !member.roles.map((role) => role.id).includes(role_id));
// TODO (erkin): have a bulk add/remove function that adds the roles in a single txn
await Promise.all([

View File

@ -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[];

View File

@ -67,10 +67,8 @@ router.patch(
relations: ["settings"],
});
if (!user.settings)
user.settings = UserSettings.create(body as UserSettingsUpdateSchema);
else
user.settings.assign(body);
if (!user.settings) user.settings = UserSettings.create<UserSettings>(body);
else user.settings.assign(body);
if (body.guild_folders)
user.settings.guild_folders = body.guild_folders;

View File

@ -83,7 +83,7 @@ export async function Close(this: WebSocket, code: number, reason: Buffer) {
user_id: this.user_id,
data: sessions,
} as SessionsReplace);
const session = sessions.first() || {
const session = sessions[0] || {
activities: [],
client_status: {},
status: "offline",

View File

@ -27,6 +27,7 @@ import {
Presence,
Channel,
Permissions,
arrayPartition,
} from "@spacebar/util";
import {
WebSocket,
@ -51,16 +52,13 @@ const getMostRelevantSession = (sessions: Session[]) => {
invisible: 3,
offline: 4,
};
// sort sessions by relevance
sessions = sessions.sort((a, b) => {
return (
statusMap[a.status] -
statusMap[b.status] +
((a.activities?.length ?? 0) - (b.activities?.length ?? 0)) * 2
);
return statusMap[a.status] - statusMap[b.status] + ((a.activities?.length ?? 0) - (b.activities?.length ?? 0)) * 2;
});
return sessions.first();
return sessions[0];
};
async function getMembers(guild_id: string, range: [number, number]) {
@ -104,23 +102,19 @@ async function getMembers(guild_id: string, range: [number, number]) {
const groups = [];
const items = [];
const member_roles = members
.map((m) => m.roles)
.flat()
.distinctBy((r: Role) => r.id);
member_roles.push(
member_roles.splice(
member_roles.findIndex((x) => x.id === x.guild_id),
1,
)[0],
);
const member_roles = [
...new Map(
members
.map((m) => m.roles)
.flat()
.map((role) => [role.id, role] as [string, Role]),
).values(),
];
const offlineItems = [];
for (const role of member_roles) {
const [role_members, other_members] = members.partition(
(m: Member) => !!m.roles.find((r) => r.id === role.id),
);
const [role_members, other_members] = arrayPartition(members, (m: Member) => !!m.roles.find((r) => r.id === role.id));
const group = {
count: role_members.length,
id: role.id === guild_id ? "online" : role.id,
@ -257,7 +251,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) {
if (!channels) throw new Error("Must provide channel ranges");
const channel_id = Object.keys(channels || {}).first();
const channel_id = Object.keys(channels || {})[0];
if (!channel_id) return;
const permissions = await getPermission(this.user_id, guild_id, channel_id);
@ -302,10 +296,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) {
});
});
const groups = ops
.map((x) => x.groups)
.flat()
.distinct();
const groups = [...new Set(ops.map((x) => x.groups).flat())];
await Send(this, {
op: OPCODES.Dispatch,

View File

@ -255,7 +255,7 @@ export class Channel extends BaseClass {
}
static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) {
recipients = recipients.distinct().filter((x) => x !== creator_user_id);
recipients = [...new Set(recipients)].filter((x) => x !== creator_user_id);
// TODO: check config for max number of recipients
/** if you want to disallow note to self channels, uncomment the conditional below
@ -280,7 +280,7 @@ export class Channel extends BaseClass {
if (!ur.channel.recipients) continue;
const re = ur.channel.recipients.map((r) => r.user_id);
if (re.length === channelRecipients.length) {
if (re.containsAll(channelRecipients)) {
if (channelRecipients.every((_) => re.includes(_))) {
if (channel == null) {
channel = ur.channel;
await ur.assign({ closed: false }).save();
@ -474,7 +474,11 @@ export class Channel extends BaseClass {
userPerms = new Permissions(userPerms.remove(overwrite.deny).add(overwrite.allow));
// member overwrite, throws if somehow we have multiple overwrites for the same member
const memberOverwrite = this.permission_overwrites.single((o) => o.type === ChannelPermissionOverwriteType.member && o.id === member?.id);
const memberOverwrite = this.permission_overwrites.find(
(o) =>
o.type === ChannelPermissionOverwriteType.member &&
o.id === member?.id
);
if (memberOverwrite) userPerms = new Permissions(userPerms.remove(memberOverwrite.deny).add(memberOverwrite.allow));
}

View File

@ -30,6 +30,7 @@ import { Template } from "./Template";
import { User } from "./User";
import { VoiceState } from "./VoiceState";
import { Webhook } from "./Webhook";
import { arrayRemove } from "@spacebar/util";
// TODO: application_command_count, application_command_counts: {1: 0, 2: 0, 3: 0}
// TODO: guild_scheduled_events
@ -420,7 +421,7 @@ export class Guild extends BaseClass {
if (typeof insertPoint == "string") position = guild.channel_ordering.indexOf(insertPoint) + 1;
else position = insertPoint;
guild.channel_ordering.remove(channel_id);
arrayRemove(guild.channel_ordering, channel_id);
guild.channel_ordering.splice(position, 0, channel_id);
await Guild.update({ id: guild_id }, { channel_ordering: guild.channel_ordering });

View File

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

View File

@ -31,7 +31,7 @@ export function FieldErrors(fields: Record<string, { code?: string; message: str
return new FieldError(
50035,
"Invalid Form Body",
fields.map<ErrorContent, ObjectErrorContent>(({ message, code }) => ({
Object.values(fields).map(({ message, code }) => ({
_errors: [
{
message,
@ -39,7 +39,7 @@ export function FieldErrors(fields: Record<string, { code?: string; message: str
},
],
})),
errors
errors,
);
}
@ -51,7 +51,7 @@ export class FieldError extends Error {
public code: string | number,
public message: string,
public errors?: object, // TODO: I don't like this typing.
public _ajvErrors?: ErrorObject[]
public _ajvErrors?: ErrorObject[],
) {
super(message);
}

View File

@ -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\\");

View File

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

View File

@ -16,19 +16,6 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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());
};

View File

@ -1,96 +1,9 @@
import moduleAlias from "module-alias";
moduleAlias();
import './Array';
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import "./Array";
import { describe, it } from "node:test";
import assert from "node:assert/strict";
describe("Array extensions", () => {
it("containsAll", () => {
const arr = [1, 2, 3, 4, 5];
assert(arr.containsAll([1, 2]));
assert(!arr.containsAll([1, 6]));
assert(arr.containsAll([]));
assert([].containsAll([]));
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
assert(![].containsAll([1]));
});
it("partition", () => {
const arr = [1, 2, 3, 4, 5];
const [even, odd] = arr.partition((n) => n % 2 === 0);
assert.deepEqual(even, [2, 4]);
assert.deepEqual(odd, [1, 3, 5]);
});
it("single", () => {
const arr = [1, 2, 3, 4, 5];
assert.strictEqual(arr.single((n) => n === 3), 3);
assert.strictEqual(arr.single((n) => n === 6), null);
assert.throws(() => arr.single((n) => n > 2));
});
it("forEachAsync", async () => {
const arr = [1, 2, 3];
let sum = 0;
await arr.forEachAsync(async (n) => {
sum += n;
});
assert.strictEqual(sum, 6);
});
it("remove", () => {
const arr = [1, 2, 3, 4, 5];
arr.remove(3);
assert.deepEqual(arr, [1, 2, 4, 5]);
arr.remove(6);
assert.deepEqual(arr, [1, 2, 4, 5]);
});
it("first", () => {
const arr = [1, 2, 3];
assert.strictEqual(arr.first(), 1);
assert.strictEqual([].first(), undefined);
});
it("last", () => {
const arr = [1, 2, 3];
assert.strictEqual(arr.last(), 3);
assert.strictEqual([].last(), undefined);
});
it("distinct", () => {
const arr = [1, 2, 2, 3, 3, 3];
assert.deepEqual(arr.distinct(), [1, 2, 3]);
assert.deepEqual([].distinct(), []);
});
it("distinctBy", () => {
const arr = [{ id: 1 }, { id: 2 }, { id: 1 }, { id: 3 }];
assert.deepEqual(arr.distinctBy((x) => x.id), [{ id: 1 }, { id: 2 }, { id: 3 }]);
assert.deepEqual([].distinctBy((x) => x), []);
});
it("intersect", () => {
const arr1 = [1, 2, 3, 4];
const arr2 = [3, 4, 5, 6];
assert.deepEqual(arr1.intersect(arr2), [3, 4]);
assert.deepEqual(arr1.intersect([]), []);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
assert.deepEqual([].intersect(arr2), []);
});
it("except", () => {
const arr1 = [1, 2, 3, 4];
const arr2 = [3, 4, 5, 6];
assert.deepEqual(arr1.except(arr2), [1, 2]);
assert.deepEqual(arr1.except([]), [1, 2, 3, 4]);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
assert.deepEqual([].except(arr2), []);
});
});
//
});

View File

@ -18,24 +18,12 @@
declare global {
interface Array<T> {
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<void>): Promise<void>;
remove(item: T): void;
first(): T | undefined;
last(): T | undefined;
distinct(): T[];
distinctBy<K>(key: (elem: T) => K): T[];
intersect(other: T[]): T[];
except(other: T[]): T[];
/**
* @deprecated never use, idk why but I can't get rid of this without errors
*/
remove(h: T): never;
}
}
export function arrayContainsAll<T>(arr: T[], target: T[]) {
return target.every((v) => arr.includes(v));
}
/* https://stackoverflow.com/a/50636286 */
export function arrayPartition<T>(array: T[], filter: (elem: T) => boolean): [T[], T[]] {
const pass: T[] = [],
@ -44,99 +32,11 @@ export function arrayPartition<T>(array: T[], filter: (elem: T) => boolean): [T[
return [pass, fail];
}
export function arraySingle<T>(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<T>(array: T[], callback: (elem: T, index: number, array: T[]) => Promise<void>): Promise<void> {
await Promise.all(array.map(callback));
}
export function arrayRemove<T>(this: T[], item: T): void {
const index = this.indexOf(item);
export function arrayRemove<T>(array: T[], item: T): void {
const index = array.indexOf(item);
if (index > -1) {
this.splice(index, 1);
array.splice(index, 1);
}
}
export function arrayFirst<T>(this: T[]): T | undefined {
return this[0];
}
export function arrayLast<T>(this: T[]): T | undefined {
return this[this.length - 1];
}
export function arrayDistinct<T>(this: T[]): T[] {
return Array.from(new Set(this));
}
export function arrayDistinctBy<T, K>(this: T[], key: (elem: T) => K): T[] {
const seen = new Set<K>();
return this.filter((item) => {
const k = key(item);
if (seen.has(k)) {
return false;
} else {
seen.add(k);
return true;
}
});
}
export function arrayIntersect<T>(this: T[], other: T[]): T[] {
return this.filter((value) => other.includes(value));
}
export function arrayExcept<T>(this: T[], other: T[]): T[] {
return this.filter((value) => !other.includes(value));
}
// register extensions
if (!Array.prototype.containsAll)
Array.prototype.containsAll = function <T>(this: T[], target: T[]) {
return arrayContainsAll(this, target);
};
if (!Array.prototype.partition)
Array.prototype.partition = function <T>(this: T[], filter: (elem: T) => boolean) {
return arrayPartition(this, filter);
};
if (!Array.prototype.single)
Array.prototype.single = function <T>(this: T[], filter: (elem: T) => boolean) {
return arraySingle(this, filter);
};
if (!Array.prototype.forEachAsync)
Array.prototype.forEachAsync = function <T>(this: T[], callback: (elem: T, index: number, array: T[]) => Promise<void>) {
return arrayForEachAsync(this, callback);
};
if (!Array.prototype.remove)
Array.prototype.remove = function <T>(this: T[], item: T) {
return arrayRemove.call(this, item);
};
if (!Array.prototype.first)
Array.prototype.first = function <T>(this: T[]) {
return arrayFirst.call(this);
};
if (!Array.prototype.last)
Array.prototype.last = function <T>(this: T[]) {
return arrayLast.call(this);
};
if (!Array.prototype.distinct)
Array.prototype.distinct = function <T>(this: T[]) {
return arrayDistinct.call(this);
};
if (!Array.prototype.distinctBy)
Array.prototype.distinctBy = function <T, K>(this: T[], key: (elem: T) => K) {
return arrayDistinctBy.call(this, key as ((elem: unknown) => unknown));
};
if (!Array.prototype.intersect)
Array.prototype.intersect = function <T>(this: T[], other: T[]) {
return arrayIntersect.call(this, other);
};
if (!Array.prototype.except)
Array.prototype.except = function <T>(this: T[], other: T[]) {
return arrayExcept.call(this, other);
};

View File

@ -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`);
});
});

View File

@ -1,12 +0,0 @@
declare global {
function sleep(ms: number): Promise<void>;
}
export function globalSleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
if (!globalThis.sleep)
globalThis.sleep = function (ms: number): Promise<void> {
return globalSleep(ms);
};

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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;

View File

@ -1,26 +0,0 @@
import moduleAlias from "module-alias";
moduleAlias();
import "./Object";
import { describe, it } from "node:test";
import assert from "node:assert/strict";
describe("Object extensions", () => {
it("forEach", async () => {
const obj: { [index:string]: number } = { a: 1, b: 2, c: 3 };
const keys: string[] = [];
const values: number[] = [];
obj.forEach<number>((value, key, _) => {
keys.push(key);
values.push(value);
});
console.log(keys, values);
assert.deepEqual(keys, ["a", "b", "c"]);
assert.deepEqual(values, [1, 2, 3]);
});
it("map", async () => {
const obj = { a: 1, b: 2, c: 3 };
const result = obj.map((value, key) => `${key}:${value}`);
assert.deepEqual(result, { a: "a:1", b: "b:2", c: "c:3" });
});
});

View File

@ -1,43 +0,0 @@
declare global {
interface Object {
forEach<T>(callback: (value: T, key: string, object: { [index: string]: T }) => void): void;
map<SV, TV>(callback: (value: SV, key: string, object: { [index: string]: SV }) => TV): { [index: string]: TV };
}
}
export function objectForEach<T>(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<SV, TV>(srcObj: { [index: string]: SV }, callback: (value: SV, key: string, object: { [index: string]: SV }) => TV): { [index: string]: TV } {
if (typeof callback !== "function") throw new TypeError(`${callback} is not a function`);
const obj: { [index: string]: TV } = {};
Object.keys(srcObj).forEach((key) => {
obj[key] = callback(srcObj[key], key, srcObj);
});
return obj;
}
if (!Object.prototype.forEach)
Object.defineProperty(Object.prototype, "forEach", {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
value: function (cb) {
return objectForEach(this, cb);
},
enumerable: false,
writable: true,
});
if (!Object.prototype.map)
Object.defineProperty(Object.prototype, "map", {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
value: function (cb) {
return objectMap(this, cb);
},
enumerable: false,
writable: true,
});

View File

@ -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"));
});
});

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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);
};

View File

@ -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}"`);
}
});
});

View File

@ -1,5 +1 @@
export * from "./Array";
export * from "./Math";
export * from "./Url";
export * from "./Object";
export * from "./String";
export * from "./Array";

View File

@ -50,3 +50,4 @@ export * from "../../schemas/HelperTypes";
export * from "./extensions";
export * from "./Random";
export * from "./WebPush";
export * from "./Url";

View File

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