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/Application.ts156
-rw-r--r--src/util/entities/Attachment.ts43
-rw-r--r--src/util/entities/AuditLog.ts194
-rw-r--r--src/util/entities/BackupCodes.ts19
-rw-r--r--src/util/entities/Ban.ts41
-rw-r--r--src/util/entities/BaseClass.ts26
-rw-r--r--src/util/entities/Categories.ts33
-rw-r--r--src/util/entities/Channel.ts391
-rw-r--r--src/util/entities/ClientRelease.ts26
-rw-r--r--src/util/entities/Config.ts11
-rw-r--r--src/util/entities/ConnectedAccount.ts42
-rw-r--r--src/util/entities/Emoji.ts46
-rw-r--r--src/util/entities/Encryption.ts35
-rw-r--r--src/util/entities/Group.ts33
-rw-r--r--src/util/entities/Guild.ts370
-rw-r--r--src/util/entities/Invite.ts88
-rw-r--r--src/util/entities/Member.ts360
-rw-r--r--src/util/entities/Message.ts284
-rw-r--r--src/util/entities/Migration.ts18
-rw-r--r--src/util/entities/Note.ts18
-rw-r--r--src/util/entities/RateLimit.ts17
-rw-r--r--src/util/entities/ReadState.ts55
-rw-r--r--src/util/entities/Recipient.ts30
-rw-r--r--src/util/entities/Relationship.ts49
-rw-r--r--src/util/entities/Role.ts51
-rw-r--r--src/util/entities/Session.ts46
-rw-r--r--src/util/entities/Sticker.ts66
-rw-r--r--src/util/entities/StickerPack.ts31
-rw-r--r--src/util/entities/Team.ts27
-rw-r--r--src/util/entities/TeamMember.ts37
-rw-r--r--src/util/entities/Template.ts44
-rw-r--r--src/util/entities/User.ts324
-rw-r--r--src/util/entities/UserGroup.ts37
-rw-r--r--src/util/entities/UserSettings.ts119
-rw-r--r--src/util/entities/VoiceState.ts77
-rw-r--r--src/util/entities/Webhook.ts76
-rw-r--r--src/util/entities/index.ts33
37 files changed, 3353 insertions, 0 deletions
diff --git a/src/util/entities/Application.ts b/src/util/entities/Application.ts
new file mode 100644
index 00000000..103f8e84
--- /dev/null
+++ b/src/util/entities/Application.ts
@@ -0,0 +1,156 @@
+import { Column, Entity, JoinColumn, ManyToOne, OneToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { Guild } from "./Guild";
+import { Team } from "./Team";
+import { User } from "./User";
+
+@Entity("applications")
+export class Application extends BaseClass {
+	@Column()
+	name: string;
+	
+	@Column({ nullable: true })
+	icon?: string;
+	
+	@Column({ nullable: true })
+	description: string;
+	
+	@Column({ nullable: true })
+	summary: string = "";
+	
+	@Column({ type: "simple-json", nullable: true })
+	type?: any;
+	
+	@Column()
+	hook: boolean = true;
+	
+	@Column()
+	bot_public?: boolean = true;
+	
+	@Column()
+	bot_require_code_grant?: boolean = false;
+	
+	@Column()
+	verify_key: string;
+	
+	@JoinColumn({ name: "owner_id" })
+	@ManyToOne(() => User)
+	owner: User;
+	
+	@Column()
+	flags: number = 0;
+	
+	@Column({ type: "simple-array", nullable: true })
+	redirect_uris: string[] = [];
+	
+	@Column({ nullable: true })
+	rpc_application_state: number = 0;
+	
+	@Column({ nullable: true })
+	store_application_state: number = 1;
+	
+	@Column({ nullable: true })
+	verification_state: number = 1;
+	
+	@Column({ nullable: true })
+	interactions_endpoint_url?: string;
+	
+	@Column({ nullable: true })
+	integration_public: boolean = true;
+	
+	@Column({ nullable: true })
+	integration_require_code_grant: boolean = false;
+	
+	@Column({ nullable: true })
+	discoverability_state: number = 1;
+	
+	@Column({ nullable: true })
+	discovery_eligibility_flags: number = 2240;
+	
+	@JoinColumn({ name: "bot_user_id" })
+	@OneToOne(() => User)
+	bot?: User;
+	
+	@Column({ type: "simple-array", nullable: true })
+	tags?: string[];
+	
+	@Column({ nullable: true })
+	cover_image?: string; // the application's default rich presence invite cover image hash
+	
+	@Column({ type: "simple-json", nullable: true })
+	install_params?: {scopes: string[], permissions: string};
+
+	@Column({ nullable: true })
+	terms_of_service_url?: string;
+
+	@Column({ nullable: true })
+	privacy_policy_url?: string;
+
+	//just for us
+
+	//@Column({ type: "simple-array", nullable: true })
+	//rpc_origins?: string[];
+	
+	//@JoinColumn({ name: "guild_id" })
+	//@ManyToOne(() => Guild)
+	//guild?: Guild; // if this application is a game sold, this field will be the guild to which it has been linked
+
+	//@Column({ nullable: true })
+	//primary_sku_id?: string; // if this application is a game sold, this field will be the id of the "Game SKU" that is created,
+
+	//@Column({ nullable: true })
+	//slug?: string; // if this application is a game sold, this field will be the URL slug that links to the store page
+
+	@JoinColumn({ name: "team_id" })
+	@ManyToOne(() => Team, {
+		onDelete: "CASCADE",
+		nullable: true
+	})
+	team?: Team;
+
+  }
+
+export interface ApplicationCommand {
+	id: string;
+	application_id: string;
+	name: string;
+	description: string;
+	options?: ApplicationCommandOption[];
+}
+
+export interface ApplicationCommandOption {
+	type: ApplicationCommandOptionType;
+	name: string;
+	description: string;
+	required?: boolean;
+	choices?: ApplicationCommandOptionChoice[];
+	options?: ApplicationCommandOption[];
+}
+
+export interface ApplicationCommandOptionChoice {
+	name: string;
+	value: string | number;
+}
+
+export enum ApplicationCommandOptionType {
+	SUB_COMMAND = 1,
+	SUB_COMMAND_GROUP = 2,
+	STRING = 3,
+	INTEGER = 4,
+	BOOLEAN = 5,
+	USER = 6,
+	CHANNEL = 7,
+	ROLE = 8,
+}
+
+export interface ApplicationCommandInteractionData {
+	id: string;
+	name: string;
+	options?: ApplicationCommandInteractionDataOption[];
+}
+
+export interface ApplicationCommandInteractionDataOption {
+	name: string;
+	value?: any;
+	options?: ApplicationCommandInteractionDataOption[];
+}
diff --git a/src/util/entities/Attachment.ts b/src/util/entities/Attachment.ts
new file mode 100644
index 00000000..7b4b17eb
--- /dev/null
+++ b/src/util/entities/Attachment.ts
@@ -0,0 +1,43 @@
+import { BeforeRemove, Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { URL } from "url";
+import { deleteFile } from "../util/cdn";
+import { BaseClass } from "./BaseClass";
+
+@Entity("attachments")
+export class Attachment extends BaseClass {
+	@Column()
+	filename: string; // name of file attached
+
+	@Column()
+	size: number; // size of file in bytes
+
+	@Column()
+	url: string; // source url of file
+
+	@Column()
+	proxy_url: string; // a proxied url of file
+
+	@Column({ nullable: true })
+	height?: number; // height of file (if image)
+
+	@Column({ nullable: true })
+	width?: number; // width of file (if image)
+
+	@Column({ nullable: true })
+	content_type?: string;
+
+	@Column({ nullable: true })
+	@RelationId((attachment: Attachment) => attachment.message)
+	message_id: string;
+
+	@JoinColumn({ name: "message_id" })
+	@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/src/util/entities/AuditLog.ts b/src/util/entities/AuditLog.ts
new file mode 100644
index 00000000..b003e7ba
--- /dev/null
+++ b/src/util/entities/AuditLog.ts
@@ -0,0 +1,194 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { ChannelPermissionOverwrite } from "./Channel";
+import { User } from "./User";
+
+export enum AuditLogEvents {
+	// guild level
+	GUILD_UPDATE = 1, 
+	GUILD_IMPORT = 2,
+	GUILD_EXPORTED = 3,
+	GUILD_ARCHIVE = 4,
+	GUILD_UNARCHIVE = 5,
+	// join-leave
+	USER_JOIN = 6, 
+	USER_LEAVE = 7,
+	// channels
+	CHANNEL_CREATE = 10, 
+	CHANNEL_UPDATE = 11,
+	CHANNEL_DELETE = 12,
+	// permission overrides
+	CHANNEL_OVERWRITE_CREATE = 13, 
+	CHANNEL_OVERWRITE_UPDATE = 14,
+	CHANNEL_OVERWRITE_DELETE = 15,
+	// kick and ban
+	MEMBER_KICK = 20, 
+	MEMBER_PRUNE = 21,
+	MEMBER_BAN_ADD = 22,
+	MEMBER_BAN_REMOVE = 23,
+	// member updates
+	MEMBER_UPDATE = 24,
+	MEMBER_ROLE_UPDATE = 25,
+	MEMBER_MOVE = 26,
+	MEMBER_DISCONNECT = 27,
+	BOT_ADD = 28,
+	// roles
+	ROLE_CREATE = 30,
+	ROLE_UPDATE = 31,
+	ROLE_DELETE = 32,
+	ROLE_SWAP = 33,
+	// invites
+	INVITE_CREATE = 40,
+	INVITE_UPDATE = 41,
+	INVITE_DELETE = 42,
+	// webhooks
+	WEBHOOK_CREATE = 50,
+	WEBHOOK_UPDATE = 51,
+	WEBHOOK_DELETE = 52,
+	WEBHOOK_SWAP = 53,
+	// custom emojis
+	EMOJI_CREATE = 60,
+	EMOJI_UPDATE = 61,
+	EMOJI_DELETE = 62,
+	EMOJI_SWAP = 63,
+	// deletion
+	MESSAGE_CREATE = 70, // messages sent using non-primary seat of the user only
+	MESSAGE_EDIT = 71, // non-self edits only
+	MESSAGE_DELETE = 72,
+	MESSAGE_BULK_DELETE = 73,
+	// pinning
+	MESSAGE_PIN = 74,
+	MESSAGE_UNPIN = 75,
+	// integrations
+	INTEGRATION_CREATE = 80,
+	INTEGRATION_UPDATE = 81,
+	INTEGRATION_DELETE = 82,
+	// stage actions
+	STAGE_INSTANCE_CREATE = 83,
+	STAGE_INSTANCE_UPDATE = 84,
+	STAGE_INSTANCE_DELETE = 85,
+	// stickers
+	STICKER_CREATE = 90,
+	STICKER_UPDATE = 91,
+	STICKER_DELETE = 92,
+	STICKER_SWAP = 93,
+	// threads
+	THREAD_CREATE = 110,
+	THREAD_UPDATE = 111,
+	THREAD_DELETE = 112,
+	// application commands
+	APPLICATION_COMMAND_PERMISSION_UPDATE = 121,
+	// automod
+	POLICY_CREATE = 140, 
+	POLICY_UPDATE = 141,
+	POLICY_DELETE = 142,
+	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_UPDATE = 226,
+}
+
+@Entity("audit_logs")
+export class AuditLog extends BaseClass {
+	@JoinColumn({ name: "target_id" })
+	@ManyToOne(() => User)
+	target?: User;
+
+	@Column({ nullable: true })
+	@RelationId((auditlog: AuditLog) => auditlog.user)
+	user_id: string;
+
+	@JoinColumn({ name: "user_id" })
+	@ManyToOne(() => User, (user: User) => user.id)
+	user: User;
+
+	@Column({ type: "int" })
+	action_type: AuditLogEvents;
+
+	@Column({ type: "simple-json", nullable: true })
+	options?: {
+		delete_member_days?: string;
+		members_removed?: string;
+		channel_id?: string;
+		messaged_id?: string;
+		count?: string;
+		id?: string;
+		type?: string;
+		role_name?: string;
+	};
+
+	@Column()
+	@Column({ type: "simple-json" })
+	changes: AuditLogChange[];
+
+	@Column({ nullable: true })
+	reason?: string;
+}
+
+export interface AuditLogChange {
+	new_value?: AuditLogChangeValue;
+	old_value?: AuditLogChangeValue;
+	key: string;
+}
+
+export interface AuditLogChangeValue {
+	name?: string;
+	description?: string;
+	icon_hash?: string;
+	splash_hash?: string;
+	discovery_splash_hash?: string;
+	banner_hash?: string;
+	owner_id?: string;
+	region?: string;
+	preferred_locale?: string;
+	afk_channel_id?: string;
+	afk_timeout?: number;
+	rules_channel_id?: string;
+	public_updates_channel_id?: string;
+	mfa_level?: number;
+	verification_level?: number;
+	explicit_content_filter?: number;
+	default_message_notifications?: number;
+	vanity_url_code?: string;
+	$add?: {}[];
+	$remove?: {}[];
+	prune_delete_days?: number;
+	widget_enabled?: boolean;
+	widget_channel_id?: string;
+	system_channel_id?: string;
+	position?: number;
+	topic?: string;
+	bitrate?: number;
+	permission_overwrites?: ChannelPermissionOverwrite[];
+	nsfw?: boolean;
+	application_id?: string;
+	rate_limit_per_user?: number;
+	permissions?: string;
+	color?: number;
+	hoist?: boolean;
+	mentionable?: boolean;
+	allow?: string;
+	deny?: string;
+	code?: string;
+	channel_id?: string;
+	inviter_id?: string;
+	max_uses?: number;
+	uses?: number;
+	max_age?: number;
+	temporary?: boolean;
+	deaf?: boolean;
+	mute?: boolean;
+	nick?: string;
+	avatar_hash?: string;
+	id?: string;
+	type?: number;
+	enable_emoticons?: boolean;
+	expire_behavior?: number;
+	expire_grace_period?: number;
+	user_limit?: number;
+}
diff --git a/src/util/entities/BackupCodes.ts b/src/util/entities/BackupCodes.ts
new file mode 100644
index 00000000..9092c14e
--- /dev/null
+++ b/src/util/entities/BackupCodes.ts
@@ -0,0 +1,19 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { User } from "./User";
+
+@Entity("backup_codes")
+export class BackupCode extends BaseClass {
+	@JoinColumn({ name: "user_id" })
+	@ManyToOne(() => User, { onDelete: "CASCADE" })
+	user: User;
+
+	@Column()
+	code: string;
+
+	@Column()
+	consumed: boolean;
+
+	@Column()
+	expired: boolean;
+}
\ No newline at end of file
diff --git a/src/util/entities/Ban.ts b/src/util/entities/Ban.ts
new file mode 100644
index 00000000..9504bd8e
--- /dev/null
+++ b/src/util/entities/Ban.ts
@@ -0,0 +1,41 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { Guild } from "./Guild";
+import { User } from "./User";
+
+@Entity("bans")
+export class Ban extends BaseClass {
+	@Column({ nullable: true })
+	@RelationId((ban: Ban) => ban.user)
+	user_id: string;
+
+	@JoinColumn({ name: "user_id" })
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
+	user: User;
+
+	@Column({ nullable: true })
+	@RelationId((ban: Ban) => ban.guild)
+	guild_id: string;
+
+	@JoinColumn({ name: "guild_id" })
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
+	guild: Guild;
+
+	@Column({ nullable: true })
+	@RelationId((ban: Ban) => ban.executor)
+	executor_id: string;
+
+	@JoinColumn({ name: "executor_id" })
+	@ManyToOne(() => User)
+	executor: User;
+
+	@Column()
+	ip: string;
+
+	@Column({ nullable: true })
+	reason?: string;
+}
diff --git a/src/util/entities/BaseClass.ts b/src/util/entities/BaseClass.ts
new file mode 100644
index 00000000..aecc2465
--- /dev/null
+++ b/src/util/entities/BaseClass.ts
@@ -0,0 +1,26 @@
+import "reflect-metadata";
+import { BaseEntity, ObjectIdColumn, PrimaryColumn, SaveOptions } from "typeorm";
+import { Snowflake } from "../util/Snowflake";
+
+export class BaseClassWithoutId extends BaseEntity {
+	constructor() {
+		super();
+	}
+}
+
+export const PrimaryIdColumn = process.env.DATABASE?.startsWith("mongodb") ? ObjectIdColumn : PrimaryColumn;
+
+export class BaseClass extends BaseClassWithoutId {
+	@PrimaryIdColumn()
+	id: string;
+
+	constructor() {
+		super();
+		if (!this.id) this.id = Snowflake.generate();
+	}
+
+	save(options?: SaveOptions | undefined): Promise<this> {
+		if (!this.id) this.id = Snowflake.generate();
+		return super.save(options);
+	}
+}
diff --git a/src/util/entities/Categories.ts b/src/util/entities/Categories.ts
new file mode 100644
index 00000000..81fbc303
--- /dev/null
+++ b/src/util/entities/Categories.ts
@@ -0,0 +1,33 @@
+import { PrimaryColumn, Column, Entity} from "typeorm";
+import { BaseClassWithoutId } from "./BaseClass";
+
+// TODO: categories:
+// [{
+// 	"id": 16,
+// 	"default": "Anime & Manga",
+// 	"localizations": {
+// 			"de": "Anime & Manga",
+// 			"fr": "Anim\u00e9s et mangas",
+// 			"ru": "\u0410\u043d\u0438\u043c\u0435 \u0438 \u043c\u0430\u043d\u0433\u0430"
+// 		}
+// 	},
+// 	"is_primary": false/true
+// }]
+// Also populate discord default categories
+
+@Entity("categories")
+export class Categories extends BaseClassWithoutId { // Not using snowflake
+    
+    @PrimaryColumn()
+	id: number;
+
+    @Column({ nullable: true })
+    name: string;
+
+    @Column({ type: "simple-json" })
+    localizations: string;
+
+    @Column({ nullable: true })
+    is_primary: boolean;
+
+}
\ No newline at end of file
diff --git a/src/util/entities/Channel.ts b/src/util/entities/Channel.ts
new file mode 100644
index 00000000..a576d7af
--- /dev/null
+++ b/src/util/entities/Channel.ts
@@ -0,0 +1,391 @@
+import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+

+export enum ChannelType {

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

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

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

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

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

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

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

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

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

+	TRANSACTIONAL = 9, // event chain style transactional channel

+	GUILD_NEWS_THREAD = 10, // a temporary sub-channel within a GUILD_NEWS channel

+	GUILD_PUBLIC_THREAD = 11, // a temporary sub-channel within a GUILD_TEXT channel

+	GUILD_PRIVATE_THREAD = 12, // a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission

+	GUILD_STAGE_VOICE = 13, // a voice channel for hosting events with an audience

+	DIRECTORY = 14, // guild directory listing channel

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

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

+	KANBAN = 34, // confluence like kanban board

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

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

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

+}

+

+@Entity("channels")

+export class Channel extends BaseClass {

+	@Column()

+	created_at: Date;

+

+	@Column({ nullable: true })

+	name?: string;

+

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

+	icon?: string | null;

+

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

+	type: ChannelType;

+

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

+		cascade: true,

+		orphanedRowAction: "delete",

+	})

+	recipients?: Recipient[];

+

+	@Column({ nullable: true })

+	last_message_id: string;

+

+	@Column({ nullable: true })

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

+	guild_id?: string;

+

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

+	@ManyToOne(() => Guild, {

+		onDelete: "CASCADE",

+	})

+	guild: Guild;

+

+	@Column({ nullable: true })

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

+	parent_id: string;

+

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

+	@ManyToOne(() => Channel)

+	parent?: Channel;

+

+	// for group DMs and owned custom channel types

+	@Column({ nullable: true })

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

+	owner_id: string;

+

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

+	@ManyToOne(() => User)

+	owner: User;

+

+	@Column({ nullable: true })

+	last_pin_timestamp?: number;

+

+	@Column({ nullable: true })

+	default_auto_archive_duration?: number;

+

+	@Column({ nullable: true })

+	position?: number;

+

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

+	permission_overwrites?: ChannelPermissionOverwrite[];

+

+	@Column({ nullable: true })

+	video_quality_mode?: number;

+

+	@Column({ nullable: true })

+	bitrate?: number;

+

+	@Column({ nullable: true })

+	user_limit?: number;

+

+	@Column({ nullable: true })

+	nsfw?: boolean;

+

+	@Column({ nullable: true })

+	rate_limit_per_user?: number;

+

+	@Column({ nullable: true })

+	topic?: string;

+

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

+		cascade: true,

+		orphanedRowAction: "delete",

+	})

+	invites?: Invite[];

+

+	@Column({ nullable: true })

+	retention_policy_id?: string;

+

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

+		cascade: true,

+		orphanedRowAction: "delete",

+	})

+	messages?: Message[];

+

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

+		cascade: true,

+		orphanedRowAction: "delete",

+	})

+	voice_states?: VoiceState[];

+

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

+		cascade: true,

+		orphanedRowAction: "delete",

+	})

+	read_states?: ReadState[];

+

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

+		cascade: true,

+		orphanedRowAction: "delete",

+	})

+	webhooks?: Webhook[];

+

+	@Column({ nullable: true })

+	flags?: number = 0;

+	

+	@Column({ nullable: true })

+	default_thread_rate_limit_per_user?: number = 0;

+

+

+	// TODO: DM channel

+	static async createChannel(

+		channel: Partial<Channel>,

+		user_id: string = "0",

+		opts?: {

+			keepId?: boolean;

+			skipExistsCheck?: boolean;

+			skipPermissionCheck?: boolean;

+			skipEventEmit?: boolean;

+			skipNameChecks?: boolean;

+		}

+	) {

+		if (!opts?.skipPermissionCheck) {

+			// Always check if user has permission first

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

+			permissions.hasThrow("MANAGE_CHANNELS");

+		}

+

+		if (!opts?.skipNameChecks) {

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

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

+				for (let character of InvisibleCharacters)

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

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

+

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

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

+

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

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

+			}

+

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

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

+			}

+		}

+

+		switch (channel.type) {

+			case ChannelType.GUILD_TEXT:

+			case ChannelType.GUILD_NEWS:

+			case ChannelType.GUILD_VOICE:

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

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

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

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

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

+				}

+				break;

+			case ChannelType.GUILD_CATEGORY:

+			case ChannelType.UNHANDLED:

+				break;

+			case ChannelType.DM:

+			case ChannelType.GROUP_DM:

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

+			case ChannelType.GUILD_STORE:

+			default:

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

+		}

+

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

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

+

+		channel = {

+			...channel,

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

+			created_at: new Date(),

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

+		};

+

+		await Promise.all([

+			OrmUtils.mergeDeep(new Channel(), channel).save(),

+			!opts?.skipEventEmit

+				? emitEvent({

+						event: "CHANNEL_CREATE",

+						data: channel,

+						guild_id: channel.guild_id,

+				  } as ChannelCreateEvent)

+				: Promise.resolve(),

+		]);

+

+		return channel;

+	}

+

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

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

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

+

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

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

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

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

+		}

+		**/

+

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

+

+		let channel = null;

+

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

+

+		const userRecipients = await Recipient.find({

+			where: { user_id: creator_user_id },

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

+		});

+

+		for (let ur of userRecipients) {

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

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

+				if (containsAll(re, channelRecipients)) {

+					if (channel == null) {

+						channel = ur.channel;

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

+						await ur.save();

+					}

+				}

+			}

+		}

+

+		if (channel == null) {

+			name = trimSpecial(name);

+

+			channel = await (

+				OrmUtils.mergeDeep(new Channel(), {

+					name,

+					type,

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

+					created_at: new Date(),

+					last_message_id: null,

+					recipients: channelRecipients.map((x) =>

+						OrmUtils.mergeDeep(new Recipient(), {

+							user_id: x,

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

+						})

+					),

+				}) as Channel

+			).save();

+		}

+

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

+

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

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

+				await emitEvent({

+					event: "CHANNEL_CREATE",

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

+					user_id: recipient.user_id,

+				});

+			}

+		} else {

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

+		}

+

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

+		else return channel_dto.excludedRecipients([creator_user_id]);

+	}

+

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

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

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

+

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

+			await Channel.deleteChannel(channel);

+			await emitEvent({

+				event: "CHANNEL_DELETE",

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

+				user_id: user_id,

+			});

+			return;

+		}

+

+		await emitEvent({

+			event: "CHANNEL_DELETE",

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

+			user_id: user_id,

+		});

+

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

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

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

+			await emitEvent({

+				event: "CHANNEL_UPDATE",

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

+				channel_id: channel.id,

+			});

+		}

+

+		await channel.save();

+

+		await emitEvent({

+			event: "CHANNEL_RECIPIENT_REMOVE",

+			data: {

+				channel_id: channel.id,

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

+			},

+			channel_id: channel.id,

+		} as ChannelRecipientRemoveEvent);

+	}

+

+	static async deleteChannel(channel: Channel) {

+		await Message.delete({ channel_id: channel.id }); //TODO we should also delete the attachments from the cdn but to do that we need to move cdn.ts in util

+		//TODO before deleting the channel we should check and delete other relations

+		await Channel.delete({ id: channel.id });

+	}

+

+	isDm() {

+		return this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM;

+	}

+

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

+	isWritable() {

+		const disallowedChannelTypes = [

+			ChannelType.GUILD_CATEGORY,

+			ChannelType.GUILD_STAGE_VOICE,

+			ChannelType.VOICELESS_WHITEBOARD,

+		];

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

+	}

+}

+

+export interface ChannelPermissionOverwrite {

+	allow: string;

+	deny: string;

+	id: string;

+	type: ChannelPermissionOverwriteType;

+}

+

+export enum ChannelPermissionOverwriteType {

+	role = 0,

+	member = 1,

+	group = 2,

+}

diff --git a/src/util/entities/ClientRelease.ts b/src/util/entities/ClientRelease.ts
new file mode 100644
index 00000000..c5afd307
--- /dev/null
+++ b/src/util/entities/ClientRelease.ts
@@ -0,0 +1,26 @@
+import { Column, Entity} from "typeorm";
+import { BaseClass } from "./BaseClass";
+
+@Entity("client_release")
+export class Release extends BaseClass {
+	@Column()
+	name: string;
+
+	@Column()
+	pub_date: string;
+
+	@Column()
+	url: string;
+
+	@Column()
+	deb_url: string;
+
+	@Column()
+	osx_url: string;
+
+	@Column()
+	win_url: string;
+
+	@Column({ nullable: true })
+	notes?: string;
+}
diff --git a/src/util/entities/Config.ts b/src/util/entities/Config.ts
new file mode 100644
index 00000000..606fe901
--- /dev/null
+++ b/src/util/entities/Config.ts
@@ -0,0 +1,11 @@
+import { Column, Entity } from "typeorm";
+import { BaseClassWithoutId, PrimaryIdColumn } from "./BaseClass";
+
+@Entity("config")
+export class ConfigEntity extends BaseClassWithoutId {
+	@PrimaryIdColumn()
+	key: string;
+
+	@Column({ type: "simple-json", nullable: true })
+	value: number | boolean | null | string | undefined;
+}
\ No newline at end of file
diff --git a/src/util/entities/ConnectedAccount.ts b/src/util/entities/ConnectedAccount.ts
new file mode 100644
index 00000000..09ae30ab
--- /dev/null
+++ b/src/util/entities/ConnectedAccount.ts
@@ -0,0 +1,42 @@
+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"> {}
+
+@Entity("connected_accounts")
+export class ConnectedAccount extends BaseClass {
+	@Column({ nullable: true })
+	@RelationId((account: ConnectedAccount) => account.user)
+	user_id: string;
+
+	@JoinColumn({ name: "user_id" })
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
+	user: User;
+
+	@Column({ select: false })
+	access_token: string;
+
+	@Column({ select: false })
+	friend_sync: boolean;
+
+	@Column()
+	name: string;
+
+	@Column({ select: false })
+	revoked: boolean;
+
+	@Column({ select: false })
+	show_activity: boolean;
+
+	@Column()
+	type: string;
+
+	@Column()
+	verified: boolean;
+
+	@Column({ select: false })
+	visibility: number;
+}
diff --git a/src/util/entities/Emoji.ts b/src/util/entities/Emoji.ts
new file mode 100644
index 00000000..a3615b7d
--- /dev/null
+++ b/src/util/entities/Emoji.ts
@@ -0,0 +1,46 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { User } from ".";
+import { BaseClass } from "./BaseClass";
+import { Guild } from "./Guild";
+import { Role } from "./Role";
+
+@Entity("emojis")
+export class Emoji extends BaseClass {
+	@Column()
+	animated: boolean;
+
+	@Column()
+	available: boolean; // whether this emoji can be used, may be false due to various reasons
+
+	@Column()
+	guild_id: string;
+
+	@JoinColumn({ name: "guild_id" })
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
+	guild: Guild;
+
+	@Column({ nullable: true })
+	@RelationId((emoji: Emoji) => emoji.user)
+	user_id: string;
+
+	@JoinColumn({ name: "user_id" })
+	@ManyToOne(() => User)
+	user: User;
+
+	@Column()
+	managed: boolean;
+
+	@Column()
+	name: string;
+
+	@Column()
+	require_colons: boolean;
+
+	@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
new file mode 100644
index 00000000..6b578d15
--- /dev/null
+++ b/src/util/entities/Encryption.ts
@@ -0,0 +1,35 @@
+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 "..";
+import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial, InvisibleCharacters } from "../util";
+import { BitField, BitFieldResolvable, BitFlag } from "../util/BitField";
+import { Recipient } from "./Recipient";
+import { Message } from "./Message";
+import { ReadState } from "./ReadState";
+import { Invite } from "./Invite";
+import { DmChannelDTO } from "../dtos";
+
+@Entity("security_settings")
+export class SecuritySettings extends BaseClass {
+
+  @Column({nullable: true})
+  guild_id: Snowflake;
+
+  @Column({nullable: true})
+  channel_id: Snowflake;
+
+  @Column()
+  encryption_permission_mask: BitField;
+
+  @Column()
+  allowed_algorithms: string[];
+
+  @Column()
+  current_algorithm: string;
+
+  @Column({nullable: true})
+  used_since_message: Snowflake;
+
+}
diff --git a/src/util/entities/Group.ts b/src/util/entities/Group.ts
new file mode 100644
index 00000000..b24d38cf
--- /dev/null
+++ b/src/util/entities/Group.ts
@@ -0,0 +1,33 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+
+import { BaseClass } from "./BaseClass";
+
+@Entity("groups")
+export class UserGroup extends BaseClass {
+  @Column({ nullable: true })
+  parent?: BigInt;
+
+	@Column()
+	color: number;
+
+	@Column()
+	hoist: boolean;
+
+ 	@Column()
+	mentionable: boolean;
+
+	@Column()
+	name: string;
+
+	@Column()
+	rights: BigInt;
+
+	@Column()
+	position: number;
+
+	@Column({ nullable: true })
+	icon: BigInt;
+
+	@Column({ nullable: true })
+	unicode_emoji: BigInt;
+}
diff --git a/src/util/entities/Guild.ts b/src/util/entities/Guild.ts
new file mode 100644
index 00000000..d146e577
--- /dev/null
+++ b/src/util/entities/Guild.ts
@@ -0,0 +1,370 @@
+import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, OneToMany, OneToOne, RelationId } from "typeorm";
+import { OrmUtils } from "../util/imports/OrmUtils";
+import { Config, handleFile, Snowflake } from "..";
+import { Ban } from "./Ban";
+import { BaseClass } from "./BaseClass";
+import { Channel } from "./Channel";
+import { Emoji } from "./Emoji";
+import { Invite } from "./Invite";
+import { Member } from "./Member";
+import { Role } from "./Role";
+import { Sticker } from "./Sticker";
+import { Template } from "./Template";
+import { User } from "./User";
+import { VoiceState } from "./VoiceState";
+import { Webhook } from "./Webhook";
+
+// TODO: application_command_count, application_command_counts: {1: 0, 2: 0, 3: 0}
+// TODO: guild_scheduled_events
+// TODO: stage_instances
+// TODO: threads
+// TODO:
+// "keywords": [
+// 		"Genshin Impact",
+// 		"Paimon",
+// 		"Honkai Impact",
+// 		"ARPG",
+// 		"Open-World",
+// 		"Waifu",
+// 		"Anime",
+// 		"Genshin",
+// 		"miHoYo",
+// 		"Gacha"
+// 	],
+
+export const PublicGuildRelations = [
+	"channels",
+	"emojis",
+	"members",
+	"roles",
+	"stickers",
+	"voice_states",
+	"members.user",
+];
+
+@Entity("guilds")
+export class Guild extends BaseClass {
+	@Column({ nullable: true })
+	@RelationId((guild: Guild) => guild.afk_channel)
+	afk_channel_id?: string;
+
+	@JoinColumn({ name: "afk_channel_id" })
+	@ManyToOne(() => Channel)
+	afk_channel?: Channel;
+
+	@Column({ nullable: true })
+	afk_timeout?: number = Config.get().defaults.guild.afkTimeout;
+
+	// * commented out -> use owner instead
+	// application id of the guild creator if it is bot-created
+	// @Column({ nullable: true })
+	// application?: string;
+
+	@JoinColumn({ name: "ban_ids" })
+	@OneToMany(() => Ban, (ban: Ban) => ban.guild, {
+		cascade: true,
+		orphanedRowAction: "delete",
+	})
+	bans: Ban[];
+
+	@Column({ nullable: true })
+	banner?: string;
+
+	@Column({ nullable: true })
+	default_message_notifications?: number = Config.get().defaults.guild.defaultMessageNotifications;
+
+	@Column({ nullable: true })
+	description?: string;
+
+	@Column({ nullable: true })
+	discovery_splash?: string;
+
+	@Column({ nullable: true })
+	explicit_content_filter?: number = Config.get().defaults.guild.explicitContentFilter;
+
+	@Column({ type: "simple-array" })
+	features: string[]; //TODO use enum
+	//TODO: https://discord.com/developers/docs/resources/guild#guild-object-guild-features
+
+	@Column({ nullable: true })
+	primary_category_id: number;
+
+	@Column({ nullable: true })
+	icon?: string;
+
+	@Column({ nullable: true })
+	large?: boolean;
+
+	@Column({ nullable: true })
+	max_members?: number = Config.get().limits.guild.maxMembers; // e.g. default 100.000
+
+	@Column({ nullable: true })
+	max_presences?: number = Config.get().defaults.guild.maxPresences;
+
+	@Column({ nullable: true })
+	max_video_channel_users?: number = Config.get().defaults.guild.maxVideoChannelUsers; // ? default: 25, is this max 25 streaming or watching
+
+	@Column({ nullable: true })
+	member_count?: number = 0;
+
+	@Column({ nullable: true })
+	presence_count?: number = 0; // users online
+
+	@OneToMany(() => Member, (member: Member) => member.guild, {
+		cascade: true,
+		orphanedRowAction: "delete",
+		onDelete: "CASCADE",
+	})
+	members: Member[];
+
+	@JoinColumn({ name: "role_ids" })
+	@OneToMany(() => Role, (role: Role) => role.guild, {
+		cascade: true,
+		orphanedRowAction: "delete",
+		onDelete: "CASCADE",
+	})
+	roles: Role[];
+
+	@JoinColumn({ name: "channel_ids" })
+	@OneToMany(() => Channel, (channel: Channel) => channel.guild, {
+		cascade: true,
+		orphanedRowAction: "delete",
+	})
+	channels: Channel[];
+
+	@Column({ nullable: true })
+	@RelationId((guild: Guild) => guild.template)
+	template_id?: string;
+
+	@JoinColumn({ name: "template_id", referencedColumnName: "id" })
+	@ManyToOne(() => Template)
+	template: Template;
+
+	@JoinColumn({ name: "emoji_ids" })
+	@OneToMany(() => Emoji, (emoji: Emoji) => emoji.guild, {
+		cascade: true,
+		orphanedRowAction: "delete",
+		onDelete: "CASCADE",
+	})
+	emojis: Emoji[];
+
+	@JoinColumn({ name: "sticker_ids" })
+	@OneToMany(() => Sticker, (sticker: Sticker) => sticker.guild, {
+		cascade: true,
+		orphanedRowAction: "delete",
+		onDelete: "CASCADE",
+	})
+	stickers: Sticker[];
+
+	@JoinColumn({ name: "invite_ids" })
+	@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, {
+		cascade: true,
+		orphanedRowAction: "delete",
+		onDelete: "CASCADE",
+	})
+	voice_states: VoiceState[];
+
+	@JoinColumn({ name: "webhook_ids" })
+	@OneToMany(() => Webhook, (webhook: Webhook) => webhook.guild, {
+		cascade: true,
+		orphanedRowAction: "delete",
+		onDelete: "CASCADE",
+	})
+	webhooks: Webhook[];
+
+	@Column({ nullable: true })
+	mfa_level?: number;
+
+	@Column()
+	name: string;
+
+	@Column({ nullable: true })
+	@RelationId((guild: Guild) => guild.owner)
+	owner_id?: string; // optional to allow for ownerless guilds
+
+	@JoinColumn({ name: "owner_id", referencedColumnName: "id" })
+	@ManyToOne(() => User)
+	owner?: User; // optional to allow for ownerless guilds
+
+	@Column({ nullable: true })
+	preferred_locale?: string;
+
+	@Column({ nullable: true })
+	premium_subscription_count?: number;
+
+	@Column({ nullable: true })
+	premium_tier?: number; // crowd premium level
+
+	@Column({ nullable: true })
+	@RelationId((guild: Guild) => guild.public_updates_channel)
+	public_updates_channel_id: string;
+
+	@JoinColumn({ name: "public_updates_channel_id" })
+	@ManyToOne(() => Channel)
+	public_updates_channel?: Channel;
+
+	@Column({ nullable: true })
+	@RelationId((guild: Guild) => guild.rules_channel)
+	rules_channel_id?: string;
+
+	@JoinColumn({ name: "rules_channel_id" })
+	@ManyToOne(() => Channel)
+	rules_channel?: string;
+
+	@Column({ nullable: true })
+	region?: string;
+
+	@Column({ nullable: true })
+	splash?: string;
+
+	@Column({ nullable: true })
+	@RelationId((guild: Guild) => guild.system_channel)
+	system_channel_id?: string;
+
+	@JoinColumn({ name: "system_channel_id" })
+	@ManyToOne(() => Channel)
+	system_channel?: Channel;
+
+	@Column({ nullable: true })
+	system_channel_flags?: number;
+
+	@Column({ nullable: true })
+	unavailable?: boolean;
+
+	@Column({ nullable: true })
+	verification_level?: number;
+
+	@Column({ type: "simple-json" })
+	welcome_screen: {
+		enabled: boolean;
+		description: string;
+		welcome_channels: {
+			description: string;
+			emoji_id?: string;
+			emoji_name: string;
+			channel_id: string;
+		}[];
+	};
+
+	@Column({ nullable: true })
+	@RelationId((guild: Guild) => guild.widget_channel)
+	widget_channel_id?: string;
+
+	@JoinColumn({ name: "widget_channel_id" })
+	@ManyToOne(() => Channel)
+	widget_channel?: Channel;
+
+	@Column({ nullable: true })
+	widget_enabled?: boolean;
+
+	@Column({ nullable: true })
+	nsfw_level?: number;
+
+	@Column({ nullable: true })
+	nsfw?: boolean;
+
+	// TODO: nested guilds
+	@Column({ nullable: true })
+	parent?: string;
+
+	// only for developer portal
+	permissions?: number;
+
+	//new guild settings, 11/08/2022:
+	@Column({ nullable: true })
+	premium_progress_bar_enabled: boolean = false;
+
+	static async createGuild(body: {
+		name?: string;
+		icon?: string | null;
+		owner_id?: string;
+		channels?: Partial<Channel>[];
+	}) {
+		const guild_id = Snowflake.generate();
+
+		const guild: Guild = OrmUtils.mergeDeep(new Guild(), {
+			name: body.name || "Fosscord",
+			icon: await handleFile(`/icons/${guild_id}`, body.icon as string),
+			region: Config.get().regions.default,
+			owner_id: body.owner_id, // TODO: need to figure out a way for ownerless guilds and multiply-owned guilds
+			afk_timeout: 300,
+			default_message_notifications: 1, // defaults effect: setting the push default at mentions-only will save a lot
+			explicit_content_filter: 0,
+			features: [],
+			primary_category_id: null,
+			id: guild_id,
+			max_members: 250000,
+			max_presences: 250000,
+			max_video_channel_users: 200,
+			presence_count: 0,
+			member_count: 0, // will automatically be increased by addMember()
+			mfa_level: 0,
+			preferred_locale: "en-US",
+			premium_subscription_count: 0,
+			premium_tier: 0,
+			system_channel_flags: 4, // defaults effect: suppress the setup tips to save performance
+			unavailable: false,
+			nsfw: false,
+			nsfw_level: 0,
+			verification_level: 0,
+			welcome_screen: {
+				enabled: false,
+				description: "Fill in your description",
+				welcome_channels: [],
+			},
+			widget_enabled: true, // NB: don't set it as false to prevent artificial restrictions
+		});
+		await guild.save();
+
+		// we have to create the role _after_ the guild because else we would get a "SQLITE_CONSTRAINT: FOREIGN KEY constraint failed" error
+		// TODO: make the @everyone a pseudorole that is dynamically generated at runtime so we can save storage
+		let role: Role = OrmUtils.mergeDeep(new Role(), {
+			id: guild_id,
+			guild_id: guild_id,
+			color: 0,
+			hoist: false,
+			managed: false,
+			// NB: in Fosscord, every role will be non-managed, as we use user-groups instead of roles for managed groups
+			mentionable: false,
+			name: "@everyone",
+			permissions: String("2251804225"),
+			position: 0,
+			icon: null,
+			unicode_emoji: null,
+		});
+		await role.save();
+
+		if (!body.channels || !body.channels.length) body.channels = [{ id: "01", type: 0, name: "general" }];
+
+		const ids = new Map();
+
+		body.channels.forEach((x) => {
+			if (x.id) {
+				ids.set(x.id, Snowflake.generate());
+			}
+		});
+
+		for (const channel of body.channels?.sort((a, b) => (a.parent_id ? 1 : -1))) {
+			let id = ids.get(channel.id) || Snowflake.generate();
+
+			let 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,
+			});
+		}
+
+		return guild;
+	}
+}
diff --git a/src/util/entities/Invite.ts b/src/util/entities/Invite.ts
new file mode 100644
index 00000000..1e0ebe52
--- /dev/null
+++ b/src/util/entities/Invite.ts
@@ -0,0 +1,88 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId, PrimaryColumn } from "typeorm";
+import { Member } from "./Member";
+import { BaseClassWithoutId } from "./BaseClass";
+import { Channel } from "./Channel";
+import { Guild } from "./Guild";
+import { User } from "./User";
+import { random } from "@fosscord/api";
+
+export const PublicInviteRelation = ["inviter", "guild", "channel"];
+
+@Entity("invites")
+export class Invite extends BaseClassWithoutId {
+	@PrimaryColumn()
+	code: string = random();
+
+	@Column()
+	temporary: boolean = true;
+
+	@Column()
+	uses: number = 0;
+
+	@Column()
+	max_uses: number;
+
+	@Column()
+	max_age: number;
+
+	@Column()
+	created_at: Date = new Date();
+
+	@Column()
+	expires_at: Date;
+
+	@Column({ nullable: true })
+	@RelationId((invite: Invite) => invite.guild)
+	guild_id: string;
+
+	@JoinColumn({ name: "guild_id" })
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
+	guild: Guild;
+
+	@Column({ nullable: true })
+	@RelationId((invite: Invite) => invite.channel)
+	channel_id: string;
+
+	@JoinColumn({ name: "channel_id" })
+	@ManyToOne(() => Channel, {
+		onDelete: "CASCADE",
+	})
+	channel: Channel;
+
+	@Column({ nullable: true })
+	@RelationId((invite: Invite) => invite.inviter)
+	inviter_id: string;
+
+	@JoinColumn({ name: "inviter_id" })
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE"
+	})
+	inviter: User;
+
+	@Column({ nullable: true })
+	@RelationId((invite: Invite) => invite.target_user)
+	target_user_id: string;
+
+	@JoinColumn({ name: "target_user_id" })
+	@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;
+
+	@Column({ nullable: true })
+	vanity_url?: boolean;
+
+	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 });
+		else await invite.save();
+
+		await Member.addToGuild(user_id, invite.guild_id);
+		return invite;
+	}
+}
diff --git a/src/util/entities/Member.ts b/src/util/entities/Member.ts
new file mode 100644
index 00000000..baac58ed
--- /dev/null
+++ b/src/util/entities/Member.ts
@@ -0,0 +1,360 @@
+import { PublicUser, User } from "./User";
+import { BaseClass } from "./BaseClass";
+import {
+	Column,
+	Entity,
+	Index,
+	JoinColumn,
+	JoinTable,
+	ManyToMany,
+	ManyToOne,
+	PrimaryGeneratedColumn,
+	RelationId,
+} from "typeorm";
+import { Guild } from "./Guild";
+import { Config, emitEvent } from "../util";
+import {
+	GuildCreateEvent,
+	GuildDeleteEvent,
+	GuildMemberAddEvent,
+	GuildMemberRemoveEvent,
+	GuildMemberUpdateEvent,
+} from "../interfaces";
+import { HTTPError } from "../util/imports/HTTPError";
+import { Role } from "./Role";
+import { BaseClassWithoutId } from "./BaseClass";
+import { Ban, PublicGuildRelations } from ".";
+import { DiscordApiErrors } from "../util/Constants";
+import { OrmUtils } from "../util/imports/OrmUtils";
+
+export const MemberPrivateProjection: (keyof Member)[] = [
+	"id",
+	"guild",
+	"guild_id",
+	"deaf",
+	"joined_at",
+	"last_message_id",
+	"mute",
+	"nick",
+	"pending",
+	"premium_since",
+	"roles",
+	"settings",
+	"user",
+];
+
+@Entity("members")
+@Index(["id", "guild_id"], { unique: true })
+export class Member extends BaseClassWithoutId {
+	@PrimaryGeneratedColumn()
+	index: string;
+
+	@Column()
+	@RelationId((member: Member) => member.user)
+	id: string;
+
+	@JoinColumn({ name: "id" })
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
+	user: User;
+
+	@Column()
+	@RelationId((member: Member) => member.guild)
+	guild_id: string;
+
+	@JoinColumn({ name: "guild_id" })
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
+	guild: Guild;
+
+	@Column({ nullable: true })
+	nick?: string;
+
+	@JoinTable({
+		name: "member_roles",
+		joinColumn: { name: "index", referencedColumnName: "index" },
+		inverseJoinColumn: {
+			name: "role_id",
+			referencedColumnName: "id",
+		},
+	})
+	@ManyToMany(() => Role, { cascade: true })
+	roles: Role[];
+
+	@Column()
+	joined_at: Date;
+
+	@Column({ nullable: true })
+	premium_since?: Date;
+
+	@Column()
+	deaf: boolean;
+
+	@Column()
+	mute: boolean;
+
+	@Column()
+	pending: boolean;
+
+	@Column({ type: "simple-json", select: false })
+	settings: UserGuildSettings;
+
+	@Column({ nullable: true })
+	last_message_id?: string;
+
+	/**
+	@JoinColumn({ name: "id" })
+	@ManyToOne(() => User, {
+		onDelete: "DO NOTHING",
+	// do not auto-kick force-joined members just because their joiners left the server
+	}) **/
+	@Column({ nullable: true })
+	joined_by?: string;
+
+	// TODO: add this when we have proper read receipts
+	// @Column({ type: "simple-json" })
+	// read_state: ReadState;
+
+	static async IsInGuildOrFail(user_id: string, guild_id: string) {
+		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", "member_count"], 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
+		//TODO: check for bugs
+		if (guild.member_count) guild.member_count--;
+		return Promise.all([
+			Member.delete({
+				id: user_id,
+				guild_id,
+			}),
+			//Guild.decrement({ id: guild_id }, "member_count", -1),
+
+			emitEvent({
+				event: "GUILD_DELETE",
+				data: {
+					id: guild_id,
+				},
+				user_id: user_id,
+			} as GuildDeleteEvent),
+			emitEvent({
+				event: "GUILD_MEMBER_REMOVE",
+				data: { guild_id, user: member.user },
+				guild_id,
+			} as GuildMemberRemoveEvent),
+		]);
+	}
+
+	static async addRole(user_id: string, guild_id: string, role_id: string) {
+		const [member, role] = await Promise.all([
+			// @ts-ignore
+			Member.findOneOrFail({
+				where: { id: user_id, guild_id },
+				relations: ["user", "roles"], // we don't want to load  the role objects just the ids
+				select: ["index"],
+			}),
+			Role.findOneOrFail({ where: { id: role_id, guild_id }, select: ["id"] }),
+		]);
+		member.roles.push(OrmUtils.mergeDeep(new Role(), { id: role_id }));
+
+		await Promise.all([
+			member.save(),
+			emitEvent({
+				event: "GUILD_MEMBER_UPDATE",
+				data: {
+					guild_id,
+					user: member.user,
+					roles: member.roles.map((x) => x.id),
+				},
+				guild_id,
+			} as GuildMemberUpdateEvent),
+		]);
+	}
+
+	static async removeRole(user_id: string, guild_id: string, role_id: string) {
+		const [member] = await Promise.all([
+			// @ts-ignore
+			Member.findOneOrFail({
+				where: { id: user_id, guild_id },
+				relations: ["user", "roles"], // we don't want to load  the role objects just the ids
+				select: ["index"],
+			}),
+			await Role.findOneOrFail({ where: { id: role_id, guild_id } }),
+		]);
+		member.roles = member.roles.filter((x) => x.id == role_id);
+
+		await Promise.all([
+			member.save(),
+			emitEvent({
+				event: "GUILD_MEMBER_UPDATE",
+				data: {
+					guild_id,
+					user: member.user,
+					roles: member.roles.map((x) => x.id),
+				},
+				guild_id,
+			} as GuildMemberUpdateEvent),
+		]);
+	}
+
+	static async changeNickname(user_id: string, guild_id: string, nickname: string) {
+		const member = await Member.findOneOrFail({
+			where: {
+				id: user_id,
+				guild_id,
+			},
+			relations: ["user"],
+		});
+		member.nick = nickname;
+
+		await Promise.all([
+			member.save(),
+
+			emitEvent({
+				event: "GUILD_MEMBER_UPDATE",
+				data: {
+					guild_id,
+					user: member.user,
+					nick: nickname,
+				},
+				guild_id,
+			} as GuildMemberUpdateEvent),
+		]);
+	}
+
+	static async addToGuild(user_id: string, guild_id: string) {
+		const user = await User.getPublicUser(user_id);
+		const isBanned = await Ban.count({ where: { guild_id, user_id } });
+		if (isBanned) {
+			throw DiscordApiErrors.USER_BANNED;
+		}
+		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);
+		}
+
+		const guild = await Guild.findOneOrFail({
+			where: {
+				id: guild_id,
+			},
+			relations: PublicGuildRelations,
+		});
+
+		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 = {
+			id: user_id,
+			guild_id,
+			nick: undefined,
+			roles: [guild_id], // @everyone role
+			joined_at: new Date(),
+			premium_since: null,
+			deaf: false,
+			mute: false,
+			pending: false,
+		};
+		//TODO: check for bugs
+		if (guild.member_count) guild.member_count++;
+		await Promise.all([
+			OrmUtils.mergeDeep(new Member(), {
+				...member,
+				roles: [OrmUtils.mergeDeep(new Role(), { id: guild_id })],
+				// read_state: {},
+				settings: {
+					channel_overrides: [],
+					message_notifications: 0,
+					mobile_push: true,
+					muted: false,
+					suppress_everyone: false,
+					suppress_roles: false,
+					version: 0,
+				},
+				// Member.save is needed because else the roles relations wouldn't be updated
+			}).save(),
+			//Guild.increment({ id: guild_id }, "member_count", 1),
+			emitEvent({
+				event: "GUILD_MEMBER_ADD",
+				data: {
+					...member,
+					user,
+					guild_id,
+				},
+				guild_id,
+			} as GuildMemberAddEvent),
+			emitEvent({
+				event: "GUILD_CREATE",
+				data: {
+					...guild,
+					members: [...guild.members, { ...member, user }],
+					member_count: (guild.member_count || 0) + 1,
+					guild_hashes: {},
+					guild_scheduled_events: [],
+					joined_at: member.joined_at,
+					presences: [],
+					stage_instances: [],
+					threads: [],
+				},
+				user_id,
+			} as GuildCreateEvent),
+		]);
+	}
+}
+
+export interface UserGuildSettings {
+	channel_overrides: {
+		channel_id: string;
+		message_notifications: number;
+		mute_config: MuteConfig;
+		muted: boolean;
+	}[];
+	message_notifications: number;
+	mobile_push: boolean;
+	mute_config: MuteConfig;
+	muted: boolean;
+	suppress_everyone: boolean;
+	suppress_roles: boolean;
+	version: number;
+}
+
+export interface MuteConfig {
+	end_time: number;
+	selected_time_window: number;
+}
+
+export type PublicMemberKeys =
+	| "id"
+	| "guild_id"
+	| "nick"
+	| "roles"
+	| "joined_at"
+	| "pending"
+	| "deaf"
+	| "mute"
+	| "premium_since";
+
+export const PublicMemberProjection: PublicMemberKeys[] = [
+	"id",
+	"guild_id",
+	"nick",
+	"roles",
+	"joined_at",
+	"pending",
+	"deaf",
+	"mute",
+	"premium_since",
+];
+
+// @ts-ignore
+export type PublicMember = Pick<Member, Omit<PublicMemberKeys, "roles">> & {
+	user: PublicUser;
+	roles: string[]; // only role ids not objects
+};
diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts
new file mode 100644
index 00000000..ba3d4f2d
--- /dev/null
+++ b/src/util/entities/Message.ts
@@ -0,0 +1,284 @@
+import { User } from "./User";
+import { Member } from "./Member";
+import { Role } from "./Role";
+import { Channel } from "./Channel";
+import { InteractionType } from "../interfaces/Interaction";
+import { Application } from "./Application";
+import {
+	Column,
+	CreateDateColumn,
+	Entity,
+	Index,
+	JoinColumn,
+	JoinTable,
+	ManyToMany,
+	ManyToOne,
+	OneToMany,
+	RelationId,
+	RemoveOptions,
+	UpdateDateColumn,
+} from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { Guild } from "./Guild";
+import { Webhook } from "./Webhook";
+import { Sticker } from "./Sticker";
+import { Attachment } from "./Attachment";
+
+export enum MessageType {
+	DEFAULT = 0,
+	RECIPIENT_ADD = 1,
+	RECIPIENT_REMOVE = 2,
+	CALL = 3,
+	CHANNEL_NAME_CHANGE = 4,
+	CHANNEL_ICON_CHANGE = 5,
+	CHANNEL_PINNED_MESSAGE = 6,
+	GUILD_MEMBER_JOIN = 7,
+	USER_PREMIUM_GUILD_SUBSCRIPTION = 8,
+	USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 = 9,
+	USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 = 10,
+	USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 = 11,
+	CHANNEL_FOLLOW_ADD = 12,
+	ACTION = 13, // /me messages
+	GUILD_DISCOVERY_DISQUALIFIED = 14,
+	GUILD_DISCOVERY_REQUALIFIED = 15,
+	ENCRYPTED = 16,
+	REPLY = 19,
+	APPLICATION_COMMAND = 20, // application command or self command invocation
+	ROUTE_ADDED = 41, // custom message routing: new route affecting that channel
+	ROUTE_DISABLED = 42, // custom message routing: given route no longer affecting that channel
+	SELF_COMMAND_SCRIPT = 43, // self command scripts
+	ENCRYPTION = 50,
+	CUSTOM_START = 63,
+	UNHANDLED = 255
+}
+
+@Entity("messages")
+@Index(["channel_id", "id"], { unique: true })
+export class Message extends BaseClass {
+	@Column({ nullable: true })
+	@RelationId((message: Message) => message.channel)
+	@Index()
+	channel_id: string;
+
+	@JoinColumn({ name: "channel_id" })
+	@ManyToOne(() => Channel, {
+		onDelete: "CASCADE",
+	})
+	channel: Channel;
+
+	@Column({ nullable: true })
+	@RelationId((message: Message) => message.guild)
+	guild_id?: string;
+
+	@JoinColumn({ name: "guild_id" })
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
+	guild?: Guild;
+
+	@Column({ nullable: true })
+	@RelationId((message: Message) => message.author)
+	@Index()
+	author_id: string;
+
+	@JoinColumn({ name: "author_id", referencedColumnName: "id" })
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
+	author?: User;
+
+	@Column({ nullable: true })
+	@RelationId((message: Message) => message.member)
+	member_id: string;
+
+	@JoinColumn({ name: "member_id", referencedColumnName: "id" })
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
+	member?: Member;
+
+	@Column({ nullable: true })
+	@RelationId((message: Message) => message.webhook)
+	webhook_id: string;
+
+	@JoinColumn({ name: "webhook_id" })
+	@ManyToOne(() => Webhook)
+	webhook?: Webhook;
+
+	@Column({ nullable: true })
+	@RelationId((message: Message) => message.application)
+	application_id: string;
+
+	@JoinColumn({ name: "application_id" })
+	@ManyToOne(() => Application)
+	application?: Application;
+
+	@Column({ nullable: true })
+	content?: string;
+
+	@Column()
+	@CreateDateColumn()
+	timestamp: Date;
+
+	@Column({ nullable: true })
+	edited_timestamp?: Date;
+
+	@Column({ nullable: true })
+	tts?: boolean;
+
+	@Column({ nullable: true })
+	mention_everyone?: boolean;
+
+	@JoinTable({ name: "message_user_mentions" })
+	@ManyToMany(() => User)
+	mentions: User[];
+
+	@JoinTable({ name: "message_role_mentions" })
+	@ManyToMany(() => Role)
+	mention_roles: Role[];
+
+	@JoinTable({ name: "message_channel_mentions" })
+	@ManyToMany(() => Channel)
+	mention_channels: Channel[];
+
+	@JoinTable({ name: "message_stickers" })
+	@ManyToMany(() => Sticker, { cascade: true, onDelete: "CASCADE" })
+	sticker_items?: Sticker[];
+
+	@OneToMany(() => Attachment, (attachment: Attachment) => attachment.message, {
+		cascade: true,
+		orphanedRowAction: "delete",
+	})
+	attachments?: Attachment[];
+
+	@Column({ type: "simple-json" })
+	embeds: Embed[];
+
+	@Column({ type: "simple-json" })
+	reactions: Reaction[];
+
+	@Column({ type: "text", nullable: true })
+	nonce?: string;
+
+	@Column({ nullable: true })
+	pinned?: boolean;
+
+	@Column({ type: "int" })
+	type: MessageType;
+
+	@Column({ type: "simple-json", nullable: true })
+	activity?: {
+		type: number;
+		party_id: string;
+	};
+
+	@Column({ nullable: true })
+	flags?: string;
+	@Column({ type: "simple-json", nullable: true })
+	message_reference?: {
+		message_id: string;
+		channel_id?: string;
+		guild_id?: string;
+	};
+
+	@JoinColumn({ name: "message_reference_id" })
+	@ManyToOne(() => Message)
+	referenced_message?: Message;
+
+	@Column({ type: "simple-json", nullable: true })
+	interaction?: {
+		id: string;
+		type: InteractionType;
+		name: string;
+		user_id: string; // the user who invoked the interaction
+		// user: User; // TODO: autopopulate user
+	};
+
+	@Column({ type: "simple-json", nullable: true })
+	components?: MessageComponent[];
+}
+
+export interface MessageComponent {
+	type: number;
+	style?: number;
+	label?: string;
+	emoji?: PartialEmoji;
+	custom_id?: string;
+	url?: string;
+	disabled?: boolean;
+	components: MessageComponent[];
+}
+
+export enum MessageComponentType {
+	Script = 0, // self command script
+	ActionRow = 1,
+	Button = 2,
+}
+
+export interface Embed {
+	title?: string; //title of embed
+	type?: EmbedType; // type of embed (always "rich" for webhook embeds)
+	description?: string; // description of embed
+	url?: string; // url of embed
+	timestamp?: Date; // timestamp of embed content
+	color?: number; // color code of the embed
+	footer?: {
+		text: string;
+		icon_url?: string;
+		proxy_icon_url?: string;
+	}; // footer object	footer information
+	image?: EmbedImage; // image object	image information
+	thumbnail?: EmbedImage; // thumbnail object	thumbnail information
+	video?: EmbedImage; // video object	video information
+	provider?: {
+		name?: string;
+		url?: string;
+	}; // provider object	provider information
+	author?: {
+		name?: string;
+		url?: string;
+		icon_url?: string;
+		proxy_icon_url?: string;
+	}; // author object	author information
+	fields?: {
+		name: string;
+		value: string;
+		inline?: boolean;
+	}[];
+}
+
+export enum EmbedType {
+	rich = "rich",
+	image = "image",
+	video = "video",
+	gifv = "gifv",
+	article = "article",
+	link = "link",
+}
+
+export interface EmbedImage {
+	url?: string;
+	proxy_url?: string;
+	height?: number;
+	width?: number;
+}
+
+export interface Reaction {
+	count: number;
+	//// not saved in the database // me: boolean; // whether the current user reacted using this emoji
+	emoji: PartialEmoji;
+	user_ids: string[];
+}
+
+export interface PartialEmoji {
+	id?: string;
+	name: string;
+	animated?: boolean;
+}
+
+export interface AllowedMentions {
+	parse?: ("users" | "roles" | "everyone")[];
+	roles?: string[];
+	users?: string[];
+	replied_user?: boolean;
+}
diff --git a/src/util/entities/Migration.ts b/src/util/entities/Migration.ts
new file mode 100644
index 00000000..3f39ae72
--- /dev/null
+++ b/src/util/entities/Migration.ts
@@ -0,0 +1,18 @@
+import { Column, Entity, ObjectIdColumn, PrimaryGeneratedColumn } from "typeorm";
+import { BaseClassWithoutId } from ".";
+
+export const PrimaryIdAutoGenerated = process.env.DATABASE?.startsWith("mongodb")
+	? ObjectIdColumn
+	: PrimaryGeneratedColumn;
+
+@Entity("migrations")
+export class Migration extends BaseClassWithoutId {
+	@PrimaryIdAutoGenerated()
+	id: number;
+
+	@Column({ type: "bigint" })
+	timestamp: number;
+
+	@Column()
+	name: string;
+}
diff --git a/src/util/entities/Note.ts b/src/util/entities/Note.ts
new file mode 100644
index 00000000..36017c5e
--- /dev/null
+++ b/src/util/entities/Note.ts
@@ -0,0 +1,18 @@
+import { Column, Entity, JoinColumn, ManyToOne, Unique } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { User } from "./User";
+
+@Entity("notes")
+@Unique(["owner", "target"])
+export class Note extends BaseClass {
+	@JoinColumn({ name: "owner_id" })
+	@ManyToOne(() => User, { onDelete: "CASCADE" })
+	owner: User;
+
+	@JoinColumn({ name: "target_id" })
+	@ManyToOne(() => User, { onDelete: "CASCADE" })
+	target: User;
+
+	@Column()
+	content: string;
+}
\ No newline at end of file
diff --git a/src/util/entities/RateLimit.ts b/src/util/entities/RateLimit.ts
new file mode 100644
index 00000000..f5916f6b
--- /dev/null
+++ b/src/util/entities/RateLimit.ts
@@ -0,0 +1,17 @@
+import { Column, Entity } from "typeorm";
+import { BaseClass } from "./BaseClass";
+
+@Entity("rate_limits")
+export class RateLimit extends BaseClass {
+	@Column() // no relation as it also
+	executor_id: string;
+
+	@Column()
+	hits: number;
+
+	@Column()
+	blocked: boolean;
+
+	@Column()
+	expires_at: Date;
+}
diff --git a/src/util/entities/ReadState.ts b/src/util/entities/ReadState.ts
new file mode 100644
index 00000000..b915573b
--- /dev/null
+++ b/src/util/entities/ReadState.ts
@@ -0,0 +1,55 @@
+import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { Channel } from "./Channel";
+import { Message } from "./Message";
+import { User } from "./User";
+
+// for read receipts
+// notification cursor and public read receipt need to be forwards-only (the former to prevent re-pinging when marked as unread, and the latter to be acceptable as a legal acknowledgement in criminal proceedings), and private read marker needs to be advance-rewind capable
+// public read receipt ≥ notification cursor ≥ private fully read marker
+
+@Entity("read_states")
+@Index(["channel_id", "user_id"], { unique: true })
+export class ReadState extends BaseClass {
+	@Column()
+	@RelationId((read_state: ReadState) => read_state.channel)
+	channel_id: string;
+
+	@JoinColumn({ name: "channel_id" })
+	@ManyToOne(() => Channel, {
+		onDelete: "CASCADE",
+	})
+	channel: Channel;
+
+	@Column()
+	@RelationId((read_state: ReadState) => read_state.user)
+	user_id: string;
+
+	@JoinColumn({ name: "user_id" })
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
+	user: User;
+
+	// fully read marker
+	@Column({ nullable: true })
+	last_message_id: string; 
+	
+	// public read receipt
+	@Column({ nullable: true })
+	public_ack: string;
+
+	// notification cursor / private read receipt
+	@Column({ nullable: true })
+	notifications_cursor: string;
+
+	@Column({ nullable: true })
+	last_pin_timestamp?: Date;
+
+	@Column({ nullable: true })
+	mention_count: number;
+
+	// @Column({ nullable: true })
+	// TODO: derive this from (last_message_id=notifications_cursor=public_ack)=true
+	manual: boolean;
+}
diff --git a/src/util/entities/Recipient.ts b/src/util/entities/Recipient.ts
new file mode 100644
index 00000000..a945f938
--- /dev/null
+++ b/src/util/entities/Recipient.ts
@@ -0,0 +1,30 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+
+@Entity("recipients")
+export class Recipient extends BaseClass {
+	@Column()
+	@RelationId((recipient: Recipient) => recipient.channel)
+	channel_id: string;
+
+	@JoinColumn({ name: "channel_id" })
+	@ManyToOne(() => require("./Channel").Channel, {
+		onDelete: "CASCADE",
+	})
+	channel: import("./Channel").Channel;
+
+	@Column()
+	@RelationId((recipient: Recipient) => recipient.user)
+	user_id: string;
+
+	@JoinColumn({ name: "user_id" })
+	@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/src/util/entities/Relationship.ts b/src/util/entities/Relationship.ts
new file mode 100644
index 00000000..c3592c76
--- /dev/null
+++ b/src/util/entities/Relationship.ts
@@ -0,0 +1,49 @@
+import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { User } from "./User";
+
+export enum RelationshipType {
+	outgoing = 4,
+	incoming = 3,
+	blocked = 2,
+	friends = 1,
+}
+
+@Entity("relationships")
+@Index(["from_id", "to_id"], { unique: true })
+export class Relationship extends BaseClass {
+	@Column({})
+	@RelationId((relationship: Relationship) => relationship.from)
+	from_id: string;
+
+	@JoinColumn({ name: "from_id" })
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
+	from: User;
+
+	@Column({})
+	@RelationId((relationship: Relationship) => relationship.to)
+	to_id: string;
+
+	@JoinColumn({ name: "to_id" })
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
+	to: User;
+
+	@Column({ nullable: true })
+	nickname?: string;
+
+	@Column({ type: "int" })
+	type: RelationshipType;
+
+	toPublicRelationship() {
+		return {
+			id: this.to?.id || this.to_id,
+			type: this.type,
+			nickname: this.nickname,
+			user: this.to?.toPublicUser(),
+		};
+	}
+}
diff --git a/src/util/entities/Role.ts b/src/util/entities/Role.ts
new file mode 100644
index 00000000..4b721b5b
--- /dev/null
+++ b/src/util/entities/Role.ts
@@ -0,0 +1,51 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+
+import { BaseClass } from "./BaseClass";
+import { Guild } from "./Guild";
+
+@Entity("roles")
+export class Role extends BaseClass {
+	@Column({ nullable: true })
+	@RelationId((role: Role) => role.guild)
+	guild_id: string;
+
+	@JoinColumn({ name: "guild_id" })
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
+	guild: Guild;
+
+	@Column()
+	color: number;
+
+	@Column()
+	hoist: boolean;
+
+	@Column()
+	managed: boolean;
+
+	@Column()
+	mentionable: boolean;
+
+	@Column()
+	name: string;
+
+	@Column()
+	permissions: string;
+
+	@Column()
+	position: number;
+
+	@Column({ nullable: true })
+	icon: string;
+
+	@Column({ nullable: true })
+	unicode_emoji: string;
+
+	@Column({ type: "simple-json", nullable: true })
+	tags?: {
+		bot_id?: string;
+		integration_id?: string;
+		premium_subscriber?: boolean;
+	};
+}
diff --git a/src/util/entities/Session.ts b/src/util/entities/Session.ts
new file mode 100644
index 00000000..969efa89
--- /dev/null
+++ b/src/util/entities/Session.ts
@@ -0,0 +1,46 @@
+import { User } from "./User";
+import { BaseClass } from "./BaseClass";
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { Status } from "../interfaces/Status";
+import { Activity } from "../interfaces/Activity";
+
+//TODO we need to remove all sessions on server start because if the server crashes without closing websockets it won't delete them
+
+@Entity("sessions")
+export class Session extends BaseClass {
+	@Column({ nullable: true })
+	@RelationId((session: Session) => session.user)
+	user_id: string;
+
+	@JoinColumn({ name: "user_id" })
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
+	user: User;
+
+	//TODO check, should be 32 char long hex string
+	@Column({ nullable: false, select: false })
+	session_id: string;
+
+	@Column({ type: "simple-json", nullable: true })
+	activities: Activity[];
+
+	// TODO client_status
+	@Column({ type: "simple-json", select: false })
+	client_info: {
+		client: string;
+		os: string;
+		version: number;
+	};
+
+	@Column({ nullable: false, type: "varchar" })
+	status: Status; //TODO enum
+}
+
+export const PrivateSessionProjection: (keyof Session)[] = [
+	"user_id",
+	"session_id",
+	"activities",
+	"client_info",
+	"status",
+];
diff --git a/src/util/entities/Sticker.ts b/src/util/entities/Sticker.ts
new file mode 100644
index 00000000..37bc6fbe
--- /dev/null
+++ b/src/util/entities/Sticker.ts
@@ -0,0 +1,66 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { User } from "./User";
+import { BaseClass } from "./BaseClass";
+import { Guild } from "./Guild";
+
+export enum StickerType {
+	STANDARD = 1,
+	GUILD = 2,
+}
+
+export enum StickerFormatType {
+	GIF = 0, // gif is a custom format type and not in discord spec
+	PNG = 1,
+	APNG = 2,
+	LOTTIE = 3,
+}
+
+@Entity("stickers")
+export class Sticker extends BaseClass {
+	@Column()
+	name: string;
+
+	@Column({ nullable: true })
+	description?: string;
+
+	@Column({ nullable: true })
+	available?: boolean;
+
+	@Column({ nullable: true })
+	tags?: string;
+
+	@Column({ nullable: true })
+	@RelationId((sticker: Sticker) => sticker.pack)
+	pack_id?: string;
+
+	@JoinColumn({ name: "pack_id" })
+	@ManyToOne(() => require("./StickerPack").StickerPack, {
+		onDelete: "CASCADE",
+		nullable: true,
+	})
+	pack: import("./StickerPack").StickerPack;
+
+	@Column({ nullable: true })
+	guild_id?: string;
+
+	@JoinColumn({ name: "guild_id" })
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
+	guild?: Guild;
+
+	@Column({ nullable: true })
+	user_id?: string;
+
+	@JoinColumn({ name: "user_id" })
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
+	user?: User;
+
+	@Column({ type: "int" })
+	type: StickerType;
+
+	@Column({ type: "int" })
+	format_type: StickerFormatType;
+}
diff --git a/src/util/entities/StickerPack.ts b/src/util/entities/StickerPack.ts
new file mode 100644
index 00000000..ec8c69a2
--- /dev/null
+++ b/src/util/entities/StickerPack.ts
@@ -0,0 +1,31 @@
+import { Column, Entity, JoinColumn, ManyToOne, OneToMany, OneToOne, RelationId } from "typeorm";
+import { Sticker } from ".";
+import { BaseClass } from "./BaseClass";
+
+@Entity("sticker_packs")
+export class StickerPack extends BaseClass {
+	@Column()
+	name: string;
+
+	@Column({ nullable: true })
+	description?: string;
+
+	@Column({ nullable: true })
+	banner_asset_id?: string;
+
+	@OneToMany(() => Sticker, (sticker: Sticker) => sticker.pack, {
+		cascade: true,
+		orphanedRowAction: "delete",
+	})
+	stickers: Sticker[];
+
+	// sku_id: string
+
+	@Column({ nullable: true })
+	@RelationId((pack: StickerPack) => pack.cover_sticker)
+	cover_sticker_id?: string;
+
+	@ManyToOne(() => Sticker, { nullable: true })
+	@JoinColumn()
+	cover_sticker?: Sticker;
+}
diff --git a/src/util/entities/Team.ts b/src/util/entities/Team.ts
new file mode 100644
index 00000000..22140b7f
--- /dev/null
+++ b/src/util/entities/Team.ts
@@ -0,0 +1,27 @@
+import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, OneToMany, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { TeamMember } from "./TeamMember";
+import { User } from "./User";
+
+@Entity("teams")
+export class Team extends BaseClass {
+	@Column({ nullable: true })
+	icon?: string;
+
+	@JoinColumn({ name: "member_ids" })
+	@OneToMany(() => TeamMember, (member: TeamMember) => member.team, {
+		orphanedRowAction: "delete",
+	})
+	members: TeamMember[];
+
+	@Column()
+	name: string;
+
+	@Column({ nullable: true })
+	@RelationId((team: Team) => team.owner_user)
+	owner_user_id: string;
+
+	@JoinColumn({ name: "owner_user_id" })
+	@ManyToOne(() => User)
+	owner_user: User;
+}
diff --git a/src/util/entities/TeamMember.ts b/src/util/entities/TeamMember.ts
new file mode 100644
index 00000000..b726e1e8
--- /dev/null
+++ b/src/util/entities/TeamMember.ts
@@ -0,0 +1,37 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { User } from "./User";
+
+export enum TeamMemberState {
+	INVITED = 1,
+	ACCEPTED = 2,
+}
+
+@Entity("team_members")
+export class TeamMember extends BaseClass {
+	@Column({ type: "int" })
+	membership_state: TeamMemberState;
+
+	@Column({ type: "simple-array" })
+	permissions: string[];
+
+	@Column({ nullable: true })
+	@RelationId((member: TeamMember) => member.team)
+	team_id: string;
+
+	@JoinColumn({ name: "team_id" })
+	@ManyToOne(() => require("./Team").Team, (team: import("./Team").Team) => team.members, {
+		onDelete: "CASCADE",
+	})
+	team: import("./Team").Team;
+
+	@Column({ nullable: true })
+	@RelationId((member: TeamMember) => member.user)
+	user_id: string;
+
+	@JoinColumn({ name: "user_id" })
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
+	user: User;
+}
diff --git a/src/util/entities/Template.ts b/src/util/entities/Template.ts
new file mode 100644
index 00000000..1d952283
--- /dev/null
+++ b/src/util/entities/Template.ts
@@ -0,0 +1,44 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { Guild } from "./Guild";
+import { User } from "./User";
+
+@Entity("templates")
+export class Template extends BaseClass {
+	@Column({ unique: true })
+	code: string;
+
+	@Column()
+	name: string;
+
+	@Column({ nullable: true })
+	description?: string;
+
+	@Column({ nullable: true })
+	usage_count?: number;
+
+	@Column({ nullable: true })
+	@RelationId((template: Template) => template.creator)
+	creator_id: string;
+
+	@JoinColumn({ name: "creator_id" })
+	@ManyToOne(() => User)
+	creator: User;
+
+	@Column()
+	created_at: Date;
+
+	@Column()
+	updated_at: Date;
+
+	@Column({ nullable: true })
+	@RelationId((template: Template) => template.source_guild)
+	source_guild_id: string;
+
+	@JoinColumn({ name: "source_guild_id" })
+	@ManyToOne(() => Guild)
+	source_guild: Guild;
+
+	@Column({ type: "simple-json" })
+	serialized_source_guild: Guild;
+}
diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts
new file mode 100644
index 00000000..61343e81
--- /dev/null
+++ b/src/util/entities/User.ts
@@ -0,0 +1,324 @@
+import { Column, Entity, FindOneOptions, FindOptionsSelectByString, JoinColumn, OneToMany, OneToOne } from "typeorm";
+import { OrmUtils } from "../util/imports/OrmUtils";
+import { BaseClass } from "./BaseClass";
+import { BitField } from "../util/BitField";
+import { Relationship } from "./Relationship";
+import { ConnectedAccount } from "./ConnectedAccount";
+import { Config, FieldErrors, Snowflake, trimSpecial } from "..";
+import { Member, Session, UserSettings } from ".";
+
+export enum PublicUserEnum {
+	username,
+	discriminator,
+	id,
+	public_flags,
+	avatar,
+	accent_color,
+	banner,
+	bio,
+	bot,
+	premium_since,
+}
+export type PublicUserKeys = keyof typeof PublicUserEnum;
+
+export enum PrivateUserEnum {
+	flags,
+	mfa_enabled,
+	email,
+	phone,
+	verified,
+	nsfw_allowed,
+	premium,
+	premium_type,
+	disabled,
+	settings,
+	// locale
+}
+export type PrivateUserKeys = keyof typeof PrivateUserEnum | PublicUserKeys;
+
+export const PublicUserProjection = Object.values(PublicUserEnum).filter(
+	(x) => typeof x === "string"
+) as PublicUserKeys[];
+export const PrivateUserProjection = [
+	...PublicUserProjection,
+	...Object.values(PrivateUserEnum).filter((x) => typeof x === "string"),
+] as PrivateUserKeys[];
+
+// 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 UserPrivate extends Pick<User, PrivateUserKeys> {
+	locale: string;
+}
+
+// TODO: add purchased_flags, premium_usage_flags
+
+@Entity("users")
+export class User extends BaseClass {
+	@Column()
+	username: string; // username max length 32, min 2 (should be configurable)
+
+	@Column()
+	discriminator: string; // opaque string: 4 digits on discord.com
+
+	setDiscriminator(val: string) {
+		const number = Number(val);
+		if (isNaN(number)) throw new Error("invalid discriminator");
+		if (number <= 0 || number >= 10000) throw new Error("discriminator must be between 1 and 9999");
+		this.discriminator = val.toString().padStart(4, "0");
+	}
+
+	@Column({ nullable: true })
+	avatar?: string; // hash of the user avatar
+
+	@Column({ nullable: true })
+	accent_color?: number; // banner color of user
+
+	@Column({ nullable: true })
+	banner?: string; // hash of the user banner
+
+	@Column({ nullable: true, select: false })
+	phone?: string; // phone number of the user
+
+	@Column({ select: false })
+	desktop: boolean = false; // if the user has desktop app installed
+
+	@Column({ select: false })
+	mobile: boolean = false; // if the user has mobile app installed
+
+	@Column()
+	premium: boolean = Config.get().defaults.user.premium; // if user bought individual premium
+
+	@Column()
+	premium_type: number = Config.get().defaults.user.premium_type; // individual premium level
+
+	@Column()
+	bot: boolean = false; // if user is bot
+
+	@Column()
+	bio: string; // short description of the user (max 190 chars -> should be configurable)
+
+	@Column()
+	system: boolean = false; // shouldn't be used, the api sends this field type true, if the generated message comes from a system generated author
+
+	@Column({ select: false })
+	nsfw_allowed: boolean = true; // if the user can do age-restricted actions (NSFW channels/guilds/commands) // TODO: depending on age
+
+	@Column({ select: false })
+	mfa_enabled: boolean; // if multi factor authentication is enabled
+
+	@Column({ select: false, nullable: true })
+	totp_secret?: string;
+
+	@Column({ nullable: true, select: false })
+	totp_last_ticket?: string;
+
+	@Column()
+	created_at: Date = new Date(); // registration date
+
+	@Column({ nullable: true })
+	premium_since: Date = new Date(); // premium date
+
+	@Column({ select: false })
+	verified: boolean = Config.get().defaults.user.verified; // if the user is offically verified
+
+	@Column()
+	disabled: boolean = false; // if the account is disabled
+
+	@Column()
+	deleted: boolean = false; // if the user was deleted
+
+	@Column({ nullable: true, select: false })
+	email?: string; // email of the user
+
+	@Column()
+	flags: string = "0"; // UserFlags // TODO: generate
+
+	@Column()
+	public_flags: number = 0;
+
+	@Column({ type: "bigint" })
+	rights: string = Config.get().register.defaultRights; // Rights
+
+	@OneToMany(() => Session, (session: Session) => session.user)
+	sessions: Session[];
+
+	@JoinColumn({ name: "relationship_ids" })
+	@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",
+	})
+	connected_accounts: ConnectedAccount[];
+
+	@Column({ type: "simple-json", select: false })
+	data: {
+		valid_tokens_since: Date; // all tokens with a previous issue date are invalid
+		hash?: string; // hash of the password, salt is saved in password (bcrypt)
+	};
+
+	@Column({ type: "simple-array", select: false })
+	fingerprints: string[] = []; // array of fingerprints -> used to prevent multiple accounts
+
+	
+	@OneToOne(()=> UserSettings, {
+		cascade: true,
+		orphanedRowAction: "delete",
+		eager: false
+	})
+	@JoinColumn()
+	settings: UserSettings;
+
+	// workaround to prevent fossord-unaware clients from deleting settings not used by them
+	@Column({ type: "simple-json", select: false })
+	extended_settings: string = "{}";
+
+	@Column({ type: "simple-json" })
+	notes: { [key: string]: string } = {}; //key is ID of user
+
+	async save(): Promise<any> {
+		if(!this.settings) this.settings = new UserSettings();
+		this.settings.id = this.id;
+		//await this.settings.save();
+		return super.save();
+	}
+
+	toPublicUser() {
+		const user: any = {};
+		PublicUserProjection.forEach((x) => {
+			user[x] = this[x];
+		});
+		return user as PublicUser;
+	}
+
+	static async getPublicUser(user_id: string, opts?: FindOneOptions<User>) {
+		return await User.findOneOrFail({
+			where: { id: user_id },
+			select: [...PublicUserProjection, ...((opts?.select as FindOptionsSelectByString<User>) || [])],
+			...opts,
+		});
+	}
+
+	public 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 discriminator = highestDiscriminator + 1;
+			if (discriminator >= 10000) {
+				return undefined;
+			}
+
+			return discriminator.toString().padStart(4, "0");
+		} else {
+			// discriminator will be randomly generated
+
+			// 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"] });
+				if (!exists) return discriminator;
+			}
+
+			return undefined;
+		}
+	}
+
+	static async register({
+		email,
+		username,
+		password,
+		date_of_birth,
+		req,
+	}: {
+		username: string;
+		password?: string;
+		email?: string;
+		date_of_birth?: Date; // "2000-04-03"
+		req?: any;
+	}) {
+		// trim special uf8 control characters -> Backspace, Newline, ...
+		username = trimSpecial(username);
+
+		const discriminator = await User.generateDiscriminator(username);
+		if (!discriminator) {
+			// We've failed to generate a valid and unused discriminator
+			throw FieldErrors({
+				username: {
+					code: "USERNAME_TOO_MANY_USERS",
+					message: req?.t("auth:register.USERNAME_TOO_MANY_USERS"),
+				},
+			});
+		}
+
+		// 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 user = OrmUtils.mergeDeep(new User(), {
+			//required:
+			username: username,
+			discriminator,
+			id: Snowflake.generate(),
+			email: email,
+			data: {
+				hash: password,
+				valid_tokens_since: new Date(),
+			},
+			settings: { ...new UserSettings(), locale: language }
+		});
+
+		await user.save();
+		await user.settings.save();
+
+		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) => {});
+				}
+			}
+		});
+
+		return user;
+	}
+}
+
+export const CUSTOM_USER_FLAG_OFFSET = BigInt(1) << BigInt(32);
+
+export class UserFlags extends BitField {
+	static FLAGS = {
+		DISCORD_EMPLOYEE: BigInt(1) << BigInt(0),
+		PARTNERED_SERVER_OWNER: BigInt(1) << BigInt(1),
+		HYPESQUAD_EVENTS: BigInt(1) << BigInt(2),
+		BUGHUNTER_LEVEL_1: BigInt(1) << BigInt(3),
+		MFA_SMS: BigInt(1) << BigInt(4),
+		PREMIUM_PROMO_DISMISSED: BigInt(1) << BigInt(5),
+		HOUSE_BRAVERY: BigInt(1) << BigInt(6),
+		HOUSE_BRILLIANCE: BigInt(1) << BigInt(7),
+		HOUSE_BALANCE: BigInt(1) << BigInt(8),
+		EARLY_SUPPORTER: BigInt(1) << BigInt(9),
+		TEAM_USER: BigInt(1) << BigInt(10),
+		TRUST_AND_SAFETY: BigInt(1) << BigInt(11),
+		SYSTEM: BigInt(1) << BigInt(12),
+		HAS_UNREAD_URGENT_MESSAGES: BigInt(1) << BigInt(13),
+		BUGHUNTER_LEVEL_2: BigInt(1) << BigInt(14),
+		UNDERAGE_DELETED: BigInt(1) << BigInt(15),
+		VERIFIED_BOT: BigInt(1) << BigInt(16),
+		EARLY_VERIFIED_BOT_DEVELOPER: BigInt(1) << BigInt(17),
+		CERTIFIED_MODERATOR: BigInt(1) << BigInt(18),
+		BOT_HTTP_INTERACTIONS: BigInt(1) << BigInt(19),
+	};
+}
diff --git a/src/util/entities/UserGroup.ts b/src/util/entities/UserGroup.ts
new file mode 100644
index 00000000..709b9d0b
--- /dev/null
+++ b/src/util/entities/UserGroup.ts
@@ -0,0 +1,37 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+
+import { BaseClass } from "./BaseClass";
+import { Guild } from "./Guild";
+import { User } from "./User";
+
+@Entity("groups")
+export class UserGroup extends BaseClass {
+	@Column()
+	color: number;
+
+	@Column()
+	hoist: boolean;
+	
+	@JoinColumn({ name: "controller", referencedColumnName: "id" })
+	@ManyToOne(() => User)
+	controller?: User;
+	 
+	@Column()
+	mentionable_by?: string;
+
+	@Column()
+	name: string;
+
+	@Column()
+	rights: string;
+
+	@Column({ nullable: true })
+	icon: string;
+	
+	@Column({ nullable: true })
+	parent?: string;
+	
+	@Column({ type: "simple-array", nullable: true})
+	associciations: string[];
+
+}
diff --git a/src/util/entities/UserSettings.ts b/src/util/entities/UserSettings.ts
new file mode 100644
index 00000000..ef6f95af
--- /dev/null
+++ b/src/util/entities/UserSettings.ts
@@ -0,0 +1,119 @@
+import { Column, Entity, JoinColumn } from "typeorm";
+import { BaseClassWithoutId, PrimaryIdColumn } from ".";
+
+@Entity("user_settings")
+export class UserSettings extends BaseClassWithoutId {
+    @PrimaryIdColumn()
+	id: string;
+
+	@Column({ nullable: true })
+    afk_timeout: number = 3600;
+
+	@Column({ nullable: true })
+    allow_accessibility_detection: boolean = true;
+	
+    @Column({ nullable: true })
+    animate_emoji: boolean = true;
+	
+    @Column({ nullable: true })
+    animate_stickers: number = 0;
+	
+    @Column({ nullable: true })
+    contact_sync_enabled: boolean = false;
+	
+    @Column({ nullable: true })
+    convert_emoticons: boolean = false;
+	
+    @Column({ nullable: true, type: "simple-json" })
+    custom_status: CustomStatus | null = null;
+	
+    @Column({ nullable: true })
+    default_guilds_restricted: boolean = false;
+	
+    @Column({ nullable: true })
+    detect_platform_accounts: boolean = false;
+	
+    @Column({ nullable: true })
+    developer_mode: boolean = true;
+	
+    @Column({ nullable: true })
+    disable_games_tab: boolean = true;
+	
+    @Column({ nullable: true })
+    enable_tts_command: boolean = false;
+	
+    @Column({ nullable: true })
+    explicit_content_filter: number = 0;
+	
+    @Column({ nullable: true, type: "simple-json" })
+    friend_source_flags: FriendSourceFlags = { all: true };
+	
+    @Column({ nullable: true })
+    gateway_connected: boolean = false;
+	
+    @Column({ nullable: true })
+    gif_auto_play: boolean = false;
+	
+    @Column({ nullable: true, type: "simple-json" })
+    guild_folders: GuildFolder[] = []; // every top guild is displayed as a "folder"
+	
+    @Column({ nullable: true, type: "simple-json" })
+    guild_positions: string[] = []; // guild ids ordered by position
+	
+    @Column({ nullable: true })
+    inline_attachment_media: boolean = true;
+	
+    @Column({ nullable: true })
+    inline_embed_media: boolean = true;
+	
+    @Column({ nullable: true })
+    locale: string = "en-US"; // en_US
+	
+    @Column({ nullable: true })
+    message_display_compact: boolean = false;
+	
+    @Column({ nullable: true })
+    native_phone_integration_enabled: boolean = true;
+	
+    @Column({ nullable: true })
+    render_embeds: boolean = true;
+	
+    @Column({ nullable: true })
+    render_reactions: boolean = true;
+	
+    @Column({ nullable: true, type: "simple-json" })
+    restricted_guilds: string[] = [];
+	
+    @Column({ nullable: true })
+    show_current_game: boolean = true;
+	
+    @Column({ nullable: true })
+    status: "online" | "offline" | "dnd" | "idle" | "invisible" = "online";
+	
+    @Column({ nullable: true })
+    stream_notifications_enabled: boolean = false;
+	
+    @Column({ nullable: true })
+    theme: "dark" | "white" = "dark"; // dark
+	
+    @Column({ nullable: true })
+    timezone_offset: number = 0; // e.g -60
+}
+
+interface CustomStatus {
+    emoji_id?: string;
+    emoji_name?: string;
+    expires_at?: number;
+    text?: string;
+}
+
+interface GuildFolder {
+    color: number;
+    guild_ids: string[];
+    id: number;
+    name: string;
+}
+
+interface FriendSourceFlags { 
+    all: boolean
+}
\ No newline at end of file
diff --git a/src/util/entities/VoiceState.ts b/src/util/entities/VoiceState.ts
new file mode 100644
index 00000000..75748a01
--- /dev/null
+++ b/src/util/entities/VoiceState.ts
@@ -0,0 +1,77 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { Channel } from "./Channel";
+import { Guild } from "./Guild";
+import { User } from "./User";
+import { Member } from "./Member";
+
+//https://gist.github.com/vassjozsef/e482c65df6ee1facaace8b3c9ff66145#file-voice_state-ex
+@Entity("voice_states")
+export class VoiceState extends BaseClass {
+	@Column({ nullable: true })
+	@RelationId((voice_state: VoiceState) => voice_state.guild)
+	guild_id: string;
+
+	@JoinColumn({ name: "guild_id" })
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
+	guild?: Guild;
+
+	@Column({ nullable: true })
+	@RelationId((voice_state: VoiceState) => voice_state.channel)
+	channel_id: string;
+
+	@JoinColumn({ name: "channel_id" })
+	@ManyToOne(() => Channel, {
+		onDelete: "CASCADE",
+	})
+	channel: Channel;
+
+	@Column({ nullable: true })
+	@RelationId((voice_state: VoiceState) => voice_state.user)
+	user_id: string;
+
+	@JoinColumn({ name: "user_id" })
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
+	user: User;
+
+	// @JoinColumn([{ name: "user_id", referencedColumnName: "id" },{ name: "guild_id", referencedColumnName: "guild_id" }])
+	// @ManyToOne(() => Member, {
+	// 	onDelete: "CASCADE",
+	// })
+	//TODO find a way to make it work without breaking Guild.voice_states
+	member: Member;
+
+	@Column()
+	session_id: string;
+
+	@Column({ nullable: true })
+	token: string;
+
+	@Column()
+	deaf: boolean;
+
+	@Column()
+	mute: boolean;
+
+	@Column()
+	self_deaf: boolean;
+
+	@Column()
+	self_mute: boolean;
+
+	@Column({ nullable: true })
+	self_stream?: boolean;
+
+	@Column()
+	self_video: boolean;
+
+	@Column()
+	suppress: boolean; // whether this user is muted by the current user
+
+	@Column({ nullable: true, default: null })
+	request_to_speak_timestamp?: Date;
+}
diff --git a/src/util/entities/Webhook.ts b/src/util/entities/Webhook.ts
new file mode 100644
index 00000000..89538417
--- /dev/null
+++ b/src/util/entities/Webhook.ts
@@ -0,0 +1,76 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { Application } from "./Application";
+import { BaseClass } from "./BaseClass";
+import { Channel } from "./Channel";
+import { Guild } from "./Guild";
+import { User } from "./User";
+
+export enum WebhookType {
+	Incoming = 1,
+	ChannelFollower = 2,
+}
+
+@Entity("webhooks")
+export class Webhook extends BaseClass {
+	@Column({ type: "int" })
+	type: WebhookType;
+
+	@Column({ nullable: true })
+	name?: string;
+
+	@Column({ nullable: true })
+	avatar?: string;
+
+	@Column({ nullable: true })
+	token?: string;
+
+	@Column({ nullable: true })
+	@RelationId((webhook: Webhook) => webhook.guild)
+	guild_id: string;
+
+	@JoinColumn({ name: "guild_id" })
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
+	guild: Guild;
+
+	@Column({ nullable: true })
+	@RelationId((webhook: Webhook) => webhook.channel)
+	channel_id: string;
+
+	@JoinColumn({ name: "channel_id" })
+	@ManyToOne(() => Channel, {
+		onDelete: "CASCADE",
+	})
+	channel: Channel;
+
+	@Column({ nullable: true })
+	@RelationId((webhook: Webhook) => webhook.application)
+	application_id: string;
+
+	@JoinColumn({ name: "application_id" })
+	@ManyToOne(() => Application, {
+		onDelete: "CASCADE",
+	})
+	application: Application;
+
+	@Column({ nullable: true })
+	@RelationId((webhook: Webhook) => webhook.user)
+	user_id: string;
+
+	@JoinColumn({ name: "user_id" })
+	@ManyToOne(() => User, {
+		onDelete: "CASCADE",
+	})
+	user: User;
+
+	@Column({ nullable: true })
+	@RelationId((webhook: Webhook) => webhook.guild)
+	source_guild_id: string;
+
+	@JoinColumn({ name: "source_guild_id" })
+	@ManyToOne(() => Guild, {
+		onDelete: "CASCADE",
+	})
+	source_guild: Guild;
+}
diff --git a/src/util/entities/index.ts b/src/util/entities/index.ts
new file mode 100644
index 00000000..c6f12022
--- /dev/null
+++ b/src/util/entities/index.ts
@@ -0,0 +1,33 @@
+export * from "./Application";
+export * from "./Attachment";
+export * from "./AuditLog";
+export * from "./Ban";
+export * from "./BaseClass";
+export * from "./Categories";
+export * from "./Channel";
+export * from "./Config";
+export * from "./ConnectedAccount";
+export * from "./Emoji";
+export * from "./Guild";
+export * from "./Invite";
+export * from "./Member";
+export * from "./Message";
+export * from "./Migration";
+export * from "./RateLimit";
+export * from "./ReadState";
+export * from "./Recipient";
+export * from "./Relationship";
+export * from "./Role";
+export * from "./Session";
+export * from "./Sticker";
+export * from "./StickerPack";
+export * from "./Team";
+export * from "./TeamMember";
+export * from "./Template";
+export * from "./User";
+export * from "./VoiceState";
+export * from "./Webhook";
+export * from "./ClientRelease";
+export * from "./BackupCodes";
+export * from "./Note";
+export * from "./UserSettings";