summary refs log tree commit diff
path: root/rtc/src/util
diff options
context:
space:
mode:
Diffstat (limited to 'rtc/src/util')
-rw-r--r--rtc/src/util/BitField.ts143
-rw-r--r--rtc/src/util/Config.ts284
-rw-r--r--rtc/src/util/Constants.ts28
-rw-r--r--rtc/src/util/Database.ts151
-rw-r--r--rtc/src/util/Intents.ts21
-rw-r--r--rtc/src/util/MessageFlags.ts14
-rw-r--r--rtc/src/util/MongoBigInt.ts82
-rw-r--r--rtc/src/util/Permissions.ts262
-rw-r--r--rtc/src/util/RabbitMQ.ts18
-rw-r--r--rtc/src/util/Regex.ts3
-rw-r--r--rtc/src/util/Snowflake.ts127
-rw-r--r--rtc/src/util/String.ts7
-rw-r--r--rtc/src/util/UserFlags.ts22
-rw-r--r--rtc/src/util/checkToken.ts24
-rw-r--r--rtc/src/util/index.ts9
-rw-r--r--rtc/src/util/toBigInt.ts3
16 files changed, 1198 insertions, 0 deletions
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); +}