🚧 typeorm

This commit is contained in:
Flam3rboy
2021-08-22 12:41:21 +02:00
parent 9fda915b37
commit 4a34892d46
18 changed files with 120 additions and 94 deletions
+13 -11
View File
@@ -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
View File
@@ -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
}
+80
View File
@@ -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);
}
}
+143
View File
@@ -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);
}
}
+28
View File
@@ -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,
}
+14
View File
@@ -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),
};
}
+262
View File
@@ -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;
}
+18
View File
@@ -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`);
},
};
+7
View File
@@ -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;
+127
View File
@@ -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;
}
}
+7
View File
@@ -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();
}
+22
View File
@@ -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),
};
}
+24
View File
@@ -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 });
});
});
}
+4
View File
@@ -0,0 +1,4 @@
export default function toBigInt(string: string): bigint {
return BigInt(string);
}