mirror of
https://github.com/spacebarchat/server.git
synced 2026-06-07 02:21:45 +00:00
🚧 typeorm
This commit is contained in:
@@ -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, {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+90
-81
@@ -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[];
|
||||
}
|
||||
@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;
|
||||
}[];
|
||||
|
||||
// @ts-ignore
|
||||
global.User = User;
|
||||
@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 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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`);
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export default function toBigInt(string: string): bigint {
|
||||
return BigInt(string);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user