diff options
author | Flam3rboy <34555296+Flam3rboy@users.noreply.github.com> | 2021-08-22 12:41:21 +0200 |
---|---|---|
committer | Flam3rboy <34555296+Flam3rboy@users.noreply.github.com> | 2021-08-22 12:41:21 +0200 |
commit | 4a34892d46165db9309a55484aff2e1a67ad91bc (patch) | |
tree | dfe5d93c9ae169b413a548b4ea5aeef948801dea /util/src | |
parent | :construction: typeorm (diff) | |
download | server-4a34892d46165db9309a55484aff2e1a67ad91bc.tar.xz |
:construction: typeorm
Diffstat (limited to 'util/src')
-rw-r--r-- | util/src/models/BaseClass.ts | 24 | ||||
-rw-r--r-- | util/src/models/User.ts | 171 | ||||
-rw-r--r-- | util/src/util/AutoUpdate.ts | 80 | ||||
-rw-r--r-- | util/src/util/BitField.ts | 143 | ||||
-rw-r--r-- | util/src/util/Constants.ts | 28 | ||||
-rw-r--r-- | util/src/util/MessageFlags.ts | 14 | ||||
-rw-r--r-- | util/src/util/Permissions.ts | 262 | ||||
-rw-r--r-- | util/src/util/RabbitMQ.ts | 18 | ||||
-rw-r--r-- | util/src/util/Regex.ts | 7 | ||||
-rw-r--r-- | util/src/util/Snowflake.ts | 127 | ||||
-rw-r--r-- | util/src/util/String.ts | 7 | ||||
-rw-r--r-- | util/src/util/UserFlags.ts | 22 | ||||
-rw-r--r-- | util/src/util/checkToken.ts | 24 | ||||
-rw-r--r-- | util/src/util/toBigInt.ts | 4 |
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); +} + |