diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/models/Activity.ts | 39 | ||||
-rw-r--r-- | src/models/Channel.ts | 41 | ||||
-rw-r--r-- | src/models/Emoji.ts | 25 | ||||
-rw-r--r-- | src/models/Event.ts | 19 | ||||
-rw-r--r-- | src/models/Guild.ts | 57 | ||||
-rw-r--r-- | src/models/Invite.ts | 44 | ||||
-rw-r--r-- | src/models/Member.ts | 47 | ||||
-rw-r--r-- | src/models/Message.ts | 106 | ||||
-rw-r--r-- | src/models/Role.ts | 20 | ||||
-rw-r--r-- | src/models/Status.ts | 6 | ||||
-rw-r--r-- | src/models/User.ts | 100 | ||||
-rw-r--r-- | src/models/VoiceState.ts | 19 | ||||
-rw-r--r-- | src/util/Config.ts | 1 | ||||
-rw-r--r-- | src/util/Database.ts | 94 | ||||
-rw-r--r-- | src/util/MongoBigInt.ts | 76 |
15 files changed, 650 insertions, 44 deletions
diff --git a/src/models/Activity.ts b/src/models/Activity.ts index e9e4224f..ee7e87cd 100644 --- a/src/models/Activity.ts +++ b/src/models/Activity.ts @@ -1,5 +1,6 @@ import { User } from ".."; import { ClientStatus, Status } from "./Status"; +import { Schema, model, Types, Document } from "mongoose"; export interface Presence { user: User; @@ -45,6 +46,44 @@ export interface Activity { flags?: bigint; } +export const Activity = { + name: String, + type: Number, + url: String, + created_at: Number, + timestamps: [ + { + start: Number, + end: Number, + }, + ], + application_id: Types.Long, + details: String, + state: String, + emoji: { + name: String, + id: Types.Long, + 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: Types.Long, +}; + export enum ActivityType { GAME = 0, STREAMING = 1, diff --git a/src/models/Channel.ts b/src/models/Channel.ts index 068f6b67..36f5e83d 100644 --- a/src/models/Channel.ts +++ b/src/models/Channel.ts @@ -1,19 +1,44 @@ +import { Schema, model, Types, Document } from "mongoose"; + +export interface ChannelDocument extends Channel, DMChannel, TextChannel, VoiceChannel, Document { + id: bigint; +} + +export const ChannelSchema = new Schema({ + id: Types.Long, + created_at: { type: Schema.Types.Date, required: true }, + name: { type: String, required: true }, + type: { type: Number, required: true }, + guild_id: Types.Long, + owner_id: Types.Long, + parent_id: Types.Long, + recipients: [Types.Long], + position: Number, + last_message_id: Types.Long, + last_pin_timestamp: Date, + nsfw: Boolean, + rate_limit_per_user: Number, + topic: String, + permission_overwrites: [ + { + allow: Types.Long, + deny: Types.Long, + id: Types.Long, + type: Number, + }, + ], +}); + +export const ChannelModel = model<ChannelDocument>("Channel", ChannelSchema, "channels"); + export interface Channel { id: bigint; created_at: number; name: string; type: number; - read_state: ReadState[]; -} - -export interface ReadState { - last_message_id: bigint; - last_pin_timestamp: number; - mention_count: number; } export interface TextBasedChannel { - messages: any[]; last_message_id?: bigint; last_pin_timestamp?: number; } diff --git a/src/models/Emoji.ts b/src/models/Emoji.ts index 1facc252..bbed9323 100644 --- a/src/models/Emoji.ts +++ b/src/models/Emoji.ts @@ -1,12 +1,27 @@ -export interface Emoji { - allNamesString: string; // e.g. :thonk: +import { Schema, model, Types, Document } from "mongoose"; + +export interface Emoji extends Document { + id: bigint; animated: boolean; available: boolean; - guildId: bigint; - id: bigint; + guild_id: bigint; managed: boolean; name: string; require_colons: boolean; url: string; - roles: []; + roles: bigint[]; // roles this emoji is whitelisted to } + +export const EmojiSchema = new Schema({ + id: Types.Long, + animated: Boolean, + available: Boolean, + guild_id: Types.Long, + managed: Boolean, + name: String, + require_colons: Boolean, + url: String, + roles: [Types.Long], +}); + +export const EmojiModel = model<Emoji>("Emoji", EmojiSchema, "emojis"); diff --git a/src/models/Event.ts b/src/models/Event.ts index 4925c7ca..e8dfe11e 100644 --- a/src/models/Event.ts +++ b/src/models/Event.ts @@ -10,8 +10,9 @@ import { Message, PartialEmoji } from "./Message"; import { VoiceState } from "./VoiceState"; import { ApplicationCommand } from "./Application"; import { Interaction } from "./Interaction"; +import { Schema, model, Types, Document } from "mongoose"; -export interface Event { +export interface Event extends Document { guild_id?: bigint; user_id?: bigint; channel_id?: bigint; @@ -20,6 +21,17 @@ export interface Event { data?: any; } +export const EventSchema = new Schema({ + guild_id: Types.Long, + user_id: Types.Long, + channel_id: Types.Long, + created_at: { type: Number, required: true }, + event: { type: String, required: true }, + data: Object, +}); + +export const EventModel = model<Event>("Event", EventSchema, "events"); + // ! Custom Events that shouldn't get sent to the client but processed by the server export interface InvalidatedEvent extends Event { @@ -120,7 +132,10 @@ export interface GuildUpdateEvent extends Event { export interface GuildDeleteEvent extends Event { event: "GUILD_DELETE"; - data: Guild; + data: { + id: bigint; + unavailable?: boolean; + }; } export interface GuildBanAddEvent extends Event { diff --git a/src/models/Guild.ts b/src/models/Guild.ts index 348f8c7c..c9a55301 100644 --- a/src/models/Guild.ts +++ b/src/models/Guild.ts @@ -1,9 +1,7 @@ -import { GuildChannel } from "./Channel"; -import { Emoji } from "./Emoji"; -import { Member } from "./Member"; -import { Role } from "./Role"; +import { Schema, model, Types, Document } from "mongoose"; -export interface Guild { +export interface Guild extends Document { + id: bigint; afk_channel_id?: bigint; afk_timeout?: number; application_id?: bigint; @@ -11,12 +9,9 @@ export interface Guild { default_message_notifications?: number; description?: string; discovery_splash?: string; - emojis: Emoji[]; explicit_content_filter?: number; features: string[]; icon?: string; - id: bigint; - // joined_at?: number; \n // owner?: boolean; // ! member specific should be removed large?: boolean; max_members?: number; // e.g. default 100.000 max_presences?: number; @@ -24,8 +19,9 @@ export interface Guild { member_count?: number; presence_count?: number; // users online // members?: Member[]; // * Members are stored in a seperate collection - // roles: Role[]; // * Role are stroed in a seperate collection - // channels: GuildChannel[]; // * Channels are stroed 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 mfa_level?: number; name: string; owner_id: bigint; @@ -46,3 +42,44 @@ export interface Guild { widget_channel_id?: bigint; widget_enabled?: boolean; } + +export const GuildSchema = new Schema({ + afk_channel_id: Types.Long, + afk_timeout: Number, + application_id: Types.Long, + banner: String, + default_message_notifications: Number, + description: String, + discovery_splash: String, + explicit_content_filter: Number, + features: { type: [String], default: [] }, + icon: String, + id: { type: Types.Long, required: true }, + large: Boolean, + max_members: { type: Number, default: 100000 }, + max_presences: Number, + max_video_channel_users: { type: Number, default: 25 }, + member_count: Number, + presence_count: Number, + mfa_level: Number, + name: { type: String, required: true }, + owner_id: { type: Types.Long, required: true }, + preferred_locale: String, + premium_subscription_count: Number, + premium_tier: Number, + public_updates_channel_id: Types.Long, + region: String, + rules_channel_id: Types.Long, + splash: String, + system_channel_flags: Number, + system_channel_id: Types.Long, + unavailable: Boolean, + vanity_url_code: String, + verification_level: Number, + voice_states: { type: [Object], default: [] }, + welcome_screen: { type: [Object], default: [] }, + widget_channel_id: Types.Long, + widget_enabled: Boolean, +}); + +export const GuildModel = model<Guild>("Guild", GuildSchema, "guilds"); diff --git a/src/models/Invite.ts b/src/models/Invite.ts index df1286f5..b4dbb8bc 100644 --- a/src/models/Invite.ts +++ b/src/models/Invite.ts @@ -1,4 +1,6 @@ -export interface Invite { +import { Schema, model, Types, Document } from "mongoose"; + +export interface Invite extends Document { code: string; temporary: boolean; uses: number; @@ -19,7 +21,6 @@ export interface Invite { name: string; type: number; }; - inviter: { id: bigint; username: string; @@ -34,3 +35,42 @@ export interface Invite { }; target_user_type: number; } + +export const InviteSchema = new Schema({ + code: String, + temporary: Boolean, + uses: Number, + max_uses: Number, + max_age: Number, + created_at: Number, + guild: { + id: Types.Long, + name: String, + splash: String, + description: String, + icon: String, + features: Object, + verification_level: Number, + }, + channel: { + id: Types.Long, + name: String, + type: Number, + }, + + inviter: { + id: Types.Long, + username: String, + avatar: String, + discriminator: Number, + }, + target_user: { + id: Types.Long, + username: String, + avatar: String, + discriminator: Number, + }, + target_user_type: Number, +}); + +export const InviteModel = model<Invite>("Invite", InviteSchema, "invites"); diff --git a/src/models/Member.ts b/src/models/Member.ts index a38a5ca3..d9f34ac8 100644 --- a/src/models/Member.ts +++ b/src/models/Member.ts @@ -1,6 +1,7 @@ import { PublicUser } from "./User"; +import { Schema, model, Types, Document } from "mongoose"; -export interface Member { +export interface Member extends Document { id: bigint; nick?: string; roles: bigint[]; @@ -13,10 +14,6 @@ export interface Member { settings: UserGuildSettings; } -export interface PublicMember extends Omit<Member, "settings" | "id"> { - user: PublicUser; -} - export interface UserGuildSettings { channel_overrides: { channel_id: bigint; @@ -37,3 +34,43 @@ 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: Types.Long, + nick: String, + roles: [Types.Long], + joined_at: Number, + premium_since: Number, + deaf: Boolean, + mute: Boolean, + pending: Boolean, + permissions: Types.Long, + settings: { + channel_overrides: [ + { + channel_id: Types.Long, + 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 const MemberModel = model<Member>("Member", MemberSchema, "members"); + +export interface PublicMember extends Omit<Member, "settings" | "id"> { + user: PublicUser; +} diff --git a/src/models/Message.ts b/src/models/Message.ts index 45aefd34..22569d8f 100644 --- a/src/models/Message.ts +++ b/src/models/Message.ts @@ -1,6 +1,7 @@ +import { Schema, model, Types, Document } from "mongoose"; import { ChannelType } from "./Channel"; -export interface Message { +export interface Message extends Document { id: bigint; author_id?: bigint; webhook_id?: bigint; @@ -27,7 +28,7 @@ export interface Message { activity?: { type: number; party_id: string; - }[]; + }; flags?: bigint; stickers?: []; message_reference?: { @@ -124,3 +125,104 @@ export interface AllowedMentions { users?: bigint[]; replied_user?: boolean; } + +const Attachment = { + id: Types.Long, // 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) +}; + +const EmbedImage = { + url: String, + proxy_url: String, + height: Number, + width: Number, +}; + +const Reaction = { + count: Number, + emoji: { + id: Types.Long, + name: String, + animated: Boolean, + }, +}; + +const Embed = { + title: String, //title of embed + type: String, // type of embed (always "rich" for webhook embeds) + description: String, // description of embed + url: String, // url of embed + timestamp: Number, // 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: Types.Long, + author_id: Types.Long, + webhook_id: Types.Long, + application_id: Types.Long, + content: String, + timestamp: Number, + edited_timestamp: Number, + tts: Boolean, + mention_everyone: Boolean, + mentions: [Types.Long], + mention_roles: [Types.Long], + mention_channels: [ + { + id: Types.Long, + guild_id: Types.Long, + type: ChannelType, + name: String, + }, + ], + attachments: [Attachment], + embeds: [Embed], + reactions: [Reaction], + nonce: Schema.Types.Mixed, // can be a long or a string + pinned: Boolean, + type: MessageType, + activity: { + type: Number, + party_id: String, + }, + flags: Types.Long, + stickers: [], + message_reference: { + message_id: Types.Long, + channel_id: Types.Long, + guild_id: Types.Long, + }, +}); + +export const MessageModel = model<Message>("Message", MessageSchema, "messages"); diff --git a/src/models/Role.ts b/src/models/Role.ts index e0f2f863..d35bd57c 100644 --- a/src/models/Role.ts +++ b/src/models/Role.ts @@ -1,4 +1,6 @@ -export interface Role { +import { Schema, model, Types, Document } from "mongoose"; + +export interface Role extends Document { id: bigint; color: number; hoist: boolean; @@ -11,3 +13,19 @@ export interface Role { bot_id?: bigint; }; } + +export const RoleSchema = new Schema({ + id: Types.Long, + color: Number, + hoist: Boolean, + managed: Boolean, + mentionable: Boolean, + name: String, + permissions: Types.Long, + position: Number, + tags: { + bot_id: Types.Long, + }, +}); + +export const RoleModel = model<Role>("Role", RoleSchema, "roles"); diff --git a/src/models/Status.ts b/src/models/Status.ts index c4dab586..5a9bf2ca 100644 --- a/src/models/Status.ts +++ b/src/models/Status.ts @@ -5,3 +5,9 @@ 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/src/models/User.ts b/src/models/User.ts index f591d26e..d79e7e0c 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -1,7 +1,8 @@ import { Activity } from "./Activity"; import { ClientStatus, Status } from "./Status"; +import { Schema, model, Types, Document } from "mongoose"; -export interface User { +export interface User extends Document { id: bigint; username: string; discriminator: string; @@ -103,3 +104,100 @@ export interface UserSettings { theme: "dark" | "white"; // dark timezone_offset: number; // e.g -60 } + +export const UserSchema = new Schema({ + id: Types.Long, + username: String, + discriminator: String, + avatar: String, + phone: String, + desktop: Boolean, + mobile: Boolean, + premium: Boolean, + premium_type: Number, + bot: Boolean, + system: Boolean, + nsfw_allowed: Boolean, + mfa_enabled: Boolean, + created_at: Number, + verified: Boolean, + email: String, + flags: Types.Long, // TODO: automatically convert Types.Long to BitField of UserFlags + public_flags: Types.Long, + hash: String, // hash of the password, salt is saved in password (bcrypt) + guilds: [Types.Long], // array of guild ids the user is part of + valid_tokens_since: Number, // all tokens with a previous issue date are invalid + 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: Types.Long, + 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 }, + gif_auto_play: Boolean, + // every top guild is displayed as a "folder" + guild_folders: [ + { + color: Number, + guild_ids: [Types.Long], + id: Number, + name: String, + }, + ], + guild_positions: [Types.Long], // 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: [Types.Long], + show_current_game: Boolean, + status: String, + stream_notifications_enabled: Boolean, + theme: String, // dark + timezone_offset: Number, // e.g -60, + }, + relationships: [ + { + id: Types.Long, + nickname: String, + type: Number, + user_id: Types.Long, + }, + ], + connected_accounts: [ + { + access_token: String, + friend_sync: Boolean, + id: String, + name: String, + revoked: Boolean, + show_activity: Boolean, + type: String, + verifie: Boolean, + visibility: Number, + }, + ], + presence: { + status: String, + activities: [Activity], + client_status: ClientStatus, + }, +}); + +export const UserModel = model<User>("User", UserSchema, "users"); diff --git a/src/models/VoiceState.ts b/src/models/VoiceState.ts index 19a1bf27..8ac5ae19 100644 --- a/src/models/VoiceState.ts +++ b/src/models/VoiceState.ts @@ -1,6 +1,7 @@ import { PublicMember } from "./Member"; +import { Schema, model, Types, Document } from "mongoose"; -export interface VoiceState { +export interface VoiceState extends Document { guild_id?: bigint; channel_id: bigint; user_id: bigint; @@ -13,3 +14,19 @@ export interface VoiceState { self_video: boolean; suppress: boolean; // whether this user is muted by the current user } + +export const VoiceSateSchema = new Schema({ + guild_id: Types.Long, + channel_id: Types.Long, + user_id: Types.Long, + 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 VoiceStateModel = model<VoiceState>("VoiceState", VoiceSateSchema, "voicestates"); diff --git a/src/util/Config.ts b/src/util/Config.ts index 5886b268..91ffda01 100644 --- a/src/util/Config.ts +++ b/src/util/Config.ts @@ -5,6 +5,7 @@ var Config: ProviderCache; export default { init: async function init(opts: DefaultOptions = DefaultOptions) { + await db.collection("config").findOne({}); Config = await db.data.config({}).cache(); await Config.init(); await Config.set(opts.merge(Config.cache || {})); diff --git a/src/util/Database.ts b/src/util/Database.ts index ed45a9ad..56f53f9a 100644 --- a/src/util/Database.ts +++ b/src/util/Database.ts @@ -1,9 +1,89 @@ -import { MongoDatabase } from "lambert-db"; +import "./MongoBigInt"; +import mongoose, { Collection } from "mongoose"; +import { ChangeStream, ChangeEvent, Long } from "mongodb"; +import EventEmitter from "events"; +const uri = process.env.MONGO_URL || "mongodb://localhost:27017/fosscord?readPreference=secondaryPreferred"; -// TODO: load url from config -const db = new MongoDatabase("mongodb://127.0.0.1:27017/lambert?readPreference=secondaryPreferred", { - useNewUrlParser: true, - useUnifiedTopology: false, -}); +const connection = mongoose.createConnection(uri, { autoIndex: true }); -export default db; +export default connection; + +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; +} + +export class MongooseCache extends EventEmitter { + public stream: ChangeStream; + public data: any; + + constructor( + public collection: Collection, + public pipeline: Array<Record<string, unknown>>, + public opts: { + onlyEvents: boolean; + } + ) { + super(); + } + + async init() { + 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) { + this.data = await this.collection.aggregate(this.pipeline).toArray(); + } + } + + 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) => { + // @ts-ignore + if (doc.fullDocument) { + // @ts-ignore + if (!this.opts.onlyEvents) this.data = doc.fullDocument; + } + + switch (doc.operationType) { + case "dropDatabase": + return this.destroy(); + case "drop": + return this.destroy(); + case "delete": + return this.emit("delete", doc.documentKey._id.toHexString()); + case "insert": + return this.emit("insert", doc.fullDocument); + case "update": + case "replace": + return this.emit("change", doc.fullDocument); + case "invalidate": + return this.destroy(); + default: + return; + } + }; + + destroy() { + this.stream.off("change", this.change); + this.emit("close"); + + if (this.stream.isClosed()) return; + + return this.stream.close(); + } +} diff --git a/src/util/MongoBigInt.ts b/src/util/MongoBigInt.ts new file mode 100644 index 00000000..cc185bed --- /dev/null +++ b/src/util/MongoBigInt.ts @@ -0,0 +1,76 @@ +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); + } + + handleArray(val: any) { + var self = this; + return val.map(function (m: any) { + return self.cast(m); + }); + } + + checkRequired(val: any) { + return null != val; + } + + cast(val: any, scope?: any, init?: any) { + if (null === val) return val; + if ("" === val) return null; + + if (val instanceof mongoose.mongo.Long) return BigInt(val.toString()); + if (val instanceof Number || "number" == typeof val) return BigInt(val); + 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); + } + } +} + +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; |