summary refs log tree commit diff
path: root/util/src
diff options
context:
space:
mode:
authorFlam3rboy <34555296+Flam3rboy@users.noreply.github.com>2021-08-12 20:33:02 +0200
committerFlam3rboy <34555296+Flam3rboy@users.noreply.github.com>2021-08-12 20:33:02 +0200
commit4f2596789875b894e5b45d7cfafcc7cf6ca46aef (patch)
tree20b4728a0ff390bd982f2346807f3176efc8c1f9 /util/src
parentMerge branch 'rtc' (diff)
downloadserver-4f2596789875b894e5b45d7cfafcc7cf6ca46aef.tar.xz
:sparkles: util
Diffstat (limited to 'util/src')
-rw-r--r--util/src/index.ts10
-rw-r--r--util/src/models/Activity.ts132
-rw-r--r--util/src/models/Application.ts67
-rw-r--r--util/src/models/AuditLog.ts220
-rw-r--r--util/src/models/Ban.ts32
-rw-r--r--util/src/models/Channel.ts109
-rw-r--r--util/src/models/Emoji.ts29
-rw-r--r--util/src/models/Event.ts540
-rw-r--r--util/src/models/Guild.ts161
-rw-r--r--util/src/models/Interaction.ts32
-rw-r--r--util/src/models/Invite.ts95
-rw-r--r--util/src/models/Member.ts109
-rw-r--r--util/src/models/Message.ts368
-rw-r--r--util/src/models/RateLimit.ts25
-rw-r--r--util/src/models/ReadState.ts26
-rw-r--r--util/src/models/Role.ts42
-rw-r--r--util/src/models/Status.ts13
-rw-r--r--util/src/models/Team.ts17
-rw-r--r--util/src/models/Template.ts51
-rw-r--r--util/src/models/User.ts252
-rw-r--r--util/src/models/VoiceState.ts34
-rw-r--r--util/src/models/Webhook.ts84
-rw-r--r--util/src/models/index.ts89
-rw-r--r--util/src/util/BitField.ts143
-rw-r--r--util/src/util/Config.ts284
-rw-r--r--util/src/util/Constants.ts28
-rw-r--r--util/src/util/Database.ts151
-rw-r--r--util/src/util/Intents.ts21
-rw-r--r--util/src/util/MessageFlags.ts14
-rw-r--r--util/src/util/MongoBigInt.ts82
-rw-r--r--util/src/util/Permissions.ts262
-rw-r--r--util/src/util/RabbitMQ.ts18
-rw-r--r--util/src/util/Regex.ts3
-rw-r--r--util/src/util/Snowflake.ts127
-rw-r--r--util/src/util/String.ts7
-rw-r--r--util/src/util/UserFlags.ts22
-rw-r--r--util/src/util/checkToken.ts24
-rw-r--r--util/src/util/index.ts9
-rw-r--r--util/src/util/toBigInt.ts3
39 files changed, 3735 insertions, 0 deletions
diff --git a/util/src/index.ts b/util/src/index.ts
new file mode 100644
index 00000000..3565fb6b
--- /dev/null
+++ b/util/src/index.ts
@@ -0,0 +1,10 @@
+export * from "./util/checkToken";
+
+export * as Constants from "./util/Constants";
+export * from "./models/index";
+export * from "./util/index";
+
+import Config from "./util/Config";
+import db, { MongooseCache, toObject } from "./util/Database";
+
+export { Config, db, MongooseCache, toObject };
diff --git a/util/src/models/Activity.ts b/util/src/models/Activity.ts
new file mode 100644
index 00000000..17abd1ca
--- /dev/null
+++ b/util/src/models/Activity.ts
@@ -0,0 +1,132 @@
+import { User } from "..";
+import { ClientStatus, Status } from "./Status";
+import { Schema, model, Types, Document } from "mongoose";
+import toBigInt from "../util/toBigInt";
+
+export interface Presence {
+	user: User;
+	guild_id?: string;
+	status: Status;
+	activities: Activity[];
+	client_status: ClientStatus;
+}
+
+export interface Activity {
+	name: string;
+	type: ActivityType;
+	url?: string;
+	created_at?: Date;
+	timestamps?: {
+		start?: number;
+		end?: number;
+	}[];
+	application_id?: string;
+	details?: string;
+	state?: string;
+	emoji?: {
+		name: string;
+		id?: string;
+		amimated?: boolean;
+	};
+	party?: {
+		id?: string;
+		size?: [number, number];
+	};
+	assets?: {
+		large_image?: string;
+		large_text?: string;
+		small_image?: string;
+		small_text?: string;
+	};
+	secrets?: {
+		join?: string;
+		spectate?: string;
+		match?: string;
+	};
+	instance?: boolean;
+	flags?: bigint;
+}
+
+export const ActivitySchema = {
+	name: { type: String, required: true },
+	type: { type: Number, required: true },
+	url: String,
+	created_at: Date,
+	timestamps: [
+		{
+			start: Number,
+			end: Number,
+		},
+	],
+	application_id: String,
+	details: String,
+	state: String,
+	emoji: {
+		name: String,
+		id: String,
+		amimated: Boolean,
+	},
+	party: {
+		id: String,
+		size: [Number, Number],
+	},
+	assets: {
+		large_image: String,
+		large_text: String,
+		small_image: String,
+		small_text: String,
+	},
+	secrets: {
+		join: String,
+		spectate: String,
+		match: String,
+	},
+	instance: Boolean,
+	flags: { type: String, get: toBigInt },
+};
+
+export const ActivityBodySchema = {
+	name: String,
+	type: Number,
+	$url: String,
+	$created_at: Date,
+	$timestamps: [
+		{
+			$start: Number,
+			$end: Number,
+		},
+	],
+	$application_id: String,
+	$details: String,
+	$state: String,
+	$emoji: {
+		$name: String,
+		$id: String,
+		$amimated: Boolean,
+	},
+	$party: {
+		$id: String,
+		$size: [Number, Number],
+	},
+	$assets: {
+		$large_image: String,
+		$large_text: String,
+		$small_image: String,
+		$small_text: String,
+	},
+	$secrets: {
+		$join: String,
+		$spectate: String,
+		$match: String,
+	},
+	$instance: Boolean,
+	$flags: BigInt,
+};
+
+export enum ActivityType {
+	GAME = 0,
+	STREAMING = 1,
+	LISTENING = 2,
+	CUSTOM = 4,
+	COMPETING = 5,
+}
diff --git a/util/src/models/Application.ts b/util/src/models/Application.ts
new file mode 100644
index 00000000..fae6e8db
--- /dev/null
+++ b/util/src/models/Application.ts
@@ -0,0 +1,67 @@
+import { Team } from "./Team";
+
+export interface Application {
+	id: string;
+	name: string;
+	icon: string | null;
+	description: string;
+	rpc_origins: string[] | null;
+	bot_public: boolean;
+	bot_require_code_grant: boolean;
+	terms_of_service_url: string | null;
+	privacy_policy_url: string | null;
+	owner_id: string;
+	summary: string | null;
+	verify_key: string;
+	team: Team | null;
+	guild_id: string; // if this application is a game sold on Discord, this field will be the guild to which it has been linked
+	primary_sku_id: string | null; // if this application is a game sold on Discord, this field will be the id of the "Game SKU" that is created, if exists
+	slug: string | null; // if this application is a game sold on Discord, this field will be the URL slug that links to the store page
+	cover_image: string | null; // the application's default rich presence invite cover image hash
+	flags: number; // 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/util/src/models/AuditLog.ts b/util/src/models/AuditLog.ts
new file mode 100644
index 00000000..02b2c444
--- /dev/null
+++ b/util/src/models/AuditLog.ts
@@ -0,0 +1,220 @@
+import { Schema, Document, Types } from "mongoose";
+import db from "../util/Database";
+import { ChannelPermissionOverwrite } from "./Channel";
+import { PublicUser } from "./User";
+
+export interface AuditLogResponse {
+	webhooks: []; // TODO:
+	users: PublicUser[];
+	audit_log_entries: AuditLogEntries[];
+	integrations: []; // TODO:
+}
+
+export interface AuditLogEntries {
+	target_id?: string;
+	user_id: string;
+	id: string;
+	action_type: AuditLogEvents;
+	options?: {
+		delete_member_days?: string;
+		members_removed?: string;
+		channel_id?: string;
+		messaged_id?: string;
+		count?: string;
+		id?: string;
+		type?: string;
+		role_name?: string;
+	};
+	changes: AuditLogChange[];
+	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;
+}
+
+export interface AuditLogEntriesDocument extends Document, AuditLogEntries {
+	id: string;
+}
+
+export const AuditLogChanges = {
+	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: [{}],
+	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,
+};
+
+export const AuditLogSchema = new Schema({
+	target_id: String,
+	user_id: { type: String, required: true },
+	id: { type: String, required: true },
+	action_type: { type: Number, required: true },
+	options: {
+		delete_member_days: String,
+		members_removed: String,
+		channel_id: String,
+		messaged_id: String,
+		count: String,
+		id: String,
+		type: { type: Number },
+		role_name: String,
+	},
+	changes: [
+		{
+			new_value: AuditLogChanges,
+			old_value: AuditLogChanges,
+			key: String,
+		},
+	],
+	reason: String,
+});
+
+// @ts-ignore
+export const AuditLogModel = db.model<AuditLogEntries>("AuditLog", AuditLogSchema, "auditlogs");
+
+export enum AuditLogEvents {
+	GUILD_UPDATE = 1,
+	CHANNEL_CREATE = 10,
+	CHANNEL_UPDATE = 11,
+	CHANNEL_DELETE = 12,
+	CHANNEL_OVERWRITE_CREATE = 13,
+	CHANNEL_OVERWRITE_UPDATE = 14,
+	CHANNEL_OVERWRITE_DELETE = 15,
+	MEMBER_KICK = 20,
+	MEMBER_PRUNE = 21,
+	MEMBER_BAN_ADD = 22,
+	MEMBER_BAN_REMOVE = 23,
+	MEMBER_UPDATE = 24,
+	MEMBER_ROLE_UPDATE = 25,
+	MEMBER_MOVE = 26,
+	MEMBER_DISCONNECT = 27,
+	BOT_ADD = 28,
+	ROLE_CREATE = 30,
+	ROLE_UPDATE = 31,
+	ROLE_DELETE = 32,
+	INVITE_CREATE = 40,
+	INVITE_UPDATE = 41,
+	INVITE_DELETE = 42,
+	WEBHOOK_CREATE = 50,
+	WEBHOOK_UPDATE = 51,
+	WEBHOOK_DELETE = 52,
+	EMOJI_CREATE = 60,
+	EMOJI_UPDATE = 61,
+	EMOJI_DELETE = 62,
+	MESSAGE_DELETE = 72,
+	MESSAGE_BULK_DELETE = 73,
+	MESSAGE_PIN = 74,
+	MESSAGE_UNPIN = 75,
+	INTEGRATION_CREATE = 80,
+	INTEGRATION_UPDATE = 81,
+	INTEGRATION_DELETE = 82,
+}
diff --git a/util/src/models/Ban.ts b/util/src/models/Ban.ts
new file mode 100644
index 00000000..f09950ee
--- /dev/null
+++ b/util/src/models/Ban.ts
@@ -0,0 +1,32 @@
+import { Schema, model, Types, Document } from "mongoose";
+import db from "../util/Database";
+import { PublicUserProjection, UserModel } from "./User";
+
+export interface Ban extends Document {
+	user_id: string;
+	guild_id: string;
+	executor_id: string;
+	ip: string;
+	reason?: string;
+}
+
+export const BanSchema = new Schema({
+	user_id: { type: String, required: true },
+	guild_id: { type: String, required: true },
+	executor_id: { type: String, required: true },
+	reason: String,
+	ip: String, // ? Should we store this in here, or in the UserModel?
+});
+
+BanSchema.virtual("user", {
+	ref: UserModel,
+	localField: "user_id",
+	foreignField: "id",
+	justOne: true,
+	autopopulate: { select: PublicUserProjection },
+});
+
+BanSchema.set("removeResponse", ["user_id"]);
+
+// @ts-ignore
+export const BanModel = db.model<Ban>("Ban", BanSchema, "bans");
diff --git a/util/src/models/Channel.ts b/util/src/models/Channel.ts
new file mode 100644
index 00000000..1dd05896
--- /dev/null
+++ b/util/src/models/Channel.ts
@@ -0,0 +1,109 @@
+import { Schema, model, Types, Document } from "mongoose";
+import db from "../util/Database";
+import toBigInt from "../util/toBigInt";
+import { PublicUserProjection, UserModel } from "./User";
+
+// @ts-ignore
+export interface AnyChannel extends Channel, DMChannel, TextChannel, VoiceChannel {
+	recipient_ids: null | string[];
+}
+
+export interface ChannelDocument extends Document, AnyChannel {
+	id: string;
+}
+
+export const ChannelSchema = new Schema({
+	id: String,
+	created_at: { type: Schema.Types.Date, required: true },
+	name: String, // can't be required for dm channels
+	type: { type: Number, required: true },
+	guild_id: String,
+	owner_id: String,
+	parent_id: String,
+	recipient_ids: [String],
+	position: Number,
+	last_message_id: String,
+	last_pin_timestamp: Date,
+	nsfw: Boolean,
+	rate_limit_per_user: Number,
+	topic: String,
+	permission_overwrites: [
+		{
+			allow: { type: String, get: toBigInt },
+			deny: { type: String, get: toBigInt },
+			id: String,
+			type: { type: Number },
+		},
+	],
+});
+
+ChannelSchema.virtual("recipients", {
+	ref: UserModel,
+	localField: "recipient_ids",
+	foreignField: "id",
+	justOne: false,
+	autopopulate: { select: PublicUserProjection },
+});
+
+ChannelSchema.set("removeResponse", ["recipient_ids"]);
+
+// @ts-ignore
+export const ChannelModel = db.model<ChannelDocument>("Channel", ChannelSchema, "channels");
+
+export interface Channel {
+	id: string;
+	created_at: Date;
+	name: string;
+	type: number;
+}
+
+export interface TextBasedChannel {
+	last_message_id?: string;
+	last_pin_timestamp?: number;
+}
+
+export interface GuildChannel extends Channel {
+	guild_id: string;
+	position: number;
+	parent_id?: string;
+	permission_overwrites: ChannelPermissionOverwrite[];
+}
+
+export interface ChannelPermissionOverwrite {
+	allow: bigint; // for bitfields we use bigints
+	deny: bigint; // for bitfields we use bigints
+	id: string;
+	type: ChannelPermissionOverwriteType;
+}
+
+export enum ChannelPermissionOverwriteType {
+	role = 0,
+	member = 1,
+}
+
+export interface VoiceChannel extends GuildChannel {
+	video_quality_mode?: number;
+	bitrate?: number;
+	user_limit?: number;
+}
+
+export interface TextChannel extends GuildChannel, TextBasedChannel {
+	nsfw: boolean;
+	rate_limit_per_user: number;
+	topic?: string;
+}
+// @ts-ignore
+export interface DMChannel extends Channel, TextBasedChannel {
+	owner_id: string;
+	recipient_ids: string[];
+}
+
+export enum ChannelType {
+	GUILD_TEXT = 0, // a text channel within a server
+	DM = 1, // a direct message between users
+	GUILD_VOICE = 2, // a voice channel within a server
+	GROUP_DM = 3, // a direct message between multiple users
+	GUILD_CATEGORY = 4, // an organizational category that contains up to 50 channels
+	GUILD_NEWS = 5, // a channel that users can follow and crosspost into their own server
+	GUILD_STORE = 6, // a channel in which game developers can sell their game on Discord
+}
diff --git a/util/src/models/Emoji.ts b/util/src/models/Emoji.ts
new file mode 100644
index 00000000..3e5cad53
--- /dev/null
+++ b/util/src/models/Emoji.ts
@@ -0,0 +1,29 @@
+import { Schema, model, Types, Document } from "mongoose";
+import db from "../util/Database";
+
+export interface Emoji extends Document {
+	id: string;
+	animated: boolean;
+	available: boolean;
+	guild_id: string;
+	managed: boolean;
+	name: string;
+	require_colons: boolean;
+	url: string;
+	roles: string[]; // roles this emoji is whitelisted to (new discord feature?)
+}
+
+export const EmojiSchema = new Schema({
+	id: { type: String, required: true },
+	animated: Boolean,
+	available: Boolean,
+	guild_id: String,
+	managed: Boolean,
+	name: String,
+	require_colons: Boolean,
+	url: String,
+	roles: [String],
+});
+
+// @ts-ignore
+export const EmojiModel = db.model<Emoji>("Emoji", EmojiSchema, "emojis");
diff --git a/util/src/models/Event.ts b/util/src/models/Event.ts
new file mode 100644
index 00000000..1564107d
--- /dev/null
+++ b/util/src/models/Event.ts
@@ -0,0 +1,540 @@
+import { ConnectedAccount, PublicUser, Relationship, User, UserSettings } from "./User";
+import { DMChannel, Channel } from "./Channel";
+import { Guild } from "./Guild";
+import { Member, PublicMember, UserGuildSettings } from "./Member";
+import { Emoji } from "./Emoji";
+import { Presence } from "./Activity";
+import { Role } from "./Role";
+import { Invite } from "./Invite";
+import { Message, PartialEmoji } from "./Message";
+import { VoiceState } from "./VoiceState";
+import { ApplicationCommand } from "./Application";
+import { Interaction } from "./Interaction";
+import { Schema, model, Types, Document } from "mongoose";
+import db from "../util/Database";
+
+export interface Event {
+	guild_id?: string;
+	user_id?: string;
+	channel_id?: string;
+	created_at?: Date;
+	event: EVENT;
+	data?: any;
+}
+
+export interface EventDocument extends Event, Document {}
+
+export const EventSchema = new Schema({
+	guild_id: String,
+	user_id: String,
+	channel_id: String,
+	created_at: { type: Date, required: true },
+	event: { type: String, required: true },
+	data: Object,
+});
+
+// @ts-ignore
+export const EventModel = db.model<EventDocument>("Event", EventSchema, "events");
+
+// ! Custom Events that shouldn't get sent to the client but processed by the server
+
+export interface InvalidatedEvent extends Event {
+	event: "INVALIDATED";
+}
+
+// ! END Custom Events that shouldn't get sent to the client but processed by the server
+
+export interface ReadyEventData {
+	v: number;
+	user: PublicUser & {
+		mobile: boolean;
+		desktop: boolean;
+		email: string | null;
+		flags: bigint;
+		mfa_enabled: boolean;
+		nsfw_allowed: boolean;
+		phone: string | null;
+		premium: boolean;
+		premium_type: number;
+		verified: boolean;
+		bot: boolean;
+	};
+	private_channels: DMChannel[]; // this will be empty for bots
+	session_id: string; // resuming
+	guilds: Guild[];
+	analytics_token?: string;
+	connected_accounts?: ConnectedAccount[];
+	consents?: {
+		personalization?: {
+			consented?: boolean;
+		};
+	};
+	country_code?: string; // e.g. DE
+	friend_suggestion_count?: number;
+	geo_ordered_rtc_regions?: string[]; // ["europe","russie","india","us-east","us-central"]
+	experiments?: [number, number, number, number, number][];
+	guild_experiments?: [
+		// ? what are guild_experiments?
+		// this is the structure of it:
+		number,
+		null,
+		number,
+		[[number, { e: number; s: number }[]]],
+		[number, [[number, [number, number]]]],
+		{ b: number; k: bigint[] }[]
+	][];
+	guild_join_requests?: []; // ? what is this? this is new
+	shard?: [number, number];
+	user_settings?: UserSettings;
+	relationships?: Relationship[]; // TODO
+	read_state: {
+		entries: []; // TODO
+		partial: boolean;
+		version: number;
+	};
+	user_guild_settings?: {
+		entries: UserGuildSettings[];
+		version: number;
+		partial: boolean;
+	};
+	application?: {
+		id: string;
+		flags: bigint;
+	};
+	merged_members?: Omit<Member, "settings" | "user">[][];
+	// probably all users who the user is in contact with
+	users?: {
+		avatar: string | null;
+		discriminator: string;
+		id: string;
+		username: string;
+		bot: boolean;
+		public_flags: bigint;
+	}[];
+}
+
+export interface ReadyEvent extends Event {
+	event: "READY";
+	data: ReadyEventData;
+}
+
+export interface ChannelCreateEvent extends Event {
+	event: "CHANNEL_CREATE";
+	data: Channel;
+}
+
+export interface ChannelUpdateEvent extends Event {
+	event: "CHANNEL_UPDATE";
+	data: Channel;
+}
+
+export interface ChannelDeleteEvent extends Event {
+	event: "CHANNEL_DELETE";
+	data: Channel;
+}
+
+export interface ChannelPinsUpdateEvent extends Event {
+	event: "CHANNEL_PINS_UPDATE";
+	data: {
+		guild_id?: string;
+		channel_id: string;
+		last_pin_timestamp?: number;
+	};
+}
+
+export interface GuildCreateEvent extends Event {
+	event: "GUILD_CREATE";
+	data: Guild;
+}
+
+export interface GuildUpdateEvent extends Event {
+	event: "GUILD_UPDATE";
+	data: Guild;
+}
+
+export interface GuildDeleteEvent extends Event {
+	event: "GUILD_DELETE";
+	data: {
+		id: string;
+		unavailable?: boolean;
+	};
+}
+
+export interface GuildBanAddEvent extends Event {
+	event: "GUILD_BAN_ADD";
+	data: {
+		guild_id: string;
+		user: User;
+	};
+}
+
+export interface GuildBanRemoveEvent extends Event {
+	event: "GUILD_BAN_REMOVE";
+	data: {
+		guild_id: string;
+		user: User;
+	};
+}
+
+export interface GuildEmojiUpdateEvent extends Event {
+	event: "GUILD_EMOJI_UPDATE";
+	data: {
+		guild_id: string;
+		emojis: Emoji[];
+	};
+}
+
+export interface GuildIntegrationUpdateEvent extends Event {
+	event: "GUILD_INTEGRATIONS_UPDATE";
+	data: {
+		guild_id: string;
+	};
+}
+
+export interface GuildMemberAddEvent extends Event {
+	event: "GUILD_MEMBER_ADD";
+	data: PublicMember & {
+		guild_id: string;
+	};
+}
+
+export interface GuildMemberRemoveEvent extends Event {
+	event: "GUILD_MEMBER_REMOVE";
+	data: {
+		guild_id: string;
+		user: User;
+	};
+}
+
+export interface GuildMemberUpdateEvent extends Event {
+	event: "GUILD_MEMBER_UPDATE";
+	data: {
+		guild_id: string;
+		roles: string[];
+		user: User;
+		nick?: string;
+		joined_at?: Date;
+		premium_since?: number;
+		pending?: boolean;
+	};
+}
+
+export interface GuildMembersChunkEvent extends Event {
+	event: "GUILD_MEMBERS_CHUNK";
+	data: {
+		guild_id: string;
+		members: PublicMember[];
+		chunk_index: number;
+		chunk_count: number;
+		not_found: string[];
+		presences: Presence[];
+		nonce?: string;
+	};
+}
+
+export interface GuildRoleCreateEvent extends Event {
+	event: "GUILD_ROLE_CREATE";
+	data: {
+		guild_id: string;
+		role: Role;
+	};
+}
+
+export interface GuildRoleUpdateEvent extends Event {
+	event: "GUILD_ROLE_UPDATE";
+	data: {
+		guild_id: string;
+		role: Role;
+	};
+}
+
+export interface GuildRoleDeleteEvent extends Event {
+	event: "GUILD_ROLE_DELETE";
+	data: {
+		guild_id: string;
+		role_id: string;
+	};
+}
+
+export interface InviteCreateEvent extends Event {
+	event: "INVITE_CREATE";
+	data: Omit<Invite, "guild" | "channel"> & {
+		channel_id: string;
+		guild_id?: string;
+	};
+}
+
+export interface InviteDeleteEvent extends Event {
+	event: "INVITE_DELETE";
+	data: {
+		channel_id: string;
+		guild_id?: string;
+		code: string;
+	};
+}
+
+export type MessagePayload = Omit<Message, "author_id"> & {
+	channel_id: string;
+	guild_id?: string;
+	author: PublicUser;
+	member: PublicMember;
+	mentions: (PublicUser & { member: PublicMember })[];
+};
+
+export interface MessageCreateEvent extends Event {
+	event: "MESSAGE_CREATE";
+	data: MessagePayload;
+}
+
+export interface MessageUpdateEvent extends Event {
+	event: "MESSAGE_UPDATE";
+	data: MessagePayload;
+}
+
+export interface MessageDeleteEvent extends Event {
+	event: "MESSAGE_DELETE";
+	data: {
+		id: string;
+		channel_id: string;
+		guild_id?: string;
+	};
+}
+
+export interface MessageDeleteBulkEvent extends Event {
+	event: "MESSAGE_DELETE_BULK";
+	data: {
+		ids: string[];
+		channel_id: string;
+		guild_id?: string;
+	};
+}
+
+export interface MessageReactionAddEvent extends Event {
+	event: "MESSAGE_REACTION_ADD";
+	data: {
+		user_id: string;
+		channel_id: string;
+		message_id: string;
+		guild_id?: string;
+		member?: PublicMember;
+		emoji: PartialEmoji;
+	};
+}
+
+export interface MessageReactionRemoveEvent extends Event {
+	event: "MESSAGE_REACTION_REMOVE";
+	data: {
+		user_id: string;
+		channel_id: string;
+		message_id: string;
+		guild_id?: string;
+		emoji: PartialEmoji;
+	};
+}
+
+export interface MessageReactionRemoveAllEvent extends Event {
+	event: "MESSAGE_REACTION_REMOVE_ALL";
+	data: {
+		channel_id: string;
+		message_id: string;
+		guild_id?: string;
+	};
+}
+
+export interface MessageReactionRemoveEmojiEvent extends Event {
+	event: "MESSAGE_REACTION_REMOVE_EMOJI";
+	data: {
+		channel_id: string;
+		message_id: string;
+		guild_id?: string;
+		emoji: PartialEmoji;
+	};
+}
+
+export interface PresenceUpdateEvent extends Event {
+	event: "PRESENCE_UPDATE";
+	data: Presence;
+}
+
+export interface TypingStartEvent extends Event {
+	event: "TYPING_START";
+	data: {
+		channel_id: string;
+		user_id: string;
+		timestamp: number;
+		guild_id?: string;
+		member?: PublicMember;
+	};
+}
+
+export interface UserUpdateEvent extends Event {
+	event: "USER_UPDATE";
+	data: User;
+}
+
+export interface VoiceStateUpdateEvent extends Event {
+	event: "VOICE_STATE_UPDATE";
+	data: VoiceState & {
+		member: PublicMember;
+	};
+}
+
+export interface VoiceServerUpdateEvent extends Event {
+	event: "VOICE_SERVER_UPDATE";
+	data: {
+		token: string;
+		guild_id: string;
+		endpoint: string;
+	};
+}
+
+export interface WebhooksUpdateEvent extends Event {
+	event: "WEBHOOKS_UPDATE";
+	data: {
+		guild_id: string;
+		channel_id: string;
+	};
+}
+
+export type ApplicationCommandPayload = ApplicationCommand & {
+	guild_id: string;
+};
+
+export interface ApplicationCommandCreateEvent extends Event {
+	event: "APPLICATION_COMMAND_CREATE";
+	data: ApplicationCommandPayload;
+}
+
+export interface ApplicationCommandUpdateEvent extends Event {
+	event: "APPLICATION_COMMAND_UPDATE";
+	data: ApplicationCommandPayload;
+}
+
+export interface ApplicationCommandDeleteEvent extends Event {
+	event: "APPLICATION_COMMAND_DELETE";
+	data: ApplicationCommandPayload;
+}
+
+export interface InteractionCreateEvent extends Event {
+	event: "INTERACTION_CREATE";
+	data: Interaction;
+}
+
+export interface MessageAckEvent extends Event {
+	event: "MESSAGE_ACK";
+	data: {
+		channel_id: string;
+		message_id: string;
+		version?: number;
+		manual?: boolean;
+		mention_count?: number;
+	};
+}
+
+export interface RelationshipAddEvent extends Event {
+	event: "RELATIONSHIP_ADD";
+	data: Relationship & {
+		should_notify?: boolean;
+		user: PublicUser;
+	};
+}
+
+export interface RelationshipRemoveEvent extends Event {
+	event: "RELATIONSHIP_REMOVE";
+	data: Omit<Relationship, "nickname">;
+}
+
+// located in collection events
+
+export enum EVENTEnum {
+	Ready = "READY",
+	ChannelCreate = "CHANNEL_CREATE",
+	ChannelUpdate = "CHANNEL_UPDATE",
+	ChannelDelete = "CHANNEL_DELETE",
+	ChannelPinsUpdate = "CHANNEL_PINS_UPDATE",
+	GuildCreate = "GUILD_CREATE",
+	GuildUpdate = "GUILD_UPDATE",
+	GuildDelete = "GUILD_DELETE",
+	GuildBanAdd = "GUILD_BAN_ADD",
+	GuildBanRemove = "GUILD_BAN_REMOVE",
+	GuildEmojUpdate = "GUILD_EMOJI_UPDATE",
+	GuildIntegrationsUpdate = "GUILD_INTEGRATIONS_UPDATE",
+	GuildMemberAdd = "GUILD_MEMBER_ADD",
+	GuildMemberRempve = "GUILD_MEMBER_REMOVE",
+	GuildMemberUpdate = "GUILD_MEMBER_UPDATE",
+	GuildMemberSpeaking = "GUILD_MEMBER_SPEAKING",
+	GuildMembersChunk = "GUILD_MEMBERS_CHUNK",
+	GuildRoleCreate = "GUILD_ROLE_CREATE",
+	GuildRoleDelete = "GUILD_ROLE_DELETE",
+	GuildRoleUpdate = "GUILD_ROLE_UPDATE",
+	InviteCreate = "INVITE_CREATE",
+	InviteDelete = "INVITE_DELETE",
+	MessageCreate = "MESSAGE_CREATE",
+	MessageUpdate = "MESSAGE_UPDATE",
+	MessageDelete = "MESSAGE_DELETE",
+	MessageDeleteBulk = "MESSAGE_DELETE_BULK",
+	MessageReactionAdd = "MESSAGE_REACTION_ADD",
+	MessageReactionRemove = "MESSAGE_REACTION_REMOVE",
+	MessageReactionRemoveAll = "MESSAGE_REACTION_REMOVE_ALL",
+	MessageReactionRemoveEmoji = "MESSAGE_REACTION_REMOVE_EMOJI",
+	PresenceUpdate = "PRESENCE_UPDATE",
+	TypingStart = "TYPING_START",
+	UserUpdate = "USER_UPDATE",
+	WebhooksUpdate = "WEBHOOKS_UPDATE",
+	InteractionCreate = "INTERACTION_CREATE",
+	VoiceStateUpdate = "VOICE_STATE_UPDATE",
+	VoiceServerUpdate = "VOICE_SERVER_UPDATE",
+	ApplicationCommandCreate = "APPLICATION_COMMAND_CREATE",
+	ApplicationCommandUpdate = "APPLICATION_COMMAND_UPDATE",
+	ApplicationCommandDelete = "APPLICATION_COMMAND_DELETE",
+}
+
+export type EVENT =
+	| "READY"
+	| "CHANNEL_CREATE"
+	| "CHANNEL_UPDATE"
+	| "CHANNEL_DELETE"
+	| "CHANNEL_PINS_UPDATE"
+	| "GUILD_CREATE"
+	| "GUILD_UPDATE"
+	| "GUILD_DELETE"
+	| "GUILD_BAN_ADD"
+	| "GUILD_BAN_REMOVE"
+	| "GUILD_EMOJI_UPDATE"
+	| "GUILD_INTEGRATIONS_UPDATE"
+	| "GUILD_MEMBER_ADD"
+	| "GUILD_MEMBER_REMOVE"
+	| "GUILD_MEMBER_UPDATE"
+	| "GUILD_MEMBER_SPEAKING"
+	| "GUILD_MEMBERS_CHUNK"
+	| "GUILD_ROLE_CREATE"
+	| "GUILD_ROLE_DELETE"
+	| "GUILD_ROLE_UPDATE"
+	| "INVITE_CREATE"
+	| "INVITE_DELETE"
+	| "MESSAGE_CREATE"
+	| "MESSAGE_UPDATE"
+	| "MESSAGE_DELETE"
+	| "MESSAGE_DELETE_BULK"
+	| "MESSAGE_REACTION_ADD"
+	// TODO: add a new event: bulk add reaction:
+	// | "MESSAGE_REACTION_BULK_ADD"
+	| "MESSAGE_REACTION_REMOVE"
+	| "MESSAGE_REACTION_REMOVE_ALL"
+	| "MESSAGE_REACTION_REMOVE_EMOJI"
+	| "PRESENCE_UPDATE"
+	| "TYPING_START"
+	| "USER_UPDATE"
+	| "WEBHOOKS_UPDATE"
+	| "INTERACTION_CREATE"
+	| "VOICE_STATE_UPDATE"
+	| "VOICE_SERVER_UPDATE"
+	| "APPLICATION_COMMAND_CREATE"
+	| "APPLICATION_COMMAND_UPDATE"
+	| "APPLICATION_COMMAND_DELETE"
+	| "MESSAGE_ACK"
+	| "RELATIONSHIP_ADD"
+	| "RELATIONSHIP_REMOVE"
+	| CUSTOMEVENTS;
+
+export type CUSTOMEVENTS = "INVALIDATED";
diff --git a/util/src/models/Guild.ts b/util/src/models/Guild.ts
new file mode 100644
index 00000000..13a7d078
--- /dev/null
+++ b/util/src/models/Guild.ts
@@ -0,0 +1,161 @@
+import { Schema, model, Types, Document } from "mongoose";
+import db from "../util/Database";
+import { ChannelModel } from "./Channel";
+import { EmojiModel } from "./Emoji";
+import { MemberModel } from "./Member";
+import { RoleModel } from "./Role";
+
+export interface GuildDocument extends Document, Guild {
+	id: string;
+}
+
+export interface Guild {
+	id: string;
+	afk_channel_id?: string;
+	afk_timeout?: number;
+	application_id?: string;
+	banner?: string;
+	default_message_notifications?: number;
+	description?: string;
+	discovery_splash?: string;
+	explicit_content_filter?: number;
+	features: string[];
+	icon?: string;
+	large?: boolean;
+	max_members?: number; // e.g. default 100.000
+	max_presences?: number;
+	max_video_channel_users?: number; // ? default: 25, is this max 25 streaming or watching
+	member_count?: number;
+	presence_count?: number; // users online
+	// members?: Member[]; // * Members are stored in a seperate collection
+	// roles: Role[]; // * Role are stored in a seperate collection
+	// channels: GuildChannel[]; // * Channels are stored in a seperate collection
+	// emojis: Emoji[];  // * Emojis are stored in a seperate collection
+	// voice_states: []; // * voice_states are stored in a seperate collection
+    //TODO:
+	presences?: object[];
+	mfa_level?: number;
+	name: string;
+	owner_id: string;
+	preferred_locale?: string; // only community guilds can choose this
+	premium_subscription_count?: number;
+	premium_tier?: number; // nitro boost level
+	public_updates_channel_id?: string;
+	region?: string;
+	rules_channel_id?: string;
+	splash?: string;
+	system_channel_flags?: number;
+	system_channel_id?: string;
+	unavailable?: boolean;
+	vanity_url?: {
+		code: string;
+		uses: number;
+	};
+	verification_level?: number;
+	welcome_screen: {
+		enabled: boolean;
+		description: string;
+		welcome_channels: { 
+		description: string;
+	    emoji_id?: string;
+	    emoji_name: string;
+	    channel_id: string }[];
+	};
+	widget_channel_id?: string;
+	widget_enabled?: boolean;
+}
+
+export const GuildSchema = new Schema({
+	id: { type: String, required: true },
+	afk_channel_id: String,
+	afk_timeout: Number,
+	application_id: String,
+	banner: String,
+	default_message_notifications: Number,
+	description: String,
+	discovery_splash: String,
+	explicit_content_filter: Number,
+	features: { type: [String], default: [] },
+	icon: String,
+	large: Boolean,
+	max_members: { type: Number, default: 100000 },
+	max_presences: Number,
+	max_video_channel_users: { type: Number, default: 25 },
+	member_count: Number,
+	presences: { type: [Object], default: [] },
+	presence_count: Number,
+	mfa_level: Number,
+	name: { type: String, required: true },
+	owner_id: { type: String, required: true },
+	preferred_locale: String,
+	premium_subscription_count: Number,
+	premium_tier: Number,
+	public_updates_channel_id: String,
+	region: String,
+	rules_channel_id: String,
+	splash: String,
+	system_channel_flags: Number,
+	system_channel_id: String,
+	unavailable: Boolean,
+	vanity_url: {
+		code: String,
+		uses: Number
+	},
+	verification_level: Number,
+	voice_states: { type: [Object], default: [] },
+	welcome_screen: {
+		enabled: Boolean,
+		description: String,
+		welcome_channels: [{ 
+		description: String,
+	    emoji_id: String,
+	    emoji_name: String,
+	    channel_id: String }],
+	},
+	widget_channel_id: String,
+	widget_enabled: Boolean,
+});
+
+GuildSchema.virtual("channels", {
+	ref: ChannelModel,
+	localField: "id",
+	foreignField: "guild_id",
+	justOne: false,
+	autopopulate: true,
+});
+
+GuildSchema.virtual("roles", {
+	ref: RoleModel,
+	localField: "id",
+	foreignField: "guild_id",
+	justOne: false,
+	autopopulate: true,
+});
+
+// nested populate is needed for member users: https://gist.github.com/yangsu/5312204
+GuildSchema.virtual("members", {
+	ref: MemberModel,
+	localField: "id",
+	foreignField: "guild_id",
+	justOne: false,
+});
+
+GuildSchema.virtual("emojis", {
+	ref: EmojiModel,
+	localField: "id",
+	foreignField: "guild_id",
+	justOne: false,
+	autopopulate: true,
+});
+
+GuildSchema.virtual("joined_at", {
+	ref: MemberModel,
+	localField: "id",
+	foreignField: "guild_id",
+	justOne: true,
+}).get((member: any, virtual: any, doc: any) => {
+	return member?.joined_at;
+});
+
+// @ts-ignore
+export const GuildModel = db.model<GuildDocument>("Guild", GuildSchema, "guilds");
diff --git a/util/src/models/Interaction.ts b/util/src/models/Interaction.ts
new file mode 100644
index 00000000..764247a5
--- /dev/null
+++ b/util/src/models/Interaction.ts
@@ -0,0 +1,32 @@
+import { AllowedMentions, Embed } from "./Message";
+
+export interface Interaction {
+	id: string;
+	type: InteractionType;
+	data?: {};
+	guild_id: string;
+	channel_id: string;
+	member_id: string;
+	token: string;
+	version: number;
+}
+
+export enum InteractionType {
+	Ping = 1,
+	ApplicationCommand = 2,
+}
+
+export enum InteractionResponseType {
+	Pong = 1,
+	Acknowledge = 2,
+	ChannelMessage = 3,
+	ChannelMessageWithSource = 4,
+	AcknowledgeWithSource = 5,
+}
+
+export interface InteractionApplicationCommandCallbackData {
+	tts?: boolean;
+	content: string;
+	embeds?: Embed[];
+	allowed_mentions?: AllowedMentions;
+}
diff --git a/util/src/models/Invite.ts b/util/src/models/Invite.ts
new file mode 100644
index 00000000..01f12003
--- /dev/null
+++ b/util/src/models/Invite.ts
@@ -0,0 +1,95 @@
+import { Schema, Document, Types } from "mongoose";
+import db from "../util/Database";
+import { ChannelModel } from "./Channel";
+import { PublicUserProjection, UserModel } from "./User";
+import { GuildModel } from "./Guild";
+
+export interface Invite {
+	code: string;
+	temporary: boolean;
+	uses: number;
+	max_uses: number;
+	max_age: number;
+	created_at: Date;
+	expires_at: Date;
+	guild_id: string;
+	channel_id: string;
+	inviter_id: string;
+
+	// ? What is this?
+	target_user_id?: string;
+	target_user_type?: number;
+}
+
+export interface InviteDocument extends Invite, Document {}
+
+export const InviteSchema = new Schema({
+	code: String,
+	temporary: Boolean,
+	uses: Number,
+	max_uses: Number,
+	max_age: Number,
+	created_at: Date,
+	expires_at: Date,
+	guild_id: String,
+	channel_id: String,
+	inviter_id: String,
+
+	// ? What is this?
+	target_user_id: String,
+	target_user_type: Number,
+});
+
+InviteSchema.virtual("channel", {
+	ref: ChannelModel,
+	localField: "channel_id",
+	foreignField: "id",
+	justOne: true,
+	autopopulate: {
+		select: {
+			id: true,
+			name: true,
+			type: true,
+		},
+	},
+});
+
+InviteSchema.virtual("inviter", {
+	ref: UserModel,
+	localField: "inviter_id",
+	foreignField: "id",
+	justOne: true,
+	autopopulate: {
+		select: PublicUserProjection,
+	},
+});
+
+InviteSchema.virtual("guild", {
+	ref: GuildModel,
+	localField: "guild_id",
+	foreignField: "id",
+	justOne: true,
+	autopopulate: {
+		select: {
+			id: true,
+			name: true,
+			splash: true,
+			banner: true,
+			description: true,
+			icon: true,
+			features: true,
+			verification_level: true,
+			vanity_url_code: true,
+			welcome_screen: true,
+			nsfw: true,
+
+			// TODO: hide the following entries:
+			// channels: false,
+			// roles: false,
+			// emojis: false,
+		},
+	},
+});
+
+// @ts-ignore
+export const InviteModel = db.model<InviteDocument>("Invite", InviteSchema, "invites");
diff --git a/util/src/models/Member.ts b/util/src/models/Member.ts
new file mode 100644
index 00000000..d1c9ad9b
--- /dev/null
+++ b/util/src/models/Member.ts
@@ -0,0 +1,109 @@
+import { PublicUser, PublicUserProjection, User, UserModel } from "./User";
+import { Schema, Types, Document } from "mongoose";
+import db from "../util/Database";
+
+export const PublicMemberProjection = {
+	id: true,
+	guild_id: true,
+	nick: true,
+	roles: true,
+	joined_at: true,
+	pending: true,
+	deaf: true,
+	mute: true,
+	premium_since: true,
+};
+
+export interface Member {
+	id: string;
+	guild_id: string;
+	nick?: string;
+	roles: string[];
+	joined_at: Date;
+	premium_since?: number;
+	deaf: boolean;
+	mute: boolean;
+	pending: boolean;
+	settings: UserGuildSettings;
+	read_state: Record<string, string | null>;
+	// virtual
+	user?: User;
+}
+
+export interface MemberDocument extends Member, Document {
+	id: string;
+}
+
+export interface UserGuildSettings {
+	channel_overrides: {
+		channel_id: string;
+		message_notifications: number;
+		mute_config: MuteConfig;
+		muted: boolean;
+	}[];
+	message_notifications: number;
+	mobile_push: boolean;
+	mute_config: MuteConfig;
+	muted: boolean;
+	suppress_everyone: boolean;
+	suppress_roles: boolean;
+	version: number;
+}
+
+export interface MuteConfig {
+	end_time: number;
+	selected_time_window: number;
+}
+
+const MuteConfig = {
+	end_time: Number,
+	selected_time_window: Number,
+};
+
+export const MemberSchema = new Schema({
+	id: { type: String, required: true },
+	guild_id: String,
+	nick: String,
+	roles: [String],
+	joined_at: Date,
+	premium_since: Number,
+	deaf: Boolean,
+	mute: Boolean,
+	pending: Boolean,
+	read_state: Object,
+	settings: {
+		channel_overrides: [
+			{
+				channel_id: String,
+				message_notifications: Number,
+				mute_config: MuteConfig,
+				muted: Boolean,
+			},
+		],
+		message_notifications: Number,
+		mobile_push: Boolean,
+		mute_config: MuteConfig,
+		muted: Boolean,
+		suppress_everyone: Boolean,
+		suppress_roles: Boolean,
+		version: Number,
+	},
+});
+
+MemberSchema.virtual("user", {
+	ref: UserModel,
+	localField: "id",
+	foreignField: "id",
+	justOne: true,
+	autopopulate: {
+		select: PublicUserProjection,
+	},
+});
+
+// @ts-ignore
+export const MemberModel = db.model<MemberDocument>("Member", MemberSchema, "members");
+
+// @ts-ignore
+export interface PublicMember extends Omit<Member, "settings" | "id" | "read_state"> {
+	user: PublicUser;
+}
diff --git a/util/src/models/Message.ts b/util/src/models/Message.ts
new file mode 100644
index 00000000..15a6f40d
--- /dev/null
+++ b/util/src/models/Message.ts
@@ -0,0 +1,368 @@
+import { Schema, Types, Document } from "mongoose";
+import db from "../util/Database";
+import { PublicUser, PublicUserProjection, UserModel } from "./User";
+import { MemberModel, PublicMember } from "./Member";
+import { Role, RoleModel } from "./Role";
+import { Channel } from "./Channel";
+import { Snowflake } from "../util";
+import { InteractionType } from "./Interaction";
+
+export interface Message {
+	id: string;
+	channel_id: string;
+	guild_id?: string;
+	author_id?: string;
+	webhook_id?: string;
+	application_id?: string;
+	content?: string;
+	timestamp: Date;
+	edited_timestamp: Date | null;
+	tts?: boolean;
+	mention_everyone?: boolean;
+	mention_user_ids: string[];
+	mention_role_ids: string[];
+	mention_channels_ids: string[];
+	attachments: Attachment[];
+	embeds: Embed[];
+	reactions: Reaction[];
+	nonce?: string | number;
+	pinned?: boolean;
+	type: MessageType;
+	activity?: {
+		type: number;
+		party_id: string;
+	};
+	flags?: bigint;
+	stickers?: any[];
+	message_reference?: {
+		message_id: string;
+		channel_id?: string;
+		guild_id?: string;
+	};
+	interaction?: {
+		id: string;
+		type: InteractionType;
+		name: string;
+		user_id: string; // the user who invoked the interaction
+		// user: User; // TODO: autopopulate user
+	};
+	components: MessageComponent[];
+
+	// * mongoose virtuals:
+	// TODO:
+	// application: Application; // TODO: auto pouplate application
+	author?: PublicUser;
+	member?: PublicMember;
+	mentions?: (PublicUser & {
+		member: PublicMember;
+	})[];
+	mention_roles?: Role[];
+	mention_channels?: Channel[];
+	created_at?: Date;
+	// thread // TODO
+}
+
+const PartialEmoji = {
+	id: String,
+	name: { type: String, required: true },
+	animated: { type: Boolean, required: true },
+};
+
+const MessageComponent: any = {
+	type: { type: Number, required: true },
+	style: Number,
+	label: String,
+	emoji: PartialEmoji,
+	custom_id: String,
+	url: String,
+	disabled: Boolean,
+	components: [Object],
+};
+
+export interface MessageComponent {
+	type: number;
+	style?: number;
+	label?: string;
+	emoji?: PartialEmoji;
+	custom_id?: string;
+	url?: string;
+	disabled?: boolean;
+	components: MessageComponent[];
+}
+
+export enum MessageComponentType {
+	ActionRow = 1,
+	Button = 2,
+}
+
+export interface MessageDocument extends Document, Message {
+	id: string;
+}
+
+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,
+	GUILD_DISCOVERY_DISQUALIFIED = 14,
+	GUILD_DISCOVERY_REQUALIFIED = 15,
+	REPLY = 19,
+	APPLICATION_COMMAND = 20,
+}
+
+export interface Attachment {
+	id: string; // attachment id
+	filename: string; // name of file attached
+	size: number; // size of file in bytes
+	url: string; // source url of file
+	proxy_url: string; // a proxied url of file
+	height?: number; // height of file (if image)
+	width?: number; // width of file (if image)
+	content_type?: string;
+}
+
+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;
+}
+
+export const Attachment = {
+	id: String, // attachment id
+	filename: String, // name of file attached
+	size: Number, // size of file in bytes
+	url: String, // source url of file
+	proxy_url: String, // a proxied url of file
+	height: Number, // height of file (if image)
+	width: Number, // width of file (if image)
+	content_type: String,
+};
+
+export const EmbedImage = {
+	url: String,
+	proxy_url: String,
+	height: Number,
+	width: Number,
+};
+
+const Reaction = {
+	count: Number,
+	user_ids: [String],
+	emoji: {
+		id: String,
+		name: String,
+		animated: Boolean,
+	},
+};
+
+export const Embed = {
+	title: String, //title of embed
+	type: { type: String }, // 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 const MessageSchema = new Schema({
+	id: String,
+	channel_id: String,
+	author_id: String,
+	webhook_id: String,
+	guild_id: String,
+	application_id: String,
+	content: String,
+	timestamp: Date,
+	edited_timestamp: Date,
+	tts: Boolean,
+	mention_everyone: Boolean,
+	mention_user_ids: [String],
+	mention_role_ids: [String],
+	mention_channel_ids: [String],
+	attachments: [Attachment],
+	embeds: [Embed],
+	reactions: [Reaction],
+	nonce: Schema.Types.Mixed, // can be a long or a string
+	pinned: Boolean,
+	type: { type: Number },
+	activity: {
+		type: { type: Number },
+		party_id: String,
+	},
+	flags: Types.Long,
+	stickers: [],
+	message_reference: {
+		message_id: String,
+		channel_id: String,
+		guild_id: String,
+	},
+	components: [MessageComponent],
+	// virtual:
+	// author: {
+	// 	ref: UserModel,
+	// 	localField: "author_id",
+	// 	foreignField: "id",
+	// 	justOne: true,
+	// 	autopopulate: { select: { id: true, user_data: false } },
+	// },
+});
+
+MessageSchema.virtual("author", {
+	ref: UserModel,
+	localField: "author_id",
+	foreignField: "id",
+	justOne: true,
+	autopopulate: { select: PublicUserProjection },
+});
+
+MessageSchema.virtual("member", {
+	ref: MemberModel,
+	localField: "author_id",
+	foreignField: "id",
+	justOne: true,
+});
+
+MessageSchema.virtual("mentions", {
+	ref: UserModel,
+	localField: "mention_user_ids",
+	foreignField: "id",
+	justOne: false,
+	autopopulate: { select: PublicUserProjection },
+});
+
+MessageSchema.virtual("mention_roles", {
+	ref: RoleModel,
+	localField: "mention_role_ids",
+	foreignField: "id",
+	justOne: false,
+	autopopulate: true,
+});
+
+MessageSchema.virtual("mention_channels", {
+	ref: RoleModel,
+	localField: "mention_channel_ids",
+	foreignField: "id",
+	justOne: false,
+	autopopulate: { select: { id: true, guild_id: true, type: true, name: true } },
+});
+
+MessageSchema.virtual("referenced_message", {
+	ref: "Message",
+	localField: "message_reference.message_id",
+	foreignField: "id",
+	justOne: true,
+	autopopulate: true,
+});
+
+MessageSchema.virtual("created_at").get(function (this: MessageDocument) {
+	return new Date(Snowflake.deconstruct(this.id).timestamp);
+});
+
+MessageSchema.set("removeResponse", ["mention_channel_ids", "mention_role_ids", "mention_user_ids", "author_id"]);
+
+// TODO: missing Application Model
+// MessageSchema.virtual("application", {
+// 	ref: Application,
+// 	localField: "mention_role_ids",
+// 	foreignField: "id",
+// 	justOne: true,
+// });
+
+// @ts-ignore
+export const MessageModel = db.model<MessageDocument>("Message", MessageSchema, "messages");
diff --git a/util/src/models/RateLimit.ts b/util/src/models/RateLimit.ts
new file mode 100644
index 00000000..6a0e1ffd
--- /dev/null
+++ b/util/src/models/RateLimit.ts
@@ -0,0 +1,25 @@
+import { Schema, Document, Types } from "mongoose";
+import db from "../util/Database";
+
+export interface Bucket {
+	id: "global" | "error" | string; // channel_239842397 | guild_238927349823 | webhook_238923423498
+	user_id: string;
+	hits: number;
+	blocked: boolean;
+	expires_at: Date;
+}
+
+export interface BucketDocument extends Bucket, Document {
+	id: string;
+}
+
+export const BucketSchema = new Schema({
+	id: { type: String, required: true },
+	user_id: { type: String, required: true }, // bot, user, oauth_application, webhook
+	hits: { type: Number, required: true }, // Number of times the user hit this bucket
+	blocked: { type: Boolean, required: true },
+	expires_at: { type: Date, required: true },
+});
+
+// @ts-ignore
+export const BucketModel = db.model<BucketDocument>("Bucket", BucketSchema, "ratelimits");
diff --git a/util/src/models/ReadState.ts b/util/src/models/ReadState.ts
new file mode 100644
index 00000000..9c4fb323
--- /dev/null
+++ b/util/src/models/ReadState.ts
@@ -0,0 +1,26 @@
+import { PublicMember } from "./Member";
+import { Schema, model, Types, Document } from "mongoose";
+import db from "../util/Database";
+
+export interface ReadState extends Document {
+	message_id: string;
+	channel_id: string;
+	user_id: string;
+	last_message_id?: string;
+	last_pin_timestamp?: Date;
+	mention_count: number;
+	manual: boolean;
+}
+
+export const ReadStateSchema = new Schema({
+	message_id: String,
+	channel_id: String,
+	user_id: String,
+	last_message_id: String,
+	last_pin_timestamp: Date,
+	mention_count: Number,
+	manual: Boolean,
+});
+
+// @ts-ignore
+export const ReadStateModel = db.model<ReadState>("ReadState", ReadStateSchema, "readstates");
diff --git a/util/src/models/Role.ts b/util/src/models/Role.ts
new file mode 100644
index 00000000..c1111c84
--- /dev/null
+++ b/util/src/models/Role.ts
@@ -0,0 +1,42 @@
+import { Schema, model, Types, Document } from "mongoose";
+import db from "../util/Database";
+import toBigInt from "../util/toBigInt";
+
+export interface Role {
+	id: string;
+	guild_id: string;
+	color: number;
+	hoist: boolean;
+	managed: boolean;
+	mentionable: boolean;
+	name: string;
+	permissions: bigint;
+	position: number;
+	tags?: {
+		bot_id?: string;
+	};
+}
+
+export interface RoleDocument extends Document, Role {
+	id: string;
+}
+
+export const RoleSchema = new Schema({
+	id: String,
+	guild_id: String,
+	color: Number,
+	hoist: Boolean,
+	managed: Boolean,
+	mentionable: Boolean,
+	name: String,
+	permissions: { type: String, get: toBigInt },
+	position: Number,
+	tags: {
+		bot_id: String,
+	},
+});
+
+RoleSchema.set("removeResponse", ["guild_id"]);
+
+// @ts-ignore
+export const RoleModel = db.model<RoleDocument>("Role", RoleSchema, "roles");
diff --git a/util/src/models/Status.ts b/util/src/models/Status.ts
new file mode 100644
index 00000000..5a9bf2ca
--- /dev/null
+++ b/util/src/models/Status.ts
@@ -0,0 +1,13 @@
+export type Status = "idle" | "dnd" | "online" | "offline";
+
+export interface ClientStatus {
+	desktop?: string; // e.g. Windows/Linux/Mac
+	mobile?: string; // e.g. iOS/Android
+	web?: string; // e.g. browser, bot account
+}
+
+export const ClientStatus = {
+	desktop: String,
+	mobile: String,
+	web: String,
+};
diff --git a/util/src/models/Team.ts b/util/src/models/Team.ts
new file mode 100644
index 00000000..795c82d2
--- /dev/null
+++ b/util/src/models/Team.ts
@@ -0,0 +1,17 @@
+export interface Team {
+	icon: string | null;
+	id: string;
+	members: {
+		membership_state: number;
+		permissions: string[];
+		team_id: string;
+		user_id: string;
+	}[];
+	name: string;
+	owner_user_id: string;
+}
+
+export enum TeamMemberState {
+	INVITED = 1,
+	ACCEPTED = 2,
+}
diff --git a/util/src/models/Template.ts b/util/src/models/Template.ts
new file mode 100644
index 00000000..ad0f9104
--- /dev/null
+++ b/util/src/models/Template.ts
@@ -0,0 +1,51 @@
+import { Schema, model, Types, Document } from "mongoose";
+import db from "../util/Database";
+import { PublicUser, User, UserModel, PublicUserProjection } from "./User";
+import { Guild, GuildModel } from "./Guild";
+
+export interface Template extends Document {
+	id: string;
+	code: string;
+	name: string;
+	description?: string;
+	usage_count?: number;
+	creator_id: string;
+	creator: User;
+	created_at: Date;
+	updated_at: Date;
+	source_guild_id: String;
+	serialized_source_guild: Guild;
+}
+
+export const TemplateSchema = new Schema({
+	id: String,
+	code: String,
+	name: String,
+	description: String,
+	usage_count: Number,
+	creator_id: String,
+	created_at: Date,
+	updated_at: Date,
+	source_guild_id: String,
+});
+
+TemplateSchema.virtual("creator", {
+	ref: UserModel,
+	localField: "creator_id",
+	foreignField: "id",
+	justOne: true,
+	autopopulate: {
+		select: PublicUserProjection,
+	},
+});
+
+TemplateSchema.virtual("serialized_source_guild", {
+	ref: GuildModel,
+	localField: "source_guild_id",
+	foreignField: "id",
+	justOne: true,
+	autopopulate: true,
+});
+
+// @ts-ignore
+export const TemplateModel = db.model<Template>("Template", TemplateSchema, "templates");
diff --git a/util/src/models/User.ts b/util/src/models/User.ts
new file mode 100644
index 00000000..c667e954
--- /dev/null
+++ b/util/src/models/User.ts
@@ -0,0 +1,252 @@
+import { Activity, ActivitySchema } from "./Activity";
+import { ClientStatus, Status } from "./Status";
+import { Schema, Types, Document } from "mongoose";
+import db from "../util/Database";
+import toBigInt from "../util/toBigInt";
+
+export const PublicUserProjection = {
+	username: true,
+	discriminator: true,
+	id: true,
+	public_flags: true,
+	avatar: true,
+	accent_color: true,
+	banner: true,
+	bio: true,
+	bot: true,
+};
+
+export interface User {
+	id: string;
+	username: string; // username max length 32, min 2
+	discriminator: string; // #0001 4 digit long string from #0001 - #9999
+	avatar: string | null; // hash of the user avatar
+	accent_color: number | null; // banner color of user
+	banner: string | null;
+	phone: string | null; // phone number of the user
+	desktop: boolean; // if the user has desktop app installed
+	mobile: boolean; // if the user has mobile app installed
+	premium: boolean; // if user bought nitro
+	premium_type: number; // nitro level
+	bot: boolean; // if user is bot
+	bio: string; // short description of the user (max 190 chars)
+	system: boolean; // shouldn't be used, the api sents this field type true, if the genetaed message comes from a system generated author
+	nsfw_allowed: boolean; // if the user is older than 18 (resp. Config)
+	mfa_enabled: boolean; // if multi factor authentication is enabled
+	created_at: Date; // registration date
+	verified: boolean; // if the user is offically verified
+	disabled: boolean; // if the account is disabled
+	deleted: boolean; // if the user was deleted
+	email: string | null; // email of the user
+	flags: bigint; // UserFlags
+	public_flags: bigint;
+	user_settings: UserSettings;
+	guilds: string[]; // array of guild ids the user is part of
+	user_data: UserData;
+	presence: {
+		status: Status;
+		activities: Activity[];
+		client_status: ClientStatus;
+	};
+}
+
+// Private user data:
+export interface UserData {
+	valid_tokens_since: Date; // all tokens with a previous issue date are invalid
+	relationships: Relationship[];
+	connected_accounts: ConnectedAccount[];
+	hash: string; // hash of the password, salt is saved in password (bcrypt)
+	fingerprints: string[]; // array of fingerprints -> used to prevent multiple accounts
+}
+
+export interface UserDocument extends User, Document {
+	id: string;
+}
+
+export interface PublicUser {
+	id: string;
+	discriminator: string;
+	username: string;
+	avatar: string | null;
+	accent_color: number;
+	banner: string | null;
+	public_flags: bigint;
+	bot: boolean;
+}
+
+export interface ConnectedAccount {
+	access_token: string;
+	friend_sync: boolean;
+	id: string;
+	name: string;
+	revoked: boolean;
+	show_activity: boolean;
+	type: string;
+	verifie: boolean;
+	visibility: number;
+}
+
+export interface Relationship {
+	id: string;
+	nickname?: string;
+	type: RelationshipType;
+}
+
+export enum RelationshipType {
+	outgoing = 4,
+	incoming = 3,
+	blocked = 2,
+	friends = 1,
+}
+
+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 | null;
+		emoji_name: string | null;
+		expires_at: number | null;
+		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;
+	guild_folders: // every top guild is displayed as a "folder"
+	{
+		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";
+	stream_notifications_enabled: boolean;
+	theme: "dark" | "white"; // dark
+	timezone_offset: number; // e.g -60
+}
+
+export const UserSchema = new Schema({
+	id: String,
+	username: String,
+	discriminator: String,
+	avatar: String,
+	accent_color: Number,
+	banner: String,
+	phone: String,
+	desktop: Boolean,
+	mobile: Boolean,
+	premium: Boolean,
+	premium_type: Number,
+	bot: Boolean,
+	bio: String,
+	system: Boolean,
+	nsfw_allowed: Boolean,
+	mfa_enabled: Boolean,
+	created_at: Date,
+	verified: Boolean,
+	disabled: Boolean,
+	deleted: Boolean,
+	email: String,
+	flags: { type: String, get: toBigInt }, // TODO: automatically convert Types.Long to BitField of UserFlags
+	public_flags: { type: String, get: toBigInt },
+	guilds: [String], // array of guild ids the user is part of
+	user_data: {
+		fingerprints: [String],
+		hash: String, // hash of the password, salt is saved in password (bcrypt)
+		valid_tokens_since: Date, // all tokens with a previous issue date are invalid
+		relationships: [
+			{
+				id: { type: String, required: true },
+				nickname: String,
+				type: { type: Number },
+			},
+		],
+		connected_accounts: [
+			{
+				access_token: String,
+				friend_sync: Boolean,
+				id: String,
+				name: String,
+				revoked: Boolean,
+				show_activity: Boolean,
+				type: { type: String },
+				verifie: Boolean,
+				visibility: Number,
+			},
+		],
+	},
+	user_settings: {
+		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,
+		},
+		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: String,
+		stream_notifications_enabled: Boolean,
+		theme: String, // dark
+		timezone_offset: Number, // e.g -60,
+	},
+
+	presence: {
+		status: String,
+		activities: [ActivitySchema],
+		client_status: ClientStatus,
+	},
+});
+
+// @ts-ignore
+export const UserModel = db.model<UserDocument>("User", UserSchema, "users");
diff --git a/util/src/models/VoiceState.ts b/util/src/models/VoiceState.ts
new file mode 100644
index 00000000..c1f90edd
--- /dev/null
+++ b/util/src/models/VoiceState.ts
@@ -0,0 +1,34 @@
+import { PublicMember } from "./Member";
+import { Schema, model, Types, Document } from "mongoose";
+import db from "../util/Database";
+
+export interface VoiceState extends Document {
+	guild_id?: string;
+	channel_id: string;
+	user_id: string;
+	session_id: string;
+	deaf: boolean;
+	mute: boolean;
+	self_deaf: boolean;
+	self_mute: boolean;
+	self_stream?: boolean;
+	self_video: boolean;
+	suppress: boolean; // whether this user is muted by the current user
+}
+
+export const VoiceSateSchema = new Schema({
+	guild_id: String,
+	channel_id: String,
+	user_id: String,
+	session_id: String,
+	deaf: Boolean,
+	mute: Boolean,
+	self_deaf: Boolean,
+	self_mute: Boolean,
+	self_stream: Boolean,
+	self_video: Boolean,
+	suppress: Boolean, // whether this user is muted by the current user
+});
+
+// @ts-ignore
+export const VoiceStateModel = db.model<VoiceState>("VoiceState", VoiceSateSchema, "voicestates");
diff --git a/util/src/models/Webhook.ts b/util/src/models/Webhook.ts
new file mode 100644
index 00000000..7379e98f
--- /dev/null
+++ b/util/src/models/Webhook.ts
@@ -0,0 +1,84 @@
+import { Schema, Document, Types } from "mongoose";
+import { transpileModule } from "typescript";
+import db from "../util/Database";
+import { ChannelModel } from "./Channel";
+import { GuildModel } from "./Guild";
+
+export interface Webhook {}
+
+export enum WebhookType {
+	Incoming = 1,
+	ChannelFollower = 2,
+}
+
+export interface WebhookDocument extends Document, Webhook {
+	id: String;
+	type: number;
+	guild_id?: string;
+	channel_id: string;
+	name?: string;
+	avatar?: string;
+	token?: string;
+	application_id?: string;
+	user_id?: string;
+	source_guild_id: string;
+}
+
+export const WebhookSchema = new Schema({
+	id: { type: String, required: true },
+	type: { type: Number, required: true },
+	guild_id: String,
+	channel_id: String,
+	name: String,
+	avatar: String,
+	token: String,
+	application_id: String,
+	user_id: String,
+	source_guild_id: String,
+	source_channel_id: String,
+});
+
+WebhookSchema.virtual("source_guild", {
+	ref: GuildModel,
+	localField: "id",
+	foreignField: "source_guild_id",
+	justOne: true,
+	autopopulate: {
+		select: {
+			icon: true,
+			id: true,
+			name: true,
+		},
+	},
+});
+
+WebhookSchema.virtual("source_channel", {
+	ref: ChannelModel,
+	localField: "id",
+	foreignField: "source_channel_id",
+	justOne: true,
+	autopopulate: {
+		select: {
+			id: true,
+			name: true,
+		},
+	},
+});
+
+WebhookSchema.virtual("source_channel", {
+	ref: ChannelModel,
+	localField: "id",
+	foreignField: "source_channel_id",
+	justOne: true,
+	autopopulate: {
+		select: {
+			id: true,
+			name: true,
+		},
+	},
+});
+
+WebhookSchema.set("removeResponse", ["source_channel_id", "source_guild_id"]);
+
+// @ts-ignore
+export const WebhookModel = db.model<WebhookDocument>("Webhook", WebhookSchema, "webhooks");
diff --git a/util/src/models/index.ts b/util/src/models/index.ts
new file mode 100644
index 00000000..d0a46bf9
--- /dev/null
+++ b/util/src/models/index.ts
@@ -0,0 +1,89 @@
+import mongoose, { Schema, Document } from "mongoose";
+import mongooseAutoPopulate from "mongoose-autopopulate";
+
+type UpdateWithAggregationPipeline = UpdateAggregationStage[];
+type UpdateAggregationStage =
+	| { $addFields: any }
+	| { $set: any }
+	| { $project: any }
+	| { $unset: any }
+	| { $replaceRoot: any }
+	| { $replaceWith: any };
+type EnforceDocument<T, TMethods> = T extends Document ? T : T & Document & TMethods;
+
+declare module "mongoose" {
+	interface Model<T, TQueryHelpers = {}, TMethods = {}> {
+		// removed null -> always return document -> throw error if it doesn't exist
+		findOne(
+			filter?: FilterQuery<T>,
+			projection?: any | null,
+			options?: QueryOptions | null,
+			callback?: (err: CallbackError, doc: EnforceDocument<T, TMethods>) => void
+		): QueryWithHelpers<EnforceDocument<T, TMethods>, EnforceDocument<T, TMethods>, TQueryHelpers>;
+		findOneAndUpdate(
+			filter?: FilterQuery<T>,
+			update?: UpdateQuery<T> | UpdateWithAggregationPipeline,
+			options?: QueryOptions | null,
+			callback?: (err: any, doc: EnforceDocument<T, TMethods> | null, res: any) => void
+		): QueryWithHelpers<EnforceDocument<T, TMethods>, EnforceDocument<T, TMethods>, TQueryHelpers>;
+	}
+}
+
+var HTTPError: any;
+
+try {
+	HTTPError = require("lambert-server").HTTPError;
+} catch (e) {
+	HTTPError = Error;
+}
+
+mongoose.plugin(mongooseAutoPopulate);
+
+mongoose.plugin((schema: Schema, opts: any) => {
+	schema.set("toObject", {
+		virtuals: true,
+		versionKey: false,
+		transform(doc: any, ret: any) {
+			delete ret._id;
+			delete ret.__v;
+			const props = schema.get("removeResponse") || [];
+			props.forEach((prop: string) => {
+				delete ret[prop];
+			});
+		},
+	});
+	schema.post("findOne", function (doc, next) {
+		try {
+			// @ts-ignore
+			const isExistsQuery = JSON.stringify(this._userProvidedFields) === JSON.stringify({ _id: 1 });
+			if (!doc && !isExistsQuery) {
+				// @ts-ignore
+				return next(new HTTPError(`${this?.mongooseCollection?.name}.${this?._conditions?.id} not found`, 400));
+			}
+			// @ts-ignore
+			return next();
+		} catch (error) {
+			// @ts-ignore
+			next();
+		}
+	});
+});
+
+export * from "./Activity";
+export * from "./Application";
+export * from "./Ban";
+export * from "./Channel";
+export * from "./Emoji";
+export * from "./Event";
+export * from "./Template";
+export * from "./Guild";
+export * from "./Invite";
+export * from "./Interaction";
+export * from "./Member";
+export * from "./Message";
+export * from "./Status";
+export * from "./Role";
+export * from "./User";
+export * from "./VoiceState";
+export * from "./ReadState";
+export * from "./RateLimit";
diff --git a/util/src/util/BitField.ts b/util/src/util/BitField.ts
new file mode 100644
index 00000000..728dc632
--- /dev/null
+++ b/util/src/util/BitField.ts
@@ -0,0 +1,143 @@
+"use strict";
+
+// https://github.com/discordjs/discord.js/blob/master/src/util/BitField.js
+// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah
+
+export type BitFieldResolvable = number | BigInt | BitField | string | BitFieldResolvable[];
+
+/**
+ * Data structure that makes it easy to interact with a bitfield.
+ */
+export class BitField {
+	public bitfield: bigint = BigInt(0);
+
+	public static FLAGS: Record<string, bigint> = {};
+
+	constructor(bits: BitFieldResolvable = 0) {
+		this.bitfield = BitField.resolve.call(this, bits);
+	}
+
+	/**
+	 * Checks whether the bitfield has a bit, or any of multiple bits.
+	 */
+	any(bit: BitFieldResolvable): boolean {
+		return (this.bitfield & BitField.resolve.call(this, bit)) !== 0n;
+	}
+
+	/**
+	 * Checks if this bitfield equals another
+	 */
+	equals(bit: BitFieldResolvable): boolean {
+		return this.bitfield === BitField.resolve.call(this, bit);
+	}
+
+	/**
+	 * Checks whether the bitfield has a bit, or multiple bits.
+	 */
+	has(bit: BitFieldResolvable): boolean {
+		if (Array.isArray(bit)) return bit.every((p) => this.has(p));
+		const BIT = BitField.resolve.call(this, bit);
+		return (this.bitfield & BIT) === BIT;
+	}
+
+	/**
+	 * Gets all given bits that are missing from the bitfield.
+	 */
+	missing(bits: BitFieldResolvable) {
+		if (!Array.isArray(bits)) bits = new BitField(bits).toArray();
+		return bits.filter((p) => !this.has(p));
+	}
+
+	/**
+	 * Freezes these bits, making them immutable.
+	 */
+	freeze(): Readonly<BitField> {
+		return Object.freeze(this);
+	}
+
+	/**
+	 * Adds bits to these ones.
+	 * @param {...BitFieldResolvable} [bits] Bits to add
+	 * @returns {BitField} These bits or new BitField if the instance is frozen.
+	 */
+	add(...bits: BitFieldResolvable[]): BitField {
+		let total = 0n;
+		for (const bit of bits) {
+			total |= BitField.resolve.call(this, bit);
+		}
+		if (Object.isFrozen(this)) return new BitField(this.bitfield | total);
+		this.bitfield |= total;
+		return this;
+	}
+
+	/**
+	 * Removes bits from these.
+	 * @param {...BitFieldResolvable} [bits] Bits to remove
+	 */
+	remove(...bits: BitFieldResolvable[]) {
+		let total = 0n;
+		for (const bit of bits) {
+			total |= BitField.resolve.call(this, bit);
+		}
+		if (Object.isFrozen(this)) return new BitField(this.bitfield & ~total);
+		this.bitfield &= ~total;
+		return this;
+	}
+
+	/**
+	 * Gets an object mapping field names to a {@link boolean} indicating whether the
+	 * bit is available.
+	 * @param {...*} hasParams Additional parameters for the has method, if any
+	 */
+	serialize() {
+		const serialized: Record<string, boolean> = {};
+		for (const [flag, bit] of Object.entries(BitField.FLAGS)) serialized[flag] = this.has(bit);
+		return serialized;
+	}
+
+	/**
+	 * Gets an {@link Array} of bitfield names based on the bits available.
+	 */
+	toArray(): string[] {
+		return Object.keys(BitField.FLAGS).filter((bit) => this.has(bit));
+	}
+
+	toJSON() {
+		return this.bitfield;
+	}
+
+	valueOf() {
+		return this.bitfield;
+	}
+
+	*[Symbol.iterator]() {
+		yield* this.toArray();
+	}
+
+	/**
+	 * Data that can be resolved to give a bitfield. This can be:
+	 * * A bit number (this can be a number literal or a value taken from {@link BitField.FLAGS})
+	 * * An instance of BitField
+	 * * An Array of BitFieldResolvable
+	 * @typedef {number|BitField|BitFieldResolvable[]} BitFieldResolvable
+	 */
+
+	/**
+	 * Resolves bitfields to their numeric form.
+	 * @param {BitFieldResolvable} [bit=0] - bit(s) to resolve
+	 * @returns {number}
+	 */
+	static resolve(bit: BitFieldResolvable = 0n): bigint {
+		// @ts-ignore
+		const FLAGS = this.FLAGS || this.constructor?.FLAGS;
+		if ((typeof bit === "number" || typeof bit === "bigint") && bit >= 0n) return BigInt(bit);
+		if (bit instanceof BitField) return bit.bitfield;
+		if (Array.isArray(bit)) {
+			// @ts-ignore
+			const resolve = this.constructor?.resolve || this.resolve;
+			return bit.map((p) => resolve.call(this, p)).reduce((prev, p) => BigInt(prev) | BigInt(p), 0n);
+		}
+		if (typeof bit === "string" && typeof FLAGS[bit] !== "undefined") return FLAGS[bit];
+		throw new RangeError("BITFIELD_INVALID: " + bit);
+	}
+}
diff --git a/util/src/util/Config.ts b/util/src/util/Config.ts
new file mode 100644
index 00000000..78b44315
--- /dev/null
+++ b/util/src/util/Config.ts
@@ -0,0 +1,284 @@
+import { Schema, model, Types, Document } from "mongoose";
+import "missing-native-js-functions";
+import db, { MongooseCache } from "./Database";
+import { Snowflake } from "./Snowflake";
+import crypto from "crypto";
+
+var config: any;
+
+export default {
+	init: async function init(defaultOpts: any = DefaultOptions) {
+		config = await db.collection("config").findOne({});
+		return this.set((config || {}).merge(defaultOpts));
+	},
+	get: function get() {
+		return config as DefaultOptions;
+	},
+	set: function set(val: any) {
+		return db.collection("config").updateOne({}, { $set: val }, { upsert: true });
+	},
+};
+
+export interface RateLimitOptions {
+	bot?: number;
+	count: number;
+	window: number;
+	onyIp?: boolean;
+}
+
+export interface Region {
+	id: string;
+	name: string;
+	vip: boolean;
+	custom: boolean;
+	deprecated: boolean;
+	optimal: boolean;
+}
+
+export interface KafkaBroker {
+	ip: string;
+	port: number;
+}
+
+export interface DefaultOptions {
+	gateway: {
+		endpoint: string | null;
+	};
+	cdn: {
+		endpoint: string | null;
+	};
+	general: {
+		instance_id: string;
+	};
+	permissions: {
+		user: {
+			createGuilds: boolean;
+		};
+	};
+	limits: {
+		user: {
+			maxGuilds: number;
+			maxUsername: number;
+			maxFriends: number;
+		};
+		guild: {
+			maxRoles: number;
+			maxMembers: number;
+			maxChannels: number;
+			maxChannelsInCategory: number;
+			hideOfflineMember: number;
+		};
+		message: {
+			maxCharacters: number;
+			maxTTSCharacters: number;
+			maxReactions: number;
+			maxAttachmentSize: number;
+			maxBulkDelete: number;
+		};
+		channel: {
+			maxPins: number;
+			maxTopic: number;
+		};
+		rate: {
+			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: {
+		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;
+	};
+	login: {
+		requireCaptcha: boolean;
+	};
+	register: {
+		email: {
+			necessary: boolean; // we have to use necessary instead of required as the cli tool uses json schema and can't use required
+			allowlist: boolean;
+			blocklist: boolean;
+			domains: string[];
+		};
+		dateOfBirth: {
+			necessary: boolean;
+			minimum: number; // in years
+		};
+		requireCaptcha: boolean;
+		requireInvite: boolean;
+		allowNewRegistration: boolean;
+		allowMultipleAccounts: boolean;
+		blockProxies: boolean;
+		password: {
+			minLength: number;
+			minNumbers: number;
+			minUpperCase: number;
+			minSymbols: number;
+		};
+	};
+	regions: {
+		default: string;
+		available: Region[];
+	};
+	rabbitmq: {
+		host: string | null;
+	};
+	kafka: {
+		brokers: KafkaBroker[] | null;
+	};
+}
+
+export const DefaultOptions: DefaultOptions = {
+	gateway: {
+		endpoint: null,
+	},
+	cdn: {
+		endpoint: null,
+	},
+	general: {
+		instance_id: Snowflake.generate(),
+	},
+	permissions: {
+		user: {
+			createGuilds: true,
+		},
+	},
+	limits: {
+		user: {
+			maxGuilds: 100,
+			maxUsername: 32,
+			maxFriends: 1000,
+		},
+		guild: {
+			maxRoles: 250,
+			maxMembers: 250000,
+			maxChannels: 500,
+			maxChannelsInCategory: 50,
+			hideOfflineMember: 1000,
+		},
+		message: {
+			maxCharacters: 2000,
+			maxTTSCharacters: 200,
+			maxReactions: 20,
+			maxAttachmentSize: 8388608,
+			maxBulkDelete: 100,
+		},
+		channel: {
+			maxPins: 50,
+			maxTopic: 1024,
+		},
+		rate: {
+			ip: {
+				count: 500,
+				window: 5,
+			},
+			global: {
+				count: 20,
+				window: 5,
+				bot: 250,
+			},
+			error: {
+				count: 10,
+				window: 5,
+			},
+			routes: {
+				guild: {
+					count: 5,
+					window: 5,
+				},
+				webhook: {
+					count: 5,
+					window: 5,
+				},
+				channel: {
+					count: 5,
+					window: 5,
+				},
+				auth: {
+					login: {
+						count: 5,
+						window: 60,
+					},
+					register: {
+						count: 2,
+						window: 60 * 60 * 12,
+					},
+				},
+			},
+		},
+	},
+	security: {
+		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",
+	},
+	login: {
+		requireCaptcha: false,
+	},
+	register: {
+		email: {
+			necessary: true,
+			allowlist: false,
+			blocklist: true,
+			domains: [], // TODO: efficiently save domain blocklist in database
+			// domains: fs.readFileSync(__dirname + "/blockedEmailDomains.txt", { encoding: "utf8" }).split("\n"),
+		},
+		dateOfBirth: {
+			necessary: true,
+			minimum: 13,
+		},
+		requireInvite: false,
+		requireCaptcha: true,
+		allowNewRegistration: true,
+		allowMultipleAccounts: true,
+		blockProxies: true,
+		password: {
+			minLength: 8,
+			minNumbers: 2,
+			minUpperCase: 2,
+			minSymbols: 0,
+		},
+	},
+	regions: {
+		default: "fosscord",
+		available: [{ id: "fosscord", name: "Fosscord", vip: false, custom: false, deprecated: false, optimal: false }],
+	},
+	rabbitmq: {
+		host: null,
+	},
+	kafka: {
+		brokers: null,
+	},
+};
+
+export const ConfigSchema = new Schema({}, { strict: false });
+
+export interface DefaultOptionsDocument extends DefaultOptions, Document {}
+
+export const ConfigModel = model<DefaultOptionsDocument>("Config", ConfigSchema, "config");
diff --git a/util/src/util/Constants.ts b/util/src/util/Constants.ts
new file mode 100644
index 00000000..a9978c51
--- /dev/null
+++ b/util/src/util/Constants.ts
@@ -0,0 +1,28 @@
+import { VerifyOptions } from "jsonwebtoken";
+
+export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] };
+
+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,
+	GUILD_DISCOVERY_DISQUALIFIED = 14,
+	GUILD_DISCOVERY_REQUALIFIED = 15,
+	GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING = 16,
+	GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING = 17,
+	THREAD_CREATED = 18,
+	REPLY = 19,
+	APPLICATION_COMMAND = 20,
+	THREAD_STARTER_MESSAGE = 21,
+	GUILD_INVITE_REMINDER = 22,
+}
diff --git a/util/src/util/Database.ts b/util/src/util/Database.ts
new file mode 100644
index 00000000..8c6847a8
--- /dev/null
+++ b/util/src/util/Database.ts
@@ -0,0 +1,151 @@
+import "./MongoBigInt";
+import mongoose, { Collection, Connection, LeanDocument } from "mongoose";
+import { ChangeStream, ChangeEvent, Long } from "mongodb";
+import EventEmitter from "events";
+const uri = process.env.MONGO_URL || "mongodb://localhost:27017/fosscord?readPreference=secondaryPreferred";
+import { URL } from "url";
+
+const url = new URL(uri.replace("mongodb://", "http://"));
+
+const connection = mongoose.createConnection(uri, {
+	autoIndex: true,
+	useNewUrlParser: true,
+	useUnifiedTopology: true,
+	useFindAndModify: false,
+});
+console.log(`[Database] connect: mongodb://${url.username}@${url.host}${url.pathname}${url.search}`);
+
+export default <Connection>connection;
+
+function transform<T>(document: T) {
+	// @ts-ignore
+	if (!document || !document.toObject) {
+		try {
+			// @ts-ignore
+			delete document._id;
+			// @ts-ignore
+			delete document.__v;
+		} catch (error) {}
+		return document;
+	}
+	// @ts-ignore
+	return document.toObject({ virtuals: true });
+}
+
+export function toObject<T>(document: T): LeanDocument<T> {
+	// @ts-ignore
+	return Array.isArray(document) ? document.map((x) => transform<T>(x)) : transform(document);
+}
+
+export interface MongooseCache {
+	on(event: "delete", listener: (id: string) => void): this;
+	on(event: "change", listener: (data: any) => void): this;
+	on(event: "insert", listener: (data: any) => void): this;
+	on(event: "close", listener: () => void): this;
+}
+
+export class MongooseCache extends EventEmitter {
+	public stream: ChangeStream;
+	public data: any;
+	public initalizing?: Promise<void>;
+
+	constructor(
+		public collection: Collection,
+		public pipeline: Array<Record<string, unknown>>,
+		public opts: {
+			onlyEvents: boolean;
+			array?: boolean;
+		}
+	) {
+		super();
+		if (this.opts.array == null) this.opts.array = true;
+	}
+
+	init = () => {
+		if (this.initalizing) return this.initalizing;
+		this.initalizing = new Promise(async (resolve, reject) => {
+			// @ts-ignore
+			this.stream = this.collection.watch(this.pipeline, { fullDocument: "updateLookup" });
+
+			this.stream.on("change", this.change);
+			this.stream.on("close", this.destroy);
+			this.stream.on("error", console.error);
+
+			if (!this.opts.onlyEvents) {
+				const arr = await this.collection.aggregate(this.pipeline).toArray();
+				if (this.opts.array) this.data = arr || [];
+				else this.data = arr?.[0];
+			}
+			resolve();
+		});
+		return this.initalizing;
+	};
+
+	changeStream = (pipeline: any) => {
+		this.pipeline = pipeline;
+		this.destroy();
+		this.init();
+	};
+
+	convertResult = (obj: any) => {
+		if (obj instanceof Long) return BigInt(obj.toString());
+		if (typeof obj === "object") {
+			Object.keys(obj).forEach((key) => {
+				obj[key] = this.convertResult(obj[key]);
+			});
+		}
+
+		return obj;
+	};
+
+	change = (doc: ChangeEvent) => {
+		try {
+			switch (doc.operationType) {
+				case "dropDatabase":
+					return this.destroy();
+				case "drop":
+					return this.destroy();
+				case "delete":
+					if (!this.opts.onlyEvents) {
+						if (this.opts.array) {
+							this.data = this.data.filter((x: any) => doc.documentKey?._id?.equals(x._id));
+						} else this.data = null;
+					}
+					return this.emit("delete", doc.documentKey._id.toHexString());
+				case "insert":
+					if (!this.opts.onlyEvents) {
+						if (this.opts.array) this.data.push(doc.fullDocument);
+						else this.data = doc.fullDocument;
+					}
+					return this.emit("insert", doc.fullDocument);
+				case "update":
+				case "replace":
+					if (!this.opts.onlyEvents) {
+						if (this.opts.array) {
+							const i = this.data.findIndex((x: any) => doc.fullDocument?._id?.equals(x._id));
+							if (i == -1) this.data.push(doc.fullDocument);
+							else this.data[i] = doc.fullDocument;
+						} else this.data = doc.fullDocument;
+					}
+
+					return this.emit("change", doc.fullDocument);
+				case "invalidate":
+					return this.destroy();
+				default:
+					return;
+			}
+		} catch (error) {
+			this.emit("error", error);
+		}
+	};
+
+	destroy = () => {
+		this.data = null;
+		this.stream?.off("change", this.change);
+		this.emit("close");
+
+		if (this.stream.isClosed()) return;
+
+		return this.stream.close();
+	};
+}
diff --git a/util/src/util/Intents.ts b/util/src/util/Intents.ts
new file mode 100644
index 00000000..943b29cf
--- /dev/null
+++ b/util/src/util/Intents.ts
@@ -0,0 +1,21 @@
+import { BitField } from "./BitField";
+
+export class Intents extends BitField {
+	static FLAGS = {
+		GUILDS: BigInt(1) << BigInt(0),
+		GUILD_MEMBERS: BigInt(1) << BigInt(1),
+		GUILD_BANS: BigInt(1) << BigInt(2),
+		GUILD_EMOJIS: BigInt(1) << BigInt(3),
+		GUILD_INTEGRATIONS: BigInt(1) << BigInt(4),
+		GUILD_WEBHOOKS: BigInt(1) << BigInt(5),
+		GUILD_INVITES: BigInt(1) << BigInt(6),
+		GUILD_VOICE_STATES: BigInt(1) << BigInt(7),
+		GUILD_PRESENCES: BigInt(1) << BigInt(8),
+		GUILD_MESSAGES: BigInt(1) << BigInt(9),
+		GUILD_MESSAGE_REACTIONS: BigInt(1) << BigInt(10),
+		GUILD_MESSAGE_TYPING: BigInt(1) << BigInt(11),
+		DIRECT_MESSAGES: BigInt(1) << BigInt(12),
+		DIRECT_MESSAGE_REACTIONS: BigInt(1) << BigInt(13),
+		DIRECT_MESSAGE_TYPING: BigInt(1) << BigInt(14),
+	};
+}
diff --git a/util/src/util/MessageFlags.ts b/util/src/util/MessageFlags.ts
new file mode 100644
index 00000000..c76be4c8
--- /dev/null
+++ b/util/src/util/MessageFlags.ts
@@ -0,0 +1,14 @@
+// https://github.com/discordjs/discord.js/blob/master/src/util/MessageFlags.js
+// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah
+
+import { BitField } from "./BitField";
+
+export class MessageFlags extends BitField {
+	static FLAGS = {
+		CROSSPOSTED: BigInt(1) << BigInt(0),
+		IS_CROSSPOST: BigInt(1) << BigInt(1),
+		SUPPRESS_EMBEDS: BigInt(1) << BigInt(2),
+		SOURCE_MESSAGE_DELETED: BigInt(1) << BigInt(3),
+		URGENT: BigInt(1) << BigInt(4),
+	};
+}
diff --git a/util/src/util/MongoBigInt.ts b/util/src/util/MongoBigInt.ts
new file mode 100644
index 00000000..fc451925
--- /dev/null
+++ b/util/src/util/MongoBigInt.ts
@@ -0,0 +1,82 @@
+import mongoose from "mongoose";
+
+class LongSchema extends mongoose.SchemaType {
+	public $conditionalHandlers = {
+		$lt: this.handleSingle,
+		$lte: this.handleSingle,
+		$gt: this.handleSingle,
+		$gte: this.handleSingle,
+		$ne: this.handleSingle,
+		$in: this.handleArray,
+		$nin: this.handleArray,
+		$mod: this.handleArray,
+		$all: this.handleArray,
+		$bitsAnySet: this.handleArray,
+		$bitsAllSet: this.handleArray,
+	};
+
+	handleSingle(val: any) {
+		return this.cast(val, null, null, "handle");
+	}
+
+	handleArray(val: any) {
+		var self = this;
+		return val.map(function (m: any) {
+			return self.cast(m, null, null, "handle");
+		});
+	}
+
+	checkRequired(val: any) {
+		return null != val;
+	}
+
+	cast(val: any, scope?: any, init?: any, type?: string) {
+		if (null === val) return val;
+		if ("" === val) return null;
+		if (typeof val === "bigint") {
+			return mongoose.mongo.Long.fromString(val.toString());
+		}
+
+		if (val instanceof mongoose.mongo.Long) {
+			if (type === "handle" || init == false) return val;
+			return BigInt(val.toString());
+		}
+		if (val instanceof Number || "number" == typeof val) return BigInt(val);
+		if (!Array.isArray(val) && val.toString) return BigInt(val.toString());
+
+		//@ts-ignore
+		throw new SchemaType.CastError("Long", val);
+	}
+
+	castForQuery($conditional: string, value: any) {
+		var handler;
+		if (2 === arguments.length) {
+			// @ts-ignore
+			handler = this.$conditionalHandlers[$conditional];
+			if (!handler) {
+				throw new Error("Can't use " + $conditional + " with Long.");
+			}
+			return handler.call(this, value);
+		} else {
+			return this.cast($conditional, null, null, "query");
+		}
+	}
+}
+
+LongSchema.cast = mongoose.SchemaType.cast;
+LongSchema.set = mongoose.SchemaType.set;
+LongSchema.get = mongoose.SchemaType.get;
+
+declare module "mongoose" {
+	namespace Types {
+		class Long extends mongoose.mongo.Long {}
+	}
+	namespace Schema {
+		namespace Types {
+			class Long extends LongSchema {}
+		}
+	}
+}
+
+mongoose.Schema.Types.Long = LongSchema;
+mongoose.Types.Long = mongoose.mongo.Long;
diff --git a/util/src/util/Permissions.ts b/util/src/util/Permissions.ts
new file mode 100644
index 00000000..445e901f
--- /dev/null
+++ b/util/src/util/Permissions.ts
@@ -0,0 +1,262 @@
+// https://github.com/discordjs/discord.js/blob/master/src/util/Permissions.js
+// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah
+import { MemberDocument, MemberModel } from "../models/Member";
+import { ChannelDocument, ChannelModel } from "../models/Channel";
+import { ChannelPermissionOverwrite } from "../models/Channel";
+import { Role, RoleDocument, RoleModel } from "../models/Role";
+import { BitField } from "./BitField";
+import { GuildDocument, GuildModel } from "../models/Guild";
+// TODO: check role hierarchy permission
+
+var HTTPError: any;
+
+try {
+	HTTPError = require("lambert-server").HTTPError;
+} catch (e) {
+	HTTPError = Error;
+}
+
+export type PermissionResolvable = bigint | number | Permissions | PermissionResolvable[] | PermissionString;
+
+type PermissionString =
+	| "CREATE_INSTANT_INVITE"
+	| "KICK_MEMBERS"
+	| "BAN_MEMBERS"
+	| "ADMINISTRATOR"
+	| "MANAGE_CHANNELS"
+	| "MANAGE_GUILD"
+	| "ADD_REACTIONS"
+	| "VIEW_AUDIT_LOG"
+	| "PRIORITY_SPEAKER"
+	| "STREAM"
+	| "VIEW_CHANNEL"
+	| "SEND_MESSAGES"
+	| "SEND_TTS_MESSAGES"
+	| "MANAGE_MESSAGES"
+	| "EMBED_LINKS"
+	| "ATTACH_FILES"
+	| "READ_MESSAGE_HISTORY"
+	| "MENTION_EVERYONE"
+	| "USE_EXTERNAL_EMOJIS"
+	| "VIEW_GUILD_INSIGHTS"
+	| "CONNECT"
+	| "SPEAK"
+	| "MUTE_MEMBERS"
+	| "DEAFEN_MEMBERS"
+	| "MOVE_MEMBERS"
+	| "USE_VAD"
+	| "CHANGE_NICKNAME"
+	| "MANAGE_NICKNAMES"
+	| "MANAGE_ROLES"
+	| "MANAGE_WEBHOOKS"
+	| "MANAGE_EMOJIS";
+
+const CUSTOM_PERMISSION_OFFSET = BigInt(1) << BigInt(48); // 16 free custom permission bits, and 16 for discord to add new ones
+
+export class Permissions extends BitField {
+	cache: PermissionCache = {};
+
+	static FLAGS = {
+		CREATE_INSTANT_INVITE: BigInt(1) << BigInt(0),
+		KICK_MEMBERS: BigInt(1) << BigInt(1),
+		BAN_MEMBERS: BigInt(1) << BigInt(2),
+		ADMINISTRATOR: BigInt(1) << BigInt(3),
+		MANAGE_CHANNELS: BigInt(1) << BigInt(4),
+		MANAGE_GUILD: BigInt(1) << BigInt(5),
+		ADD_REACTIONS: BigInt(1) << BigInt(6),
+		VIEW_AUDIT_LOG: BigInt(1) << BigInt(7),
+		PRIORITY_SPEAKER: BigInt(1) << BigInt(8),
+		STREAM: BigInt(1) << BigInt(9),
+		VIEW_CHANNEL: BigInt(1) << BigInt(10),
+		SEND_MESSAGES: BigInt(1) << BigInt(11),
+		SEND_TTS_MESSAGES: BigInt(1) << BigInt(12),
+		MANAGE_MESSAGES: BigInt(1) << BigInt(13),
+		EMBED_LINKS: BigInt(1) << BigInt(14),
+		ATTACH_FILES: BigInt(1) << BigInt(15),
+		READ_MESSAGE_HISTORY: BigInt(1) << BigInt(16),
+		MENTION_EVERYONE: BigInt(1) << BigInt(17),
+		USE_EXTERNAL_EMOJIS: BigInt(1) << BigInt(18),
+		VIEW_GUILD_INSIGHTS: BigInt(1) << BigInt(19),
+		CONNECT: BigInt(1) << BigInt(20),
+		SPEAK: BigInt(1) << BigInt(21),
+		MUTE_MEMBERS: BigInt(1) << BigInt(22),
+		DEAFEN_MEMBERS: BigInt(1) << BigInt(23),
+		MOVE_MEMBERS: BigInt(1) << BigInt(24),
+		USE_VAD: BigInt(1) << BigInt(25),
+		CHANGE_NICKNAME: BigInt(1) << BigInt(26),
+		MANAGE_NICKNAMES: BigInt(1) << BigInt(27),
+		MANAGE_ROLES: BigInt(1) << BigInt(28),
+		MANAGE_WEBHOOKS: BigInt(1) << BigInt(29),
+		MANAGE_EMOJIS: BigInt(1) << BigInt(30),
+		/**
+		 * CUSTOM PERMISSIONS ideas:
+		 * - allow user to dm members
+		 * - allow user to pin messages (without MANAGE_MESSAGES)
+		 * - allow user to publish messages (without MANAGE_MESSAGES)
+		 */
+		// CUSTOM_PERMISSION: BigInt(1) << BigInt(0) + CUSTOM_PERMISSION_OFFSET
+	};
+
+	any(permission: PermissionResolvable, checkAdmin = true) {
+		return (checkAdmin && super.any(Permissions.FLAGS.ADMINISTRATOR)) || super.any(permission);
+	}
+
+	/**
+	 * Checks whether the bitfield has a permission, or multiple permissions.
+	 */
+	has(permission: PermissionResolvable, checkAdmin = true) {
+		return (checkAdmin && super.has(Permissions.FLAGS.ADMINISTRATOR)) || super.has(permission);
+	}
+
+	/**
+	 * Checks whether the bitfield has a permission, or multiple permissions, but throws an Error if user fails to match auth criteria.
+	 */
+	hasThrow(permission: PermissionResolvable) {
+		if (this.has(permission) && this.has("VIEW_CHANNEL")) return true;
+		// @ts-ignore
+		throw new HTTPError(`You are missing the following permissions ${permission}`, 403);
+	}
+
+	overwriteChannel(overwrites: ChannelPermissionOverwrite[]) {
+		if (!this.cache) throw new Error("permission chache not available");
+		overwrites = overwrites.filter((x) => {
+			if (x.type === 0 && this.cache.roles?.some((r) => r.id === x.id)) return true;
+			if (x.type === 1 && x.id == this.cache.user_id) return true;
+			return false;
+		});
+		return new Permissions(Permissions.channelPermission(overwrites, this.bitfield));
+	}
+
+	static channelPermission(overwrites: ChannelPermissionOverwrite[], init?: bigint) {
+		// TODO: do not deny any permissions if admin
+		return overwrites.reduce((permission, overwrite) => {
+			// apply disallowed permission
+			// * permission: current calculated permission (e.g. 010)
+			// * deny contains all denied permissions (e.g. 011)
+			// * allow contains all explicitly allowed permisions (e.g. 100)
+			return (permission & ~BigInt(overwrite.deny)) | BigInt(overwrite.allow);
+			// ~ operator inverts deny (e.g. 011 -> 100)
+			// & operator only allows 1 for both ~deny and permission (e.g. 010 & 100 -> 000)
+			// | operators adds both together (e.g. 000 + 100 -> 100)
+		}, init || 0n);
+	}
+
+	static rolePermission(roles: Role[]) {
+		// adds all permissions of all roles together (Bit OR)
+		return roles.reduce((permission, role) => permission | BigInt(role.permissions), 0n);
+	}
+
+	static finalPermission({
+		user,
+		guild,
+		channel,
+	}: {
+		user: { id: string; roles: string[] };
+		guild: { roles: Role[] };
+		channel?: {
+			overwrites?: ChannelPermissionOverwrite[];
+			recipient_ids?: string[] | null;
+			owner_id?: string;
+		};
+	}) {
+		if (user.id === "0") return new Permissions("ADMINISTRATOR"); // system user id
+
+		let roles = guild.roles.filter((x) => user.roles.includes(x.id));
+		let permission = Permissions.rolePermission(roles);
+
+		if (channel?.overwrites) {
+			let overwrites = channel.overwrites.filter((x) => {
+				if (x.type === 0 && user.roles.includes(x.id)) return true;
+				if (x.type === 1 && x.id == user.id) return true;
+				return false;
+			});
+			permission = Permissions.channelPermission(overwrites, permission);
+		}
+
+		if (channel?.recipient_ids) {
+			if (channel?.owner_id === user.id) return new Permissions("ADMINISTRATOR");
+			if (channel.recipient_ids.includes(user.id)) {
+				// Default dm permissions
+				return new Permissions([
+					"VIEW_CHANNEL",
+					"SEND_MESSAGES",
+					"STREAM",
+					"ADD_REACTIONS",
+					"EMBED_LINKS",
+					"ATTACH_FILES",
+					"READ_MESSAGE_HISTORY",
+					"MENTION_EVERYONE",
+					"USE_EXTERNAL_EMOJIS",
+					"CONNECT",
+					"SPEAK",
+					"MANAGE_CHANNELS",
+				]);
+			}
+
+			return new Permissions();
+		}
+
+		return new Permissions(permission);
+	}
+}
+
+export type PermissionCache = {
+	channel?: ChannelDocument | null;
+	member?: MemberDocument | null;
+	guild?: GuildDocument | null;
+	roles?: RoleDocument[] | null;
+	user_id?: string;
+};
+
+export async function getPermission(
+	user_id?: string,
+	guild_id?: string,
+	channel_id?: string,
+	cache: PermissionCache = {}
+) {
+	var { channel, member, guild, roles } = cache;
+
+	if (!user_id) throw new HTTPError("User not found");
+
+	if (channel_id && !channel) {
+		channel = await ChannelModel.findOne(
+			{ id: channel_id },
+			{ permission_overwrites: true, recipient_ids: true, owner_id: true, guild_id: true }
+		).exec();
+		if (!channel) throw new HTTPError("Channel not found", 404);
+		if (channel.guild_id) guild_id = channel.guild_id;
+	}
+
+	if (guild_id) {
+		if (!guild) guild = await GuildModel.findOne({ id: guild_id }, { owner_id: true }).exec();
+		if (!guild) throw new HTTPError("Guild not found");
+		if (guild.owner_id === user_id) return new Permissions(Permissions.FLAGS.ADMINISTRATOR);
+
+		if (!member) member = await MemberModel.findOne({ guild_id, id: user_id }, "roles").exec();
+		if (!member) throw new HTTPError("Member not found");
+
+		if (!roles) roles = await RoleModel.find({ guild_id, id: { $in: member.roles } }).exec();
+	}
+
+	var permission = Permissions.finalPermission({
+		user: {
+			id: user_id,
+			roles: member?.roles || [],
+		},
+		guild: {
+			roles: roles || [],
+		},
+		channel: {
+			overwrites: channel?.permission_overwrites,
+			owner_id: channel?.owner_id,
+			recipient_ids: channel?.recipient_ids,
+		},
+	});
+
+	const obj = new Permissions(permission);
+
+	// pass cache to permission for possible future getPermission calls
+	obj.cache = { guild, member, channel, roles, user_id };
+
+	return obj;
+}
diff --git a/util/src/util/RabbitMQ.ts b/util/src/util/RabbitMQ.ts
new file mode 100644
index 00000000..9da41990
--- /dev/null
+++ b/util/src/util/RabbitMQ.ts
@@ -0,0 +1,18 @@
+import amqp, { Connection, Channel } from "amqplib";
+import Config from "./Config";
+
+export const RabbitMQ: { connection: Connection | null; channel: Channel | null; init: () => Promise<void> } = {
+	connection: null,
+	channel: null,
+	init: async function () {
+		const host = Config.get().rabbitmq.host;
+		if (!host) return;
+		console.log(`[RabbitMQ] connect: ${host}`);
+		this.connection = await amqp.connect(host, {
+			timeout: 1000 * 60,
+		});
+		console.log(`[RabbitMQ] connected`);
+		this.channel = await this.connection.createChannel();
+		console.log(`[RabbitMQ] channel created`);
+	},
+};
diff --git a/util/src/util/Regex.ts b/util/src/util/Regex.ts
new file mode 100644
index 00000000..bbd48bca
--- /dev/null
+++ b/util/src/util/Regex.ts
@@ -0,0 +1,3 @@
+export const DOUBLE_WHITE_SPACE = /\s\s+/g;
+export const SPECIAL_CHAR = /[@#`:\r\n\t\f\v\p{C}]/gu;
+export const CHANNEL_MENTION = /<#(\d+)>/g;
diff --git a/util/src/util/Snowflake.ts b/util/src/util/Snowflake.ts
new file mode 100644
index 00000000..1d725710
--- /dev/null
+++ b/util/src/util/Snowflake.ts
@@ -0,0 +1,127 @@
+// @ts-nocheck
+import cluster from "cluster";
+
+// https://github.com/discordjs/discord.js/blob/master/src/util/Snowflake.js
+// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah
+("use strict");
+
+// Discord epoch (2015-01-01T00:00:00.000Z)
+
+/**
+ * A container for useful snowflake-related methods.
+ */
+export class Snowflake {
+	static readonly EPOCH = 1420070400000;
+	static INCREMENT = 0n; // max 4095
+	static processId = BigInt(process.pid % 31); // max 31
+	static workerId = BigInt((cluster.worker?.id || 0) % 31); // max 31
+
+	constructor() {
+		throw new Error(`The ${this.constructor.name} class may not be instantiated.`);
+	}
+
+	/**
+	 * A Twitter snowflake, except the epoch is 2015-01-01T00:00:00.000Z
+	 * ```
+	 * If we have a snowflake '266241948824764416' we can represent it as binary:
+	 *
+	 * 64                                          22     17     12          0
+	 *  000000111011000111100001101001000101000000  00001  00000  000000000000
+	 *       number of ms since Discord epoch       worker  pid    increment
+	 * ```
+	 * @typedef {string} Snowflake
+	 */
+
+	/**
+	 * Transforms a snowflake from a decimal string to a bit string.
+	 * @param  {Snowflake} num Snowflake to be transformed
+	 * @returns {string}
+	 * @private
+	 */
+	static idToBinary(num) {
+		let bin = "";
+		let high = parseInt(num.slice(0, -10)) || 0;
+		let low = parseInt(num.slice(-10));
+		while (low > 0 || high > 0) {
+			bin = String(low & 1) + bin;
+			low = Math.floor(low / 2);
+			if (high > 0) {
+				low += 5000000000 * (high % 2);
+				high = Math.floor(high / 2);
+			}
+		}
+		return bin;
+	}
+
+	/**
+	 * Transforms a snowflake from a bit string to a decimal string.
+	 * @param  {string} num Bit string to be transformed
+	 * @returns {Snowflake}
+	 * @private
+	 */
+	static binaryToID(num) {
+		let dec = "";
+
+		while (num.length > 50) {
+			const high = parseInt(num.slice(0, -32), 2);
+			const low = parseInt((high % 10).toString(2) + num.slice(-32), 2);
+
+			dec = (low % 10).toString() + dec;
+			num =
+				Math.floor(high / 10).toString(2) +
+				Math.floor(low / 10)
+					.toString(2)
+					.padStart(32, "0");
+		}
+
+		num = parseInt(num, 2);
+		while (num > 0) {
+			dec = (num % 10).toString() + dec;
+			num = Math.floor(num / 10);
+		}
+
+		return dec;
+	}
+
+	static generate() {
+		var time = BigInt(Date.now() - Snowflake.EPOCH) << 22n;
+		var worker = Snowflake.workerId << 17n;
+		var process = Snowflake.processId << 12n;
+		var increment = Snowflake.INCREMENT++;
+		return (time | worker | process | increment).toString();
+	}
+
+	/**
+	 * A deconstructed snowflake.
+	 * @typedef {Object} DeconstructedSnowflake
+	 * @property {number} timestamp Timestamp the snowflake was created
+	 * @property {Date} date Date the snowflake was created
+	 * @property {number} workerID Worker ID in the snowflake
+	 * @property {number} processID Process ID in the snowflake
+	 * @property {number} increment Increment in the snowflake
+	 * @property {string} binary Binary representation of the snowflake
+	 */
+
+	/**
+	 * Deconstructs a Discord snowflake.
+	 * @param {Snowflake} snowflake Snowflake to deconstruct
+	 * @returns {DeconstructedSnowflake} Deconstructed snowflake
+	 */
+	static deconstruct(snowflake) {
+		const BINARY = Snowflake.idToBinary(snowflake).toString(2).padStart(64, "0");
+		const res = {
+			timestamp: parseInt(BINARY.substring(0, 42), 2) + Snowflake.EPOCH,
+			workerID: parseInt(BINARY.substring(42, 47), 2),
+			processID: parseInt(BINARY.substring(47, 52), 2),
+			increment: parseInt(BINARY.substring(52, 64), 2),
+			binary: BINARY,
+		};
+		Object.defineProperty(res, "date", {
+			get: function get() {
+				return new Date(this.timestamp);
+			},
+			enumerable: true,
+		});
+		return res;
+	}
+}
diff --git a/util/src/util/String.ts b/util/src/util/String.ts
new file mode 100644
index 00000000..55f11e8d
--- /dev/null
+++ b/util/src/util/String.ts
@@ -0,0 +1,7 @@
+import { SPECIAL_CHAR } from "./Regex";
+
+export function trimSpecial(str?: string): string {
+	// @ts-ignore
+	if (!str) return;
+	return str.replace(SPECIAL_CHAR, "").trim();
+}
diff --git a/util/src/util/UserFlags.ts b/util/src/util/UserFlags.ts
new file mode 100644
index 00000000..72394eff
--- /dev/null
+++ b/util/src/util/UserFlags.ts
@@ -0,0 +1,22 @@
+// https://github.com/discordjs/discord.js/blob/master/src/util/UserFlags.js
+// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah
+
+import { BitField } from "./BitField";
+
+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),
+		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),
+		SYSTEM: BigInt(1) << BigInt(12),
+		BUGHUNTER_LEVEL_2: BigInt(1) << BigInt(14),
+		VERIFIED_BOT: BigInt(1) << BigInt(16),
+		EARLY_VERIFIED_BOT_DEVELOPER: BigInt(1) << BigInt(17),
+	};
+}
diff --git a/util/src/util/checkToken.ts b/util/src/util/checkToken.ts
new file mode 100644
index 00000000..91bf08d5
--- /dev/null
+++ b/util/src/util/checkToken.ts
@@ -0,0 +1,24 @@
+import { JWTOptions } from "./Constants";
+import jwt from "jsonwebtoken";
+import { UserModel } from "../models";
+
+export function checkToken(token: string, jwtSecret: string): Promise<any> {
+	return new Promise((res, rej) => {
+		token = token.replace("Bot ", ""); // TODO: proper bot support
+		jwt.verify(token, jwtSecret, JWTOptions, async (err, decoded: any) => {
+			if (err || !decoded) return rej("Invalid Token");
+
+			const user = await UserModel.findOne(
+				{ id: decoded.id },
+				{ "user_data.valid_tokens_since": true, bot: true, disabled: true, deleted: true }
+			).exec();
+			if (!user) return rej("Invalid Token");
+			// we need to round it to seconds as it saved as seconds in jwt iat and valid_tokens_since is stored in milliseconds
+			if (decoded.iat * 1000 < user.user_data.valid_tokens_since.setSeconds(0, 0)) return rej("Invalid Token");
+			if (user.disabled) return rej("User disabled");
+			if (user.deleted) return rej("User not found");
+
+			return res({ decoded, user });
+		});
+	});
+}
diff --git a/util/src/util/index.ts b/util/src/util/index.ts
new file mode 100644
index 00000000..7523a6ad
--- /dev/null
+++ b/util/src/util/index.ts
@@ -0,0 +1,9 @@
+export * from "./String";
+export * from "./BitField";
+export * from "./Intents";
+export * from "./MessageFlags";
+export * from "./Permissions";
+export * from "./Snowflake";
+export * from "./UserFlags";
+export * from "./toBigInt";
+export * from "./RabbitMQ";
diff --git a/util/src/util/toBigInt.ts b/util/src/util/toBigInt.ts
new file mode 100644
index 00000000..d57c4568
--- /dev/null
+++ b/util/src/util/toBigInt.ts
@@ -0,0 +1,3 @@
+export default function toBigInt(string: String): BigInt {
+	return BigInt(string);
+}