diff options
author | Flam3rboy <34555296+Flam3rboy@users.noreply.github.com> | 2021-08-12 20:33:42 +0200 |
---|---|---|
committer | Flam3rboy <34555296+Flam3rboy@users.noreply.github.com> | 2021-08-12 20:33:42 +0200 |
commit | a92c6637cea0ce595c712c3d445b57a1184e833e (patch) | |
tree | b4aa50b4091617195025e2ed36406ae61e8fe3b9 /rtc/src | |
parent | :sparkles: util (diff) | |
download | server-a92c6637cea0ce595c712c3d445b57a1184e833e.tar.xz |
:sparkles: rtc
Diffstat (limited to 'rtc/src')
45 files changed, 4022 insertions, 0 deletions
diff --git a/rtc/src/index.ts b/rtc/src/index.ts new file mode 100644 index 00000000..3565fb6b --- /dev/null +++ b/rtc/src/index.ts @@ -0,0 +1,10 @@ +export * from "./util/checkToken"; + +export * as Constants from "./util/Constants"; +export * from "./models/index"; +export * from "./util/index"; + +import Config from "./util/Config"; +import db, { MongooseCache, toObject } from "./util/Database"; + +export { Config, db, MongooseCache, toObject }; diff --git a/rtc/src/main.cpp b/rtc/src/main.cpp new file mode 100644 index 00000000..372eaa00 --- /dev/null +++ b/rtc/src/main.cpp @@ -0,0 +1,47 @@ +// $$$$$$\ $$\ +// $$ __$$\ $$ | +// $$ / \__|$$$$$$\ $$$$$$$\ $$$$$$$\ $$$$$$$\ $$$$$$\ $$$$$$\ $$$$$$$ | +// $$$$\ $$ __$$\ $$ _____|$$ _____|$$ _____|$$ __$$\ $$ __$$\ $$ __$$ | +// $$ _| $$ / $$ |\$$$$$$\ \$$$$$$\ $$ / $$ / $$ |$$ | \__|$$ / $$ | +// $$ | $$ | $$ | \____$$\ \____$$\ $$ | $$ | $$ |$$ | $$ | $$ | +// $$ | \$$$$$$ |$$$$$$$ |$$$$$$$ |\$$$$$$$\ \$$$$$$ |$$ | \$$$$$$$ | +// \__| \______/ \_______/ \_______/ \_______| \______/ \__| \_______| +// +// +// +// $$\ $$$$$$\ +// \__| $$ __$$\ +// $$\ $$\ $$$$$$\ $$\ $$$$$$$\ $$$$$$\ $$ / \__| $$$$$$\ $$$$$$\ $$\ $$\ $$$$$$\ $$$$$$\ +// \$$\ $$ |$$ __$$\ $$ |$$ _____|$$ __$$\ \$$$$$$\ $$ __$$\ $$ __$$\\$$\ $$ |$$ __$$\ $$ __$$\ +// \$$\$$ / $$ / $$ |$$ |$$ / $$$$$$$$ | \____$$\ $$$$$$$$ |$$ | \__|\$$\$$ / $$$$$$$$ |$$ | \__| +// \$$$ / $$ | $$ |$$ |$$ | $$ ____|$$\ $$ |$$ ____|$$ | \$$$ / $$ ____|$$ | +// \$ / \$$$$$$ |$$ |\$$$$$$$\ \$$$$$$$\ \$$$$$$ |\$$$$$$$\ $$ | \$ / \$$$$$$$\ $$ | +// \_/ \______/ \__| \_______| \_______| \______/ \_______|\__| \_/ \_______|\__| +// +// +// + +#include "rtcPeerHandler.hpp" //Handle peer connection requests +#include "mongoStub.hpp" //Handle communication with the MongoDB server + +int main(int argc, char **argv){ + + auto commsHandler = std::make_shared<rtcPeerHandler>(); + auto mongoHandler = std::make_unique<mongoStub>(); + + mongocxx::options::change_stream options; + //voiceEvents collection watcher + mongocxx::change_stream colCs = mongoHandler->getCol().watch(options); + + std::cout << "Server created and listening for events" << std::endl; + + //Check for new messages in the collection + for (;;){ + std::vector<mongoStub::mongoMessage> t = mongoHandler->getNewMessages(&colCs); + for(auto &i : t){ + std::cout << "[" << i.eventName << "] " << std::endl; + } + } + + return 0; +} \ No newline at end of file diff --git a/rtc/src/models/Activity.ts b/rtc/src/models/Activity.ts new file mode 100644 index 00000000..17abd1ca --- /dev/null +++ b/rtc/src/models/Activity.ts @@ -0,0 +1,132 @@ +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/rtc/src/models/Application.ts b/rtc/src/models/Application.ts new file mode 100644 index 00000000..fae6e8db --- /dev/null +++ b/rtc/src/models/Application.ts @@ -0,0 +1,67 @@ +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/rtc/src/models/AuditLog.ts b/rtc/src/models/AuditLog.ts new file mode 100644 index 00000000..02b2c444 --- /dev/null +++ b/rtc/src/models/AuditLog.ts @@ -0,0 +1,220 @@ +import { Schema, Document, Types } from "mongoose"; +import db from "../util/Database"; +import { ChannelPermissionOverwrite } from "./Channel"; +import { PublicUser } from "./User"; + +export interface AuditLogResponse { + webhooks: []; // TODO: + users: PublicUser[]; + audit_log_entries: AuditLogEntries[]; + integrations: []; // TODO: +} + +export interface AuditLogEntries { + target_id?: string; + user_id: string; + id: string; + action_type: AuditLogEvents; + options?: { + delete_member_days?: string; + members_removed?: string; + channel_id?: string; + messaged_id?: string; + count?: string; + id?: string; + type?: string; + role_name?: string; + }; + changes: AuditLogChange[]; + reason?: string; +} + +export interface AuditLogChange { + new_value?: AuditLogChangeValue; + old_value?: AuditLogChangeValue; + key: string; +} + +export interface AuditLogChangeValue { + 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?: ChannelPermissionOverwrite[]; + 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 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/rtc/src/models/Ban.ts b/rtc/src/models/Ban.ts new file mode 100644 index 00000000..f09950ee --- /dev/null +++ b/rtc/src/models/Ban.ts @@ -0,0 +1,32 @@ +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/rtc/src/models/Channel.ts b/rtc/src/models/Channel.ts new file mode 100644 index 00000000..1dd05896 --- /dev/null +++ b/rtc/src/models/Channel.ts @@ -0,0 +1,109 @@ +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, + 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; +} + +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/rtc/src/models/Emoji.ts b/rtc/src/models/Emoji.ts new file mode 100644 index 00000000..3e5cad53 --- /dev/null +++ b/rtc/src/models/Emoji.ts @@ -0,0 +1,29 @@ +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/rtc/src/models/Event.ts b/rtc/src/models/Event.ts new file mode 100644 index 00000000..1564107d --- /dev/null +++ b/rtc/src/models/Event.ts @@ -0,0 +1,540 @@ +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 { Interaction } from "./Interaction"; +import { Schema, model, Types, Document } from "mongoose"; +import db from "../util/Database"; + +export interface Event { + guild_id?: string; + user_id?: string; + channel_id?: string; + created_at?: Date; + event: EVENT; + data?: any; +} + +export interface EventDocument extends Event, Document {} + +export const EventSchema = new Schema({ + guild_id: String, + user_id: String, + channel_id: String, + created_at: { type: Date, required: true }, + event: { type: String, required: true }, + data: Object, +}); + +// @ts-ignore +export const EventModel = db.model<EventDocument>("Event", EventSchema, "events"); + +// ! Custom Events that shouldn't get sent to the client but processed by the server + +export interface InvalidatedEvent extends Event { + event: "INVALIDATED"; +} + +// ! END Custom Events that shouldn't get sent to the client but processed by the server + +export interface ReadyEventData { + v: number; + user: PublicUser & { + mobile: boolean; + desktop: boolean; + email: string | null; + flags: bigint; + mfa_enabled: boolean; + nsfw_allowed: boolean; + phone: string | null; + premium: boolean; + premium_type: number; + verified: boolean; + bot: boolean; + }; + private_channels: DMChannel[]; // this will be empty for bots + session_id: string; // resuming + guilds: Guild[]; + analytics_token?: string; + connected_accounts?: ConnectedAccount[]; + consents?: { + personalization?: { + consented?: boolean; + }; + }; + country_code?: string; // e.g. DE + friend_suggestion_count?: number; + geo_ordered_rtc_regions?: string[]; // ["europe","russie","india","us-east","us-central"] + experiments?: [number, number, number, number, number][]; + guild_experiments?: [ + // ? what are guild_experiments? + // this is the structure of it: + number, + null, + number, + [[number, { e: number; s: number }[]]], + [number, [[number, [number, number]]]], + { b: number; k: bigint[] }[] + ][]; + guild_join_requests?: []; // ? what is this? this is new + shard?: [number, number]; + user_settings?: UserSettings; + relationships?: Relationship[]; // TODO + read_state: { + entries: []; // TODO + partial: boolean; + version: number; + }; + user_guild_settings?: { + entries: UserGuildSettings[]; + version: number; + partial: boolean; + }; + application?: { + id: string; + flags: bigint; + }; + 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; + }[]; +} + +export interface ReadyEvent extends Event { + event: "READY"; + data: ReadyEventData; +} + +export interface ChannelCreateEvent extends Event { + event: "CHANNEL_CREATE"; + data: Channel; +} + +export interface ChannelUpdateEvent extends Event { + event: "CHANNEL_UPDATE"; + data: Channel; +} + +export interface ChannelDeleteEvent extends Event { + event: "CHANNEL_DELETE"; + data: Channel; +} + +export interface ChannelPinsUpdateEvent extends Event { + event: "CHANNEL_PINS_UPDATE"; + data: { + guild_id?: string; + channel_id: string; + last_pin_timestamp?: number; + }; +} + +export interface GuildCreateEvent extends Event { + event: "GUILD_CREATE"; + data: Guild; +} + +export interface GuildUpdateEvent extends Event { + event: "GUILD_UPDATE"; + data: Guild; +} + +export interface GuildDeleteEvent extends Event { + event: "GUILD_DELETE"; + data: { + id: string; + unavailable?: boolean; + }; +} + +export interface GuildBanAddEvent extends Event { + event: "GUILD_BAN_ADD"; + data: { + guild_id: string; + user: User; + }; +} + +export interface GuildBanRemoveEvent extends Event { + event: "GUILD_BAN_REMOVE"; + data: { + guild_id: string; + user: User; + }; +} + +export interface GuildEmojiUpdateEvent extends Event { + event: "GUILD_EMOJI_UPDATE"; + data: { + guild_id: string; + emojis: Emoji[]; + }; +} + +export interface GuildIntegrationUpdateEvent extends Event { + event: "GUILD_INTEGRATIONS_UPDATE"; + data: { + guild_id: string; + }; +} + +export interface GuildMemberAddEvent extends Event { + event: "GUILD_MEMBER_ADD"; + data: PublicMember & { + guild_id: string; + }; +} + +export interface GuildMemberRemoveEvent extends Event { + event: "GUILD_MEMBER_REMOVE"; + data: { + guild_id: string; + user: User; + }; +} + +export interface GuildMemberUpdateEvent extends Event { + event: "GUILD_MEMBER_UPDATE"; + data: { + guild_id: string; + roles: string[]; + user: User; + nick?: string; + joined_at?: Date; + premium_since?: number; + pending?: boolean; + }; +} + +export interface GuildMembersChunkEvent extends Event { + event: "GUILD_MEMBERS_CHUNK"; + data: { + guild_id: string; + members: PublicMember[]; + chunk_index: number; + chunk_count: number; + not_found: string[]; + presences: Presence[]; + nonce?: string; + }; +} + +export interface GuildRoleCreateEvent extends Event { + event: "GUILD_ROLE_CREATE"; + data: { + guild_id: string; + role: Role; + }; +} + +export interface GuildRoleUpdateEvent extends Event { + event: "GUILD_ROLE_UPDATE"; + data: { + guild_id: string; + role: Role; + }; +} + +export interface GuildRoleDeleteEvent extends Event { + event: "GUILD_ROLE_DELETE"; + data: { + guild_id: string; + role_id: string; + }; +} + +export interface InviteCreateEvent extends Event { + event: "INVITE_CREATE"; + data: Omit<Invite, "guild" | "channel"> & { + channel_id: string; + guild_id?: string; + }; +} + +export interface InviteDeleteEvent extends Event { + event: "INVITE_DELETE"; + data: { + channel_id: string; + guild_id?: string; + code: string; + }; +} + +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; +} + +export interface MessageUpdateEvent extends Event { + event: "MESSAGE_UPDATE"; + data: MessagePayload; +} + +export interface MessageDeleteEvent extends Event { + event: "MESSAGE_DELETE"; + data: { + id: string; + channel_id: string; + guild_id?: string; + }; +} + +export interface MessageDeleteBulkEvent extends Event { + event: "MESSAGE_DELETE_BULK"; + data: { + ids: string[]; + channel_id: string; + guild_id?: string; + }; +} + +export interface MessageReactionAddEvent extends Event { + event: "MESSAGE_REACTION_ADD"; + data: { + user_id: string; + channel_id: string; + message_id: string; + guild_id?: string; + member?: PublicMember; + emoji: PartialEmoji; + }; +} + +export interface MessageReactionRemoveEvent extends Event { + event: "MESSAGE_REACTION_REMOVE"; + data: { + user_id: string; + channel_id: string; + message_id: string; + guild_id?: string; + emoji: PartialEmoji; + }; +} + +export interface MessageReactionRemoveAllEvent extends Event { + event: "MESSAGE_REACTION_REMOVE_ALL"; + data: { + channel_id: string; + message_id: string; + guild_id?: string; + }; +} + +export interface MessageReactionRemoveEmojiEvent extends Event { + event: "MESSAGE_REACTION_REMOVE_EMOJI"; + data: { + channel_id: string; + message_id: string; + guild_id?: string; + emoji: PartialEmoji; + }; +} + +export interface PresenceUpdateEvent extends Event { + event: "PRESENCE_UPDATE"; + data: Presence; +} + +export interface TypingStartEvent extends Event { + event: "TYPING_START"; + data: { + channel_id: string; + user_id: string; + timestamp: number; + guild_id?: string; + member?: PublicMember; + }; +} + +export interface UserUpdateEvent extends Event { + event: "USER_UPDATE"; + data: User; +} + +export interface VoiceStateUpdateEvent extends Event { + event: "VOICE_STATE_UPDATE"; + data: VoiceState & { + member: PublicMember; + }; +} + +export interface VoiceServerUpdateEvent extends Event { + event: "VOICE_SERVER_UPDATE"; + data: { + token: string; + guild_id: string; + endpoint: string; + }; +} + +export interface WebhooksUpdateEvent extends Event { + event: "WEBHOOKS_UPDATE"; + data: { + guild_id: string; + channel_id: string; + }; +} + +export type ApplicationCommandPayload = ApplicationCommand & { + guild_id: string; +}; + +export interface ApplicationCommandCreateEvent extends Event { + event: "APPLICATION_COMMAND_CREATE"; + data: ApplicationCommandPayload; +} + +export interface ApplicationCommandUpdateEvent extends Event { + event: "APPLICATION_COMMAND_UPDATE"; + data: ApplicationCommandPayload; +} + +export interface ApplicationCommandDeleteEvent extends Event { + event: "APPLICATION_COMMAND_DELETE"; + data: ApplicationCommandPayload; +} + +export interface InteractionCreateEvent extends Event { + event: "INTERACTION_CREATE"; + data: Interaction; +} + +export interface MessageAckEvent extends Event { + event: "MESSAGE_ACK"; + data: { + channel_id: string; + message_id: string; + version?: number; + manual?: boolean; + mention_count?: number; + }; +} + +export interface RelationshipAddEvent extends Event { + event: "RELATIONSHIP_ADD"; + data: Relationship & { + should_notify?: boolean; + user: PublicUser; + }; +} + +export interface RelationshipRemoveEvent extends Event { + event: "RELATIONSHIP_REMOVE"; + data: Omit<Relationship, "nickname">; +} + +// located in collection events + +export enum EVENTEnum { + Ready = "READY", + ChannelCreate = "CHANNEL_CREATE", + ChannelUpdate = "CHANNEL_UPDATE", + ChannelDelete = "CHANNEL_DELETE", + ChannelPinsUpdate = "CHANNEL_PINS_UPDATE", + GuildCreate = "GUILD_CREATE", + GuildUpdate = "GUILD_UPDATE", + GuildDelete = "GUILD_DELETE", + GuildBanAdd = "GUILD_BAN_ADD", + GuildBanRemove = "GUILD_BAN_REMOVE", + GuildEmojUpdate = "GUILD_EMOJI_UPDATE", + GuildIntegrationsUpdate = "GUILD_INTEGRATIONS_UPDATE", + GuildMemberAdd = "GUILD_MEMBER_ADD", + GuildMemberRempve = "GUILD_MEMBER_REMOVE", + GuildMemberUpdate = "GUILD_MEMBER_UPDATE", + GuildMemberSpeaking = "GUILD_MEMBER_SPEAKING", + GuildMembersChunk = "GUILD_MEMBERS_CHUNK", + GuildRoleCreate = "GUILD_ROLE_CREATE", + GuildRoleDelete = "GUILD_ROLE_DELETE", + GuildRoleUpdate = "GUILD_ROLE_UPDATE", + InviteCreate = "INVITE_CREATE", + InviteDelete = "INVITE_DELETE", + MessageCreate = "MESSAGE_CREATE", + MessageUpdate = "MESSAGE_UPDATE", + MessageDelete = "MESSAGE_DELETE", + MessageDeleteBulk = "MESSAGE_DELETE_BULK", + MessageReactionAdd = "MESSAGE_REACTION_ADD", + MessageReactionRemove = "MESSAGE_REACTION_REMOVE", + MessageReactionRemoveAll = "MESSAGE_REACTION_REMOVE_ALL", + MessageReactionRemoveEmoji = "MESSAGE_REACTION_REMOVE_EMOJI", + PresenceUpdate = "PRESENCE_UPDATE", + TypingStart = "TYPING_START", + UserUpdate = "USER_UPDATE", + WebhooksUpdate = "WEBHOOKS_UPDATE", + InteractionCreate = "INTERACTION_CREATE", + VoiceStateUpdate = "VOICE_STATE_UPDATE", + VoiceServerUpdate = "VOICE_SERVER_UPDATE", + ApplicationCommandCreate = "APPLICATION_COMMAND_CREATE", + ApplicationCommandUpdate = "APPLICATION_COMMAND_UPDATE", + ApplicationCommandDelete = "APPLICATION_COMMAND_DELETE", +} + +export type EVENT = + | "READY" + | "CHANNEL_CREATE" + | "CHANNEL_UPDATE" + | "CHANNEL_DELETE" + | "CHANNEL_PINS_UPDATE" + | "GUILD_CREATE" + | "GUILD_UPDATE" + | "GUILD_DELETE" + | "GUILD_BAN_ADD" + | "GUILD_BAN_REMOVE" + | "GUILD_EMOJI_UPDATE" + | "GUILD_INTEGRATIONS_UPDATE" + | "GUILD_MEMBER_ADD" + | "GUILD_MEMBER_REMOVE" + | "GUILD_MEMBER_UPDATE" + | "GUILD_MEMBER_SPEAKING" + | "GUILD_MEMBERS_CHUNK" + | "GUILD_ROLE_CREATE" + | "GUILD_ROLE_DELETE" + | "GUILD_ROLE_UPDATE" + | "INVITE_CREATE" + | "INVITE_DELETE" + | "MESSAGE_CREATE" + | "MESSAGE_UPDATE" + | "MESSAGE_DELETE" + | "MESSAGE_DELETE_BULK" + | "MESSAGE_REACTION_ADD" + // TODO: add a new event: bulk add reaction: + // | "MESSAGE_REACTION_BULK_ADD" + | "MESSAGE_REACTION_REMOVE" + | "MESSAGE_REACTION_REMOVE_ALL" + | "MESSAGE_REACTION_REMOVE_EMOJI" + | "PRESENCE_UPDATE" + | "TYPING_START" + | "USER_UPDATE" + | "WEBHOOKS_UPDATE" + | "INTERACTION_CREATE" + | "VOICE_STATE_UPDATE" + | "VOICE_SERVER_UPDATE" + | "APPLICATION_COMMAND_CREATE" + | "APPLICATION_COMMAND_UPDATE" + | "APPLICATION_COMMAND_DELETE" + | "MESSAGE_ACK" + | "RELATIONSHIP_ADD" + | "RELATIONSHIP_REMOVE" + | CUSTOMEVENTS; + +export type CUSTOMEVENTS = "INVALIDATED"; diff --git a/rtc/src/models/Guild.ts b/rtc/src/models/Guild.ts new file mode 100644 index 00000000..13a7d078 --- /dev/null +++ b/rtc/src/models/Guild.ts @@ -0,0 +1,161 @@ +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; + uses: number; + }; + 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, + uses: Number + }, + 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/rtc/src/models/Interaction.ts b/rtc/src/models/Interaction.ts new file mode 100644 index 00000000..764247a5 --- /dev/null +++ b/rtc/src/models/Interaction.ts @@ -0,0 +1,32 @@ +import { AllowedMentions, Embed } from "./Message"; + +export interface Interaction { + id: string; + type: InteractionType; + data?: {}; + guild_id: string; + channel_id: string; + member_id: string; + token: string; + version: number; +} + +export enum InteractionType { + Ping = 1, + ApplicationCommand = 2, +} + +export enum InteractionResponseType { + Pong = 1, + Acknowledge = 2, + ChannelMessage = 3, + ChannelMessageWithSource = 4, + AcknowledgeWithSource = 5, +} + +export interface InteractionApplicationCommandCallbackData { + tts?: boolean; + content: string; + embeds?: Embed[]; + allowed_mentions?: AllowedMentions; +} diff --git a/rtc/src/models/Invite.ts b/rtc/src/models/Invite.ts new file mode 100644 index 00000000..01f12003 --- /dev/null +++ b/rtc/src/models/Invite.ts @@ -0,0 +1,95 @@ +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/rtc/src/models/Member.ts b/rtc/src/models/Member.ts new file mode 100644 index 00000000..d1c9ad9b --- /dev/null +++ b/rtc/src/models/Member.ts @@ -0,0 +1,109 @@ +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/rtc/src/models/Message.ts b/rtc/src/models/Message.ts new file mode 100644 index 00000000..15a6f40d --- /dev/null +++ b/rtc/src/models/Message.ts @@ -0,0 +1,368 @@ +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/rtc/src/models/RateLimit.ts b/rtc/src/models/RateLimit.ts new file mode 100644 index 00000000..6a0e1ffd --- /dev/null +++ b/rtc/src/models/RateLimit.ts @@ -0,0 +1,25 @@ +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/rtc/src/models/ReadState.ts b/rtc/src/models/ReadState.ts new file mode 100644 index 00000000..9c4fb323 --- /dev/null +++ b/rtc/src/models/ReadState.ts @@ -0,0 +1,26 @@ +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/rtc/src/models/Role.ts b/rtc/src/models/Role.ts new file mode 100644 index 00000000..c1111c84 --- /dev/null +++ b/rtc/src/models/Role.ts @@ -0,0 +1,42 @@ +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/rtc/src/models/Status.ts b/rtc/src/models/Status.ts new file mode 100644 index 00000000..5a9bf2ca --- /dev/null +++ b/rtc/src/models/Status.ts @@ -0,0 +1,13 @@ +export type Status = "idle" | "dnd" | "online" | "offline"; + +export interface ClientStatus { + desktop?: string; // e.g. Windows/Linux/Mac + 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/rtc/src/models/Team.ts b/rtc/src/models/Team.ts new file mode 100644 index 00000000..795c82d2 --- /dev/null +++ b/rtc/src/models/Team.ts @@ -0,0 +1,17 @@ +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/rtc/src/models/Template.ts b/rtc/src/models/Template.ts new file mode 100644 index 00000000..ad0f9104 --- /dev/null +++ b/rtc/src/models/Template.ts @@ -0,0 +1,51 @@ +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/rtc/src/models/User.ts b/rtc/src/models/User.ts new file mode 100644 index 00000000..c667e954 --- /dev/null +++ b/rtc/src/models/User.ts @@ -0,0 +1,252 @@ +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/rtc/src/models/VoiceState.ts b/rtc/src/models/VoiceState.ts new file mode 100644 index 00000000..c1f90edd --- /dev/null +++ b/rtc/src/models/VoiceState.ts @@ -0,0 +1,34 @@ +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/rtc/src/models/Webhook.ts b/rtc/src/models/Webhook.ts new file mode 100644 index 00000000..7379e98f --- /dev/null +++ b/rtc/src/models/Webhook.ts @@ -0,0 +1,84 @@ +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/rtc/src/models/index.ts b/rtc/src/models/index.ts new file mode 100644 index 00000000..d0a46bf9 --- /dev/null +++ b/rtc/src/models/index.ts @@ -0,0 +1,89 @@ +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 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/rtc/src/mongoStub.cpp b/rtc/src/mongoStub.cpp new file mode 100644 index 00000000..ccd2abda --- /dev/null +++ b/rtc/src/mongoStub.cpp @@ -0,0 +1,84 @@ +#include "mongoStub.hpp" + +mongoStub::mongoStub() { + if (this->client) { + this->db = client["fosscord"]; + + if (this->db) { + this->col = db["events"]; + + } else { + std::cout << "db not found"; + exit(-1); + } + } else { + std::cout << "Client couldn't be initialized"; + exit(-1); + } +} + +// Too slow for my liking +std::vector<mongoStub::mongoMessage> mongoStub::getNewMessages( + mongocxx::change_stream* colCs) { + std::vector<mongoStub::mongoMessage> retVec; + + for (auto&& event : *colCs) { + mongoStub::mongoMessage returnValue; + + std::cout << bsoncxx::to_json(event) << std::endl; + + // Only listen to insert events (to avoid "precondition failed: data" + // exception) + if (event["operationType"].get_utf8().value.to_string() != "insert") { + continue; + } + + std::string evName = event["fullDocument"]["event"].get_utf8().value.to_string(); + + if(evName.substr(0, 7)=="VSERVER"){ continue; } //Ignore the event if it's been emited by a voice server + + if (evName == "UDP_CONNECTION") { + handleUdpRequest( + event["fullDocument"]["data"]["d"]["address"].get_utf8().value.to_string(), + event["fullDocument"]["data"]["d"]["port"].get_int32().value, + event["fullDocument"]["data"]["d"]["mode"].get_utf8().value.to_string() + ); + + } else if (evName == "VOICE_REQUEST") { + //TODO + continue; + } + + returnValue.eventName = evName; + retVec.push_back(returnValue); + } + + return retVec; +} + + +void mongoStub::handleUdpRequest(std::string address, int port, std::string mode) { + using bsoncxx::builder::basic::kvp; + using bsoncxx::builder::basic::sub_array; + using bsoncxx::builder::basic::sub_document; + + auto builder = bsoncxx::builder::basic::document{}; + + //Handle UDP socket stuff (later tho) + + builder.append(kvp("event", "VSERVER_UDP_RESPONSE")); + builder.append(kvp("op", "4")); + builder.append(kvp("d", [](sub_document subdoc) { + subdoc.append(kvp("mode", "CRYPT_MODE")), + subdoc.append(kvp("secret_key", [](sub_array subarr) { + subarr.append(1, 2, 3, 5); // HOW DO I GEN A SKEY? + })); + })); + + + bsoncxx::stdx::optional<mongocxx::result::insert_one> r= col.insert_one(builder.view()); +} + +void mongoStub::handleVoiceRequest() { + //Is this really needed? idk +} \ No newline at end of file diff --git a/rtc/src/mongoStub.hpp b/rtc/src/mongoStub.hpp new file mode 100644 index 00000000..2809142f --- /dev/null +++ b/rtc/src/mongoStub.hpp @@ -0,0 +1,41 @@ +#ifndef MONGOSTUB_HPP +#define MONGOSTUB_HPP + +#include <boost/utility.hpp> +#include <cstdint> +#include <iostream> +#include <vector> +#include <mongocxx/client.hpp> +#include <mongocxx/instance.hpp> +#include <mongocxx/change_stream.hpp> +#include <bsoncxx/json.hpp> +#include <bsoncxx/document/element.hpp> + + +class mongoStub{ + public: + mongoStub(); + + struct mongoMessage{ + std::string eventName; + std::vector<std::string> data; + }; + + std::vector<mongoMessage> getNewMessages(mongocxx::change_stream* colCs); + + mongocxx::collection getCol() const { return col; } + + + + private: + mongocxx::instance instance; + mongocxx::client client{mongocxx::uri{}}; + mongocxx::database db; + mongocxx::collection col; + mongocxx::change_stream* colCs = nullptr; + + void handleUdpRequest(std::string address, int port, std::string mode); + void handleVoiceRequest(); +}; + +#endif diff --git a/rtc/src/rtcPeerHandler.cpp b/rtc/src/rtcPeerHandler.cpp new file mode 100644 index 00000000..9bfc6466 --- /dev/null +++ b/rtc/src/rtcPeerHandler.cpp @@ -0,0 +1,83 @@ +#include "rtcPeerHandler.hpp" + +rtcPeerHandler::rtcPeerHandler() { + rtc::InitLogger(rtc::LogLevel::Verbose, NULL); +} + +void rtcPeerHandler::initiateConnection(std::string peerIP, int peerPort) { + // Socket connection between client and server + SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0); + sockaddr_in addr; + addr.sin_addr.s_addr = inet_addr(peerIP.c_str()); + addr.sin_port = htons(peerPort); + addr.sin_family = AF_INET; + + rtc::Configuration conf; + conf.enableIceTcp = false; + conf.disableAutoNegotiation = false; + + auto pc = std::make_shared<rtc::PeerConnection>(conf); + + rtc::Description::Audio media("audio", + rtc::Description::Direction::SendRecv); + media.addOpusCodec(96); + media.setBitrate(64); + + auto track = pc->addTrack(media); + + // auto session = std::make_shared<rtc::MediaHandler>(); + + // track->setMediaHandler(session); + + rtc::Reliability rtcRel; + rtcRel.unordered = true; + rtcRel.type = rtc::Reliability::Type::Timed; + rtcRel.rexmit = 500; + + rtc::DataChannelInit rtcConf; + rtcConf.reliability = rtcRel; + rtcConf.negotiated = false; + + pc->onStateChange([](rtc::PeerConnection::State state) { + std::cout << "State: " << state << std::endl; + if (state == rtc::PeerConnection::State::Disconnected || + state == rtc::PeerConnection::State::Failed || + state == rtc::PeerConnection::State::Closed) { + // remove disconnected client + } + }); + + pc->onGatheringStateChange([](rtc::PeerConnection::GatheringState state) { + std::cout << "Gathering State: " << state << std::endl; + }); + + /*std::tuple<rtc::Track*, rtc::RtcpSrReporter*> addAudio( + + const std::shared_ptr<rtc::PeerConnection> pc, + const uint8_t payloadType, const uint32_t ssrc, const std::string cname, + const std::string msid, const std::function<void(void)> onOpen) { + auto audio = Description::Audio(cname); + audio.addOpusCodec(payloadType); + audio.addSSRC(ssrc, cname, msid, cname); + auto track = pc->addTrack(audio); + // create RTP configuration + auto rtpConfig = make_shared<RtpPacketizationConfig>( + ssrc, cname, payloadType, OpusRtpPacketizer::defaultClockRate); + // create packetizer + auto packetizer = make_shared<OpusRtpPacketizer>(rtpConfig); + // create opus handler + auto opusHandler = make_shared<OpusPacketizationHandler>(packetizer); + + // add RTCP SR handler + auto srReporter = make_shared<RtcpSrReporter>(rtpConfig); + opusHandler->addToChain(srReporter); + + // set handler + track->setMediaHandler(opusHandler); + track->onOpen(onOpen); + auto trackData = make_shared<ClientTrackData>(track, srReporter); + return trackData; + }*/ + + pc->createDataChannel("Fosscord voice connection", rtcConf); +} diff --git a/rtc/src/rtcPeerHandler.hpp b/rtc/src/rtcPeerHandler.hpp new file mode 100644 index 00000000..3ba32a83 --- /dev/null +++ b/rtc/src/rtcPeerHandler.hpp @@ -0,0 +1,32 @@ +#include "libdatachannel/rtc.hpp" +#include <iostream> +#include <memory> +#include "nlohmann/json.hpp" +#include <array> + +#ifdef _WIN32 +#include <winsock2.h> +#else +#include <arpa/inet.h> +typedef int SOCKET; +#endif + +using json = nlohmann::json; + +#ifndef RTCPEERHANDLER +#define RTCPEERHANDLER +class rtcPeerHandler{ +public: + rtcPeerHandler(); + void initiateConnection(std::string peerIP, int peerPort); + + struct client + { + std::shared_ptr<rtc::PeerConnection> pc; + std::shared_ptr<rtc::DataChannel> dc; + }; + +private: + std::map<SOCKET, client> clients; +}; +#endif \ No newline at end of file diff --git a/rtc/src/rtcServer.hpp b/rtc/src/rtcServer.hpp new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/rtc/src/rtcServer.hpp diff --git a/rtc/src/util/BitField.ts b/rtc/src/util/BitField.ts new file mode 100644 index 00000000..728dc632 --- /dev/null +++ b/rtc/src/util/BitField.ts @@ -0,0 +1,143 @@ +"use strict"; + +// https://github.com/discordjs/discord.js/blob/master/src/util/BitField.js +// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah + +export type BitFieldResolvable = number | BigInt | BitField | string | BitFieldResolvable[]; + +/** + * Data structure that makes it easy to interact with a bitfield. + */ +export class BitField { + public bitfield: bigint = BigInt(0); + + public static FLAGS: Record<string, bigint> = {}; + + constructor(bits: BitFieldResolvable = 0) { + this.bitfield = BitField.resolve.call(this, bits); + } + + /** + * Checks whether the bitfield has a bit, or any of multiple bits. + */ + any(bit: BitFieldResolvable): boolean { + return (this.bitfield & BitField.resolve.call(this, bit)) !== 0n; + } + + /** + * Checks if this bitfield equals another + */ + equals(bit: BitFieldResolvable): boolean { + return this.bitfield === BitField.resolve.call(this, bit); + } + + /** + * Checks whether the bitfield has a bit, or multiple bits. + */ + has(bit: BitFieldResolvable): boolean { + if (Array.isArray(bit)) return bit.every((p) => this.has(p)); + const BIT = BitField.resolve.call(this, bit); + return (this.bitfield & BIT) === BIT; + } + + /** + * Gets all given bits that are missing from the bitfield. + */ + missing(bits: BitFieldResolvable) { + if (!Array.isArray(bits)) bits = new BitField(bits).toArray(); + return bits.filter((p) => !this.has(p)); + } + + /** + * Freezes these bits, making them immutable. + */ + freeze(): Readonly<BitField> { + return Object.freeze(this); + } + + /** + * Adds bits to these ones. + * @param {...BitFieldResolvable} [bits] Bits to add + * @returns {BitField} These bits or new BitField if the instance is frozen. + */ + add(...bits: BitFieldResolvable[]): BitField { + let total = 0n; + for (const bit of bits) { + total |= BitField.resolve.call(this, bit); + } + if (Object.isFrozen(this)) return new BitField(this.bitfield | total); + this.bitfield |= total; + return this; + } + + /** + * Removes bits from these. + * @param {...BitFieldResolvable} [bits] Bits to remove + */ + remove(...bits: BitFieldResolvable[]) { + let total = 0n; + for (const bit of bits) { + total |= BitField.resolve.call(this, bit); + } + if (Object.isFrozen(this)) return new BitField(this.bitfield & ~total); + this.bitfield &= ~total; + return this; + } + + /** + * Gets an object mapping field names to a {@link boolean} indicating whether the + * bit is available. + * @param {...*} hasParams Additional parameters for the has method, if any + */ + serialize() { + const serialized: Record<string, boolean> = {}; + for (const [flag, bit] of Object.entries(BitField.FLAGS)) serialized[flag] = this.has(bit); + return serialized; + } + + /** + * Gets an {@link Array} of bitfield names based on the bits available. + */ + toArray(): string[] { + return Object.keys(BitField.FLAGS).filter((bit) => this.has(bit)); + } + + toJSON() { + return this.bitfield; + } + + valueOf() { + return this.bitfield; + } + + *[Symbol.iterator]() { + yield* this.toArray(); + } + + /** + * Data that can be resolved to give a bitfield. This can be: + * * A bit number (this can be a number literal or a value taken from {@link BitField.FLAGS}) + * * An instance of BitField + * * An Array of BitFieldResolvable + * @typedef {number|BitField|BitFieldResolvable[]} BitFieldResolvable + */ + + /** + * Resolves bitfields to their numeric form. + * @param {BitFieldResolvable} [bit=0] - bit(s) to resolve + * @returns {number} + */ + static resolve(bit: BitFieldResolvable = 0n): bigint { + // @ts-ignore + const FLAGS = this.FLAGS || this.constructor?.FLAGS; + if ((typeof bit === "number" || typeof bit === "bigint") && bit >= 0n) return BigInt(bit); + if (bit instanceof BitField) return bit.bitfield; + if (Array.isArray(bit)) { + // @ts-ignore + const resolve = this.constructor?.resolve || this.resolve; + return bit.map((p) => resolve.call(this, p)).reduce((prev, p) => BigInt(prev) | BigInt(p), 0n); + } + if (typeof bit === "string" && typeof FLAGS[bit] !== "undefined") return FLAGS[bit]; + throw new RangeError("BITFIELD_INVALID: " + bit); + } +} diff --git a/rtc/src/util/Config.ts b/rtc/src/util/Config.ts new file mode 100644 index 00000000..78b44315 --- /dev/null +++ b/rtc/src/util/Config.ts @@ -0,0 +1,284 @@ +import { Schema, model, Types, Document } from "mongoose"; +import "missing-native-js-functions"; +import db, { MongooseCache } from "./Database"; +import { Snowflake } from "./Snowflake"; +import crypto from "crypto"; + +var config: any; + +export default { + init: async function init(defaultOpts: any = DefaultOptions) { + config = await db.collection("config").findOne({}); + return this.set((config || {}).merge(defaultOpts)); + }, + get: function get() { + return config as DefaultOptions; + }, + set: function set(val: any) { + return db.collection("config").updateOne({}, { $set: val }, { upsert: true }); + }, +}; + +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: { + endpoint: string | null; + }; + cdn: { + 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: { + 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: { + endpoint: null, + }, + cdn: { + 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: 5, + }, + channel: { + count: 5, + window: 5, + }, + auth: { + login: { + count: 5, + window: 60, + }, + register: { + count: 2, + window: 60 * 60 * 12, + }, + }, + }, + }, + }, + security: { + 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/rtc/src/util/Constants.ts b/rtc/src/util/Constants.ts new file mode 100644 index 00000000..a9978c51 --- /dev/null +++ b/rtc/src/util/Constants.ts @@ -0,0 +1,28 @@ +import { VerifyOptions } from "jsonwebtoken"; + +export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] }; + +export enum MessageType { + DEFAULT = 0, + RECIPIENT_ADD = 1, + RECIPIENT_REMOVE = 2, + CALL = 3, + CHANNEL_NAME_CHANGE = 4, + CHANNEL_ICON_CHANGE = 5, + CHANNEL_PINNED_MESSAGE = 6, + GUILD_MEMBER_JOIN = 7, + USER_PREMIUM_GUILD_SUBSCRIPTION = 8, + USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 = 9, + USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 = 10, + USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 = 11, + CHANNEL_FOLLOW_ADD = 12, + GUILD_DISCOVERY_DISQUALIFIED = 14, + GUILD_DISCOVERY_REQUALIFIED = 15, + GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING = 16, + GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING = 17, + THREAD_CREATED = 18, + REPLY = 19, + APPLICATION_COMMAND = 20, + THREAD_STARTER_MESSAGE = 21, + GUILD_INVITE_REMINDER = 22, +} diff --git a/rtc/src/util/Database.ts b/rtc/src/util/Database.ts new file mode 100644 index 00000000..8c6847a8 --- /dev/null +++ b/rtc/src/util/Database.ts @@ -0,0 +1,151 @@ +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"; + +const url = new URL(uri.replace("mongodb://", "http://")); + +const connection = mongoose.createConnection(uri, { + autoIndex: true, + useNewUrlParser: true, + useUnifiedTopology: true, + useFindAndModify: false, +}); +console.log(`[Database] connect: mongodb://${url.username}@${url.host}${url.pathname}${url.search}`); + +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> { + // @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; +} + +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; + + return this.stream.close(); + }; +} diff --git a/rtc/src/util/Intents.ts b/rtc/src/util/Intents.ts new file mode 100644 index 00000000..943b29cf --- /dev/null +++ b/rtc/src/util/Intents.ts @@ -0,0 +1,21 @@ +import { BitField } from "./BitField"; + +export class Intents extends BitField { + static FLAGS = { + GUILDS: BigInt(1) << BigInt(0), + GUILD_MEMBERS: BigInt(1) << BigInt(1), + GUILD_BANS: BigInt(1) << BigInt(2), + GUILD_EMOJIS: BigInt(1) << BigInt(3), + GUILD_INTEGRATIONS: BigInt(1) << BigInt(4), + GUILD_WEBHOOKS: BigInt(1) << BigInt(5), + GUILD_INVITES: BigInt(1) << BigInt(6), + GUILD_VOICE_STATES: BigInt(1) << BigInt(7), + GUILD_PRESENCES: BigInt(1) << BigInt(8), + GUILD_MESSAGES: BigInt(1) << BigInt(9), + GUILD_MESSAGE_REACTIONS: BigInt(1) << BigInt(10), + GUILD_MESSAGE_TYPING: BigInt(1) << BigInt(11), + DIRECT_MESSAGES: BigInt(1) << BigInt(12), + DIRECT_MESSAGE_REACTIONS: BigInt(1) << BigInt(13), + DIRECT_MESSAGE_TYPING: BigInt(1) << BigInt(14), + }; +} diff --git a/rtc/src/util/MessageFlags.ts b/rtc/src/util/MessageFlags.ts new file mode 100644 index 00000000..c76be4c8 --- /dev/null +++ b/rtc/src/util/MessageFlags.ts @@ -0,0 +1,14 @@ +// https://github.com/discordjs/discord.js/blob/master/src/util/MessageFlags.js +// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah + +import { BitField } from "./BitField"; + +export class MessageFlags extends BitField { + static FLAGS = { + CROSSPOSTED: BigInt(1) << BigInt(0), + IS_CROSSPOST: BigInt(1) << BigInt(1), + SUPPRESS_EMBEDS: BigInt(1) << BigInt(2), + SOURCE_MESSAGE_DELETED: BigInt(1) << BigInt(3), + URGENT: BigInt(1) << BigInt(4), + }; +} diff --git a/rtc/src/util/MongoBigInt.ts b/rtc/src/util/MongoBigInt.ts new file mode 100644 index 00000000..fc451925 --- /dev/null +++ b/rtc/src/util/MongoBigInt.ts @@ -0,0 +1,82 @@ +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); + 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/rtc/src/util/Permissions.ts b/rtc/src/util/Permissions.ts new file mode 100644 index 00000000..445e901f --- /dev/null +++ b/rtc/src/util/Permissions.ts @@ -0,0 +1,262 @@ +// https://github.com/discordjs/discord.js/blob/master/src/util/Permissions.js +// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah +import { MemberDocument, MemberModel } from "../models/Member"; +import { ChannelDocument, ChannelModel } from "../models/Channel"; +import { ChannelPermissionOverwrite } from "../models/Channel"; +import { Role, RoleDocument, RoleModel } from "../models/Role"; +import { BitField } from "./BitField"; +import { GuildDocument, GuildModel } from "../models/Guild"; +// TODO: check role hierarchy permission + +var HTTPError: any; + +try { + HTTPError = require("lambert-server").HTTPError; +} catch (e) { + HTTPError = Error; +} + +export type PermissionResolvable = bigint | number | Permissions | PermissionResolvable[] | PermissionString; + +type PermissionString = + | "CREATE_INSTANT_INVITE" + | "KICK_MEMBERS" + | "BAN_MEMBERS" + | "ADMINISTRATOR" + | "MANAGE_CHANNELS" + | "MANAGE_GUILD" + | "ADD_REACTIONS" + | "VIEW_AUDIT_LOG" + | "PRIORITY_SPEAKER" + | "STREAM" + | "VIEW_CHANNEL" + | "SEND_MESSAGES" + | "SEND_TTS_MESSAGES" + | "MANAGE_MESSAGES" + | "EMBED_LINKS" + | "ATTACH_FILES" + | "READ_MESSAGE_HISTORY" + | "MENTION_EVERYONE" + | "USE_EXTERNAL_EMOJIS" + | "VIEW_GUILD_INSIGHTS" + | "CONNECT" + | "SPEAK" + | "MUTE_MEMBERS" + | "DEAFEN_MEMBERS" + | "MOVE_MEMBERS" + | "USE_VAD" + | "CHANGE_NICKNAME" + | "MANAGE_NICKNAMES" + | "MANAGE_ROLES" + | "MANAGE_WEBHOOKS" + | "MANAGE_EMOJIS"; + +const CUSTOM_PERMISSION_OFFSET = BigInt(1) << BigInt(48); // 16 free custom permission bits, and 16 for discord to add new ones + +export class Permissions extends BitField { + cache: PermissionCache = {}; + + static FLAGS = { + CREATE_INSTANT_INVITE: BigInt(1) << BigInt(0), + KICK_MEMBERS: BigInt(1) << BigInt(1), + BAN_MEMBERS: BigInt(1) << BigInt(2), + ADMINISTRATOR: BigInt(1) << BigInt(3), + MANAGE_CHANNELS: BigInt(1) << BigInt(4), + MANAGE_GUILD: BigInt(1) << BigInt(5), + ADD_REACTIONS: BigInt(1) << BigInt(6), + VIEW_AUDIT_LOG: BigInt(1) << BigInt(7), + PRIORITY_SPEAKER: BigInt(1) << BigInt(8), + STREAM: BigInt(1) << BigInt(9), + VIEW_CHANNEL: BigInt(1) << BigInt(10), + SEND_MESSAGES: BigInt(1) << BigInt(11), + SEND_TTS_MESSAGES: BigInt(1) << BigInt(12), + MANAGE_MESSAGES: BigInt(1) << BigInt(13), + EMBED_LINKS: BigInt(1) << BigInt(14), + ATTACH_FILES: BigInt(1) << BigInt(15), + READ_MESSAGE_HISTORY: BigInt(1) << BigInt(16), + MENTION_EVERYONE: BigInt(1) << BigInt(17), + USE_EXTERNAL_EMOJIS: BigInt(1) << BigInt(18), + VIEW_GUILD_INSIGHTS: BigInt(1) << BigInt(19), + CONNECT: BigInt(1) << BigInt(20), + SPEAK: BigInt(1) << BigInt(21), + MUTE_MEMBERS: BigInt(1) << BigInt(22), + DEAFEN_MEMBERS: BigInt(1) << BigInt(23), + MOVE_MEMBERS: BigInt(1) << BigInt(24), + USE_VAD: BigInt(1) << BigInt(25), + CHANGE_NICKNAME: BigInt(1) << BigInt(26), + MANAGE_NICKNAMES: BigInt(1) << BigInt(27), + MANAGE_ROLES: BigInt(1) << BigInt(28), + MANAGE_WEBHOOKS: BigInt(1) << BigInt(29), + MANAGE_EMOJIS: BigInt(1) << BigInt(30), + /** + * CUSTOM PERMISSIONS ideas: + * - allow user to dm members + * - allow user to pin messages (without MANAGE_MESSAGES) + * - allow user to publish messages (without MANAGE_MESSAGES) + */ + // CUSTOM_PERMISSION: BigInt(1) << BigInt(0) + CUSTOM_PERMISSION_OFFSET + }; + + any(permission: PermissionResolvable, checkAdmin = true) { + return (checkAdmin && super.any(Permissions.FLAGS.ADMINISTRATOR)) || super.any(permission); + } + + /** + * Checks whether the bitfield has a permission, or multiple permissions. + */ + has(permission: PermissionResolvable, checkAdmin = true) { + return (checkAdmin && super.has(Permissions.FLAGS.ADMINISTRATOR)) || super.has(permission); + } + + /** + * Checks whether the bitfield has a permission, or multiple permissions, but throws an Error if user fails to match auth criteria. + */ + hasThrow(permission: PermissionResolvable) { + if (this.has(permission) && this.has("VIEW_CHANNEL")) return true; + // @ts-ignore + throw new HTTPError(`You are missing the following permissions ${permission}`, 403); + } + + overwriteChannel(overwrites: ChannelPermissionOverwrite[]) { + if (!this.cache) throw new Error("permission chache not available"); + overwrites = overwrites.filter((x) => { + if (x.type === 0 && this.cache.roles?.some((r) => r.id === x.id)) return true; + if (x.type === 1 && x.id == this.cache.user_id) return true; + return false; + }); + return new Permissions(Permissions.channelPermission(overwrites, this.bitfield)); + } + + static channelPermission(overwrites: ChannelPermissionOverwrite[], init?: bigint) { + // TODO: do not deny any permissions if admin + return overwrites.reduce((permission, overwrite) => { + // apply disallowed permission + // * permission: current calculated permission (e.g. 010) + // * deny contains all denied permissions (e.g. 011) + // * allow contains all explicitly allowed permisions (e.g. 100) + return (permission & ~BigInt(overwrite.deny)) | BigInt(overwrite.allow); + // ~ operator inverts deny (e.g. 011 -> 100) + // & operator only allows 1 for both ~deny and permission (e.g. 010 & 100 -> 000) + // | operators adds both together (e.g. 000 + 100 -> 100) + }, init || 0n); + } + + static rolePermission(roles: Role[]) { + // adds all permissions of all roles together (Bit OR) + return roles.reduce((permission, role) => permission | BigInt(role.permissions), 0n); + } + + static finalPermission({ + user, + guild, + channel, + }: { + user: { id: string; roles: string[] }; + guild: { roles: Role[] }; + channel?: { + overwrites?: ChannelPermissionOverwrite[]; + recipient_ids?: string[] | null; + owner_id?: string; + }; + }) { + if (user.id === "0") return new Permissions("ADMINISTRATOR"); // system user id + + let roles = guild.roles.filter((x) => user.roles.includes(x.id)); + let permission = Permissions.rolePermission(roles); + + if (channel?.overwrites) { + let overwrites = channel.overwrites.filter((x) => { + if (x.type === 0 && user.roles.includes(x.id)) return true; + if (x.type === 1 && x.id == user.id) return true; + return false; + }); + permission = Permissions.channelPermission(overwrites, permission); + } + + if (channel?.recipient_ids) { + if (channel?.owner_id === user.id) return new Permissions("ADMINISTRATOR"); + if (channel.recipient_ids.includes(user.id)) { + // Default dm permissions + return new Permissions([ + "VIEW_CHANNEL", + "SEND_MESSAGES", + "STREAM", + "ADD_REACTIONS", + "EMBED_LINKS", + "ATTACH_FILES", + "READ_MESSAGE_HISTORY", + "MENTION_EVERYONE", + "USE_EXTERNAL_EMOJIS", + "CONNECT", + "SPEAK", + "MANAGE_CHANNELS", + ]); + } + + return new Permissions(); + } + + return new Permissions(permission); + } +} + +export type PermissionCache = { + channel?: ChannelDocument | null; + member?: MemberDocument | null; + guild?: GuildDocument | null; + roles?: RoleDocument[] | null; + user_id?: string; +}; + +export async function getPermission( + user_id?: string, + guild_id?: string, + channel_id?: string, + cache: PermissionCache = {} +) { + var { channel, member, guild, roles } = cache; + + if (!user_id) throw new HTTPError("User not found"); + + if (channel_id && !channel) { + channel = await ChannelModel.findOne( + { id: channel_id }, + { permission_overwrites: true, recipient_ids: true, owner_id: true, guild_id: true } + ).exec(); + if (!channel) throw new HTTPError("Channel not found", 404); + if (channel.guild_id) guild_id = channel.guild_id; + } + + if (guild_id) { + if (!guild) guild = await GuildModel.findOne({ id: guild_id }, { owner_id: true }).exec(); + if (!guild) throw new HTTPError("Guild not found"); + if (guild.owner_id === user_id) return new Permissions(Permissions.FLAGS.ADMINISTRATOR); + + if (!member) member = await MemberModel.findOne({ guild_id, id: user_id }, "roles").exec(); + if (!member) throw new HTTPError("Member not found"); + + if (!roles) roles = await RoleModel.find({ guild_id, id: { $in: member.roles } }).exec(); + } + + var permission = Permissions.finalPermission({ + user: { + id: user_id, + roles: member?.roles || [], + }, + guild: { + roles: roles || [], + }, + channel: { + overwrites: channel?.permission_overwrites, + owner_id: channel?.owner_id, + recipient_ids: channel?.recipient_ids, + }, + }); + + const obj = new Permissions(permission); + + // pass cache to permission for possible future getPermission calls + obj.cache = { guild, member, channel, roles, user_id }; + + return obj; +} diff --git a/rtc/src/util/RabbitMQ.ts b/rtc/src/util/RabbitMQ.ts new file mode 100644 index 00000000..9da41990 --- /dev/null +++ b/rtc/src/util/RabbitMQ.ts @@ -0,0 +1,18 @@ +import amqp, { Connection, Channel } from "amqplib"; +import Config from "./Config"; + +export const RabbitMQ: { connection: Connection | null; channel: Channel | null; init: () => Promise<void> } = { + connection: null, + channel: null, + init: async function () { + const host = Config.get().rabbitmq.host; + if (!host) return; + console.log(`[RabbitMQ] connect: ${host}`); + this.connection = await amqp.connect(host, { + timeout: 1000 * 60, + }); + console.log(`[RabbitMQ] connected`); + this.channel = await this.connection.createChannel(); + console.log(`[RabbitMQ] channel created`); + }, +}; diff --git a/rtc/src/util/Regex.ts b/rtc/src/util/Regex.ts new file mode 100644 index 00000000..bbd48bca --- /dev/null +++ b/rtc/src/util/Regex.ts @@ -0,0 +1,3 @@ +export const DOUBLE_WHITE_SPACE = /\s\s+/g; +export const SPECIAL_CHAR = /[@#`:\r\n\t\f\v\p{C}]/gu; +export const CHANNEL_MENTION = /<#(\d+)>/g; diff --git a/rtc/src/util/Snowflake.ts b/rtc/src/util/Snowflake.ts new file mode 100644 index 00000000..1d725710 --- /dev/null +++ b/rtc/src/util/Snowflake.ts @@ -0,0 +1,127 @@ +// @ts-nocheck +import cluster from "cluster"; + +// https://github.com/discordjs/discord.js/blob/master/src/util/Snowflake.js +// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah +("use strict"); + +// Discord epoch (2015-01-01T00:00:00.000Z) + +/** + * A container for useful snowflake-related methods. + */ +export class Snowflake { + static readonly EPOCH = 1420070400000; + static INCREMENT = 0n; // max 4095 + static processId = BigInt(process.pid % 31); // max 31 + static workerId = BigInt((cluster.worker?.id || 0) % 31); // max 31 + + constructor() { + throw new Error(`The ${this.constructor.name} class may not be instantiated.`); + } + + /** + * A Twitter snowflake, except the epoch is 2015-01-01T00:00:00.000Z + * ``` + * If we have a snowflake '266241948824764416' we can represent it as binary: + * + * 64 22 17 12 0 + * 000000111011000111100001101001000101000000 00001 00000 000000000000 + * number of ms since Discord epoch worker pid increment + * ``` + * @typedef {string} Snowflake + */ + + /** + * Transforms a snowflake from a decimal string to a bit string. + * @param {Snowflake} num Snowflake to be transformed + * @returns {string} + * @private + */ + static idToBinary(num) { + let bin = ""; + let high = parseInt(num.slice(0, -10)) || 0; + let low = parseInt(num.slice(-10)); + while (low > 0 || high > 0) { + bin = String(low & 1) + bin; + low = Math.floor(low / 2); + if (high > 0) { + low += 5000000000 * (high % 2); + high = Math.floor(high / 2); + } + } + return bin; + } + + /** + * Transforms a snowflake from a bit string to a decimal string. + * @param {string} num Bit string to be transformed + * @returns {Snowflake} + * @private + */ + static binaryToID(num) { + let dec = ""; + + while (num.length > 50) { + const high = parseInt(num.slice(0, -32), 2); + const low = parseInt((high % 10).toString(2) + num.slice(-32), 2); + + dec = (low % 10).toString() + dec; + num = + Math.floor(high / 10).toString(2) + + Math.floor(low / 10) + .toString(2) + .padStart(32, "0"); + } + + num = parseInt(num, 2); + while (num > 0) { + dec = (num % 10).toString() + dec; + num = Math.floor(num / 10); + } + + return dec; + } + + static generate() { + var time = BigInt(Date.now() - Snowflake.EPOCH) << 22n; + var worker = Snowflake.workerId << 17n; + var process = Snowflake.processId << 12n; + var increment = Snowflake.INCREMENT++; + return (time | worker | process | increment).toString(); + } + + /** + * A deconstructed snowflake. + * @typedef {Object} DeconstructedSnowflake + * @property {number} timestamp Timestamp the snowflake was created + * @property {Date} date Date the snowflake was created + * @property {number} workerID Worker ID in the snowflake + * @property {number} processID Process ID in the snowflake + * @property {number} increment Increment in the snowflake + * @property {string} binary Binary representation of the snowflake + */ + + /** + * Deconstructs a Discord snowflake. + * @param {Snowflake} snowflake Snowflake to deconstruct + * @returns {DeconstructedSnowflake} Deconstructed snowflake + */ + static deconstruct(snowflake) { + const BINARY = Snowflake.idToBinary(snowflake).toString(2).padStart(64, "0"); + const res = { + timestamp: parseInt(BINARY.substring(0, 42), 2) + Snowflake.EPOCH, + workerID: parseInt(BINARY.substring(42, 47), 2), + processID: parseInt(BINARY.substring(47, 52), 2), + increment: parseInt(BINARY.substring(52, 64), 2), + binary: BINARY, + }; + Object.defineProperty(res, "date", { + get: function get() { + return new Date(this.timestamp); + }, + enumerable: true, + }); + return res; + } +} diff --git a/rtc/src/util/String.ts b/rtc/src/util/String.ts new file mode 100644 index 00000000..55f11e8d --- /dev/null +++ b/rtc/src/util/String.ts @@ -0,0 +1,7 @@ +import { SPECIAL_CHAR } from "./Regex"; + +export function trimSpecial(str?: string): string { + // @ts-ignore + if (!str) return; + return str.replace(SPECIAL_CHAR, "").trim(); +} diff --git a/rtc/src/util/UserFlags.ts b/rtc/src/util/UserFlags.ts new file mode 100644 index 00000000..72394eff --- /dev/null +++ b/rtc/src/util/UserFlags.ts @@ -0,0 +1,22 @@ +// https://github.com/discordjs/discord.js/blob/master/src/util/UserFlags.js +// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah + +import { BitField } from "./BitField"; + +export class UserFlags extends BitField { + static FLAGS = { + DISCORD_EMPLOYEE: BigInt(1) << BigInt(0), + PARTNERED_SERVER_OWNER: BigInt(1) << BigInt(1), + HYPESQUAD_EVENTS: BigInt(1) << BigInt(2), + BUGHUNTER_LEVEL_1: BigInt(1) << BigInt(3), + HOUSE_BRAVERY: BigInt(1) << BigInt(6), + HOUSE_BRILLIANCE: BigInt(1) << BigInt(7), + HOUSE_BALANCE: BigInt(1) << BigInt(8), + EARLY_SUPPORTER: BigInt(1) << BigInt(9), + TEAM_USER: BigInt(1) << BigInt(10), + SYSTEM: BigInt(1) << BigInt(12), + BUGHUNTER_LEVEL_2: BigInt(1) << BigInt(14), + VERIFIED_BOT: BigInt(1) << BigInt(16), + EARLY_VERIFIED_BOT_DEVELOPER: BigInt(1) << BigInt(17), + }; +} diff --git a/rtc/src/util/checkToken.ts b/rtc/src/util/checkToken.ts new file mode 100644 index 00000000..91bf08d5 --- /dev/null +++ b/rtc/src/util/checkToken.ts @@ -0,0 +1,24 @@ +import { JWTOptions } from "./Constants"; +import jwt from "jsonwebtoken"; +import { UserModel } from "../models"; + +export function checkToken(token: string, jwtSecret: string): Promise<any> { + return new Promise((res, rej) => { + token = token.replace("Bot ", ""); // TODO: proper bot support + jwt.verify(token, jwtSecret, JWTOptions, async (err, decoded: any) => { + if (err || !decoded) return rej("Invalid Token"); + + const user = await UserModel.findOne( + { id: decoded.id }, + { "user_data.valid_tokens_since": true, bot: true, disabled: true, deleted: true } + ).exec(); + if (!user) return rej("Invalid Token"); + // we need to round it to seconds as it saved as seconds in jwt iat and valid_tokens_since is stored in milliseconds + if (decoded.iat * 1000 < user.user_data.valid_tokens_since.setSeconds(0, 0)) return rej("Invalid Token"); + if (user.disabled) return rej("User disabled"); + if (user.deleted) return rej("User not found"); + + return res({ decoded, user }); + }); + }); +} diff --git a/rtc/src/util/index.ts b/rtc/src/util/index.ts new file mode 100644 index 00000000..7523a6ad --- /dev/null +++ b/rtc/src/util/index.ts @@ -0,0 +1,9 @@ +export * from "./String"; +export * from "./BitField"; +export * from "./Intents"; +export * from "./MessageFlags"; +export * from "./Permissions"; +export * from "./Snowflake"; +export * from "./UserFlags"; +export * from "./toBigInt"; +export * from "./RabbitMQ"; diff --git a/rtc/src/util/toBigInt.ts b/rtc/src/util/toBigInt.ts new file mode 100644 index 00000000..d57c4568 --- /dev/null +++ b/rtc/src/util/toBigInt.ts @@ -0,0 +1,3 @@ +export default function toBigInt(string: String): BigInt { + return BigInt(string); +} |