summary refs log tree commit diff
path: root/src/util/entities
diff options
context:
space:
mode:
authorMadeline <46743919+MaddyUnderStars@users.noreply.github.com>2022-09-25 18:24:21 +1000
committerMadeline <46743919+MaddyUnderStars@users.noreply.github.com>2022-09-25 23:35:18 +1000
commitf44f5d7ac2d24ff836c2e1d4b2fa58da04b13052 (patch)
treea6655c41bb3db79c30fd876b06ee60fe9cf70c9b /src/util/entities
parentAllow edited_timestamp to passthrough in handleMessage (diff)
downloadserver-f44f5d7ac2d24ff836c2e1d4b2fa58da04b13052.tar.xz
Refactor to mono-repo + upgrade packages
Diffstat (limited to 'src/util/entities')
-rw-r--r--src/util/entities/Application.ts109
-rw-r--r--src/util/entities/Attachment.ts43
-rw-r--r--src/util/entities/AuditLog.ts194
-rw-r--r--src/util/entities/BackupCodes.ts35
-rw-r--r--src/util/entities/Ban.ts41
-rw-r--r--src/util/entities/BaseClass.ts52
-rw-r--r--src/util/entities/Categories.ts33
-rw-r--r--src/util/entities/Channel.ts390
-rw-r--r--src/util/entities/ClientRelease.ts26
-rw-r--r--src/util/entities/Config.ts416
-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/Guild.ts363
-rw-r--r--src/util/entities/Invite.ts85
-rw-r--r--src/util/entities/Member.ts430
-rw-r--r--src/util/entities/Message.ts296
-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.ts430
-rw-r--r--src/util/entities/VoiceState.ts77
-rw-r--r--src/util/entities/Webhook.ts76
-rw-r--r--src/util/entities/index.ts32
34 files changed, 3740 insertions, 0 deletions
diff --git a/src/util/entities/Application.ts b/src/util/entities/Application.ts
new file mode 100644
index 00000000..fab3d93f
--- /dev/null
+++ b/src/util/entities/Application.ts
@@ -0,0 +1,109 @@
+import { Column, Entity, JoinColumn, ManyToOne, 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()
+	description: string;
+
+	@Column({ type: "simple-array", nullable: true })
+	rpc_origins?: string[];
+
+	@Column()
+	bot_public: boolean;
+
+	@Column()
+	bot_require_code_grant: boolean;
+
+	@Column({ nullable: true })
+	terms_of_service_url?: string;
+
+	@Column({ nullable: true })
+	privacy_policy_url?: string;
+
+	@JoinColumn({ name: "owner_id" })
+	@ManyToOne(() => User)
+	owner?: User;
+
+	@Column({ nullable: true })
+	summary?: string;
+
+	@Column()
+	verify_key: string;
+
+	@JoinColumn({ name: "team_id" })
+	@ManyToOne(() => Team, {
+		onDelete: "CASCADE",
+	})
+	team?: Team;
+
+	@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
+
+	@Column({ nullable: true })
+	cover_image?: string; // the application's default rich presence invite cover image hash
+
+	@Column()
+	flags: string; // the application's public flags
+}
+
+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..d532a39a
--- /dev/null
+++ b/src/util/entities/BackupCodes.ts
@@ -0,0 +1,35 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { User } from "./User";
+import crypto from "crypto";
+
+@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;
+}
+
+export function generateMfaBackupCodes(user_id: string) {
+	let backup_codes: BackupCode[] = [];
+	for (let i = 0; i < 10; i++) {
+		const code = BackupCode.create({
+			user: { id: user_id },
+			code: crypto.randomBytes(4).toString("hex"),	// 8 characters
+			consumed: false,
+			expired: false,
+		});
+		backup_codes.push(code);
+	}
+
+	return backup_codes;
+}
\ 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..d5a7c2bf
--- /dev/null
+++ b/src/util/entities/BaseClass.ts
@@ -0,0 +1,52 @@
+import "reflect-metadata";
+import { BaseEntity, BeforeInsert, BeforeUpdate, FindOptionsWhere, ObjectIdColumn, PrimaryColumn } from "typeorm";
+import { Snowflake } from "../util/Snowflake";
+import "missing-native-js-functions";
+import { getDatabase } from "..";
+import { OrmUtils } from "@fosscord/util";
+
+export class BaseClassWithoutId extends BaseEntity {
+	private get construct(): any {
+		return this.constructor;
+	}
+
+	private get metadata() {
+		return getDatabase()?.getMetadata(this.construct);
+	}
+
+	assign(props: any) {
+		OrmUtils.mergeDeep(this, props);
+		return this;
+	}
+
+	toJSON(): any {
+		return Object.fromEntries(
+			this.metadata!.columns // @ts-ignore
+				.map((x) => [x.propertyName, this[x.propertyName]]) // @ts-ignore
+				.concat(this.metadata.relations.map((x) => [x.propertyName, this[x.propertyName]]))
+		);
+	}
+
+	static increment<T extends BaseClass>(conditions: FindOptionsWhere<T>, propertyPath: string, value: number | string) {
+		const repository = this.getRepository();
+		return repository.increment(conditions, propertyPath, value);
+	}
+
+	static decrement<T extends BaseClass>(conditions: FindOptionsWhere<T>, propertyPath: string, value: number | string) {
+		const repository = this.getRepository();
+		return repository.decrement(conditions, propertyPath, value);
+	}
+}
+
+export const PrimaryIdColumn = process.env.DATABASE?.startsWith("mongodb") ? ObjectIdColumn : PrimaryColumn;
+
+export class BaseClass extends BaseClassWithoutId {
+	@PrimaryIdColumn()
+	id: string;
+
+	@BeforeUpdate()
+	@BeforeInsert()
+	do_validate() {
+		if (!this.id) this.id = Snowflake.generate();
+	}
+}
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..577b627e
--- /dev/null
+++ b/src/util/entities/Channel.ts
@@ -0,0 +1,390 @@
+import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";

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

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

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

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

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

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

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

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

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

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

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

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

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

+

+export enum ChannelType {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+	DIRECTORY = 14, // guild directory listing channel

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

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

+	KANBAN = 34, // confluence like kanban board

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

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

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

+}

+

+@Entity("channels")

+export class Channel extends BaseClass {

+	@Column()

+	created_at: Date;

+

+	@Column({ nullable: true })

+	name?: string;

+

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

+	icon?: string | null;

+

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

+	type: ChannelType;

+

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

+		cascade: true,

+		orphanedRowAction: "delete",

+	})

+	recipients?: Recipient[];

+

+	@Column({ nullable: true })

+	last_message_id?: string;

+

+	@Column({ nullable: true })

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

+	guild_id?: string;

+

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

+	@ManyToOne(() => Guild, {

+		onDelete: "CASCADE",

+	})

+	guild: Guild;

+

+	@Column({ nullable: true })

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

+	parent_id: string;

+

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

+	@ManyToOne(() => Channel)

+	parent?: Channel;

+

+	// for group DMs and owned custom channel types

+	@Column({ nullable: true })

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

+	owner_id?: string;

+

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

+	@ManyToOne(() => User)

+	owner: User;

+

+	@Column({ nullable: true })

+	last_pin_timestamp?: number;

+

+	@Column({ nullable: true })

+	default_auto_archive_duration?: number;

+

+	@Column({ nullable: true })

+	position?: number;

+

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

+	permission_overwrites?: ChannelPermissionOverwrite[];

+

+	@Column({ nullable: true })

+	video_quality_mode?: number;

+

+	@Column({ nullable: true })

+	bitrate?: number;

+

+	@Column({ nullable: true })

+	user_limit?: number;

+

+	@Column()

+	nsfw: boolean = false;

+

+	@Column({ nullable: true })

+	rate_limit_per_user?: number;

+

+	@Column({ nullable: true })

+	topic?: string;

+

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

+		cascade: true,

+		orphanedRowAction: "delete",

+	})

+	invites?: Invite[];

+

+	@Column({ nullable: true })

+	retention_policy_id?: string;

+

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

+		cascade: true,

+		orphanedRowAction: "delete",

+	})

+	messages?: Message[];

+

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

+		cascade: true,

+		orphanedRowAction: "delete",

+	})

+	voice_states?: VoiceState[];

+

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

+		cascade: true,

+		orphanedRowAction: "delete",

+	})

+	read_states?: ReadState[];

+

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

+		cascade: true,

+		orphanedRowAction: "delete",

+	})

+	webhooks?: Webhook[];

+

+	// TODO: DM channel

+	static async createChannel(

+		channel: Partial<Channel>,

+		user_id: string = "0",

+		opts?: {

+			keepId?: boolean;

+			skipExistsCheck?: boolean;

+			skipPermissionCheck?: boolean;

+			skipEventEmit?: boolean;

+			skipNameChecks?: boolean;

+		}

+	) {

+		if (!opts?.skipPermissionCheck) {

+			// Always check if user has permission first

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

+			permissions.hasThrow("MANAGE_CHANNELS");

+		}

+

+		if (!opts?.skipNameChecks) {

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

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

+				for (var character of InvisibleCharacters)

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

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

+

+				// Categories skip these checks on discord.com

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

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

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

+

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

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

+

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

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

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

+				}

+				else

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

+			}

+

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

+				if (!channel.name)

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

+			}

+		}

+

+		switch (channel.type) {

+			case ChannelType.GUILD_TEXT:

+			case ChannelType.GUILD_NEWS:

+			case ChannelType.GUILD_VOICE:

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

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

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

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

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

+				}

+				break;

+			case ChannelType.GUILD_CATEGORY:

+			case ChannelType.UNHANDLED:

+				break;

+			case ChannelType.DM:

+			case ChannelType.GROUP_DM:

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

+			case ChannelType.GUILD_STORE:

+			default:

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

+		}

+

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

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

+

+		channel = {

+			...channel,

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

+			created_at: new Date(),

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

+		};

+

+		await Promise.all([

+			Channel.create(channel).save(),

+			!opts?.skipEventEmit

+				? emitEvent({

+					event: "CHANNEL_CREATE",

+					data: channel,

+					guild_id: channel.guild_id,

+				} as ChannelCreateEvent)

+				: Promise.resolve(),

+		]);

+

+		return channel;

+	}

+

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

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

+		//@ts-ignore	some typeorm typescript issue

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

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

+					}

+				}

+			}

+		}

+

+		if (channel == null) {

+			name = trimSpecial(name);

+

+			channel = await Channel.create({

+				name,

+				type,

+				owner_id: undefined,

+				created_at: new Date(),

+				last_message_id: undefined,

+				recipients: channelRecipients.map(

+					(x) =>

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

+				),

+				nsfw: false,

+			}).save();

+		}

+

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

+

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

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

+				await emitEvent({

+					event: "CHANNEL_CREATE",

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

+					user_id: recipient.user_id,

+				});

+			}

+		} else {

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

+		}

+

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

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

+	}

+

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

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

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

+

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

+			await Channel.deleteChannel(channel);

+			await emitEvent({

+				event: "CHANNEL_DELETE",

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

+				user_id: user_id,

+			});

+			return;

+		}

+

+		await emitEvent({

+			event: "CHANNEL_DELETE",

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

+			user_id: user_id,

+		});

+

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

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

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

+			await emitEvent({

+				event: "CHANNEL_UPDATE",

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

+				channel_id: channel.id,

+			});

+		}

+

+		await channel.save();

+

+		await emitEvent({

+			event: "CHANNEL_RECIPIENT_REMOVE",

+			data: {

+				channel_id: channel.id,

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

+			},

+			channel_id: channel.id,

+		} as ChannelRecipientRemoveEvent);

+	}

+

+	static async deleteChannel(channel: Channel) {

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

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

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

+	}

+

+	isDm() {

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

+	}

+

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

+	isWritable() {

+		const disallowedChannelTypes = [

+			ChannelType.GUILD_CATEGORY,

+			ChannelType.GUILD_STAGE_VOICE,

+			ChannelType.VOICELESS_WHITEBOARD,

+		];

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

+	}

+}

+

+export interface ChannelPermissionOverwrite {

+	allow: string;

+	deny: string;

+	id: string;

+	type: ChannelPermissionOverwriteType;

+}

+

+export enum ChannelPermissionOverwriteType {

+	role = 0,

+	member = 1,

+	group = 2,

+}

diff --git a/src/util/entities/ClientRelease.ts b/src/util/entities/ClientRelease.ts
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..9aabc1a8
--- /dev/null
+++ b/src/util/entities/Config.ts
@@ -0,0 +1,416 @@
+import { Column, Entity } from "typeorm";
+import { BaseClassWithoutId, PrimaryIdColumn } from "./BaseClass";
+import crypto from "crypto";
+import { Snowflake } from "../util/Snowflake";
+import { SessionsReplace } from "..";
+import { hostname } from "os";
+import { Rights } from "../util/Rights";
+
+@Entity("config")
+export class ConfigEntity extends BaseClassWithoutId {
+	@PrimaryIdColumn()
+	key: string;
+
+	@Column({ type: "simple-json", nullable: true })
+	value: number | boolean | null | string | undefined;
+}
+
+export interface RateLimitOptions {
+	bot?: number;
+	count: number;
+	window: number;
+	onyIp?: boolean;
+}
+
+export interface Region {
+	id: string;
+	name: string;
+	endpoint: string;
+	location?: {
+		latitude: number;
+		longitude: number;
+	};
+	vip: boolean;
+	custom: boolean;
+	deprecated: boolean;
+}
+
+export interface KafkaBroker {
+	ip: string;
+	port: number;
+}
+
+export interface ConfigValue {
+	gateway: {
+		endpointClient: string | null;
+		endpointPrivate: string | null;
+		endpointPublic: string | null;
+	};
+	cdn: {
+		endpointClient: string | null;
+		endpointPublic: string | null;
+		endpointPrivate: string | null;
+		resizeHeightMax: number | null;
+		resizeWidthMax: number | null;
+	};
+	api: {
+		defaultVersion: string;
+		activeVersions: string[];
+		useFosscordEnhancements: boolean;
+	};
+	general: {
+		instanceName: string;
+		instanceDescription: string | null;
+		frontPage: string | null;
+		tosPage: string | null;
+		correspondenceEmail: string | null;
+		correspondenceUserID: string | null;
+		image: string | null;
+		instanceId: string;
+	};
+	limits: {
+		user: {
+			maxGuilds: number;
+			maxUsername: number;
+			maxFriends: number;
+		};
+		guild: {
+			maxRoles: number;
+			maxEmojis: number;
+			maxMembers: number;
+			maxChannels: number;
+			maxChannelsInCategory: number;
+			hideOfflineMember: number;
+		};
+		message: {
+			maxCharacters: number;
+			maxTTSCharacters: number;
+			maxReactions: number;
+			maxAttachmentSize: number;
+			maxBulkDelete: number;
+			maxEmbedDownloadSize: number;
+		};
+		channel: {
+			maxPins: number;
+			maxTopic: number;
+			maxWebhooks: number;
+		};
+		rate: {
+			disabled: boolean;
+			ip: Omit<RateLimitOptions, "bot_count">;
+			global: RateLimitOptions;
+			error: RateLimitOptions;
+			routes: {
+				guild: RateLimitOptions;
+				webhook: RateLimitOptions;
+				channel: RateLimitOptions;
+				auth: {
+					login: RateLimitOptions;
+					register: RateLimitOptions;
+				};
+				// TODO: rate limit configuration for all routes
+			};
+		};
+	};
+	security: {
+		autoUpdate: boolean | number;
+		requestSignature: string;
+		jwtSecret: string;
+		forwadedFor: string | null; // header to get the real user ip address
+		captcha: {
+			enabled: boolean;
+			service: "recaptcha" | "hcaptcha" | null; // TODO: hcaptcha, custom
+			sitekey: string | null;
+			secret: string | null;
+		};
+		ipdataApiKey: string | null;
+		defaultRights: string;
+	};
+	login: {
+		requireCaptcha: boolean;
+	};
+	register: {
+		email: {
+			required: boolean;
+			allowlist: boolean;
+			blocklist: boolean;
+			domains: string[];
+		};
+		dateOfBirth: {
+			required: boolean;
+			minimum: number; // in years
+		};
+		disabled: boolean;
+		requireCaptcha: boolean;
+		requireInvite: boolean;
+		guestsRequireInvite: boolean;
+		allowNewRegistration: boolean;
+		allowMultipleAccounts: boolean;
+		blockProxies: boolean;
+		password: {
+			required: boolean;
+			minLength: number;
+			minNumbers: number;
+			minUpperCase: number;
+			minSymbols: number;
+		};
+		incrementingDiscriminators: boolean; // random otherwise
+	};
+	regions: {
+		default: string;
+		useDefaultAsOptimal: boolean;
+		available: Region[];
+	};
+	guild: {
+		discovery: {
+			showAllGuilds: boolean;
+			useRecommendation: boolean; // TODO: Recommendation, privacy concern?
+			offset: number;
+			limit: number;
+		};
+		autoJoin: {
+			enabled: boolean;
+			guilds: string[];
+			canLeave: boolean;
+		};
+		defaultFeatures: string[];
+	};
+	gif: {
+		enabled: boolean;
+		provider: "tenor"; // more coming soon
+		apiKey?: string;
+	};
+	rabbitmq: {
+		host: string | null;
+	};
+	kafka: {
+		brokers: KafkaBroker[] | null;
+	};
+	templates: {
+		enabled: Boolean;
+		allowTemplateCreation: Boolean;
+		allowDiscordTemplates: Boolean;
+		allowRaws: Boolean;
+	},
+	client: {
+		useTestClient: Boolean;
+		releases: {
+			useLocalRelease: Boolean; //TODO
+			upstreamVersion: string;
+		};
+	},
+	metrics: {
+		timeout: number;
+	},
+	sentry: {
+		enabled: boolean;
+		endpoint: string;
+		traceSampleRate: number;
+		environment: string;
+	};
+}
+
+export const DefaultConfigOptions: ConfigValue = {
+	gateway: {
+		endpointClient: null,
+		endpointPrivate: null,
+		endpointPublic: null,
+	},
+	cdn: {
+		endpointClient: null,
+		endpointPrivate: null,
+		endpointPublic: null,
+		resizeHeightMax: 1000,
+		resizeWidthMax: 1000,
+	},
+	api: {
+		defaultVersion: "9",
+		activeVersions: ["6", "7", "8", "9"],
+		useFosscordEnhancements: true,
+	},
+	general: {
+		instanceName: "Fosscord Instance",
+		instanceDescription: "This is a Fosscord instance made in pre-release days",
+		frontPage: null,
+		tosPage: null,
+		correspondenceEmail: "noreply@localhost.local",
+		correspondenceUserID: null,
+		image: null,
+		instanceId: Snowflake.generate(),
+	},
+	limits: {
+		user: {
+			maxGuilds: 1048576,
+			maxUsername: 127,
+			maxFriends: 5000,
+		},
+		guild: {
+			maxRoles: 1000,
+			maxEmojis: 2000,
+			maxMembers: 25000000,
+			maxChannels: 65535,
+			maxChannelsInCategory: 65535,
+			hideOfflineMember: 3,
+		},
+		message: {
+			maxCharacters: 1048576,
+			maxTTSCharacters: 160,
+			maxReactions: 2048,
+			maxAttachmentSize: 1024 * 1024 * 1024,
+			maxEmbedDownloadSize: 1024 * 1024 * 5,
+			maxBulkDelete: 1000,
+		},
+		channel: {
+			maxPins: 500,
+			maxTopic: 1024,
+			maxWebhooks: 100,
+		},
+		rate: {
+			disabled: true,
+			ip: {
+				count: 500,
+				window: 5,
+			},
+			global: {
+				count: 250,
+				window: 5,
+			},
+			error: {
+				count: 10,
+				window: 5,
+			},
+			routes: {
+				guild: {
+					count: 5,
+					window: 5,
+				},
+				webhook: {
+					count: 10,
+					window: 5,
+				},
+				channel: {
+					count: 10,
+					window: 5,
+				},
+				auth: {
+					login: {
+						count: 5,
+						window: 60,
+					},
+					register: {
+						count: 2,
+						window: 60 * 60 * 12,
+					},
+				},
+			},
+		},
+	},
+	security: {
+		autoUpdate: true,
+		requestSignature: crypto.randomBytes(32).toString("base64"),
+		jwtSecret: crypto.randomBytes(256).toString("base64"),
+		forwadedFor: null,
+		// forwadedFor: "X-Forwarded-For" // nginx/reverse proxy
+		// forwadedFor: "CF-Connecting-IP" // cloudflare:
+		captcha: {
+			enabled: false,
+			service: null,
+			sitekey: null,
+			secret: null,
+		},
+		ipdataApiKey: "eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9",
+		defaultRights: "30644591655936",	// See util/scripts/rights.js
+	},
+	login: {
+		requireCaptcha: false,
+	},
+	register: {
+		email: {
+			required: false,
+			allowlist: false,
+			blocklist: true,
+			domains: [], // TODO: efficiently save domain blocklist in database
+			// domains: fs.readFileSync(__dirname + "/blockedEmailDomains.txt", { encoding: "utf8" }).split("\n"),
+		},
+		dateOfBirth: {
+			required: true,
+			minimum: 13,
+		},
+		disabled: false,
+		requireInvite: false,
+		guestsRequireInvite: true,
+		requireCaptcha: true,
+		allowNewRegistration: true,
+		allowMultipleAccounts: true,
+		blockProxies: true,
+		password: {
+			required: false,
+			minLength: 8,
+			minNumbers: 2,
+			minUpperCase: 2,
+			minSymbols: 0,
+		},
+		incrementingDiscriminators: false,
+	},
+	regions: {
+		default: "fosscord",
+		useDefaultAsOptimal: true,
+		available: [
+			{
+				id: "fosscord",
+				name: "Fosscord",
+				endpoint: "127.0.0.1:3004",
+				vip: false,
+				custom: false,
+				deprecated: false,
+			},
+		],
+	},
+	guild: {
+		discovery: {
+			showAllGuilds: false,
+			useRecommendation: false,
+			offset: 0,
+			limit: 24,
+		},
+		autoJoin: {
+			enabled: true,
+			canLeave: true,
+			guilds: [],
+		},
+		defaultFeatures: [],
+	},
+	gif: {
+		enabled: true,
+		provider: "tenor",
+		apiKey: "LIVDSRZULELA",
+	},
+	rabbitmq: {
+		host: null,
+	},
+	kafka: {
+		brokers: null,
+	},
+	templates: {
+		enabled: true,
+		allowTemplateCreation: true,
+		allowDiscordTemplates: true,
+		allowRaws: false
+	},
+	client: {
+		useTestClient: true,
+		releases: {
+			useLocalRelease: true,
+			upstreamVersion: "0.0.264"
+		}
+	},
+	metrics: {
+		timeout: 30000
+	},
+	sentry: {
+		enabled: false,
+		endpoint: "https://05e8e3d005f34b7d97e920ae5870a5e5@sentry.thearcanebrony.net/6",
+		traceSampleRate: 1.0,
+		environment: hostname()
+	}
+};
\ 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..b597b90a
--- /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 "lambert-server";
+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: string;
+
+	@Column({ nullable: true })
+	channel_id: string;
+
+	@Column()
+	encryption_permission_mask: number;
+
+	@Column({ type: "simple-array" })
+	allowed_algorithms: string[];
+
+	@Column()
+	current_algorithm: string;
+
+	@Column({ nullable: true })
+	used_since_message: string;
+
+}
diff --git a/src/util/entities/Guild.ts b/src/util/entities/Guild.ts
new file mode 100644
index 00000000..2ce7c213
--- /dev/null
+++ b/src/util/entities/Guild.ts
@@ -0,0 +1,363 @@
+import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, OneToMany, OneToOne, RelationId } from "typeorm";
+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;
+
+	// * 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;
+
+	@Column({ nullable: true })
+	description?: string;
+
+	@Column({ nullable: true })
+	discovery_splash?: string;
+
+	@Column({ nullable: true })
+	explicit_content_filter?: number;
+
+	@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?: string;	// TODO: this was number?
+
+	@Column({ nullable: true })
+	icon?: string;
+
+	@Column({ nullable: true })
+	large?: boolean;
+
+	@Column({ nullable: true })
+	max_members?: number; // e.g. default 100.000
+
+	@Column({ nullable: true })
+	max_presences?: number;
+
+	@Column({ nullable: true })
+	max_video_channel_users?: number; // ? default: 25, is this max 25 streaming or watching
+
+	@Column({ nullable: true })
+	member_count?: number;
+
+	@Column({ nullable: true })
+	presence_count?: number; // 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()
+	nsfw: boolean;
+	
+	// TODO: nested guilds
+	@Column({ nullable: true })
+	parent?: string;
+
+	// only for developer portal
+	permissions?: number;
+
+	static async createGuild(body: {
+		name?: string;
+		icon?: string | null;
+		owner_id?: string;
+		channels?: Partial<Channel>[];
+	}) {
+		const guild_id = Snowflake.generate();
+
+		const guild = await Guild.create({
+			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: Config.get().guild.defaultFeatures,
+			primary_category_id: undefined,
+			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
+		}).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
+		await Role.create({
+			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: undefined,
+			unicode_emoji: undefined
+		}).save();
+
+		if (!body.channels || !body.channels.length) body.channels = [{ id: "01", type: 0, name: "general", nsfw: false }];
+
+		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))) {
+			var id = ids.get(channel.id) || Snowflake.generate();
+
+			var parent_id = ids.get(channel.parent_id);
+
+			await Channel.createChannel({ ...channel, guild_id, id, parent_id }, body.owner_id, {
+				keepId: true,
+				skipExistsCheck: true,
+				skipPermissionCheck: true,
+				skipEventEmit: true,
+			});
+		}
+
+		return guild;
+	}
+}
diff --git a/src/util/entities/Invite.ts b/src/util/entities/Invite.ts
new file mode 100644
index 00000000..4f36f247
--- /dev/null
+++ b/src/util/entities/Invite.ts
@@ -0,0 +1,85 @@
+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";
+
+export const PublicInviteRelation = ["inviter", "guild", "channel"];
+
+@Entity("invites")
+export class Invite extends BaseClassWithoutId {
+	@PrimaryColumn()
+	code: string;
+
+	@Column()
+	temporary: boolean;
+
+	@Column()
+	uses: number;
+
+	@Column()
+	max_uses: number;
+
+	@Column()
+	max_age: number;
+
+	@Column()
+	created_at: 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)
+	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..d7bcefea
--- /dev/null
+++ b/src/util/entities/Member.ts
@@ -0,0 +1,430 @@
+import { PublicUser, User } from "./User";
+import { Message } from "./Message";
+import {
+	BeforeInsert,
+	BeforeUpdate,
+	Column,
+	Entity,
+	Index,
+	JoinColumn,
+	JoinTable,
+	ManyToMany,
+	ManyToOne,
+	PrimaryGeneratedColumn,
+	RelationId,
+} from "typeorm";
+import { Guild } from "./Guild";
+import { Config, emitEvent, BannedWords, FieldErrors } from "../util";
+import {
+	GuildCreateEvent,
+	GuildDeleteEvent,
+	GuildMemberAddEvent,
+	GuildMemberRemoveEvent,
+	GuildMemberUpdateEvent,
+	MessageCreateEvent,
+
+} from "../interfaces";
+import { HTTPError } from "lambert-server";
+import { Role } from "./Role";
+import { BaseClassWithoutId } from "./BaseClass";
+import { Ban, PublicGuildRelations } from ".";
+import { DiscordApiErrors } from "../util/Constants";
+
+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({ type: "bigint", nullable: true })
+	premium_since?: number;
+
+	@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;
+
+	@BeforeUpdate()
+	@BeforeInsert()
+	validate() {
+		if (this.nick) {
+			this.nick = this.nick.split("\n").join("");
+			this.nick = this.nick.split("\t").join("");
+			if (BannedWords.find(this.nick)) throw FieldErrors({ nick: { message: "Bad nickname", code: "INVALID_NICKNAME" } });
+		}
+	}
+
+	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"], where: { id: guild_id } });
+		if (guild.owner_id === user_id) throw new Error("The owner cannot be removed of the guild");
+		const member = await Member.findOneOrFail({ where: { id: user_id, guild_id }, relations: ["user"] });
+
+		// use promise all to execute all promises at the same time -> save time
+		return Promise.all([
+			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
+				//@ts-ignore
+				select: ["index", "roles.id"],	// TODO fix type
+			}),
+			Role.findOneOrFail({ where: { id: role_id, guild_id }, select: ["id"] }),
+		]);
+		member.roles.push(Role.create({ 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
+				//@ts-ignore
+				select: ["roles.id", "index"], // TODO: fix type
+			}),
+			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, "system_channel"],
+		});
+
+		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: (new Date()).getTime(),
+			deaf: false,
+			mute: false,
+			pending: false,
+		};
+
+		await Promise.all([
+			Member.create({
+				...member,
+				roles: [Role.create({ id: guild_id })],
+				// read_state: {},
+				settings: {
+					guild_id: null,
+					mute_config: null,
+					mute_scheduled_events: false,
+					flags: 0,
+					hide_muted_channels: false,
+					notify_highlights: 0,
+					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),
+		]);
+
+		if (guild.system_channel_id) {
+			// send welcome message
+			const message = Message.create({
+				type: 7,
+				guild_id: guild.id,
+				channel_id: guild.system_channel_id,
+				author: user,
+				timestamp: new Date(),
+				reactions: [],
+				attachments: [],
+				embeds: [],
+				sticker_items: [],
+				edited_timestamp: undefined,
+			});
+			await Promise.all([
+				message.save(),
+				emitEvent({ event: "MESSAGE_CREATE", channel_id: message.channel_id, data: message } as MessageCreateEvent)
+			]);
+		}
+	}
+}
+
+export interface ChannelOverride {
+	message_notifications: number;
+	mute_config: MuteConfig;
+	muted: boolean;
+	channel_id: string | null;
+}
+
+export interface UserGuildSettings {
+	// channel_overrides: {
+	// 	channel_id: string;
+	// 	message_notifications: number;
+	// 	mute_config: MuteConfig;
+	// 	muted: boolean;
+	// }[];
+
+	channel_overrides: {
+		[channel_id: string]: ChannelOverride;
+	} | null,
+	message_notifications: number;
+	mobile_push: boolean;
+	mute_config: MuteConfig | null;
+	muted: boolean;
+	suppress_everyone: boolean;
+	suppress_roles: boolean;
+	version: number;
+	guild_id: string | null;
+	flags: number;
+	mute_scheduled_events: boolean;
+	hide_muted_channels: boolean;
+	notify_highlights: 0;
+}
+
+export const DefaultUserGuildSettings: UserGuildSettings = {
+	channel_overrides: null,
+	message_notifications: 1,
+	flags: 0,
+	hide_muted_channels: false,
+	mobile_push: true,
+	mute_config: null,
+	mute_scheduled_events: false,
+	muted: false,
+	notify_highlights: 0,
+	suppress_everyone: false,
+	suppress_roles: false,
+	version: 453,	// ?
+	guild_id: null,
+};
+
+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..3a3dd5e4
--- /dev/null
+++ b/src/util/entities/Message.ts
@@ -0,0 +1,296 @@
+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 {
+	BeforeInsert,
+	BeforeUpdate,
+	Column,
+	CreateDateColumn,
+	Entity,
+	Index,
+	JoinColumn,
+	JoinTable,
+	ManyToMany,
+	ManyToOne,
+	OneToMany,
+	RelationId,
+} from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { Guild } from "./Guild";
+import { Webhook } from "./Webhook";
+import { Sticker } from "./Sticker";
+import { Attachment } from "./Attachment";
+import { BannedWords } from "../util";
+import { HTTPError } from "lambert-server";
+import { ValidatorConstraint } from "class-validator";
+
+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, type: process.env.PRODUCTION ? "longtext" : undefined })
+	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[];
+
+	@BeforeUpdate()
+	@BeforeInsert()
+	validate() {
+		if (this.content) {
+			if (BannedWords.find(this.content)) throw new HTTPError("Message was blocked by automatic moderation", 200000);
+		}
+	}
+}
+
+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..d87b835f
--- /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..84a8a674
--- /dev/null
+++ b/src/util/entities/User.ts
@@ -0,0 +1,430 @@
+import { BeforeInsert, BeforeUpdate, Column, Entity, FindOneOptions, JoinColumn, OneToMany } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { BitField } from "../util/BitField";
+import { Relationship } from "./Relationship";
+import { ConnectedAccount } from "./ConnectedAccount";
+import { Config, FieldErrors, Snowflake, trimSpecial, BannedWords, adjustEmail } from "..";
+import { Member, Session } 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,
+	purchased_flags,
+	premium_usage_flags,
+	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;
+}
+
+@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
+
+	@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; // if the user has desktop app installed
+
+	@Column({ select: false })
+	mobile: boolean; // if the user has mobile app installed
+
+	@Column()
+	premium: boolean; // if user bought individual premium
+
+	@Column()
+	premium_type: number; // individual premium level
+
+	@Column()
+	bot: boolean; // if user is bot
+
+	@Column()
+	bio: string; // short description of the user (max 190 chars -> should be configurable)
+
+	@Column()
+	system: boolean; // 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; // if the user can do age-restricted actions (NSFW channels/guilds/commands)
+
+	@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; // registration date
+
+	@Column({ nullable: true })
+	premium_since: Date; // premium date
+
+	@Column({ select: false })
+	verified: boolean; // if the user is offically verified
+
+	@Column()
+	disabled: boolean; // if the account is disabled
+
+	@Column()
+	deleted: boolean; // if the user was deleted
+
+	@Column({ nullable: true, select: false })
+	email?: string; // email of the user
+
+	@Column()
+	flags: string; // UserFlags
+
+	@Column()
+	public_flags: number;
+
+	@Column()
+	purchased_flags: number;
+
+	@Column()
+	premium_usage_flags: number;
+
+	@Column({ type: "bigint" })
+	rights: string; // 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
+
+	@Column({ type: "simple-json", select: false })
+	settings: UserSettings;
+
+	// workaround to prevent fossord-unaware clients from deleting settings not used by them
+	@Column({ type: "simple-json", select: false })
+	extended_settings: string;
+
+	@BeforeUpdate()
+	@BeforeInsert()
+	validate() {
+		this.email = adjustEmail(this.email);
+		if (!this.email) throw FieldErrors({ email: { message: "Invalid email", code: "EMAIL_INVALID" } });
+		if (!this.email.match(/([a-z\d.-]{3,})@([a-z\d.-]+).([a-z]{2,})/g)) throw FieldErrors({ email: { message: "Invalid email", code: "EMAIL_INVALID" } });
+
+		const discrim = Number(this.discriminator);
+		if (this.discriminator.length > 4) throw FieldErrors({ email: { message: "Discriminator cannot be more than 4 digits.", code: "DISCRIMINATOR_INVALID" } });
+		if (isNaN(discrim)) throw FieldErrors({ email: { message: "Discriminator must be a number.", code: "DISCRIMINATOR_INVALID" } });
+		if (discrim <= 0 || discrim >= 10000) throw FieldErrors({ email: { message: "Discriminator must be a number.", code: "DISCRIMINATOR_INVALID" } });
+		this.discriminator = discrim.toString().padStart(4, "0");
+
+		if (BannedWords.find(this.username)) throw FieldErrors({ username: { message: "Bad username", code: "INVALID_USERNAME" } });
+	}
+
+	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 },
+			...opts,
+			//@ts-ignore
+			select: [...PublicUserProjection, ...(opts?.select || [])],	// TODO: fix
+		});
+	}
+
+	private static async generateDiscriminator(username: string): Promise<string | undefined> {
+		if (Config.get().register.incrementingDiscriminators) {
+			// discriminator will be incrementally generated
+
+			// First we need to figure out the currently highest discrimnator for the given username and then increment it
+			const users = await User.find({ where: { username }, select: ["discriminator"] });
+			const highestDiscriminator = Math.max(0, ...users.map((u) => Number(u.discriminator)));
+
+			const 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 = User.create({
+			created_at: new Date(),
+			username: username,
+			discriminator,
+			id: Snowflake.generate(),
+			bot: false,
+			system: false,
+			premium_since: new Date(),
+			desktop: false,
+			mobile: false,
+			premium: true,
+			premium_type: 2,
+			bio: "",
+			mfa_enabled: false,
+			verified: true,
+			disabled: false,
+			deleted: false,
+			email: email,
+			rights: Config.get().security.defaultRights,
+			nsfw_allowed: true, // TODO: depending on age
+			public_flags: 0,
+			flags: "0", // TODO: generate
+			data: {
+				hash: password,
+				valid_tokens_since: new Date(),
+			},
+			settings: { ...defaultSettings, locale: language },
+			purchased_flags: 5, // TODO: idk what the values for this are
+			premium_usage_flags: 2,  // TODO: idk what the values for this are
+			extended_settings: "",	// TODO: was {}
+			fingerprints: [],
+		});
+
+		await user.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 defaultSettings: UserSettings = {
+	afk_timeout: 3600,
+	allow_accessibility_detection: true,
+	animate_emoji: true,
+	animate_stickers: 0,
+	contact_sync_enabled: false,
+	convert_emoticons: false,
+	custom_status: null,
+	default_guilds_restricted: false,
+	detect_platform_accounts: false,
+	developer_mode: true,
+	disable_games_tab: true,
+	enable_tts_command: false,
+	explicit_content_filter: 0,
+	friend_source_flags: { all: true },
+	gateway_connected: false,
+	gif_auto_play: true,
+	guild_folders: [],
+	guild_positions: [],
+	inline_attachment_media: true,
+	inline_embed_media: true,
+	locale: "en-US",
+	message_display_compact: false,
+	native_phone_integration_enabled: true,
+	render_embeds: true,
+	render_reactions: true,
+	restricted_guilds: [],
+	show_current_game: true,
+	status: "online",
+	stream_notifications_enabled: false,
+	theme: "dark",
+	timezone_offset: 0, // TODO: timezone from request
+
+	banner_color: null,
+	friend_discovery_flags: 0,
+	view_nsfw_guilds: true,
+	passwordless: false,
+};
+
+export interface UserSettings {
+	afk_timeout: number;
+	allow_accessibility_detection: boolean;
+	animate_emoji: boolean;
+	animate_stickers: number;
+	contact_sync_enabled: boolean;
+	convert_emoticons: boolean;
+	custom_status: {
+		emoji_id?: string;
+		emoji_name?: string;
+		expires_at?: number;
+		text?: string;
+	} | null;
+	default_guilds_restricted: boolean;
+	detect_platform_accounts: boolean;
+	developer_mode: boolean;
+	disable_games_tab: boolean;
+	enable_tts_command: boolean;
+	explicit_content_filter: number;
+	friend_source_flags: { all: boolean; };
+	gateway_connected: boolean;
+	gif_auto_play: boolean;
+	// every top guild is displayed as a "folder"
+	guild_folders: {
+		color: number;
+		guild_ids: string[];
+		id: number;
+		name: string;
+	}[];
+	guild_positions: string[]; // guild ids ordered by position
+	inline_attachment_media: boolean;
+	inline_embed_media: boolean;
+	locale: string; // en_US
+	message_display_compact: boolean;
+	native_phone_integration_enabled: boolean;
+	render_embeds: boolean;
+	render_reactions: boolean;
+	restricted_guilds: string[];
+	show_current_game: boolean;
+	status: "online" | "offline" | "dnd" | "idle" | "invisible";
+	stream_notifications_enabled: boolean;
+	theme: "dark" | "white"; // dark
+	timezone_offset: number; // e.g -60
+	banner_color: string | null;
+	friend_discovery_flags: number;
+	view_nsfw_guilds: boolean;
+	passwordless: boolean;
+}
+
+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/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..49793810
--- /dev/null
+++ b/src/util/entities/index.ts
@@ -0,0 +1,32 @@
+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";
\ No newline at end of file