summary refs log tree commit diff
path: root/src/util/entities
diff options
context:
space:
mode:
Diffstat (limited to 'src/util/entities')
-rw-r--r--src/util/entities/Attachment.ts19
-rw-r--r--src/util/entities/AuditLog.ts16
-rw-r--r--src/util/entities/BackupCodes.ts4
-rw-r--r--src/util/entities/BaseClass.ts36
-rw-r--r--src/util/entities/Categories.ts24
-rw-r--r--src/util/entities/Channel.ts846
-rw-r--r--src/util/entities/ClientRelease.ts2
-rw-r--r--src/util/entities/Config.ts31
-rw-r--r--src/util/entities/ConnectedAccount.ts3
-rw-r--r--src/util/entities/Emoji.ts2
-rw-r--r--src/util/entities/Encryption.ts20
-rw-r--r--src/util/entities/Guild.ts42
-rw-r--r--src/util/entities/Invite.ts12
-rw-r--r--src/util/entities/Member.ts68
-rw-r--r--src/util/entities/Message.ts27
-rw-r--r--src/util/entities/Migration.ts11
-rw-r--r--src/util/entities/Note.ts2
-rw-r--r--src/util/entities/ReadState.ts13
-rw-r--r--src/util/entities/Relationship.ts9
-rw-r--r--src/util/entities/StickerPack.ts10
-rw-r--r--src/util/entities/Team.ts10
-rw-r--r--src/util/entities/TeamMember.ts10
-rw-r--r--src/util/entities/User.ts122
-rw-r--r--src/util/entities/index.ts2
24 files changed, 819 insertions, 522 deletions
diff --git a/src/util/entities/Attachment.ts b/src/util/entities/Attachment.ts
index 7b4b17eb..055b6f4b 100644
--- a/src/util/entities/Attachment.ts
+++ b/src/util/entities/Attachment.ts
@@ -1,4 +1,11 @@
-import { BeforeRemove, Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import {
+	BeforeRemove,
+	Column,
+	Entity,
+	JoinColumn,
+	ManyToOne,
+	RelationId,
+} from "typeorm";
 import { URL } from "url";
 import { deleteFile } from "../util/cdn";
 import { BaseClass } from "./BaseClass";
@@ -31,9 +38,13 @@ export class Attachment extends BaseClass {
 	message_id: string;
 
 	@JoinColumn({ name: "message_id" })
-	@ManyToOne(() => require("./Message").Message, (message: import("./Message").Message) => message.attachments, {
-		onDelete: "CASCADE",
-	})
+	@ManyToOne(
+		() => require("./Message").Message,
+		(message: import("./Message").Message) => message.attachments,
+		{
+			onDelete: "CASCADE",
+		},
+	)
 	message: import("./Message").Message;
 
 	@BeforeRemove()
diff --git a/src/util/entities/AuditLog.ts b/src/util/entities/AuditLog.ts
index b003e7ba..9cc97742 100644
--- a/src/util/entities/AuditLog.ts
+++ b/src/util/entities/AuditLog.ts
@@ -5,24 +5,24 @@ import { User } from "./User";
 
 export enum AuditLogEvents {
 	// guild level
-	GUILD_UPDATE = 1, 
+	GUILD_UPDATE = 1,
 	GUILD_IMPORT = 2,
 	GUILD_EXPORTED = 3,
 	GUILD_ARCHIVE = 4,
 	GUILD_UNARCHIVE = 5,
 	// join-leave
-	USER_JOIN = 6, 
+	USER_JOIN = 6,
 	USER_LEAVE = 7,
 	// channels
-	CHANNEL_CREATE = 10, 
+	CHANNEL_CREATE = 10,
 	CHANNEL_UPDATE = 11,
 	CHANNEL_DELETE = 12,
 	// permission overrides
-	CHANNEL_OVERWRITE_CREATE = 13, 
+	CHANNEL_OVERWRITE_CREATE = 13,
 	CHANNEL_OVERWRITE_UPDATE = 14,
 	CHANNEL_OVERWRITE_DELETE = 15,
 	// kick and ban
-	MEMBER_KICK = 20, 
+	MEMBER_KICK = 20,
 	MEMBER_PRUNE = 21,
 	MEMBER_BAN_ADD = 22,
 	MEMBER_BAN_REMOVE = 23,
@@ -79,17 +79,17 @@ export enum AuditLogEvents {
 	// application commands
 	APPLICATION_COMMAND_PERMISSION_UPDATE = 121,
 	// automod
-	POLICY_CREATE = 140, 
+	POLICY_CREATE = 140,
 	POLICY_UPDATE = 141,
 	POLICY_DELETE = 142,
-	MESSAGE_BLOCKED_BY_POLICIES = 143,  // in fosscord, blocked messages are stealth-dropped
+	MESSAGE_BLOCKED_BY_POLICIES = 143, // in fosscord, blocked messages are stealth-dropped
 	// instance policies affecting the guild
 	GUILD_AFFECTED_BY_POLICIES = 216,
 	// message moves
 	IN_GUILD_MESSAGE_MOVE = 223,
 	CROSS_GUILD_MESSAGE_MOVE = 224,
 	// message routing
-	ROUTE_CREATE = 225, 
+	ROUTE_CREATE = 225,
 	ROUTE_UPDATE = 226,
 }
 
diff --git a/src/util/entities/BackupCodes.ts b/src/util/entities/BackupCodes.ts
index d532a39a..81cdbb6d 100644
--- a/src/util/entities/BackupCodes.ts
+++ b/src/util/entities/BackupCodes.ts
@@ -24,7 +24,7 @@ export function generateMfaBackupCodes(user_id: string) {
 	for (let i = 0; i < 10; i++) {
 		const code = BackupCode.create({
 			user: { id: user_id },
-			code: crypto.randomBytes(4).toString("hex"),	// 8 characters
+			code: crypto.randomBytes(4).toString("hex"), // 8 characters
 			consumed: false,
 			expired: false,
 		});
@@ -32,4 +32,4 @@ export function generateMfaBackupCodes(user_id: string) {
 	}
 
 	return backup_codes;
-}
\ No newline at end of file
+}
diff --git a/src/util/entities/BaseClass.ts b/src/util/entities/BaseClass.ts
index d5a7c2bf..9942b60e 100644
--- a/src/util/entities/BaseClass.ts
+++ b/src/util/entities/BaseClass.ts
@@ -1,5 +1,12 @@
 import "reflect-metadata";
-import { BaseEntity, BeforeInsert, BeforeUpdate, FindOptionsWhere, ObjectIdColumn, PrimaryColumn } from "typeorm";
+import {
+	BaseEntity,
+	BeforeInsert,
+	BeforeUpdate,
+	FindOptionsWhere,
+	ObjectIdColumn,
+	PrimaryColumn,
+} from "typeorm";
 import { Snowflake } from "../util/Snowflake";
 import "missing-native-js-functions";
 import { getDatabase } from "..";
@@ -22,23 +29,40 @@ export class BaseClassWithoutId extends BaseEntity {
 	toJSON(): any {
 		return Object.fromEntries(
 			this.metadata!.columns // @ts-ignore
-				.map((x) => [x.propertyName, this[x.propertyName]]) // @ts-ignore
-				.concat(this.metadata.relations.map((x) => [x.propertyName, this[x.propertyName]]))
+				.map((x) => [x.propertyName, this[x.propertyName]])
+				.concat(
+					// @ts-ignore
+					this.metadata.relations.map((x) => [
+						x.propertyName,
+						// @ts-ignore
+						this[x.propertyName],
+					]),
+				),
 		);
 	}
 
-	static increment<T extends BaseClass>(conditions: FindOptionsWhere<T>, propertyPath: string, value: number | string) {
+	static increment<T extends BaseClass>(
+		conditions: FindOptionsWhere<T>,
+		propertyPath: string,
+		value: number | string,
+	) {
 		const repository = this.getRepository();
 		return repository.increment(conditions, propertyPath, value);
 	}
 
-	static decrement<T extends BaseClass>(conditions: FindOptionsWhere<T>, propertyPath: string, value: number | string) {
+	static decrement<T extends BaseClass>(
+		conditions: FindOptionsWhere<T>,
+		propertyPath: string,
+		value: number | string,
+	) {
 		const repository = this.getRepository();
 		return repository.decrement(conditions, propertyPath, value);
 	}
 }
 
-export const PrimaryIdColumn = process.env.DATABASE?.startsWith("mongodb") ? ObjectIdColumn : PrimaryColumn;
+export const PrimaryIdColumn = process.env.DATABASE?.startsWith("mongodb")
+	? ObjectIdColumn
+	: PrimaryColumn;
 
 export class BaseClass extends BaseClassWithoutId {
 	@PrimaryIdColumn()
diff --git a/src/util/entities/Categories.ts b/src/util/entities/Categories.ts
index 81fbc303..f12b237d 100644
--- a/src/util/entities/Categories.ts
+++ b/src/util/entities/Categories.ts
@@ -1,4 +1,4 @@
-import { PrimaryColumn, Column, Entity} from "typeorm";
+import { PrimaryColumn, Column, Entity } from "typeorm";
 import { BaseClassWithoutId } from "./BaseClass";
 
 // TODO: categories:
@@ -16,18 +16,18 @@ import { BaseClassWithoutId } from "./BaseClass";
 // Also populate discord default categories
 
 @Entity("categories")
-export class Categories extends BaseClassWithoutId { // Not using snowflake
-    
-    @PrimaryColumn()
-	id: number;
+export class Categories extends BaseClassWithoutId {
+	// Not using snowflake
 
-    @Column({ nullable: true })
-    name: string;
+	@PrimaryColumn()
+	id: number;
 
-    @Column({ type: "simple-json" })
-    localizations: string;
+	@Column({ nullable: true })
+	name: string;
 
-    @Column({ nullable: true })
-    is_primary: boolean;
+	@Column({ type: "simple-json" })
+	localizations: string;
 
-}
\ No newline at end of file
+	@Column({ nullable: true })
+	is_primary: boolean;
+}
diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts
index 2200bfa3..14f36857 100644
--- a/src/util/entities/Channel.ts
+++ b/src/util/entities/Channel.ts
@@ -1,389 +1,457 @@
-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, ChannelTypes } 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 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()

-	nsfw: boolean = false;

-

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

-

-	// 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 (var character of InvisibleCharacters)

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

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

-

-				// Categories skip these checks on discord.com

-				if (channel.type !== ChannelType.GUILD_CATEGORY) {

-					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([

-			Channel.create(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);

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

-		/** if you want to disallow note to self channels, uncomment the conditional below

-

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

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

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

-					}

-				}

-			}

-		}

-

-		if (channel == null) {

-			name = trimSpecial(name);

-

-			channel = await Channel.create({

-				name,

-				type,

-				owner_id: undefined,

-				created_at: new Date(),

-				last_message_id: undefined,

-				recipients: channelRecipients.map(

-					(x) =>

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

-				),

-				nsfw: false,

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

-}

+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,
+	ChannelTypes,
+} 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 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()
+	nsfw: boolean = false;
+
+	@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[];
+
+	// 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 (var character of InvisibleCharacters)
+					if (channel.name.includes(character))
+						throw new HTTPError(
+							"Channel name cannot include invalid characters",
+							403,
+						);
+
+				// Categories skip these checks on discord.com
+				if (channel.type !== ChannelType.GUILD_CATEGORY) {
+					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([
+			Channel.create(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);
+		// TODO: check config for max number of recipients
+		/** if you want to disallow note to self channels, uncomment the conditional below
+
+		const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) });
+		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;
+						await ur.assign({ closed: false }).save();
+					}
+				}
+			}
+		}
+
+		if (channel == null) {
+			name = trimSpecial(name);
+
+			channel = await Channel.create({
+				name,
+				type,
+				owner_id: undefined,
+				created_at: new Date(),
+				last_message_id: undefined,
+				recipients: channelRecipients.map((x) =>
+					Recipient.create({
+						user_id: x,
+						closed: !(
+							type === ChannelType.GROUP_DM ||
+							x === creator_user_id
+						),
+					}),
+				),
+				nsfw: false,
+			}).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/entities/ClientRelease.ts b/src/util/entities/ClientRelease.ts
index c5afd307..2723ab67 100644
--- a/src/util/entities/ClientRelease.ts
+++ b/src/util/entities/ClientRelease.ts
@@ -1,4 +1,4 @@
-import { Column, Entity} from "typeorm";
+import { Column, Entity } from "typeorm";
 import { BaseClass } from "./BaseClass";
 
 @Entity("client_release")
diff --git a/src/util/entities/Config.ts b/src/util/entities/Config.ts
index 9aabc1a8..cd7a6923 100644
--- a/src/util/entities/Config.ts
+++ b/src/util/entities/Config.ts
@@ -191,17 +191,17 @@ export interface ConfigValue {
 		allowTemplateCreation: Boolean;
 		allowDiscordTemplates: Boolean;
 		allowRaws: Boolean;
-	},
+	};
 	client: {
 		useTestClient: Boolean;
 		releases: {
 			useLocalRelease: Boolean; //TODO
 			upstreamVersion: string;
 		};
-	},
+	};
 	metrics: {
 		timeout: number;
-	},
+	};
 	sentry: {
 		enabled: boolean;
 		endpoint: string;
@@ -230,7 +230,8 @@ export const DefaultConfigOptions: ConfigValue = {
 	},
 	general: {
 		instanceName: "Fosscord Instance",
-		instanceDescription: "This is a Fosscord instance made in pre-release days",
+		instanceDescription:
+			"This is a Fosscord instance made in pre-release days",
 		frontPage: null,
 		tosPage: null,
 		correspondenceEmail: "noreply@localhost.local",
@@ -318,8 +319,9 @@ export const DefaultConfigOptions: ConfigValue = {
 			sitekey: null,
 			secret: null,
 		},
-		ipdataApiKey: "eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9",
-		defaultRights: "30644591655936",	// See util/scripts/rights.js
+		ipdataApiKey:
+			"eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9",
+		defaultRights: "30644591655936", // See util/scripts/rights.js
 	},
 	login: {
 		requireCaptcha: false,
@@ -395,22 +397,23 @@ export const DefaultConfigOptions: ConfigValue = {
 		enabled: true,
 		allowTemplateCreation: true,
 		allowDiscordTemplates: true,
-		allowRaws: false
+		allowRaws: false,
 	},
 	client: {
 		useTestClient: true,
 		releases: {
 			useLocalRelease: true,
-			upstreamVersion: "0.0.264"
-		}
+			upstreamVersion: "0.0.264",
+		},
 	},
 	metrics: {
-		timeout: 30000
+		timeout: 30000,
 	},
 	sentry: {
 		enabled: false,
-		endpoint: "https://05e8e3d005f34b7d97e920ae5870a5e5@sentry.thearcanebrony.net/6",
+		endpoint:
+			"https://05e8e3d005f34b7d97e920ae5870a5e5@sentry.thearcanebrony.net/6",
 		traceSampleRate: 1.0,
-		environment: hostname()
-	}
-};
\ No newline at end of file
+		environment: hostname(),
+	},
+};
diff --git a/src/util/entities/ConnectedAccount.ts b/src/util/entities/ConnectedAccount.ts
index 09ae30ab..a893ff34 100644
--- a/src/util/entities/ConnectedAccount.ts
+++ b/src/util/entities/ConnectedAccount.ts
@@ -2,7 +2,8 @@ import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
 import { BaseClass } from "./BaseClass";
 import { User } from "./User";
 
-export interface PublicConnectedAccount extends Pick<ConnectedAccount, "name" | "type" | "verified"> {}
+export interface PublicConnectedAccount
+	extends Pick<ConnectedAccount, "name" | "type" | "verified"> {}
 
 @Entity("connected_accounts")
 export class ConnectedAccount extends BaseClass {
diff --git a/src/util/entities/Emoji.ts b/src/util/entities/Emoji.ts
index a3615b7d..0aa640b5 100644
--- a/src/util/entities/Emoji.ts
+++ b/src/util/entities/Emoji.ts
@@ -40,7 +40,7 @@ export class Emoji extends BaseClass {
 
 	@Column({ type: "simple-array" })
 	roles: string[]; // roles this emoji is whitelisted to (new discord feature?)
-	
+
 	@Column({ type: "simple-array", nullable: true })
 	groups: string[]; // user groups this emoji is whitelisted to (Fosscord extension)
 }
diff --git a/src/util/entities/Encryption.ts b/src/util/entities/Encryption.ts
index b597b90a..4c427b32 100644
--- a/src/util/entities/Encryption.ts
+++ b/src/util/entities/Encryption.ts
@@ -1,9 +1,23 @@
-import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";
+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 {
+	containsAll,
+	emitEvent,
+	getPermission,
+	Snowflake,
+	trimSpecial,
+	InvisibleCharacters,
+} from "../util";
 import { BitField, BitFieldResolvable, BitFlag } from "../util/BitField";
 import { Recipient } from "./Recipient";
 import { Message } from "./Message";
@@ -13,7 +27,6 @@ import { DmChannelDTO } from "../dtos";
 
 @Entity("security_settings")
 export class SecuritySettings extends BaseClass {
-
 	@Column({ nullable: true })
 	guild_id: string;
 
@@ -31,5 +44,4 @@ export class SecuritySettings extends BaseClass {
 
 	@Column({ nullable: true })
 	used_since_message: string;
-
 }
diff --git a/src/util/entities/Guild.ts b/src/util/entities/Guild.ts
index 2ce7c213..8854fec0 100644
--- a/src/util/entities/Guild.ts
+++ b/src/util/entities/Guild.ts
@@ -1,4 +1,13 @@
-import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, OneToMany, OneToOne, RelationId } from "typeorm";
+import {
+	Column,
+	Entity,
+	JoinColumn,
+	ManyToMany,
+	ManyToOne,
+	OneToMany,
+	OneToOne,
+	RelationId,
+} from "typeorm";
 import { Config, handleFile, Snowflake } from "..";
 import { Ban } from "./Ban";
 import { BaseClass } from "./BaseClass";
@@ -86,7 +95,7 @@ export class Guild extends BaseClass {
 	//TODO: https://discord.com/developers/docs/resources/guild#guild-object-guild-features
 
 	@Column({ nullable: true })
-	primary_category_id?: string;	// TODO: this was number?
+	primary_category_id?: string; // TODO: this was number?
 
 	@Column({ nullable: true })
 	icon?: string;
@@ -269,7 +278,7 @@ export class Guild extends BaseClass {
 
 	@Column()
 	nsfw: boolean;
-	
+
 	// TODO: nested guilds
 	@Column({ nullable: true })
 	parent?: string;
@@ -332,10 +341,13 @@ export class Guild extends BaseClass {
 			permissions: String("2251804225"),
 			position: 0,
 			icon: undefined,
-			unicode_emoji: undefined
+			unicode_emoji: undefined,
 		}).save();
 
-		if (!body.channels || !body.channels.length) body.channels = [{ id: "01", type: 0, name: "general", nsfw: false }];
+		if (!body.channels || !body.channels.length)
+			body.channels = [
+				{ id: "01", type: 0, name: "general", nsfw: false },
+			];
 
 		const ids = new Map();
 
@@ -345,17 +357,23 @@ export class Guild extends BaseClass {
 			}
 		});
 
-		for (const channel of body.channels?.sort((a, b) => (a.parent_id ? 1 : -1))) {
+		for (const channel of body.channels?.sort((a, b) =>
+			a.parent_id ? 1 : -1,
+		)) {
 			var id = ids.get(channel.id) || Snowflake.generate();
 
 			var parent_id = ids.get(channel.parent_id);
 
-			await Channel.createChannel({ ...channel, guild_id, id, parent_id }, body.owner_id, {
-				keepId: true,
-				skipExistsCheck: true,
-				skipPermissionCheck: true,
-				skipEventEmit: true,
-			});
+			await Channel.createChannel(
+				{ ...channel, guild_id, id, parent_id },
+				body.owner_id,
+				{
+					keepId: true,
+					skipExistsCheck: true,
+					skipPermissionCheck: true,
+					skipEventEmit: true,
+				},
+			);
 		}
 
 		return guild;
diff --git a/src/util/entities/Invite.ts b/src/util/entities/Invite.ts
index 4f36f247..90dec92a 100644
--- a/src/util/entities/Invite.ts
+++ b/src/util/entities/Invite.ts
@@ -1,4 +1,11 @@
-import { Column, Entity, JoinColumn, ManyToOne, RelationId, PrimaryColumn } from "typeorm";
+import {
+	Column,
+	Entity,
+	JoinColumn,
+	ManyToOne,
+	RelationId,
+	PrimaryColumn,
+} from "typeorm";
 import { Member } from "./Member";
 import { BaseClassWithoutId } from "./BaseClass";
 import { Channel } from "./Channel";
@@ -76,7 +83,8 @@ export class Invite extends BaseClassWithoutId {
 
 	static async joinGuild(user_id: string, code: string) {
 		const invite = await Invite.findOneOrFail({ where: { code } });
-		if (invite.uses++ >= invite.max_uses && invite.max_uses !== 0) await Invite.delete({ code });
+		if (invite.uses++ >= invite.max_uses && invite.max_uses !== 0)
+			await Invite.delete({ code });
 		else await invite.save();
 
 		await Member.addToGuild(user_id, invite.guild_id);
diff --git a/src/util/entities/Member.ts b/src/util/entities/Member.ts
index 7d1346ba..f2762adc 100644
--- a/src/util/entities/Member.ts
+++ b/src/util/entities/Member.ts
@@ -22,7 +22,6 @@ import {
 	GuildMemberRemoveEvent,
 	GuildMemberUpdateEvent,
 	MessageCreateEvent,
-
 } from "../interfaces";
 import { HTTPError } from "lambert-server";
 import { Role } from "./Role";
@@ -126,19 +125,34 @@ export class Member extends BaseClassWithoutId {
 		if (this.nick) {
 			this.nick = this.nick.split("\n").join("");
 			this.nick = this.nick.split("\t").join("");
-			if (BannedWords.find(this.nick)) throw FieldErrors({ nick: { message: "Bad nickname", code: "INVALID_NICKNAME" } });
+			if (BannedWords.find(this.nick))
+				throw FieldErrors({
+					nick: { message: "Bad nickname", code: "INVALID_NICKNAME" },
+				});
 		}
 	}
 
 	static async IsInGuildOrFail(user_id: string, guild_id: string) {
-		if (await Member.count({ where: { id: user_id, guild: { id: guild_id } } })) return true;
+		if (
+			await Member.count({
+				where: { id: user_id, guild: { id: guild_id } },
+			})
+		)
+			return true;
 		throw new HTTPError("You are not member of this guild", 403);
 	}
 
 	static async removeFromGuild(user_id: string, guild_id: string) {
-		const guild = await Guild.findOneOrFail({ select: ["owner_id"], where: { id: guild_id } });
-		if (guild.owner_id === user_id) throw new Error("The owner cannot be removed of the guild");
-		const member = await Member.findOneOrFail({ where: { id: user_id, guild_id }, relations: ["user"] });
+		const guild = await Guild.findOneOrFail({
+			select: ["owner_id"],
+			where: { id: guild_id },
+		});
+		if (guild.owner_id === user_id)
+			throw new Error("The owner cannot be removed of the guild");
+		const member = await Member.findOneOrFail({
+			where: { id: user_id, guild_id },
+			relations: ["user"],
+		});
 
 		// use promise all to execute all promises at the same time -> save time
 		return Promise.all([
@@ -169,9 +183,12 @@ export class Member extends BaseClassWithoutId {
 				where: { id: user_id, guild_id },
 				relations: ["user", "roles"], // we don't want to load  the role objects just the ids
 				//@ts-ignore
-				select: ["index", "roles.id"],	// TODO fix type
+				select: ["index", "roles.id"], // TODO fix type
+			}),
+			Role.findOneOrFail({
+				where: { id: role_id, guild_id },
+				select: ["id"],
 			}),
-			Role.findOneOrFail({ where: { id: role_id, guild_id }, select: ["id"] }),
 		]);
 		member.roles.push(Role.create({ id: role_id }));
 
@@ -189,7 +206,11 @@ export class Member extends BaseClassWithoutId {
 		]);
 	}
 
-	static async removeRole(user_id: string, guild_id: string, role_id: string) {
+	static async removeRole(
+		user_id: string,
+		guild_id: string,
+		role_id: string,
+	) {
 		const [member] = await Promise.all([
 			Member.findOneOrFail({
 				where: { id: user_id, guild_id },
@@ -215,7 +236,11 @@ export class Member extends BaseClassWithoutId {
 		]);
 	}
 
-	static async changeNickname(user_id: string, guild_id: string, nickname: string) {
+	static async changeNickname(
+		user_id: string,
+		guild_id: string,
+		nickname: string,
+	) {
 		const member = await Member.findOneOrFail({
 			where: {
 				id: user_id,
@@ -249,7 +274,10 @@ export class Member extends BaseClassWithoutId {
 		const { maxGuilds } = Config.get().limits.user;
 		const guild_count = await Member.count({ where: { id: user_id } });
 		if (guild_count >= maxGuilds) {
-			throw new HTTPError(`You are at the ${maxGuilds} server limit.`, 403);
+			throw new HTTPError(
+				`You are at the ${maxGuilds} server limit.`,
+				403,
+			);
 		}
 
 		const guild = await Guild.findOneOrFail({
@@ -259,7 +287,11 @@ export class Member extends BaseClassWithoutId {
 			relations: [...PublicGuildRelations, "system_channel"],
 		});
 
-		if (await Member.count({ where: { id: user.id, guild: { id: guild_id } } }))
+		if (
+			await Member.count({
+				where: { id: user.id, guild: { id: guild_id } },
+			})
+		)
 			throw new HTTPError("You are already a member of this guild", 400);
 
 		const member = {
@@ -268,7 +300,7 @@ export class Member extends BaseClassWithoutId {
 			nick: undefined,
 			roles: [guild_id], // @everyone role
 			joined_at: new Date(),
-			premium_since: (new Date()).getTime(),
+			premium_since: new Date().getTime(),
 			deaf: false,
 			mute: false,
 			pending: false,
@@ -339,7 +371,11 @@ export class Member extends BaseClassWithoutId {
 			});
 			await Promise.all([
 				message.save(),
-				emitEvent({ event: "MESSAGE_CREATE", channel_id: message.channel_id, data: message } as MessageCreateEvent)
+				emitEvent({
+					event: "MESSAGE_CREATE",
+					channel_id: message.channel_id,
+					data: message,
+				} as MessageCreateEvent),
 			]);
 		}
 	}
@@ -362,7 +398,7 @@ export interface UserGuildSettings {
 
 	channel_overrides: {
 		[channel_id: string]: ChannelOverride;
-	} | null,
+	} | null;
 	message_notifications: number;
 	mobile_push: boolean;
 	mute_config: MuteConfig | null;
@@ -389,7 +425,7 @@ export const DefaultUserGuildSettings: UserGuildSettings = {
 	notify_highlights: 0,
 	suppress_everyone: false,
 	suppress_roles: false,
-	version: 453,	// ?
+	version: 453, // ?
 	guild_id: null,
 };
 
diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts
index be790502..a52b4785 100644
--- a/src/util/entities/Message.ts
+++ b/src/util/entities/Message.ts
@@ -51,7 +51,7 @@ export enum MessageType {
 	SELF_COMMAND_SCRIPT = 43, // self command scripts
 	ENCRYPTION = 50,
 	CUSTOM_START = 63,
-	UNHANDLED = 255
+	UNHANDLED = 255,
 }
 
 @Entity("messages")
@@ -115,7 +115,10 @@ export class Message extends BaseClass {
 	@ManyToOne(() => Application)
 	application?: Application;
 
-	@Column({ nullable: true, type: process.env.PRODUCTION ? "longtext" : undefined })
+	@Column({
+		nullable: true,
+		type: process.env.PRODUCTION ? "longtext" : undefined,
+	})
 	content?: string;
 
 	@Column()
@@ -147,10 +150,14 @@ export class Message extends BaseClass {
 	@ManyToMany(() => Sticker, { cascade: true, onDelete: "CASCADE" })
 	sticker_items?: Sticker[];
 
-	@OneToMany(() => Attachment, (attachment: Attachment) => attachment.message, {
-		cascade: true,
-		orphanedRowAction: "delete",
-	})
+	@OneToMany(
+		() => Attachment,
+		(attachment: Attachment) => attachment.message,
+		{
+			cascade: true,
+			orphanedRowAction: "delete",
+		},
+	)
 	attachments?: Attachment[];
 
 	@Column({ type: "simple-json" })
@@ -176,7 +183,7 @@ export class Message extends BaseClass {
 
 	@Column({ nullable: true })
 	flags?: string;
-	
+
 	@Column({ type: "simple-json", nullable: true })
 	message_reference?: {
 		message_id: string;
@@ -204,7 +211,11 @@ export class Message extends BaseClass {
 	@BeforeInsert()
 	validate() {
 		if (this.content) {
-			if (BannedWords.find(this.content)) throw new HTTPError("Message was blocked by automatic moderation", 200000);
+			if (BannedWords.find(this.content))
+				throw new HTTPError(
+					"Message was blocked by automatic moderation",
+					200000,
+				);
 		}
 	}
 }
diff --git a/src/util/entities/Migration.ts b/src/util/entities/Migration.ts
index 3f39ae72..f4e54eae 100644
--- a/src/util/entities/Migration.ts
+++ b/src/util/entities/Migration.ts
@@ -1,7 +1,14 @@
-import { Column, Entity, ObjectIdColumn, PrimaryGeneratedColumn } from "typeorm";
+import {
+	Column,
+	Entity,
+	ObjectIdColumn,
+	PrimaryGeneratedColumn,
+} from "typeorm";
 import { BaseClassWithoutId } from ".";
 
-export const PrimaryIdAutoGenerated = process.env.DATABASE?.startsWith("mongodb")
+export const PrimaryIdAutoGenerated = process.env.DATABASE?.startsWith(
+	"mongodb",
+)
 	? ObjectIdColumn
 	: PrimaryGeneratedColumn;
 
diff --git a/src/util/entities/Note.ts b/src/util/entities/Note.ts
index 36017c5e..b3ac45ee 100644
--- a/src/util/entities/Note.ts
+++ b/src/util/entities/Note.ts
@@ -15,4 +15,4 @@ export class Note extends BaseClass {
 
 	@Column()
 	content: string;
-}
\ No newline at end of file
+}
diff --git a/src/util/entities/ReadState.ts b/src/util/entities/ReadState.ts
index b915573b..53ed5589 100644
--- a/src/util/entities/ReadState.ts
+++ b/src/util/entities/ReadState.ts
@@ -1,4 +1,11 @@
-import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import {
+	Column,
+	Entity,
+	Index,
+	JoinColumn,
+	ManyToOne,
+	RelationId,
+} from "typeorm";
 import { BaseClass } from "./BaseClass";
 import { Channel } from "./Channel";
 import { Message } from "./Message";
@@ -33,8 +40,8 @@ export class ReadState extends BaseClass {
 
 	// fully read marker
 	@Column({ nullable: true })
-	last_message_id: string; 
-	
+	last_message_id: string;
+
 	// public read receipt
 	@Column({ nullable: true })
 	public_ack: string;
diff --git a/src/util/entities/Relationship.ts b/src/util/entities/Relationship.ts
index c3592c76..25b52757 100644
--- a/src/util/entities/Relationship.ts
+++ b/src/util/entities/Relationship.ts
@@ -1,4 +1,11 @@
-import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import {
+	Column,
+	Entity,
+	Index,
+	JoinColumn,
+	ManyToOne,
+	RelationId,
+} from "typeorm";
 import { BaseClass } from "./BaseClass";
 import { User } from "./User";
 
diff --git a/src/util/entities/StickerPack.ts b/src/util/entities/StickerPack.ts
index ec8c69a2..04d74bac 100644
--- a/src/util/entities/StickerPack.ts
+++ b/src/util/entities/StickerPack.ts
@@ -1,4 +1,12 @@
-import { Column, Entity, JoinColumn, ManyToOne, OneToMany, OneToOne, RelationId } from "typeorm";
+import {
+	Column,
+	Entity,
+	JoinColumn,
+	ManyToOne,
+	OneToMany,
+	OneToOne,
+	RelationId,
+} from "typeorm";
 import { Sticker } from ".";
 import { BaseClass } from "./BaseClass";
 
diff --git a/src/util/entities/Team.ts b/src/util/entities/Team.ts
index 22140b7f..8f410bb4 100644
--- a/src/util/entities/Team.ts
+++ b/src/util/entities/Team.ts
@@ -1,4 +1,12 @@
-import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, OneToMany, RelationId } from "typeorm";
+import {
+	Column,
+	Entity,
+	JoinColumn,
+	ManyToMany,
+	ManyToOne,
+	OneToMany,
+	RelationId,
+} from "typeorm";
 import { BaseClass } from "./BaseClass";
 import { TeamMember } from "./TeamMember";
 import { User } from "./User";
diff --git a/src/util/entities/TeamMember.ts b/src/util/entities/TeamMember.ts
index b726e1e8..3f4a0422 100644
--- a/src/util/entities/TeamMember.ts
+++ b/src/util/entities/TeamMember.ts
@@ -20,9 +20,13 @@ export class TeamMember extends BaseClass {
 	team_id: string;
 
 	@JoinColumn({ name: "team_id" })
-	@ManyToOne(() => require("./Team").Team, (team: import("./Team").Team) => team.members, {
-		onDelete: "CASCADE",
-	})
+	@ManyToOne(
+		() => require("./Team").Team,
+		(team: import("./Team").Team) => team.members,
+		{
+			onDelete: "CASCADE",
+		},
+	)
 	team: import("./Team").Team;
 
 	@Column({ nullable: true })
diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts
index 84a8a674..1389a424 100644
--- a/src/util/entities/User.ts
+++ b/src/util/entities/User.ts
@@ -1,9 +1,24 @@
-import { BeforeInsert, BeforeUpdate, Column, Entity, FindOneOptions, JoinColumn, OneToMany } from "typeorm";
+import {
+	BeforeInsert,
+	BeforeUpdate,
+	Column,
+	Entity,
+	FindOneOptions,
+	JoinColumn,
+	OneToMany,
+} from "typeorm";
 import { BaseClass } from "./BaseClass";
 import { BitField } from "../util/BitField";
 import { Relationship } from "./Relationship";
 import { ConnectedAccount } from "./ConnectedAccount";
-import { Config, FieldErrors, Snowflake, trimSpecial, BannedWords, adjustEmail } from "..";
+import {
+	Config,
+	FieldErrors,
+	Snowflake,
+	trimSpecial,
+	BannedWords,
+	adjustEmail,
+} from "..";
 import { Member, Session } from ".";
 
 export enum PublicUserEnum {
@@ -38,7 +53,7 @@ export enum PrivateUserEnum {
 export type PrivateUserKeys = keyof typeof PrivateUserEnum | PublicUserKeys;
 
 export const PublicUserProjection = Object.values(PublicUserEnum).filter(
-	(x) => typeof x === "string"
+	(x) => typeof x === "string",
 ) as PublicUserKeys[];
 export const PrivateUserProjection = [
 	...PublicUserProjection,
@@ -48,7 +63,7 @@ export const PrivateUserProjection = [
 // Private user data that should never get sent to the client
 export type PublicUser = Pick<User, PublicUserKeys>;
 
-export interface UserPublic extends Pick<User, PublicUserKeys> { }
+export interface UserPublic extends Pick<User, PublicUserKeys> {}
 
 export interface UserPrivate extends Pick<User, PrivateUserKeys> {
 	locale: string;
@@ -144,17 +159,25 @@ export class User extends BaseClass {
 	sessions: Session[];
 
 	@JoinColumn({ name: "relationship_ids" })
-	@OneToMany(() => Relationship, (relationship: Relationship) => relationship.from, {
-		cascade: true,
-		orphanedRowAction: "delete",
-	})
+	@OneToMany(
+		() => Relationship,
+		(relationship: Relationship) => relationship.from,
+		{
+			cascade: true,
+			orphanedRowAction: "delete",
+		},
+	)
 	relationships: Relationship[];
 
 	@JoinColumn({ name: "connected_account_ids" })
-	@OneToMany(() => ConnectedAccount, (account: ConnectedAccount) => account.user, {
-		cascade: true,
-		orphanedRowAction: "delete",
-	})
+	@OneToMany(
+		() => ConnectedAccount,
+		(account: ConnectedAccount) => account.user,
+		{
+			cascade: true,
+			orphanedRowAction: "delete",
+		},
+	)
 	connected_accounts: ConnectedAccount[];
 
 	@Column({ type: "simple-json", select: false })
@@ -177,16 +200,43 @@ export class User extends BaseClass {
 	@BeforeInsert()
 	validate() {
 		this.email = adjustEmail(this.email);
-		if (!this.email) throw FieldErrors({ email: { message: "Invalid email", code: "EMAIL_INVALID" } });
-		if (!this.email.match(/([a-z\d.-]{3,})@([a-z\d.-]+).([a-z]{2,})/g)) throw FieldErrors({ email: { message: "Invalid email", code: "EMAIL_INVALID" } });
+		if (!this.email)
+			throw FieldErrors({
+				email: { message: "Invalid email", code: "EMAIL_INVALID" },
+			});
+		if (!this.email.match(/([a-z\d.-]{3,})@([a-z\d.-]+).([a-z]{2,})/g))
+			throw FieldErrors({
+				email: { message: "Invalid email", code: "EMAIL_INVALID" },
+			});
 
 		const discrim = Number(this.discriminator);
-		if (this.discriminator.length > 4) throw FieldErrors({ email: { message: "Discriminator cannot be more than 4 digits.", code: "DISCRIMINATOR_INVALID" } });
-		if (isNaN(discrim)) throw FieldErrors({ email: { message: "Discriminator must be a number.", code: "DISCRIMINATOR_INVALID" } });
-		if (discrim <= 0 || discrim >= 10000) throw FieldErrors({ email: { message: "Discriminator must be a number.", code: "DISCRIMINATOR_INVALID" } });
+		if (this.discriminator.length > 4)
+			throw FieldErrors({
+				email: {
+					message: "Discriminator cannot be more than 4 digits.",
+					code: "DISCRIMINATOR_INVALID",
+				},
+			});
+		if (isNaN(discrim))
+			throw FieldErrors({
+				email: {
+					message: "Discriminator must be a number.",
+					code: "DISCRIMINATOR_INVALID",
+				},
+			});
+		if (discrim <= 0 || discrim >= 10000)
+			throw FieldErrors({
+				email: {
+					message: "Discriminator must be a number.",
+					code: "DISCRIMINATOR_INVALID",
+				},
+			});
 		this.discriminator = discrim.toString().padStart(4, "0");
 
-		if (BannedWords.find(this.username)) throw FieldErrors({ username: { message: "Bad username", code: "INVALID_USERNAME" } });
+		if (BannedWords.find(this.username))
+			throw FieldErrors({
+				username: { message: "Bad username", code: "INVALID_USERNAME" },
+			});
 	}
 
 	toPublicUser() {
@@ -202,17 +252,25 @@ export class User extends BaseClass {
 			where: { id: user_id },
 			...opts,
 			//@ts-ignore
-			select: [...PublicUserProjection, ...(opts?.select || [])],	// TODO: fix
+			select: [...PublicUserProjection, ...(opts?.select || [])], // TODO: fix
 		});
 	}
 
-	private static async generateDiscriminator(username: string): Promise<string | undefined> {
+	private static async generateDiscriminator(
+		username: string,
+	): Promise<string | undefined> {
 		if (Config.get().register.incrementingDiscriminators) {
 			// discriminator will be incrementally generated
 
 			// First we need to figure out the currently highest discrimnator for the given username and then increment it
-			const users = await User.find({ where: { username }, select: ["discriminator"] });
-			const highestDiscriminator = Math.max(0, ...users.map((u) => Number(u.discriminator)));
+			const users = await User.find({
+				where: { username },
+				select: ["discriminator"],
+			});
+			const highestDiscriminator = Math.max(
+				0,
+				...users.map((u) => Number(u.discriminator)),
+			);
 
 			const discriminator = highestDiscriminator + 1;
 			if (discriminator >= 10000) {
@@ -226,8 +284,13 @@ export class User extends BaseClass {
 			// randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists
 			// TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the database?
 			for (let tries = 0; tries < 5; tries++) {
-				const discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0");
-				const exists = await User.findOne({ where: { discriminator, username: username }, select: ["id"] });
+				const discriminator = Math.randomIntBetween(1, 9999)
+					.toString()
+					.padStart(4, "0");
+				const exists = await User.findOne({
+					where: { discriminator, username: username },
+					select: ["id"],
+				});
 				if (!exists) return discriminator;
 			}
 
@@ -265,7 +328,8 @@ export class User extends BaseClass {
 		// TODO: save date_of_birth
 		// appearently discord doesn't save the date of birth and just calculate if nsfw is allowed
 		// if nsfw_allowed is null/undefined it'll require date_of_birth to set it to true/false
-		const language = req.language === "en" ? "en-US" : req.language || "en-US";
+		const language =
+			req.language === "en" ? "en-US" : req.language || "en-US";
 
 		const user = User.create({
 			created_at: new Date(),
@@ -295,8 +359,8 @@ export class User extends BaseClass {
 			},
 			settings: { ...defaultSettings, locale: language },
 			purchased_flags: 5, // TODO: idk what the values for this are
-			premium_usage_flags: 2,  // TODO: idk what the values for this are
-			extended_settings: "",	// TODO: was {}
+			premium_usage_flags: 2, // TODO: idk what the values for this are
+			extended_settings: "", // TODO: was {}
 			fingerprints: [],
 		});
 
@@ -305,7 +369,7 @@ export class User extends BaseClass {
 		setImmediate(async () => {
 			if (Config.get().guild.autoJoin.enabled) {
 				for (const guild of Config.get().guild.autoJoin.guilds || []) {
-					await Member.addToGuild(user.id, guild).catch((e) => { });
+					await Member.addToGuild(user.id, guild).catch((e) => {});
 				}
 			}
 		});
@@ -372,7 +436,7 @@ export interface UserSettings {
 	disable_games_tab: boolean;
 	enable_tts_command: boolean;
 	explicit_content_filter: number;
-	friend_source_flags: { all: boolean; };
+	friend_source_flags: { all: boolean };
 	gateway_connected: boolean;
 	gif_auto_play: boolean;
 	// every top guild is displayed as a "folder"
diff --git a/src/util/entities/index.ts b/src/util/entities/index.ts
index 49793810..c439a4b7 100644
--- a/src/util/entities/index.ts
+++ b/src/util/entities/index.ts
@@ -29,4 +29,4 @@ export * from "./VoiceState";
 export * from "./Webhook";
 export * from "./ClientRelease";
 export * from "./BackupCodes";
-export * from "./Note";
\ No newline at end of file
+export * from "./Note";