summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/util/entities/Channel.ts794
-rw-r--r--src/util/util/InvisibleCharacters.ts102
2 files changed, 440 insertions, 456 deletions
diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts
index f97aa3ab..b17fdba0 100644
--- a/src/util/entities/Channel.ts
+++ b/src/util/entities/Channel.ts
@@ -1,405 +1,389 @@
-import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";

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

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

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

-import { HTTPError } from "../util/imports/HTTPError";

-import { OrmUtils } from "../util/imports/OrmUtils";

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

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

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

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

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

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

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

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

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

-

-export enum ChannelType {

-	GUILD_TEXT = 0, // a text channel within a guild

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

-	GUILD_VOICE = 2, // a voice channel within a guild

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

-	GUILD_CATEGORY = 4, // an organizational category that contains zero or more channels

-	GUILD_NEWS = 5, // a channel that users can follow and crosspost into a guild or route

-	GUILD_STORE = 6, // a channel in which game developers can sell their things

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

-	ENCRYPTED_THREAD = 8, // end-to-end encrypted thread channel

-	TRANSACTIONAL = 9, // event chain style transactional 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

-	DIRECTORY = 14, // guild directory listing channel

-	GUILD_FORUM = 15, // forum composed of IM threads

-	TICKET_TRACKER = 33, // ticket tracker, individual ticket items shall have type 12

-	KANBAN = 34, // confluence like kanban board

-	VOICELESS_WHITEBOARD = 35, // whiteboard but without voice (whiteboard + voice is the same as stage)

-	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;

-

-	// for group DMs and owned custom channel types

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

-

-	@Column({ nullable: true })

-	retention_policy_id?: string;

-

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

-

-	@Column({ nullable: true })

-	flags?: number = 0;

-

-	@Column({ nullable: true })

-	default_thread_rate_limit_per_user?: number = 0;

-

-	// 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({ where: { id: channel.guild_id } });

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

-				for (let character of InvisibleCharacters)

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

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

-

-				// Categories and voice skip these checks on discord.com

-				const skipChecksTypes = [

-					ChannelType.GUILD_CATEGORY,

-					ChannelType.GUILD_VOICE,

-				]

-				if (channel.type

-					&& !skipChecksTypes.includes(channel.type)

-					|| guild.features.includes("IRC_LIKE_CHANNEL_NAMES")) {

-					if (channel.name.includes(" "))

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

-				}

-				else

-					channel.name = channel.name.trim();	//category names are trimmed client side on discord.com

-			}

-

-			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_NEWS:

-			case ChannelType.GUILD_VOICE:

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

-					const exists = await Channel.findOneOrFail({ where: { 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:

-			case ChannelType.UNHANDLED:

-				break;

-			case ChannelType.DM:

-			case ChannelType.GROUP_DM:

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

-			case ChannelType.GUILD_STORE:

-			default:

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

-		}

-

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

-		// TODO: eagerly auto generate position of all guild channels

-

-		channel = {

-			...channel,

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

-			created_at: new Date(),

-			position: (channel.type === ChannelType.UNHANDLED ? 0 : channel.position) || 0

-		};

-

-		await Promise.all([

-			OrmUtils.mergeDeep(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 you want to disallow note to self channels, uncomment the conditional below

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

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

-		}

-		**/

-

-		const type = recipients.length > 1 ? ChannelType.GROUP_DM : ChannelType.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;

-						ur = OrmUtils.mergeDeep(ur, { closed: false });

-						await ur.save();

-					}

-				}

-			}

-		}

-

-		if (channel == null) {

-			name = trimSpecial(name);

-

-			channel = await (

-				OrmUtils.mergeDeep(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) =>

-						OrmUtils.mergeDeep(new Recipient(), {

-							user_id: x,

-							closed: !(type === ChannelType.GROUP_DM || x === creator_user_id)

-						})

-					)

-				}) as Channel

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

-		}

-

-		if (recipients.length === 1) return channel_dto;

-		else 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;

-	}

-

-	// Does the channel support sending messages ( eg categories do not )

-	isWritable() {

-		const disallowedChannelTypes = [

-			ChannelType.GUILD_CATEGORY,

-			ChannelType.GUILD_STAGE_VOICE,

-			ChannelType.VOICELESS_WHITEBOARD

-		];

-		return disallowedChannelTypes.indexOf(this.type) == -1;

-	}

-}

-

-export interface ChannelPermissionOverwrite {

-	allow: string;

-	deny: string;

-	id: string;

-	type: ChannelPermissionOverwriteType;

-}

-

-export enum ChannelPermissionOverwriteType {

-	role = 0,

-	member = 1,

-	group = 2

-}
\ No newline at end of file
+import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";
+import { DmChannelDTO } from "../dtos";
+import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces";
+import { containsAll, emitEvent, getPermission, InvisibleCharacters, Snowflake, trimSpecial } from "../util";
+import { HTTPError } from "../util/imports/HTTPError";
+import { OrmUtils } from "../util/imports/OrmUtils";
+import { BaseClass } from "./BaseClass";
+import { Guild } from "./Guild";
+import { Invite } from "./Invite";
+import { Message } from "./Message";
+import { ReadState } from "./ReadState";
+import { Recipient } from "./Recipient";
+import { PublicUserProjection, User } from "./User";
+import { VoiceState } from "./VoiceState";
+import { Webhook } from "./Webhook";
+
+export enum ChannelType {
+	GUILD_TEXT = 0, // a text channel within a guild
+	DM = 1, // a direct message between users
+	GUILD_VOICE = 2, // a voice channel within a guild
+	GROUP_DM = 3, // a direct message between multiple users
+	GUILD_CATEGORY = 4, // an organizational category that contains zero or more channels
+	GUILD_NEWS = 5, // a channel that users can follow and crosspost into a guild or route
+	GUILD_STORE = 6, // a channel in which game developers can sell their things
+	ENCRYPTED = 7, // end-to-end encrypted channel
+	ENCRYPTED_THREAD = 8, // end-to-end encrypted thread channel
+	TRANSACTIONAL = 9, // event chain style transactional 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
+	DIRECTORY = 14, // guild directory listing channel
+	GUILD_FORUM = 15, // forum composed of IM threads
+	TICKET_TRACKER = 33, // ticket tracker, individual ticket items shall have type 12
+	KANBAN = 34, // confluence like kanban board
+	VOICELESS_WHITEBOARD = 35, // whiteboard but without voice (whiteboard + voice is the same as stage)
+	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;
+
+	// for group DMs and owned custom channel types
+	@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[];
+
+	@Column({ nullable: true })
+	retention_policy_id?: string;
+
+	@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[];
+
+	@Column({ nullable: true })
+	flags?: number = 0;
+
+	@Column({ nullable: true })
+	default_thread_rate_limit_per_user?: number = 0;
+
+	// 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({ where: { id: channel.guild_id } });
+			if (!guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") && channel.name) {
+				for (let character of InvisibleCharacters)
+					if (channel.name.includes(character)) throw new HTTPError("Channel name cannot include invalid characters", 403);
+
+				// Categories and voice skip these checks on discord.com
+				const skipChecksTypes = [ChannelType.GUILD_CATEGORY, ChannelType.GUILD_VOICE];
+				if ((channel.type && !skipChecksTypes.includes(channel.type)) || guild.features.includes("IRC_LIKE_CHANNEL_NAMES")) {
+					if (channel.name.includes(" ")) 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);
+				} else channel.name = channel.name.trim(); //category names are trimmed client side on discord.com
+			}
+
+			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_NEWS:
+			case ChannelType.GUILD_VOICE:
+				if (channel.parent_id && !opts?.skipExistsCheck) {
+					const exists = await Channel.findOneOrFail({ where: { 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:
+			case ChannelType.UNHANDLED:
+				break;
+			case ChannelType.DM:
+			case ChannelType.GROUP_DM:
+				throw new HTTPError("You can't create a dm channel in a guild");
+			case ChannelType.GUILD_STORE:
+			default:
+				throw new HTTPError("Not yet supported");
+		}
+
+		if (!channel.permission_overwrites) channel.permission_overwrites = [];
+		// TODO: eagerly auto generate position of all guild channels
+
+		channel = {
+			...channel,
+			...(!opts?.keepId && { id: Snowflake.generate() }),
+			created_at: new Date(),
+			position: (channel.type === ChannelType.UNHANDLED ? 0 : channel.position) || 0
+		};
+
+		await Promise.all([
+			OrmUtils.mergeDeep(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 you want to disallow note to self channels, uncomment the conditional below
+		if (otherRecipientsUsers.length !== recipients.length) {
+			throw new HTTPError("Recipient/s not found");
+		}
+		**/
+
+		const type = recipients.length > 1 ? ChannelType.GROUP_DM : ChannelType.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;
+						ur = OrmUtils.mergeDeep(ur, { closed: false });
+						await ur.save();
+					}
+				}
+			}
+		}
+
+		if (channel == null) {
+			name = trimSpecial(name);
+
+			channel = await (
+				OrmUtils.mergeDeep(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) =>
+						OrmUtils.mergeDeep(new Recipient(), {
+							user_id: x,
+							closed: !(type === ChannelType.GROUP_DM || x === creator_user_id)
+						})
+					)
+				}) as Channel
+			).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 });
+		}
+
+		if (recipients.length === 1) return channel_dto;
+		else 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;
+	}
+
+	// Does the channel support sending messages ( eg categories do not )
+	isWritable() {
+		const disallowedChannelTypes = [ChannelType.GUILD_CATEGORY, ChannelType.GUILD_STAGE_VOICE, ChannelType.VOICELESS_WHITEBOARD];
+		return disallowedChannelTypes.indexOf(this.type) == -1;
+	}
+}
+
+export interface ChannelPermissionOverwrite {
+	allow: string;
+	deny: string;
+	id: string;
+	type: ChannelPermissionOverwriteType;
+}
+
+export enum ChannelPermissionOverwriteType {
+	role = 0,
+	member = 1,
+	group = 2
+}
diff --git a/src/util/util/InvisibleCharacters.ts b/src/util/util/InvisibleCharacters.ts
index 0aab3ec3..9bffec58 100644
--- a/src/util/util/InvisibleCharacters.ts
+++ b/src/util/util/InvisibleCharacters.ts
@@ -1,57 +1,57 @@
 // List from https://invisible-characters.com/
 export const InvisibleCharacters = [
-	'\u{9}',			//Tab
-	'\u{c}',			//Form feed
+	"\u{9}", //Tab
+	"\u{c}", //Form feed
 	//'\u{20}',			//Space	//categories can have spaces in them
-	'\u{ad}',			//Soft hyphen
+	"\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{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{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 -- WJ is required in some languages that don't use spaces to split words
-	'\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
-]; 
+	"\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
+];