summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json3
-rw-r--r--package.json3
-rw-r--r--src/models/Activity.ts39
-rw-r--r--src/models/Channel.ts41
-rw-r--r--src/models/Emoji.ts25
-rw-r--r--src/models/Event.ts19
-rw-r--r--src/models/Guild.ts57
-rw-r--r--src/models/Invite.ts44
-rw-r--r--src/models/Member.ts47
-rw-r--r--src/models/Message.ts106
-rw-r--r--src/models/Role.ts20
-rw-r--r--src/models/Status.ts6
-rw-r--r--src/models/User.ts100
-rw-r--r--src/models/VoiceState.ts19
-rw-r--r--src/util/Config.ts1
-rw-r--r--src/util/Database.ts94
-rw-r--r--src/util/MongoBigInt.ts76
17 files changed, 654 insertions, 46 deletions
diff --git a/package-lock.json b/package-lock.json
index fff37683..4e40ede6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,7 +11,8 @@
 			"dependencies": {
 				"jsonwebtoken": "^8.5.1",
 				"lambert-db": "^1.1.7",
-				"missing-native-js-functions": "^1.2.2"
+				"missing-native-js-functions": "^1.2.2",
+				"mongodb": "^3.6.4"
 			},
 			"devDependencies": {
 				"@types/jsonwebtoken": "^8.5.0",
diff --git a/package.json b/package.json
index be9e0907..4e2b4c1e 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,8 @@
 	"dependencies": {
 		"jsonwebtoken": "^8.5.1",
 		"lambert-db": "^1.1.7",
-		"missing-native-js-functions": "^1.2.2"
+		"missing-native-js-functions": "^1.2.2",
+		"mongodb": "^3.6.4"
 	},
 	"devDependencies": {
 		"@types/jsonwebtoken": "^8.5.0",
diff --git a/src/models/Activity.ts b/src/models/Activity.ts
index e9e4224f..ee7e87cd 100644
--- a/src/models/Activity.ts
+++ b/src/models/Activity.ts
@@ -1,5 +1,6 @@
 import { User } from "..";
 import { ClientStatus, Status } from "./Status";
+import { Schema, model, Types, Document } from "mongoose";
 
 export interface Presence {
 	user: User;
@@ -45,6 +46,44 @@ export interface Activity {
 	flags?: bigint;
 }
 
+export const Activity = {
+	name: String,
+	type: Number,
+	url: String,
+	created_at: Number,
+	timestamps: [
+		{
+			start: Number,
+			end: Number,
+		},
+	],
+	application_id: Types.Long,
+	details: String,
+	state: String,
+	emoji: {
+		name: String,
+		id: Types.Long,
+		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: Types.Long,
+};
+
 export enum ActivityType {
 	GAME = 0,
 	STREAMING = 1,
diff --git a/src/models/Channel.ts b/src/models/Channel.ts
index 068f6b67..36f5e83d 100644
--- a/src/models/Channel.ts
+++ b/src/models/Channel.ts
@@ -1,19 +1,44 @@
+import { Schema, model, Types, Document } from "mongoose";
+
+export interface ChannelDocument extends Channel, DMChannel, TextChannel, VoiceChannel, Document {
+	id: bigint;
+}
+
+export const ChannelSchema = new Schema({
+	id: Types.Long,
+	created_at: { type: Schema.Types.Date, required: true },
+	name: { type: String, required: true },
+	type: { type: Number, required: true },
+	guild_id: Types.Long,
+	owner_id: Types.Long,
+	parent_id: Types.Long,
+	recipients: [Types.Long],
+	position: Number,
+	last_message_id: Types.Long,
+	last_pin_timestamp: Date,
+	nsfw: Boolean,
+	rate_limit_per_user: Number,
+	topic: String,
+	permission_overwrites: [
+		{
+			allow: Types.Long,
+			deny: Types.Long,
+			id: Types.Long,
+			type: Number,
+		},
+	],
+});
+
+export const ChannelModel = model<ChannelDocument>("Channel", ChannelSchema, "channels");
+
 export interface Channel {
 	id: bigint;
 	created_at: number;
 	name: string;
 	type: number;
-	read_state: ReadState[];
-}
-
-export interface ReadState {
-	last_message_id: bigint;
-	last_pin_timestamp: number;
-	mention_count: number;
 }
 
 export interface TextBasedChannel {
-	messages: any[];
 	last_message_id?: bigint;
 	last_pin_timestamp?: number;
 }
diff --git a/src/models/Emoji.ts b/src/models/Emoji.ts
index 1facc252..bbed9323 100644
--- a/src/models/Emoji.ts
+++ b/src/models/Emoji.ts
@@ -1,12 +1,27 @@
-export interface Emoji {
-	allNamesString: string; // e.g. :thonk:
+import { Schema, model, Types, Document } from "mongoose";
+
+export interface Emoji extends Document {
+	id: bigint;
 	animated: boolean;
 	available: boolean;
-	guildId: bigint;
-	id: bigint;
+	guild_id: bigint;
 	managed: boolean;
 	name: string;
 	require_colons: boolean;
 	url: string;
-	roles: [];
+	roles: bigint[]; // roles this emoji is whitelisted to
 }
+
+export const EmojiSchema = new Schema({
+	id: Types.Long,
+	animated: Boolean,
+	available: Boolean,
+	guild_id: Types.Long,
+	managed: Boolean,
+	name: String,
+	require_colons: Boolean,
+	url: String,
+	roles: [Types.Long],
+});
+
+export const EmojiModel = model<Emoji>("Emoji", EmojiSchema, "emojis");
diff --git a/src/models/Event.ts b/src/models/Event.ts
index 4925c7ca..e8dfe11e 100644
--- a/src/models/Event.ts
+++ b/src/models/Event.ts
@@ -10,8 +10,9 @@ import { Message, PartialEmoji } from "./Message";
 import { VoiceState } from "./VoiceState";
 import { ApplicationCommand } from "./Application";
 import { Interaction } from "./Interaction";
+import { Schema, model, Types, Document } from "mongoose";
 
-export interface Event {
+export interface Event extends Document {
 	guild_id?: bigint;
 	user_id?: bigint;
 	channel_id?: bigint;
@@ -20,6 +21,17 @@ export interface Event {
 	data?: any;
 }
 
+export const EventSchema = new Schema({
+	guild_id: Types.Long,
+	user_id: Types.Long,
+	channel_id: Types.Long,
+	created_at: { type: Number, required: true },
+	event: { type: String, required: true },
+	data: Object,
+});
+
+export const EventModel = model<Event>("Event", EventSchema, "events");
+
 // ! Custom Events that shouldn't get sent to the client but processed by the server
 
 export interface InvalidatedEvent extends Event {
@@ -120,7 +132,10 @@ export interface GuildUpdateEvent extends Event {
 
 export interface GuildDeleteEvent extends Event {
 	event: "GUILD_DELETE";
-	data: Guild;
+	data: {
+		id: bigint;
+		unavailable?: boolean;
+	};
 }
 
 export interface GuildBanAddEvent extends Event {
diff --git a/src/models/Guild.ts b/src/models/Guild.ts
index 348f8c7c..c9a55301 100644
--- a/src/models/Guild.ts
+++ b/src/models/Guild.ts
@@ -1,9 +1,7 @@
-import { GuildChannel } from "./Channel";
-import { Emoji } from "./Emoji";
-import { Member } from "./Member";
-import { Role } from "./Role";
+import { Schema, model, Types, Document } from "mongoose";
 
-export interface Guild {
+export interface Guild extends Document {
+	id: bigint;
 	afk_channel_id?: bigint;
 	afk_timeout?: number;
 	application_id?: bigint;
@@ -11,12 +9,9 @@ export interface Guild {
 	default_message_notifications?: number;
 	description?: string;
 	discovery_splash?: string;
-	emojis: Emoji[];
 	explicit_content_filter?: number;
 	features: string[];
 	icon?: string;
-	id: bigint;
-	// joined_at?: number; \n // owner?: boolean;  // ! member specific should be removed
 	large?: boolean;
 	max_members?: number; // e.g. default 100.000
 	max_presences?: number;
@@ -24,8 +19,9 @@ export interface Guild {
 	member_count?: number;
 	presence_count?: number; // users online
 	// members?: Member[]; // * Members are stored in a seperate collection
-	// roles: Role[]; // * Role are stroed in a seperate collection
-	// channels: GuildChannel[]; // * Channels are stroed 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
 	mfa_level?: number;
 	name: string;
 	owner_id: bigint;
@@ -46,3 +42,44 @@ export interface Guild {
 	widget_channel_id?: bigint;
 	widget_enabled?: boolean;
 }
+
+export const GuildSchema = new Schema({
+	afk_channel_id: Types.Long,
+	afk_timeout: Number,
+	application_id: Types.Long,
+	banner: String,
+	default_message_notifications: Number,
+	description: String,
+	discovery_splash: String,
+	explicit_content_filter: Number,
+	features: { type: [String], default: [] },
+	icon: String,
+	id: { type: Types.Long, required: true },
+	large: Boolean,
+	max_members: { type: Number, default: 100000 },
+	max_presences: Number,
+	max_video_channel_users: { type: Number, default: 25 },
+	member_count: Number,
+	presence_count: Number,
+	mfa_level: Number,
+	name: { type: String, required: true },
+	owner_id: { type: Types.Long, required: true },
+	preferred_locale: String,
+	premium_subscription_count: Number,
+	premium_tier: Number,
+	public_updates_channel_id: Types.Long,
+	region: String,
+	rules_channel_id: Types.Long,
+	splash: String,
+	system_channel_flags: Number,
+	system_channel_id: Types.Long,
+	unavailable: Boolean,
+	vanity_url_code: String,
+	verification_level: Number,
+	voice_states: { type: [Object], default: [] },
+	welcome_screen: { type: [Object], default: [] },
+	widget_channel_id: Types.Long,
+	widget_enabled: Boolean,
+});
+
+export const GuildModel = model<Guild>("Guild", GuildSchema, "guilds");
diff --git a/src/models/Invite.ts b/src/models/Invite.ts
index df1286f5..b4dbb8bc 100644
--- a/src/models/Invite.ts
+++ b/src/models/Invite.ts
@@ -1,4 +1,6 @@
-export interface Invite {
+import { Schema, model, Types, Document } from "mongoose";
+
+export interface Invite extends Document {
 	code: string;
 	temporary: boolean;
 	uses: number;
@@ -19,7 +21,6 @@ export interface Invite {
 		name: string;
 		type: number;
 	};
-
 	inviter: {
 		id: bigint;
 		username: string;
@@ -34,3 +35,42 @@ export interface Invite {
 	};
 	target_user_type: number;
 }
+
+export const InviteSchema = new Schema({
+	code: String,
+	temporary: Boolean,
+	uses: Number,
+	max_uses: Number,
+	max_age: Number,
+	created_at: Number,
+	guild: {
+		id: Types.Long,
+		name: String,
+		splash: String,
+		description: String,
+		icon: String,
+		features: Object,
+		verification_level: Number,
+	},
+	channel: {
+		id: Types.Long,
+		name: String,
+		type: Number,
+	},
+
+	inviter: {
+		id: Types.Long,
+		username: String,
+		avatar: String,
+		discriminator: Number,
+	},
+	target_user: {
+		id: Types.Long,
+		username: String,
+		avatar: String,
+		discriminator: Number,
+	},
+	target_user_type: Number,
+});
+
+export const InviteModel = model<Invite>("Invite", InviteSchema, "invites");
diff --git a/src/models/Member.ts b/src/models/Member.ts
index a38a5ca3..d9f34ac8 100644
--- a/src/models/Member.ts
+++ b/src/models/Member.ts
@@ -1,6 +1,7 @@
 import { PublicUser } from "./User";
+import { Schema, model, Types, Document } from "mongoose";
 
-export interface Member {
+export interface Member extends Document {
 	id: bigint;
 	nick?: string;
 	roles: bigint[];
@@ -13,10 +14,6 @@ export interface Member {
 	settings: UserGuildSettings;
 }
 
-export interface PublicMember extends Omit<Member, "settings" | "id"> {
-	user: PublicUser;
-}
-
 export interface UserGuildSettings {
 	channel_overrides: {
 		channel_id: bigint;
@@ -37,3 +34,43 @@ 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: Types.Long,
+	nick: String,
+	roles: [Types.Long],
+	joined_at: Number,
+	premium_since: Number,
+	deaf: Boolean,
+	mute: Boolean,
+	pending: Boolean,
+	permissions: Types.Long,
+	settings: {
+		channel_overrides: [
+			{
+				channel_id: Types.Long,
+				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 const MemberModel = model<Member>("Member", MemberSchema, "members");
+
+export interface PublicMember extends Omit<Member, "settings" | "id"> {
+	user: PublicUser;
+}
diff --git a/src/models/Message.ts b/src/models/Message.ts
index 45aefd34..22569d8f 100644
--- a/src/models/Message.ts
+++ b/src/models/Message.ts
@@ -1,6 +1,7 @@
+import { Schema, model, Types, Document } from "mongoose";
 import { ChannelType } from "./Channel";
 
-export interface Message {
+export interface Message extends Document {
 	id: bigint;
 	author_id?: bigint;
 	webhook_id?: bigint;
@@ -27,7 +28,7 @@ export interface Message {
 	activity?: {
 		type: number;
 		party_id: string;
-	}[];
+	};
 	flags?: bigint;
 	stickers?: [];
 	message_reference?: {
@@ -124,3 +125,104 @@ export interface AllowedMentions {
 	users?: bigint[];
 	replied_user?: boolean;
 }
+
+const Attachment = {
+	id: Types.Long, // 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)
+};
+
+const EmbedImage = {
+	url: String,
+	proxy_url: String,
+	height: Number,
+	width: Number,
+};
+
+const Reaction = {
+	count: Number,
+	emoji: {
+		id: Types.Long,
+		name: String,
+		animated: Boolean,
+	},
+};
+
+const Embed = {
+	title: String, //title of embed
+	type: String, // type of embed (always "rich" for webhook embeds)
+	description: String, // description of embed
+	url: String, // url of embed
+	timestamp: Number, // 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: Types.Long,
+	author_id: Types.Long,
+	webhook_id: Types.Long,
+	application_id: Types.Long,
+	content: String,
+	timestamp: Number,
+	edited_timestamp: Number,
+	tts: Boolean,
+	mention_everyone: Boolean,
+	mentions: [Types.Long],
+	mention_roles: [Types.Long],
+	mention_channels: [
+		{
+			id: Types.Long,
+			guild_id: Types.Long,
+			type: ChannelType,
+			name: String,
+		},
+	],
+	attachments: [Attachment],
+	embeds: [Embed],
+	reactions: [Reaction],
+	nonce: Schema.Types.Mixed, // can be a long or a string
+	pinned: Boolean,
+	type: MessageType,
+	activity: {
+		type: Number,
+		party_id: String,
+	},
+	flags: Types.Long,
+	stickers: [],
+	message_reference: {
+		message_id: Types.Long,
+		channel_id: Types.Long,
+		guild_id: Types.Long,
+	},
+});
+
+export const MessageModel = model<Message>("Message", MessageSchema, "messages");
diff --git a/src/models/Role.ts b/src/models/Role.ts
index e0f2f863..d35bd57c 100644
--- a/src/models/Role.ts
+++ b/src/models/Role.ts
@@ -1,4 +1,6 @@
-export interface Role {
+import { Schema, model, Types, Document } from "mongoose";
+
+export interface Role extends Document {
 	id: bigint;
 	color: number;
 	hoist: boolean;
@@ -11,3 +13,19 @@ export interface Role {
 		bot_id?: bigint;
 	};
 }
+
+export const RoleSchema = new Schema({
+	id: Types.Long,
+	color: Number,
+	hoist: Boolean,
+	managed: Boolean,
+	mentionable: Boolean,
+	name: String,
+	permissions: Types.Long,
+	position: Number,
+	tags: {
+		bot_id: Types.Long,
+	},
+});
+
+export const RoleModel = model<Role>("Role", RoleSchema, "roles");
diff --git a/src/models/Status.ts b/src/models/Status.ts
index c4dab586..5a9bf2ca 100644
--- a/src/models/Status.ts
+++ b/src/models/Status.ts
@@ -5,3 +5,9 @@ export interface ClientStatus {
 	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/src/models/User.ts b/src/models/User.ts
index f591d26e..d79e7e0c 100644
--- a/src/models/User.ts
+++ b/src/models/User.ts
@@ -1,7 +1,8 @@
 import { Activity } from "./Activity";
 import { ClientStatus, Status } from "./Status";
+import { Schema, model, Types, Document } from "mongoose";
 
-export interface User {
+export interface User extends Document {
 	id: bigint;
 	username: string;
 	discriminator: string;
@@ -103,3 +104,100 @@ export interface UserSettings {
 	theme: "dark" | "white"; // dark
 	timezone_offset: number; // e.g -60
 }
+
+export const UserSchema = new Schema({
+	id: Types.Long,
+	username: String,
+	discriminator: String,
+	avatar: String,
+	phone: String,
+	desktop: Boolean,
+	mobile: Boolean,
+	premium: Boolean,
+	premium_type: Number,
+	bot: Boolean,
+	system: Boolean,
+	nsfw_allowed: Boolean,
+	mfa_enabled: Boolean,
+	created_at: Number,
+	verified: Boolean,
+	email: String,
+	flags: Types.Long, // TODO: automatically convert Types.Long to BitField of UserFlags
+	public_flags: Types.Long,
+	hash: String, // hash of the password, salt is saved in password (bcrypt)
+	guilds: [Types.Long], // array of guild ids the user is part of
+	valid_tokens_since: Number, // all tokens with a previous issue date are invalid
+	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: Types.Long,
+			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 },
+		gif_auto_play: Boolean,
+		// every top guild is displayed as a "folder"
+		guild_folders: [
+			{
+				color: Number,
+				guild_ids: [Types.Long],
+				id: Number,
+				name: String,
+			},
+		],
+		guild_positions: [Types.Long], // 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: [Types.Long],
+		show_current_game: Boolean,
+		status: String,
+		stream_notifications_enabled: Boolean,
+		theme: String, // dark
+		timezone_offset: Number, // e.g -60,
+	},
+	relationships: [
+		{
+			id: Types.Long,
+			nickname: String,
+			type: Number,
+			user_id: Types.Long,
+		},
+	],
+	connected_accounts: [
+		{
+			access_token: String,
+			friend_sync: Boolean,
+			id: String,
+			name: String,
+			revoked: Boolean,
+			show_activity: Boolean,
+			type: String,
+			verifie: Boolean,
+			visibility: Number,
+		},
+	],
+	presence: {
+		status: String,
+		activities: [Activity],
+		client_status: ClientStatus,
+	},
+});
+
+export const UserModel = model<User>("User", UserSchema, "users");
diff --git a/src/models/VoiceState.ts b/src/models/VoiceState.ts
index 19a1bf27..8ac5ae19 100644
--- a/src/models/VoiceState.ts
+++ b/src/models/VoiceState.ts
@@ -1,6 +1,7 @@
 import { PublicMember } from "./Member";
+import { Schema, model, Types, Document } from "mongoose";
 
-export interface VoiceState {
+export interface VoiceState extends Document {
 	guild_id?: bigint;
 	channel_id: bigint;
 	user_id: bigint;
@@ -13,3 +14,19 @@ export interface VoiceState {
 	self_video: boolean;
 	suppress: boolean; // whether this user is muted by the current user
 }
+
+export const VoiceSateSchema = new Schema({
+	guild_id: Types.Long,
+	channel_id: Types.Long,
+	user_id: Types.Long,
+	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 VoiceStateModel = model<VoiceState>("VoiceState", VoiceSateSchema, "voicestates");
diff --git a/src/util/Config.ts b/src/util/Config.ts
index 5886b268..91ffda01 100644
--- a/src/util/Config.ts
+++ b/src/util/Config.ts
@@ -5,6 +5,7 @@ var Config: ProviderCache;
 
 export default {
 	init: async function init(opts: DefaultOptions = DefaultOptions) {
+		await db.collection("config").findOne({});
 		Config = await db.data.config({}).cache();
 		await Config.init();
 		await Config.set(opts.merge(Config.cache || {}));
diff --git a/src/util/Database.ts b/src/util/Database.ts
index ed45a9ad..56f53f9a 100644
--- a/src/util/Database.ts
+++ b/src/util/Database.ts
@@ -1,9 +1,89 @@
-import { MongoDatabase } from "lambert-db";
+import "./MongoBigInt";
+import mongoose, { Collection } from "mongoose";
+import { ChangeStream, ChangeEvent, Long } from "mongodb";
+import EventEmitter from "events";
+const uri = process.env.MONGO_URL || "mongodb://localhost:27017/fosscord?readPreference=secondaryPreferred";
 
-// TODO: load url from config
-const db = new MongoDatabase("mongodb://127.0.0.1:27017/lambert?readPreference=secondaryPreferred", {
-	useNewUrlParser: true,
-	useUnifiedTopology: false,
-});
+const connection = mongoose.createConnection(uri, { autoIndex: true });
 
-export default db;
+export default connection;
+
+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;
+
+	constructor(
+		public collection: Collection,
+		public pipeline: Array<Record<string, unknown>>,
+		public opts: {
+			onlyEvents: boolean;
+		}
+	) {
+		super();
+	}
+
+	async init() {
+		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) {
+			this.data = await this.collection.aggregate(this.pipeline).toArray();
+		}
+	}
+
+	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) => {
+		// @ts-ignore
+		if (doc.fullDocument) {
+			// @ts-ignore
+			if (!this.opts.onlyEvents) this.data = doc.fullDocument;
+		}
+
+		switch (doc.operationType) {
+			case "dropDatabase":
+				return this.destroy();
+			case "drop":
+				return this.destroy();
+			case "delete":
+				return this.emit("delete", doc.documentKey._id.toHexString());
+			case "insert":
+				return this.emit("insert", doc.fullDocument);
+			case "update":
+			case "replace":
+				return this.emit("change", doc.fullDocument);
+			case "invalidate":
+				return this.destroy();
+			default:
+				return;
+		}
+	};
+
+	destroy() {
+		this.stream.off("change", this.change);
+		this.emit("close");
+
+		if (this.stream.isClosed()) return;
+
+		return this.stream.close();
+	}
+}
diff --git a/src/util/MongoBigInt.ts b/src/util/MongoBigInt.ts
new file mode 100644
index 00000000..cc185bed
--- /dev/null
+++ b/src/util/MongoBigInt.ts
@@ -0,0 +1,76 @@
+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);
+	}
+
+	handleArray(val: any) {
+		var self = this;
+		return val.map(function (m: any) {
+			return self.cast(m);
+		});
+	}
+
+	checkRequired(val: any) {
+		return null != val;
+	}
+
+	cast(val: any, scope?: any, init?: any) {
+		if (null === val) return val;
+		if ("" === val) return null;
+
+		if (val instanceof mongoose.mongo.Long) 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);
+		}
+	}
+}
+
+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;