summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--api/src/routes/channels/#channel_id/messages/index.ts8
-rw-r--r--util/src/entities/Channel.ts689
-rw-r--r--util/src/util/InvisibleCharacters.ts56
-rw-r--r--util/src/util/index.ts1
4 files changed, 420 insertions, 334 deletions
diff --git a/api/src/routes/channels/#channel_id/messages/index.ts b/api/src/routes/channels/#channel_id/messages/index.ts
index c3d3d408..1ae9d676 100644
--- a/api/src/routes/channels/#channel_id/messages/index.ts
+++ b/api/src/routes/channels/#channel_id/messages/index.ts
@@ -37,7 +37,11 @@ export function isTextChannel(type: ChannelType): boolean {
 		case ChannelType.GUILD_PUBLIC_THREAD:
 		case ChannelType.GUILD_PRIVATE_THREAD:
 		case ChannelType.GUILD_TEXT:
+		case ChannelType.ENCRYPTED:
+		case ChannelType.ENCRYPTED_THREAD:
 			return true;
+		default:
+			throw new HTTPError("unimplemented", 400);
 	}
 }
 
@@ -87,7 +91,7 @@ router.get("/", async (req: Request, res: Response) => {
 	permissions.hasThrow("VIEW_CHANNEL");
 	if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]);
 
-	var query: FindManyOptions<Message> & { where: { id?: any } } = {
+	var query: FindManyOptions<Message> & { where: { id?: any; }; } = {
 		order: { id: "DESC" },
 		take: limit,
 		where: { channel_id },
@@ -216,7 +220,7 @@ router.post(
 			channel.save()
 		]);
 
-		postHandleMessage(message).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error
+		postHandleMessage(message).catch((e) => { }); // no await as it shouldnt block the message send function and silently catch error
 
 		return res.json(message);
 	}
diff --git a/util/src/entities/Channel.ts b/util/src/entities/Channel.ts
index 4036b5d6..1cc4a538 100644
--- a/util/src/entities/Channel.ts
+++ b/util/src/entities/Channel.ts
@@ -1,332 +1,357 @@
-import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";
-import { BaseClass } from "./BaseClass";
-import { Guild } from "./Guild";
-import { PublicUserProjection, User } from "./User";
-import { HTTPError } from "lambert-server";
-import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial } from "../util";
-import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces";
-import { Recipient } from "./Recipient";
-import { Message } from "./Message";
-import { ReadState } from "./ReadState";
-import { Invite } from "./Invite";
-import { VoiceState } from "./VoiceState";
-import { Webhook } from "./Webhook";
-import { DmChannelDTO } from "../dtos";
-
-export enum ChannelType {
-	GUILD_TEXT = 0, // a text channel within a server
-	DM = 1, // a direct message between users
-	GUILD_VOICE = 2, // a voice channel within a server
-	GROUP_DM = 3, // a direct message between multiple users
-	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_STORE = 6, // a channel in which game developers can sell their game on Discord
-	// TODO: what are channel types between 7-9?
-	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_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
-}
-
-@Entity("channels")
-export class Channel extends BaseClass {
-	@Column()
-	created_at: Date;
-
-	@Column({ nullable: true })
-	name?: string;
-
-	@Column({ type: "text", nullable: true })
-	icon?: string | null;
-
-	@Column({ type: "int" })
-	type: ChannelType;
-
-	@OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, {
-		cascade: true,
-		orphanedRowAction: "delete",
-	})
-	recipients?: Recipient[];
-
-	@Column({ nullable: true })
-	last_message_id: string;
-
-	@Column({ nullable: true })
-	@RelationId((channel: Channel) => channel.guild)
-	guild_id?: string;
-
-	@JoinColumn({ name: "guild_id" })
-	@ManyToOne(() => Guild, {
-		onDelete: "CASCADE",
-	})
-	guild: Guild;
-
-	@Column({ nullable: true })
-	@RelationId((channel: Channel) => channel.parent)
-	parent_id: string;
-
-	@JoinColumn({ name: "parent_id" })
-	@ManyToOne(() => Channel)
-	parent?: Channel;
-
-	// only for group dms
-	@Column({ nullable: true })
-	@RelationId((channel: Channel) => channel.owner)
-	owner_id: string;
-
-	@JoinColumn({ name: "owner_id" })
-	@ManyToOne(() => User)
-	owner: User;
-
-	@Column({ nullable: true })
-	last_pin_timestamp?: number;
-
-	@Column({ nullable: true })
-	default_auto_archive_duration?: number;
-
-	@Column({ nullable: true })
-	position?: number;
-
-	@Column({ type: "simple-json", nullable: true })
-	permission_overwrites?: ChannelPermissionOverwrite[];
-
-	@Column({ nullable: true })
-	video_quality_mode?: number;
-
-	@Column({ nullable: true })
-	bitrate?: number;
-
-	@Column({ nullable: true })
-	user_limit?: number;
-
-	@Column({ nullable: true })
-	nsfw?: boolean;
-
-	@Column({ nullable: true })
-	rate_limit_per_user?: number;
-
-	@Column({ nullable: true })
-	topic?: string;
-
-	@OneToMany(() => Invite, (invite: Invite) => invite.channel, {
-		cascade: true,
-		orphanedRowAction: "delete",
-	})
-	invites?: Invite[];
-
-	@OneToMany(() => Message, (message: Message) => message.channel, {
-		cascade: true,
-		orphanedRowAction: "delete",
-	})
-	messages?: Message[];
-
-	@OneToMany(() => VoiceState, (voice_state: VoiceState) => voice_state.channel, {
-		cascade: true,
-		orphanedRowAction: "delete",
-	})
-	voice_states?: VoiceState[];
-
-	@OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, {
-		cascade: true,
-		orphanedRowAction: "delete",
-	})
-	read_states?: ReadState[];
-
-	@OneToMany(() => Webhook, (webhook: Webhook) => webhook.channel, {
-		cascade: true,
-		orphanedRowAction: "delete",
-	})
-	webhooks?: Webhook[];
-
-	// TODO: DM channel
-	static async createChannel(
-		channel: Partial<Channel>,
-		user_id: string = "0",
-		opts?: {
-			keepId?: boolean;
-			skipExistsCheck?: boolean;
-			skipPermissionCheck?: boolean;
-			skipEventEmit?: boolean;
-		}
-	) {
-		if (!opts?.skipPermissionCheck) {
-			// Always check if user has permission first
-			const permissions = await getPermission(user_id, channel.guild_id);
-			permissions.hasThrow("MANAGE_CHANNELS");
-		}
-
-		switch (channel.type) {
-			case ChannelType.GUILD_TEXT:
-			case ChannelType.GUILD_VOICE:
-				if (channel.parent_id && !opts?.skipExistsCheck) {
-					const exists = await Channel.findOneOrFail({ id: channel.parent_id });
-					if (!exists) throw new HTTPError("Parent id channel doesn't exist", 400);
-					if (exists.guild_id !== channel.guild_id)
-						throw new HTTPError("The category channel needs to be in the guild");
-				}
-				break;
-			case ChannelType.GUILD_CATEGORY:
-				break;
-			case ChannelType.DM:
-			case ChannelType.GROUP_DM:
-				throw new HTTPError("You can't create a dm channel in a guild");
-			// TODO: check if guild is community server
-			case ChannelType.GUILD_STORE:
-			case ChannelType.GUILD_NEWS:
-			default:
-				throw new HTTPError("Not yet supported");
-		}
-
-		if (!channel.permission_overwrites) channel.permission_overwrites = [];
-		// TODO: auto generate position
-
-		channel = {
-			...channel,
-			...(!opts?.keepId && { id: Snowflake.generate() }),
-			created_at: new Date(),
-			position: channel.position || 0,
-		};
-
-		await Promise.all([
-			new Channel(channel).save(),
-			!opts?.skipEventEmit
-				? emitEvent({
-						event: "CHANNEL_CREATE",
-						data: channel,
-						guild_id: channel.guild_id,
-				  } as ChannelCreateEvent)
-				: Promise.resolve(),
-		]);
-
-		return channel;
-	}
-
-	static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) {
-		recipients = recipients.unique().filter((x) => x !== creator_user_id);
-		const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) });
-
-		// TODO: check config for max number of recipients
-		if (otherRecipientsUsers.length !== recipients.length) {
-			throw new HTTPError("Recipient/s not found");
-		}
-
-		const type = recipients.length === 1 ? ChannelType.DM : ChannelType.GROUP_DM;
-
-		let channel = null;
-
-		const channelRecipients = [...recipients, creator_user_id];
-
-		const userRecipients = await Recipient.find({
-			where: { user_id: creator_user_id },
-			relations: ["channel", "channel.recipients"],
-		});
-
-		for (let ur of userRecipients) {
-			let re = ur.channel.recipients!.map((r) => r.user_id);
-			if (re.length === channelRecipients.length) {
-				if (containsAll(re, channelRecipients)) {
-					if (channel == null) {
-						channel = ur.channel;
-						await ur.assign({ closed: false }).save();
-					}
-				}
-			}
-		}
-
-		if (channel == null) {
-			name = trimSpecial(name);
-
-			channel = await new Channel({
-				name,
-				type,
-				owner_id: type === ChannelType.DM ? undefined : creator_user_id,
-				created_at: new Date(),
-				last_message_id: null,
-				recipients: channelRecipients.map(
-					(x) =>
-						new Recipient({ user_id: x, closed: !(type === ChannelType.GROUP_DM || x === creator_user_id) })
-				),
-			}).save();
-		}
-
-		const channel_dto = await DmChannelDTO.from(channel);
-
-		if (type === ChannelType.GROUP_DM) {
-			for (let recipient of channel.recipients!) {
-				await emitEvent({
-					event: "CHANNEL_CREATE",
-					data: channel_dto.excludedRecipients([recipient.user_id]),
-					user_id: recipient.user_id,
-				});
-			}
-		} else {
-			await emitEvent({ event: "CHANNEL_CREATE", data: channel_dto, user_id: creator_user_id });
-		}
-
-		return channel_dto.excludedRecipients([creator_user_id]);
-	}
-
-	static async removeRecipientFromChannel(channel: Channel, user_id: string) {
-		await Recipient.delete({ channel_id: channel.id, user_id: user_id });
-		channel.recipients = channel.recipients?.filter((r) => r.user_id !== user_id);
-
-		if (channel.recipients?.length === 0) {
-			await Channel.deleteChannel(channel);
-			await emitEvent({
-				event: "CHANNEL_DELETE",
-				data: await DmChannelDTO.from(channel, [user_id]),
-				user_id: user_id,
-			});
-			return;
-		}
-
-		await emitEvent({
-			event: "CHANNEL_DELETE",
-			data: await DmChannelDTO.from(channel, [user_id]),
-			user_id: user_id,
-		});
-
-		//If the owner leave we make the first recipient in the list the new owner
-		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({
-				event: "CHANNEL_UPDATE",
-				data: await DmChannelDTO.from(channel, [user_id]),
-				channel_id: channel.id,
-			});
-		}
-
-		await channel.save();
-
-		await emitEvent({
-			event: "CHANNEL_RECIPIENT_REMOVE",
-			data: {
-				channel_id: channel.id,
-				user: await User.findOneOrFail({ where: { id: user_id }, select: PublicUserProjection }),
-			},
-			channel_id: channel.id,
-		} as ChannelRecipientRemoveEvent);
-	}
-
-	static async deleteChannel(channel: Channel) {
-		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
-		//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,
-}
+import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";

+import { BaseClass } from "./BaseClass";

+import { Guild } from "./Guild";

+import { PublicUserProjection, User } from "./User";

+import { HTTPError } from "lambert-server";

+import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial, InvisibleCharacters } from "../util";

+import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces";

+import { Recipient } from "./Recipient";

+import { Message } from "./Message";

+import { ReadState } from "./ReadState";

+import { Invite } from "./Invite";

+import { VoiceState } from "./VoiceState";

+import { Webhook } from "./Webhook";

+import { DmChannelDTO } from "../dtos";

+

+export enum ChannelType {

+	GUILD_TEXT = 0, // a text channel within a server

+	DM = 1, // a direct message between users

+	GUILD_VOICE = 2, // a voice channel within a server

+	GROUP_DM = 3, // a direct message between multiple users

+	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_STORE = 6, // a channel in which game developers can sell their game on Discord

+	ENCRYPTED = 7, // end-to-end encrypted channel

+	ENCRYPTED_THREAD = 8, // end-to-end encrypted thread 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_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

+	CUSTOM_START = 64, // start custom channel types from here

+	UNHANDLED = 255 // unhandled unowned pass-through channel type

+}

+

+@Entity("channels")

+export class Channel extends BaseClass {

+	@Column()

+	created_at: Date;

+

+	@Column({ nullable: true })

+	name?: string;

+

+	@Column({ type: "text", nullable: true })

+	icon?: string | null;

+

+	@Column({ type: "int" })

+	type: ChannelType;

+

+	@OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, {

+		cascade: true,

+		orphanedRowAction: "delete",

+	})

+	recipients?: Recipient[];

+

+	@Column({ nullable: true })

+	last_message_id: string;

+

+	@Column({ nullable: true })

+	@RelationId((channel: Channel) => channel.guild)

+	guild_id?: string;

+

+	@JoinColumn({ name: "guild_id" })

+	@ManyToOne(() => Guild, {

+		onDelete: "CASCADE",

+	})

+	guild: Guild;

+

+	@Column({ nullable: true })

+	@RelationId((channel: Channel) => channel.parent)

+	parent_id: string;

+

+	@JoinColumn({ name: "parent_id" })

+	@ManyToOne(() => Channel)

+	parent?: Channel;

+

+	// only for group dms

+	@Column({ nullable: true })

+	@RelationId((channel: Channel) => channel.owner)

+	owner_id: string;

+

+	@JoinColumn({ name: "owner_id" })

+	@ManyToOne(() => User)

+	owner: User;

+

+	@Column({ nullable: true })

+	last_pin_timestamp?: number;

+

+	@Column({ nullable: true })

+	default_auto_archive_duration?: number;

+

+	@Column({ nullable: true })

+	position?: number;

+

+	@Column({ type: "simple-json", nullable: true })

+	permission_overwrites?: ChannelPermissionOverwrite[];

+

+	@Column({ nullable: true })

+	video_quality_mode?: number;

+

+	@Column({ nullable: true })

+	bitrate?: number;

+

+	@Column({ nullable: true })

+	user_limit?: number;

+

+	@Column({ nullable: true })

+	nsfw?: boolean;

+

+	@Column({ nullable: true })

+	rate_limit_per_user?: number;

+

+	@Column({ nullable: true })

+	topic?: string;

+

+	@OneToMany(() => Invite, (invite: Invite) => invite.channel, {

+		cascade: true,

+		orphanedRowAction: "delete",

+	})

+	invites?: Invite[];

+

+	@OneToMany(() => Message, (message: Message) => message.channel, {

+		cascade: true,

+		orphanedRowAction: "delete",

+	})

+	messages?: Message[];

+

+	@OneToMany(() => VoiceState, (voice_state: VoiceState) => voice_state.channel, {

+		cascade: true,

+		orphanedRowAction: "delete",

+	})

+	voice_states?: VoiceState[];

+

+	@OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, {

+		cascade: true,

+		orphanedRowAction: "delete",

+	})

+	read_states?: ReadState[];

+

+	@OneToMany(() => Webhook, (webhook: Webhook) => webhook.channel, {

+		cascade: true,

+		orphanedRowAction: "delete",

+	})

+	webhooks?: Webhook[];

+

+	// TODO: DM channel

+	static async createChannel(

+		channel: Partial<Channel>,

+		user_id: string = "0",

+		opts?: {

+			keepId?: boolean;

+			skipExistsCheck?: boolean;

+			skipPermissionCheck?: boolean;

+			skipEventEmit?: boolean;

+			skipNameChecks?: boolean;

+		}

+	) {

+		if (!opts?.skipPermissionCheck) {

+			// Always check if user has permission first

+			const permissions = await getPermission(user_id, channel.guild_id);

+			permissions.hasThrow("MANAGE_CHANNELS");

+		}

+

+		if (!opts?.skipNameChecks) {

+			const guild = await Guild.findOneOrFail({ id: channel.guild_id });

+			if (!guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") && channel.name) {

+				for (var character of InvisibleCharacters)

+					if (channel.name.includes(character))

+						throw new HTTPError("Channel name cannot include invalid characters", 403);

+

+				if (channel.name.match(/\-\-+/g))

+					throw new HTTPError("Channel name cannot include multiple adjacent dashes.", 403)

+

+				if (channel.name.charAt(0) === "-" ||

+					channel.name.charAt(channel.name.length - 1) === "-")

+					throw new HTTPError("Channel name cannot start/end with dash.", 403)

+			}

+

+			if (!guild.features.includes("ALLOW_UNNAMED_CHANNELS")) {

+				if (!channel.name)

+					throw new HTTPError("Channel name cannot be empty.", 403);

+			}

+		}

+

+		switch (channel.type) {

+			case ChannelType.GUILD_TEXT:

+			case ChannelType.GUILD_VOICE:

+				if (channel.parent_id && !opts?.skipExistsCheck) {

+					const exists = await Channel.findOneOrFail({ id: channel.parent_id });

+					if (!exists) throw new HTTPError("Parent id channel doesn't exist", 400);

+					if (exists.guild_id !== channel.guild_id)

+						throw new HTTPError("The category channel needs to be in the guild");

+				}

+				break;

+			case ChannelType.GUILD_CATEGORY:

+				break;

+			case ChannelType.DM:

+			case ChannelType.GROUP_DM:

+				throw new HTTPError("You can't create a dm channel in a guild");

+			// TODO: check if guild is community server

+			case ChannelType.GUILD_STORE:

+			case ChannelType.GUILD_NEWS:

+			default:

+				throw new HTTPError("Not yet supported");

+		}

+

+		if (!channel.permission_overwrites) channel.permission_overwrites = [];

+		// TODO: auto generate position

+

+		channel = {

+			...channel,

+			...(!opts?.keepId && { id: Snowflake.generate() }),

+			created_at: new Date(),

+			position: channel.position || 0,

+		};

+

+		await Promise.all([

+			new Channel(channel).save(),

+			!opts?.skipEventEmit

+				? emitEvent({

+					event: "CHANNEL_CREATE",

+					data: channel,

+					guild_id: channel.guild_id,

+				} as ChannelCreateEvent)

+				: Promise.resolve(),

+		]);

+

+		return channel;

+	}

+

+	static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) {

+		recipients = recipients.unique().filter((x) => x !== creator_user_id);

+		const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) });

+

+		// TODO: check config for max number of recipients

+		if (otherRecipientsUsers.length !== recipients.length) {

+			throw new HTTPError("Recipient/s not found");

+		}

+

+		const type = recipients.length === 1 ? ChannelType.DM : ChannelType.GROUP_DM;

+

+		let channel = null;

+

+		const channelRecipients = [...recipients, creator_user_id];

+

+		const userRecipients = await Recipient.find({

+			where: { user_id: creator_user_id },

+			relations: ["channel", "channel.recipients"],

+		});

+

+		for (let ur of userRecipients) {

+			let re = ur.channel.recipients!.map((r) => r.user_id);

+			if (re.length === channelRecipients.length) {

+				if (containsAll(re, channelRecipients)) {

+					if (channel == null) {

+						channel = ur.channel;

+						await ur.assign({ closed: false }).save();

+					}

+				}

+			}

+		}

+

+		if (channel == null) {

+			name = trimSpecial(name);

+

+			channel = await new Channel({

+				name,

+				type,

+				owner_id: type === ChannelType.DM ? undefined : null, // 1:1 DMs are ownerless in fosscord-server

+				created_at: new Date(),

+				last_message_id: null,

+				recipients: channelRecipients.map(

+					(x) =>

+						new Recipient({ user_id: x, closed: !(type === ChannelType.GROUP_DM || x === creator_user_id) })

+				),

+			}).save();

+		}

+

+		const channel_dto = await DmChannelDTO.from(channel);

+

+		if (type === ChannelType.GROUP_DM) {

+			for (let recipient of channel.recipients!) {

+				await emitEvent({

+					event: "CHANNEL_CREATE",

+					data: channel_dto.excludedRecipients([recipient.user_id]),

+					user_id: recipient.user_id,

+				});

+			}

+		} else {

+			await emitEvent({ event: "CHANNEL_CREATE", data: channel_dto, user_id: creator_user_id });

+		}

+

+		return channel_dto.excludedRecipients([creator_user_id]);

+	}

+

+	static async removeRecipientFromChannel(channel: Channel, user_id: string) {

+		await Recipient.delete({ channel_id: channel.id, user_id: user_id });

+		channel.recipients = channel.recipients?.filter((r) => r.user_id !== user_id);

+

+		if (channel.recipients?.length === 0) {

+			await Channel.deleteChannel(channel);

+			await emitEvent({

+				event: "CHANNEL_DELETE",

+				data: await DmChannelDTO.from(channel, [user_id]),

+				user_id: user_id,

+			});

+			return;

+		}

+

+		await emitEvent({

+			event: "CHANNEL_DELETE",

+			data: await DmChannelDTO.from(channel, [user_id]),

+			user_id: user_id,

+		});

+

+		//If the owner leave the server user is the new owner

+		if (channel.owner_id === user_id) {

+			channel.owner_id = "1"; // The channel is now owned by the server user

+			await emitEvent({

+				event: "CHANNEL_UPDATE",

+				data: await DmChannelDTO.from(channel, [user_id]),

+				channel_id: channel.id,

+			});

+		}

+

+		await channel.save();

+

+		await emitEvent({

+			event: "CHANNEL_RECIPIENT_REMOVE",

+			data: {

+				channel_id: channel.id,

+				user: await User.findOneOrFail({ where: { id: user_id }, select: PublicUserProjection }),

+			},

+			channel_id: channel.id,

+		} as ChannelRecipientRemoveEvent);

+	}

+

+	static async deleteChannel(channel: Channel) {

+		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

+		//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,

+}

diff --git a/util/src/util/InvisibleCharacters.ts b/util/src/util/InvisibleCharacters.ts
new file mode 100644
index 00000000..2b014e14
--- /dev/null
+++ b/util/src/util/InvisibleCharacters.ts
@@ -0,0 +1,56 @@
+// List from https://invisible-characters.com/

+export const InvisibleCharacters = [

+	'\u{9}',			//Tab

+	'\u{20}',			//Space

+	'\u{ad}',			//Soft hyphen

+	'\u{34f}',			//Combining grapheme joiner

+	'\u{61c}',			//Arabic letter mark

+	'\u{115f}',			//Hangul choseong filler

+	'\u{1160}',			//Hangul jungseong filler

+	'\u{17b4}',			//Khmer vowel inherent AQ

+	'\u{17b5}',			//Khmer vowel inherent AA

+	'\u{180e}',			//Mongolian vowel separator

+	'\u{2000}',			//En quad

+	'\u{2001}',			//Em quad

+	'\u{2002}',			//En space

+	'\u{2003}',			//Em space

+	'\u{2004}',			//Three-per-em space

+	'\u{2005}',			//Four-per-em space

+	'\u{2006}',			//Six-per-em space

+	'\u{2007}',			//Figure space

+	'\u{2008}',			//Punctuation space

+	'\u{2009}',			//Thin space

+	'\u{200a}',			//Hair space

+	'\u{200b}',			//Zero width space

+	'\u{200c}',			//Zero width non-joiner

+	'\u{200d}',			//Zero width joiner

+	'\u{200e}',			//Left-to-right mark

+	'\u{200f}',			//Right-to-left mark

+	'\u{202f}',			//Narrow no-break space

+	'\u{205f}',			//Medium mathematical space

+	'\u{2060}',			//Word joiner

+	'\u{2061}',			//Function application

+	'\u{2062}',			//Invisible times

+	'\u{2063}',			//Invisible separator

+	'\u{2064}',			//Invisible plus

+	'\u{206a}',			//Inhibit symmetric swapping

+	'\u{206b}',			//Activate symmetric swapping

+	'\u{206c}',			//Inhibit arabic form shaping

+	'\u{206d}',			//Activate arabic form shaping

+	'\u{206e}',			//National digit shapes

+	'\u{206f}',			//Nominal digit shapes

+	'\u{3000}',			//Ideographic space

+	'\u{2800}',			//Braille pattern blank

+	'\u{3164}',			//Hangul filler

+	'\u{feff}',			//Zero width no-break space

+	'\u{ffa0}',			//Haldwidth hangul filler

+	'\u{1d159}',		//Musical symbol null notehead

+	'\u{1d173}',		//Musical symbol begin beam 

+	'\u{1d174}',		//Musical symbol end beam

+	'\u{1d175}',		//Musical symbol begin tie

+	'\u{1d176}',		//Musical symbol end tie

+	'\u{1d177}',		//Musical symbol begin slur

+	'\u{1d178}',		//Musical symbol end slur

+	'\u{1d179}',		//Musical symbol begin phrase

+	'\u{1d17a}'			//Musical symbol end phrase

+]; 
\ No newline at end of file
diff --git a/util/src/util/index.ts b/util/src/util/index.ts
index c5703468..98e1146c 100644
--- a/util/src/util/index.ts
+++ b/util/src/util/index.ts
@@ -18,3 +18,4 @@ export * from "./Snowflake";
 export * from "./String";
 export * from "./Array";
 export * from "./TraverseDirectory";
+export * from "./InvisibleCharacters";
\ No newline at end of file