diff --git a/util/src/entities/AuditLog.ts b/util/src/entities/AuditLog.ts
index ae9feb76..4b81ed6a 100644
--- a/util/src/entities/AuditLog.ts
+++ b/util/src/entities/AuditLog.ts
@@ -55,10 +55,7 @@ export class AuditLog extends BaseClass {
@ManyToOne(() => User, (user: User) => user.id)
user: User;
- @Column({
- type: "simple-enum",
- enum: AuditLogEvents,
- })
+ @Column({ type: "int" })
action_type: AuditLogEvents;
@Column({ type: "simple-json", nullable: true })
diff --git a/util/src/entities/BaseClass.ts b/util/src/entities/BaseClass.ts
index beccf04b..d20078e5 100644
--- a/util/src/entities/BaseClass.ts
+++ b/util/src/entities/BaseClass.ts
@@ -1,19 +1,8 @@
import "reflect-metadata";
-import {
- BaseEntity,
- BeforeInsert,
- BeforeUpdate,
- EntityMetadata,
- FindConditions,
- ObjectIdColumn,
- PrimaryColumn,
-} from "typeorm";
+import { BaseEntity, EntityMetadata, FindConditions, ObjectIdColumn, PrimaryColumn } from "typeorm";
import { Snowflake } from "../util/Snowflake";
import "missing-native-js-functions";
-// TODO use class-validator https://typeorm.io/#/validation with class annotators (isPhone/isEmail) combined with types from typescript-json-schema
-// btw. we don't use class-validator for everything, because we need to explicitly set the type instead of deriving it from typescript also it doesn't easily support nested objects
-
export class BaseClassWithoutId extends BaseEntity {
constructor(props?: any) {
super();
@@ -42,7 +31,7 @@ export class BaseClassWithoutId extends BaseEntity {
for (const key in props) {
if (!properties.has(key)) continue;
// @ts-ignore
- const setter = this[`set${key.capitalize()}`];
+ const setter = this[`set${key.capitalize()}`]; // use setter function if it exists
if (setter) {
setter.call(this, props[key]);
@@ -53,12 +42,6 @@ export class BaseClassWithoutId extends BaseEntity {
}
}
- @BeforeUpdate()
- @BeforeInsert()
- validate() {
- return this;
- }
-
toJSON(): any {
return Object.fromEntries(
this.metadata.columns // @ts-ignore
@@ -76,42 +59,6 @@ export class BaseClassWithoutId extends BaseEntity {
const repository = this.getRepository();
return repository.decrement(conditions, propertyPath, value);
}
-
- // static async delete<T>(criteria: FindConditions<T>, options?: RemoveOptions) {
- // if (!criteria) throw new Error("You need to specify delete criteria");
-
- // const repository = this.getRepository();
- // const promises = repository.metadata.relations.map(async (x) => {
- // if (x.orphanedRowAction !== "delete") return;
-
- // const foreignKey =
- // x.foreignKeys.find((key) => key.entityMetadata === repository.metadata) ||
- // x.inverseRelation?.foreignKeys[0]; // find foreign key for this entity
- // if (!foreignKey) {
- // throw new Error(
- // `Foreign key not found for entity ${repository.metadata.name} in relation ${x.propertyName}`
- // );
- // }
- // const id = (criteria as any)[foreignKey.referencedColumnNames[0]];
- // if (!id) throw new Error("id missing in criteria options " + foreignKey.referencedColumnNames);
-
- // if (x.relationType === "many-to-many") {
- // return getConnection()
- // .createQueryBuilder()
- // .relation(this, x.propertyName)
- // .of(id)
- // .remove({ [foreignKey.columnNames[0]]: id });
- // } else if (
- // x.relationType === "one-to-one" ||
- // x.relationType === "many-to-one" ||
- // x.relationType === "one-to-many"
- // ) {
- // return (x.inverseEntityMetadata.target as any).delete({ [foreignKey.columnNames[0]]: id });
- // }
- // });
- // await Promise.all(promises);
- // return super.delete(criteria, options);
- // }
}
export const PrimaryIdColumn = process.env.DATABASE?.startsWith("mongodb") ? ObjectIdColumn : PrimaryColumn;
diff --git a/util/src/entities/Channel.ts b/util/src/entities/Channel.ts
index 51d8b026..bd2e5a58 100644
--- a/util/src/entities/Channel.ts
+++ b/util/src/entities/Channel.ts
@@ -39,7 +39,7 @@ export class Channel extends BaseClass {
@Column({ type: "text", nullable: true })
icon?: string | null;
- @Column({ type: "simple-enum", enum: ChannelType })
+ @Column({ type: "int" })
type: ChannelType;
@OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, {
diff --git a/util/src/entities/Config.ts b/util/src/entities/Config.ts
index 813649ac..b3167ac7 100644
--- a/util/src/entities/Config.ts
+++ b/util/src/entities/Config.ts
@@ -51,11 +51,6 @@ export interface ConfigValue {
general: {
instanceId: string;
};
- permissions: {
- user: {
- createGuilds: boolean;
- };
- };
limits: {
user: {
maxGuilds: number;
@@ -64,6 +59,7 @@ export interface ConfigValue {
};
guild: {
maxRoles: number;
+ maxEmojis: number;
maxMembers: number;
maxChannels: number;
maxChannelsInCategory: number;
@@ -153,6 +149,11 @@ export interface ConfigValue {
canLeave: boolean;
};
};
+ gif: {
+ enabled: boolean;
+ provider: "tenor"; // more coming soon
+ apiKey?: string;
+ };
rabbitmq: {
host: string | null;
};
@@ -175,11 +176,6 @@ export const DefaultConfigOptions: ConfigValue = {
general: {
instanceId: Snowflake.generate(),
},
- permissions: {
- user: {
- createGuilds: true,
- },
- },
limits: {
user: {
maxGuilds: 100,
@@ -188,6 +184,7 @@ export const DefaultConfigOptions: ConfigValue = {
},
guild: {
maxRoles: 250,
+ maxEmojis: 50, // TODO: max emojis per guild per nitro level
maxMembers: 250000,
maxChannels: 500,
maxChannelsInCategory: 50,
@@ -305,7 +302,6 @@ export const DefaultConfigOptions: ConfigValue = {
},
],
},
-
guild: {
showAllGuildsInDiscovery: false,
autoJoin: {
@@ -314,6 +310,11 @@ export const DefaultConfigOptions: ConfigValue = {
guilds: [],
},
},
+ gif: {
+ enabled: true,
+ provider: "tenor",
+ apiKey: "LIVDSRZULELA",
+ },
rabbitmq: {
host: null,
},
diff --git a/util/src/entities/Emoji.ts b/util/src/entities/Emoji.ts
index a252d9f4..03218375 100644
--- a/util/src/entities/Emoji.ts
+++ b/util/src/entities/Emoji.ts
@@ -1,4 +1,5 @@
-import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { User } from ".";
import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild";
import { Role } from "./Role";
@@ -20,6 +21,14 @@ export class Emoji extends BaseClass {
})
guild: Guild;
+ @Column({ nullable: true })
+ @RelationId((emoji: Emoji) => emoji.user)
+ user_id: string;
+
+ @JoinColumn({ name: "user_id" })
+ @ManyToOne(() => User)
+ user: User;
+
@Column()
managed: boolean;
@@ -28,4 +37,7 @@ export class Emoji extends BaseClass {
@Column()
require_colons: boolean;
+
+ @Column({ type: "simple-array" })
+ roles: string[]; // roles this emoji is whitelisted to (new discord feature?)
}
diff --git a/util/src/entities/Guild.ts b/util/src/entities/Guild.ts
index 35595191..157f0921 100644
--- a/util/src/entities/Guild.ts
+++ b/util/src/entities/Guild.ts
@@ -258,14 +258,6 @@ export class Guild extends BaseClass {
unavailable?: boolean;
@Column({ nullable: true })
- @RelationId((guild: Guild) => guild.vanity_url)
- vanity_url_code?: string;
-
- @JoinColumn({ name: "vanity_url_code" })
- @ManyToOne(() => Invite)
- vanity_url?: Invite;
-
- @Column({ nullable: true })
verification_level?: number;
@Column({ type: "simple-json" })
diff --git a/util/src/entities/Invite.ts b/util/src/entities/Invite.ts
index 82556fab..b3e00957 100644
--- a/util/src/entities/Invite.ts
+++ b/util/src/entities/Invite.ts
@@ -1,6 +1,6 @@
-import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { Column, Entity, JoinColumn, ManyToOne, RelationId, PrimaryColumn } from "typeorm";
import { Member } from "./Member";
-import { BaseClass, PrimaryIdColumn } from "./BaseClass";
+import { BaseClassWithoutId } from "./BaseClass";
import { Channel } from "./Channel";
import { Guild } from "./Guild";
import { User } from "./User";
@@ -8,8 +8,8 @@ import { User } from "./User";
export const PublicInviteRelation = ["inviter", "guild", "channel"];
@Entity("invites")
-export class Invite extends BaseClass {
- @PrimaryIdColumn()
+export class Invite extends BaseClassWithoutId {
+ @PrimaryColumn()
code: string;
@Column()
@@ -71,6 +71,9 @@ export class Invite extends BaseClass {
@Column({ nullable: true })
target_user_type?: number;
+ @Column({ nullable: true})
+ vanity_url?: boolean;
+
static async joinGuild(user_id: string, code: string) {
const invite = await Invite.findOneOrFail({ code });
if (invite.uses++ >= invite.max_uses && invite.max_uses !== 0) await Invite.delete({ code });
diff --git a/util/src/entities/Member.ts b/util/src/entities/Member.ts
index 7d7ac40a..0f7be2a7 100644
--- a/util/src/entities/Member.ts
+++ b/util/src/entities/Member.ts
@@ -26,6 +26,22 @@ import { BaseClassWithoutId } from "./BaseClass";
import { Ban, PublicGuildRelations } from ".";
import { DiscordApiErrors } from "../util/Constants";
+export const MemberPrivateProjection: (keyof Member)[] = [
+ "id",
+ "guild",
+ "guild_id",
+ "deaf",
+ "joined_at",
+ "last_message_id",
+ "mute",
+ "nick",
+ "pending",
+ "premium_since",
+ "roles",
+ "settings",
+ "user",
+];
+
@Entity("members")
@Index(["id", "guild_id"], { unique: true })
export class Member extends BaseClassWithoutId {
@@ -81,9 +97,12 @@ export class Member extends BaseClassWithoutId {
@Column()
pending: boolean;
- @Column({ type: "simple-json" })
+ @Column({ type: "simple-json", select: false })
settings: UserGuildSettings;
+ @Column({ nullable: true })
+ last_message_id?: string;
+
// TODO: update
// @Column({ type: "simple-json" })
// read_state: ReadState;
diff --git a/util/src/entities/Message.ts b/util/src/entities/Message.ts
index 04c3c7aa..a4d38315 100644
--- a/util/src/entities/Message.ts
+++ b/util/src/entities/Message.ts
@@ -46,9 +46,6 @@ export enum MessageType {
@Entity("messages")
export class Message extends BaseClass {
- @Column()
- id: string;
-
@Column({ nullable: true })
@RelationId((message: Message) => message.channel)
channel_id: string;
@@ -130,7 +127,7 @@ export class Message extends BaseClass {
mention_channels: Channel[];
@JoinTable({ name: "message_stickers" })
- @ManyToMany(() => Sticker)
+ @ManyToMany(() => Sticker, { cascade: true, onDelete: "CASCADE" })
sticker_items?: Sticker[];
@OneToMany(() => Attachment, (attachment: Attachment) => attachment.message, {
@@ -151,7 +148,7 @@ export class Message extends BaseClass {
@Column({ nullable: true })
pinned?: boolean;
- @Column({ type: "simple-enum", enum: MessageType })
+ @Column({ type: "int" })
type: MessageType;
@Column({ type: "simple-json", nullable: true })
diff --git a/util/src/entities/Migration.ts b/util/src/entities/Migration.ts
new file mode 100644
index 00000000..7393496f
--- /dev/null
+++ b/util/src/entities/Migration.ts
@@ -0,0 +1,18 @@
+import { Column, Entity, ObjectIdColumn, PrimaryGeneratedColumn } from "typeorm";
+import { BaseClassWithoutId } from ".";
+
+export const PrimaryIdAutoGenerated = process.env.DATABASE?.startsWith("mongodb")
+ ? ObjectIdColumn
+ : PrimaryGeneratedColumn;
+
+@Entity("migrations")
+export class Migration extends BaseClassWithoutId {
+ @PrimaryIdAutoGenerated()
+ id: number;
+
+ @Column({ type: 'bigint' })
+ timestamp: number;
+
+ @Column()
+ name: string;
+}
diff --git a/util/src/entities/RateLimit.ts b/util/src/entities/RateLimit.ts
index fa9c32c1..f5916f6b 100644
--- a/util/src/entities/RateLimit.ts
+++ b/util/src/entities/RateLimit.ts
@@ -3,9 +3,6 @@ import { BaseClass } from "./BaseClass";
@Entity("rate_limits")
export class RateLimit extends BaseClass {
- @Column()
- id: "global" | "error" | string; // channel_239842397 | guild_238927349823 | webhook_238923423498
-
@Column() // no relation as it also
executor_id: string;
diff --git a/util/src/entities/ReadState.ts b/util/src/entities/ReadState.ts
index 89480e83..ebef89be 100644
--- a/util/src/entities/ReadState.ts
+++ b/util/src/entities/ReadState.ts
@@ -32,13 +32,8 @@ export class ReadState extends BaseClass {
user: User;
@Column({ nullable: true })
- @RelationId((read_state: ReadState) => read_state.last_message)
last_message_id: string;
- @JoinColumn({ name: "last_message_id" })
- @ManyToOne(() => Message, { nullable: true })
- last_message?: Message;
-
@Column({ nullable: true })
last_pin_timestamp?: Date;
diff --git a/util/src/entities/Relationship.ts b/util/src/entities/Relationship.ts
index e016b36b..c3592c76 100644
--- a/util/src/entities/Relationship.ts
+++ b/util/src/entities/Relationship.ts
@@ -35,7 +35,7 @@ export class Relationship extends BaseClass {
@Column({ nullable: true })
nickname?: string;
- @Column({ type: "simple-enum", enum: RelationshipType })
+ @Column({ type: "int" })
type: RelationshipType;
toPublicRelationship() {
diff --git a/util/src/entities/Session.ts b/util/src/entities/Session.ts
index 7cc325f5..969efa89 100644
--- a/util/src/entities/Session.ts
+++ b/util/src/entities/Session.ts
@@ -1,6 +1,8 @@
import { User } from "./User";
import { BaseClass } from "./BaseClass";
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { Status } from "../interfaces/Status";
+import { Activity } from "../interfaces/Activity";
//TODO we need to remove all sessions on server start because if the server crashes without closing websockets it won't delete them
@@ -17,11 +19,13 @@ export class Session extends BaseClass {
user: User;
//TODO check, should be 32 char long hex string
- @Column({ nullable: false })
+ @Column({ nullable: false, select: false })
session_id: string;
- activities: []; //TODO
+ @Column({ type: "simple-json", nullable: true })
+ activities: Activity[];
+ // TODO client_status
@Column({ type: "simple-json", select: false })
client_info: {
client: string;
@@ -29,6 +33,14 @@ export class Session extends BaseClass {
version: number;
};
- @Column({ nullable: false })
- status: string; //TODO enum
+ @Column({ nullable: false, type: "varchar" })
+ status: Status; //TODO enum
}
+
+export const PrivateSessionProjection: (keyof Session)[] = [
+ "user_id",
+ "session_id",
+ "activities",
+ "client_info",
+ "status",
+];
diff --git a/util/src/entities/Sticker.ts b/util/src/entities/Sticker.ts
index ab224d1d..37bc6fbe 100644
--- a/util/src/entities/Sticker.ts
+++ b/util/src/entities/Sticker.ts
@@ -1,4 +1,5 @@
-import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { User } from "./User";
import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild";
@@ -8,6 +9,7 @@ export enum StickerType {
}
export enum StickerFormatType {
+ GIF = 0, // gif is a custom format type and not in discord spec
PNG = 1,
APNG = 2,
LOTTIE = 3,
@@ -21,11 +23,22 @@ export class Sticker extends BaseClass {
@Column({ nullable: true })
description?: string;
- @Column()
- tags: string;
+ @Column({ nullable: true })
+ available?: boolean;
- @Column()
- pack_id: string;
+ @Column({ nullable: true })
+ tags?: string;
+
+ @Column({ nullable: true })
+ @RelationId((sticker: Sticker) => sticker.pack)
+ pack_id?: string;
+
+ @JoinColumn({ name: "pack_id" })
+ @ManyToOne(() => require("./StickerPack").StickerPack, {
+ onDelete: "CASCADE",
+ nullable: true,
+ })
+ pack: import("./StickerPack").StickerPack;
@Column({ nullable: true })
guild_id?: string;
@@ -36,9 +49,18 @@ export class Sticker extends BaseClass {
})
guild?: Guild;
- @Column({ type: "simple-enum", enum: StickerType })
+ @Column({ nullable: true })
+ user_id?: string;
+
+ @JoinColumn({ name: "user_id" })
+ @ManyToOne(() => User, {
+ onDelete: "CASCADE",
+ })
+ user?: User;
+
+ @Column({ type: "int" })
type: StickerType;
- @Column({ type: "simple-enum", enum: StickerFormatType })
+ @Column({ type: "int" })
format_type: StickerFormatType;
}
diff --git a/util/src/entities/StickerPack.ts b/util/src/entities/StickerPack.ts
new file mode 100644
index 00000000..ec8c69a2
--- /dev/null
+++ b/util/src/entities/StickerPack.ts
@@ -0,0 +1,31 @@
+import { Column, Entity, JoinColumn, ManyToOne, OneToMany, OneToOne, RelationId } from "typeorm";
+import { Sticker } from ".";
+import { BaseClass } from "./BaseClass";
+
+@Entity("sticker_packs")
+export class StickerPack extends BaseClass {
+ @Column()
+ name: string;
+
+ @Column({ nullable: true })
+ description?: string;
+
+ @Column({ nullable: true })
+ banner_asset_id?: string;
+
+ @OneToMany(() => Sticker, (sticker: Sticker) => sticker.pack, {
+ cascade: true,
+ orphanedRowAction: "delete",
+ })
+ stickers: Sticker[];
+
+ // sku_id: string
+
+ @Column({ nullable: true })
+ @RelationId((pack: StickerPack) => pack.cover_sticker)
+ cover_sticker_id?: string;
+
+ @ManyToOne(() => Sticker, { nullable: true })
+ @JoinColumn()
+ cover_sticker?: Sticker;
+}
diff --git a/util/src/entities/TeamMember.ts b/util/src/entities/TeamMember.ts
index bdfdccf0..b726e1e8 100644
--- a/util/src/entities/TeamMember.ts
+++ b/util/src/entities/TeamMember.ts
@@ -9,7 +9,7 @@ export enum TeamMemberState {
@Entity("team_members")
export class TeamMember extends BaseClass {
- @Column({ type: "simple-enum", enum: TeamMemberState })
+ @Column({ type: "int" })
membership_state: TeamMemberState;
@Column({ type: "simple-array" })
diff --git a/util/src/entities/User.ts b/util/src/entities/User.ts
index 97564af3..bc852616 100644
--- a/util/src/entities/User.ts
+++ b/util/src/entities/User.ts
@@ -4,7 +4,7 @@ import { BitField } from "../util/BitField";
import { Relationship } from "./Relationship";
import { ConnectedAccount } from "./ConnectedAccount";
import { Config, FieldErrors, Snowflake, trimSpecial } from "..";
-import { Member } from ".";
+import { Member, Session } from ".";
export enum PublicUserEnum {
username,
@@ -131,6 +131,9 @@ export class User extends BaseClass {
@Column()
rights: string; // Rights
+ @OneToMany(() => Session, (session: Session) => session.user)
+ sessions: Session[];
+
@JoinColumn({ name: "relationship_ids" })
@OneToMany(() => Relationship, (relationship: Relationship) => relationship.from, {
cascade: true,
@@ -198,7 +201,7 @@ export class User extends BaseClass {
// randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists
// if it all five times already exists, abort with USERNAME_TOO_MANY_USERS error
// else just continue
- // TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the mongodb database?
+ // TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the database?
for (let tries = 0; tries < 5; tries++) {
discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0");
exists = await User.findOne({ where: { discriminator, username: username }, select: ["id"] });
@@ -219,7 +222,7 @@ export class User extends BaseClass {
// if nsfw_allowed is null/undefined it'll require date_of_birth to set it to true/false
const language = req.language === "en" ? "en-US" : req.language || "en-US";
- const user = await new User({
+ const user = new User({
created_at: new Date(),
username: username,
discriminator,
@@ -246,13 +249,17 @@ export class User extends BaseClass {
},
settings: { ...defaultSettings, locale: language },
fingerprints: [],
- }).save();
+ });
+
+ await user.save();
- if (Config.get().guild.autoJoin.enabled) {
- for (const guild of Config.get().guild.autoJoin.guilds || []) {
- await Member.addToGuild(user.id, guild);
+ setImmediate(async () => {
+ if (Config.get().guild.autoJoin.enabled) {
+ for (const guild of Config.get().guild.autoJoin.guilds || []) {
+ await Member.addToGuild(user.id, guild).catch((e) => {});
+ }
}
- }
+ });
return user;
}
@@ -291,7 +298,7 @@ export const defaultSettings: UserSettings = {
render_reactions: true,
restricted_guilds: [],
show_current_game: true,
- status: "offline",
+ status: "online",
stream_notifications_enabled: true,
theme: "dark",
timezone_offset: 0,
diff --git a/util/src/entities/Webhook.ts b/util/src/entities/Webhook.ts
index 8382435f..89538417 100644
--- a/util/src/entities/Webhook.ts
+++ b/util/src/entities/Webhook.ts
@@ -12,10 +12,7 @@ export enum WebhookType {
@Entity("webhooks")
export class Webhook extends BaseClass {
- @Column()
- id: string;
-
- @Column({ type: "simple-enum", enum: WebhookType })
+ @Column({ type: "int" })
type: WebhookType;
@Column({ nullable: true })
diff --git a/util/src/entities/index.ts b/util/src/entities/index.ts
index 7b1c9750..b52841c9 100644
--- a/util/src/entities/index.ts
+++ b/util/src/entities/index.ts
@@ -11,6 +11,7 @@ export * from "./Guild";
export * from "./Invite";
export * from "./Member";
export * from "./Message";
+export * from "./Migration";
export * from "./RateLimit";
export * from "./ReadState";
export * from "./Recipient";
@@ -18,6 +19,7 @@ export * from "./Relationship";
export * from "./Role";
export * from "./Session";
export * from "./Sticker";
+export * from "./StickerPack";
export * from "./Team";
export * from "./TeamMember";
export * from "./Template";
diff --git a/util/src/index.ts b/util/src/index.ts
index fc00d46b..ae0f7e54 100644
--- a/util/src/index.ts
+++ b/util/src/index.ts
@@ -1,12 +1,6 @@
import "reflect-metadata";
-// export * as Constants from "../util/Constants";
export * from "./util/index";
export * from "./interfaces/index";
export * from "./entities/index";
export * from "./dtos/index";
-
-// import Config from "../util/Config";
-// import db, { MongooseCache, toObject } from "./util/Database";
-
-// export { Config };
diff --git a/util/src/interfaces/Activity.ts b/util/src/interfaces/Activity.ts
index f5a3c270..43984afd 100644
--- a/util/src/interfaces/Activity.ts
+++ b/util/src/interfaces/Activity.ts
@@ -1,37 +1,38 @@
export interface Activity {
- name: string;
- type: ActivityType;
- url?: string;
- created_at?: Date;
+ name: string; // the activity's name
+ type: ActivityType; // activity type // TODO: check if its between range 0-5
+ url?: string; // stream url, is validated when type is 1
+ created_at?: number; // unix timestamp of when the activity was added to the user's session
timestamps?: {
- start?: number;
- end?: number;
- }[];
- application_id?: string;
+ // unix timestamps for start and/or end of the game
+ start: number;
+ end: number;
+ };
+ application_id?: string; // application id for the game
details?: string;
state?: string;
emoji?: {
name: string;
id?: string;
- amimated?: boolean;
+ animated: boolean;
};
party?: {
id?: string;
- size?: [number, number];
+ size?: [number]; // used to show the party's current and maximum size // TODO: array length 2
};
assets?: {
- large_image?: string;
- large_text?: string;
- small_image?: string;
- small_text?: string;
+ large_image?: string; // the id for a large asset of the activity, usually a snowflake
+ large_text?: string; // text displayed when hovering over the large image of the activity
+ small_image?: string; // the id for a small asset of the activity, usually a snowflake
+ small_text?: string; // text displayed when hovering over the small image of the activity
};
secrets?: {
- join?: string;
- spectate?: string;
- match?: string;
+ join?: string; // the secret for joining a party
+ spectate?: string; // the secret for spectating a game
+ match?: string; // the secret for a specific instanced match
};
instance?: boolean;
- flags?: bigint;
+ flags: string; // activity flags OR d together, describes what the payload includes
}
export enum ActivityType {
diff --git a/util/src/interfaces/Event.ts b/util/src/interfaces/Event.ts
index 03099bbb..a5253c09 100644
--- a/util/src/interfaces/Event.ts
+++ b/util/src/interfaces/Event.ts
@@ -12,6 +12,8 @@ import { Interaction } from "./Interaction";
import { ConnectedAccount } from "../entities/ConnectedAccount";
import { Relationship, RelationshipType } from "../entities/Relationship";
import { Presence } from "./Presence";
+import { Sticker } from "..";
+import { Activity, Status } from ".";
export interface Event {
guild_id?: string;
@@ -185,14 +187,22 @@ export interface GuildBanRemoveEvent extends Event {
};
}
-export interface GuildEmojiUpdateEvent extends Event {
- event: "GUILD_EMOJI_UPDATE";
+export interface GuildEmojisUpdateEvent extends Event {
+ event: "GUILD_EMOJIS_UPDATE";
data: {
guild_id: string;
emojis: Emoji[];
};
}
+export interface GuildStickersUpdateEvent extends Event {
+ event: "GUILD_STICKERS_UPDATE";
+ data: {
+ guild_id: string;
+ stickers: Sticker[];
+ };
+}
+
export interface GuildIntegrationUpdateEvent extends Event {
event: "GUILD_INTEGRATIONS_UPDATE";
data: {
@@ -445,6 +455,37 @@ export interface RelationshipRemoveEvent extends Event {
data: Omit<PublicRelationship, "nickname">;
}
+export interface SessionsReplace extends Event {
+ event: "SESSIONS_REPLACE";
+ data: {
+ activities: Activity[];
+ client_info: {
+ version: number;
+ os: string;
+ client: string;
+ };
+ status: Status;
+ }[];
+}
+
+export interface GuildMemberListUpdate extends Event {
+ event: "GUILD_MEMBER_LIST_UPDATE";
+ data: {
+ groups: { id: string; count: number }[];
+ guild_id: string;
+ id: string;
+ member_count: number;
+ online_count: number;
+ ops: {
+ index: number;
+ item: {
+ member?: PublicMember & { presence: Presence };
+ group?: { id: string; count: number }[];
+ };
+ }[];
+ };
+}
+
export type EventData =
| InvalidatedEvent
| ReadyEvent
@@ -459,12 +500,13 @@ export type EventData =
| GuildDeleteEvent
| GuildBanAddEvent
| GuildBanRemoveEvent
- | GuildEmojiUpdateEvent
+ | GuildEmojisUpdateEvent
| GuildIntegrationUpdateEvent
| GuildMemberAddEvent
| GuildMemberRemoveEvent
| GuildMemberUpdateEvent
| GuildMembersChunkEvent
+ | GuildMemberListUpdate
| GuildRoleCreateEvent
| GuildRoleUpdateEvent
| GuildRoleDeleteEvent
@@ -514,6 +556,7 @@ export enum EVENTEnum {
GuildMemberUpdate = "GUILD_MEMBER_UPDATE",
GuildMemberSpeaking = "GUILD_MEMBER_SPEAKING",
GuildMembersChunk = "GUILD_MEMBERS_CHUNK",
+ GuildMemberListUpdate = "GUILD_MEMBER_LIST_UPDATE",
GuildRoleCreate = "GUILD_ROLE_CREATE",
GuildRoleDelete = "GUILD_ROLE_DELETE",
GuildRoleUpdate = "GUILD_ROLE_UPDATE",
@@ -537,6 +580,7 @@ export enum EVENTEnum {
ApplicationCommandCreate = "APPLICATION_COMMAND_CREATE",
ApplicationCommandUpdate = "APPLICATION_COMMAND_UPDATE",
ApplicationCommandDelete = "APPLICATION_COMMAND_DELETE",
+ SessionsReplace = "SESSIONS_REPLACE",
}
export type EVENT =
@@ -552,13 +596,15 @@ export type EVENT =
| "GUILD_DELETE"
| "GUILD_BAN_ADD"
| "GUILD_BAN_REMOVE"
- | "GUILD_EMOJI_UPDATE"
+ | "GUILD_EMOJIS_UPDATE"
+ | "GUILD_STICKERS_UPDATE"
| "GUILD_INTEGRATIONS_UPDATE"
| "GUILD_MEMBER_ADD"
| "GUILD_MEMBER_REMOVE"
| "GUILD_MEMBER_UPDATE"
| "GUILD_MEMBER_SPEAKING"
| "GUILD_MEMBERS_CHUNK"
+ | "GUILD_MEMBER_LIST_UPDATE"
| "GUILD_ROLE_CREATE"
| "GUILD_ROLE_DELETE"
| "GUILD_ROLE_UPDATE"
@@ -587,6 +633,7 @@ export type EVENT =
| "MESSAGE_ACK"
| "RELATIONSHIP_ADD"
| "RELATIONSHIP_REMOVE"
+ | "SESSIONS_REPLACE"
| CUSTOMEVENTS;
export type CUSTOMEVENTS = "INVALIDATED" | "RATELIMIT";
diff --git a/util/src/interfaces/Presence.ts b/util/src/interfaces/Presence.ts
index 4a1ff038..7663891a 100644
--- a/util/src/interfaces/Presence.ts
+++ b/util/src/interfaces/Presence.ts
@@ -1,10 +1,12 @@
import { ClientStatus, Status } from "./Status";
import { Activity } from "./Activity";
+import { PublicUser } from "../entities/User";
export interface Presence {
- user_id: string;
+ user: PublicUser;
guild_id?: string;
status: Status;
activities: Activity[];
client_status: ClientStatus;
+ // TODO: game
}
diff --git a/util/src/migrations/1633864260873-EmojiRoles.ts b/util/src/migrations/1633864260873-EmojiRoles.ts
new file mode 100644
index 00000000..f0d709f2
--- /dev/null
+++ b/util/src/migrations/1633864260873-EmojiRoles.ts
@@ -0,0 +1,13 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class EmojiRoles1633864260873 implements MigrationInterface {
+ name = "EmojiRoles1633864260873";
+
+ public async up(queryRunner: QueryRunner): Promise<void> {
+ await queryRunner.query(`ALTER TABLE "emojis" ADD "roles" text NOT NULL DEFAULT ''`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise<void> {
+ await queryRunner.query(`ALTER TABLE "emojis" DROP COLUMN column_name "roles"`);
+ }
+}
diff --git a/util/src/migrations/1633864669243-EmojiUser.ts b/util/src/migrations/1633864669243-EmojiUser.ts
new file mode 100644
index 00000000..982405d7
--- /dev/null
+++ b/util/src/migrations/1633864669243-EmojiUser.ts
@@ -0,0 +1,23 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class EmojiUser1633864669243 implements MigrationInterface {
+ name = "EmojiUser1633864669243";
+
+ public async up(queryRunner: QueryRunner): Promise<void> {
+ await queryRunner.query(`ALTER TABLE "emojis" ADD "user_id" varchar`);
+ try {
+ await queryRunner.query(
+ `ALTER TABLE "emojis" ADD CONSTRAINT FK_fa7ddd5f9a214e28ce596548421 FOREIGN KEY (user_id) REFERENCES users(id)`
+ );
+ } catch (error) {
+ console.error(
+ "sqlite doesn't support altering foreign keys: https://stackoverflow.com/questions/1884818/how-do-i-add-a-foreign-key-to-an-existing-sqlite-table"
+ );
+ }
+ }
+
+ public async down(queryRunner: QueryRunner): Promise<void> {
+ await queryRunner.query(`ALTER TABLE "emojis" DROP COLUMN column_name "user_id"`);
+ await queryRunner.query(`ALTER TABLE "emojis" DROP CONSTRAINT FK_fa7ddd5f9a214e28ce596548421`);
+ }
+}
diff --git a/util/src/migrations/1633881705509-VanityInvite.ts b/util/src/migrations/1633881705509-VanityInvite.ts
new file mode 100644
index 00000000..45485310
--- /dev/null
+++ b/util/src/migrations/1633881705509-VanityInvite.ts
@@ -0,0 +1,19 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class VanityInvite1633881705509 implements MigrationInterface {
+ name = "VanityInvite1633881705509";
+
+ public async up(queryRunner: QueryRunner): Promise<void> {
+ try {
+ await queryRunner.query(`ALTER TABLE "emojis" DROP COLUMN vanity_url_code`);
+ await queryRunner.query(`ALTER TABLE "emojis" DROP CONSTRAINT FK_c2c1809d79eb120ea0cb8d342ad`);
+ } catch (error) {}
+ }
+
+ public async down(queryRunner: QueryRunner): Promise<void> {
+ await queryRunner.query(`ALTER TABLE "emojis" ADD vanity_url_code varchar`);
+ await queryRunner.query(
+ `ALTER TABLE "emojis" ADD CONSTRAINT FK_c2c1809d79eb120ea0cb8d342ad FOREIGN KEY ("vanity_url_code") REFERENCES "invites"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`
+ );
+ }
+}
diff --git a/util/src/migrations/1634308884591-Stickers.ts b/util/src/migrations/1634308884591-Stickers.ts
new file mode 100644
index 00000000..fbc4649f
--- /dev/null
+++ b/util/src/migrations/1634308884591-Stickers.ts
@@ -0,0 +1,66 @@
+import { MigrationInterface, QueryRunner, Table, TableColumn, TableForeignKey } from "typeorm";
+
+export class Stickers1634308884591 implements MigrationInterface {
+ name = "Stickers1634308884591";
+
+ public async up(queryRunner: QueryRunner): Promise<void> {
+ await queryRunner.dropForeignKey("read_states", "FK_6f255d873cfbfd7a93849b7ff74");
+ await queryRunner.changeColumn(
+ "stickers",
+ "tags",
+ new TableColumn({ name: "tags", type: "varchar", isNullable: true })
+ );
+ await queryRunner.changeColumn(
+ "stickers",
+ "pack_id",
+ new TableColumn({ name: "pack_id", type: "varchar", isNullable: true })
+ );
+ await queryRunner.changeColumn("stickers", "type", new TableColumn({ name: "type", type: "integer" }));
+ await queryRunner.changeColumn(
+ "stickers",
+ "format_type",
+ new TableColumn({ name: "format_type", type: "integer" })
+ );
+ await queryRunner.changeColumn(
+ "stickers",
+ "available",
+ new TableColumn({ name: "available", type: "boolean", isNullable: true })
+ );
+ await queryRunner.changeColumn(
+ "stickers",
+ "user_id",
+ new TableColumn({ name: "user_id", type: "boolean", isNullable: true })
+ );
+ await queryRunner.createForeignKey(
+ "stickers",
+ new TableForeignKey({
+ name: "FK_8f4ee73f2bb2325ff980502e158",
+ columnNames: ["user_id"],
+ referencedColumnNames: ["id"],
+ referencedTableName: "users",
+ onDelete: "CASCADE",
+ })
+ );
+ await queryRunner.createTable(
+ new Table({
+ name: "sticker_packs",
+ columns: [
+ new TableColumn({ name: "id", type: "varchar", isPrimary: true }),
+ new TableColumn({ name: "name", type: "varchar" }),
+ new TableColumn({ name: "description", type: "varchar", isNullable: true }),
+ new TableColumn({ name: "banner_asset_id", type: "varchar", isNullable: true }),
+ new TableColumn({ name: "cover_sticker_id", type: "varchar", isNullable: true }),
+ ],
+ foreignKeys: [
+ new TableForeignKey({
+ columnNames: ["cover_sticker_id"],
+ referencedColumnNames: ["id"],
+ referencedTableName: "stickers",
+ }),
+ ],
+ })
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise<void> {}
+}
diff --git a/util/src/migrations/1634424361103-Presence.ts b/util/src/migrations/1634424361103-Presence.ts
new file mode 100644
index 00000000..729955b8
--- /dev/null
+++ b/util/src/migrations/1634424361103-Presence.ts
@@ -0,0 +1,11 @@
+import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
+
+export class Presence1634424361103 implements MigrationInterface {
+ name = "Presence1634424361103";
+
+ public async up(queryRunner: QueryRunner): Promise<void> {
+ queryRunner.addColumn("sessions", new TableColumn({ name: "activites", type: "text" }));
+ }
+
+ public async down(queryRunner: QueryRunner): Promise<void> {}
+}
diff --git a/util/src/migrations/1634426540271-MigrationTimestamp.ts b/util/src/migrations/1634426540271-MigrationTimestamp.ts
new file mode 100644
index 00000000..3208b25b
--- /dev/null
+++ b/util/src/migrations/1634426540271-MigrationTimestamp.ts
@@ -0,0 +1,15 @@
+import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
+
+export class MigrationTimestamp1634426540271 implements MigrationInterface {
+ name = "MigrationTimestamp1634426540271";
+
+ public async up(queryRunner: QueryRunner): Promise<void> {
+ await queryRunner.changeColumn(
+ "migrations",
+ "timestamp",
+ new TableColumn({ name: "timestampe", type: "bigint", isNullable: false })
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise<void> {}
+}
diff --git a/util/src/migrations/migrate_db_engine.ts b/util/src/migrations/migrate_db_engine.ts
deleted file mode 100644
index 33024a8d..00000000
--- a/util/src/migrations/migrate_db_engine.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-import { config } from "dotenv";
-config();
-import { BaseEntity, createConnection, EntityTarget } from "typeorm";
-import { initDatabase } from "../util/Database";
-import "missing-native-js-functions";
-import {
- Application,
- Attachment,
- Ban,
- Channel,
- ConnectedAccount,
- defaultSettings,
- Emoji,
- Guild,
- Invite,
- Member,
- Message,
- RateLimit,
- ReadState,
- Recipient,
- Relationship,
- Role,
- Sticker,
- Team,
- TeamMember,
- Template,
- User,
- VoiceState,
- Webhook,
-} from "..";
-
-async function main() {
- if (!process.env.FROM) throw new Error("FROM database env connection string not set");
-
- // manually arrange them because of foreign key
- const entities = [
- User,
- Guild,
- Channel,
- Invite,
- Role,
- Ban,
- Application,
- Emoji,
- ConnectedAccount,
- Member,
- ReadState,
- Recipient,
- Relationship,
- Sticker,
- Team,
- TeamMember,
- Template,
- VoiceState,
- Webhook,
- Message,
- Attachment,
- ];
-
- const newDB = await initDatabase();
-
- // @ts-ignore
- const oldDB = await createConnection({
- type: process.env.FROM.split(":")[0]?.replace("+srv", ""),
- url: process.env.FROM,
- entities,
- name: "old",
- });
- let i = 0;
-
- try {
- for (const e of entities) {
- const entity = e as EntityTarget<any>;
- const entries = await oldDB.manager.find(entity);
- //@ts-ignore
- console.log("migrated " + entries.length + " " + entity.name);
-
- for (const entry of entries) {
- console.log(i++);
-
- if (entry instanceof User) {
- console.log("instance of User");
- if (entry.bio == null) entry.bio = "";
- if (entry.rights == null) entry.rights = "0";
- if (entry.disabled == null) entry.disabled = false;
- if (entry.fingerprints == null) entry.fingerprints = [];
- if (entry.deleted == null) entry.deleted = false;
- if (entry.data == null) {
- entry.data = {
- valid_tokens_since: new Date(0),
- hash: undefined,
- };
- // @ts-ignore
- if (entry.user_data) {
- // TODO: relationships
- entry.data = {
- // @ts-ignore
- valid_tokens_since: entry.user_data.valid_tokens_since, // @ts-ignore
- hash: entry.user_data.hash,
- };
- }
- }
- // @ts-ignore
- if (entry.settings == null) {
- entry.settings = defaultSettings;
- // @ts-ignore
- if (entry.user_data) entry.settings = entry.user_settings;
- }
- }
-
- // try {
- await newDB.manager.insert(entity, entry);
- // } catch (error) {
- // if (!entry.id) throw new Error("object doesn't have a unique id: " + entry);
- // await newDB.manager.update(entity, { id: entry.id }, entry);
- // }
- }
- // @ts-ignore
- console.log("migrated all " + entity.name);
- }
- } catch (error) {
- console.error((error as any).message);
- }
-
- console.log("SUCCESS migrated all data");
- await newDB.close();
-}
-
-main().caught();
diff --git a/util/src/util/Database.ts b/util/src/util/Database.ts
index 8bce3a6f..6124ffab 100644
--- a/util/src/util/Database.ts
+++ b/util/src/util/Database.ts
@@ -2,6 +2,7 @@ import path from "path";
import "reflect-metadata";
import { Connection, createConnection } from "typeorm";
import * as Models from "../entities";
+import { Migration } from "../entities/Migration";
import { yellow, green } from "nanocolors";
// UUID extension option is only supported with postgres
@@ -33,10 +34,27 @@ export function initDatabase(): Promise<Connection> {
bigNumberStrings: false,
supportBigNumbers: true,
name: "default",
+ migrations: [path.join(__dirname, "..", "migrations", "*.js")],
});
- promise.then((connection) => {
+ promise.then(async (connection: Connection) => {
dbConnection = connection;
+
+ // run migrations, and if it is a new fresh database, set it to the last migration
+ if (connection.migrations.length) {
+ if (!(await Migration.findOne({}))) {
+ let i = 0;
+
+ await Migration.insert(
+ connection.migrations.map((x) => ({
+ id: i++,
+ name: x.name,
+ timestamp: Date.now(),
+ }))
+ );
+ }
+ }
+ await connection.runMigrations();
console.log(`[Database] ${green("connected")}`);
});
diff --git a/util/src/util/Event.ts b/util/src/util/Event.ts
index bf9547b1..8ed009d5 100644
--- a/util/src/util/Event.ts
+++ b/util/src/util/Event.ts
@@ -46,7 +46,9 @@ export async function listenEvent(event: string, callback: (event: EventOpts) =>
} else {
const cancel = () => {
events.removeListener(event, callback);
+ events.setMaxListeners(events.getMaxListeners() - 1);
};
+ events.setMaxListeners(events.getMaxListeners() + 1);
events.addListener(event, (opts) => callback({ ...opts, cancel }));
return cancel;
diff --git a/util/src/util/Rights.ts b/util/src/util/Rights.ts
index a266e4f7..5edd9142 100644
--- a/util/src/util/Rights.ts
+++ b/util/src/util/Rights.ts
@@ -30,7 +30,7 @@ export class Rights extends BitField {
MANAGE_MESSAGES: BitFlag(3), // Can't see other messages but delete/edit them in channels that they can see
MANAGE_RATE_LIMITS: BitFlag(4),
MANAGE_ROUTING: BitFlag(5), // can create custom message routes to any channel/guild
- MANAGE_TICKETS: BitFlag(6),
+ MANAGE_TICKETS: BitFlag(6), // can respond to and resolve support tickets
MANAGE_USERS: BitFlag(7),
ADD_MEMBERS: BitFlag(8), // can manually add any members in their guilds
BYPASS_RATE_LIMITS: BitFlag(9),
@@ -39,7 +39,7 @@ export class Rights extends BitField {
CREATE_DMS: BitFlag(12),
CREATE_DM_GROUPS: BitFlag(13),
CREATE_GUILDS: BitFlag(14),
- CREATE_INVITES: BitFlag(15),
+ CREATE_INVITES: BitFlag(15), // can create mass invites in the guilds that they have CREATE_INSTANT_INVITE
CREATE_ROLES: BitFlag(16),
CREATE_TEMPLATES: BitFlag(17),
CREATE_WEBHOOKS: BitFlag(18),
@@ -50,9 +50,13 @@ export class Rights extends BitField {
SELF_EDIT_MESSAGES: BitFlag(23),
SELF_EDIT_NAME: BitFlag(24),
SEND_MESSAGES: BitFlag(25),
- USE_SCREEN: BitFlag(26),
+ USE_ACTIVITIES: BitFlag(26), // use (game) activities in voice channels (e.g. Watch together)
USE_VIDEO: BitFlag(27),
USE_VOICE: BitFlag(28),
+ INVITE_USERS: BitFlag(29), // can create user-specific invites in the guilds that they have INVITE_USERS
+ SELF_DELETE_DISABLE: BitFlag(30), // can disable/delete own account
+ DEBTABLE: BitFlag(31), // can use pay-to-use features
+ CREDITABLE: BitFlag(32) // can receive money from monetisation related features
};
any(permission: RightResolvable, checkOperator = true) {
diff --git a/util/src/util/Snowflake.ts b/util/src/util/Snowflake.ts
index f7a13388..3f6e3c63 100644
--- a/util/src/util/Snowflake.ts
+++ b/util/src/util/Snowflake.ts
@@ -84,7 +84,7 @@ export class Snowflake {
}
static generate() {
- var time = BigInt(Date.now() - Snowflake.EPOCH) << 22n;
+ var time = BigInt(Date.now() - Snowflake.EPOCH) << BigInt(22);
var worker = Snowflake.workerId << 17n;
var process = Snowflake.processId << 12n;
var increment = Snowflake.INCREMENT++;
diff --git a/util/src/util/TraverseDirectory.ts b/util/src/util/TraverseDirectory.ts
new file mode 100644
index 00000000..275b7dcc
--- /dev/null
+++ b/util/src/util/TraverseDirectory.ts
@@ -0,0 +1,10 @@
+import { Server, traverseDirectory } from "lambert-server";
+
+const DEFAULT_FILTER = /^([^\.].*)(?<!\.d)\.(js)$/;
+
+export function registerRoutes(server: Server, root: string) {
+ return traverseDirectory(
+ { dirname: root, recursive: true, filter: DEFAULT_FILTER },
+ server.registerRoute.bind(server, root)
+ );
+}
diff --git a/util/src/util/cdn.ts b/util/src/util/cdn.ts
index 8d45f85f..ea950cd1 100644
--- a/util/src/util/cdn.ts
+++ b/util/src/util/cdn.ts
@@ -4,7 +4,9 @@ import fetch from "node-fetch";
import { Config } from "./Config";
import multer from "multer";
-export async function uploadFile(path: string, file: Express.Multer.File) {
+export async function uploadFile(path: string, file?: Express.Multer.File) {
+ if (!file?.buffer) throw new HTTPError("Missing file in body");
+
const form = new FormData();
form.append("file", file.buffer, {
contentType: file.mimetype,
@@ -25,30 +27,15 @@ export async function uploadFile(path: string, file: Express.Multer.File) {
return result;
}
-export async function handleFile(
- path: string,
- body?: string
-): Promise<
- | (string & {
- id: string;
- content_type: string;
- size: number;
- url: string;
- })
- | undefined
-> {
+export async function handleFile(path: string, body?: string): Promise<string | undefined> {
if (!body || !body.startsWith("data:")) return undefined;
try {
const mimetype = body.split(":")[1].split(";")[0];
const buffer = Buffer.from(body.split(",")[1], "base64");
// @ts-ignore
- const file = await uploadFile(path, { buffer, mimetype, originalname: "banner" });
- const obj = file.id;
- for (const key in file) {
- obj[key] = file[key];
- }
- return obj;
+ const { id } = await uploadFile(path, { buffer, mimetype, originalname: "banner" });
+ return id;
} catch (error) {
console.error(error);
throw new HTTPError("Invalid " + path);
diff --git a/util/src/util/index.ts b/util/src/util/index.ts
index 67583635..c5703468 100644
--- a/util/src/util/index.ts
+++ b/util/src/util/index.ts
@@ -17,3 +17,4 @@ export * from "./Rights";
export * from "./Snowflake";
export * from "./String";
export * from "./Array";
+export * from "./TraverseDirectory";
|