Added ILLEGAL_CHANNEL_NAMES and NULL_CHANNEL_NAMES guild feature flags

This commit is contained in:
Madeline 2022-01-14 01:20:26 +11:00 committed by Erkin Alp Güney
parent f9ff5b35f3
commit 35c7489f72
3 changed files with 406 additions and 332 deletions

View File

@ -1,332 +1,350 @@
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm"; import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";
import { BaseClass } from "./BaseClass"; import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild"; import { Guild } from "./Guild";
import { PublicUserProjection, User } from "./User"; import { PublicUserProjection, User } from "./User";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial } from "../util"; import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial, InvisibleCharacters } from "../util";
import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces"; import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces";
import { Recipient } from "./Recipient"; import { Recipient } from "./Recipient";
import { Message } from "./Message"; import { Message } from "./Message";
import { ReadState } from "./ReadState"; import { ReadState } from "./ReadState";
import { Invite } from "./Invite"; import { Invite } from "./Invite";
import { VoiceState } from "./VoiceState"; import { VoiceState } from "./VoiceState";
import { Webhook } from "./Webhook"; import { Webhook } from "./Webhook";
import { DmChannelDTO } from "../dtos"; import { DmChannelDTO } from "../dtos";
export enum ChannelType { export enum ChannelType {
GUILD_TEXT = 0, // a text channel within a server GUILD_TEXT = 0, // a text channel within a server
DM = 1, // a direct message between users DM = 1, // a direct message between users
GUILD_VOICE = 2, // a voice channel within a server GUILD_VOICE = 2, // a voice channel within a server
GROUP_DM = 3, // a direct message between multiple users GROUP_DM = 3, // a direct message between multiple users
GUILD_CATEGORY = 4, // an organizational category that contains up to 50 channels GUILD_CATEGORY = 4, // an organizational category that contains up to 50 channels
GUILD_NEWS = 5, // a channel that users can follow and crosspost into their own server GUILD_NEWS = 5, // a channel that users can follow and crosspost into their own server
GUILD_STORE = 6, // a channel in which game developers can sell their game on Discord GUILD_STORE = 6, // a channel in which game developers can sell their game on Discord
// TODO: what are channel types between 7-9? // TODO: what are channel types between 7-9?
GUILD_NEWS_THREAD = 10, // a temporary sub-channel within a GUILD_NEWS channel GUILD_NEWS_THREAD = 10, // a temporary sub-channel within a GUILD_NEWS channel
GUILD_PUBLIC_THREAD = 11, // a temporary sub-channel within a GUILD_TEXT channel GUILD_PUBLIC_THREAD = 11, // a temporary sub-channel within a GUILD_TEXT channel
GUILD_PRIVATE_THREAD = 12, // a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission GUILD_PRIVATE_THREAD = 12, // a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission
GUILD_STAGE_VOICE = 13, // a voice channel for hosting events with an audience GUILD_STAGE_VOICE = 13, // a voice channel for hosting events with an audience
} }
@Entity("channels") @Entity("channels")
export class Channel extends BaseClass { export class Channel extends BaseClass {
@Column() @Column()
created_at: Date; created_at: Date;
@Column({ nullable: true }) @Column({ nullable: true })
name?: string; name?: string;
@Column({ type: "text", nullable: true }) @Column({ type: "text", nullable: true })
icon?: string | null; icon?: string | null;
@Column({ type: "int" }) @Column({ type: "int" })
type: ChannelType; type: ChannelType;
@OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, { @OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, {
cascade: true, cascade: true,
orphanedRowAction: "delete", orphanedRowAction: "delete",
}) })
recipients?: Recipient[]; recipients?: Recipient[];
@Column({ nullable: true }) @Column({ nullable: true })
last_message_id: string; last_message_id: string;
@Column({ nullable: true }) @Column({ nullable: true })
@RelationId((channel: Channel) => channel.guild) @RelationId((channel: Channel) => channel.guild)
guild_id?: string; guild_id?: string;
@JoinColumn({ name: "guild_id" }) @JoinColumn({ name: "guild_id" })
@ManyToOne(() => Guild, { @ManyToOne(() => Guild, {
onDelete: "CASCADE", onDelete: "CASCADE",
}) })
guild: Guild; guild: Guild;
@Column({ nullable: true }) @Column({ nullable: true })
@RelationId((channel: Channel) => channel.parent) @RelationId((channel: Channel) => channel.parent)
parent_id: string; parent_id: string;
@JoinColumn({ name: "parent_id" }) @JoinColumn({ name: "parent_id" })
@ManyToOne(() => Channel) @ManyToOne(() => Channel)
parent?: Channel; parent?: Channel;
// only for group dms // only for group dms
@Column({ nullable: true }) @Column({ nullable: true })
@RelationId((channel: Channel) => channel.owner) @RelationId((channel: Channel) => channel.owner)
owner_id: string; owner_id: string;
@JoinColumn({ name: "owner_id" }) @JoinColumn({ name: "owner_id" })
@ManyToOne(() => User) @ManyToOne(() => User)
owner: User; owner: User;
@Column({ nullable: true }) @Column({ nullable: true })
last_pin_timestamp?: number; last_pin_timestamp?: number;
@Column({ nullable: true }) @Column({ nullable: true })
default_auto_archive_duration?: number; default_auto_archive_duration?: number;
@Column({ nullable: true }) @Column({ nullable: true })
position?: number; position?: number;
@Column({ type: "simple-json", nullable: true }) @Column({ type: "simple-json", nullable: true })
permission_overwrites?: ChannelPermissionOverwrite[]; permission_overwrites?: ChannelPermissionOverwrite[];
@Column({ nullable: true }) @Column({ nullable: true })
video_quality_mode?: number; video_quality_mode?: number;
@Column({ nullable: true }) @Column({ nullable: true })
bitrate?: number; bitrate?: number;
@Column({ nullable: true }) @Column({ nullable: true })
user_limit?: number; user_limit?: number;
@Column({ nullable: true }) @Column({ nullable: true })
nsfw?: boolean; nsfw?: boolean;
@Column({ nullable: true }) @Column({ nullable: true })
rate_limit_per_user?: number; rate_limit_per_user?: number;
@Column({ nullable: true }) @Column({ nullable: true })
topic?: string; topic?: string;
@OneToMany(() => Invite, (invite: Invite) => invite.channel, { @OneToMany(() => Invite, (invite: Invite) => invite.channel, {
cascade: true, cascade: true,
orphanedRowAction: "delete", orphanedRowAction: "delete",
}) })
invites?: Invite[]; invites?: Invite[];
@OneToMany(() => Message, (message: Message) => message.channel, { @OneToMany(() => Message, (message: Message) => message.channel, {
cascade: true, cascade: true,
orphanedRowAction: "delete", orphanedRowAction: "delete",
}) })
messages?: Message[]; messages?: Message[];
@OneToMany(() => VoiceState, (voice_state: VoiceState) => voice_state.channel, { @OneToMany(() => VoiceState, (voice_state: VoiceState) => voice_state.channel, {
cascade: true, cascade: true,
orphanedRowAction: "delete", orphanedRowAction: "delete",
}) })
voice_states?: VoiceState[]; voice_states?: VoiceState[];
@OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, { @OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, {
cascade: true, cascade: true,
orphanedRowAction: "delete", orphanedRowAction: "delete",
}) })
read_states?: ReadState[]; read_states?: ReadState[];
@OneToMany(() => Webhook, (webhook: Webhook) => webhook.channel, { @OneToMany(() => Webhook, (webhook: Webhook) => webhook.channel, {
cascade: true, cascade: true,
orphanedRowAction: "delete", orphanedRowAction: "delete",
}) })
webhooks?: Webhook[]; webhooks?: Webhook[];
// TODO: DM channel // TODO: DM channel
static async createChannel( static async createChannel(
channel: Partial<Channel>, channel: Partial<Channel>,
user_id: string = "0", user_id: string = "0",
opts?: { opts?: {
keepId?: boolean; keepId?: boolean;
skipExistsCheck?: boolean; skipExistsCheck?: boolean;
skipPermissionCheck?: boolean; skipPermissionCheck?: boolean;
skipEventEmit?: boolean; skipEventEmit?: boolean;
} skipNameChecks?: boolean;
) { }
if (!opts?.skipPermissionCheck) { ) {
// Always check if user has permission first if (!opts?.skipPermissionCheck) {
const permissions = await getPermission(user_id, channel.guild_id); // Always check if user has permission first
permissions.hasThrow("MANAGE_CHANNELS"); const permissions = await getPermission(user_id, channel.guild_id);
} permissions.hasThrow("MANAGE_CHANNELS");
}
switch (channel.type) {
case ChannelType.GUILD_TEXT: if (!opts?.skipNameChecks) {
case ChannelType.GUILD_VOICE: const guild = await Guild.findOneOrFail({ id: channel.guild_id });
if (channel.parent_id && !opts?.skipExistsCheck) { if (!guild.features.includes("ILLEGAL_CHANNEL_NAMES") && channel.name) {
const exists = await Channel.findOneOrFail({ id: channel.parent_id }); for (var character of InvisibleCharacters)
if (!exists) throw new HTTPError("Parent id channel doesn't exist", 400); channel.name = channel.name.split(character).join("-");
if (exists.guild_id !== channel.guild_id)
throw new HTTPError("The category channel needs to be in the guild"); channel.name = channel.name.split(/\-+/g).join("-"); //replace multiple occurances with just one
} channel.name = channel.name.split("-").filter(Boolean).join("-"); //trim '-' character
break; }
case ChannelType.GUILD_CATEGORY:
break; if (!guild.features.includes("NULL_CHANNEL_NAMES")) {
case ChannelType.DM: if (channel.name) channel.name = channel.name.trim();
case ChannelType.GROUP_DM:
throw new HTTPError("You can't create a dm channel in a guild"); if (!channel.name) throw new HTTPError("Channel name cannot be empty.");
// TODO: check if guild is community server }
case ChannelType.GUILD_STORE: }
case ChannelType.GUILD_NEWS:
default: switch (channel.type) {
throw new HTTPError("Not yet supported"); case ChannelType.GUILD_TEXT:
} case ChannelType.GUILD_VOICE:
if (channel.parent_id && !opts?.skipExistsCheck) {
if (!channel.permission_overwrites) channel.permission_overwrites = []; const exists = await Channel.findOneOrFail({ id: channel.parent_id });
// TODO: auto generate position if (!exists) throw new HTTPError("Parent id channel doesn't exist", 400);
if (exists.guild_id !== channel.guild_id)
channel = { throw new HTTPError("The category channel needs to be in the guild");
...channel, }
...(!opts?.keepId && { id: Snowflake.generate() }), break;
created_at: new Date(), case ChannelType.GUILD_CATEGORY:
position: channel.position || 0, break;
}; case ChannelType.DM:
case ChannelType.GROUP_DM:
await Promise.all([ throw new HTTPError("You can't create a dm channel in a guild");
new Channel(channel).save(), // TODO: check if guild is community server
!opts?.skipEventEmit case ChannelType.GUILD_STORE:
? emitEvent({ case ChannelType.GUILD_NEWS:
event: "CHANNEL_CREATE", default:
data: channel, throw new HTTPError("Not yet supported");
guild_id: channel.guild_id, }
} as ChannelCreateEvent)
: Promise.resolve(), if (!channel.permission_overwrites) channel.permission_overwrites = [];
]); // TODO: auto generate position
return channel; channel = {
} ...channel,
...(!opts?.keepId && { id: Snowflake.generate() }),
static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) { created_at: new Date(),
recipients = recipients.unique().filter((x) => x !== creator_user_id); position: channel.position || 0,
const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) }); };
// TODO: check config for max number of recipients await Promise.all([
if (otherRecipientsUsers.length !== recipients.length) { new Channel(channel).save(),
throw new HTTPError("Recipient/s not found"); !opts?.skipEventEmit
} ? emitEvent({
event: "CHANNEL_CREATE",
const type = recipients.length === 1 ? ChannelType.DM : ChannelType.GROUP_DM; data: channel,
guild_id: channel.guild_id,
let channel = null; } as ChannelCreateEvent)
: Promise.resolve(),
const channelRecipients = [...recipients, creator_user_id]; ]);
const userRecipients = await Recipient.find({ return channel;
where: { user_id: creator_user_id }, }
relations: ["channel", "channel.recipients"],
}); static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) {
recipients = recipients.unique().filter((x) => x !== creator_user_id);
for (let ur of userRecipients) { const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) });
let re = ur.channel.recipients!.map((r) => r.user_id);
if (re.length === channelRecipients.length) { // TODO: check config for max number of recipients
if (containsAll(re, channelRecipients)) { if (otherRecipientsUsers.length !== recipients.length) {
if (channel == null) { throw new HTTPError("Recipient/s not found");
channel = ur.channel; }
await ur.assign({ closed: false }).save();
} const type = recipients.length === 1 ? ChannelType.DM : ChannelType.GROUP_DM;
}
} let channel = null;
}
const channelRecipients = [...recipients, creator_user_id];
if (channel == null) {
name = trimSpecial(name); const userRecipients = await Recipient.find({
where: { user_id: creator_user_id },
channel = await new Channel({ relations: ["channel", "channel.recipients"],
name, });
type,
owner_id: type === ChannelType.DM ? undefined : creator_user_id, for (let ur of userRecipients) {
created_at: new Date(), let re = ur.channel.recipients!.map((r) => r.user_id);
last_message_id: null, if (re.length === channelRecipients.length) {
recipients: channelRecipients.map( if (containsAll(re, channelRecipients)) {
(x) => if (channel == null) {
new Recipient({ user_id: x, closed: !(type === ChannelType.GROUP_DM || x === creator_user_id) }) channel = ur.channel;
), await ur.assign({ closed: false }).save();
}).save(); }
} }
}
const channel_dto = await DmChannelDTO.from(channel); }
if (type === ChannelType.GROUP_DM) { if (channel == null) {
for (let recipient of channel.recipients!) { name = trimSpecial(name);
await emitEvent({
event: "CHANNEL_CREATE", channel = await new Channel({
data: channel_dto.excludedRecipients([recipient.user_id]), name,
user_id: recipient.user_id, type,
}); owner_id: type === ChannelType.DM ? undefined : creator_user_id,
} created_at: new Date(),
} else { last_message_id: null,
await emitEvent({ event: "CHANNEL_CREATE", data: channel_dto, user_id: creator_user_id }); recipients: channelRecipients.map(
} (x) =>
new Recipient({ user_id: x, closed: !(type === ChannelType.GROUP_DM || x === creator_user_id) })
return channel_dto.excludedRecipients([creator_user_id]); ),
} }).save();
}
static async removeRecipientFromChannel(channel: Channel, user_id: string) {
await Recipient.delete({ channel_id: channel.id, user_id: user_id }); const channel_dto = await DmChannelDTO.from(channel);
channel.recipients = channel.recipients?.filter((r) => r.user_id !== user_id);
if (type === ChannelType.GROUP_DM) {
if (channel.recipients?.length === 0) { for (let recipient of channel.recipients!) {
await Channel.deleteChannel(channel); await emitEvent({
await emitEvent({ event: "CHANNEL_CREATE",
event: "CHANNEL_DELETE", data: channel_dto.excludedRecipients([recipient.user_id]),
data: await DmChannelDTO.from(channel, [user_id]), user_id: recipient.user_id,
user_id: user_id, });
}); }
return; } else {
} await emitEvent({ event: "CHANNEL_CREATE", data: channel_dto, user_id: creator_user_id });
}
await emitEvent({
event: "CHANNEL_DELETE", return channel_dto.excludedRecipients([creator_user_id]);
data: await DmChannelDTO.from(channel, [user_id]), }
user_id: user_id,
}); static async removeRecipientFromChannel(channel: Channel, user_id: string) {
await Recipient.delete({ channel_id: channel.id, user_id: user_id });
//If the owner leave we make the first recipient in the list the new owner channel.recipients = channel.recipients?.filter((r) => r.user_id !== user_id);
if (channel.owner_id === user_id) {
channel.owner_id = channel.recipients!.find((r) => r.user_id !== user_id)!.user_id; //Is there a criteria to choose the new owner? if (channel.recipients?.length === 0) {
await emitEvent({ await Channel.deleteChannel(channel);
event: "CHANNEL_UPDATE", await emitEvent({
data: await DmChannelDTO.from(channel, [user_id]), event: "CHANNEL_DELETE",
channel_id: channel.id, data: await DmChannelDTO.from(channel, [user_id]),
}); user_id: user_id,
} });
return;
await channel.save(); }
await emitEvent({ await emitEvent({
event: "CHANNEL_RECIPIENT_REMOVE", event: "CHANNEL_DELETE",
data: { data: await DmChannelDTO.from(channel, [user_id]),
channel_id: channel.id, user_id: user_id,
user: await User.findOneOrFail({ where: { id: user_id }, select: PublicUserProjection }), });
},
channel_id: channel.id, //If the owner leave we make the first recipient in the list the new owner
} as ChannelRecipientRemoveEvent); if (channel.owner_id === user_id) {
} channel.owner_id = channel.recipients!.find((r) => r.user_id !== user_id)!.user_id; //Is there a criteria to choose the new owner?
await emitEvent({
static async deleteChannel(channel: Channel) { event: "CHANNEL_UPDATE",
await Message.delete({ channel_id: channel.id }); //TODO we should also delete the attachments from the cdn but to do that we need to move cdn.ts in util data: await DmChannelDTO.from(channel, [user_id]),
//TODO before deleting the channel we should check and delete other relations channel_id: channel.id,
await Channel.delete({ id: channel.id }); });
} }
isDm() { await channel.save();
return this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM;
} await emitEvent({
} event: "CHANNEL_RECIPIENT_REMOVE",
data: {
export interface ChannelPermissionOverwrite { channel_id: channel.id,
allow: string; user: await User.findOneOrFail({ where: { id: user_id }, select: PublicUserProjection }),
deny: string; },
id: string; channel_id: channel.id,
type: ChannelPermissionOverwriteType; } as ChannelRecipientRemoveEvent);
} }
export enum ChannelPermissionOverwriteType { static async deleteChannel(channel: Channel) {
role = 0, await Message.delete({ channel_id: channel.id }); //TODO we should also delete the attachments from the cdn but to do that we need to move cdn.ts in util
member = 1, //TODO before deleting the channel we should check and delete other relations
} await Channel.delete({ id: channel.id });
}
isDm() {
return this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM;
}
}
export interface ChannelPermissionOverwrite {
allow: string;
deny: string;
id: string;
type: ChannelPermissionOverwriteType;
}
export enum ChannelPermissionOverwriteType {
role = 0,
member = 1,
}

View File

@ -0,0 +1,55 @@
export const InvisibleCharacters = [
"\t",
" ",
"­",
"͏",
"؜",
"",
"",
"",
"",
"",
" ",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
" ",
"",
"",
"",
"",
"𝅙",
"𝅳",
"𝅴",
"𝅵",
"𝅶",
"𝅷",
"𝅸",
"𝅹",
"𝅺"
]

View File

@ -18,3 +18,4 @@ export * from "./Snowflake";
export * from "./String"; export * from "./String";
export * from "./Array"; export * from "./Array";
export * from "./TraverseDirectory"; export * from "./TraverseDirectory";
export * from "./InvisibleCharacters";