mongoose Schemas

This commit is contained in:
Flam3rboy 2021-02-13 09:30:21 +01:00
parent f572c7142b
commit 8595646b72
17 changed files with 654 additions and 46 deletions

3
package-lock.json generated
View File

@ -11,7 +11,8 @@
"dependencies": { "dependencies": {
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lambert-db": "^1.1.7", "lambert-db": "^1.1.7",
"missing-native-js-functions": "^1.2.2" "missing-native-js-functions": "^1.2.2",
"mongodb": "^3.6.4"
}, },
"devDependencies": { "devDependencies": {
"@types/jsonwebtoken": "^8.5.0", "@types/jsonwebtoken": "^8.5.0",

View File

@ -22,7 +22,8 @@
"dependencies": { "dependencies": {
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lambert-db": "^1.1.7", "lambert-db": "^1.1.7",
"missing-native-js-functions": "^1.2.2" "missing-native-js-functions": "^1.2.2",
"mongodb": "^3.6.4"
}, },
"devDependencies": { "devDependencies": {
"@types/jsonwebtoken": "^8.5.0", "@types/jsonwebtoken": "^8.5.0",

View File

@ -1,5 +1,6 @@
import { User } from ".."; import { User } from "..";
import { ClientStatus, Status } from "./Status"; import { ClientStatus, Status } from "./Status";
import { Schema, model, Types, Document } from "mongoose";
export interface Presence { export interface Presence {
user: User; user: User;
@ -45,6 +46,44 @@ export interface Activity {
flags?: bigint; 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 { export enum ActivityType {
GAME = 0, GAME = 0,
STREAMING = 1, STREAMING = 1,

View File

@ -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 { export interface Channel {
id: bigint; id: bigint;
created_at: number; created_at: number;
name: string; name: string;
type: number; type: number;
read_state: ReadState[];
}
export interface ReadState {
last_message_id: bigint;
last_pin_timestamp: number;
mention_count: number;
} }
export interface TextBasedChannel { export interface TextBasedChannel {
messages: any[];
last_message_id?: bigint; last_message_id?: bigint;
last_pin_timestamp?: number; last_pin_timestamp?: number;
} }

View File

@ -1,12 +1,27 @@
export interface Emoji { import { Schema, model, Types, Document } from "mongoose";
allNamesString: string; // e.g. :thonk:
export interface Emoji extends Document {
id: bigint;
animated: boolean; animated: boolean;
available: boolean; available: boolean;
guildId: bigint; guild_id: bigint;
id: bigint;
managed: boolean; managed: boolean;
name: string; name: string;
require_colons: boolean; require_colons: boolean;
url: string; 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");

View File

@ -10,8 +10,9 @@ import { Message, PartialEmoji } from "./Message";
import { VoiceState } from "./VoiceState"; import { VoiceState } from "./VoiceState";
import { ApplicationCommand } from "./Application"; import { ApplicationCommand } from "./Application";
import { Interaction } from "./Interaction"; import { Interaction } from "./Interaction";
import { Schema, model, Types, Document } from "mongoose";
export interface Event { export interface Event extends Document {
guild_id?: bigint; guild_id?: bigint;
user_id?: bigint; user_id?: bigint;
channel_id?: bigint; channel_id?: bigint;
@ -20,6 +21,17 @@ export interface Event {
data?: any; 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 // ! Custom Events that shouldn't get sent to the client but processed by the server
export interface InvalidatedEvent extends Event { export interface InvalidatedEvent extends Event {
@ -120,7 +132,10 @@ export interface GuildUpdateEvent extends Event {
export interface GuildDeleteEvent extends Event { export interface GuildDeleteEvent extends Event {
event: "GUILD_DELETE"; event: "GUILD_DELETE";
data: Guild; data: {
id: bigint;
unavailable?: boolean;
};
} }
export interface GuildBanAddEvent extends Event { export interface GuildBanAddEvent extends Event {

View File

@ -1,9 +1,7 @@
import { GuildChannel } from "./Channel"; import { Schema, model, Types, Document } from "mongoose";
import { Emoji } from "./Emoji";
import { Member } from "./Member";
import { Role } from "./Role";
export interface Guild { export interface Guild extends Document {
id: bigint;
afk_channel_id?: bigint; afk_channel_id?: bigint;
afk_timeout?: number; afk_timeout?: number;
application_id?: bigint; application_id?: bigint;
@ -11,12 +9,9 @@ export interface Guild {
default_message_notifications?: number; default_message_notifications?: number;
description?: string; description?: string;
discovery_splash?: string; discovery_splash?: string;
emojis: Emoji[];
explicit_content_filter?: number; explicit_content_filter?: number;
features: string[]; features: string[];
icon?: string; icon?: string;
id: bigint;
// joined_at?: number; \n // owner?: boolean; // ! member specific should be removed
large?: boolean; large?: boolean;
max_members?: number; // e.g. default 100.000 max_members?: number; // e.g. default 100.000
max_presences?: number; max_presences?: number;
@ -24,8 +19,9 @@ export interface Guild {
member_count?: number; member_count?: number;
presence_count?: number; // users online presence_count?: number; // users online
// members?: Member[]; // * Members are stored in a seperate collection // members?: Member[]; // * Members are stored in a seperate collection
// roles: Role[]; // * Role are stroed in a seperate collection // roles: Role[]; // * Role are stored in a seperate collection
// channels: GuildChannel[]; // * Channels are stroed 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; mfa_level?: number;
name: string; name: string;
owner_id: bigint; owner_id: bigint;
@ -46,3 +42,44 @@ export interface Guild {
widget_channel_id?: bigint; widget_channel_id?: bigint;
widget_enabled?: boolean; 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");

View File

@ -1,4 +1,6 @@
export interface Invite { import { Schema, model, Types, Document } from "mongoose";
export interface Invite extends Document {
code: string; code: string;
temporary: boolean; temporary: boolean;
uses: number; uses: number;
@ -19,7 +21,6 @@ export interface Invite {
name: string; name: string;
type: number; type: number;
}; };
inviter: { inviter: {
id: bigint; id: bigint;
username: string; username: string;
@ -34,3 +35,42 @@ export interface Invite {
}; };
target_user_type: number; 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");

View File

@ -1,6 +1,7 @@
import { PublicUser } from "./User"; import { PublicUser } from "./User";
import { Schema, model, Types, Document } from "mongoose";
export interface Member { export interface Member extends Document {
id: bigint; id: bigint;
nick?: string; nick?: string;
roles: bigint[]; roles: bigint[];
@ -13,10 +14,6 @@ export interface Member {
settings: UserGuildSettings; settings: UserGuildSettings;
} }
export interface PublicMember extends Omit<Member, "settings" | "id"> {
user: PublicUser;
}
export interface UserGuildSettings { export interface UserGuildSettings {
channel_overrides: { channel_overrides: {
channel_id: bigint; channel_id: bigint;
@ -37,3 +34,43 @@ export interface MuteConfig {
end_time: number; end_time: number;
selected_time_window: 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;
}

View File

@ -1,6 +1,7 @@
import { Schema, model, Types, Document } from "mongoose";
import { ChannelType } from "./Channel"; import { ChannelType } from "./Channel";
export interface Message { export interface Message extends Document {
id: bigint; id: bigint;
author_id?: bigint; author_id?: bigint;
webhook_id?: bigint; webhook_id?: bigint;
@ -27,7 +28,7 @@ export interface Message {
activity?: { activity?: {
type: number; type: number;
party_id: string; party_id: string;
}[]; };
flags?: bigint; flags?: bigint;
stickers?: []; stickers?: [];
message_reference?: { message_reference?: {
@ -124,3 +125,104 @@ export interface AllowedMentions {
users?: bigint[]; users?: bigint[];
replied_user?: boolean; 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");

View File

@ -1,4 +1,6 @@
export interface Role { import { Schema, model, Types, Document } from "mongoose";
export interface Role extends Document {
id: bigint; id: bigint;
color: number; color: number;
hoist: boolean; hoist: boolean;
@ -11,3 +13,19 @@ export interface Role {
bot_id?: bigint; 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");

View File

@ -5,3 +5,9 @@ export interface ClientStatus {
mobile?: string; // e.g. iOS/Android mobile?: string; // e.g. iOS/Android
web?: string; // e.g. browser, bot account web?: string; // e.g. browser, bot account
} }
export const ClientStatus = {
desktop: String,
mobile: String,
web: String,
};

View File

@ -1,7 +1,8 @@
import { Activity } from "./Activity"; import { Activity } from "./Activity";
import { ClientStatus, Status } from "./Status"; import { ClientStatus, Status } from "./Status";
import { Schema, model, Types, Document } from "mongoose";
export interface User { export interface User extends Document {
id: bigint; id: bigint;
username: string; username: string;
discriminator: string; discriminator: string;
@ -103,3 +104,100 @@ export interface UserSettings {
theme: "dark" | "white"; // dark theme: "dark" | "white"; // dark
timezone_offset: number; // e.g -60 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");

View File

@ -1,6 +1,7 @@
import { PublicMember } from "./Member"; import { PublicMember } from "./Member";
import { Schema, model, Types, Document } from "mongoose";
export interface VoiceState { export interface VoiceState extends Document {
guild_id?: bigint; guild_id?: bigint;
channel_id: bigint; channel_id: bigint;
user_id: bigint; user_id: bigint;
@ -13,3 +14,19 @@ export interface VoiceState {
self_video: boolean; self_video: boolean;
suppress: boolean; // whether this user is muted by the current user 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");

View File

@ -5,6 +5,7 @@ var Config: ProviderCache;
export default { export default {
init: async function init(opts: DefaultOptions = DefaultOptions) { init: async function init(opts: DefaultOptions = DefaultOptions) {
await db.collection("config").findOne({});
Config = await db.data.config({}).cache(); Config = await db.data.config({}).cache();
await Config.init(); await Config.init();
await Config.set(opts.merge(Config.cache || {})); await Config.set(opts.merge(Config.cache || {}));

View File

@ -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 connection = mongoose.createConnection(uri, { autoIndex: true });
const db = new MongoDatabase("mongodb://127.0.0.1:27017/lambert?readPreference=secondaryPreferred", {
useNewUrlParser: true,
useUnifiedTopology: false,
});
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();
}
}

76
src/util/MongoBigInt.ts Normal file
View File

@ -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;