diff --git a/util/src/entities/Application.ts b/util/src/entities/Application.ts
new file mode 100644
index 00000000..2092cd4e
--- /dev/null
+++ b/util/src/entities/Application.ts
@@ -0,0 +1,107 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { Guild } from "./Guild";
+import { Team } from "./Team";
+import { User } from "./User";
+
+@Entity("applications")
+export class Application extends BaseClass {
+ @Column()
+ name: string;
+
+ @Column({ nullable: true })
+ icon?: string;
+
+ @Column()
+ description: string;
+
+ @Column({ type: "simple-array", nullable: true })
+ rpc_origins?: string[];
+
+ @Column()
+ bot_public: boolean;
+
+ @Column()
+ bot_require_code_grant: boolean;
+
+ @Column({ nullable: true })
+ terms_of_service_url?: string;
+
+ @Column({ nullable: true })
+ privacy_policy_url?: string;
+
+ @JoinColumn({ name: "owner_id" })
+ @ManyToOne(() => User)
+ owner?: User;
+
+ @Column({ nullable: true })
+ summary?: string;
+
+ @Column()
+ verify_key: string;
+
+ @JoinColumn({ name: "team_id" })
+ @ManyToOne(() => Team)
+ team?: Team;
+
+ @JoinColumn({ name: "guild_id" })
+ @ManyToOne(() => Guild)
+ guild: Guild; // if this application is a game sold, this field will be the guild to which it has been linked
+
+ @Column({ nullable: true })
+ primary_sku_id?: string; // if this application is a game sold, this field will be the id of the "Game SKU" that is created,
+
+ @Column({ nullable: true })
+ slug?: string; // if this application is a game sold, this field will be the URL slug that links to the store page
+
+ @Column({ nullable: true })
+ cover_image?: string; // the application's default rich presence invite cover image hash
+
+ @Column()
+ flags: string; // the application's public flags
+}
+
+export interface ApplicationCommand {
+ id: string;
+ application_id: string;
+ name: string;
+ description: string;
+ options?: ApplicationCommandOption[];
+}
+
+export interface ApplicationCommandOption {
+ type: ApplicationCommandOptionType;
+ name: string;
+ description: string;
+ required?: boolean;
+ choices?: ApplicationCommandOptionChoice[];
+ options?: ApplicationCommandOption[];
+}
+
+export interface ApplicationCommandOptionChoice {
+ name: string;
+ value: string | number;
+}
+
+export enum ApplicationCommandOptionType {
+ SUB_COMMAND = 1,
+ SUB_COMMAND_GROUP = 2,
+ STRING = 3,
+ INTEGER = 4,
+ BOOLEAN = 5,
+ USER = 6,
+ CHANNEL = 7,
+ ROLE = 8,
+}
+
+export interface ApplicationCommandInteractionData {
+ id: string;
+ name: string;
+ options?: ApplicationCommandInteractionDataOption[];
+}
+
+export interface ApplicationCommandInteractionDataOption {
+ name: string;
+ value?: any;
+ options?: ApplicationCommandInteractionDataOption[];
+}
diff --git a/util/src/entities/Attachment.ts b/util/src/entities/Attachment.ts
new file mode 100644
index 00000000..ca893400
--- /dev/null
+++ b/util/src/entities/Attachment.ts
@@ -0,0 +1,34 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+
+@Entity("attachments")
+export class Attachment extends BaseClass {
+ @Column()
+ filename: string; // name of file attached
+
+ @Column()
+ size: number; // size of file in bytes
+
+ @Column()
+ url: string; // source url of file
+
+ @Column()
+ proxy_url: string; // a proxied url of file
+
+ @Column({ nullable: true })
+ height?: number; // height of file (if image)
+
+ @Column({ nullable: true })
+ width?: number; // width of file (if image)
+
+ @Column({ nullable: true })
+ content_type?: string;
+
+ @Column({ nullable: true })
+ @RelationId((attachment: Attachment) => attachment.message)
+ message_id: string;
+
+ @JoinColumn({ name: "message_id" })
+ @ManyToOne(() => require("./Message").Message, (message: import("./Message").Message) => message.attachments)
+ message: import("./Message").Message;
+}
diff --git a/util/src/models/AuditLog.ts b/util/src/entities/AuditLog.ts
index 02b2c444..ceeb21fd 100644
--- a/util/src/models/AuditLog.ts
+++ b/util/src/entities/AuditLog.ts
@@ -1,20 +1,67 @@
-import { Schema, Document, Types } from "mongoose";
-import db from "../util/Database";
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
import { ChannelPermissionOverwrite } from "./Channel";
-import { PublicUser } from "./User";
+import { User } from "./User";
-export interface AuditLogResponse {
- webhooks: []; // TODO:
- users: PublicUser[];
- audit_log_entries: AuditLogEntries[];
- integrations: []; // TODO:
+export enum AuditLogEvents {
+ GUILD_UPDATE = 1,
+ CHANNEL_CREATE = 10,
+ CHANNEL_UPDATE = 11,
+ CHANNEL_DELETE = 12,
+ CHANNEL_OVERWRITE_CREATE = 13,
+ CHANNEL_OVERWRITE_UPDATE = 14,
+ CHANNEL_OVERWRITE_DELETE = 15,
+ MEMBER_KICK = 20,
+ MEMBER_PRUNE = 21,
+ MEMBER_BAN_ADD = 22,
+ MEMBER_BAN_REMOVE = 23,
+ MEMBER_UPDATE = 24,
+ MEMBER_ROLE_UPDATE = 25,
+ MEMBER_MOVE = 26,
+ MEMBER_DISCONNECT = 27,
+ BOT_ADD = 28,
+ ROLE_CREATE = 30,
+ ROLE_UPDATE = 31,
+ ROLE_DELETE = 32,
+ INVITE_CREATE = 40,
+ INVITE_UPDATE = 41,
+ INVITE_DELETE = 42,
+ WEBHOOK_CREATE = 50,
+ WEBHOOK_UPDATE = 51,
+ WEBHOOK_DELETE = 52,
+ EMOJI_CREATE = 60,
+ EMOJI_UPDATE = 61,
+ EMOJI_DELETE = 62,
+ MESSAGE_DELETE = 72,
+ MESSAGE_BULK_DELETE = 73,
+ MESSAGE_PIN = 74,
+ MESSAGE_UNPIN = 75,
+ INTEGRATION_CREATE = 80,
+ INTEGRATION_UPDATE = 81,
+ INTEGRATION_DELETE = 82,
}
-export interface AuditLogEntries {
- target_id?: string;
+@Entity("audit_logs")
+export class AuditLogEntry extends BaseClass {
+ @JoinColumn({ name: "target_id" })
+ @ManyToOne(() => User)
+ target?: User;
+
+ @Column({ nullable: true })
+ @RelationId((auditlog: AuditLogEntry) => auditlog.user)
user_id: string;
- id: string;
+
+ @JoinColumn({ name: "user_id" })
+ @ManyToOne(() => User)
+ user: User;
+
+ @Column({
+ type: "simple-enum",
+ enum: AuditLogEvents,
+ })
action_type: AuditLogEvents;
+
+ @Column({ type: "simple-json", nullable: true })
options?: {
delete_member_days?: string;
members_removed?: string;
@@ -25,7 +72,12 @@ export interface AuditLogEntries {
type?: string;
role_name?: string;
};
+
+ @Column()
+ @Column({ type: "simple-json" })
changes: AuditLogChange[];
+
+ @Column({ nullable: true })
reason?: string;
}
@@ -91,130 +143,3 @@ export interface AuditLogChangeValue {
expire_grace_period?: number;
user_limit?: number;
}
-
-export interface AuditLogEntriesDocument extends Document, AuditLogEntries {
- id: string;
-}
-
-export const AuditLogChanges = {
- name: String,
- description: String,
- icon_hash: String,
- splash_hash: String,
- discovery_splash_hash: String,
- banner_hash: String,
- owner_id: String,
- region: String,
- preferred_locale: String,
- afk_channel_id: String,
- afk_timeout: Number,
- rules_channel_id: String,
- public_updates_channel_id: String,
- mfa_level: Number,
- verification_level: Number,
- explicit_content_filter: Number,
- default_message_notifications: Number,
- vanity_url_code: String,
- $add: [{}],
- $remove: [{}],
- prune_delete_days: Number,
- widget_enabled: Boolean,
- widget_channel_id: String,
- system_channel_id: String,
- position: Number,
- topic: String,
- bitrate: Number,
- permission_overwrites: [{}],
- nsfw: Boolean,
- application_id: String,
- rate_limit_per_user: Number,
- permissions: String,
- color: Number,
- hoist: Boolean,
- mentionable: Boolean,
- allow: String,
- deny: String,
- code: String,
- channel_id: String,
- inviter_id: String,
- max_uses: Number,
- uses: Number,
- max_age: Number,
- temporary: Boolean,
- deaf: Boolean,
- mute: Boolean,
- nick: String,
- avatar_hash: String,
- id: String,
- type: Number,
- enable_emoticons: Boolean,
- expire_behavior: Number,
- expire_grace_period: Number,
- user_limit: Number,
-};
-
-export const AuditLogSchema = new Schema({
- target_id: String,
- user_id: { type: String, required: true },
- id: { type: String, required: true },
- action_type: { type: Number, required: true },
- options: {
- delete_member_days: String,
- members_removed: String,
- channel_id: String,
- messaged_id: String,
- count: String,
- id: String,
- type: { type: Number },
- role_name: String,
- },
- changes: [
- {
- new_value: AuditLogChanges,
- old_value: AuditLogChanges,
- key: String,
- },
- ],
- reason: String,
-});
-
-// @ts-ignore
-export const AuditLogModel = db.model<AuditLogEntries>("AuditLog", AuditLogSchema, "auditlogs");
-
-export enum AuditLogEvents {
- GUILD_UPDATE = 1,
- CHANNEL_CREATE = 10,
- CHANNEL_UPDATE = 11,
- CHANNEL_DELETE = 12,
- CHANNEL_OVERWRITE_CREATE = 13,
- CHANNEL_OVERWRITE_UPDATE = 14,
- CHANNEL_OVERWRITE_DELETE = 15,
- MEMBER_KICK = 20,
- MEMBER_PRUNE = 21,
- MEMBER_BAN_ADD = 22,
- MEMBER_BAN_REMOVE = 23,
- MEMBER_UPDATE = 24,
- MEMBER_ROLE_UPDATE = 25,
- MEMBER_MOVE = 26,
- MEMBER_DISCONNECT = 27,
- BOT_ADD = 28,
- ROLE_CREATE = 30,
- ROLE_UPDATE = 31,
- ROLE_DELETE = 32,
- INVITE_CREATE = 40,
- INVITE_UPDATE = 41,
- INVITE_DELETE = 42,
- WEBHOOK_CREATE = 50,
- WEBHOOK_UPDATE = 51,
- WEBHOOK_DELETE = 52,
- EMOJI_CREATE = 60,
- EMOJI_UPDATE = 61,
- EMOJI_DELETE = 62,
- MESSAGE_DELETE = 72,
- MESSAGE_BULK_DELETE = 73,
- MESSAGE_PIN = 74,
- MESSAGE_UNPIN = 75,
- INTEGRATION_CREATE = 80,
- INTEGRATION_UPDATE = 81,
- INTEGRATION_DELETE = 82,
-}
diff --git a/util/src/entities/Ban.ts b/util/src/entities/Ban.ts
new file mode 100644
index 00000000..e8a6d648
--- /dev/null
+++ b/util/src/entities/Ban.ts
@@ -0,0 +1,37 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { Guild } from "./Guild";
+import { User } from "./User";
+
+@Entity("bans")
+export class Ban extends BaseClass {
+ @Column({ nullable: true })
+ @RelationId((ban: Ban) => ban.user)
+ user_id: string;
+
+ @JoinColumn({ name: "user_id" })
+ @ManyToOne(() => User)
+ user: User;
+
+ @Column({ nullable: true })
+ @RelationId((ban: Ban) => ban.guild)
+ guild_id: string;
+
+ @JoinColumn({ name: "guild_id" })
+ @ManyToOne(() => Guild)
+ guild: Guild;
+
+ @Column({ nullable: true })
+ @RelationId((ban: Ban) => ban.executor)
+ executor_id: string;
+
+ @JoinColumn({ name: "executor_id" })
+ @ManyToOne(() => User)
+ executor: User;
+
+ @Column()
+ ip: string;
+
+ @Column({ nullable: true })
+ reason?: string;
+}
diff --git a/util/src/entities/BaseClass.ts b/util/src/entities/BaseClass.ts
new file mode 100644
index 00000000..0856ccd1
--- /dev/null
+++ b/util/src/entities/BaseClass.ts
@@ -0,0 +1,77 @@
+import "reflect-metadata";
+import { BaseEntity, BeforeInsert, BeforeUpdate, EntityMetadata, FindConditions, 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 BaseClass extends BaseEntity {
+ @PrimaryColumn()
+ id: string = Snowflake.generate();
+
+ // @ts-ignore
+ constructor(public props?: any) {
+ super();
+ this.assign(props);
+ }
+
+ get construct(): any {
+ return this.constructor;
+ }
+
+ get metadata() {
+ return this.construct.getRepository().metadata as EntityMetadata;
+ }
+
+ assign(props: any) {
+ if (!props || typeof props !== "object") return;
+ delete props.opts;
+ delete props.props;
+
+ const properties = new Set(
+ this.metadata.columns
+ .map((x: any) => x.propertyName)
+ .concat(this.metadata.relations.map((x) => x.propertyName))
+ );
+ // will not include relational properties
+
+ for (const key in props) {
+ if (!properties.has(key)) continue;
+ // @ts-ignore
+ const setter = this[`set${key.capitalize()}`];
+
+ if (setter) {
+ setter.call(this, props[key]);
+ } else {
+ // @ts-ignore
+ this[key] = props[key];
+ }
+ }
+ }
+
+ @BeforeUpdate()
+ @BeforeInsert()
+ validate() {
+ this.assign(this.props);
+ return this;
+ }
+
+ toJSON(): any {
+ return Object.fromEntries(
+ this.metadata.columns // @ts-ignore
+ .map((x) => [x.propertyName, this[x.propertyName]]) // @ts-ignore
+ .concat(this.metadata.relations.map((x) => [x.propertyName, this[x.propertyName]]))
+ );
+ }
+
+ static increment<T extends BaseClass>(conditions: FindConditions<T>, propertyPath: string, value: number | string) {
+ const repository = this.getRepository();
+ return repository.increment(conditions, propertyPath, value);
+ }
+
+ static decrement<T extends BaseClass>(conditions: FindConditions<T>, propertyPath: string, value: number | string) {
+ const repository = this.getRepository();
+ return repository.decrement(conditions, propertyPath, value);
+ }
+}
diff --git a/util/src/entities/Channel.ts b/util/src/entities/Channel.ts
new file mode 100644
index 00000000..e3586dfc
--- /dev/null
+++ b/util/src/entities/Channel.ts
@@ -0,0 +1,171 @@
+import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { Guild } from "./Guild";
+import { Message } from "./Message";
+import { User } from "./User";
+import { HTTPError } from "lambert-server";
+import { emitEvent, getPermission, Snowflake } from "../util";
+import { ChannelCreateEvent } from "../interfaces";
+import { Recipient } from "./Recipient";
+
+export enum ChannelType {
+ GUILD_TEXT = 0, // a text channel within a server
+ DM = 1, // a direct message between users
+ GUILD_VOICE = 2, // a voice channel within a server
+ GROUP_DM = 3, // a direct message between multiple users
+ GUILD_CATEGORY = 4, // an organizational category that contains up to 50 channels
+ GUILD_NEWS = 5, // a channel that users can follow and crosspost into their own server
+ GUILD_STORE = 6, // a channel in which game developers can sell their game on Discord
+}
+
+@Entity("channels")
+export class Channel extends BaseClass {
+ @Column()
+ created_at: Date;
+
+ @Column()
+ name: string;
+
+ @Column({ type: "simple-enum", enum: ChannelType })
+ type: ChannelType;
+
+ @OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, { cascade: true })
+ recipients?: Recipient[];
+
+ @Column({ nullable: true })
+ @RelationId((channel: Channel) => channel.last_message)
+ last_message_id: string;
+
+ @JoinColumn({ name: "last_message_id" })
+ @ManyToOne(() => Message)
+ last_message?: Message;
+
+ @Column({ nullable: true })
+ @RelationId((channel: Channel) => channel.guild)
+ guild_id?: string;
+
+ @JoinColumn({ name: "guild_id" })
+ @ManyToOne(() => Guild)
+ guild: Guild;
+
+ @Column({ nullable: true })
+ @RelationId((channel: Channel) => channel.parent)
+ parent_id: string;
+
+ @JoinColumn({ name: "parent_id" })
+ @ManyToOne(() => Channel)
+ parent?: Channel;
+
+ @Column({ nullable: true })
+ @RelationId((channel: Channel) => channel.owner)
+ owner_id: string;
+
+ @JoinColumn({ name: "owner_id" })
+ @ManyToOne(() => User)
+ owner: User;
+
+ @Column({ nullable: true })
+ last_pin_timestamp?: number;
+
+ @Column({ nullable: true })
+ default_auto_archive_duration?: number;
+
+ @Column()
+ position: number;
+
+ @Column({ type: "simple-json" })
+ permission_overwrites: ChannelPermissionOverwrite[];
+
+ @Column({ nullable: true })
+ video_quality_mode?: number;
+
+ @Column({ nullable: true })
+ bitrate?: number;
+
+ @Column({ nullable: true })
+ user_limit?: number;
+
+ @Column({ nullable: true })
+ nsfw?: boolean;
+
+ @Column({ nullable: true })
+ rate_limit_per_user?: number;
+
+ @Column({ nullable: true })
+ topic?: string;
+
+ // TODO: DM channel
+ static async createChannel(
+ channel: Partial<Channel>,
+ user_id: string = "0",
+ opts?: {
+ keepId?: boolean;
+ skipExistsCheck?: boolean;
+ skipPermissionCheck?: boolean;
+ skipEventEmit?: boolean;
+ }
+ ) {
+ if (!opts?.skipPermissionCheck) {
+ // Always check if user has permission first
+ const permissions = await getPermission(user_id, channel.guild_id);
+ permissions.hasThrow("MANAGE_CHANNELS");
+ }
+
+ switch (channel.type) {
+ case ChannelType.GUILD_TEXT:
+ case ChannelType.GUILD_VOICE:
+ if (channel.parent_id && !opts?.skipExistsCheck) {
+ const exists = await Channel.findOneOrFail({ id: channel.parent_id });
+ if (!exists) throw new HTTPError("Parent id channel doesn't exist", 400);
+ if (exists.guild_id !== channel.guild_id)
+ throw new HTTPError("The category channel needs to be in the guild");
+ }
+ break;
+ case ChannelType.GUILD_CATEGORY:
+ break;
+ case ChannelType.DM:
+ case ChannelType.GROUP_DM:
+ throw new HTTPError("You can't create a dm channel in a guild");
+ // TODO: check if guild is community server
+ case ChannelType.GUILD_STORE:
+ case ChannelType.GUILD_NEWS:
+ default:
+ throw new HTTPError("Not yet supported");
+ }
+
+ if (!channel.permission_overwrites) channel.permission_overwrites = [];
+ // TODO: auto generate position
+
+ channel = {
+ ...channel,
+ ...(!opts?.keepId && { id: Snowflake.generate() }),
+ created_at: new Date(),
+ position: channel.position || 0,
+ };
+
+ await Promise.all([
+ Channel.insert(channel),
+ !opts?.skipEventEmit
+ ? emitEvent({
+ event: "CHANNEL_CREATE",
+ data: channel,
+ guild_id: channel.guild_id,
+ } as ChannelCreateEvent)
+ : Promise.resolve(),
+ ]);
+
+ return channel;
+ }
+}
+
+export interface ChannelPermissionOverwrite {
+ allow: bigint; // for bitfields we use bigints
+ deny: bigint; // for bitfields we use bigints
+ id: string;
+ type: ChannelPermissionOverwriteType;
+}
+
+export enum ChannelPermissionOverwriteType {
+ role = 0,
+ member = 1,
+}
diff --git a/util/src/entities/Config.ts b/util/src/entities/Config.ts
new file mode 100644
index 00000000..5eb55933
--- /dev/null
+++ b/util/src/entities/Config.ts
@@ -0,0 +1,280 @@
+import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import crypto from "crypto";
+import { Snowflake } from "../util/Snowflake";
+
+@Entity("config")
+export class ConfigEntity extends BaseClass {
+ @Column({ type: "simple-json" })
+ value: ConfigValue;
+}
+
+export interface RateLimitOptions {
+ bot?: number;
+ count: number;
+ window: number;
+ onyIp?: boolean;
+}
+
+export interface Region {
+ id: string;
+ name: string;
+ endpoint: string;
+ location?: {
+ latitude: number;
+ longitude: number;
+ };
+ vip: boolean;
+ custom: boolean;
+ deprecated: boolean;
+}
+
+export interface KafkaBroker {
+ ip: string;
+ port: number;
+}
+
+export interface ConfigValue {
+ gateway: {
+ endpointClient: string | null;
+ endpoint: string | null;
+ };
+ cdn: {
+ endpointClient: string | null;
+ endpoint: string | null;
+ };
+ general: {
+ instance_id: string;
+ };
+ permissions: {
+ user: {
+ createGuilds: boolean;
+ };
+ };
+ limits: {
+ user: {
+ maxGuilds: number;
+ maxUsername: number;
+ maxFriends: number;
+ };
+ guild: {
+ maxRoles: number;
+ maxMembers: number;
+ maxChannels: number;
+ maxChannelsInCategory: number;
+ hideOfflineMember: number;
+ };
+ message: {
+ maxCharacters: number;
+ maxTTSCharacters: number;
+ maxReactions: number;
+ maxAttachmentSize: number;
+ maxBulkDelete: number;
+ };
+ channel: {
+ maxPins: number;
+ maxTopic: number;
+ };
+ rate: {
+ ip: Omit<RateLimitOptions, "bot_count">;
+ global: RateLimitOptions;
+ error: RateLimitOptions;
+ routes: {
+ guild: RateLimitOptions;
+ webhook: RateLimitOptions;
+ channel: RateLimitOptions;
+ auth: {
+ login: RateLimitOptions;
+ register: RateLimitOptions;
+ };
+ // TODO: rate limit configuration for all routes
+ };
+ };
+ };
+ security: {
+ autoUpdate: boolean | number;
+ requestSignature: string;
+ jwtSecret: string;
+ forwadedFor: string | null; // header to get the real user ip address
+ captcha: {
+ enabled: boolean;
+ service: "recaptcha" | "hcaptcha" | null; // TODO: hcaptcha, custom
+ sitekey: string | null;
+ secret: string | null;
+ };
+ ipdataApiKey: string | null;
+ };
+ login: {
+ requireCaptcha: boolean;
+ };
+ register: {
+ email: {
+ necessary: boolean; // we have to use necessary instead of required as the cli tool uses json schema and can't use required
+ allowlist: boolean;
+ blocklist: boolean;
+ domains: string[];
+ };
+ dateOfBirth: {
+ necessary: boolean;
+ minimum: number; // in years
+ };
+ requireCaptcha: boolean;
+ requireInvite: boolean;
+ allowNewRegistration: boolean;
+ allowMultipleAccounts: boolean;
+ blockProxies: boolean;
+ password: {
+ minLength: number;
+ minNumbers: number;
+ minUpperCase: number;
+ minSymbols: number;
+ };
+ };
+ regions: {
+ default: string;
+ useDefaultAsOptimal: boolean;
+ available: Region[];
+ };
+ rabbitmq: {
+ host: string | null;
+ };
+ kafka: {
+ brokers: KafkaBroker[] | null;
+ };
+}
+
+export const DefaultConfigOptions: ConfigValue = {
+ gateway: {
+ endpointClient: null,
+ endpoint: null,
+ },
+ cdn: {
+ endpointClient: null,
+ endpoint: null,
+ },
+ general: {
+ instance_id: Snowflake.generate(),
+ },
+ permissions: {
+ user: {
+ createGuilds: true,
+ },
+ },
+ limits: {
+ user: {
+ maxGuilds: 100,
+ maxUsername: 32,
+ maxFriends: 1000,
+ },
+ guild: {
+ maxRoles: 250,
+ maxMembers: 250000,
+ maxChannels: 500,
+ maxChannelsInCategory: 50,
+ hideOfflineMember: 1000,
+ },
+ message: {
+ maxCharacters: 2000,
+ maxTTSCharacters: 200,
+ maxReactions: 20,
+ maxAttachmentSize: 8388608,
+ maxBulkDelete: 100,
+ },
+ channel: {
+ maxPins: 50,
+ maxTopic: 1024,
+ },
+ rate: {
+ ip: {
+ count: 500,
+ window: 5,
+ },
+ global: {
+ count: 20,
+ window: 5,
+ bot: 250,
+ },
+ error: {
+ count: 10,
+ window: 5,
+ },
+ routes: {
+ guild: {
+ count: 5,
+ window: 5,
+ },
+ webhook: {
+ count: 10,
+ window: 5,
+ },
+ channel: {
+ count: 10,
+ window: 5,
+ },
+ auth: {
+ login: {
+ count: 5,
+ window: 60,
+ },
+ register: {
+ count: 2,
+ window: 60 * 60 * 12,
+ },
+ },
+ },
+ },
+ },
+ security: {
+ autoUpdate: true,
+ requestSignature: crypto.randomBytes(32).toString("base64"),
+ jwtSecret: crypto.randomBytes(256).toString("base64"),
+ forwadedFor: null,
+ // forwadedFor: "X-Forwarded-For" // nginx/reverse proxy
+ // forwadedFor: "CF-Connecting-IP" // cloudflare:
+ captcha: {
+ enabled: false,
+ service: null,
+ sitekey: null,
+ secret: null,
+ },
+ ipdataApiKey: "eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9",
+ },
+ login: {
+ requireCaptcha: false,
+ },
+ register: {
+ email: {
+ necessary: true,
+ allowlist: false,
+ blocklist: true,
+ domains: [], // TODO: efficiently save domain blocklist in database
+ // domains: fs.readFileSync(__dirname + "/blockedEmailDomains.txt", { encoding: "utf8" }).split("\n"),
+ },
+ dateOfBirth: {
+ necessary: true,
+ minimum: 13,
+ },
+ requireInvite: false,
+ requireCaptcha: true,
+ allowNewRegistration: true,
+ allowMultipleAccounts: true,
+ blockProxies: true,
+ password: {
+ minLength: 8,
+ minNumbers: 2,
+ minUpperCase: 2,
+ minSymbols: 0,
+ },
+ },
+ regions: {
+ default: "fosscord",
+ useDefaultAsOptimal: true,
+ available: [{ id: "fosscord", name: "Fosscord", endpoint: "127.0.0.1", vip: false, custom: false, deprecated: false }],
+ },
+ rabbitmq: {
+ host: null,
+ },
+ kafka: {
+ brokers: null,
+ },
+};
diff --git a/util/src/entities/ConnectedAccount.ts b/util/src/entities/ConnectedAccount.ts
new file mode 100644
index 00000000..75982d01
--- /dev/null
+++ b/util/src/entities/ConnectedAccount.ts
@@ -0,0 +1,38 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { User } from "./User";
+
+@Entity("connected_accounts")
+export class ConnectedAccount extends BaseClass {
+ @Column({ nullable: true })
+ @RelationId((account: ConnectedAccount) => account.user)
+ user_id: string;
+
+ @JoinColumn({ name: "user_id" })
+ @ManyToOne(() => User)
+ user: User;
+
+ @Column({ select: false })
+ access_token: string;
+
+ @Column({ select: false })
+ friend_sync: boolean;
+
+ @Column()
+ name: string;
+
+ @Column({ select: false })
+ revoked: boolean;
+
+ @Column({ select: false })
+ show_activity: boolean;
+
+ @Column()
+ type: string;
+
+ @Column()
+ verifie: boolean;
+
+ @Column({ select: false })
+ visibility: number;
+}
diff --git a/util/src/entities/Emoji.ts b/util/src/entities/Emoji.ts
new file mode 100644
index 00000000..181aff2c
--- /dev/null
+++ b/util/src/entities/Emoji.ts
@@ -0,0 +1,29 @@
+import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { Guild } from "./Guild";
+import { Role } from "./Role";
+
+@Entity("emojis")
+export class Emoji extends BaseClass {
+ @Column()
+ animated: boolean;
+
+ @Column()
+ available: boolean; // whether this emoji can be used, may be false due to loss of Server Boosts
+
+ @Column()
+ guild_id: string;
+
+ @JoinColumn({ name: "guild_id" })
+ @ManyToOne(() => Guild)
+ guild: Guild;
+
+ @Column()
+ managed: boolean;
+
+ @Column()
+ name: string;
+
+ @Column()
+ require_colons: boolean;
+}
diff --git a/util/src/entities/Guild.ts b/util/src/entities/Guild.ts
new file mode 100644
index 00000000..032a9415
--- /dev/null
+++ b/util/src/entities/Guild.ts
@@ -0,0 +1,212 @@
+import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, OneToMany, OneToOne, RelationId } from "typeorm";
+import { Ban } from "./Ban";
+import { BaseClass } from "./BaseClass";
+import { Channel } from "./Channel";
+import { Emoji } from "./Emoji";
+import { Invite } from "./Invite";
+import { Member } from "./Member";
+import { Role } from "./Role";
+import { Sticker } from "./Sticker";
+import { Template } from "./Template";
+import { User } from "./User";
+import { VoiceState } from "./VoiceState";
+import { Webhook } from "./Webhook";
+
+// TODO: application_command_count, application_command_counts: {1: 0, 2: 0, 3: 0}
+// TODO: guild_scheduled_events
+// TODO: stage_instances
+// TODO: threads
+
+@Entity("guilds")
+export class Guild extends BaseClass {
+ @Column({ nullable: true })
+ @RelationId((guild: Guild) => guild.afk_channel)
+ afk_channel_id?: string;
+
+ @JoinColumn({ name: "afk_channel_id" })
+ @ManyToOne(() => Channel)
+ afk_channel?: Channel;
+
+ @Column({ nullable: true })
+ afk_timeout?: number;
+
+ // * commented out -> use owner instead
+ // application id of the guild creator if it is bot-created
+ // @Column({ nullable: true })
+ // application?: string;
+
+ @JoinColumn({ name: "ban_ids" })
+ @OneToMany(() => Ban, (ban: Ban) => ban.guild)
+ bans: Ban[];
+
+ @Column({ nullable: true })
+ banner?: string;
+
+ @Column({ nullable: true })
+ default_message_notifications?: number;
+
+ @Column({ nullable: true })
+ description?: string;
+
+ @Column({ nullable: true })
+ discovery_splash?: string;
+
+ @Column({ nullable: true })
+ explicit_content_filter?: number;
+
+ @Column({ type: "simple-array" })
+ features: string[]; //TODO use enum
+
+ @Column({ nullable: true })
+ icon?: string;
+
+ @Column({ nullable: true })
+ large?: boolean;
+
+ @Column({ nullable: true })
+ max_members?: number; // e.g. default 100.000
+
+ @Column({ nullable: true })
+ max_presences?: number;
+
+ @Column({ nullable: true })
+ max_video_channel_users?: number; // ? default: 25, is this max 25 streaming or watching
+
+ @Column({ nullable: true })
+ member_count?: number;
+
+ @Column({ nullable: true })
+ presence_count?: number; // users online
+
+ @OneToMany(() => Member, (member: Member) => member.guild)
+ members: Member[];
+
+ @JoinColumn({ name: "role_ids" })
+ @OneToMany(() => Role, (role: Role) => role.guild)
+ roles: Role[];
+
+ @JoinColumn({ name: "channel_ids" })
+ @OneToMany(() => Channel, (channel: Channel) => channel.guild)
+ channels: Channel[];
+
+ @Column({ nullable: true })
+ @RelationId((guild: Guild) => guild.template)
+ template_id: string;
+
+ @JoinColumn({ name: "template_id" })
+ @ManyToOne(() => Template)
+ template: Template;
+
+ @JoinColumn({ name: "emoji_ids" })
+ @OneToMany(() => Emoji, (emoji: Emoji) => emoji.guild)
+ emojis: Emoji[];
+
+ @JoinColumn({ name: "sticker_ids" })
+ @OneToMany(() => Sticker, (sticker: Sticker) => sticker.guild)
+ stickers: Sticker[];
+
+ @JoinColumn({ name: "invite_ids" })
+ @OneToMany(() => Invite, (invite: Invite) => invite.guild)
+ invites: Invite[];
+
+ @JoinColumn({ name: "voice_state_ids" })
+ @OneToMany(() => VoiceState, (voicestate: VoiceState) => voicestate.guild)
+ voice_states: VoiceState[];
+
+ @JoinColumn({ name: "webhook_ids" })
+ @OneToMany(() => Webhook, (webhook: Webhook) => webhook.guild)
+ webhooks: Webhook[];
+
+ @Column({ nullable: true })
+ mfa_level?: number;
+
+ @Column()
+ name: string;
+
+ @Column({ nullable: true })
+ @RelationId((guild: Guild) => guild.owner)
+ owner_id: string;
+
+ @JoinColumn([{ name: "owner_id", referencedColumnName: "id" }])
+ @ManyToOne(() => User)
+ owner: User;
+
+ @Column({ nullable: true })
+ preferred_locale?: string; // only community guilds can choose this
+
+ @Column({ nullable: true })
+ premium_subscription_count?: number;
+
+ @Column({ nullable: true })
+ premium_tier?: number; // nitro boost level
+
+ @Column({ nullable: true })
+ @RelationId((guild: Guild) => guild.public_updates_channel)
+ public_updates_channel_id: string;
+
+ @JoinColumn({ name: "public_updates_channel_id" })
+ @ManyToOne(() => Channel)
+ public_updates_channel?: Channel;
+
+ @Column({ nullable: true })
+ @RelationId((guild: Guild) => guild.rules_channel)
+ rules_channel_id?: string;
+
+ @JoinColumn({ name: "rules_channel_id" })
+ @ManyToOne(() => Channel)
+ rules_channel?: string;
+
+ @Column({ nullable: true })
+ region?: string;
+
+ @Column({ nullable: true })
+ splash?: string;
+
+ @Column({ nullable: true })
+ @RelationId((guild: Guild) => guild.system_channel)
+ system_channel_id?: string;
+
+ @JoinColumn({ name: "system_channel_id" })
+ @ManyToOne(() => Channel)
+ system_channel?: Channel;
+
+ @Column({ nullable: true })
+ system_channel_flags?: number;
+
+ @Column({ nullable: true })
+ 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" })
+ welcome_screen: {
+ enabled: boolean;
+ description: string;
+ welcome_channels: {
+ description: string;
+ emoji_id?: string;
+ emoji_name: string;
+ channel_id: string;
+ }[];
+ };
+
+ @Column({ nullable: true })
+ @RelationId((guild: Guild) => guild.widget_channel)
+ widget_channel_id?: string;
+
+ @JoinColumn({ name: "widget_channel_id" })
+ @ManyToOne(() => Channel)
+ widget_channel?: Channel;
+
+ @Column({ nullable: true })
+ widget_enabled?: boolean;
+}
diff --git a/util/src/entities/Invite.ts b/util/src/entities/Invite.ts
new file mode 100644
index 00000000..01e22294
--- /dev/null
+++ b/util/src/entities/Invite.ts
@@ -0,0 +1,64 @@
+import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { Channel } from "./Channel";
+import { Guild } from "./Guild";
+import { User } from "./User";
+
+@Entity("invites")
+export class Invite extends BaseClass {
+ @PrimaryColumn()
+ code: string;
+
+ @Column()
+ temporary: boolean;
+
+ @Column()
+ uses: number;
+
+ @Column()
+ max_uses: number;
+
+ @Column()
+ max_age: number;
+
+ @Column()
+ created_at: Date;
+
+ @Column()
+ expires_at: Date;
+
+ @Column({ nullable: true })
+ @RelationId((invite: Invite) => invite.guild)
+ guild_id: string;
+
+ @JoinColumn({ name: "guild_id" })
+ @ManyToOne(() => Guild)
+ guild: Guild;
+
+ @Column({ nullable: true })
+ @RelationId((invite: Invite) => invite.channel)
+ channel_id: string;
+
+ @JoinColumn({ name: "channel_id" })
+ @ManyToOne(() => Channel)
+ channel: Channel;
+
+ @Column({ nullable: true })
+ @RelationId((invite: Invite) => invite.inviter)
+ inviter_id: string;
+
+ @JoinColumn({ name: "inviter_id" })
+ @ManyToOne(() => User)
+ inviter: User;
+
+ @Column({ nullable: true })
+ @RelationId((invite: Invite) => invite.target_user)
+ target_user_id: string;
+
+ @JoinColumn({ name: "target_user_id" })
+ @ManyToOne(() => User)
+ target_user?: string; // could be used for "User specific invites" https://github.com/fosscord/fosscord/issues/62
+
+ @Column({ nullable: true })
+ target_user_type?: number;
+}
diff --git a/util/src/entities/Member.ts b/util/src/entities/Member.ts
new file mode 100644
index 00000000..d2d78bb9
--- /dev/null
+++ b/util/src/entities/Member.ts
@@ -0,0 +1,287 @@
+import { PublicUser, User } from "./User";
+import { BaseClass } from "./BaseClass";
+import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, RelationId } from "typeorm";
+import { Guild } from "./Guild";
+import { Config, emitEvent } from "../util";
+import {
+ GuildCreateEvent,
+ GuildDeleteEvent,
+ GuildMemberAddEvent,
+ GuildMemberRemoveEvent,
+ GuildMemberUpdateEvent,
+} from "../interfaces";
+import { HTTPError } from "lambert-server";
+import { Role } from "./Role";
+
+@Entity("members")
+export class Member extends BaseClass {
+ @JoinColumn({ name: "id" })
+ @ManyToOne(() => User)
+ user: User;
+
+ @Column({ nullable: true })
+ @RelationId((member: Member) => member.guild)
+ guild_id: string;
+
+ @JoinColumn({ name: "guild_id" })
+ @ManyToOne(() => Guild)
+ guild: Guild;
+
+ @Column({ nullable: true })
+ nick?: string;
+
+ @JoinTable({ name: "member_roles" })
+ @ManyToMany(() => Role)
+ roles: Role[];
+
+ @Column()
+ joined_at: Date;
+
+ @Column({ nullable: true })
+ premium_since?: number;
+
+ @Column()
+ deaf: boolean;
+
+ @Column()
+ mute: boolean;
+
+ @Column()
+ pending: boolean;
+
+ @Column({ type: "simple-json" })
+ settings: UserGuildSettings;
+
+ // TODO: update
+ @Column({ type: "simple-json" })
+ read_state: Record<string, string | null>;
+
+ static async IsInGuildOrFail(user_id: string, guild_id: string) {
+ if (await Member.count({ id: user_id, guild: { id: guild_id } })) return true;
+ throw new HTTPError("You are not member of this guild", 403);
+ }
+
+ static async removeFromGuild(user_id: string, guild_id: string) {
+ const guild = await Guild.findOneOrFail({ select: ["owner_id"], where: { id: guild_id } });
+ if (guild.owner_id === user_id) throw new Error("The owner cannot be removed of the guild");
+ const member = await Member.findOneOrFail({ where: { id: user_id, guild_id }, relations: ["user"] });
+
+ // use promise all to execute all promises at the same time -> save time
+ return Promise.all([
+ Member.delete({
+ id: user_id,
+ guild_id: guild_id,
+ }),
+ Guild.decrement({ id: guild_id }, "member_count", -1),
+
+ emitEvent({
+ event: "GUILD_DELETE",
+ data: {
+ id: guild_id,
+ },
+ user_id: user_id,
+ } as GuildDeleteEvent),
+ emitEvent({
+ event: "GUILD_MEMBER_REMOVE",
+ data: {
+ guild_id: guild_id,
+ user: member.user,
+ },
+ guild_id: guild_id,
+ } as GuildMemberRemoveEvent),
+ ]);
+ }
+
+ static async addRole(user_id: string, guild_id: string, role_id: string) {
+ const [member] = await Promise.all([
+ // @ts-ignore
+ Member.findOneOrFail({
+ where: { id: user_id, guild_id: guild_id },
+ relations: ["user", "roles"], // we don't want to load the role objects just the ids
+ select: ["roles.id"],
+ }),
+ await Role.findOneOrFail({ id: role_id, guild_id: guild_id }),
+ ]);
+ member.roles.push(new Role({ id: role_id }));
+
+ await Promise.all([
+ member.save(),
+ emitEvent({
+ event: "GUILD_MEMBER_UPDATE",
+ data: {
+ guild_id: guild_id,
+ user: member.user,
+ roles: member.roles.map((x) => x.id),
+ },
+ guild_id: guild_id,
+ } as GuildMemberUpdateEvent),
+ ]);
+ }
+
+ static async removeRole(user_id: string, guild_id: string, role_id: string) {
+ const [member] = await Promise.all([
+ // @ts-ignore
+ Member.findOneOrFail({
+ where: { id: user_id, guild_id: guild_id },
+ relations: ["user", "roles"], // we don't want to load the role objects just the ids
+ select: ["roles.id"],
+ }),
+ await Role.findOneOrFail({ id: role_id, guild_id: guild_id }),
+ ]);
+ member.roles = member.roles.filter((x) => x.id == role_id);
+
+ await Promise.all([
+ member.save(),
+ emitEvent({
+ event: "GUILD_MEMBER_UPDATE",
+ data: {
+ guild_id: guild_id,
+ user: member.user,
+ roles: member.roles.map((x) => x.id),
+ },
+ guild_id: guild_id,
+ } as GuildMemberUpdateEvent),
+ ]);
+ }
+
+ static async changeNickname(user_id: string, guild_id: string, nickname: string) {
+ const member = await Member.findOneOrFail({
+ where: {
+ id: user_id,
+ guild_id: guild_id,
+ },
+ relations: ["user"],
+ });
+ member.nick = nickname;
+
+ await Promise.all([
+ member.save(),
+
+ emitEvent({
+ event: "GUILD_MEMBER_UPDATE",
+ data: {
+ guild_id: guild_id,
+ user: member.user,
+ nick: nickname,
+ },
+ guild_id: guild_id,
+ } as GuildMemberUpdateEvent),
+ ]);
+ }
+
+ static async addToGuild(user_id: string, guild_id: string) {
+ const user = await User.getPublicUser(user_id);
+
+ const { maxGuilds } = Config.get().limits.user;
+ const guild_count = await Member.count({ id: user_id });
+ if (guild_count >= maxGuilds) {
+ throw new HTTPError(`You are at the ${maxGuilds} server limit.`, 403);
+ }
+
+ const guild = await Guild.findOneOrFail({
+ where: {
+ id: guild_id,
+ },
+ relations: ["channels", "emojis", "members", "roles", "stickers"],
+ });
+
+ if (await Member.count({ id: user.id, guild: { id: guild_id } }))
+ throw new HTTPError("You are already a member of this guild", 400);
+
+ const member = {
+ id: user_id,
+ guild_id: guild_id,
+ nick: undefined,
+ roles: [guild_id], // @everyone role
+ joined_at: new Date(),
+ premium_since: undefined,
+ deaf: false,
+ mute: false,
+ pending: false,
+ };
+ // @ts-ignore
+ guild.joined_at = member.joined_at.toISOString();
+
+ await Promise.all([
+ Member.insert({
+ ...member,
+ roles: undefined,
+ read_state: {},
+ settings: {
+ channel_overrides: [],
+ message_notifications: 0,
+ mobile_push: true,
+ muted: false,
+ suppress_everyone: false,
+ suppress_roles: false,
+ version: 0,
+ },
+ }),
+ Guild.increment({ id: guild_id }, "member_count", 1),
+ emitEvent({
+ event: "GUILD_MEMBER_ADD",
+ data: {
+ ...member,
+ user,
+ guild_id: guild_id,
+ },
+ guild_id: guild_id,
+ } as GuildMemberAddEvent),
+ emitEvent({
+ event: "GUILD_CREATE",
+ data: { ...guild, members: [...guild.members, member] },
+ user_id,
+ } as GuildCreateEvent),
+ ]);
+ }
+}
+
+export interface UserGuildSettings {
+ channel_overrides: {
+ channel_id: string;
+ message_notifications: number;
+ mute_config: MuteConfig;
+ muted: boolean;
+ }[];
+ message_notifications: number;
+ mobile_push: boolean;
+ mute_config: MuteConfig;
+ muted: boolean;
+ suppress_everyone: boolean;
+ suppress_roles: boolean;
+ version: number;
+}
+
+export interface MuteConfig {
+ end_time: number;
+ selected_time_window: number;
+}
+
+export type PublicMemberKeys =
+ | "id"
+ | "guild_id"
+ | "nick"
+ | "roles"
+ | "joined_at"
+ | "pending"
+ | "deaf"
+ | "mute"
+ | "premium_since";
+
+export const PublicMemberProjection: PublicMemberKeys[] = [
+ "id",
+ "guild_id",
+ "nick",
+ "roles",
+ "joined_at",
+ "pending",
+ "deaf",
+ "mute",
+ "premium_since",
+];
+
+// @ts-ignore
+export type PublicMember = Pick<Member, Omit<PublicMemberKeys, "roles">> & {
+ user: PublicUser;
+ roles: string[]; // only role ids not objects
+};
diff --git a/util/src/entities/Message.ts b/util/src/entities/Message.ts
new file mode 100644
index 00000000..542b2b55
--- /dev/null
+++ b/util/src/entities/Message.ts
@@ -0,0 +1,264 @@
+import { User } from "./User";
+import { Member } from "./Member";
+import { Role } from "./Role";
+import { Channel } from "./Channel";
+import { InteractionType } from "../interfaces/Interaction";
+import { Application } from "./Application";
+import {
+ Column,
+ CreateDateColumn,
+ Entity,
+ JoinColumn,
+ JoinTable,
+ ManyToMany,
+ ManyToOne,
+ OneToMany,
+ RelationId,
+ UpdateDateColumn,
+} from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { Guild } from "./Guild";
+import { Webhook } from "./Webhook";
+import { Sticker } from "./Sticker";
+import { Attachment } from "./Attachment";
+
+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,
+ REPLY = 19,
+ APPLICATION_COMMAND = 20,
+}
+
+@Entity("messages")
+export class Message extends BaseClass {
+ @Column()
+ id: string;
+
+ @Column({ nullable: true })
+ @RelationId((message: Message) => message.channel)
+ channel_id: string;
+
+ @JoinColumn({ name: "channel_id" })
+ @ManyToOne(() => Channel)
+ channel: Channel;
+
+ @Column({ nullable: true })
+ @RelationId((message: Message) => message.guild)
+ guild_id?: string;
+
+ @JoinColumn({ name: "guild_id" })
+ @ManyToOne(() => Guild)
+ guild?: Guild;
+
+ @Column({ nullable: true })
+ @RelationId((message: Message) => message.author)
+ author_id: string;
+
+ @JoinColumn({ name: "author_id", referencedColumnName: "id" })
+ @ManyToOne(() => User)
+ author?: User;
+
+ @Column({ nullable: true })
+ @RelationId((message: Message) => message.member)
+ member_id: string;
+
+ @JoinColumn({ name: "member_id" })
+ @ManyToOne(() => Member)
+ member?: Member;
+
+ @Column({ nullable: true })
+ @RelationId((message: Message) => message.webhook)
+ webhook_id: string;
+
+ @JoinColumn({ name: "webhook_id" })
+ @ManyToOne(() => Webhook)
+ webhook?: Webhook;
+
+ @Column({ nullable: true })
+ @RelationId((message: Message) => message.application)
+ application_id: string;
+
+ @JoinColumn({ name: "application_id" })
+ @ManyToOne(() => Application)
+ application?: Application;
+
+ @Column({ nullable: true })
+ content?: string;
+
+ @Column()
+ @CreateDateColumn()
+ timestamp: Date;
+
+ @Column()
+ @UpdateDateColumn()
+ edited_timestamp?: Date;
+
+ @Column({ nullable: true })
+ tts?: boolean;
+
+ @Column({ nullable: true })
+ mention_everyone?: boolean;
+
+ @JoinTable({ name: "message_user_mentions" })
+ @ManyToMany(() => User)
+ mentions: User[];
+
+ @JoinTable({ name: "message_role_mentions" })
+ @ManyToMany(() => Role)
+ mention_roles: Role[];
+
+ @JoinTable({ name: "message_channel_mentions" })
+ @ManyToMany(() => Channel)
+ mention_channels: Channel[];
+
+ @JoinTable({ name: "message_stickers" })
+ @ManyToMany(() => Sticker)
+ sticker_items?: Sticker[];
+
+ @JoinColumn({ name: "attachment_ids" })
+ @OneToMany(() => Attachment, (attachment: Attachment) => attachment.message)
+ attachments?: Attachment[];
+
+ @Column({ type: "simple-json" })
+ embeds: Embed[];
+
+ @Column({ type: "simple-json" })
+ reactions: Reaction[];
+
+ @Column({ type: "text", nullable: true })
+ nonce?: string | number;
+
+ @Column({ nullable: true })
+ pinned?: boolean;
+
+ @Column({ type: "simple-enum", enum: MessageType })
+ type: MessageType;
+
+ @Column({ type: "simple-json", nullable: true })
+ activity?: {
+ type: number;
+ party_id: string;
+ };
+
+ @Column({ nullable: true })
+ flags?: string;
+ @Column({ type: "simple-json", nullable: true })
+ message_reference?: {
+ message_id: string;
+ channel_id?: string;
+ guild_id?: string;
+ };
+
+ @JoinColumn({ name: "message_reference_id" })
+ @ManyToOne(() => Message)
+ referenced_message?: Message;
+
+ @Column({ type: "simple-json", nullable: true })
+ interaction?: {
+ id: string;
+ type: InteractionType;
+ name: string;
+ user_id: string; // the user who invoked the interaction
+ // user: User; // TODO: autopopulate user
+ };
+
+ @Column({ type: "simple-json", nullable: true })
+ components?: MessageComponent[];
+}
+
+export interface MessageComponent {
+ type: number;
+ style?: number;
+ label?: string;
+ emoji?: PartialEmoji;
+ custom_id?: string;
+ url?: string;
+ disabled?: boolean;
+ components: MessageComponent[];
+}
+
+export enum MessageComponentType {
+ ActionRow = 1,
+ Button = 2,
+}
+
+export interface Embed {
+ title?: string; //title of embed
+ type?: EmbedType; // type of embed (always "rich" for webhook embeds)
+ description?: string; // description of embed
+ url?: string; // url of embed
+ timestamp?: Date; // timestamp of embed content
+ color?: number; // color code of the embed
+ footer?: {
+ text: string;
+ icon_url?: string;
+ proxy_icon_url?: string;
+ }; // footer object footer information
+ image?: EmbedImage; // image object image information
+ thumbnail?: EmbedImage; // thumbnail object thumbnail information
+ video?: EmbedImage; // video object video information
+ provider?: {
+ name?: string;
+ url?: string;
+ }; // provider object provider information
+ author?: {
+ name?: string;
+ url?: string;
+ icon_url?: string;
+ proxy_icon_url?: string;
+ }; // author object author information
+ fields?: {
+ name: string;
+ value: string;
+ inline?: boolean;
+ }[];
+}
+
+export enum EmbedType {
+ rich = "rich",
+ image = "image",
+ video = "video",
+ gifv = "gifv",
+ article = "article",
+ link = "link",
+}
+
+export interface EmbedImage {
+ url?: string;
+ proxy_url?: string;
+ height?: number;
+ width?: number;
+}
+
+export interface Reaction {
+ count: number;
+ //// not saved in the database // me: boolean; // whether the current user reacted using this emoji
+ emoji: PartialEmoji;
+ user_ids: string[];
+}
+
+export interface PartialEmoji {
+ id?: string;
+ name: string;
+ animated?: boolean;
+}
+
+export interface AllowedMentions {
+ parse?: ("users" | "roles" | "everyone")[];
+ roles?: string[];
+ users?: string[];
+ replied_user?: boolean;
+}
diff --git a/util/src/entities/RateLimit.ts b/util/src/entities/RateLimit.ts
new file mode 100644
index 00000000..fa9c32c1
--- /dev/null
+++ b/util/src/entities/RateLimit.ts
@@ -0,0 +1,20 @@
+import { Column, Entity } from "typeorm";
+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;
+
+ @Column()
+ hits: number;
+
+ @Column()
+ blocked: boolean;
+
+ @Column()
+ expires_at: Date;
+}
diff --git a/util/src/entities/ReadState.ts b/util/src/entities/ReadState.ts
new file mode 100644
index 00000000..8dd05b21
--- /dev/null
+++ b/util/src/entities/ReadState.ts
@@ -0,0 +1,45 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { Channel } from "./Channel";
+import { Message } from "./Message";
+import { User } from "./User";
+
+// for read receipts
+// notification cursor and public read receipt need to be forwards-only (the former to prevent re-pinging when marked as unread, and the latter to be acceptable as a legal acknowledgement in criminal proceedings), and private read marker needs to be advance-rewind capable
+// public read receipt ≥ notification cursor ≥ private fully read marker
+
+@Entity("read_states")
+export class ReadState extends BaseClass {
+ @Column({ nullable: true })
+ @RelationId((read_state: ReadState) => read_state.channel)
+ channel_id: string;
+
+ @JoinColumn({ name: "channel_id" })
+ @ManyToOne(() => Channel)
+ channel: Channel;
+
+ @Column({ nullable: true })
+ @RelationId((read_state: ReadState) => read_state.user)
+ user_id: string;
+
+ @JoinColumn({ name: "user_id" })
+ @ManyToOne(() => User)
+ user: User;
+
+ @Column({ nullable: true })
+ @RelationId((read_state: ReadState) => read_state.last_message)
+ last_message_id: string;
+
+ @JoinColumn({ name: "last_message_id" })
+ @ManyToOne(() => Message)
+ last_message?: Message;
+
+ @Column({ nullable: true })
+ last_pin_timestamp?: Date;
+
+ @Column()
+ mention_count: number;
+
+ @Column()
+ manual: boolean;
+}
diff --git a/util/src/entities/Recipient.ts b/util/src/entities/Recipient.ts
new file mode 100644
index 00000000..75d5b94d
--- /dev/null
+++ b/util/src/entities/Recipient.ts
@@ -0,0 +1,19 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+
+@Entity("recipients")
+export class Recipient extends BaseClass {
+ @Column()
+ @RelationId((recipient: Recipient) => recipient.channel)
+ channel_id: string;
+
+ @JoinColumn({ name: "channel_id" })
+ @ManyToOne(() => require("./Channel").Channel)
+ channel: import("./Channel").Channel;
+
+ @JoinColumn({ name: "id" })
+ @ManyToOne(() => require("./User").User)
+ user: import("./User").User;
+
+ // TODO: settings/mute/nick/added at/encryption keys/read_state
+}
diff --git a/util/src/entities/Relationship.ts b/util/src/entities/Relationship.ts
new file mode 100644
index 00000000..5935f5b6
--- /dev/null
+++ b/util/src/entities/Relationship.ts
@@ -0,0 +1,27 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { User } from "./User";
+
+export enum RelationshipType {
+ outgoing = 4,
+ incoming = 3,
+ blocked = 2,
+ friends = 1,
+}
+
+@Entity("relationships")
+export class Relationship extends BaseClass {
+ @Column({ nullable: true })
+ @RelationId((relationship: Relationship) => relationship.user)
+ user_id: string;
+
+ @JoinColumn({ name: "user_id" })
+ @ManyToOne(() => User)
+ user: User;
+
+ @Column({ nullable: true })
+ nickname?: string;
+
+ @Column({ type: "simple-enum", enum: RelationshipType })
+ type: RelationshipType;
+}
diff --git a/util/src/entities/Role.ts b/util/src/entities/Role.ts
new file mode 100644
index 00000000..33c8d272
--- /dev/null
+++ b/util/src/entities/Role.ts
@@ -0,0 +1,43 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+
+import { BaseClass } from "./BaseClass";
+import { Guild } from "./Guild";
+
+@Entity("roles")
+export class Role extends BaseClass {
+ @Column({ nullable: true })
+ @RelationId((role: Role) => role.guild)
+ guild_id: string;
+
+ @JoinColumn({ name: "guild_id" })
+ @ManyToOne(() => Guild)
+ guild: Guild;
+
+ @Column()
+ color: number;
+
+ @Column()
+ hoist: boolean;
+
+ @Column()
+ managed: boolean;
+
+ @Column()
+ mentionable: boolean;
+
+ @Column()
+ name: string;
+
+ @Column()
+ permissions: string;
+
+ @Column()
+ position: number;
+
+ @Column({ type: "simple-json", nullable: true })
+ tags?: {
+ bot_id?: string;
+ integration_id?: string;
+ premium_subscriber?: boolean;
+ };
+}
diff --git a/util/src/entities/Sticker.ts b/util/src/entities/Sticker.ts
new file mode 100644
index 00000000..7730a86a
--- /dev/null
+++ b/util/src/entities/Sticker.ts
@@ -0,0 +1,42 @@
+import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { Guild } from "./Guild";
+
+export enum StickerType {
+ STANDARD = 1,
+ GUILD = 2,
+}
+
+export enum StickerFormatType {
+ PNG = 1,
+ APNG = 2,
+ LOTTIE = 3,
+}
+
+@Entity("stickers")
+export class Sticker extends BaseClass {
+ @Column()
+ name: string;
+
+ @Column({ nullable: true })
+ description?: string;
+
+ @Column()
+ tags: string;
+
+ @Column()
+ pack_id: string;
+
+ @Column({ nullable: true })
+ guild_id?: string;
+
+ @JoinColumn({ name: "guild_id" })
+ @ManyToOne(() => Guild)
+ guild?: Guild;
+
+ @Column({ type: "simple-enum", enum: StickerType })
+ type: StickerType;
+
+ @Column({ type: "simple-enum", enum: StickerFormatType })
+ format_type: StickerFormatType;
+}
diff --git a/util/src/entities/Team.ts b/util/src/entities/Team.ts
new file mode 100644
index 00000000..beb8bf68
--- /dev/null
+++ b/util/src/entities/Team.ts
@@ -0,0 +1,25 @@
+import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, OneToMany, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { TeamMember } from "./TeamMember";
+import { User } from "./User";
+
+@Entity("teams")
+export class Team extends BaseClass {
+ @Column({ nullable: true })
+ icon?: string;
+
+ @JoinColumn({ name: "member_ids" })
+ @OneToMany(() => TeamMember, (member: TeamMember) => member.team)
+ members: TeamMember[];
+
+ @Column()
+ name: string;
+
+ @Column({ nullable: true })
+ @RelationId((team: Team) => team.owner_user)
+ owner_user_id: string;
+
+ @JoinColumn({ name: "owner_user_id" })
+ @ManyToOne(() => User)
+ owner_user: User;
+}
diff --git a/util/src/entities/TeamMember.ts b/util/src/entities/TeamMember.ts
new file mode 100644
index 00000000..6b184d08
--- /dev/null
+++ b/util/src/entities/TeamMember.ts
@@ -0,0 +1,33 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { User } from "./User";
+
+export enum TeamMemberState {
+ INVITED = 1,
+ ACCEPTED = 2,
+}
+
+@Entity("team_members")
+export class TeamMember extends BaseClass {
+ @Column({ type: "simple-enum", enum: TeamMemberState })
+ membership_state: TeamMemberState;
+
+ @Column({ type: "simple-array" })
+ permissions: string[];
+
+ @Column({ nullable: true })
+ @RelationId((member: TeamMember) => member.team)
+ team_id: string;
+
+ @JoinColumn({ name: "team_id" })
+ @ManyToOne(() => require("./Team").Team, (team: import("./Team").Team) => team.members)
+ team: import("./Team").Team;
+
+ @Column({ nullable: true })
+ @RelationId((member: TeamMember) => member.user)
+ user_id: string;
+
+ @JoinColumn({ name: "user_id" })
+ @ManyToOne(() => User)
+ user: User;
+}
diff --git a/util/src/entities/Template.ts b/util/src/entities/Template.ts
new file mode 100644
index 00000000..76f77ba6
--- /dev/null
+++ b/util/src/entities/Template.ts
@@ -0,0 +1,44 @@
+import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { Guild } from "./Guild";
+import { User } from "./User";
+
+@Entity("templates")
+export class Template extends BaseClass {
+ @PrimaryColumn()
+ code: string;
+
+ @Column()
+ name: string;
+
+ @Column({ nullable: true })
+ description?: string;
+
+ @Column({ nullable: true })
+ usage_count?: number;
+
+ @Column({ nullable: true })
+ @RelationId((template: Template) => template.creator)
+ creator_id: string;
+
+ @JoinColumn({ name: "creator_id" })
+ @ManyToOne(() => User)
+ creator: User;
+
+ @Column()
+ created_at: Date;
+
+ @Column()
+ updated_at: Date;
+
+ @Column({ nullable: true })
+ @RelationId((template: Template) => template.source_guild)
+ source_guild_id: string;
+
+ @JoinColumn({ name: "source_guild_id" })
+ @ManyToOne(() => Guild)
+ source_guild: Guild;
+
+ @Column({ type: "simple-json" })
+ serialized_source_guild: Guild;
+}
diff --git a/util/src/entities/User.ts b/util/src/entities/User.ts
new file mode 100644
index 00000000..39f654be
--- /dev/null
+++ b/util/src/entities/User.ts
@@ -0,0 +1,243 @@
+import { Column, Entity, FindOneOptions, JoinColumn, ManyToMany, OneToMany, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { BitField } from "../util/BitField";
+import { Relationship } from "./Relationship";
+import { ConnectedAccount } from "./ConnectedAccount";
+import { HTTPError } from "lambert-server";
+import { Channel } from "./Channel";
+
+type PublicUserKeys =
+ | "username"
+ | "discriminator"
+ | "id"
+ | "public_flags"
+ | "avatar"
+ | "accent_color"
+ | "banner"
+ | "bio"
+ | "bot";
+export const PublicUserProjection: PublicUserKeys[] = [
+ "username",
+ "discriminator",
+ "id",
+ "public_flags",
+ "avatar",
+ "accent_color",
+ "banner",
+ "bio",
+ "bot",
+];
+
+// Private user data that should never get sent to the client
+export type PublicUser = Pick<User, PublicUserKeys>;
+
+@Entity("users")
+export class User extends BaseClass {
+ @Column()
+ username: string; // username max length 32, min 2 (should be configurable)
+
+ @Column()
+ discriminator: string; // #0001 4 digit long string from #0001 - #9999
+
+ setDiscriminator(val: string) {
+ const number = Number(val);
+ if (isNaN(number)) throw new Error("invalid discriminator");
+ if (number <= 0 || number > 10000) throw new Error("discriminator must be between 1 and 9999");
+ this.discriminator = val.toString().padStart(4, "0");
+ }
+
+ @Column({ nullable: true })
+ avatar?: string; // hash of the user avatar
+
+ @Column({ nullable: true })
+ accent_color?: number; // banner color of user
+
+ @Column({ nullable: true })
+ banner?: string; // hash of the user banner
+
+ @Column({ nullable: true })
+ phone?: string; // phone number of the user
+
+ @Column()
+ desktop: boolean; // if the user has desktop app installed
+
+ @Column()
+ mobile: boolean; // if the user has mobile app installed
+
+ @Column()
+ premium: boolean; // if user bought nitro
+
+ @Column()
+ premium_type: number; // nitro level
+
+ @Column()
+ bot: boolean; // if user is bot
+
+ @Column()
+ bio: string; // short description of the user (max 190 chars -> should be configurable)
+
+ @Column()
+ system: boolean; // shouldn't be used, the api sents this field type true, if the generated message comes from a system generated author
+
+ @Column()
+ nsfw_allowed: boolean; // if the user is older than 18 (resp. Config)
+
+ @Column()
+ mfa_enabled: boolean; // if multi factor authentication is enabled
+
+ @Column()
+ created_at: Date = new Date(); // registration date
+
+ @Column()
+ verified: boolean; // if the user is offically verified
+
+ @Column()
+ disabled: boolean; // if the account is disabled
+
+ @Column()
+ deleted: boolean; // if the user was deleted
+
+ @Column({ nullable: true })
+ email?: string; // email of the user
+
+ @Column()
+ flags: string; // UserFlags
+
+ @Column()
+ public_flags: string;
+
+ @JoinColumn({ name: "relationship_ids" })
+ @OneToMany(() => Relationship, (relationship: Relationship) => relationship.user, { cascade: true })
+ relationships: Relationship[];
+
+ @JoinColumn({ name: "connected_account_ids" })
+ @OneToMany(() => ConnectedAccount, (account: ConnectedAccount) => account.user)
+ connected_accounts: ConnectedAccount[];
+
+ @Column({ type: "simple-json", select: false })
+ 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)
+ };
+
+ @Column({ type: "simple-array" })
+ fingerprints: string[]; // array of fingerprints -> used to prevent multiple accounts
+
+ @Column({ type: "simple-json" })
+ settings: UserSettings;
+
+ static async getPublicUser(user_id: string, opts?: FindOneOptions<User>) {
+ const user = await User.findOne(
+ { id: user_id },
+ {
+ ...opts,
+ select: [...PublicUserProjection, ...(opts?.select || [])],
+ }
+ );
+ if (!user) throw new HTTPError("User not found", 404);
+ return user;
+ }
+}
+
+export const defaultSettings: UserSettings = {
+ afk_timeout: 300,
+ allow_accessibility_detection: true,
+ animate_emoji: true,
+ animate_stickers: 0,
+ contact_sync_enabled: false,
+ convert_emoticons: false,
+ custom_status: {
+ emoji_id: undefined,
+ emoji_name: undefined,
+ expires_at: undefined,
+ text: undefined,
+ },
+ default_guilds_restricted: false,
+ detect_platform_accounts: true,
+ developer_mode: false,
+ disable_games_tab: false,
+ enable_tts_command: true,
+ explicit_content_filter: 0,
+ friend_source_flags: { all: true },
+ gateway_connected: false,
+ gif_auto_play: true,
+ guild_folders: [],
+ guild_positions: [],
+ inline_attachment_media: true,
+ inline_embed_media: true,
+ locale: "en",
+ message_display_compact: false,
+ native_phone_integration_enabled: true,
+ render_embeds: true,
+ render_reactions: true,
+ restricted_guilds: [],
+ show_current_game: true,
+ status: "offline",
+ stream_notifications_enabled: true,
+ theme: "dark",
+ timezone_offset: 0,
+ // timezone_offset: // TODO: timezone from request
+};
+
+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;
+ emoji_name?: string;
+ expires_at?: number;
+ text?: string;
+ };
+ 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;
+ // every top guild is displayed as a "folder"
+ guild_folders: {
+ 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
+}
+
+export class UserFlags extends BitField {
+ static FLAGS = {
+ DISCORD_EMPLOYEE: BigInt(1) << BigInt(0),
+ PARTNERED_SERVER_OWNER: BigInt(1) << BigInt(1),
+ HYPESQUAD_EVENTS: BigInt(1) << BigInt(2),
+ BUGHUNTER_LEVEL_1: BigInt(1) << BigInt(3),
+ HOUSE_BRAVERY: BigInt(1) << BigInt(6),
+ HOUSE_BRILLIANCE: BigInt(1) << BigInt(7),
+ HOUSE_BALANCE: BigInt(1) << BigInt(8),
+ EARLY_SUPPORTER: BigInt(1) << BigInt(9),
+ TEAM_USER: BigInt(1) << BigInt(10),
+ SYSTEM: BigInt(1) << BigInt(12),
+ BUGHUNTER_LEVEL_2: BigInt(1) << BigInt(14),
+ VERIFIED_BOT: BigInt(1) << BigInt(16),
+ EARLY_VERIFIED_BOT_DEVELOPER: BigInt(1) << BigInt(17),
+ };
+}
diff --git a/util/src/entities/VoiceState.ts b/util/src/entities/VoiceState.ts
new file mode 100644
index 00000000..c5040cf1
--- /dev/null
+++ b/util/src/entities/VoiceState.ts
@@ -0,0 +1,56 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { BaseClass } from "./BaseClass";
+import { Channel } from "./Channel";
+import { Guild } from "./Guild";
+import { User } from "./User";
+
+@Entity("voice_states")
+export class VoiceState extends BaseClass {
+ @Column({ nullable: true })
+ @RelationId((voice_state: VoiceState) => voice_state.guild)
+ guild_id: string;
+
+ @JoinColumn({ name: "guild_id" })
+ @ManyToOne(() => Guild)
+ guild?: Guild;
+
+ @Column({ nullable: true })
+ @RelationId((voice_state: VoiceState) => voice_state.channel)
+ channel_id: string;
+
+ @JoinColumn({ name: "channel_id" })
+ @ManyToOne(() => Channel)
+ channel: Channel;
+
+ @Column({ nullable: true })
+ @RelationId((voice_state: VoiceState) => voice_state.user)
+ user_id: string;
+
+ @JoinColumn({ name: "user_id" })
+ @ManyToOne(() => User)
+ user: User;
+
+ @Column()
+ session_id: string;
+
+ @Column()
+ deaf: boolean;
+
+ @Column()
+ mute: boolean;
+
+ @Column()
+ self_deaf: boolean;
+
+ @Column()
+ self_mute: boolean;
+
+ @Column({ nullable: true })
+ self_stream?: boolean;
+
+ @Column()
+ self_video: boolean;
+
+ @Column()
+ suppress: boolean; // whether this user is muted by the current user
+}
diff --git a/util/src/entities/Webhook.ts b/util/src/entities/Webhook.ts
new file mode 100644
index 00000000..12ba0d08
--- /dev/null
+++ b/util/src/entities/Webhook.ts
@@ -0,0 +1,69 @@
+import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { Application } from "./Application";
+import { BaseClass } from "./BaseClass";
+import { Channel } from "./Channel";
+import { Guild } from "./Guild";
+import { User } from "./User";
+
+export enum WebhookType {
+ Incoming = 1,
+ ChannelFollower = 2,
+}
+
+@Entity("webhooks")
+export class Webhook extends BaseClass {
+ @Column()
+ id: string;
+
+ @Column({ type: "simple-enum", enum: WebhookType })
+ type: WebhookType;
+
+ @Column({ nullable: true })
+ name?: string;
+
+ @Column({ nullable: true })
+ avatar?: string;
+
+ @Column({ nullable: true })
+ token?: string;
+
+ @Column({ nullable: true })
+ @RelationId((webhook: Webhook) => webhook.guild)
+ guild_id: string;
+
+ @JoinColumn({ name: "guild_id" })
+ @ManyToOne(() => Guild)
+ guild: Guild;
+
+ @Column({ nullable: true })
+ @RelationId((webhook: Webhook) => webhook.channel)
+ channel_id: string;
+
+ @JoinColumn({ name: "channel_id" })
+ @ManyToOne(() => Channel)
+ channel: Channel;
+
+ @Column({ nullable: true })
+ @RelationId((webhook: Webhook) => webhook.application)
+ application_id: string;
+
+ @JoinColumn({ name: "application_id" })
+ @ManyToOne(() => Application)
+ application: Application;
+
+ @Column({ nullable: true })
+ @RelationId((webhook: Webhook) => webhook.user)
+ user_id: string;
+
+ @JoinColumn({ name: "user_id" })
+ @ManyToOne(() => User)
+ user: User;
+
+ @Column({ nullable: true })
+ @RelationId((webhook: Webhook) => webhook.guild)
+ source_guild_id: string;
+
+ @JoinColumn({ name: "source_guild_id" })
+ @ManyToOne(() => Guild)
+ source_guild: Guild;
+}
diff --git a/util/src/entities/index.ts b/util/src/entities/index.ts
new file mode 100644
index 00000000..aa37ae2e
--- /dev/null
+++ b/util/src/entities/index.ts
@@ -0,0 +1,25 @@
+export * from "./Application";
+export * from "./Attachment";
+export * from "./AuditLog";
+export * from "./Ban";
+export * from "./BaseClass";
+export * from "./Channel";
+export * from "./Config";
+export * from "./ConnectedAccount";
+export * from "./Emoji";
+export * from "./Guild";
+export * from "./Invite";
+export * from "./Member";
+export * from "./Message";
+export * from "./RateLimit";
+export * from "./ReadState";
+export * from "./Recipient";
+export * from "./Relationship";
+export * from "./Role";
+export * from "./Sticker";
+export * from "./Team";
+export * from "./TeamMember";
+export * from "./Template";
+export * from "./User";
+export * from "./VoiceState";
+export * from "./Webhook";
diff --git a/util/src/index.ts b/util/src/index.ts
index 3565fb6b..f3bd9e9b 100644
--- a/util/src/index.ts
+++ b/util/src/index.ts
@@ -1,10 +1,11 @@
-export * from "./util/checkToken";
+import "reflect-metadata";
-export * as Constants from "./util/Constants";
-export * from "./models/index";
+// export * as Constants from "../util/Constants";
export * from "./util/index";
+export * from "./interfaces/index";
+export * from "./entities/index";
-import Config from "./util/Config";
-import db, { MongooseCache, toObject } from "./util/Database";
+// import Config from "../util/Config";
+// import db, { MongooseCache, toObject } from "./util/Database";
-export { Config, db, MongooseCache, toObject };
+// export { Config };
diff --git a/util/src/interfaces/Activity.ts b/util/src/interfaces/Activity.ts
new file mode 100644
index 00000000..f5a3c270
--- /dev/null
+++ b/util/src/interfaces/Activity.ts
@@ -0,0 +1,43 @@
+export interface Activity {
+ name: string;
+ type: ActivityType;
+ url?: string;
+ created_at?: Date;
+ timestamps?: {
+ start?: number;
+ end?: number;
+ }[];
+ application_id?: string;
+ details?: string;
+ state?: string;
+ emoji?: {
+ name: string;
+ id?: string;
+ amimated?: boolean;
+ };
+ party?: {
+ id?: string;
+ size?: [number, number];
+ };
+ assets?: {
+ large_image?: string;
+ large_text?: string;
+ small_image?: string;
+ small_text?: string;
+ };
+ secrets?: {
+ join?: string;
+ spectate?: string;
+ match?: string;
+ };
+ instance?: boolean;
+ flags?: bigint;
+}
+
+export enum ActivityType {
+ GAME = 0,
+ STREAMING = 1,
+ LISTENING = 2,
+ CUSTOM = 4,
+ COMPETING = 5,
+}
diff --git a/util/src/models/Event.ts b/util/src/interfaces/Event.ts
index 86d0fd00..7ea1bd49 100644
--- a/util/src/models/Event.ts
+++ b/util/src/interfaces/Event.ts
@@ -1,15 +1,17 @@
-import { ConnectedAccount, PublicUser, Relationship, User, UserSettings } from "./User";
-import { DMChannel, Channel } from "./Channel";
-import { Guild } from "./Guild";
-import { Member, PublicMember, UserGuildSettings } from "./Member";
-import { Emoji } from "./Emoji";
-import { Presence } from "./Activity";
-import { Role } from "./Role";
-import { Invite } from "./Invite";
-import { Message, PartialEmoji } from "./Message";
-import { VoiceState } from "./VoiceState";
-import { ApplicationCommand } from "./Application";
+import { PublicUser, User, UserSettings } from "../entities/User";
+import { Channel } from "../entities/Channel";
+import { Guild } from "../entities/Guild";
+import { Member, PublicMember, UserGuildSettings } from "../entities/Member";
+import { Emoji } from "../entities/Emoji";
+import { Role } from "../entities/Role";
+import { Invite } from "../entities/Invite";
+import { Message, PartialEmoji } from "../entities/Message";
+import { VoiceState } from "../entities/VoiceState";
+import { ApplicationCommand } from "../entities/Application";
import { Interaction } from "./Interaction";
+import { ConnectedAccount } from "../entities/ConnectedAccount";
+import { Relationship } from "../entities/Relationship";
+import { Presence } from "./Presence";
export interface Event {
guild_id?: string;
@@ -33,17 +35,17 @@ export interface ReadyEventData {
user: PublicUser & {
mobile: boolean;
desktop: boolean;
- email: string | null;
- flags: bigint;
+ email: string | undefined;
+ flags: string;
mfa_enabled: boolean;
nsfw_allowed: boolean;
- phone: string | null;
+ phone: string | undefined;
premium: boolean;
premium_type: number;
verified: boolean;
bot: boolean;
};
- private_channels: DMChannel[]; // this will be empty for bots
+ private_channels: Channel[]; // this will be empty for bots
session_id: string; // resuming
guilds: Guild[];
analytics_token?: string;
@@ -67,12 +69,12 @@ export interface ReadyEventData {
[number, [[number, [number, number]]]],
{ b: number; k: bigint[] }[]
][];
- guild_join_requests?: []; // ? what is this? this is new
+ guild_join_requests?: any[]; // ? what is this? this is new
shard?: [number, number];
user_settings?: UserSettings;
relationships?: Relationship[]; // TODO
read_state: {
- entries: []; // TODO
+ entries: any[]; // TODO
partial: boolean;
version: number;
};
@@ -83,18 +85,11 @@ export interface ReadyEventData {
};
application?: {
id: string;
- flags: bigint;
+ flags: string;
};
merged_members?: Omit<Member, "settings" | "user">[][];
// probably all users who the user is in contact with
- users?: {
- avatar: string | null;
- discriminator: string;
- id: string;
- username: string;
- bot: boolean;
- public_flags: bigint;
- }[];
+ users?: PublicUser[];
}
export interface ReadyEvent extends Event {
@@ -128,7 +123,9 @@ export interface ChannelPinsUpdateEvent extends Event {
export interface GuildCreateEvent extends Event {
event: "GUILD_CREATE";
- data: Guild;
+ data: Guild & {
+ joined_at: Date;
+ };
}
export interface GuildUpdateEvent extends Event {
@@ -257,22 +254,14 @@ export interface InviteDeleteEvent extends Event {
};
}
-export type MessagePayload = Omit<Message, "author_id"> & {
- channel_id: string;
- guild_id?: string;
- author: PublicUser;
- member: PublicMember;
- mentions: (PublicUser & { member: PublicMember })[];
-};
-
export interface MessageCreateEvent extends Event {
event: "MESSAGE_CREATE";
- data: MessagePayload;
+ data: Message;
}
export interface MessageUpdateEvent extends Event {
event: "MESSAGE_UPDATE";
- data: MessagePayload;
+ data: Message;
}
export interface MessageDeleteEvent extends Event {
@@ -521,4 +510,4 @@ export type EVENT =
| "RELATIONSHIP_REMOVE"
| CUSTOMEVENTS;
-export type CUSTOMEVENTS = "INVALIDATED";
+export type CUSTOMEVENTS = "INVALIDATED" | "RATELIMIT";
diff --git a/util/src/models/Interaction.ts b/util/src/interfaces/Interaction.ts
index 764247a5..3cafb2d5 100644
--- a/util/src/models/Interaction.ts
+++ b/util/src/interfaces/Interaction.ts
@@ -1,4 +1,4 @@
-import { AllowedMentions, Embed } from "./Message";
+import { AllowedMentions, Embed } from "../entities/Message";
export interface Interaction {
id: string;
diff --git a/util/src/interfaces/Presence.ts b/util/src/interfaces/Presence.ts
new file mode 100644
index 00000000..4a1ff038
--- /dev/null
+++ b/util/src/interfaces/Presence.ts
@@ -0,0 +1,10 @@
+import { ClientStatus, Status } from "./Status";
+import { Activity } from "./Activity";
+
+export interface Presence {
+ user_id: string;
+ guild_id?: string;
+ status: Status;
+ activities: Activity[];
+ client_status: ClientStatus;
+}
diff --git a/util/src/models/Status.ts b/util/src/interfaces/Status.ts
index 5a9bf2ca..c4dab586 100644
--- a/util/src/models/Status.ts
+++ b/util/src/interfaces/Status.ts
@@ -5,9 +5,3 @@ export interface ClientStatus {
mobile?: string; // e.g. iOS/Android
web?: string; // e.g. browser, bot account
}
-
-export const ClientStatus = {
- desktop: String,
- mobile: String,
- web: String,
-};
diff --git a/util/src/interfaces/index.ts b/util/src/interfaces/index.ts
new file mode 100644
index 00000000..ab7fa429
--- /dev/null
+++ b/util/src/interfaces/index.ts
@@ -0,0 +1,5 @@
+export * from "./Activity";
+export * from "./Presence";
+export * from "./Interaction";
+export * from "./Event";
+export * from "./Status";
diff --git a/util/src/models/Activity.ts b/util/src/models/Activity.ts
deleted file mode 100644
index 17abd1ca..00000000
--- a/util/src/models/Activity.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-import { User } from "..";
-import { ClientStatus, Status } from "./Status";
-import { Schema, model, Types, Document } from "mongoose";
-import toBigInt from "../util/toBigInt";
-
-export interface Presence {
- user: User;
- guild_id?: string;
- status: Status;
- activities: Activity[];
- client_status: ClientStatus;
-}
-
-export interface Activity {
- name: string;
- type: ActivityType;
- url?: string;
- created_at?: Date;
- timestamps?: {
- start?: number;
- end?: number;
- }[];
- application_id?: string;
- details?: string;
- state?: string;
- emoji?: {
- name: string;
- id?: string;
- amimated?: boolean;
- };
- party?: {
- id?: string;
- size?: [number, number];
- };
- assets?: {
- large_image?: string;
- large_text?: string;
- small_image?: string;
- small_text?: string;
- };
- secrets?: {
- join?: string;
- spectate?: string;
- match?: string;
- };
- instance?: boolean;
- flags?: bigint;
-}
-
-export const ActivitySchema = {
- name: { type: String, required: true },
- type: { type: Number, required: true },
- url: String,
- created_at: Date,
- timestamps: [
- {
- start: Number,
- end: Number,
- },
- ],
- application_id: String,
- details: String,
- state: String,
- emoji: {
- name: String,
- id: String,
- amimated: Boolean,
- },
- party: {
- id: String,
- size: [Number, Number],
- },
- assets: {
- large_image: String,
- large_text: String,
- small_image: String,
- small_text: String,
- },
- secrets: {
- join: String,
- spectate: String,
- match: String,
- },
- instance: Boolean,
- flags: { type: String, get: toBigInt },
-};
-
-export const ActivityBodySchema = {
- name: String,
- type: Number,
- $url: String,
- $created_at: Date,
- $timestamps: [
- {
- $start: Number,
- $end: Number,
- },
- ],
- $application_id: String,
- $details: String,
- $state: String,
- $emoji: {
- $name: String,
- $id: String,
- $amimated: Boolean,
- },
- $party: {
- $id: String,
- $size: [Number, Number],
- },
- $assets: {
- $large_image: String,
- $large_text: String,
- $small_image: String,
- $small_text: String,
- },
- $secrets: {
- $join: String,
- $spectate: String,
- $match: String,
- },
- $instance: Boolean,
- $flags: BigInt,
-};
-
-export enum ActivityType {
- GAME = 0,
- STREAMING = 1,
- LISTENING = 2,
- CUSTOM = 4,
- COMPETING = 5,
-}
diff --git a/util/src/models/Application.ts b/util/src/models/Application.ts
deleted file mode 100644
index fae6e8db..00000000
--- a/util/src/models/Application.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { Team } from "./Team";
-
-export interface Application {
- id: string;
- name: string;
- icon: string | null;
- description: string;
- rpc_origins: string[] | null;
- bot_public: boolean;
- bot_require_code_grant: boolean;
- terms_of_service_url: string | null;
- privacy_policy_url: string | null;
- owner_id: string;
- summary: string | null;
- verify_key: string;
- team: Team | null;
- guild_id: string; // if this application is a game sold on Discord, this field will be the guild to which it has been linked
- primary_sku_id: string | null; // if this application is a game sold on Discord, this field will be the id of the "Game SKU" that is created, if exists
- slug: string | null; // if this application is a game sold on Discord, this field will be the URL slug that links to the store page
- cover_image: string | null; // the application's default rich presence invite cover image hash
- flags: number; // the application's public flags
-}
-
-export interface ApplicationCommand {
- id: string;
- application_id: string;
- name: string;
- description: string;
- options?: ApplicationCommandOption[];
-}
-
-export interface ApplicationCommandOption {
- type: ApplicationCommandOptionType;
- name: string;
- description: string;
- required?: boolean;
- choices?: ApplicationCommandOptionChoice[];
- options?: ApplicationCommandOption[];
-}
-
-export interface ApplicationCommandOptionChoice {
- name: string;
- value: string | number;
-}
-
-export enum ApplicationCommandOptionType {
- SUB_COMMAND = 1,
- SUB_COMMAND_GROUP = 2,
- STRING = 3,
- INTEGER = 4,
- BOOLEAN = 5,
- USER = 6,
- CHANNEL = 7,
- ROLE = 8,
-}
-
-export interface ApplicationCommandInteractionData {
- id: string;
- name: string;
- options?: ApplicationCommandInteractionDataOption[];
-}
-
-export interface ApplicationCommandInteractionDataOption {
- name: string;
- value?: any;
- options?: ApplicationCommandInteractionDataOption[];
-}
diff --git a/util/src/models/Ban.ts b/util/src/models/Ban.ts
deleted file mode 100644
index f09950ee..00000000
--- a/util/src/models/Ban.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Schema, model, Types, Document } from "mongoose";
-import db from "../util/Database";
-import { PublicUserProjection, UserModel } from "./User";
-
-export interface Ban extends Document {
- user_id: string;
- guild_id: string;
- executor_id: string;
- ip: string;
- reason?: string;
-}
-
-export const BanSchema = new Schema({
- user_id: { type: String, required: true },
- guild_id: { type: String, required: true },
- executor_id: { type: String, required: true },
- reason: String,
- ip: String, // ? Should we store this in here, or in the UserModel?
-});
-
-BanSchema.virtual("user", {
- ref: UserModel,
- localField: "user_id",
- foreignField: "id",
- justOne: true,
- autopopulate: { select: PublicUserProjection },
-});
-
-BanSchema.set("removeResponse", ["user_id"]);
-
-// @ts-ignore
-export const BanModel = db.model<Ban>("Ban", BanSchema, "bans");
diff --git a/util/src/models/Channel.ts b/util/src/models/Channel.ts
deleted file mode 100644
index 2959decd..00000000
--- a/util/src/models/Channel.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import { Schema, model, Types, Document } from "mongoose";
-import db from "../util/Database";
-import toBigInt from "../util/toBigInt";
-import { PublicUserProjection, UserModel } from "./User";
-
-// @ts-ignore
-export interface AnyChannel extends Channel, DMChannel, TextChannel, VoiceChannel {
- recipient_ids: null | string[];
-}
-
-export interface ChannelDocument extends Document, AnyChannel {
- id: string;
-}
-
-export const ChannelSchema = new Schema({
- id: String,
- created_at: { type: Schema.Types.Date, required: true },
- name: String, // can't be required for dm channels
- type: { type: Number, required: true },
- guild_id: String,
- owner_id: String,
- parent_id: String,
- recipient_ids: [String],
- position: Number,
- last_message_id: String,
- last_pin_timestamp: Date,
- nsfw: Boolean,
- rate_limit_per_user: Number,
- default_auto_archive_duration: Number,
- topic: String,
- permission_overwrites: [
- {
- allow: { type: String, get: toBigInt },
- deny: { type: String, get: toBigInt },
- id: String,
- type: { type: Number },
- },
- ],
-});
-
-ChannelSchema.virtual("recipients", {
- ref: UserModel,
- localField: "recipient_ids",
- foreignField: "id",
- justOne: false,
- autopopulate: { select: PublicUserProjection },
-});
-
-ChannelSchema.set("removeResponse", ["recipient_ids"]);
-
-// @ts-ignore
-export const ChannelModel = db.model<ChannelDocument>("Channel", ChannelSchema, "channels");
-
-export interface Channel {
- id: string;
- created_at: Date;
- name: string;
- type: number;
-}
-
-export interface TextBasedChannel {
- last_message_id?: string;
- last_pin_timestamp?: number;
- default_auto_archive_duration?: number;
-}
-
-export interface GuildChannel extends Channel {
- guild_id: string;
- position: number;
- parent_id?: string;
- permission_overwrites: ChannelPermissionOverwrite[];
-}
-
-export interface ChannelPermissionOverwrite {
- allow: bigint; // for bitfields we use bigints
- deny: bigint; // for bitfields we use bigints
- id: string;
- type: ChannelPermissionOverwriteType;
-}
-
-export enum ChannelPermissionOverwriteType {
- role = 0,
- member = 1,
-}
-
-export interface VoiceChannel extends GuildChannel {
- video_quality_mode?: number;
- bitrate?: number;
- user_limit?: number;
-}
-
-export interface TextChannel extends GuildChannel, TextBasedChannel {
- nsfw: boolean;
- rate_limit_per_user: number;
- topic?: string;
-}
-// @ts-ignore
-export interface DMChannel extends Channel, TextBasedChannel {
- owner_id: string;
- recipient_ids: string[];
-}
-
-export enum ChannelType {
- GUILD_TEXT = 0, // a text channel within a server
- DM = 1, // a direct message between users
- GUILD_VOICE = 2, // a voice channel within a server
- GROUP_DM = 3, // a direct message between multiple users
- GUILD_CATEGORY = 4, // an organizational category that contains up to 50 channels
- GUILD_NEWS = 5, // a channel that users can follow and crosspost into their own server
- GUILD_STORE = 6, // a channel in which game developers can sell their game on Discord
-}
diff --git a/util/src/models/Emoji.ts b/util/src/models/Emoji.ts
deleted file mode 100644
index 3e5cad53..00000000
--- a/util/src/models/Emoji.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Schema, model, Types, Document } from "mongoose";
-import db from "../util/Database";
-
-export interface Emoji extends Document {
- id: string;
- animated: boolean;
- available: boolean;
- guild_id: string;
- managed: boolean;
- name: string;
- require_colons: boolean;
- url: string;
- roles: string[]; // roles this emoji is whitelisted to (new discord feature?)
-}
-
-export const EmojiSchema = new Schema({
- id: { type: String, required: true },
- animated: Boolean,
- available: Boolean,
- guild_id: String,
- managed: Boolean,
- name: String,
- require_colons: Boolean,
- url: String,
- roles: [String],
-});
-
-// @ts-ignore
-export const EmojiModel = db.model<Emoji>("Emoji", EmojiSchema, "emojis");
diff --git a/util/src/models/Guild.ts b/util/src/models/Guild.ts
deleted file mode 100644
index a5dcd8e3..00000000
--- a/util/src/models/Guild.ts
+++ /dev/null
@@ -1,159 +0,0 @@
-import { Schema, model, Types, Document } from "mongoose";
-import db from "../util/Database";
-import { ChannelModel } from "./Channel";
-import { EmojiModel } from "./Emoji";
-import { MemberModel } from "./Member";
-import { RoleModel } from "./Role";
-
-export interface GuildDocument extends Document, Guild {
- id: string;
-}
-
-export interface Guild {
- id: string;
- afk_channel_id?: string;
- afk_timeout?: number;
- application_id?: string;
- banner?: string;
- default_message_notifications?: number;
- description?: string;
- discovery_splash?: string;
- explicit_content_filter?: number;
- features: string[];
- icon?: string;
- large?: boolean;
- max_members?: number; // e.g. default 100.000
- max_presences?: number;
- max_video_channel_users?: number; // ? default: 25, is this max 25 streaming or watching
- member_count?: number;
- presence_count?: number; // users online
- // members?: Member[]; // * Members are stored in a seperate collection
- // roles: Role[]; // * Role are stored in a seperate collection
- // channels: GuildChannel[]; // * Channels are stored in a seperate collection
- // emojis: Emoji[]; // * Emojis are stored in a seperate collection
- // voice_states: []; // * voice_states are stored in a seperate collection
- //TODO:
- presences?: object[];
- mfa_level?: number;
- name: string;
- owner_id: string;
- preferred_locale?: string; // only community guilds can choose this
- premium_subscription_count?: number;
- premium_tier?: number; // nitro boost level
- public_updates_channel_id?: string;
- region?: string;
- rules_channel_id?: string;
- splash?: string;
- system_channel_flags?: number;
- system_channel_id?: string;
- unavailable?: boolean;
- vanity_url_code?: string;
- verification_level?: number;
- welcome_screen: {
- enabled: boolean;
- description: string;
- welcome_channels: {
- description: string;
- emoji_id?: string;
- emoji_name: string;
- channel_id: string;
- }[];
- };
- widget_channel_id?: string;
- widget_enabled?: boolean;
-}
-
-export const GuildSchema = new Schema({
- id: { type: String, required: true },
- afk_channel_id: String,
- afk_timeout: Number,
- application_id: String,
- banner: String,
- default_message_notifications: Number,
- description: String,
- discovery_splash: String,
- explicit_content_filter: Number,
- features: { type: [String], default: [] },
- icon: String,
- large: Boolean,
- max_members: { type: Number, default: 100000 },
- max_presences: Number,
- max_video_channel_users: { type: Number, default: 25 },
- member_count: Number,
- presences: { type: [Object], default: [] },
- presence_count: Number,
- mfa_level: Number,
- name: { type: String, required: true },
- owner_id: { type: String, required: true },
- preferred_locale: String,
- premium_subscription_count: Number,
- premium_tier: Number,
- public_updates_channel_id: String,
- region: String,
- rules_channel_id: String,
- splash: String,
- system_channel_flags: Number,
- system_channel_id: String,
- unavailable: Boolean,
- vanity_url_code: String,
- verification_level: Number,
- voice_states: { type: [Object], default: [] },
- welcome_screen: {
- enabled: Boolean,
- description: String,
- welcome_channels: [
- {
- description: String,
- emoji_id: String,
- emoji_name: String,
- channel_id: String,
- },
- ],
- },
- widget_channel_id: String,
- widget_enabled: Boolean,
-});
-
-GuildSchema.virtual("channels", {
- ref: ChannelModel,
- localField: "id",
- foreignField: "guild_id",
- justOne: false,
- autopopulate: true,
-});
-
-GuildSchema.virtual("roles", {
- ref: RoleModel,
- localField: "id",
- foreignField: "guild_id",
- justOne: false,
- autopopulate: true,
-});
-
-// nested populate is needed for member users: https://gist.github.com/yangsu/5312204
-GuildSchema.virtual("members", {
- ref: MemberModel,
- localField: "id",
- foreignField: "guild_id",
- justOne: false,
-});
-
-GuildSchema.virtual("emojis", {
- ref: EmojiModel,
- localField: "id",
- foreignField: "guild_id",
- justOne: false,
- autopopulate: true,
-});
-
-GuildSchema.virtual("joined_at", {
- ref: MemberModel,
- localField: "id",
- foreignField: "guild_id",
- justOne: true,
-}).get((member: any, virtual: any, doc: any) => {
- return member?.joined_at;
-});
-
-// @ts-ignore
-export const GuildModel = db.model<GuildDocument>("Guild", GuildSchema, "guilds");
diff --git a/util/src/models/Invite.ts b/util/src/models/Invite.ts
deleted file mode 100644
index 01f12003..00000000
--- a/util/src/models/Invite.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { Schema, Document, Types } from "mongoose";
-import db from "../util/Database";
-import { ChannelModel } from "./Channel";
-import { PublicUserProjection, UserModel } from "./User";
-import { GuildModel } from "./Guild";
-
-export interface Invite {
- code: string;
- temporary: boolean;
- uses: number;
- max_uses: number;
- max_age: number;
- created_at: Date;
- expires_at: Date;
- guild_id: string;
- channel_id: string;
- inviter_id: string;
-
- // ? What is this?
- target_user_id?: string;
- target_user_type?: number;
-}
-
-export interface InviteDocument extends Invite, Document {}
-
-export const InviteSchema = new Schema({
- code: String,
- temporary: Boolean,
- uses: Number,
- max_uses: Number,
- max_age: Number,
- created_at: Date,
- expires_at: Date,
- guild_id: String,
- channel_id: String,
- inviter_id: String,
-
- // ? What is this?
- target_user_id: String,
- target_user_type: Number,
-});
-
-InviteSchema.virtual("channel", {
- ref: ChannelModel,
- localField: "channel_id",
- foreignField: "id",
- justOne: true,
- autopopulate: {
- select: {
- id: true,
- name: true,
- type: true,
- },
- },
-});
-
-InviteSchema.virtual("inviter", {
- ref: UserModel,
- localField: "inviter_id",
- foreignField: "id",
- justOne: true,
- autopopulate: {
- select: PublicUserProjection,
- },
-});
-
-InviteSchema.virtual("guild", {
- ref: GuildModel,
- localField: "guild_id",
- foreignField: "id",
- justOne: true,
- autopopulate: {
- select: {
- id: true,
- name: true,
- splash: true,
- banner: true,
- description: true,
- icon: true,
- features: true,
- verification_level: true,
- vanity_url_code: true,
- welcome_screen: true,
- nsfw: true,
-
- // TODO: hide the following entries:
- // channels: false,
- // roles: false,
- // emojis: false,
- },
- },
-});
-
-// @ts-ignore
-export const InviteModel = db.model<InviteDocument>("Invite", InviteSchema, "invites");
diff --git a/util/src/models/Member.ts b/util/src/models/Member.ts
deleted file mode 100644
index d1c9ad9b..00000000
--- a/util/src/models/Member.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { PublicUser, PublicUserProjection, User, UserModel } from "./User";
-import { Schema, Types, Document } from "mongoose";
-import db from "../util/Database";
-
-export const PublicMemberProjection = {
- id: true,
- guild_id: true,
- nick: true,
- roles: true,
- joined_at: true,
- pending: true,
- deaf: true,
- mute: true,
- premium_since: true,
-};
-
-export interface Member {
- id: string;
- guild_id: string;
- nick?: string;
- roles: string[];
- joined_at: Date;
- premium_since?: number;
- deaf: boolean;
- mute: boolean;
- pending: boolean;
- settings: UserGuildSettings;
- read_state: Record<string, string | null>;
- // virtual
- user?: User;
-}
-
-export interface MemberDocument extends Member, Document {
- id: string;
-}
-
-export interface UserGuildSettings {
- channel_overrides: {
- channel_id: string;
- message_notifications: number;
- mute_config: MuteConfig;
- muted: boolean;
- }[];
- message_notifications: number;
- mobile_push: boolean;
- mute_config: MuteConfig;
- muted: boolean;
- suppress_everyone: boolean;
- suppress_roles: boolean;
- version: number;
-}
-
-export interface MuteConfig {
- end_time: number;
- selected_time_window: number;
-}
-
-const MuteConfig = {
- end_time: Number,
- selected_time_window: Number,
-};
-
-export const MemberSchema = new Schema({
- id: { type: String, required: true },
- guild_id: String,
- nick: String,
- roles: [String],
- joined_at: Date,
- premium_since: Number,
- deaf: Boolean,
- mute: Boolean,
- pending: Boolean,
- read_state: Object,
- settings: {
- channel_overrides: [
- {
- channel_id: String,
- message_notifications: Number,
- mute_config: MuteConfig,
- muted: Boolean,
- },
- ],
- message_notifications: Number,
- mobile_push: Boolean,
- mute_config: MuteConfig,
- muted: Boolean,
- suppress_everyone: Boolean,
- suppress_roles: Boolean,
- version: Number,
- },
-});
-
-MemberSchema.virtual("user", {
- ref: UserModel,
- localField: "id",
- foreignField: "id",
- justOne: true,
- autopopulate: {
- select: PublicUserProjection,
- },
-});
-
-// @ts-ignore
-export const MemberModel = db.model<MemberDocument>("Member", MemberSchema, "members");
-
-// @ts-ignore
-export interface PublicMember extends Omit<Member, "settings" | "id" | "read_state"> {
- user: PublicUser;
-}
diff --git a/util/src/models/Message.ts b/util/src/models/Message.ts
deleted file mode 100644
index 15a6f40d..00000000
--- a/util/src/models/Message.ts
+++ /dev/null
@@ -1,368 +0,0 @@
-import { Schema, Types, Document } from "mongoose";
-import db from "../util/Database";
-import { PublicUser, PublicUserProjection, UserModel } from "./User";
-import { MemberModel, PublicMember } from "./Member";
-import { Role, RoleModel } from "./Role";
-import { Channel } from "./Channel";
-import { Snowflake } from "../util";
-import { InteractionType } from "./Interaction";
-
-export interface Message {
- id: string;
- channel_id: string;
- guild_id?: string;
- author_id?: string;
- webhook_id?: string;
- application_id?: string;
- content?: string;
- timestamp: Date;
- edited_timestamp: Date | null;
- tts?: boolean;
- mention_everyone?: boolean;
- mention_user_ids: string[];
- mention_role_ids: string[];
- mention_channels_ids: string[];
- attachments: Attachment[];
- embeds: Embed[];
- reactions: Reaction[];
- nonce?: string | number;
- pinned?: boolean;
- type: MessageType;
- activity?: {
- type: number;
- party_id: string;
- };
- flags?: bigint;
- stickers?: any[];
- message_reference?: {
- message_id: string;
- channel_id?: string;
- guild_id?: string;
- };
- interaction?: {
- id: string;
- type: InteractionType;
- name: string;
- user_id: string; // the user who invoked the interaction
- // user: User; // TODO: autopopulate user
- };
- components: MessageComponent[];
-
- // * mongoose virtuals:
- // TODO:
- // application: Application; // TODO: auto pouplate application
- author?: PublicUser;
- member?: PublicMember;
- mentions?: (PublicUser & {
- member: PublicMember;
- })[];
- mention_roles?: Role[];
- mention_channels?: Channel[];
- created_at?: Date;
- // thread // TODO
-}
-
-const PartialEmoji = {
- id: String,
- name: { type: String, required: true },
- animated: { type: Boolean, required: true },
-};
-
-const MessageComponent: any = {
- type: { type: Number, required: true },
- style: Number,
- label: String,
- emoji: PartialEmoji,
- custom_id: String,
- url: String,
- disabled: Boolean,
- components: [Object],
-};
-
-export interface MessageComponent {
- type: number;
- style?: number;
- label?: string;
- emoji?: PartialEmoji;
- custom_id?: string;
- url?: string;
- disabled?: boolean;
- components: MessageComponent[];
-}
-
-export enum MessageComponentType {
- ActionRow = 1,
- Button = 2,
-}
-
-export interface MessageDocument extends Document, Message {
- id: string;
-}
-
-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,
- REPLY = 19,
- APPLICATION_COMMAND = 20,
-}
-
-export interface Attachment {
- id: string; // attachment id
- filename: string; // name of file attached
- size: number; // size of file in bytes
- url: string; // source url of file
- proxy_url: string; // a proxied url of file
- height?: number; // height of file (if image)
- width?: number; // width of file (if image)
- content_type?: string;
-}
-
-export interface Embed {
- title?: string; //title of embed
- type?: EmbedType; // type of embed (always "rich" for webhook embeds)
- description?: string; // description of embed
- url?: string; // url of embed
- timestamp?: Date; // timestamp of embed content
- color?: number; // color code of the embed
- footer?: {
- text: string;
- icon_url?: string;
- proxy_icon_url?: string;
- }; // footer object footer information
- image?: EmbedImage; // image object image information
- thumbnail?: EmbedImage; // thumbnail object thumbnail information
- video?: EmbedImage; // video object video information
- provider?: {
- name?: string;
- url?: string;
- }; // provider object provider information
- author?: {
- name?: string;
- url?: string;
- icon_url?: string;
- proxy_icon_url?: string;
- }; // author object author information
- fields?: {
- name: string;
- value: string;
- inline?: boolean;
- }[];
-}
-
-export enum EmbedType {
- rich = "rich",
- image = "image",
- video = "video",
- gifv = "gifv",
- article = "article",
- link = "link",
-}
-
-export interface EmbedImage {
- url?: string;
- proxy_url?: string;
- height?: number;
- width?: number;
-}
-
-export interface Reaction {
- count: number;
- //// not saved in the database // me: boolean; // whether the current user reacted using this emoji
- emoji: PartialEmoji;
- user_ids: string[];
-}
-
-export interface PartialEmoji {
- id?: string;
- name: string;
- animated?: boolean;
-}
-
-export interface AllowedMentions {
- parse?: ("users" | "roles" | "everyone")[];
- roles?: string[];
- users?: string[];
- replied_user?: boolean;
-}
-
-export const Attachment = {
- id: String, // attachment id
- filename: String, // name of file attached
- size: Number, // size of file in bytes
- url: String, // source url of file
- proxy_url: String, // a proxied url of file
- height: Number, // height of file (if image)
- width: Number, // width of file (if image)
- content_type: String,
-};
-
-export const EmbedImage = {
- url: String,
- proxy_url: String,
- height: Number,
- width: Number,
-};
-
-const Reaction = {
- count: Number,
- user_ids: [String],
- emoji: {
- id: String,
- name: String,
- animated: Boolean,
- },
-};
-
-export const Embed = {
- title: String, //title of embed
- type: { type: String }, // type of embed (always "rich" for webhook embeds)
- description: String, // description of embed
- url: String, // url of embed
- timestamp: Date, // timestamp of embed content
- color: Number, // color code of the embed
- footer: {
- text: String,
- icon_url: String,
- proxy_icon_url: String,
- }, // footer object footer information
- image: EmbedImage, // image object image information
- thumbnail: EmbedImage, // thumbnail object thumbnail information
- video: EmbedImage, // video object video information
- provider: {
- name: String,
- url: String,
- }, // provider object provider information
- author: {
- name: String,
- url: String,
- icon_url: String,
- proxy_icon_url: String,
- }, // author object author information
- fields: [
- {
- name: String,
- value: String,
- inline: Boolean,
- },
- ],
-};
-
-export const MessageSchema = new Schema({
- id: String,
- channel_id: String,
- author_id: String,
- webhook_id: String,
- guild_id: String,
- application_id: String,
- content: String,
- timestamp: Date,
- edited_timestamp: Date,
- tts: Boolean,
- mention_everyone: Boolean,
- mention_user_ids: [String],
- mention_role_ids: [String],
- mention_channel_ids: [String],
- attachments: [Attachment],
- embeds: [Embed],
- reactions: [Reaction],
- nonce: Schema.Types.Mixed, // can be a long or a string
- pinned: Boolean,
- type: { type: Number },
- activity: {
- type: { type: Number },
- party_id: String,
- },
- flags: Types.Long,
- stickers: [],
- message_reference: {
- message_id: String,
- channel_id: String,
- guild_id: String,
- },
- components: [MessageComponent],
- // virtual:
- // author: {
- // ref: UserModel,
- // localField: "author_id",
- // foreignField: "id",
- // justOne: true,
- // autopopulate: { select: { id: true, user_data: false } },
- // },
-});
-
-MessageSchema.virtual("author", {
- ref: UserModel,
- localField: "author_id",
- foreignField: "id",
- justOne: true,
- autopopulate: { select: PublicUserProjection },
-});
-
-MessageSchema.virtual("member", {
- ref: MemberModel,
- localField: "author_id",
- foreignField: "id",
- justOne: true,
-});
-
-MessageSchema.virtual("mentions", {
- ref: UserModel,
- localField: "mention_user_ids",
- foreignField: "id",
- justOne: false,
- autopopulate: { select: PublicUserProjection },
-});
-
-MessageSchema.virtual("mention_roles", {
- ref: RoleModel,
- localField: "mention_role_ids",
- foreignField: "id",
- justOne: false,
- autopopulate: true,
-});
-
-MessageSchema.virtual("mention_channels", {
- ref: RoleModel,
- localField: "mention_channel_ids",
- foreignField: "id",
- justOne: false,
- autopopulate: { select: { id: true, guild_id: true, type: true, name: true } },
-});
-
-MessageSchema.virtual("referenced_message", {
- ref: "Message",
- localField: "message_reference.message_id",
- foreignField: "id",
- justOne: true,
- autopopulate: true,
-});
-
-MessageSchema.virtual("created_at").get(function (this: MessageDocument) {
- return new Date(Snowflake.deconstruct(this.id).timestamp);
-});
-
-MessageSchema.set("removeResponse", ["mention_channel_ids", "mention_role_ids", "mention_user_ids", "author_id"]);
-
-// TODO: missing Application Model
-// MessageSchema.virtual("application", {
-// ref: Application,
-// localField: "mention_role_ids",
-// foreignField: "id",
-// justOne: true,
-// });
-
-// @ts-ignore
-export const MessageModel = db.model<MessageDocument>("Message", MessageSchema, "messages");
diff --git a/util/src/models/RateLimit.ts b/util/src/models/RateLimit.ts
deleted file mode 100644
index 6a0e1ffd..00000000
--- a/util/src/models/RateLimit.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Schema, Document, Types } from "mongoose";
-import db from "../util/Database";
-
-export interface Bucket {
- id: "global" | "error" | string; // channel_239842397 | guild_238927349823 | webhook_238923423498
- user_id: string;
- hits: number;
- blocked: boolean;
- expires_at: Date;
-}
-
-export interface BucketDocument extends Bucket, Document {
- id: string;
-}
-
-export const BucketSchema = new Schema({
- id: { type: String, required: true },
- user_id: { type: String, required: true }, // bot, user, oauth_application, webhook
- hits: { type: Number, required: true }, // Number of times the user hit this bucket
- blocked: { type: Boolean, required: true },
- expires_at: { type: Date, required: true },
-});
-
-// @ts-ignore
-export const BucketModel = db.model<BucketDocument>("Bucket", BucketSchema, "ratelimits");
diff --git a/util/src/models/ReadState.ts b/util/src/models/ReadState.ts
deleted file mode 100644
index 9c4fb323..00000000
--- a/util/src/models/ReadState.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { PublicMember } from "./Member";
-import { Schema, model, Types, Document } from "mongoose";
-import db from "../util/Database";
-
-export interface ReadState extends Document {
- message_id: string;
- channel_id: string;
- user_id: string;
- last_message_id?: string;
- last_pin_timestamp?: Date;
- mention_count: number;
- manual: boolean;
-}
-
-export const ReadStateSchema = new Schema({
- message_id: String,
- channel_id: String,
- user_id: String,
- last_message_id: String,
- last_pin_timestamp: Date,
- mention_count: Number,
- manual: Boolean,
-});
-
-// @ts-ignore
-export const ReadStateModel = db.model<ReadState>("ReadState", ReadStateSchema, "readstates");
diff --git a/util/src/models/Role.ts b/util/src/models/Role.ts
deleted file mode 100644
index c1111c84..00000000
--- a/util/src/models/Role.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { Schema, model, Types, Document } from "mongoose";
-import db from "../util/Database";
-import toBigInt from "../util/toBigInt";
-
-export interface Role {
- id: string;
- guild_id: string;
- color: number;
- hoist: boolean;
- managed: boolean;
- mentionable: boolean;
- name: string;
- permissions: bigint;
- position: number;
- tags?: {
- bot_id?: string;
- };
-}
-
-export interface RoleDocument extends Document, Role {
- id: string;
-}
-
-export const RoleSchema = new Schema({
- id: String,
- guild_id: String,
- color: Number,
- hoist: Boolean,
- managed: Boolean,
- mentionable: Boolean,
- name: String,
- permissions: { type: String, get: toBigInt },
- position: Number,
- tags: {
- bot_id: String,
- },
-});
-
-RoleSchema.set("removeResponse", ["guild_id"]);
-
-// @ts-ignore
-export const RoleModel = db.model<RoleDocument>("Role", RoleSchema, "roles");
diff --git a/util/src/models/Team.ts b/util/src/models/Team.ts
deleted file mode 100644
index 795c82d2..00000000
--- a/util/src/models/Team.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-export interface Team {
- icon: string | null;
- id: string;
- members: {
- membership_state: number;
- permissions: string[];
- team_id: string;
- user_id: string;
- }[];
- name: string;
- owner_user_id: string;
-}
-
-export enum TeamMemberState {
- INVITED = 1,
- ACCEPTED = 2,
-}
diff --git a/util/src/models/Template.ts b/util/src/models/Template.ts
deleted file mode 100644
index ad0f9104..00000000
--- a/util/src/models/Template.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { Schema, model, Types, Document } from "mongoose";
-import db from "../util/Database";
-import { PublicUser, User, UserModel, PublicUserProjection } from "./User";
-import { Guild, GuildModel } from "./Guild";
-
-export interface Template extends Document {
- id: string;
- code: string;
- name: string;
- description?: string;
- usage_count?: number;
- creator_id: string;
- creator: User;
- created_at: Date;
- updated_at: Date;
- source_guild_id: String;
- serialized_source_guild: Guild;
-}
-
-export const TemplateSchema = new Schema({
- id: String,
- code: String,
- name: String,
- description: String,
- usage_count: Number,
- creator_id: String,
- created_at: Date,
- updated_at: Date,
- source_guild_id: String,
-});
-
-TemplateSchema.virtual("creator", {
- ref: UserModel,
- localField: "creator_id",
- foreignField: "id",
- justOne: true,
- autopopulate: {
- select: PublicUserProjection,
- },
-});
-
-TemplateSchema.virtual("serialized_source_guild", {
- ref: GuildModel,
- localField: "source_guild_id",
- foreignField: "id",
- justOne: true,
- autopopulate: true,
-});
-
-// @ts-ignore
-export const TemplateModel = db.model<Template>("Template", TemplateSchema, "templates");
diff --git a/util/src/models/User.ts b/util/src/models/User.ts
deleted file mode 100644
index c667e954..00000000
--- a/util/src/models/User.ts
+++ /dev/null
@@ -1,252 +0,0 @@
-import { Activity, ActivitySchema } from "./Activity";
-import { ClientStatus, Status } from "./Status";
-import { Schema, Types, Document } from "mongoose";
-import db from "../util/Database";
-import toBigInt from "../util/toBigInt";
-
-export const PublicUserProjection = {
- username: true,
- discriminator: true,
- id: true,
- public_flags: true,
- avatar: true,
- accent_color: true,
- banner: true,
- bio: true,
- bot: true,
-};
-
-export interface User {
- id: string;
- username: string; // username max length 32, min 2
- discriminator: string; // #0001 4 digit long string from #0001 - #9999
- avatar: string | null; // hash of the user avatar
- accent_color: number | null; // banner color of user
- banner: string | null;
- phone: string | null; // phone number of the user
- desktop: boolean; // if the user has desktop app installed
- mobile: boolean; // if the user has mobile app installed
- premium: boolean; // if user bought nitro
- premium_type: number; // nitro level
- bot: boolean; // if user is bot
- bio: string; // short description of the user (max 190 chars)
- system: boolean; // shouldn't be used, the api sents this field type true, if the genetaed message comes from a system generated author
- nsfw_allowed: boolean; // if the user is older than 18 (resp. Config)
- mfa_enabled: boolean; // if multi factor authentication is enabled
- created_at: Date; // registration date
- verified: boolean; // if the user is offically verified
- disabled: boolean; // if the account is disabled
- deleted: boolean; // if the user was deleted
- email: string | null; // email of the user
- flags: bigint; // UserFlags
- public_flags: bigint;
- user_settings: UserSettings;
- guilds: string[]; // array of guild ids the user is part of
- user_data: UserData;
- presence: {
- status: Status;
- activities: Activity[];
- client_status: ClientStatus;
- };
-}
-
-// Private user data:
-export interface UserData {
- valid_tokens_since: Date; // all tokens with a previous issue date are invalid
- relationships: Relationship[];
- connected_accounts: ConnectedAccount[];
- hash: string; // hash of the password, salt is saved in password (bcrypt)
- fingerprints: string[]; // array of fingerprints -> used to prevent multiple accounts
-}
-
-export interface UserDocument extends User, Document {
- id: string;
-}
-
-export interface PublicUser {
- id: string;
- discriminator: string;
- username: string;
- avatar: string | null;
- accent_color: number;
- banner: string | null;
- public_flags: bigint;
- 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
-}
-
-export const UserSchema = new Schema({
- id: String,
- username: String,
- discriminator: String,
- avatar: String,
- accent_color: Number,
- banner: String,
- phone: String,
- desktop: Boolean,
- mobile: Boolean,
- premium: Boolean,
- premium_type: Number,
- bot: Boolean,
- bio: String,
- system: Boolean,
- nsfw_allowed: Boolean,
- mfa_enabled: Boolean,
- created_at: Date,
- verified: Boolean,
- disabled: Boolean,
- deleted: Boolean,
- email: String,
- flags: { type: String, get: toBigInt }, // TODO: automatically convert Types.Long to BitField of UserFlags
- public_flags: { type: String, get: toBigInt },
- guilds: [String], // array of guild ids the user is part of
- user_data: {
- fingerprints: [String],
- hash: String, // hash of the password, salt is saved in password (bcrypt)
- valid_tokens_since: Date, // all tokens with a previous issue date are invalid
- relationships: [
- {
- id: { type: String, required: true },
- nickname: String,
- type: { type: Number },
- },
- ],
- connected_accounts: [
- {
- access_token: String,
- friend_sync: Boolean,
- id: String,
- name: String,
- revoked: Boolean,
- show_activity: Boolean,
- type: { type: String },
- verifie: Boolean,
- visibility: Number,
- },
- ],
- },
- 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,
- emoji_name: String,
- expires_at: Number,
- text: String,
- },
- 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,
- // every top guild is displayed as a "folder"
- guild_folders: [
- {
- 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: String,
- stream_notifications_enabled: Boolean,
- theme: String, // dark
- timezone_offset: Number, // e.g -60,
- },
-
- presence: {
- status: String,
- activities: [ActivitySchema],
- client_status: ClientStatus,
- },
-});
-
-// @ts-ignore
-export const UserModel = db.model<UserDocument>("User", UserSchema, "users");
diff --git a/util/src/models/VoiceState.ts b/util/src/models/VoiceState.ts
deleted file mode 100644
index c1f90edd..00000000
--- a/util/src/models/VoiceState.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { PublicMember } from "./Member";
-import { Schema, model, Types, Document } from "mongoose";
-import db from "../util/Database";
-
-export interface VoiceState extends Document {
- guild_id?: string;
- channel_id: string;
- user_id: string;
- session_id: string;
- deaf: boolean;
- mute: boolean;
- self_deaf: boolean;
- self_mute: boolean;
- self_stream?: boolean;
- self_video: boolean;
- suppress: boolean; // whether this user is muted by the current user
-}
-
-export const VoiceSateSchema = new Schema({
- guild_id: String,
- channel_id: String,
- user_id: String,
- session_id: String,
- deaf: Boolean,
- mute: Boolean,
- self_deaf: Boolean,
- self_mute: Boolean,
- self_stream: Boolean,
- self_video: Boolean,
- suppress: Boolean, // whether this user is muted by the current user
-});
-
-// @ts-ignore
-export const VoiceStateModel = db.model<VoiceState>("VoiceState", VoiceSateSchema, "voicestates");
diff --git a/util/src/models/Webhook.ts b/util/src/models/Webhook.ts
deleted file mode 100644
index 7379e98f..00000000
--- a/util/src/models/Webhook.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import { Schema, Document, Types } from "mongoose";
-import { transpileModule } from "typescript";
-import db from "../util/Database";
-import { ChannelModel } from "./Channel";
-import { GuildModel } from "./Guild";
-
-export interface Webhook {}
-
-export enum WebhookType {
- Incoming = 1,
- ChannelFollower = 2,
-}
-
-export interface WebhookDocument extends Document, Webhook {
- id: String;
- type: number;
- guild_id?: string;
- channel_id: string;
- name?: string;
- avatar?: string;
- token?: string;
- application_id?: string;
- user_id?: string;
- source_guild_id: string;
-}
-
-export const WebhookSchema = new Schema({
- id: { type: String, required: true },
- type: { type: Number, required: true },
- guild_id: String,
- channel_id: String,
- name: String,
- avatar: String,
- token: String,
- application_id: String,
- user_id: String,
- source_guild_id: String,
- source_channel_id: String,
-});
-
-WebhookSchema.virtual("source_guild", {
- ref: GuildModel,
- localField: "id",
- foreignField: "source_guild_id",
- justOne: true,
- autopopulate: {
- select: {
- icon: true,
- id: true,
- name: true,
- },
- },
-});
-
-WebhookSchema.virtual("source_channel", {
- ref: ChannelModel,
- localField: "id",
- foreignField: "source_channel_id",
- justOne: true,
- autopopulate: {
- select: {
- id: true,
- name: true,
- },
- },
-});
-
-WebhookSchema.virtual("source_channel", {
- ref: ChannelModel,
- localField: "id",
- foreignField: "source_channel_id",
- justOne: true,
- autopopulate: {
- select: {
- id: true,
- name: true,
- },
- },
-});
-
-WebhookSchema.set("removeResponse", ["source_channel_id", "source_guild_id"]);
-
-// @ts-ignore
-export const WebhookModel = db.model<WebhookDocument>("Webhook", WebhookSchema, "webhooks");
diff --git a/util/src/models/index.ts b/util/src/models/index.ts
deleted file mode 100644
index b6100f86..00000000
--- a/util/src/models/index.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-// @ts-nocheck
-import mongoose, { Schema, Document } from "mongoose";
-import mongooseAutoPopulate from "mongoose-autopopulate";
-
-type UpdateWithAggregationPipeline = UpdateAggregationStage[];
-type UpdateAggregationStage =
- | { $addFields: any }
- | { $set: any }
- | { $project: any }
- | { $unset: any }
- | { $replaceRoot: any }
- | { $replaceWith: any };
-type EnforceDocument<T, TMethods> = T extends Document ? T : T & Document & TMethods;
-
-declare module "mongoose" {
- interface SchemaOptions {
- removeResponse?: string[];
- }
- interface Model<T, TQueryHelpers = {}, TMethods = {}> {
- // removed null -> always return document -> throw error if it doesn't exist
- findOne(
- filter?: FilterQuery<T>,
- projection?: any | null,
- options?: QueryOptions | null,
- callback?: (err: CallbackError, doc: EnforceDocument<T, TMethods>) => void
- ): QueryWithHelpers<EnforceDocument<T, TMethods>, EnforceDocument<T, TMethods>, TQueryHelpers>;
- findOneAndUpdate(
- filter?: FilterQuery<T>,
- update?: UpdateQuery<T> | UpdateWithAggregationPipeline,
- options?: QueryOptions | null,
- callback?: (err: any, doc: EnforceDocument<T, TMethods> | null, res: any) => void
- ): QueryWithHelpers<EnforceDocument<T, TMethods>, EnforceDocument<T, TMethods>, TQueryHelpers>;
- }
-}
-
-var HTTPError: any;
-
-try {
- HTTPError = require("lambert-server").HTTPError;
-} catch (e) {
- HTTPError = Error;
-}
-
-mongoose.plugin(mongooseAutoPopulate);
-
-mongoose.plugin((schema: Schema, opts: any) => {
- schema.set("toObject", {
- virtuals: true,
- versionKey: false,
- transform(doc: any, ret: any) {
- delete ret._id;
- delete ret.__v;
- const props = schema.get("removeResponse") || [];
- props.forEach((prop: string) => {
- delete ret[prop];
- });
- },
- });
- schema.post("findOne", function (doc, next) {
- try {
- // @ts-ignore
- const isExistsQuery = JSON.stringify(this._userProvidedFields) === JSON.stringify({ _id: 1 });
- if (!doc && !isExistsQuery) {
- // @ts-ignore
- return next(new HTTPError(`${this?.mongooseCollection?.name}.${this?._conditions?.id} not found`, 400));
- }
- // @ts-ignore
- return next();
- } catch (error) {
- // @ts-ignore
- next();
- }
- });
-});
-
-export * from "./Activity";
-export * from "./Application";
-export * from "./Ban";
-export * from "./Channel";
-export * from "./Emoji";
-export * from "./Event";
-export * from "./Template";
-export * from "./Guild";
-export * from "./Invite";
-export * from "./Interaction";
-export * from "./Member";
-export * from "./Message";
-export * from "./Status";
-export * from "./Role";
-export * from "./User";
-export * from "./VoiceState";
-export * from "./ReadState";
-export * from "./RateLimit";
diff --git a/util/src/tes.ts b/util/src/tes.ts
new file mode 100644
index 00000000..e326dee1
--- /dev/null
+++ b/util/src/tes.ts
@@ -0,0 +1,23 @@
+import { performance } from "perf_hooks";
+import { Guild, Relationship, RelationshipType } from "./entities";
+import { User } from "./entities/User";
+import { initDatabase } from "./util";
+
+initDatabase().then(async (x) => {
+ try {
+ const user = await new User({
+ guilds: [],
+ discriminator: "1",
+ username: "test",
+ flags: "0",
+ public_flags: "0",
+ id: "0",
+ }).save();
+
+ user.relationships = [new Relationship({ type: RelationshipType.friends })];
+
+ user.save();
+ } catch (error) {
+ console.error(error);
+ }
+});
diff --git a/util/src/util/AutoUpdate.ts b/util/src/util/AutoUpdate.ts
index a2ce73c2..cafc7bdb 100644
--- a/util/src/util/AutoUpdate.ts
+++ b/util/src/util/AutoUpdate.ts
@@ -40,6 +40,7 @@ export function enableAutoUpdate(opts: {
console.log(`[Auto update] updating ...`);
download(opts.downloadUrl, opts.path);
} else {
+ console.log(`[Auto update] aborted`);
}
}
);
diff --git a/util/src/util/BitField.ts b/util/src/util/BitField.ts
index 728dc632..ac19763e 100644
--- a/util/src/util/BitField.ts
+++ b/util/src/util/BitField.ts
@@ -21,7 +21,7 @@ export class BitField {
* 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;
+ return (this.bitfield & BitField.resolve.call(this, bit)) !== BigInt(0);
}
/**
@@ -61,7 +61,7 @@ export class BitField {
* @returns {BitField} These bits or new BitField if the instance is frozen.
*/
add(...bits: BitFieldResolvable[]): BitField {
- let total = 0n;
+ let total = BigInt(0);
for (const bit of bits) {
total |= BitField.resolve.call(this, bit);
}
@@ -75,7 +75,7 @@ export class BitField {
* @param {...BitFieldResolvable} [bits] Bits to remove
*/
remove(...bits: BitFieldResolvable[]) {
- let total = 0n;
+ let total = BigInt(0);
for (const bit of bits) {
total |= BitField.resolve.call(this, bit);
}
@@ -127,15 +127,15 @@ export class BitField {
* @param {BitFieldResolvable} [bit=0] - bit(s) to resolve
* @returns {number}
*/
- static resolve(bit: BitFieldResolvable = 0n): bigint {
+ static resolve(bit: BitFieldResolvable = BigInt(0)): bigint {
// @ts-ignore
const FLAGS = this.FLAGS || this.constructor?.FLAGS;
- if ((typeof bit === "number" || typeof bit === "bigint") && bit >= 0n) return BigInt(bit);
+ if ((typeof bit === "number" || typeof bit === "bigint") && bit >= BigInt(0)) 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);
+ return bit.map((p) => resolve.call(this, p)).reduce((prev, p) => BigInt(prev) | BigInt(p), BigInt(0));
}
if (typeof bit === "string" && typeof FLAGS[bit] !== "undefined") return FLAGS[bit];
throw new RangeError("BITFIELD_INVALID: " + bit);
diff --git a/util/src/util/Config.ts b/util/src/util/Config.ts
index a93e1846..1ec71ad0 100644
--- a/util/src/util/Config.ts
+++ b/util/src/util/Config.ts
@@ -1,291 +1,22 @@
-import { Schema, model, Types, Document } from "mongoose";
import "missing-native-js-functions";
-import db from "./Database";
-import { Snowflake } from "./Snowflake";
-import crypto from "crypto";
+import { ConfigValue, ConfigEntity, DefaultConfigOptions } from "../entities/Config";
-var config: any;
+var config: ConfigEntity;
+// TODO: use events to inform about config updates
-export default {
- init: async function init(defaultOpts: any = DefaultOptions) {
- config = await db.collection("config").findOne({});
- return this.set((config || {}).merge(defaultOpts));
+export const Config = {
+ init: async function init() {
+ if (config) return config;
+ config = (await ConfigEntity.findOne({ id: "0" })) || new ConfigEntity({ id: "0" });
+
+ return this.set((config.value || {}).merge(DefaultConfigOptions));
},
get: function get() {
- return config as DefaultOptions;
+ return config.value as ConfigValue;
},
set: function set(val: any) {
- config = val.merge(config);
- return db.collection("config").updateOne({}, { $set: val }, { upsert: true });
+ if (!config) return;
+ config.value = val.merge(config?.value || {});
+ return config.save();
},
};
-
-export interface RateLimitOptions {
- bot?: number;
- count: number;
- window: number;
- onyIp?: boolean;
-}
-
-export interface Region {
- id: string;
- name: string;
- vip: boolean;
- custom: boolean;
- deprecated: boolean;
- optimal: boolean;
-}
-
-export interface KafkaBroker {
- ip: string;
- port: number;
-}
-
-export interface DefaultOptions {
- gateway: {
- endpointClient: string | null;
- endpoint: string | null;
- };
- cdn: {
- endpointClient: string | null;
- endpoint: string | null;
- };
- general: {
- instance_id: string;
- };
- permissions: {
- user: {
- createGuilds: boolean;
- };
- };
- limits: {
- user: {
- maxGuilds: number;
- maxUsername: number;
- maxFriends: number;
- };
- guild: {
- maxRoles: number;
- maxMembers: number;
- maxChannels: number;
- maxChannelsInCategory: number;
- hideOfflineMember: number;
- };
- message: {
- maxCharacters: number;
- maxTTSCharacters: number;
- maxReactions: number;
- maxAttachmentSize: number;
- maxBulkDelete: number;
- };
- channel: {
- maxPins: number;
- maxTopic: number;
- };
- rate: {
- ip: Omit<RateLimitOptions, "bot_count">;
- global: RateLimitOptions;
- error: RateLimitOptions;
- routes: {
- guild: RateLimitOptions;
- webhook: RateLimitOptions;
- channel: RateLimitOptions;
- auth: {
- login: RateLimitOptions;
- register: RateLimitOptions;
- };
- // TODO: rate limit configuration for all routes
- };
- };
- };
- security: {
- autoUpdate: boolean | number;
- requestSignature: string;
- jwtSecret: string;
- forwadedFor: string | null; // header to get the real user ip address
- captcha: {
- enabled: boolean;
- service: "recaptcha" | "hcaptcha" | null; // TODO: hcaptcha, custom
- sitekey: string | null;
- secret: string | null;
- };
- ipdataApiKey: string | null;
- };
- login: {
- requireCaptcha: boolean;
- };
- register: {
- email: {
- necessary: boolean; // we have to use necessary instead of required as the cli tool uses json schema and can't use required
- allowlist: boolean;
- blocklist: boolean;
- domains: string[];
- };
- dateOfBirth: {
- necessary: boolean;
- minimum: number; // in years
- };
- requireCaptcha: boolean;
- requireInvite: boolean;
- allowNewRegistration: boolean;
- allowMultipleAccounts: boolean;
- blockProxies: boolean;
- password: {
- minLength: number;
- minNumbers: number;
- minUpperCase: number;
- minSymbols: number;
- };
- };
- regions: {
- default: string;
- available: Region[];
- };
- rabbitmq: {
- host: string | null;
- };
- kafka: {
- brokers: KafkaBroker[] | null;
- };
-}
-
-export const DefaultOptions: DefaultOptions = {
- gateway: {
- endpointClient: null,
- endpoint: null,
- },
- cdn: {
- endpointClient: null,
- endpoint: null,
- },
- general: {
- instance_id: Snowflake.generate(),
- },
- permissions: {
- user: {
- createGuilds: true,
- },
- },
- limits: {
- user: {
- maxGuilds: 100,
- maxUsername: 32,
- maxFriends: 1000,
- },
- guild: {
- maxRoles: 250,
- maxMembers: 250000,
- maxChannels: 500,
- maxChannelsInCategory: 50,
- hideOfflineMember: 1000,
- },
- message: {
- maxCharacters: 2000,
- maxTTSCharacters: 200,
- maxReactions: 20,
- maxAttachmentSize: 8388608,
- maxBulkDelete: 100,
- },
- channel: {
- maxPins: 50,
- maxTopic: 1024,
- },
- rate: {
- ip: {
- count: 500,
- window: 5,
- },
- global: {
- count: 20,
- window: 5,
- bot: 250,
- },
- error: {
- count: 10,
- window: 5,
- },
- routes: {
- guild: {
- count: 5,
- window: 5,
- },
- webhook: {
- count: 5,
- window: 20,
- },
- channel: {
- count: 5,
- window: 20,
- },
- auth: {
- login: {
- count: 5,
- window: 60,
- },
- register: {
- count: 2,
- window: 60 * 60 * 12,
- },
- },
- },
- },
- },
- security: {
- autoUpdate: true,
- requestSignature: crypto.randomBytes(32).toString("base64"),
- jwtSecret: crypto.randomBytes(256).toString("base64"),
- forwadedFor: null,
- // forwadedFor: "X-Forwarded-For" // nginx/reverse proxy
- // forwadedFor: "CF-Connecting-IP" // cloudflare:
- captcha: {
- enabled: false,
- service: null,
- sitekey: null,
- secret: null,
- },
- ipdataApiKey: "eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9",
- },
- login: {
- requireCaptcha: false,
- },
- register: {
- email: {
- necessary: true,
- allowlist: false,
- blocklist: true,
- domains: [], // TODO: efficiently save domain blocklist in database
- // domains: fs.readFileSync(__dirname + "/blockedEmailDomains.txt", { encoding: "utf8" }).split("\n"),
- },
- dateOfBirth: {
- necessary: true,
- minimum: 13,
- },
- requireInvite: false,
- requireCaptcha: true,
- allowNewRegistration: true,
- allowMultipleAccounts: true,
- blockProxies: true,
- password: {
- minLength: 8,
- minNumbers: 2,
- minUpperCase: 2,
- minSymbols: 0,
- },
- },
- regions: {
- default: "fosscord",
- available: [{ id: "fosscord", name: "Fosscord", vip: false, custom: false, deprecated: false, optimal: false }],
- },
- rabbitmq: {
- host: null,
- },
- kafka: {
- brokers: null,
- },
-};
-
-export const ConfigSchema = new Schema({}, { strict: false });
-
-export interface DefaultOptionsDocument extends DefaultOptions, Document {}
-
-export const ConfigModel = model<DefaultOptionsDocument>("Config", ConfigSchema, "config");
diff --git a/util/src/util/Constants.ts b/util/src/util/Constants.ts
deleted file mode 100644
index a9978c51..00000000
--- a/util/src/util/Constants.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { VerifyOptions } from "jsonwebtoken";
-
-export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] };
-
-export enum MessageType {
- DEFAULT = 0,
- RECIPIENT_ADD = 1,
- RECIPIENT_REMOVE = 2,
- CALL = 3,
- CHANNEL_NAME_CHANGE = 4,
- CHANNEL_ICON_CHANGE = 5,
- CHANNEL_PINNED_MESSAGE = 6,
- GUILD_MEMBER_JOIN = 7,
- USER_PREMIUM_GUILD_SUBSCRIPTION = 8,
- USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 = 9,
- USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 = 10,
- USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 = 11,
- CHANNEL_FOLLOW_ADD = 12,
- GUILD_DISCOVERY_DISQUALIFIED = 14,
- GUILD_DISCOVERY_REQUALIFIED = 15,
- GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING = 16,
- GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING = 17,
- THREAD_CREATED = 18,
- REPLY = 19,
- APPLICATION_COMMAND = 20,
- THREAD_STARTER_MESSAGE = 21,
- GUILD_INVITE_REMINDER = 22,
-}
diff --git a/util/src/util/Database.ts b/util/src/util/Database.ts
index ea517234..c22d8abd 100644
--- a/util/src/util/Database.ts
+++ b/util/src/util/Database.ts
@@ -1,159 +1,44 @@
-// @ts-nocheck
-import "./MongoBigInt";
-import mongoose, { Collection, Connection, LeanDocument } from "mongoose";
-import { ChangeStream, ChangeEvent, Long } from "mongodb";
-import EventEmitter from "events";
-const uri = process.env.MONGO_URL || "mongodb://localhost:27017/fosscord?readPreference=secondaryPreferred";
-import { URL } from "url";
+import "reflect-metadata";
+import { Connection, createConnection, ValueTransformer } from "typeorm";
+import * as Models from "../entities";
-const url = new URL(uri.replace("mongodb://", "http://"));
+// UUID extension option is only supported with postgres
+// We want to generate all id's with Snowflakes that's why we have our own BaseEntity class
-const connection = mongoose.createConnection(uri, {
- autoIndex: true,
- useNewUrlParser: true,
- useUnifiedTopology: true,
- useFindAndModify: true,
-});
+var promise: Promise<any>;
+var dbConnection: Connection | undefined;
-// this will return the new updated document for findOneAndUpdate
-mongoose.set("returnOriginal", false); // https://mongoosejs.com/docs/api/model.html#model_Model.findOneAndUpdate
+export function initDatabase() {
+ if (promise) return promise; // prevent initalizing multiple times
-console.log(`[Database] connect: mongodb://${url.username}@${url.host}${url.pathname}${url.search}`);
-connection.once("open", () => {
- console.log("[Database] connected");
-});
-
-export default <Connection>connection;
-
-function transform<T>(document: T) {
- // @ts-ignore
- if (!document || !document.toObject) {
- try {
- // @ts-ignore
- delete document._id;
- // @ts-ignore
- delete document.__v;
- } catch (error) {}
- return document;
- }
- // @ts-ignore
- return document.toObject({ virtuals: true });
-}
-
-export function toObject<T>(document: T): LeanDocument<T> {
+ console.log("[Database] connecting ...");
// @ts-ignore
- return Array.isArray(document) ? document.map((x) => transform<T>(x)) : transform(document);
-}
-
-export interface MongooseCache {
- on(event: "delete", listener: (id: string) => void): this;
- on(event: "change", listener: (data: any) => void): this;
- on(event: "insert", listener: (data: any) => void): this;
- on(event: "close", listener: () => void): this;
+ promise = createConnection({
+ type: "sqlite",
+ database: "database.db",
+ // type: "postgres",
+ // url: "postgres://fosscord:wb94SmuURM2Syv&@localhost/fosscord",
+ //
+ entities: Object.values(Models).filter((x) => x.constructor.name !== "Object"),
+ synchronize: true,
+ logging: true,
+ cache: {
+ duration: 1000 * 3, // cache all find queries for 3 seconds
+ },
+ bigNumberStrings: false,
+ supportBigNumbers: true,
+ });
+
+ promise.then((connection) => {
+ dbConnection = connection;
+ console.log("[Database] connected");
+ });
+
+ return promise;
}
-export class MongooseCache extends EventEmitter {
- public stream: ChangeStream;
- public data: any;
- public initalizing?: Promise<void>;
-
- constructor(
- public collection: Collection,
- public pipeline: Array<Record<string, unknown>>,
- public opts: {
- onlyEvents: boolean;
- array?: boolean;
- }
- ) {
- super();
- if (this.opts.array == null) this.opts.array = true;
- }
-
- init = () => {
- if (this.initalizing) return this.initalizing;
- this.initalizing = new Promise(async (resolve, reject) => {
- // @ts-ignore
- this.stream = this.collection.watch(this.pipeline, { fullDocument: "updateLookup" });
-
- this.stream.on("change", this.change);
- this.stream.on("close", this.destroy);
- this.stream.on("error", console.error);
-
- if (!this.opts.onlyEvents) {
- const arr = await this.collection.aggregate(this.pipeline).toArray();
- if (this.opts.array) this.data = arr || [];
- else this.data = arr?.[0];
- }
- resolve();
- });
- return this.initalizing;
- };
-
- changeStream = (pipeline: any) => {
- this.pipeline = pipeline;
- this.destroy();
- this.init();
- };
-
- convertResult = (obj: any) => {
- if (obj instanceof Long) return BigInt(obj.toString());
- if (typeof obj === "object") {
- Object.keys(obj).forEach((key) => {
- obj[key] = this.convertResult(obj[key]);
- });
- }
-
- return obj;
- };
-
- change = (doc: ChangeEvent) => {
- try {
- switch (doc.operationType) {
- case "dropDatabase":
- return this.destroy();
- case "drop":
- return this.destroy();
- case "delete":
- if (!this.opts.onlyEvents) {
- if (this.opts.array) {
- this.data = this.data.filter((x: any) => doc.documentKey?._id?.equals(x._id));
- } else this.data = null;
- }
- return this.emit("delete", doc.documentKey._id.toHexString());
- case "insert":
- if (!this.opts.onlyEvents) {
- if (this.opts.array) this.data.push(doc.fullDocument);
- else this.data = doc.fullDocument;
- }
- return this.emit("insert", doc.fullDocument);
- case "update":
- case "replace":
- if (!this.opts.onlyEvents) {
- if (this.opts.array) {
- const i = this.data.findIndex((x: any) => doc.fullDocument?._id?.equals(x._id));
- if (i == -1) this.data.push(doc.fullDocument);
- else this.data[i] = doc.fullDocument;
- } else this.data = doc.fullDocument;
- }
-
- return this.emit("change", doc.fullDocument);
- case "invalidate":
- return this.destroy();
- default:
- return;
- }
- } catch (error) {
- this.emit("error", error);
- }
- };
-
- destroy = () => {
- this.data = null;
- this.stream?.off("change", this.change);
- this.emit("close");
-
- if (this.stream.isClosed()) return;
+export { dbConnection };
- return this.stream.close();
- };
+export function closeDatabase() {
+ dbConnection?.close();
}
diff --git a/util/src/util/Event.ts b/util/src/util/Event.ts
index 0dbddc76..765e5fc7 100644
--- a/util/src/util/Event.ts
+++ b/util/src/util/Event.ts
@@ -1,7 +1,7 @@
import { Channel } from "amqplib";
-import { EVENT, Event } from "../models";
import { RabbitMQ } from "./RabbitMQ";
import EventEmitter from "events";
+import { EVENT, Event } from "../interfaces";
const events = new EventEmitter();
export async function emitEvent(payload: Omit<Event, "created_at">) {
diff --git a/util/src/util/MongoBigInt.ts b/util/src/util/MongoBigInt.ts
deleted file mode 100644
index 2ae8b91c..00000000
--- a/util/src/util/MongoBigInt.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-// @ts-nocheck
-import mongoose from "mongoose";
-
-class LongSchema extends mongoose.SchemaType {
- public $conditionalHandlers = {
- $lt: this.handleSingle,
- $lte: this.handleSingle,
- $gt: this.handleSingle,
- $gte: this.handleSingle,
- $ne: this.handleSingle,
- $in: this.handleArray,
- $nin: this.handleArray,
- $mod: this.handleArray,
- $all: this.handleArray,
- $bitsAnySet: this.handleArray,
- $bitsAllSet: this.handleArray,
- };
-
- handleSingle(val: any) {
- return this.cast(val, null, null, "handle");
- }
-
- handleArray(val: any) {
- var self = this;
- return val.map(function (m: any) {
- return self.cast(m, null, null, "handle");
- });
- }
-
- checkRequired(val: any) {
- return null != val;
- }
-
- cast(val: any, scope?: any, init?: any, type?: string) {
- if (null === val) return val;
- if ("" === val) return null;
- if (typeof val === "bigint") {
- return mongoose.mongo.Long.fromString(val.toString());
- }
-
- if (val instanceof mongoose.mongo.Long) {
- if (type === "handle" || init == false) return val;
- return BigInt(val.toString());
- }
- if (val instanceof Number || "number" == typeof val) return BigInt(val as number);
- if (!Array.isArray(val) && val.toString) return BigInt(val.toString());
-
- //@ts-ignore
- throw new SchemaType.CastError("Long", val);
- }
-
- castForQuery($conditional: string, value: any) {
- var handler;
- if (2 === arguments.length) {
- // @ts-ignore
- handler = this.$conditionalHandlers[$conditional];
- if (!handler) {
- throw new Error("Can't use " + $conditional + " with Long.");
- }
- return handler.call(this, value);
- } else {
- return this.cast($conditional, null, null, "query");
- }
- }
-}
-
-LongSchema.cast = mongoose.SchemaType.cast;
-LongSchema.set = mongoose.SchemaType.set;
-LongSchema.get = mongoose.SchemaType.get;
-
-declare module "mongoose" {
- namespace Types {
- class Long extends mongoose.mongo.Long {}
- }
- namespace Schema {
- namespace Types {
- class Long extends LongSchema {}
- }
- }
-}
-
-mongoose.Schema.Types.Long = LongSchema;
-mongoose.Types.Long = mongoose.mongo.Long;
diff --git a/util/src/util/Permissions.ts b/util/src/util/Permissions.ts
index 63d87e48..89b316ee 100644
--- a/util/src/util/Permissions.ts
+++ b/util/src/util/Permissions.ts
@@ -1,11 +1,8 @@
// 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 { In } from "typeorm";
+import { Channel, ChannelPermissionOverwrite, Guild, Member, Role } from "../entities";
import { BitField } from "./BitField";
-import { GuildDocument, GuildModel } from "../models/Guild";
// TODO: check role hierarchy permission
var HTTPError: any;
@@ -138,12 +135,12 @@ export class Permissions extends BitField {
// ~ 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);
+ }, init || BigInt(0));
}
static rolePermission(roles: Role[]) {
// adds all permissions of all roles together (Bit OR)
- return roles.reduce((permission, role) => permission | BigInt(role.permissions), 0n);
+ return roles.reduce((permission, role) => permission | BigInt(role.permissions), BigInt(0));
}
static finalPermission({
@@ -201,62 +198,51 @@ export class Permissions extends BitField {
}
export type PermissionCache = {
- channel?: ChannelDocument | null;
- member?: MemberDocument | null;
- guild?: GuildDocument | null;
- roles?: RoleDocument[] | null;
+ channel?: Channel | undefined;
+ member?: Member | undefined;
+ guild?: Guild | undefined;
+ roles?: Role[] | undefined;
user_id?: string;
};
-export async function getPermission(
- user_id?: string,
- guild_id?: string,
- channel_id?: string,
- cache: PermissionCache = {}
-) {
- var { channel, member, guild, roles } = cache;
-
+export async function getPermission(user_id?: string, guild_id?: string, channel_id?: string) {
if (!user_id) throw new HTTPError("User not found");
+ var channel: Channel | undefined;
+ var member: Member | undefined;
+ var guild: Guild | undefined;
- 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 (channel_id) {
+ channel = await Channel.findOneOrFail({ id: channel_id });
+ if (channel.guild_id) guild_id = channel.guild_id; // derive guild_id from the channel
}
if (guild_id) {
- if (!guild) guild = await GuildModel.findOne({ id: guild_id }, { owner_id: true }).exec();
- if (!guild) throw new HTTPError("Guild not found");
+ guild = await Guild.findOneOrFail({ id: guild_id });
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();
+ member = await Member.findOneOrFail({ where: { guild: guild_id, id: user_id }, relations: ["roles"] });
}
+ // TODO: remove guild.roles and convert recipient_ids to recipients
var permission = Permissions.finalPermission({
user: {
id: user_id,
- roles: member?.roles || [],
+ roles: member?.roles.map((x) => x.id) || [],
},
guild: {
- roles: roles || [],
+ roles: member?.roles || [],
},
channel: {
overwrites: channel?.permission_overwrites,
owner_id: channel?.owner_id,
- recipient_ids: channel?.recipient_ids,
+ recipient_ids: channel?.recipients?.map((x) => x.id),
},
});
const obj = new Permissions(permission);
// pass cache to permission for possible future getPermission calls
- obj.cache = { guild, member, channel, roles, user_id };
+ obj.cache = { guild, member, channel, roles: member?.roles, user_id };
return obj;
}
diff --git a/util/src/util/RabbitMQ.ts b/util/src/util/RabbitMQ.ts
index 9da41990..0f5eb6aa 100644
--- a/util/src/util/RabbitMQ.ts
+++ b/util/src/util/RabbitMQ.ts
@@ -1,18 +1,19 @@
import amqp, { Connection, Channel } from "amqplib";
-import Config from "./Config";
+// 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`);
+ return;
+ // const host = Config.get().rabbitmq.host;
+ // if (!host) return;
+ // console.log(`[RabbitMQ] connect: ${host}`);
+ // this.connection = await amqp.connect(host, {
+ // timeout: 1000 * 60,
+ // });
+ // console.log(`[RabbitMQ] connected`);
+ // this.channel = await this.connection.createChannel();
+ // console.log(`[RabbitMQ] channel created`);
},
};
diff --git a/util/src/util/UserFlags.ts b/util/src/util/UserFlags.ts
deleted file mode 100644
index 72394eff..00000000
--- a/util/src/util/UserFlags.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-// https://github.com/discordjs/discord.js/blob/master/src/util/UserFlags.js
-// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah
-
-import { BitField } from "./BitField";
-
-export class UserFlags extends BitField {
- static FLAGS = {
- DISCORD_EMPLOYEE: BigInt(1) << BigInt(0),
- PARTNERED_SERVER_OWNER: BigInt(1) << BigInt(1),
- HYPESQUAD_EVENTS: BigInt(1) << BigInt(2),
- BUGHUNTER_LEVEL_1: BigInt(1) << BigInt(3),
- HOUSE_BRAVERY: BigInt(1) << BigInt(6),
- HOUSE_BRILLIANCE: BigInt(1) << BigInt(7),
- HOUSE_BALANCE: BigInt(1) << BigInt(8),
- EARLY_SUPPORTER: BigInt(1) << BigInt(9),
- TEAM_USER: BigInt(1) << BigInt(10),
- SYSTEM: BigInt(1) << BigInt(12),
- BUGHUNTER_LEVEL_2: BigInt(1) << BigInt(14),
- VERIFIED_BOT: BigInt(1) << BigInt(16),
- EARLY_VERIFIED_BOT_DEVELOPER: BigInt(1) << BigInt(17),
- };
-}
diff --git a/util/src/util/checkToken.ts b/util/src/util/checkToken.ts
index 91bf08d5..8415e8c0 100644
--- a/util/src/util/checkToken.ts
+++ b/util/src/util/checkToken.ts
@@ -1,6 +1,7 @@
-import { JWTOptions } from "./Constants";
-import jwt from "jsonwebtoken";
-import { UserModel } from "../models";
+import jwt, { VerifyOptions } from "jsonwebtoken";
+import { User } from "../entities";
+
+export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] };
export function checkToken(token: string, jwtSecret: string): Promise<any> {
return new Promise((res, rej) => {
@@ -8,13 +9,11 @@ export function checkToken(token: string, jwtSecret: string): Promise<any> {
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();
+ const user = await User.findOne({ id: decoded.id }, { select: ["data", "bot", "disabled", "deleted"] });
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 (decoded.iat * 1000 < new Date(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");
diff --git a/util/src/util/index.ts b/util/src/util/index.ts
index e52a23b7..16b98ca3 100644
--- a/util/src/util/index.ts
+++ b/util/src/util/index.ts
@@ -1,11 +1,13 @@
-export * from "./Regex";
-export * from "./String";
+export * from "./Database";
+
export * from "./BitField";
+export * from "./Config";
+export * from "./checkToken";
+export * from "./Event";
export * from "./Intents";
export * from "./MessageFlags";
export * from "./Permissions";
export * from "./Snowflake";
-export * from "./UserFlags";
-export * from "./toBigInt";
export * from "./RabbitMQ";
-export * from "./Event";
+export * from "./Regex";
+export * from "./String";
diff --git a/util/src/util/toBigInt.ts b/util/src/util/toBigInt.ts
deleted file mode 100644
index b7985928..00000000
--- a/util/src/util/toBigInt.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export default function toBigInt(string: string): bigint {
- return BigInt(string);
-}
-
|