diff --git a/rtc/src/util/BitField.ts b/rtc/src/util/BitField.ts
new file mode 100644
index 00000000..728dc632
--- /dev/null
+++ b/rtc/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/rtc/src/util/Config.ts b/rtc/src/util/Config.ts
new file mode 100644
index 00000000..78b44315
--- /dev/null
+++ b/rtc/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/rtc/src/util/Constants.ts b/rtc/src/util/Constants.ts
new file mode 100644
index 00000000..a9978c51
--- /dev/null
+++ b/rtc/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/rtc/src/util/Database.ts b/rtc/src/util/Database.ts
new file mode 100644
index 00000000..8c6847a8
--- /dev/null
+++ b/rtc/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/rtc/src/util/Intents.ts b/rtc/src/util/Intents.ts
new file mode 100644
index 00000000..943b29cf
--- /dev/null
+++ b/rtc/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/rtc/src/util/MessageFlags.ts b/rtc/src/util/MessageFlags.ts
new file mode 100644
index 00000000..c76be4c8
--- /dev/null
+++ b/rtc/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/rtc/src/util/MongoBigInt.ts b/rtc/src/util/MongoBigInt.ts
new file mode 100644
index 00000000..fc451925
--- /dev/null
+++ b/rtc/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/rtc/src/util/Permissions.ts b/rtc/src/util/Permissions.ts
new file mode 100644
index 00000000..445e901f
--- /dev/null
+++ b/rtc/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/rtc/src/util/RabbitMQ.ts b/rtc/src/util/RabbitMQ.ts
new file mode 100644
index 00000000..9da41990
--- /dev/null
+++ b/rtc/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/rtc/src/util/Regex.ts b/rtc/src/util/Regex.ts
new file mode 100644
index 00000000..bbd48bca
--- /dev/null
+++ b/rtc/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/rtc/src/util/Snowflake.ts b/rtc/src/util/Snowflake.ts
new file mode 100644
index 00000000..1d725710
--- /dev/null
+++ b/rtc/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/rtc/src/util/String.ts b/rtc/src/util/String.ts
new file mode 100644
index 00000000..55f11e8d
--- /dev/null
+++ b/rtc/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/rtc/src/util/UserFlags.ts b/rtc/src/util/UserFlags.ts
new file mode 100644
index 00000000..72394eff
--- /dev/null
+++ b/rtc/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/rtc/src/util/checkToken.ts b/rtc/src/util/checkToken.ts
new file mode 100644
index 00000000..91bf08d5
--- /dev/null
+++ b/rtc/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/rtc/src/util/index.ts b/rtc/src/util/index.ts
new file mode 100644
index 00000000..7523a6ad
--- /dev/null
+++ b/rtc/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/rtc/src/util/toBigInt.ts b/rtc/src/util/toBigInt.ts
new file mode 100644
index 00000000..d57c4568
--- /dev/null
+++ b/rtc/src/util/toBigInt.ts
@@ -0,0 +1,3 @@
+export default function toBigInt(string: String): BigInt {
+ return BigInt(string);
+}
|