summary refs log tree commit diff
path: root/util/src
diff options
context:
space:
mode:
Diffstat (limited to 'util/src')
-rw-r--r--util/src/models/BaseClass.ts24
-rw-r--r--util/src/models/User.ts171
-rw-r--r--util/src/util/AutoUpdate.ts80
-rw-r--r--util/src/util/BitField.ts143
-rw-r--r--util/src/util/Constants.ts28
-rw-r--r--util/src/util/MessageFlags.ts14
-rw-r--r--util/src/util/Permissions.ts262
-rw-r--r--util/src/util/RabbitMQ.ts18
-rw-r--r--util/src/util/Regex.ts7
-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/toBigInt.ts4
14 files changed, 839 insertions, 92 deletions
diff --git a/util/src/models/BaseClass.ts b/util/src/models/BaseClass.ts
index 78cd329c..d4f635f6 100644
--- a/util/src/models/BaseClass.ts
+++ b/util/src/models/BaseClass.ts
@@ -1,22 +1,24 @@
 import "reflect-metadata";
-import { BaseEntity, Column } from "typeorm";
+import { BaseEntity, BeforeInsert, BeforeUpdate, Column, PrimaryGeneratedColumn } from "typeorm";
+import { Snowflake } from "../util/Snowflake";
+import { IsString, validateOrReject } from "class-validator";
 
 export class BaseClass extends BaseEntity {
+	@PrimaryGeneratedColumn()
 	@Column()
-	id?: string;
+	@IsString()
+	id: string;
 
-	constructor(props?: any) {
+	constructor(props?: any, opts: { id?: string } = {}) {
 		super();
-		BaseClass.assign(props, this, "body.");
+		this.id = opts.id || Snowflake.generate();
+		Object.defineProperties(this, props);
 	}
 
-	private static assign(props: any, object: any, path?: string): any {
-		const expectedType = Reflect.getMetadata("design:type", object, props);
-		console.log(expectedType, object, props, path, typeof object);
-
-		if (typeof object !== typeof props) throw new Error(`Property at ${path} must be`);
-		if (typeof object === "object")
-			return Object.keys(object).map((key) => BaseClass.assign(props[key], object[key], `${path}.${key}`));
+	@BeforeUpdate()
+	@BeforeInsert()
+	async validate() {
+		await validateOrReject(this, {});
 	}
 }
 
diff --git a/util/src/models/User.ts b/util/src/models/User.ts
index 38045738..27aa63d1 100644
--- a/util/src/models/User.ts
+++ b/util/src/models/User.ts
@@ -2,6 +2,7 @@ import { Column, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm";
 import { Activity } from "./Activity";
 import { BaseClass } from "./BaseClass";
 import { ClientStatus, Status } from "./Status";
+import { validateOrReject, IsInt, IsEmail, IsPhoneNumber, IsBoolean, IsString, ValidateNested } from "class-validator";
 
 export const PublicUserProjection = {
 	username: true,
@@ -16,67 +17,80 @@ export const PublicUserProjection = {
 };
 
 export class User extends BaseClass {
-	@PrimaryGeneratedColumn()
-	id: string;
-
 	@Column()
+	@IsString()
 	username: string; // username max length 32, min 2 (should be configurable)
 
 	@Column()
+	@IsInt()
 	discriminator: string; // #0001 4 digit long string from #0001 - #9999
 
 	@Column()
+	@IsString()
 	avatar: string | null; // hash of the user avatar
 
 	@Column()
+	@IsInt()
 	accent_color: number | null; // banner color of user
 
 	@Column()
 	banner: string | null; // hash of the user banner
 
 	@Column()
+	@IsPhoneNumber()
 	phone: string | null; // phone number of the user
 
 	@Column()
+	@IsBoolean()
 	desktop: boolean; // if the user has desktop app installed
 
 	@Column()
+	@IsBoolean()
 	mobile: boolean; // if the user has mobile app installed
 
 	@Column()
+	@IsBoolean()
 	premium: boolean; // if user bought nitro
 
 	@Column()
 	premium_type: number; // nitro level
 
 	@Column()
+	@IsBoolean()
 	bot: boolean; // if user is bot
 
 	@Column()
 	bio: string; // short description of the user (max 190 chars -> should be configurable)
 
 	@Column()
+	@IsBoolean()
 	system: boolean; // shouldn't be used, the api sents this field type true, if the generated message comes from a system generated author
 
 	@Column()
+	@IsBoolean()
 	nsfw_allowed: boolean; // if the user is older than 18 (resp. Config)
 
 	@Column()
+	@IsBoolean()
 	mfa_enabled: boolean; // if multi factor authentication is enabled
 
 	@Column()
 	created_at: Date; // registration date
 
 	@Column()
+	@IsBoolean()
 	verified: boolean; // if the user is offically verified
 
 	@Column()
+	@IsBoolean()
 	disabled: boolean; // if the account is disabled
 
 	@Column()
+	@IsBoolean()
 	deleted: boolean; // if the user was deleted
 
 	@Column()
+	@IsEmail()
 	email: string | null; // email of the user
 
 	@Column()
@@ -86,15 +100,19 @@ export class User extends BaseClass {
 	public_flags: bigint;
 
 	@Column("simple-array") // string in simple-array must not contain commas
+	@IsString({ each: true })
 	guilds: string[]; // array of guild ids the user is part of
 
 	@Column("simple-json")
-	user_settings: UserSettings;
-
-	@Column("simple-json")
-	user_data: UserData;
+	@ValidateNested() // TODO: https://github.com/typestack/class-validator#validating-nested-objects
+	user_data: {
+		valid_tokens_since: Date; // all tokens with a previous issue date are invalid
+		hash: string; // hash of the password, salt is saved in password (bcrypt)
+		fingerprints: string[]; // array of fingerprints -> used to prevent multiple accounts
+	};
 
 	@Column("simple-json")
+	@ValidateNested() // TODO: https://github.com/typestack/class-validator#validating-nested-objects
 	presence: {
 		status: Status;
 		activities: Activity[];
@@ -102,22 +120,76 @@ export class User extends BaseClass {
 	};
 
 	@Column("simple-json")
-	relationships: Relationship[];
+	@ValidateNested() // TODO: https://github.com/typestack/class-validator#validating-nested-objects
+	relationships: {
+		id: string;
+		nickname?: string;
+		type: RelationshipType;
+	}[];
 
 	@Column("simple-json")
-	connected_accounts: ConnectedAccount[];
-}
-
-// @ts-ignore
-global.User = User;
+	@ValidateNested() // TODO: https://github.com/typestack/class-validator#validating-nested-objects
+	connected_accounts: {
+		access_token: string;
+		friend_sync: boolean;
+		id: string;
+		name: string;
+		revoked: boolean;
+		show_activity: boolean;
+		type: string;
+		verifie: boolean;
+		visibility: number;
+	}[];
 
-// Private user data that should never get sent to the client
-export interface UserData {
-	valid_tokens_since: Date; // all tokens with a previous issue date are invalid
-	hash: string; // hash of the password, salt is saved in password (bcrypt)
-	fingerprints: string[]; // array of fingerprints -> used to prevent multiple accounts
+	@Column("simple-json")
+	@ValidateNested() // TODO: https://github.com/typestack/class-validator#validating-nested-objects
+	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 | 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
+	};
 }
 
+// Private user data that should never get sent to the client
 export interface PublicUser {
 	id: string;
 	discriminator: string;
@@ -129,72 +201,9 @@ export interface PublicUser {
 	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
-}
diff --git a/util/src/util/AutoUpdate.ts b/util/src/util/AutoUpdate.ts
new file mode 100644
index 00000000..a2ce73c2
--- /dev/null
+++ b/util/src/util/AutoUpdate.ts
@@ -0,0 +1,80 @@
+import "missing-native-js-functions";
+import fetch from "node-fetch";
+import readline from "readline";
+import fs from "fs/promises";
+import path from "path";
+
+const rl = readline.createInterface({
+	input: process.stdin,
+	output: process.stdout,
+});
+
+export function enableAutoUpdate(opts: {
+	checkInterval: number | boolean;
+	packageJsonLink: string;
+	path: string;
+	downloadUrl: string;
+	downloadType?: "zip";
+}) {
+	if (!opts.checkInterval) return;
+	var interval = 1000 * 60 * 60 * 24;
+	if (typeof opts.checkInterval === "number") opts.checkInterval = 1000 * interval;
+
+	const i = setInterval(async () => {
+		const currentVersion = await getCurrentVersion(opts.path);
+		const latestVersion = await getLatestVersion(opts.packageJsonLink);
+		if (currentVersion !== latestVersion) {
+			clearInterval(i);
+			console.log(`[Auto Update] Current version (${currentVersion}) is out of date, updating ...`);
+			await download(opts.downloadUrl, opts.path);
+		}
+	}, interval);
+	setImmediate(async () => {
+		const currentVersion = await getCurrentVersion(opts.path);
+		const latestVersion = await getLatestVersion(opts.packageJsonLink);
+		if (currentVersion !== latestVersion) {
+			rl.question(
+				`[Auto Update] Current version (${currentVersion}) is out of date, would you like to update? (yes/no)`,
+				(answer) => {
+					if (answer.toBoolean()) {
+						console.log(`[Auto update] updating ...`);
+						download(opts.downloadUrl, opts.path);
+					} else {
+					}
+				}
+			);
+		}
+	});
+}
+
+async function download(url: string, dir: string) {
+	try {
+		// TODO: use file stream instead of buffer (to prevent crash because of high memory usage for big files)
+		// TODO check file hash
+		const response = await fetch(url);
+		const buffer = await response.buffer();
+		const tempDir = await fs.mkdtemp("fosscord");
+		fs.writeFile(path.join(tempDir, "Fosscord.zip"), buffer);
+	} catch (error) {
+		console.error(`[Auto Update] download failed`, error);
+	}
+}
+
+async function getCurrentVersion(dir: string) {
+	try {
+		const content = await fs.readFile(path.join(dir, "package.json"), { encoding: "utf8" });
+		return JSON.parse(content).version;
+	} catch (error) {
+		throw new Error("[Auto update] couldn't get current version in " + dir);
+	}
+}
+
+async function getLatestVersion(url: string) {
+	try {
+		const response = await fetch(url);
+		const content = await response.json();
+		return content.version;
+	} catch (error) {
+		throw new Error("[Auto update] check failed for " + url);
+	}
+}
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/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/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/Permissions.ts b/util/src/util/Permissions.ts
new file mode 100644
index 00000000..63d87e48
--- /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_AND_STICKERS";
+
+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_AND_STICKERS: 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..83fc9fe8
--- /dev/null
+++ b/util/src/util/Regex.ts
@@ -0,0 +1,7 @@
+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;
+export const USER_MENTION = /<@!?(\d+)>/g;
+export const ROLE_MENTION = /<@&(\d+)>/g;
+export const EVERYONE_MENTION = /@everyone/g;
+export const HERE_MENTION = /@here/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/toBigInt.ts b/util/src/util/toBigInt.ts
new file mode 100644
index 00000000..b7985928
--- /dev/null
+++ b/util/src/util/toBigInt.ts
@@ -0,0 +1,4 @@
+export default function toBigInt(string: string): bigint {
+	return BigInt(string);
+}
+