Fix merge issues

This commit is contained in:
MathMan05
2025-11-29 10:54:39 -06:00
34 changed files with 175 additions and 809 deletions
Binary file not shown.
Binary file not shown.
+15 -38
View File
@@ -20,9 +20,7 @@ require("module-alias/register");
const getRouteDescriptions = require("./util/getRouteDescriptions");
const path = require("path");
const fs = require("fs");
const {
NO_AUTHORIZATION_ROUTES,
} = require("../dist/api/middlewares/Authentication");
const { NO_AUTHORIZATION_ROUTES } = require("../dist/api/middlewares/Authentication");
require("../dist/util/util/extensions");
const { bgRedBright } = require("picocolors");
@@ -89,8 +87,7 @@ function combineSchemas(schemas) {
continue;
}
specification.components = specification.components || {};
specification.components.schemas =
specification.components.schemas || {};
specification.components.schemas = specification.components.schemas || {};
specification.components.schemas[key] = definitions[key];
delete definitions[key].additionalProperties;
delete definitions[key].$schema;
@@ -122,7 +119,7 @@ function apiRoutes(missingRoutes) {
const tags = Array.from(routes.keys())
.map((x) => getTag(x))
.sort((a, b) => a.localeCompare(b));
specification.tags = tags.distinct().map((x) => ({ name: x }));
specification.tags = [...new Set(tags)].map((x) => ({ name: x }));
routes.forEach((route, pathAndMethod) => {
const [p, method] = pathAndMethod.split("|");
@@ -135,8 +132,7 @@ function apiRoutes(missingRoutes) {
if (
!NO_AUTHORIZATION_ROUTES.some((x) => {
if (typeof x === "string")
return (method.toUpperCase() + " " + path).startsWith(x);
if (typeof x === "string") return (method.toUpperCase() + " " + path).startsWith(x);
return x.test(method.toUpperCase() + " " + path);
})
) {
@@ -177,9 +173,7 @@ function apiRoutes(missingRoutes) {
};
else
obj.responses[k] = {
description:
obj?.responses?.[k]?.description ||
"No description available",
description: obj?.responses?.[k]?.description || "No description available",
};
}
} else {
@@ -214,7 +208,7 @@ function apiRoutes(missingRoutes) {
obj.parameters = [...(obj.parameters || []), ...query];
}
obj.tags = [...(obj.tags || []), getTag(p)].distinct();
obj.tags = [...new Set([...(obj.tags || []), getTag(p)])];
if (missingRoutes.additional.includes(path.replace(/\/$/, ""))) {
obj["x-badges"] = [
@@ -225,45 +219,28 @@ function apiRoutes(missingRoutes) {
];
}
specification.paths[path] = Object.assign(
specification.paths[path] || {},
{
[method]: obj,
},
);
specification.paths[path] = Object.assign(specification.paths[path] || {}, {
[method]: obj,
});
});
}
async function main() {
console.log("Generating OpenAPI Specification...");
const routesRes = await fetch(
"https://github.com/spacebarchat/missing-routes/raw/main/missing.json",
{
headers: {
Accept: "application/json",
},
const routesRes = await fetch("https://github.com/spacebarchat/missing-routes/raw/main/missing.json", {
headers: {
Accept: "application/json",
},
);
});
const missingRoutes = await routesRes.json();
combineSchemas(schemas);
apiRoutes(missingRoutes);
fs.writeFileSync(
openapiPath,
JSON.stringify(specification, null, 4)
.replaceAll("#/definitions", "#/components/schemas")
.replaceAll("bigint", "number"),
);
fs.writeFileSync(openapiPath, JSON.stringify(specification, null, 4).replaceAll("#/definitions", "#/components/schemas").replaceAll("bigint", "number"));
console.log("Wrote OpenAPI specification to", openapiPath);
console.log(
"Specification contains",
Object.keys(specification.paths).length,
"paths and",
Object.keys(specification.components.schemas).length,
"schemas.",
);
console.log("Specification contains", Object.keys(specification.paths).length, "paths and", Object.keys(specification.components.schemas).length, "schemas.");
}
main();
@@ -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();
@@ -41,6 +41,7 @@ import {
Snowflake,
uploadFile,
User,
stringGlobToRegexp,
} from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
@@ -244,24 +245,28 @@ router.get(
return x;
});
await ret
.filter((x: MessageCreateSchema) => x.interaction_metadata && !x.interaction_metadata.user)
.forEachAsync(async (x: MessageCreateSchema) => {
x.interaction_metadata!.user = x.interaction!.user = await User.findOneOrFail({ where: { id: (x as Message).interaction_metadata!.user_id } });
});
await Promise.all(
ret
.filter((x: MessageCreateSchema) => x.interaction_metadata && !x.interaction_metadata.user)
.map(async (x: MessageCreateSchema) => {
x.interaction_metadata!.user = x.interaction!.user = await User.findOneOrFail({ where: { id: (x as Message).interaction_metadata!.user_id } });
}),
);
// polyfill message references for old messages
await ret
.filter((msg) => msg.message_reference && !msg.referenced_message?.id)
.forEachAsync(async (msg) => {
const whereOptions: { id: string; guild_id?: string; channel_id?: string } = {
id: msg.message_reference!.message_id,
};
if (msg.message_reference!.guild_id) whereOptions.guild_id = msg.message_reference!.guild_id;
if (msg.message_reference!.channel_id) whereOptions.channel_id = msg.message_reference!.channel_id;
await Promise.all(
ret
.filter((msg) => msg.message_reference && !msg.referenced_message?.id)
.map(async (msg) => {
const whereOptions: { id: string; guild_id?: string; channel_id?: string } = {
id: msg.message_reference!.message_id,
};
if (msg.message_reference!.guild_id) whereOptions.guild_id = msg.message_reference!.guild_id;
if (msg.message_reference!.channel_id) whereOptions.channel_id = msg.message_reference!.channel_id;
msg.referenced_message = await Message.findOne({ where: whereOptions, relations: ["author", "mentions", "mention_roles", "mention_channels"] });
});
msg.referenced_message = await Message.findOne({ where: whereOptions, relations: ["author", "mentions", "mention_roles", "mention_channels"] });
}),
);
return res.json(ret);
},
@@ -449,8 +454,8 @@ router.post(
if (rule.trigger_type == AutomodTriggerTypes.CUSTOM_WORDS) {
const triggerMeta = rule.trigger_metadata as AutomodCustomWordsRule;
const regexes = triggerMeta.regex_patterns.map((x) => new RegExp(x, "i")).concat(triggerMeta.keyword_filter.map((k) => k.globToRegexp("i")));
const allowedRegexes = triggerMeta.allow_list.map((k) => k.globToRegexp("i"));
const regexes = triggerMeta.regex_patterns.map((x) => new RegExp(x, "i")).concat(triggerMeta.keyword_filter.map((k) => stringGlobToRegexp(k, "i")));
const allowedRegexes = triggerMeta.allow_list.map((k) => stringGlobToRegexp(k, "i"));
const matches = regexes
.map((r) => message.content!.match(r))
@@ -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?
+7 -5
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()));
},
@@ -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;
+2 -2
View File
@@ -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[];
+26 -74
View File
@@ -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<Message>[] = [
{
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 {
+1 -1
View File
@@ -67,7 +67,7 @@ router.patch(
relations: ["settings"],
});
if (!user.settings) user.settings = UserSettings.create(body as UserSettingsUpdateSchema);
if (!user.settings) user.settings = UserSettings.create<UserSettings>(body);
else user.settings.assign(body);
if (body.guild_folders) user.settings.guild_folders = body.guild_folders;
+3 -16
View File
@@ -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",
+29 -84
View File
@@ -16,28 +16,11 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
getDatabase,
getPermission,
listenEvent,
Member,
Role,
Session,
User,
Presence,
Channel,
Permissions,
} from "@spacebar/util";
import {
WebSocket,
Payload,
handlePresenceUpdate,
OPCODES,
Send,
} from "@spacebar/gateway";
import { getDatabase, getPermission, listenEvent, Member, Role, Session, User, Presence, Channel, Permissions, arrayPartition } from "@spacebar/util";
import { WebSocket, Payload, handlePresenceUpdate, OPCODES, Send } from "@spacebar/gateway";
import murmur from "murmurhash-js/murmurhash3_gc";
import { check } from "./instanceOf";
import { LazyRequestSchema } from "@spacebar/schemas"
import { LazyRequestSchema } from "@spacebar/schemas";
// TODO: only show roles/members that have access to this channel
// TODO: config: to list all members (even those who are offline) sorted by role, or just those who are online
@@ -53,14 +36,10 @@ const getMostRelevantSession = (sessions: Session[]) => {
};
// sort sessions by relevance
sessions = sessions.sort((a, b) => {
return (
statusMap[a.status] -
statusMap[b.status] +
((a.activities?.length ?? 0) - (b.activities?.length ?? 0)) * 2
);
return statusMap[a.status] - statusMap[b.status] + ((a.activities?.length ?? 0) - (b.activities?.length ?? 0)) * 2;
});
return sessions.first();
return sessions[0];
};
async function getMembers(guild_id: string, range: [number, number]) {
@@ -79,10 +58,7 @@ async function getMembers(guild_id: string, range: [number, number]) {
.leftJoinAndSelect("member.user", "user")
.leftJoinAndSelect("user.sessions", "session")
.addSelect("user.settings")
.addSelect(
"CASE WHEN session.status IS NULL OR session.status = 'offline' OR session.status = 'invisible' THEN 0 ELSE 1 END",
"_status",
)
.addSelect("CASE WHEN session.status IS NULL OR session.status = 'offline' OR session.status = 'invisible' THEN 0 ELSE 1 END", "_status")
.orderBy("_status", "DESC")
.addOrderBy("role.position", "DESC")
.addOrderBy("user.username", "ASC")
@@ -104,10 +80,14 @@ async function getMembers(guild_id: string, range: [number, number]) {
const groups = [];
const items = [];
const member_roles = members
.map((m) => m.roles)
.flat()
.distinctBy((r: Role) => r.id);
const member_roles = [
...new Map(
members
.map((m) => m.roles)
.flat()
.map((role) => [role.id, role] as [string, Role]),
).values(),
];
member_roles.push(
member_roles.splice(
member_roles.findIndex((x) => x.id === x.guild_id),
@@ -118,9 +98,7 @@ async function getMembers(guild_id: string, range: [number, number]) {
const offlineItems = [];
for (const role of member_roles) {
const [role_members, other_members] = members.partition(
(m: Member) => !!m.roles.find((r) => r.id === role.id),
);
const [role_members, other_members] = arrayPartition(members, (m: Member) => !!m.roles.find((r) => r.id === role.id));
const group = {
count: role_members.length,
id: role.id === guild_id ? "online" : role.id,
@@ -130,13 +108,9 @@ async function getMembers(guild_id: string, range: [number, number]) {
groups.push(group);
for (const member of role_members) {
const roles = member.roles
.filter((x: Role) => x.id !== guild_id)
.map((x: Role) => x.id);
const roles = member.roles.filter((x: Role) => x.id !== guild_id).map((x: Role) => x.id);
const session: Session | undefined = getMostRelevantSession(
member.user.sessions,
);
const session: Session | undefined = getMostRelevantSession(member.user.sessions);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
@@ -157,11 +131,7 @@ async function getMembers(guild_id: string, range: [number, number]) {
},
};
if (
!session ||
session.status == "invisible" ||
session.status == "offline"
) {
if (!session || session.status == "invisible" || session.status == "offline") {
item.member.presence.status = "offline";
offlineItems.push(item);
group.count--;
@@ -188,24 +158,14 @@ async function getMembers(guild_id: string, range: [number, number]) {
items,
groups,
range,
members: items
.map((x) =>
"member" in x
? { ...x.member, settings: undefined }
: undefined,
)
.filter((x) => !!x),
members: items.map((x) => ("member" in x ? { ...x.member, settings: undefined } : undefined)).filter((x) => !!x),
};
}
async function subscribeToMemberEvents(this: WebSocket, user_id: string) {
if (this.events[user_id]) return false; // already subscribed as friend
if (this.member_events[user_id]) return false; // already subscribed in member list
this.member_events[user_id] = await listenEvent(
user_id,
handlePresenceUpdate.bind(this),
this.listen_options,
);
this.member_events[user_id] = await listenEvent(user_id, handlePresenceUpdate.bind(this), this.listen_options);
return true;
}
@@ -213,8 +173,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) {
const startTime = Date.now();
// TODO: check data
check.call(this, LazyRequestSchema, d);
const { guild_id, typing, channels, activities, members } =
d as LazyRequestSchema;
const { guild_id, typing, channels, activities, members } = d as LazyRequestSchema;
if (members) {
// Client has requested a PRESENCE_UPDATE for specific member
@@ -222,10 +181,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) {
await Promise.all([
members.map(async (x) => {
if (!x) return;
const didSubscribe = await subscribeToMemberEvents.call(
this,
x,
);
const didSubscribe = await subscribeToMemberEvents.call(this, x);
if (!didSubscribe) return;
// if we didn't subscribe just now, this is a new subscription
@@ -257,7 +213,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) {
if (!channels) throw new Error("Must provide channel ranges");
const channel_id = Object.keys(channels || {}).first();
const channel_id = Object.keys(channels || {})[0];
if (!channel_id) return;
const permissions = await getPermission(this.user_id, guild_id, channel_id);
@@ -267,9 +223,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) {
if (!Array.isArray(ranges)) throw new Error("Not a valid Array");
const member_count = await Member.count({ where: { guild_id } });
const ops = await Promise.all(
ranges.map((x) => getMembers(guild_id, x as [number, number])),
);
const ops = await Promise.all(ranges.map((x) => getMembers(guild_id, x as [number, number])));
let list_id = "everyone";
@@ -282,10 +236,8 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) {
channel.permission_overwrites.forEach((overwrite) => {
const { id, allow, deny } = overwrite;
if (BigInt(allow) & Permissions.FLAGS.VIEW_CHANNEL)
perms.push(`allow:${id}`);
else if (BigInt(deny) & Permissions.FLAGS.VIEW_CHANNEL)
perms.push(`deny:${id}`);
if (BigInt(allow) & Permissions.FLAGS.VIEW_CHANNEL) perms.push(`allow:${id}`);
else if (BigInt(deny) & Permissions.FLAGS.VIEW_CHANNEL) perms.push(`deny:${id}`);
});
if (perms.length > 0) {
@@ -302,10 +254,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) {
});
});
const groups = ops
.map((x) => x.groups)
.flat()
.distinct();
const groups = [...new Set(ops.map((x) => x.groups).flat())];
await Send(this, {
op: OPCODES.Dispatch,
@@ -317,9 +266,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) {
op: "SYNC",
range: x.range,
})),
online_count:
member_count -
(groups.find((x) => x.id == "offline")?.count ?? 0),
online_count: member_count - (groups.find((x) => x.id == "offline")?.count ?? 0),
member_count,
id: list_id,
guild_id,
@@ -327,7 +274,5 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) {
},
});
console.log(
`[Gateway] LAZY_REQUEST ${guild_id} ${channel_id} took ${Date.now() - startTime}ms`,
);
console.log(`[Gateway] LAZY_REQUEST ${guild_id} ${channel_id} took ${Date.now() - startTime}ms`);
}
+4 -4
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();
@@ -429,7 +429,7 @@ export class Channel extends BaseClass {
}
async getUserPermissions(opts: { user_id?: string; user?: User; member?: Member; guild?: Guild }): Promise<Permissions> {
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
+2 -2
View File
@@ -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 });
+2 -2
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];
}
}
+3 -3
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);
}
+6 -13
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\\");
+7 -1
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);
}
@@ -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());
};
+5 -98
View File
@@ -1,102 +1,9 @@
import moduleAlias from "module-alias";
moduleAlias();
import './Array';
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import "./Array";
import { describe, it } from "node:test";
import assert from "node:assert/strict";
describe("Array extensions", () => {
it("containsAll", () => {
const arr = [1, 2, 3, 4, 5];
assert(arr.containsAll([1, 2]));
assert(!arr.containsAll([1, 6]));
assert(arr.containsAll([]));
assert([].containsAll([]));
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
assert(![].containsAll([1]));
});
it("partition", () => {
const arr = [1, 2, 3, 4, 5];
const [even, odd] = arr.partition((n) => n % 2 === 0);
assert.deepEqual(even, [2, 4]);
assert.deepEqual(odd, [1, 3, 5]);
});
it("single", () => {
const arr = [1, 2, 3, 4, 5];
assert.strictEqual(arr.single((n) => n === 3), 3);
assert.strictEqual(arr.single((n) => n === 6), null);
assert.throws(() => arr.single((n) => n > 2));
});
it("forEachAsync", async () => {
const arr = [1, 2, 3];
let sum = 0;
await arr.forEachAsync(async (n) => {
sum += n;
});
assert.strictEqual(sum, 6);
});
it("filterAsync", async () => {
const arr = [1, 2, 3, 4, 5];
const even = await arr.filterAsync(async (n) => n % 2 === 0);
assert.deepEqual(even, [2, 4]);
});
it("remove", () => {
const arr = [1, 2, 3, 4, 5];
arr.remove(3);
assert.deepEqual(arr, [1, 2, 4, 5]);
arr.remove(6);
assert.deepEqual(arr, [1, 2, 4, 5]);
});
it("first", () => {
const arr = [1, 2, 3];
assert.strictEqual(arr.first(), 1);
assert.strictEqual([].first(), undefined);
});
it("last", () => {
const arr = [1, 2, 3];
assert.strictEqual(arr.last(), 3);
assert.strictEqual([].last(), undefined);
});
it("distinct", () => {
const arr = [1, 2, 2, 3, 3, 3];
assert.deepEqual(arr.distinct(), [1, 2, 3]);
assert.deepEqual([].distinct(), []);
});
it("distinctBy", () => {
const arr = [{ id: 1 }, { id: 2 }, { id: 1 }, { id: 3 }];
assert.deepEqual(arr.distinctBy((x) => x.id), [{ id: 1 }, { id: 2 }, { id: 3 }]);
assert.deepEqual([].distinctBy((x) => x), []);
});
it("intersect", () => {
const arr1 = [1, 2, 3, 4];
const arr2 = [3, 4, 5, 6];
assert.deepEqual(arr1.intersect(arr2), [3, 4]);
assert.deepEqual(arr1.intersect([]), []);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
assert.deepEqual([].intersect(arr2), []);
});
it("except", () => {
const arr1 = [1, 2, 3, 4];
const arr2 = [3, 4, 5, 6];
assert.deepEqual(arr1.except(arr2), [1, 2]);
assert.deepEqual(arr1.except([]), [1, 2, 3, 4]);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
assert.deepEqual([].except(arr2), []);
});
});
//
});
+7 -117
View File
@@ -18,25 +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>;
filterAsync(callback: (elem: T, index: number, array: T[]) => Promise<boolean>): Promise<T[]>;
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[] = [],
@@ -45,108 +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 async function arrayFilterAsync<T>(array: T[], callback: (elem: T, index: number, array: T[]) => Promise<boolean>): Promise<T[]> {
const results = await Promise.all(array.map(callback));
return array.filter((_, index) => results[index]);
}
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.filterAsync)
Array.prototype.filterAsync = function <T>(this: T[], callback: (elem: T, index: number, array: T[]) => Promise<boolean>) {
return arrayFilterAsync(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);
};
-16
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`);
});
});
-12
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);
};
-19
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);
});
});
-31
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;
-26
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" });
});
});
-43
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,
});
-15
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"));
});
});
-37
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);
};
-37
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}"`);
}
});
});
-4
View File
@@ -1,5 +1 @@
export * from "./Array";
export * from "./Math";
export * from "./Url";
export * from "./Object";
export * from "./String";
+2 -1
View File
@@ -48,4 +48,5 @@ export * from "./Application";
export * from "./NameValidation";
export * from "../../schemas/HelperTypes";
export * from "./extensions";
export * from "./Random";
export * from "./Random";
export * from "./Url";
+7 -11
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}`;