Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions assets/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -11700,10 +11700,7 @@
"type": "string"
}
},
"additionalProperties": false,
"required": [
"text"
]
"additionalProperties": false
},
"image": {
"$ref": "#/components/schemas/EmbedImage"
Expand Down
5 changes: 1 addition & 4 deletions assets/schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -12433,10 +12433,7 @@
"type": "string"
}
},
"additionalProperties": false,
"required": [
"text"
]
"additionalProperties": false
},
"image": {
"$ref": "#/definitions/EmbedImage"
Expand Down
4 changes: 4 additions & 0 deletions src/api/routes/channels/#channel_id/messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ router.get(
}

messages = await Message.find(query);

if (after) {
messages.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
}
}

await Message.fillReplies(messages);
Expand Down
14 changes: 13 additions & 1 deletion src/api/routes/channels/#channel_id/typing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*/

import { route } from "@spacebar/api";
import { Channel, emitEvent, Member, TypingStartEvent } from "@spacebar/util";
import { Channel, DiscordApiErrors, emitEvent, getPermission, Member, Message, TypingStartEvent } from "@spacebar/util";
import { Request, Response, Router } from "express";

const router: Router = Router({ mergeParams: true });
Expand All @@ -39,6 +39,18 @@ router.post(
const channel = await Channel.findOneOrFail({
where: { id: channel_id },
});

if (channel.rate_limit_per_user) {
const lastMsgTime = (await Message.findOne({ where: { channel_id: channel.id, author_id: user_id }, select: { timestamp: true }, order: { timestamp: "DESC" } }))
?.timestamp;
if (lastMsgTime && Date.now() - channel.rate_limit_per_user * 1000 < +lastMsgTime) {
const permission = await getPermission(user_id, channel.guild_id, channel_id);
if (!permission.has("MANAGE_MESSAGES") && !permission.has("MANAGE_CHANNELS") && !permission.has("BYPASS_SLOWMODE")) {
throw DiscordApiErrors.SLOWMODE_RATE_LIMIT;
}
}
}

const member = await Member.findOne({
where: { id: user_id, guild_id: channel.guild_id },
relations: { roles: true, user: true },
Expand Down
3 changes: 1 addition & 2 deletions src/api/routes/channels/#channel_id/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,8 @@ router.post(
let { avatar, name } = req.body as WebhookCreateSchema;
name = trimSpecial(name);

// TODO: move this
if (name) {
ValidateName(name);
ValidateName(name, "name", 80);
}

if (avatar) avatar = await handleFile(`/avatars/${channel_id}`, avatar);
Expand Down
25 changes: 24 additions & 1 deletion src/api/routes/guilds/#guild_id/members/#member_id/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,21 @@
*/

import { route } from "@spacebar/api";
import { Config, DiscordApiErrors, emitEvent, Emoji, getPermission, getRights, Guild, GuildMemberUpdateEvent, handleFile, Member, Role, Sticker } from "@spacebar/util";
import {
Config,
DiscordApiErrors,
emitEvent,
Emoji,
FieldErrors,
getPermission,
getRights,
Guild,
GuildMemberUpdateEvent,
handleFile,
Member,
Role,
Sticker,
} from "@spacebar/util";
import { Request, Response, Router } from "express";
import { MemberChangeSchema, PublicMemberProjection, PublicUserProjection } from "@spacebar/schemas";

Expand Down Expand Up @@ -102,6 +116,15 @@ router.patch(
permission.hasThrow("CHANGE_NICKNAME");
}

if (body.nick && body.nick.length > 32) {
throw FieldErrors({
nick: {
code: "BASE_TYPE_BAD_LENGTH",
message: "Must be between 1 and 32 in length.",
},
});
}

if (!body.nick) {
delete body.nick;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand Down
5 changes: 4 additions & 1 deletion src/api/routes/guilds/#guild_id/roles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ router.post(
if (body.name && body.name.length > 255) throw new Error("Role name must not exceed 255 characters");

const everyoneRole = await Role.findOne({ where: { id: guild_id } });
const defaultPermissions = everyoneRole?.permissions || "0";

const role = Role.create({
// values before ...body are default and can be overridden
Expand All @@ -74,7 +75,9 @@ router.post(
...body,
guild_id: guild_id,
managed: false,
permissions: String((req.permission?.bitfield || 0n) & BigInt(body.permissions || everyoneRole?.permissions || 0)),
permissions: body.permissions
? String((req.permission?.bitfield || 0n) & BigInt(body.permissions))
: String((req.permission?.bitfield || 0n) & BigInt(defaultPermissions)),
tags: undefined,
icon: undefined,
unicode_emoji: undefined,
Expand Down
2 changes: 1 addition & 1 deletion src/api/routes/users/#user_id/relationships.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ router.get(
username: relation_user.username,
avatar: relation_user.avatar,
discriminator: relation_user.discriminator,
public_flags: relation_user.public_flags,
public_flags: Number(relation_user.public_flags) || 0,
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/api/routes/webhooks/#webhook_id/#token/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ router.patch(
throw new HTTPError("Empty webhook updates are not allowed", 50006);
}
if (body.name) {
ValidateName(body.name);
ValidateName(body.name, "name", 80);
}
if (body.avatar) {
body.avatar = await handleFile(`/avatars/${webhook_id}`, body.avatar as string);
Expand Down
2 changes: 1 addition & 1 deletion src/api/routes/webhooks/#webhook_id/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ router.patch(
if (body.avatar) body.avatar = await handleFile(`/avatars/${webhook_id}`, body.avatar as string);

if (body.name) {
ValidateName(body.name);
ValidateName(body.name, "name", 80);
}

const channel_id = body.channel_id || webhook.channel_id;
Expand Down
29 changes: 10 additions & 19 deletions src/api/util/utility/EmbedHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -653,35 +653,26 @@ export async function fillMessageUrlEmbeds(message: Message) {
const linkMatches = getMessageContentUrls(message).filter((l) => !l.startsWith("<") && !l.endsWith(">"));

// Filter out embeds that could be links, start from scratch
message.embeds = message.embeds.filter((embed) => embed.type === "rich");
const richEmbeds = message.embeds.filter((embed) => embed.type === "rich");
message.embeds = richEmbeds;

if (linkMatches.length == 0) return message;

const uniqueLinks: string[] = arrayDistinctBy(linkMatches, normalizeUrl);

if (uniqueLinks.length === 0) {
// No valid unique links found, update message to remove old embeds
message.embeds = message.embeds.filter((embed) => embed.type === "rich");
await saveAndEmitMessageUpdate(message);
return message;
}
if (uniqueLinks.length === 0) return message;

// avoid a race condition updating the same row
let messageUpdateLock = saveAndEmitMessageUpdate(message);
await getOrUpdateEmbedCache(uniqueLinks, async (url, embeds) => {
if (url !== "cached" && message.embeds.length + embeds.length > Config.get().limits.message.maxEmbeds) return;
let embedsChanged = false;
await getOrUpdateEmbedCache(uniqueLinks, async (_url, embeds) => {
if (_url !== "cached" && message.embeds.length + embeds.length > Config.get().limits.message.maxEmbeds) return;
message.embeds.push(...embeds);
if (message.embeds.length > Config.get().limits.message.maxEmbeds) message.embeds = message.embeds.slice(0, Config.get().limits.message.maxEmbeds);

try {
await messageUpdateLock;
} catch {
/* empty */
}
messageUpdateLock = saveAndEmitMessageUpdate(message);
embedsChanged = true;
});

await saveAndEmitMessageUpdate(message);
if (embedsChanged) {
await saveAndEmitMessageUpdate(message);
}
return message;
}

Expand Down
2 changes: 1 addition & 1 deletion src/schemas/api/messages/Embeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface Embed {
timestamp?: Date; // timestamp of embed content
color?: number; // color code of the embed
footer?: {
text: string;
text?: string;
icon_url?: string;
proxy_icon_url?: string;
}; // footer object footer information
Expand Down
2 changes: 1 addition & 1 deletion src/util/dtos/UserDTO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class MinimalPublicUserDTO {
this.avatar = user.avatar;
this.discriminator = user.discriminator;
this.id = user.id;
this.public_flags = user.public_flags;
this.public_flags = Number(user.public_flags) || 0;
this.username = user.username;
this.badge_ids = user.badge_ids;
}
Expand Down
3 changes: 1 addition & 2 deletions src/util/entities/Channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,8 +530,7 @@ export class Channel extends BaseClass {
});
}

if (recipients.length === 1) return channel_dto;
else return channel_dto.excludedRecipients([creator_user_id]);
return channel_dto.excludedRecipients([creator_user_id]);
}

static async checkServerDmReopenPrivacy(channel: Channel, creatorUserId: string) {
Expand Down
11 changes: 10 additions & 1 deletion src/util/entities/Member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { BeforeInsert, BeforeUpdate, Column, Entity, Index, JoinColumn, JoinTabl
import { Ban, Channel, PublicGuildRelations } from ".";
import { ReadyGuildDTO } from "../dtos";
import { GuildCreateEvent, GuildDeleteEvent, GuildMemberAddEvent, GuildMemberRemoveEvent, GuildMemberUpdateEvent, MessageCreateEvent } from "../interfaces";
import { Config, emitEvent, DiscordApiErrors, Stopwatch } from "../util";
import { Config, emitEvent, DiscordApiErrors, FieldErrors, Stopwatch } from "../util";
import { BaseClassWithoutId } from "./BaseClass";
import { Guild } from "./Guild";
import { Message } from "./Message";
Expand Down Expand Up @@ -277,6 +277,15 @@ export class Member extends BaseClassWithoutId {
}

static async changeNickname(user_id: string, guild_id: string, nickname: string) {
if (nickname && nickname.length > 32) {
throw FieldErrors({
nick: {
code: "BASE_TYPE_BAD_LENGTH",
message: "Must be between 1 and 32 in length.",
},
});
}

const member = await Member.findOneOrFail({
where: {
id: user_id,
Expand Down
27 changes: 18 additions & 9 deletions src/util/util/NameValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,39 +18,48 @@

import { Config } from "./Config";
import { FieldErrors } from "./FieldError";
import { HTTPError } from "lambert-server";

export function ValidateName(name: string) {
export function ValidateName(name: string, field: string = "username", maxLength?: number) {
const check_username = name.replace(/\s/g, "");
if (!check_username) {
throw FieldErrors({
username: {
[field]: {
code: "BASE_TYPE_REQUIRED",
message: "common:field.BASE_TYPE_REQUIRED",
},
});
}
const general = Config.get();
const { maxUsername } = general.limits.user;
if (check_username.length > maxUsername || check_username.length < 2) {
const limit = maxLength ?? general.limits.user.maxUsername;
if (check_username.length > limit || check_username.length < 2) {
throw FieldErrors({
username: {
[field]: {
code: "BASE_TYPE_BAD_LENGTH",
message: `Must be between 2 and ${maxUsername} in length.`,
message: `Must be between 2 and ${limit} in length.`,
},
});
}

const { blockedContains, blockedEquals } = general.user;
for (const word of blockedContains) {
if (name.toLowerCase().includes(word)) {
throw new HTTPError(`Username cannot contain "${word}"`, 400);
throw FieldErrors({
[field]: {
code: "NAME_BLOCKED",
message: `Name cannot contain "${word}"`,
},
});
}
}

for (const word of blockedEquals) {
if (name.toLowerCase() === word) {
throw new HTTPError(`Username cannot be "${word}"`, 400);
throw FieldErrors({
[field]: {
code: "NAME_BLOCKED",
message: `Name cannot be "${word}"`,
},
});
}
}
return name;
Expand Down