summary refs log tree commit diff
path: root/util/src
diff options
context:
space:
mode:
Diffstat (limited to 'util/src')
-rw-r--r--util/src/dtos/DmChannelDTO.ts35
-rw-r--r--util/src/dtos/UserDTO.ts17
-rw-r--r--util/src/dtos/index.ts2
-rw-r--r--util/src/entities/Application.ts4
-rw-r--r--util/src/entities/Attachment.ts13
-rw-r--r--util/src/entities/Ban.ts8
-rw-r--r--util/src/entities/BaseClass.ts48
-rw-r--r--util/src/entities/Channel.ts179
-rw-r--r--util/src/entities/Config.ts10
-rw-r--r--util/src/entities/ConnectedAccount.ts4
-rw-r--r--util/src/entities/Emoji.ts4
-rw-r--r--util/src/entities/Guild.ts52
-rw-r--r--util/src/entities/Invite.ts22
-rw-r--r--util/src/entities/Member.ts9
-rw-r--r--util/src/entities/Message.ts22
-rw-r--r--util/src/entities/ReadState.ts8
-rw-r--r--util/src/entities/Recipient.ts11
-rw-r--r--util/src/entities/Relationship.ts8
-rw-r--r--util/src/entities/Role.ts4
-rw-r--r--util/src/entities/Session.ts4
-rw-r--r--util/src/entities/Sticker.ts4
-rw-r--r--util/src/entities/Team.ts4
-rw-r--r--util/src/entities/TeamMember.ts8
-rw-r--r--util/src/entities/User.ts12
-rw-r--r--util/src/entities/VoiceState.ts16
-rw-r--r--util/src/entities/Webhook.ts20
-rw-r--r--util/src/index.ts1
-rw-r--r--util/src/interfaces/Event.ts22
-rw-r--r--util/src/util/Array.ts3
-rw-r--r--util/src/util/Database.ts2
-rw-r--r--util/src/util/Permissions.ts1
-rw-r--r--util/src/util/cdn.ts54
-rw-r--r--util/src/util/index.ts2
33 files changed, 543 insertions, 70 deletions
diff --git a/util/src/dtos/DmChannelDTO.ts b/util/src/dtos/DmChannelDTO.ts
new file mode 100644
index 00000000..8b7a18fd
--- /dev/null
+++ b/util/src/dtos/DmChannelDTO.ts
@@ -0,0 +1,35 @@
+import { MinimalPublicUserDTO } from "./UserDTO";
+import { Channel, PublicUserProjection, User } from "../entities";
+
+export class DmChannelDTO {
+	icon: string | null;
+	id: string;
+	last_message_id: string | null;
+	name: string | null;
+	origin_channel_id: string | null;
+	owner_id?: string;
+	recipients: MinimalPublicUserDTO[];
+	type: number;
+
+	static async from(channel: Channel, excluded_recipients: string[] = [], origin_channel_id?: string) {
+		const obj = new DmChannelDTO()
+		obj.icon = channel.icon || null
+		obj.id = channel.id
+		obj.last_message_id = channel.last_message_id || null
+		obj.name = channel.name || null
+		obj.origin_channel_id = origin_channel_id || null
+		obj.owner_id = channel.owner_id
+		obj.type = channel.type
+		obj.recipients = (await Promise.all(channel.recipients!.filter(r => !excluded_recipients.includes(r.user_id)).map(async r => {
+			return await User.findOneOrFail({ where: { id: r.user_id }, select: PublicUserProjection })
+		}))).map(u => new MinimalPublicUserDTO(u))
+		return obj
+	}
+
+	excludedRecipients(excluded_recipients: string[]): DmChannelDTO {
+		return {
+			...this,
+			recipients: this.recipients.filter(r => !excluded_recipients.includes(r.id))
+		}
+	}
+}
\ No newline at end of file
diff --git a/util/src/dtos/UserDTO.ts b/util/src/dtos/UserDTO.ts
new file mode 100644
index 00000000..f09b5f4e
--- /dev/null
+++ b/util/src/dtos/UserDTO.ts
@@ -0,0 +1,17 @@
+import { User } from "../entities";
+
+export class MinimalPublicUserDTO {
+	avatar?: string | null;
+	discriminator: string;
+	id: string;
+	public_flags: number;
+	username: string;
+
+	constructor(user: User) {
+		this.avatar = user.avatar
+		this.discriminator = user.discriminator
+		this.id = user.id
+		this.public_flags = user.public_flags
+		this.username = user.username
+	}
+}
\ No newline at end of file
diff --git a/util/src/dtos/index.ts b/util/src/dtos/index.ts
new file mode 100644
index 00000000..13702342
--- /dev/null
+++ b/util/src/dtos/index.ts
@@ -0,0 +1,2 @@
+export * from "./DmChannelDTO";
+export * from "./UserDTO";
\ No newline at end of file
diff --git a/util/src/entities/Application.ts b/util/src/entities/Application.ts
index 2092cd4e..fab3d93f 100644
--- a/util/src/entities/Application.ts
+++ b/util/src/entities/Application.ts
@@ -41,7 +41,9 @@ export class Application extends BaseClass {
 	verify_key: string;
 
 	@JoinColumn({ name: "team_id" })
-	@ManyToOne(() => Team)
+	@ManyToOne(() => Team, {
+		onDelete: "CASCADE",
+	})
 	team?: Team;
 
 	@JoinColumn({ name: "guild_id" })
diff --git a/util/src/entities/Attachment.ts b/util/src/entities/Attachment.ts
index ca893400..7b4b17eb 100644
--- a/util/src/entities/Attachment.ts
+++ b/util/src/entities/Attachment.ts
@@ -1,4 +1,6 @@
-import { 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";
 
 @Entity("attachments")
@@ -29,6 +31,13 @@ export class Attachment extends BaseClass {
 	message_id: string;
 
 	@JoinColumn({ name: "message_id" })
-	@ManyToOne(() => require("./Message").Message, (message: import("./Message").Message) => message.attachments)
+	@ManyToOne(() => require("./Message").Message, (message: import("./Message").Message) => message.attachments, {
+		onDelete: "CASCADE",
+	})
 	message: import("./Message").Message;
+
+	@BeforeRemove()
+	onDelete() {
+		return deleteFile(new URL(this.url).pathname);
+	}
 }
diff --git a/util/src/entities/Ban.ts b/util/src/entities/Ban.ts
index e8a6d648..9504bd8e 100644
--- a/util/src/entities/Ban.ts
+++ b/util/src/entities/Ban.ts
@@ -10,7 +10,9 @@ export class Ban extends BaseClass {
 	user_id: string;
 
 	@JoinColumn({ name: "user_id" })
-	@ManyToOne(() => User)
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
 	user: User;
 
 	@Column({ nullable: true })
@@ -18,7 +20,9 @@ export class Ban extends BaseClass {
 	guild_id: string;
 
 	@JoinColumn({ name: "guild_id" })
-	@ManyToOne(() => Guild)
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
 	guild: Guild;
 
 	@Column({ nullable: true })
diff --git a/util/src/entities/BaseClass.ts b/util/src/entities/BaseClass.ts
index 9b2ce058..d18757f2 100644
--- a/util/src/entities/BaseClass.ts
+++ b/util/src/entities/BaseClass.ts
@@ -1,5 +1,15 @@
 import "reflect-metadata";
-import { BaseEntity, BeforeInsert, BeforeUpdate, EntityMetadata, FindConditions, PrimaryColumn } from "typeorm";
+import {
+	BaseEntity,
+	BeforeInsert,
+	BeforeUpdate,
+	EntityMetadata,
+	FindConditions,
+	getConnection,
+	getManager,
+	PrimaryColumn,
+	RemoveOptions,
+} from "typeorm";
 import { Snowflake } from "../util/Snowflake";
 import "missing-native-js-functions";
 
@@ -69,6 +79,42 @@ export class BaseClassWithoutId extends BaseEntity {
 		const repository = this.getRepository();
 		return repository.decrement(conditions, propertyPath, value);
 	}
+
+	// static async delete<T>(criteria: FindConditions<T>, options?: RemoveOptions) {
+	// 	if (!criteria) throw new Error("You need to specify delete criteria");
+
+	// 	const repository = this.getRepository();
+	// 	const promises = repository.metadata.relations.map(async (x) => {
+	// 		if (x.orphanedRowAction !== "delete") return;
+
+	// 		const foreignKey =
+	// 			x.foreignKeys.find((key) => key.entityMetadata === repository.metadata) ||
+	// 			x.inverseRelation?.foreignKeys[0]; // find foreign key for this entity
+	// 		if (!foreignKey) {
+	// 			throw new Error(
+	// 				`Foreign key not found for entity ${repository.metadata.name} in relation ${x.propertyName}`
+	// 			);
+	// 		}
+	// 		const id = (criteria as any)[foreignKey.referencedColumnNames[0]];
+	// 		if (!id) throw new Error("id missing in criteria options " + foreignKey.referencedColumnNames);
+
+	// 		if (x.relationType === "many-to-many") {
+	// 			return getConnection()
+	// 				.createQueryBuilder()
+	// 				.relation(this, x.propertyName)
+	// 				.of(id)
+	// 				.remove({ [foreignKey.columnNames[0]]: id });
+	// 		} else if (
+	// 			x.relationType === "one-to-one" ||
+	// 			x.relationType === "many-to-one" ||
+	// 			x.relationType === "one-to-many"
+	// 		) {
+	// 			return (x.inverseEntityMetadata.target as any).delete({ [foreignKey.columnNames[0]]: id });
+	// 		}
+	// 	});
+	// 	await Promise.all(promises);
+	// 	return super.delete(criteria, options);
+	// }
 }
 
 export class BaseClass extends BaseClassWithoutId {
diff --git a/util/src/entities/Channel.ts b/util/src/entities/Channel.ts
index fc954f63..ece82bd0 100644
--- a/util/src/entities/Channel.ts
+++ b/util/src/entities/Channel.ts
@@ -1,12 +1,17 @@
-import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, RelationId } from "typeorm";
+import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";
 import { BaseClass } from "./BaseClass";
 import { Guild } from "./Guild";
-import { Message } from "./Message";
-import { User } from "./User";
+import { PublicUserProjection, User } from "./User";
 import { HTTPError } from "lambert-server";
-import { emitEvent, getPermission, Snowflake } from "../util";
-import { ChannelCreateEvent } from "../interfaces";
+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
@@ -31,26 +36,29 @@ export class Channel extends BaseClass {
 	@Column({ nullable: true })
 	name?: string;
 
+	@Column({ type: "text", nullable: true })
+	icon?: string | null;
+
 	@Column({ type: "simple-enum", enum: ChannelType })
 	type: ChannelType;
 
-	@OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, { cascade: true })
+	@OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, {
+		cascade: true,
+		orphanedRowAction: "delete",
+	})
 	recipients?: Recipient[];
 
 	@Column({ nullable: true })
-	@RelationId((channel: Channel) => channel.last_message)
 	last_message_id: string;
 
-	@JoinColumn({ name: "last_message_id" })
-	@ManyToOne(() => Message)
-	last_message?: Message;
-
 	@Column({ nullable: true })
 	@RelationId((channel: Channel) => channel.guild)
 	guild_id?: string;
 
 	@JoinColumn({ name: "guild_id" })
-	@ManyToOne(() => Guild)
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
 	guild: Guild;
 
 	@Column({ nullable: true })
@@ -100,6 +108,36 @@ export class Channel extends BaseClass {
 	@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>,
@@ -162,6 +200,123 @@ export class Channel extends BaseClass {
 
 		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 {
diff --git a/util/src/entities/Config.ts b/util/src/entities/Config.ts
index fd830db8..f969b6bb 100644
--- a/util/src/entities/Config.ts
+++ b/util/src/entities/Config.ts
@@ -110,13 +110,13 @@ export interface ConfigValue {
 	};
 	register: {
 		email: {
-			necessary: boolean; // we have to use necessary instead of required as the cli tool uses json schema and can't use required
+			required: boolean;
 			allowlist: boolean;
 			blocklist: boolean;
 			domains: string[];
 		};
 		dateOfBirth: {
-			necessary: boolean;
+			required: boolean;
 			minimum: number; // in years
 		};
 		requireCaptcha: boolean;
@@ -125,6 +125,7 @@ export interface ConfigValue {
 		allowMultipleAccounts: boolean;
 		blockProxies: boolean;
 		password: {
+			required: boolean;
 			minLength: number;
 			minNumbers: number;
 			minUpperCase: number;
@@ -246,14 +247,14 @@ export const DefaultConfigOptions: ConfigValue = {
 	},
 	register: {
 		email: {
-			necessary: true,
+			required: false,
 			allowlist: false,
 			blocklist: true,
 			domains: [], // TODO: efficiently save domain blocklist in database
 			// domains: fs.readFileSync(__dirname + "/blockedEmailDomains.txt", { encoding: "utf8" }).split("\n"),
 		},
 		dateOfBirth: {
-			necessary: true,
+			required: false,
 			minimum: 13,
 		},
 		requireInvite: false,
@@ -262,6 +263,7 @@ export const DefaultConfigOptions: ConfigValue = {
 		allowMultipleAccounts: true,
 		blockProxies: true,
 		password: {
+			required: false,
 			minLength: 8,
 			minNumbers: 2,
 			minUpperCase: 2,
diff --git a/util/src/entities/ConnectedAccount.ts b/util/src/entities/ConnectedAccount.ts
index 59a8f423..b8aa2889 100644
--- a/util/src/entities/ConnectedAccount.ts
+++ b/util/src/entities/ConnectedAccount.ts
@@ -11,7 +11,9 @@ export class ConnectedAccount extends BaseClass {
 	user_id: string;
 
 	@JoinColumn({ name: "user_id" })
-	@ManyToOne(() => User)
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
 	user: User;
 
 	@Column({ select: false })
diff --git a/util/src/entities/Emoji.ts b/util/src/entities/Emoji.ts
index 181aff2c..a252d9f4 100644
--- a/util/src/entities/Emoji.ts
+++ b/util/src/entities/Emoji.ts
@@ -15,7 +15,9 @@ export class Emoji extends BaseClass {
 	guild_id: string;
 
 	@JoinColumn({ name: "guild_id" })
-	@ManyToOne(() => Guild)
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
 	guild: Guild;
 
 	@Column()
diff --git a/util/src/entities/Guild.ts b/util/src/entities/Guild.ts
index 7b5d2908..e107937d 100644
--- a/util/src/entities/Guild.ts
+++ b/util/src/entities/Guild.ts
@@ -81,7 +81,10 @@ export class Guild extends BaseClass {
 	// application?: string;
 
 	@JoinColumn({ name: "ban_ids" })
-	@OneToMany(() => Ban, (ban: Ban) => ban.guild)
+	@OneToMany(() => Ban, (ban: Ban) => ban.guild, {
+		cascade: true,
+		orphanedRowAction: "delete",
+	})
 	bans: Ban[];
 
 	@Column({ nullable: true })
@@ -124,15 +127,26 @@ export class Guild extends BaseClass {
 	@Column({ nullable: true })
 	presence_count?: number; // users online
 
-	@OneToMany(() => Member, (member: Member) => member.guild)
+	@OneToMany(() => Member, (member: Member) => member.guild, {
+		cascade: true,
+		orphanedRowAction: "delete",
+		onDelete: "CASCADE",
+	})
 	members: Member[];
 
 	@JoinColumn({ name: "role_ids" })
-	@OneToMany(() => Role, (role: Role) => role.guild)
+	@OneToMany(() => Role, (role: Role) => role.guild, {
+		cascade: true,
+		orphanedRowAction: "delete",
+		onDelete: "CASCADE",
+	})
 	roles: Role[];
 
 	@JoinColumn({ name: "channel_ids" })
-	@OneToMany(() => Channel, (channel: Channel) => channel.guild)
+	@OneToMany(() => Channel, (channel: Channel) => channel.guild, {
+		cascade: true,
+		orphanedRowAction: "delete",
+	})
 	channels: Channel[];
 
 	@Column({ nullable: true })
@@ -144,23 +158,43 @@ export class Guild extends BaseClass {
 	template: Template;
 
 	@JoinColumn({ name: "emoji_ids" })
-	@OneToMany(() => Emoji, (emoji: Emoji) => emoji.guild)
+	@OneToMany(() => Emoji, (emoji: Emoji) => emoji.guild, {
+		cascade: true,
+		orphanedRowAction: "delete",
+		onDelete: "CASCADE",
+	})
 	emojis: Emoji[];
 
 	@JoinColumn({ name: "sticker_ids" })
-	@OneToMany(() => Sticker, (sticker: Sticker) => sticker.guild)
+	@OneToMany(() => Sticker, (sticker: Sticker) => sticker.guild, {
+		cascade: true,
+		orphanedRowAction: "delete",
+		onDelete: "CASCADE",
+	})
 	stickers: Sticker[];
 
 	@JoinColumn({ name: "invite_ids" })
-	@OneToMany(() => Invite, (invite: Invite) => invite.guild)
+	@OneToMany(() => Invite, (invite: Invite) => invite.guild, {
+		cascade: true,
+		orphanedRowAction: "delete",
+		onDelete: "CASCADE",
+	})
 	invites: Invite[];
 
 	@JoinColumn({ name: "voice_state_ids" })
-	@OneToMany(() => VoiceState, (voicestate: VoiceState) => voicestate.guild)
+	@OneToMany(() => VoiceState, (voicestate: VoiceState) => voicestate.guild, {
+		cascade: true,
+		orphanedRowAction: "delete",
+		onDelete: "CASCADE",
+	})
 	voice_states: VoiceState[];
 
 	@JoinColumn({ name: "webhook_ids" })
-	@OneToMany(() => Webhook, (webhook: Webhook) => webhook.guild)
+	@OneToMany(() => Webhook, (webhook: Webhook) => webhook.guild, {
+		cascade: true,
+		orphanedRowAction: "delete",
+		onDelete: "CASCADE",
+	})
 	webhooks: Webhook[];
 
 	@Column({ nullable: true })
diff --git a/util/src/entities/Invite.ts b/util/src/entities/Invite.ts
index afad9c02..78545b02 100644
--- a/util/src/entities/Invite.ts
+++ b/util/src/entities/Invite.ts
@@ -1,4 +1,5 @@
 import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, RelationId } from "typeorm";
+import { Member } from ".";
 import { BaseClass } from "./BaseClass";
 import { Channel } from "./Channel";
 import { Guild } from "./Guild";
@@ -34,7 +35,9 @@ export class Invite extends BaseClass {
 	guild_id: string;
 
 	@JoinColumn({ name: "guild_id" })
-	@ManyToOne(() => Guild)
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
 	guild: Guild;
 
 	@Column({ nullable: true })
@@ -42,7 +45,9 @@ export class Invite extends BaseClass {
 	channel_id: string;
 
 	@JoinColumn({ name: "channel_id" })
-	@ManyToOne(() => Channel)
+	@ManyToOne(() => Channel, {
+		onDelete: "CASCADE",
+	})
 	channel: Channel;
 
 	@Column({ nullable: true })
@@ -58,9 +63,20 @@ export class Invite extends BaseClass {
 	target_user_id: string;
 
 	@JoinColumn({ name: "target_user_id" })
-	@ManyToOne(() => User)
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
 	target_user?: string; // could be used for "User specific invites" https://github.com/fosscord/fosscord/issues/62
 
 	@Column({ nullable: true })
 	target_user_type?: number;
+
+	static async joinGuild(user_id: string, code: string) {
+		const invite = await Invite.findOneOrFail({ 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);
+		return invite;
+	}
 }
diff --git a/util/src/entities/Member.ts b/util/src/entities/Member.ts
index 66f5d9a1..feb9c069 100644
--- a/util/src/entities/Member.ts
+++ b/util/src/entities/Member.ts
@@ -39,7 +39,9 @@ export class Member extends BaseClassWithoutId {
 	id: string;
 
 	@JoinColumn({ name: "id" })
-	@ManyToOne(() => User)
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
 	user: User;
 
 	@Column()
@@ -47,7 +49,9 @@ export class Member extends BaseClassWithoutId {
 	guild_id: string;
 
 	@JoinColumn({ name: "guild_id" })
-	@ManyToOne(() => Guild)
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
 	guild: Guild;
 
 	@Column({ nullable: true })
@@ -55,7 +59,6 @@ export class Member extends BaseClassWithoutId {
 
 	@JoinTable({
 		name: "member_roles",
-
 		joinColumn: { name: "index", referencedColumnName: "index" },
 		inverseJoinColumn: {
 			name: "role_id",
diff --git a/util/src/entities/Message.ts b/util/src/entities/Message.ts
index 506db71a..c4901693 100644
--- a/util/src/entities/Message.ts
+++ b/util/src/entities/Message.ts
@@ -8,12 +8,14 @@ import {
 	Column,
 	CreateDateColumn,
 	Entity,
+	FindConditions,
 	JoinColumn,
 	JoinTable,
 	ManyToMany,
 	ManyToOne,
 	OneToMany,
 	RelationId,
+	RemoveOptions,
 	UpdateDateColumn,
 } from "typeorm";
 import { BaseClass } from "./BaseClass";
@@ -52,7 +54,9 @@ export class Message extends BaseClass {
 	channel_id: string;
 
 	@JoinColumn({ name: "channel_id" })
-	@ManyToOne(() => Channel)
+	@ManyToOne(() => Channel, {
+		onDelete: "CASCADE",
+	})
 	channel: Channel;
 
 	@Column({ nullable: true })
@@ -60,7 +64,9 @@ export class Message extends BaseClass {
 	guild_id?: string;
 
 	@JoinColumn({ name: "guild_id" })
-	@ManyToOne(() => Guild)
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
 	guild?: Guild;
 
 	@Column({ nullable: true })
@@ -68,7 +74,9 @@ export class Message extends BaseClass {
 	author_id: string;
 
 	@JoinColumn({ name: "author_id", referencedColumnName: "id" })
-	@ManyToOne(() => User)
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
 	author?: User;
 
 	@Column({ nullable: true })
@@ -112,7 +120,7 @@ export class Message extends BaseClass {
 	mention_everyone?: boolean;
 
 	@JoinTable({ name: "message_user_mentions" })
-	@ManyToMany(() => User)
+	@ManyToMany(() => User, { orphanedRowAction: "delete", onDelete: "CASCADE", cascade: true })
 	mentions: User[];
 
 	@JoinTable({ name: "message_role_mentions" })
@@ -127,8 +135,10 @@ export class Message extends BaseClass {
 	@ManyToMany(() => Sticker)
 	sticker_items?: Sticker[];
 
-	@JoinColumn({ name: "attachment_ids" })
-	@OneToMany(() => Attachment, (attachment: Attachment) => attachment.message, { cascade: true })
+	@OneToMany(() => Attachment, (attachment: Attachment) => attachment.message, {
+		cascade: true,
+		orphanedRowAction: "delete",
+	})
 	attachments?: Attachment[];
 
 	@Column({ type: "simple-json" })
diff --git a/util/src/entities/ReadState.ts b/util/src/entities/ReadState.ts
index 8dd05b21..68e867a0 100644
--- a/util/src/entities/ReadState.ts
+++ b/util/src/entities/ReadState.ts
@@ -15,7 +15,9 @@ export class ReadState extends BaseClass {
 	channel_id: string;
 
 	@JoinColumn({ name: "channel_id" })
-	@ManyToOne(() => Channel)
+	@ManyToOne(() => Channel, {
+		onDelete: "CASCADE",
+	})
 	channel: Channel;
 
 	@Column({ nullable: true })
@@ -23,7 +25,9 @@ export class ReadState extends BaseClass {
 	user_id: string;
 
 	@JoinColumn({ name: "user_id" })
-	@ManyToOne(() => User)
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
 	user: User;
 
 	@Column({ nullable: true })
diff --git a/util/src/entities/Recipient.ts b/util/src/entities/Recipient.ts
index 2a27b29f..a945f938 100644
--- a/util/src/entities/Recipient.ts
+++ b/util/src/entities/Recipient.ts
@@ -8,7 +8,9 @@ export class Recipient extends BaseClass {
 	channel_id: string;
 
 	@JoinColumn({ name: "channel_id" })
-	@ManyToOne(() => require("./Channel").Channel)
+	@ManyToOne(() => require("./Channel").Channel, {
+		onDelete: "CASCADE",
+	})
 	channel: import("./Channel").Channel;
 
 	@Column()
@@ -16,8 +18,13 @@ export class Recipient extends BaseClass {
 	user_id: string;
 
 	@JoinColumn({ name: "user_id" })
-	@ManyToOne(() => require("./User").User)
+	@ManyToOne(() => require("./User").User, {
+		onDelete: "CASCADE",
+	})
 	user: import("./User").User;
 
+	@Column({ default: false })
+	closed: boolean;
+
 	// TODO: settings/mute/nick/added at/encryption keys/read_state
 }
diff --git a/util/src/entities/Relationship.ts b/util/src/entities/Relationship.ts
index 61b3ac82..e016b36b 100644
--- a/util/src/entities/Relationship.ts
+++ b/util/src/entities/Relationship.ts
@@ -17,7 +17,9 @@ export class Relationship extends BaseClass {
 	from_id: string;
 
 	@JoinColumn({ name: "from_id" })
-	@ManyToOne(() => User)
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
 	from: User;
 
 	@Column({})
@@ -25,7 +27,9 @@ export class Relationship extends BaseClass {
 	to_id: string;
 
 	@JoinColumn({ name: "to_id" })
-	@ManyToOne(() => User)
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
 	to: User;
 
 	@Column({ nullable: true })
diff --git a/util/src/entities/Role.ts b/util/src/entities/Role.ts
index 33c8d272..9fca99a5 100644
--- a/util/src/entities/Role.ts
+++ b/util/src/entities/Role.ts
@@ -10,7 +10,9 @@ export class Role extends BaseClass {
 	guild_id: string;
 
 	@JoinColumn({ name: "guild_id" })
-	@ManyToOne(() => Guild)
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
 	guild: Guild;
 
 	@Column()
diff --git a/util/src/entities/Session.ts b/util/src/entities/Session.ts
index d42a8f98..7cc325f5 100644
--- a/util/src/entities/Session.ts
+++ b/util/src/entities/Session.ts
@@ -11,7 +11,9 @@ export class Session extends BaseClass {
 	user_id: string;
 
 	@JoinColumn({ name: "user_id" })
-	@ManyToOne(() => User)
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
 	user: User;
 
 	//TODO check, should be 32 char long hex string
diff --git a/util/src/entities/Sticker.ts b/util/src/entities/Sticker.ts
index 7730a86a..ab224d1d 100644
--- a/util/src/entities/Sticker.ts
+++ b/util/src/entities/Sticker.ts
@@ -31,7 +31,9 @@ export class Sticker extends BaseClass {
 	guild_id?: string;
 
 	@JoinColumn({ name: "guild_id" })
-	@ManyToOne(() => Guild)
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
 	guild?: Guild;
 
 	@Column({ type: "simple-enum", enum: StickerType })
diff --git a/util/src/entities/Team.ts b/util/src/entities/Team.ts
index beb8bf68..22140b7f 100644
--- a/util/src/entities/Team.ts
+++ b/util/src/entities/Team.ts
@@ -9,7 +9,9 @@ export class Team extends BaseClass {
 	icon?: string;
 
 	@JoinColumn({ name: "member_ids" })
-	@OneToMany(() => TeamMember, (member: TeamMember) => member.team)
+	@OneToMany(() => TeamMember, (member: TeamMember) => member.team, {
+		orphanedRowAction: "delete",
+	})
 	members: TeamMember[];
 
 	@Column()
diff --git a/util/src/entities/TeamMember.ts b/util/src/entities/TeamMember.ts
index 6b184d08..bdfdccf0 100644
--- a/util/src/entities/TeamMember.ts
+++ b/util/src/entities/TeamMember.ts
@@ -20,7 +20,9 @@ export class TeamMember extends BaseClass {
 	team_id: string;
 
 	@JoinColumn({ name: "team_id" })
-	@ManyToOne(() => require("./Team").Team, (team: import("./Team").Team) => team.members)
+	@ManyToOne(() => require("./Team").Team, (team: import("./Team").Team) => team.members, {
+		onDelete: "CASCADE",
+	})
 	team: import("./Team").Team;
 
 	@Column({ nullable: true })
@@ -28,6 +30,8 @@ export class TeamMember extends BaseClass {
 	user_id: string;
 
 	@JoinColumn({ name: "user_id" })
-	@ManyToOne(() => User)
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
 	user: User;
 }
diff --git a/util/src/entities/User.ts b/util/src/entities/User.ts
index 736704f8..4c86b2d8 100644
--- a/util/src/entities/User.ts
+++ b/util/src/entities/User.ts
@@ -124,14 +124,20 @@ export class User extends BaseClass {
 	flags: string; // UserFlags
 
 	@Column()
-	public_flags: string;
+	public_flags: number;
 
 	@JoinColumn({ name: "relationship_ids" })
-	@OneToMany(() => Relationship, (relationship: Relationship) => relationship.from)
+	@OneToMany(() => Relationship, (relationship: Relationship) => relationship.from, {
+		cascade: true,
+		orphanedRowAction: "delete",
+	})
 	relationships: Relationship[];
 
 	@JoinColumn({ name: "connected_account_ids" })
-	@OneToMany(() => ConnectedAccount, (account: ConnectedAccount) => account.user)
+	@OneToMany(() => ConnectedAccount, (account: ConnectedAccount) => account.user, {
+		cascade: true,
+		orphanedRowAction: "delete",
+	})
 	connected_accounts: ConnectedAccount[];
 
 	@Column({ type: "simple-json", select: false })
diff --git a/util/src/entities/VoiceState.ts b/util/src/entities/VoiceState.ts
index 56eb244e..75748a01 100644
--- a/util/src/entities/VoiceState.ts
+++ b/util/src/entities/VoiceState.ts
@@ -13,7 +13,9 @@ export class VoiceState extends BaseClass {
 	guild_id: string;
 
 	@JoinColumn({ name: "guild_id" })
-	@ManyToOne(() => Guild)
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
 	guild?: Guild;
 
 	@Column({ nullable: true })
@@ -21,7 +23,9 @@ export class VoiceState extends BaseClass {
 	channel_id: string;
 
 	@JoinColumn({ name: "channel_id" })
-	@ManyToOne(() => Channel)
+	@ManyToOne(() => Channel, {
+		onDelete: "CASCADE",
+	})
 	channel: Channel;
 
 	@Column({ nullable: true })
@@ -29,11 +33,15 @@ export class VoiceState extends BaseClass {
 	user_id: string;
 
 	@JoinColumn({ name: "user_id" })
-	@ManyToOne(() => User)
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
 	user: User;
 
 	// @JoinColumn([{ name: "user_id", referencedColumnName: "id" },{ name: "guild_id", referencedColumnName: "guild_id" }])
-	// @ManyToOne(() => Member)
+	// @ManyToOne(() => Member, {
+	// 	onDelete: "CASCADE",
+	// })
 	//TODO find a way to make it work without breaking Guild.voice_states
 	member: Member;
 
diff --git a/util/src/entities/Webhook.ts b/util/src/entities/Webhook.ts
index 12ba0d08..8382435f 100644
--- a/util/src/entities/Webhook.ts
+++ b/util/src/entities/Webhook.ts
@@ -32,7 +32,9 @@ export class Webhook extends BaseClass {
 	guild_id: string;
 
 	@JoinColumn({ name: "guild_id" })
-	@ManyToOne(() => Guild)
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
 	guild: Guild;
 
 	@Column({ nullable: true })
@@ -40,7 +42,9 @@ export class Webhook extends BaseClass {
 	channel_id: string;
 
 	@JoinColumn({ name: "channel_id" })
-	@ManyToOne(() => Channel)
+	@ManyToOne(() => Channel, {
+		onDelete: "CASCADE",
+	})
 	channel: Channel;
 
 	@Column({ nullable: true })
@@ -48,7 +52,9 @@ export class Webhook extends BaseClass {
 	application_id: string;
 
 	@JoinColumn({ name: "application_id" })
-	@ManyToOne(() => Application)
+	@ManyToOne(() => Application, {
+		onDelete: "CASCADE",
+	})
 	application: Application;
 
 	@Column({ nullable: true })
@@ -56,7 +62,9 @@ export class Webhook extends BaseClass {
 	user_id: string;
 
 	@JoinColumn({ name: "user_id" })
-	@ManyToOne(() => User)
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
 	user: User;
 
 	@Column({ nullable: true })
@@ -64,6 +72,8 @@ export class Webhook extends BaseClass {
 	source_guild_id: string;
 
 	@JoinColumn({ name: "source_guild_id" })
-	@ManyToOne(() => Guild)
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
 	source_guild: Guild;
 }
diff --git a/util/src/index.ts b/util/src/index.ts
index f3bd9e9b..fc00d46b 100644
--- a/util/src/index.ts
+++ b/util/src/index.ts
@@ -4,6 +4,7 @@ import "reflect-metadata";
 export * from "./util/index";
 export * from "./interfaces/index";
 export * from "./entities/index";
+export * from "./dtos/index";
 
 // import Config from "../util/Config";
 // import db, { MongooseCache, toObject } from "./util/Database";
diff --git a/util/src/interfaces/Event.ts b/util/src/interfaces/Event.ts
index aff50300..03099bbb 100644
--- a/util/src/interfaces/Event.ts
+++ b/util/src/interfaces/Event.ts
@@ -127,6 +127,22 @@ export interface ChannelPinsUpdateEvent extends Event {
 	};
 }
 
+export interface ChannelRecipientAddEvent extends Event {
+	event: "CHANNEL_RECIPIENT_ADD";
+	data: {
+		channel_id: string;
+		user: User;
+	};
+}
+
+export interface ChannelRecipientRemoveEvent extends Event {
+	event: "CHANNEL_RECIPIENT_REMOVE";
+	data: {
+		channel_id: string;
+		user: User;
+	};
+}
+
 export interface GuildCreateEvent extends Event {
 	event: "GUILD_CREATE";
 	data: Guild & {
@@ -436,6 +452,8 @@ export type EventData =
 	| ChannelUpdateEvent
 	| ChannelDeleteEvent
 	| ChannelPinsUpdateEvent
+	| ChannelRecipientAddEvent
+	| ChannelRecipientRemoveEvent
 	| GuildCreateEvent
 	| GuildUpdateEvent
 	| GuildDeleteEvent
@@ -482,6 +500,8 @@ export enum EVENTEnum {
 	ChannelUpdate = "CHANNEL_UPDATE",
 	ChannelDelete = "CHANNEL_DELETE",
 	ChannelPinsUpdate = "CHANNEL_PINS_UPDATE",
+	ChannelRecipientAdd = "CHANNEL_RECIPIENT_ADD",
+	ChannelRecipientRemove = "CHANNEL_RECIPIENT_REMOVE",
 	GuildCreate = "GUILD_CREATE",
 	GuildUpdate = "GUILD_UPDATE",
 	GuildDelete = "GUILD_DELETE",
@@ -525,6 +545,8 @@ export type EVENT =
 	| "CHANNEL_UPDATE"
 	| "CHANNEL_DELETE"
 	| "CHANNEL_PINS_UPDATE"
+	| "CHANNEL_RECIPIENT_ADD"
+	| "CHANNEL_RECIPIENT_REMOVE"
 	| "GUILD_CREATE"
 	| "GUILD_UPDATE"
 	| "GUILD_DELETE"
diff --git a/util/src/util/Array.ts b/util/src/util/Array.ts
new file mode 100644
index 00000000..27f7c961
--- /dev/null
+++ b/util/src/util/Array.ts
@@ -0,0 +1,3 @@
+export function containsAll(arr: any[], target: any[]) {
+	return target.every(v => arr.includes(v));
+}
\ No newline at end of file
diff --git a/util/src/util/Database.ts b/util/src/util/Database.ts
index d3844cd9..c22d8abd 100644
--- a/util/src/util/Database.ts
+++ b/util/src/util/Database.ts
@@ -21,7 +21,7 @@ export function initDatabase() {
 		//
 		entities: Object.values(Models).filter((x) => x.constructor.name !== "Object"),
 		synchronize: true,
-		logging: false,
+		logging: true,
 		cache: {
 			duration: 1000 * 3, // cache all find queries for 3 seconds
 		},
diff --git a/util/src/util/Permissions.ts b/util/src/util/Permissions.ts
index 9d87253a..44852f1e 100644
--- a/util/src/util/Permissions.ts
+++ b/util/src/util/Permissions.ts
@@ -92,6 +92,7 @@ export class Permissions extends BitField {
 	}
 
 	overwriteChannel(overwrites: ChannelPermissionOverwrite[]) {
+		if (!overwrites) return this
 		if (!this.cache) throw new Error("permission chache not available");
 		overwrites = overwrites.filter((x) => {
 			if (x.type === 0 && this.cache.roles?.some((r) => r.id === x.id)) return true;
diff --git a/util/src/util/cdn.ts b/util/src/util/cdn.ts
new file mode 100644
index 00000000..754d6244
--- /dev/null
+++ b/util/src/util/cdn.ts
@@ -0,0 +1,54 @@
+import FormData from "form-data";
+import { HTTPError } from "lambert-server";
+import fetch from "node-fetch";
+import { Config } from "./Config";
+import multer from "multer";
+
+export async function uploadFile(path: string, file: Express.Multer.File) {
+	const form = new FormData();
+	form.append("file", file.buffer, {
+		contentType: file.mimetype,
+		filename: file.originalname,
+	});
+
+	const response = await fetch(`${Config.get().cdn.endpoint || "http://localhost:3003"}${path}`, {
+		headers: {
+			signature: Config.get().security.requestSignature,
+			...form.getHeaders(),
+		},
+		method: "POST",
+		body: form,
+	});
+	const result = await response.json();
+
+	if (response.status !== 200) throw result;
+	return result;
+}
+
+export async function handleFile(path: string, body?: string): Promise<string | undefined> {
+	if (!body || !body.startsWith("data:")) return body;
+	try {
+		const mimetype = body.split(":")[1].split(";")[0];
+		const buffer = Buffer.from(body.split(",")[1], "base64");
+
+		// @ts-ignore
+		const { id } = await uploadFile(path, { buffer, mimetype, originalname: "banner" });
+		return id;
+	} catch (error) {
+		console.error(error);
+		throw new HTTPError("Invalid " + path);
+	}
+}
+
+export async function deleteFile(path: string) {
+	const response = await fetch(`${Config.get().cdn.endpoint || "http://localhost:3003"}${path}`, {
+		headers: {
+			signature: Config.get().security.requestSignature,
+		},
+		method: "DELETE",
+	});
+	const result = await response.json();
+
+	if (response.status !== 200) throw result;
+	return result;
+}
diff --git a/util/src/util/index.ts b/util/src/util/index.ts
index 4e92f017..3160380f 100644
--- a/util/src/util/index.ts
+++ b/util/src/util/index.ts
@@ -1,6 +1,7 @@
 export * from "./ApiError";
 export * from "./BitField";
 export * from "./checkToken";
+export * from "./cdn";
 export * from "./Config";
 export * from "./Constants";
 export * from "./Database";
@@ -12,3 +13,4 @@ export * from "./RabbitMQ";
 export * from "./Regex";
 export * from "./Snowflake";
 export * from "./String";
+export * from "./Array";