diff --git a/Cargo.toml b/Cargo.toml index d0312a4..53489ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,9 +5,9 @@ license = "AGPL-3" edition = "2021" [dependencies] -tokio = {version = "1.27.0", features = ["rt", "macros", "rt-multi-thread"]} -serde = {version = "1.0.159", features = ["derive"]} -serde_json = "1.0.95" +tokio = {version = "1.28.1", features = ["rt", "macros", "rt-multi-thread", "full"]} +serde = {version = "1.0.162", features = ["derive"]} +serde_json = { version = "1.0.96", features = ["raw_value"] } reqwest = {version = "0.11.16", features = ["multipart"]} url = "2.3.1" chrono = {version = "0.4.24", features = ["serde"]} @@ -17,6 +17,18 @@ native-tls = "0.2.11" tokio-tungstenite = {version = "0.18.0", features = ["native-tls"]} futures-util = "0.3.28" http = "0.2.9" +openssl = "0.10.52" +base64 = "0.21.2" +hostname = "0.3.1" +bitflags = { version = "2.2.1", features = ["serde"] } +atomic = "0.5.3" +bigdecimal = "0.3.1" +num-bigint = "0.4.3" +lazy_static = "1.4.0" +poem = { version = "1.3.55", optional = true } +sqlx = { version = "0.6.3", features = ["mysql", "sqlite", "json", "chrono", "ipnetwork", "runtime-tokio-native-tls", "any"], optional = true } +thiserror = "1.0.40" +jsonwebtoken = "8.3.0" [dev-dependencies] -lazy_static = "1.4.0" \ No newline at end of file +lazy_static = "1.4.0" diff --git a/src/lib.rs b/src/lib.rs index b69e360..aa73302 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ pub mod errors; pub mod gateway; pub mod instance; pub mod limit; +pub mod types; pub mod voice; use url::{ParseError, Url}; diff --git a/src/types/config/mod.rs b/src/types/config/mod.rs new file mode 100644 index 0000000..521d303 --- /dev/null +++ b/src/types/config/mod.rs @@ -0,0 +1,187 @@ +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +pub use crate::{ + types::config::types::{ + api_configuration::ApiConfiguration, cdn_configuration::CdnConfiguration, + defaults_configuration::DefaultsConfiguration, email_configuration::EmailConfiguration, + endpoint_configuration::EndpointConfiguration, + external_tokens_configuration::ExternalTokensConfiguration, + general_configuration::GeneralConfiguration, gif_configuration::GifConfiguration, + guild_configuration::GuildConfiguration, kafka_configuration::KafkaConfiguration, + limit_configuration::LimitsConfiguration, login_configuration::LoginConfiguration, + metrics_configuration::MetricsConfiguration, + password_reset_configuration::PasswordResetConfiguration, + rabbit_mq_configuration::RabbitMQConfiguration, region_configuration::RegionConfiguration, + register_configuration::RegisterConfiguration, + security_configuration::SecurityConfiguration, sentry_configuration::SentryConfiguration, + template_configuration::TemplateConfiguration, + }, + types::entities::ConfigEntity, +}; + +pub mod types; + +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConfigValue { + pub gateway: EndpointConfiguration, + pub cdn: CdnConfiguration, + pub api: ApiConfiguration, + pub general: GeneralConfiguration, + pub limits: LimitsConfiguration, + pub security: SecurityConfiguration, + pub login: LoginConfiguration, + pub register: RegisterConfiguration, + pub regions: RegionConfiguration, + pub guild: GuildConfiguration, + pub gif: GifConfiguration, + pub rabbitmq: RabbitMQConfiguration, + pub kafka: KafkaConfiguration, + pub templates: TemplateConfiguration, + pub metrics: MetricsConfiguration, + pub sentry: SentryConfiguration, + pub defaults: DefaultsConfiguration, + pub external: ExternalTokensConfiguration, + pub email: EmailConfiguration, + pub password_reset: PasswordResetConfiguration, +} + +impl ConfigValue { + pub fn to_pairs(&self) -> Vec { + let v = serde_json::json!(self); + + generate_pairs(&v, "") + } + + pub fn from_pairs(pairs: Vec) -> Self { + pairs_to_config(pairs) + } +} + +fn generate_pairs(obj: &Value, key: &str) -> Vec { + let mut pairs = Vec::new(); + match obj { + Value::Object(map) => { + for (k, v) in map { + let new_key = if key.is_empty() { + k.to_string() + } else { + format!("{}_{}", key, k) + }; + pairs.extend(generate_pairs(v, &new_key)); + } + } + Value::Array(arr) => { + for (i, v) in arr.iter().enumerate() { + let new_key = format!("{}_{}", key, i); + pairs.extend(generate_pairs(v, &new_key)); + } + } + _ => pairs.push(ConfigEntity { + key: key.to_string(), + value: Some(obj.clone()), + }), + } + pairs +} + +fn pairs_to_config(pairs: Vec) -> ConfigValue { + let mut value = Value::Object(Map::new()); + + for p in pairs { + let keys: Vec<&str> = p.key.split('_').collect(); + let mut path = vec![]; + + for (i, &key) in keys.iter().enumerate() { + path.push(key); + + if i == keys.len() - 1 { + insert_into(&mut value, &path, p.value.clone().unwrap_or(Value::Null)); + } else if keys[i + 1].parse::().is_ok() { + if !path_exists(&value, &path) { + insert_into(&mut value, &path, Value::Array(Vec::new())); + } + } else if !path_exists(&value, &path) { + insert_into(&mut value, &path, Value::Object(Map::new())); + } + } + } + + serde_json::from_value(value).unwrap() +} + +fn path_exists(value: &Value, path: &[&str]) -> bool { + let mut current = value; + + for &key in path { + match current { + Value::Object(map) => { + if let Some(v) = map.get(key) { + current = v; + } else { + return false; + } + } + Value::Array(arr) => { + if let Ok(index) = key.parse::() { + if let Some(v) = arr.get(index) { + current = v; + } else { + return false; + } + } else { + return false; + } + } + _ => return false, + } + } + + true +} + +fn insert_into(value: &mut Value, path: &[&str], new_value: Value) { + let last_key = path.last().unwrap(); + let parent_path = &path[0..path.len() - 1]; + + let mut current = value; + + for &key in parent_path { + current = match current { + Value::Object(map) => map.get_mut(key).unwrap(), + Value::Array(arr) => arr.get_mut(key.parse::().unwrap()).unwrap(), + _ => unreachable!(), + }; + } + + match current { + Value::Object(map) => { + map.insert((*last_key).to_string(), new_value); + } + Value::Array(arr) => { + let index = last_key.parse::().unwrap(); + if index >= arr.len() { + arr.resize(index + 1, Value::Null); + } + arr[index] = new_value; + } + _ => unreachable!(), + }; +} + +#[cfg(test)] +mod test { + use crate::types::config::{generate_pairs, pairs_to_config, ConfigValue}; + + #[test] + fn test_pairs() { + let c = ConfigValue::default(); + let v = serde_json::json!(&c); + + let pairs = generate_pairs(&v, ""); + + let cfg = pairs_to_config(pairs); + assert_eq!(cfg, c) + } +} diff --git a/src/types/config/types/api_configuration.rs b/src/types/config/types/api_configuration.rs new file mode 100644 index 0000000..2d617fe --- /dev/null +++ b/src/types/config/types/api_configuration.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ApiConfiguration { + pub default_version: String, + pub active_versions: Vec, + pub endpoint_public: Option, +} + +impl Default for ApiConfiguration { + fn default() -> Self { + Self { + default_version: String::from("9"), + active_versions: vec![ + String::from("6"), + String::from("7"), + String::from("8"), + String::from("9"), + ], + endpoint_public: None, + } + } +} diff --git a/src/types/config/types/cdn_configuration.rs b/src/types/config/types/cdn_configuration.rs new file mode 100644 index 0000000..5c76273 --- /dev/null +++ b/src/types/config/types/cdn_configuration.rs @@ -0,0 +1,25 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CdnConfiguration { + pub resize_height_max: u64, + pub resize_width_max: u64, + pub imagor_server_url: Option, + + pub endpoint_public: Option, + pub endpoint_private: Option, +} + +impl Default for CdnConfiguration { + fn default() -> Self { + Self { + resize_height_max: 1000, + resize_width_max: 1000, + imagor_server_url: None, + + endpoint_private: None, + endpoint_public: None, + } + } +} diff --git a/src/types/config/types/defaults_configuration.rs b/src/types/config/types/defaults_configuration.rs new file mode 100644 index 0000000..c2b67c2 --- /dev/null +++ b/src/types/config/types/defaults_configuration.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::config::types::subconfigs::defaults::{guild::GuildDefaults, user::UserDefaults}; + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DefaultsConfiguration { + pub guild: GuildDefaults, + pub user: UserDefaults, +} diff --git a/src/types/config/types/email_configuration.rs b/src/types/config/types/email_configuration.rs new file mode 100644 index 0000000..954f4de --- /dev/null +++ b/src/types/config/types/email_configuration.rs @@ -0,0 +1,25 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::config::types::subconfigs::email::{ + mailgun::MailGunConfiguration, mailjet::MailJetConfiguration, sendgrid::SendGridConfiguration, + smtp::SMTPConfiguration, +}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +pub enum EmailProvider { + Smtp, + MailGun, + MailJet, + SendGrid, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[derive(Default)] +pub struct EmailConfiguration { + pub provider: Option, + pub smtp: SMTPConfiguration, + pub mailgun: MailGunConfiguration, + pub mailjet: MailJetConfiguration, + pub sendgrid: SendGridConfiguration, +} diff --git a/src/types/config/types/endpoint_configuration.rs b/src/types/config/types/endpoint_configuration.rs new file mode 100644 index 0000000..b484791 --- /dev/null +++ b/src/types/config/types/endpoint_configuration.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EndpointConfiguration { + pub endpoint_client: Option, + pub endpoint_private: Option, + pub endpoint_public: Option, +} diff --git a/src/types/config/types/external_tokens_configuration.rs b/src/types/config/types/external_tokens_configuration.rs new file mode 100644 index 0000000..f417b2f --- /dev/null +++ b/src/types/config/types/external_tokens_configuration.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExternalTokensConfiguration { + pub twitter: Option, +} diff --git a/src/types/config/types/general_configuration.rs b/src/types/config/types/general_configuration.rs new file mode 100644 index 0000000..e30e29e --- /dev/null +++ b/src/types/config/types/general_configuration.rs @@ -0,0 +1,35 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::utils::Snowflake; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GeneralConfiguration { + pub instance_name: String, + pub instance_description: Option, + pub front_page: Option, + pub tos_page: Option, + pub correspondence_email: Option, + pub correspondence_user_id: Option, + pub image: Option, + pub instance_id: Option, + pub auto_create_bot_users: Option, +} + +impl Default for GeneralConfiguration { + fn default() -> Self { + Self { + instance_name: String::from("Spacebar Instance"), + instance_description: Some(String::from( + "This is a Spacebar instance made in the pre-release days", + )), + front_page: None, + tos_page: None, + correspondence_email: None, + correspondence_user_id: None, + image: None, + instance_id: Some(Snowflake::generate()), + auto_create_bot_users: Some(false), + } + } +} diff --git a/src/types/config/types/gif_configuration.rs b/src/types/config/types/gif_configuration.rs new file mode 100644 index 0000000..8644fb4 --- /dev/null +++ b/src/types/config/types/gif_configuration.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum GifProvider { + #[default] + Tenor, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GifConfiguration { + pub enabled: bool, + pub provider: GifProvider, + pub api_key: Option, +} + +impl Default for GifConfiguration { + fn default() -> Self { + Self { + enabled: true, + provider: GifProvider::Tenor, + api_key: Some(String::from("LIVDSRZULELA")), + } + } +} diff --git a/src/types/config/types/guild_configuration.rs b/src/types/config/types/guild_configuration.rs new file mode 100644 index 0000000..e98c6c5 --- /dev/null +++ b/src/types/config/types/guild_configuration.rs @@ -0,0 +1,126 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::config::types::subconfigs::guild::{ + autojoin::AutoJoinConfiguration, discovery::DiscoverConfiguration, +}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum GuildFeatures { + ActivitiesAlpha, + ActivitiesEmployee, + ActivitiesInternalDev, + AnimatedBanner, + AnimatedIcon, + ApplicationCommandPermissionsV2, + AutoModeration, + AutoModTriggerKeywordFilter, + AutoModTriggerMLSpamFilter, + AutoModTriggerSpamLinkFilter, + AutoModTriggerUserProfile, + Banner, + BFG, + BoostingTiersExperimentMediumGuild, + BoostingTiersExperimentSmallGuild, + BotDeveloperEarlyAccess, + BurstReactions, + CommunityCanary, + CommunityExpLargeGated, + CommunityExpLargeUngated, + CommunityExpMedium, + ChannelEmojisGenerated, + ChannelHighlights, + ChannelHighlightsDisabled, + ClydeEnabled, + ClydeExperimentEnabled, + ClydeDisabled, + Community, + CreatorAcceptedNewTerms, + CreatorMonetizable, + CreatorMonetizableDisabled, + CreatorMonetizablePendingNewOwnerOnboarding, + CreatorMonetizableProvisional, + CreatorMonetizableRestricted, + CreatorMonetizableWhiteglove, + CreatorMonetizableApplicationAllowlist, + CreateStorePage, + DeveloperSupportServer, + DiscoverableDisabled, + Discoverable, + EnabledDiscoverableBefore, + ExposedToActivitiesWTPExperiment, + GuestsEnabled, + GuildAutomodDefaultList, + GuildCommunicationDisabledGuilds, + GuildHomeDeprecationOverride, + GuildHomeOverride, + GuildHomeTest, + GuildMemberVerificationExperiment, + GuildOnboarding, + GuildOnboardingAdminOnly, + GuildOnboardingEverEnabled, + GuildOnboardingHasPrompts, + GuildRoleSubscription, + GuildRoleSubscriptionPurchaseFeedbackLoop, + GuildRoleSubscriptionTrials, + GuildServerGuide, + GuildWebPageVanityURL, + HadEarlyActivitiesAccess, + HasDirectoryEntry, + HideFromExperimentUI, + Hub, + IncreasedThreadLimit, + InternalEmployeeOnly, + InviteSplash, + InvitesDisabled, + LinkedToHub, + MarketplacesConnectionRoles, + MemberProfiles, + MemberVerificationGateEnabled, + MemberVerificationManualApproval, + MobileWebRoleSubscriptionPurchasePage, + MonetizationEnabled, + MoreEmoji, + MoreStickers, + News, + NewThreadPermissions, + Partnered, + PremiumTier3Override, + PreviewEnabled, + RaidAlertsDisabled, + RelayEnabled, + RestrictSpamRiskGuild, + RoleIcons, + RoleSubscriptionsAvailableForPurchase, + RoleSubscriptionsEnabled, + RoleSubscriptionsEnabledForPurchase, + Shard, + SharedCanvasFriendsAndFamilyTest, + Soundboard, + SummariesEnabled, + SummariesEnabledGA, + SummariesDisabledByUser, + SummariesEnabledByUser, + TextInStageEnabled, + TextInVoiceEnabled, + ThreadsEnabledTesting, + ThreadsEnabled, + ThreadDefaultAutoArchiveDuration, + ThreadsOnlyChannel, + TicketedEventsEnabled, + TicketingEnabled, + VanityUrls, + Verified, + VIPRegions, + VoiceChannelEffects, + WelcomeScreenEnabled, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GuildConfiguration { + pub discovery: DiscoverConfiguration, + pub auto_join: AutoJoinConfiguration, + #[serde(default)] + pub default_features: Vec, +} diff --git a/src/types/config/types/kafka_configuration.rs b/src/types/config/types/kafka_configuration.rs new file mode 100644 index 0000000..46d10b6 --- /dev/null +++ b/src/types/config/types/kafka_configuration.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::config::types::subconfigs::kafka::KafkaBroker; + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct KafkaConfiguration { + #[serde(default)] + pub brokers: Option>, +} diff --git a/src/types/config/types/limit_configuration.rs b/src/types/config/types/limit_configuration.rs new file mode 100644 index 0000000..44f888a --- /dev/null +++ b/src/types/config/types/limit_configuration.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::config::types::subconfigs::limits::{ + channel::ChannelLimits, global::GlobalRateLimits, guild::GuildLimits, message::MessageLimits, + rates::RateLimits, user::UserLimits, +}; + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LimitsConfiguration { + pub user: UserLimits, + pub guild: GuildLimits, + pub message: MessageLimits, + pub channel: ChannelLimits, + pub rate: RateLimits, + pub absolute_rate: GlobalRateLimits, +} diff --git a/src/types/config/types/login_configuration.rs b/src/types/config/types/login_configuration.rs new file mode 100644 index 0000000..a2b1039 --- /dev/null +++ b/src/types/config/types/login_configuration.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LoginConfiguration { + pub require_captcha: bool, + pub require_verification: bool, +} diff --git a/src/types/config/types/metrics_configuration.rs b/src/types/config/types/metrics_configuration.rs new file mode 100644 index 0000000..336ac84 --- /dev/null +++ b/src/types/config/types/metrics_configuration.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MetricsConfiguration { + pub timeout: u64, +} + +impl Default for MetricsConfiguration { + fn default() -> Self { + Self { timeout: 30000 } + } +} diff --git a/src/types/config/types/mod.rs b/src/types/config/types/mod.rs new file mode 100644 index 0000000..dce4eb0 --- /dev/null +++ b/src/types/config/types/mod.rs @@ -0,0 +1,21 @@ +pub mod api_configuration; +pub mod cdn_configuration; +pub mod defaults_configuration; +pub mod email_configuration; +pub mod endpoint_configuration; +pub mod external_tokens_configuration; +pub mod general_configuration; +pub mod gif_configuration; +pub mod guild_configuration; +pub mod kafka_configuration; +pub mod limit_configuration; +pub mod login_configuration; +pub mod metrics_configuration; +pub mod password_reset_configuration; +pub mod rabbit_mq_configuration; +pub mod region_configuration; +pub mod register_configuration; +pub mod security_configuration; +pub mod sentry_configuration; +pub mod subconfigs; +pub mod template_configuration; diff --git a/src/types/config/types/password_reset_configuration.rs b/src/types/config/types/password_reset_configuration.rs new file mode 100644 index 0000000..4dddae9 --- /dev/null +++ b/src/types/config/types/password_reset_configuration.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasswordResetConfiguration { + pub require_captcha: bool, +} diff --git a/src/types/config/types/rabbit_mq_configuration.rs b/src/types/config/types/rabbit_mq_configuration.rs new file mode 100644 index 0000000..2437055 --- /dev/null +++ b/src/types/config/types/rabbit_mq_configuration.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RabbitMQConfiguration { + pub host: Option, +} diff --git a/src/types/config/types/region_configuration.rs b/src/types/config/types/region_configuration.rs new file mode 100644 index 0000000..078fa03 --- /dev/null +++ b/src/types/config/types/region_configuration.rs @@ -0,0 +1,29 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::config::types::subconfigs::region::Region; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RegionConfiguration { + pub default: String, + pub use_default_as_optimal: bool, + pub available: Vec, +} + +impl Default for RegionConfiguration { + fn default() -> Self { + Self { + default: String::from("spacebar"), + use_default_as_optimal: true, + available: vec![Region { + id: String::from("spacebar"), + name: String::from("spacebar"), + endpoint: String::from("127.0.0.1:3004"), + location: None, + vip: false, + custom: false, + deprecated: false, + }], + } + } +} diff --git a/src/types/config/types/register_configuration.rs b/src/types/config/types/register_configuration.rs new file mode 100644 index 0000000..4a20824 --- /dev/null +++ b/src/types/config/types/register_configuration.rs @@ -0,0 +1,41 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::config::types::subconfigs::register::{ + DateOfBirthConfiguration, PasswordConfiguration, RegistrationEmailConfiguration, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RegisterConfiguration { + pub email: RegistrationEmailConfiguration, + pub date_of_birth: DateOfBirthConfiguration, + pub password: PasswordConfiguration, + pub disabled: bool, + pub require_captcha: bool, + pub require_invite: bool, + pub guests_require_invite: bool, + pub allow_new_registration: bool, + pub allow_multiple_accounts: bool, + pub block_proxies: bool, + pub incrementing_discriminators: bool, + pub default_rights: String, +} + +impl Default for RegisterConfiguration { + fn default() -> Self { + Self { + email: RegistrationEmailConfiguration::default(), + date_of_birth: DateOfBirthConfiguration::default(), + password: PasswordConfiguration::default(), + disabled: false, + require_captcha: true, + require_invite: false, + guests_require_invite: true, + allow_new_registration: true, + allow_multiple_accounts: true, + block_proxies: true, + incrementing_discriminators: false, + default_rights: String::from("875069521787904"), + } + } +} diff --git a/src/types/config/types/security_configuration.rs b/src/types/config/types/security_configuration.rs new file mode 100644 index 0000000..d025a4b --- /dev/null +++ b/src/types/config/types/security_configuration.rs @@ -0,0 +1,44 @@ +use base64::Engine; +use serde::{Deserialize, Serialize}; + +use crate::types::config::types::subconfigs::security::{ + CaptchaConfiguration, TwoFactorConfiguration, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SecurityConfiguration { + pub captcha: CaptchaConfiguration, + pub two_factor: TwoFactorConfiguration, + pub auto_update: bool, + pub request_signature: String, + pub jwt_secret: String, + pub forwarded_for: Option, + pub ipdata_api_key: Option, + pub mfa_backup_code_count: u8, + pub stats_world_readable: bool, + pub default_registration_token_expiration: u64, +} + +impl Default for SecurityConfiguration { + fn default() -> Self { + let mut req_sig: [u8; 32] = [0; 32]; + let _ = openssl::rand::rand_bytes(&mut req_sig); + let mut jwt_secret: [u8; 256] = [0; 256]; + let _ = openssl::rand::rand_bytes(&mut jwt_secret); + Self { + captcha: Default::default(), + two_factor: Default::default(), + auto_update: true, + request_signature: base64::engine::general_purpose::STANDARD.encode(req_sig), + jwt_secret: base64::engine::general_purpose::STANDARD.encode(jwt_secret), + forwarded_for: None, + ipdata_api_key: Some(String::from( + "eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9", + )), + mfa_backup_code_count: 10, + stats_world_readable: true, + default_registration_token_expiration: 1000 * 60 * 60 * 24 * 7, + } + } +} diff --git a/src/types/config/types/sentry_configuration.rs b/src/types/config/types/sentry_configuration.rs new file mode 100644 index 0000000..256bab9 --- /dev/null +++ b/src/types/config/types/sentry_configuration.rs @@ -0,0 +1,28 @@ +use std::ffi::OsString; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SentryConfiguration { + pub enabled: bool, + pub endpoint: String, + pub trace_sample_rate: f64, + pub environment: String, +} + +impl Default for SentryConfiguration { + fn default() -> Self { + Self { + enabled: false, + endpoint: String::from( + "https://241c6fb08adb469da1bb82522b25c99f@sentry.quartzinc.space/3", + ), + trace_sample_rate: 1.0, + environment: hostname::get() + .unwrap_or_else(|_| OsString::new()) + .to_string_lossy() + .to_string(), + } + } +} diff --git a/src/types/config/types/subconfigs/client/mod.rs b/src/types/config/types/subconfigs/client/mod.rs new file mode 100644 index 0000000..5d3d304 --- /dev/null +++ b/src/types/config/types/subconfigs/client/mod.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientReleaseConfiguration { + pub use_local_release: bool, + pub upstream_version: String, +} + +impl Default for ClientReleaseConfiguration { + fn default() -> Self { + Self { + use_local_release: true, + upstream_version: String::from("0.0.264"), + } + } +} diff --git a/src/types/config/types/subconfigs/defaults/guild.rs b/src/types/config/types/subconfigs/defaults/guild.rs new file mode 100644 index 0000000..966c7af --- /dev/null +++ b/src/types/config/types/subconfigs/defaults/guild.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GuildDefaults { + pub max_presences: u64, + pub max_video_channel_users: u16, + pub afk_timeout: u16, + pub default_message_notifications: u8, + pub explicit_content_filter: u8, +} + +impl Default for GuildDefaults { + fn default() -> Self { + Self { + max_presences: 250_000, + max_video_channel_users: 200, + afk_timeout: 300, + default_message_notifications: 1, + explicit_content_filter: 0, + } + } +} diff --git a/src/types/config/types/subconfigs/defaults/mod.rs b/src/types/config/types/subconfigs/defaults/mod.rs new file mode 100644 index 0000000..56d877f --- /dev/null +++ b/src/types/config/types/subconfigs/defaults/mod.rs @@ -0,0 +1,2 @@ +pub mod guild; +pub mod user; diff --git a/src/types/config/types/subconfigs/defaults/user.rs b/src/types/config/types/subconfigs/defaults/user.rs new file mode 100644 index 0000000..635d6d4 --- /dev/null +++ b/src/types/config/types/subconfigs/defaults/user.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UserDefaults { + pub premium: bool, + pub premium_type: u8, + pub verified: bool, +} + +impl Default for UserDefaults { + fn default() -> Self { + Self { + premium: true, + premium_type: 2, + verified: true, + } + } +} diff --git a/src/types/config/types/subconfigs/email/mailgun.rs b/src/types/config/types/subconfigs/email/mailgun.rs new file mode 100644 index 0000000..636e462 --- /dev/null +++ b/src/types/config/types/subconfigs/email/mailgun.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MailGunConfiguration { + pub api_key: Option, + pub domain: Option, +} diff --git a/src/types/config/types/subconfigs/email/mailjet.rs b/src/types/config/types/subconfigs/email/mailjet.rs new file mode 100644 index 0000000..4e505c1 --- /dev/null +++ b/src/types/config/types/subconfigs/email/mailjet.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MailJetConfiguration { + pub api_key: Option, + pub api_secret: Option, +} diff --git a/src/types/config/types/subconfigs/email/mod.rs b/src/types/config/types/subconfigs/email/mod.rs new file mode 100644 index 0000000..21253fd --- /dev/null +++ b/src/types/config/types/subconfigs/email/mod.rs @@ -0,0 +1,4 @@ +pub mod mailgun; +pub mod mailjet; +pub mod sendgrid; +pub mod smtp; diff --git a/src/types/config/types/subconfigs/email/sendgrid.rs b/src/types/config/types/subconfigs/email/sendgrid.rs new file mode 100644 index 0000000..879c719 --- /dev/null +++ b/src/types/config/types/subconfigs/email/sendgrid.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SendGridConfiguration { + pub api_key: Option, +} diff --git a/src/types/config/types/subconfigs/email/smtp.rs b/src/types/config/types/subconfigs/email/smtp.rs new file mode 100644 index 0000000..a02c66f --- /dev/null +++ b/src/types/config/types/subconfigs/email/smtp.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct SMTPConfiguration { + pub host: Option, + pub port: Option, + pub secure: bool, + pub username: Option, + pub password: Option, +} diff --git a/src/types/config/types/subconfigs/guild/autojoin.rs b/src/types/config/types/subconfigs/guild/autojoin.rs new file mode 100644 index 0000000..fe72c6c --- /dev/null +++ b/src/types/config/types/subconfigs/guild/autojoin.rs @@ -0,0 +1,22 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::utils::Snowflake; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutoJoinConfiguration { + pub enabled: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub guilds: Option>, + pub can_leave: bool, +} + +impl Default for AutoJoinConfiguration { + fn default() -> Self { + Self { + enabled: true, + guilds: None, + can_leave: true, + } + } +} diff --git a/src/types/config/types/subconfigs/guild/discovery.rs b/src/types/config/types/subconfigs/guild/discovery.rs new file mode 100644 index 0000000..1e283b0 --- /dev/null +++ b/src/types/config/types/subconfigs/guild/discovery.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DiscoverConfiguration { + pub show_all_guilds: bool, + pub use_recommendation: bool, + pub offset: u16, + pub limit: u16, +} + +impl Default for DiscoverConfiguration { + fn default() -> Self { + Self { + show_all_guilds: false, + use_recommendation: false, + offset: 0, + limit: 24, + } + } +} diff --git a/src/types/config/types/subconfigs/guild/mod.rs b/src/types/config/types/subconfigs/guild/mod.rs new file mode 100644 index 0000000..e4d7dcf --- /dev/null +++ b/src/types/config/types/subconfigs/guild/mod.rs @@ -0,0 +1,2 @@ +pub mod autojoin; +pub mod discovery; diff --git a/src/types/config/types/subconfigs/kafka/mod.rs b/src/types/config/types/subconfigs/kafka/mod.rs new file mode 100644 index 0000000..1ee4015 --- /dev/null +++ b/src/types/config/types/subconfigs/kafka/mod.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct KafkaBroker { + pub ip: String, + pub port: u16, +} diff --git a/src/types/config/types/subconfigs/limits/channel.rs b/src/types/config/types/subconfigs/limits/channel.rs new file mode 100644 index 0000000..03e46e5 --- /dev/null +++ b/src/types/config/types/subconfigs/limits/channel.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChannelLimits { + pub max_pins: u16, + pub max_topic: u16, + pub max_webhooks: u16, +} + +impl Default for ChannelLimits { + fn default() -> Self { + Self { + max_pins: 500, + max_topic: 1024, + max_webhooks: 100, + } + } +} diff --git a/src/types/config/types/subconfigs/limits/global.rs b/src/types/config/types/subconfigs/limits/global.rs new file mode 100644 index 0000000..87f9e1c --- /dev/null +++ b/src/types/config/types/subconfigs/limits/global.rs @@ -0,0 +1,41 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct GlobalRateLimit { + pub limit: u16, + pub window: u64, + pub enabled: bool, +} + +impl Default for GlobalRateLimit { + fn default() -> Self { + Self { + limit: 100, + window: 60 * 60 * 1000, + enabled: true, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GlobalRateLimits { + pub register: GlobalRateLimit, + pub send_message: GlobalRateLimit, +} + +impl Default for GlobalRateLimits { + fn default() -> Self { + Self { + register: GlobalRateLimit { + limit: 25, + ..Default::default() + }, + send_message: GlobalRateLimit { + limit: 200, + window: 60 * 1000, + ..Default::default() + }, + } + } +} diff --git a/src/types/config/types/subconfigs/limits/guild.rs b/src/types/config/types/subconfigs/limits/guild.rs new file mode 100644 index 0000000..6def5a0 --- /dev/null +++ b/src/types/config/types/subconfigs/limits/guild.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GuildLimits { + pub max_roles: u16, + pub max_emojis: u16, + pub max_members: u64, + pub max_channels: u32, + pub max_channels_in_category: u32, +} + +impl Default for GuildLimits { + fn default() -> Self { + Self { + max_roles: 1000, + max_emojis: 20_000, + max_members: 25_000_000, + max_channels: 65_535, + max_channels_in_category: 65_535, + } + } +} diff --git a/src/types/config/types/subconfigs/limits/message.rs b/src/types/config/types/subconfigs/limits/message.rs new file mode 100644 index 0000000..9d368b9 --- /dev/null +++ b/src/types/config/types/subconfigs/limits/message.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MessageLimits { + pub max_characters: u32, + #[serde(default)] + pub max_tts_characters: u32, + pub max_reactions: u32, + pub max_attachment_size: u64, + pub max_bulk_delete: u32, + pub max_embed_download_size: u64, +} + +impl Default for MessageLimits { + fn default() -> Self { + Self { + max_characters: 1048576, + max_tts_characters: 160, + max_reactions: 2048, + max_attachment_size: 1024 * 1024 * 1024, + max_bulk_delete: 1000, + max_embed_download_size: 1024 * 1024 * 5, + } + } +} diff --git a/src/types/config/types/subconfigs/limits/mod.rs b/src/types/config/types/subconfigs/limits/mod.rs new file mode 100644 index 0000000..4dbc2fa --- /dev/null +++ b/src/types/config/types/subconfigs/limits/mod.rs @@ -0,0 +1,7 @@ +pub mod channel; +pub mod global; +pub mod guild; +pub mod message; +pub mod ratelimits; +pub mod rates; +pub mod user; diff --git a/src/types/config/types/subconfigs/limits/ratelimits/auth.rs b/src/types/config/types/subconfigs/limits/ratelimits/auth.rs new file mode 100644 index 0000000..f5abb0f --- /dev/null +++ b/src/types/config/types/subconfigs/limits/ratelimits/auth.rs @@ -0,0 +1,28 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::config::types::subconfigs::limits::ratelimits::RateLimitOptions; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AuthRateLimit { + pub login: RateLimitOptions, + pub register: RateLimitOptions, +} + +impl Default for AuthRateLimit { + fn default() -> Self { + Self { + login: RateLimitOptions { + bot: None, + count: 5, + window: 60, + only_ip: false, + }, + register: RateLimitOptions { + bot: None, + count: 2, + window: 60 * 60 * 12, + only_ip: false, + }, + } + } +} diff --git a/src/types/config/types/subconfigs/limits/ratelimits/mod.rs b/src/types/config/types/subconfigs/limits/ratelimits/mod.rs new file mode 100644 index 0000000..66a2b78 --- /dev/null +++ b/src/types/config/types/subconfigs/limits/ratelimits/mod.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +pub mod auth; +pub mod route; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RateLimitOptions { + pub bot: Option, + pub count: u64, + pub window: u64, + #[serde(default)] + pub only_ip: bool, +} diff --git a/src/types/config/types/subconfigs/limits/ratelimits/route.rs b/src/types/config/types/subconfigs/limits/ratelimits/route.rs new file mode 100644 index 0000000..5529af3 --- /dev/null +++ b/src/types/config/types/subconfigs/limits/ratelimits/route.rs @@ -0,0 +1,39 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::config::types::subconfigs::limits::ratelimits::{ + auth::AuthRateLimit, RateLimitOptions, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RouteRateLimit { + pub guild: RateLimitOptions, + pub webhook: RateLimitOptions, + pub channel: RateLimitOptions, + pub auth: AuthRateLimit, +} + +impl Default for RouteRateLimit { + fn default() -> Self { + Self { + guild: RateLimitOptions { + bot: None, + count: 5, + window: 5, + only_ip: false, + }, + webhook: RateLimitOptions { + bot: None, + count: 10, + window: 5, + only_ip: false, + }, + channel: RateLimitOptions { + bot: None, + count: 10, + window: 5, + only_ip: false, + }, + auth: AuthRateLimit::default(), + } + } +} diff --git a/src/types/config/types/subconfigs/limits/rates.rs b/src/types/config/types/subconfigs/limits/rates.rs new file mode 100644 index 0000000..9d0cab1 --- /dev/null +++ b/src/types/config/types/subconfigs/limits/rates.rs @@ -0,0 +1,41 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::config::types::subconfigs::limits::ratelimits::{ + route::RouteRateLimit, RateLimitOptions, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RateLimits { + pub enabled: bool, + pub ip: RateLimitOptions, + pub global: RateLimitOptions, + pub error: RateLimitOptions, + pub routes: RouteRateLimit, +} + +impl Default for RateLimits { + fn default() -> Self { + Self { + enabled: false, + ip: RateLimitOptions { + bot: None, + count: 500, + window: 5, + only_ip: false, + }, + global: RateLimitOptions { + bot: None, + count: 250, + window: 5, + only_ip: false, + }, + error: RateLimitOptions { + bot: None, + count: 10, + window: 5, + only_ip: false, + }, + routes: RouteRateLimit::default(), + } + } +} diff --git a/src/types/config/types/subconfigs/limits/user.rs b/src/types/config/types/subconfigs/limits/user.rs new file mode 100644 index 0000000..e43b746 --- /dev/null +++ b/src/types/config/types/subconfigs/limits/user.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UserLimits { + pub max_guilds: u64, + pub max_username: u16, + pub max_friends: u64, +} + +impl Default for UserLimits { + fn default() -> Self { + Self { + max_guilds: 1048576, + max_username: 32, + max_friends: 5000, + } + } +} diff --git a/src/types/config/types/subconfigs/mod.rs b/src/types/config/types/subconfigs/mod.rs new file mode 100644 index 0000000..4c85096 --- /dev/null +++ b/src/types/config/types/subconfigs/mod.rs @@ -0,0 +1,9 @@ +pub mod client; +pub mod defaults; +pub mod email; +pub mod guild; +pub mod kafka; +pub mod limits; +pub mod region; +pub mod register; +pub mod security; diff --git a/src/types/config/types/subconfigs/region/mod.rs b/src/types/config/types/subconfigs/region/mod.rs new file mode 100644 index 0000000..e0b1800 --- /dev/null +++ b/src/types/config/types/subconfigs/region/mod.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; + + + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LatLong { + pub latitude: f64, + pub longitude: f64, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Region { + pub id: String, + pub name: String, + pub endpoint: String, + pub location: Option, + pub vip: bool, + pub custom: bool, + #[serde(default)] + pub deprecated: bool, +} diff --git a/src/types/config/types/subconfigs/register/date_of_birth.rs b/src/types/config/types/subconfigs/register/date_of_birth.rs new file mode 100644 index 0000000..9c1bec1 --- /dev/null +++ b/src/types/config/types/subconfigs/register/date_of_birth.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DateOfBirthConfiguration { + pub required: bool, + pub minimum: u8, +} + +impl Default for DateOfBirthConfiguration { + fn default() -> Self { + Self { + required: true, + minimum: 13, + } + } +} diff --git a/src/types/config/types/subconfigs/register/email.rs b/src/types/config/types/subconfigs/register/email.rs new file mode 100644 index 0000000..ac99bfc --- /dev/null +++ b/src/types/config/types/subconfigs/register/email.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RegistrationEmailConfiguration { + pub required: bool, + pub allowlist: bool, + #[serde(default)] + pub blacklist: bool, + #[serde(default)] + pub domains: Vec, +} +impl Default for RegistrationEmailConfiguration { + fn default() -> Self { + Self { + required: false, + allowlist: false, + blacklist: true, + domains: Vec::new(), + } + } +} diff --git a/src/types/config/types/subconfigs/register/mod.rs b/src/types/config/types/subconfigs/register/mod.rs new file mode 100644 index 0000000..ad92571 --- /dev/null +++ b/src/types/config/types/subconfigs/register/mod.rs @@ -0,0 +1,7 @@ +mod date_of_birth; +mod email; +mod password; + +pub use date_of_birth::DateOfBirthConfiguration; +pub use email::RegistrationEmailConfiguration; +pub use password::PasswordConfiguration; diff --git a/src/types/config/types/subconfigs/register/password.rs b/src/types/config/types/subconfigs/register/password.rs new file mode 100644 index 0000000..9247f7d --- /dev/null +++ b/src/types/config/types/subconfigs/register/password.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PasswordConfiguration { + pub required: bool, + pub min_length: u8, + pub min_numbers: u8, + pub min_upper_case: u8, + pub min_symbols: u8, +} + +impl Default for PasswordConfiguration { + fn default() -> Self { + Self { + required: false, + min_length: 8, + min_numbers: 2, + min_upper_case: 2, + min_symbols: 0, + } + } +} diff --git a/src/types/config/types/subconfigs/security/captcha.rs b/src/types/config/types/subconfigs/security/captcha.rs new file mode 100644 index 0000000..82bb517 --- /dev/null +++ b/src/types/config/types/subconfigs/security/captcha.rs @@ -0,0 +1,27 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum CaptchaService { + Recaptcha, + HCaptcha, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CaptchaConfiguration { + pub enabled: bool, + pub service: CaptchaService, + pub sitekey: Option, + pub secret: Option, +} + +impl Default for CaptchaConfiguration { + fn default() -> Self { + Self { + enabled: false, + service: CaptchaService::HCaptcha, + sitekey: None, + secret: None, + } + } +} diff --git a/src/types/config/types/subconfigs/security/mod.rs b/src/types/config/types/subconfigs/security/mod.rs new file mode 100644 index 0000000..ceeb0d3 --- /dev/null +++ b/src/types/config/types/subconfigs/security/mod.rs @@ -0,0 +1,5 @@ +mod captcha; +mod twofactor; + +pub use captcha::{CaptchaConfiguration, CaptchaService}; +pub use twofactor::TwoFactorConfiguration; diff --git a/src/types/config/types/subconfigs/security/twofactor.rs b/src/types/config/types/subconfigs/security/twofactor.rs new file mode 100644 index 0000000..39a0373 --- /dev/null +++ b/src/types/config/types/subconfigs/security/twofactor.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TwoFactorConfiguration { + pub generate_backup_codes: bool, +} + +impl Default for TwoFactorConfiguration { + fn default() -> Self { + Self { + generate_backup_codes: true, + } + } +} diff --git a/src/types/config/types/template_configuration.rs b/src/types/config/types/template_configuration.rs new file mode 100644 index 0000000..932670e --- /dev/null +++ b/src/types/config/types/template_configuration.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TemplateConfiguration { + pub enabled: bool, + pub allow_template_creation: bool, + pub allow_discord_templates: bool, + pub allow_raws: bool, +} + +impl Default for TemplateConfiguration { + fn default() -> Self { + Self { + enabled: true, + allow_template_creation: true, + allow_discord_templates: true, + allow_raws: true, + } + } +} diff --git a/src/types/entities/application.rs b/src/types/entities/application.rs new file mode 100644 index 0000000..2ab3f99 --- /dev/null +++ b/src/types/entities/application.rs @@ -0,0 +1,138 @@ +use crate::types::utils::Snowflake; +use bitflags::{bitflags, Flags}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +#[cfg(feature = "sqlx")] +use sqlx::FromRow; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "sqlx", derive(FromRow))] +pub struct Application { + pub id: Snowflake, + pub name: String, + pub icon: Option, + pub description: Option, + pub summary: Option, + #[cfg(feature = "sqlx")] + pub r#type: Option>, + #[cfg(not(feature = "sqlx"))] + pub r#type: Option, + pub hook: bool, + pub bot_public: bool, + pub bot_require_code_grant: bool, + pub verify_key: String, + pub owner_id: Snowflake, + pub flags: u64, + #[cfg(feature = "sqlx")] + pub redirect_uris: Option>>, + #[cfg(not(feature = "sqlx"))] + pub redirect_uris: Option>, + pub rpc_application_state: i64, + pub store_application_state: i64, + pub verification_state: i64, + pub interactions_endpoint_url: Option, + pub integration_public: bool, + pub integration_require_code_grant: bool, + pub discoverability_state: i64, + pub discovery_eligibility_flags: i64, + pub bot_user_id: Snowflake, + #[cfg(feature = "sqlx")] + pub tags: Option>>, + #[cfg(not(feature = "sqlx"))] + pub tags: Option>, + pub cover_image: Option, + #[cfg(feature = "sqlx")] + pub install_params: Option>, + #[cfg(not(feature = "sqlx"))] + pub install_params: Option, + pub terms_of_service_url: Option, + pub privacy_policy_url: Option, + pub team_id: Option, +} + +impl Application { + pub fn flags(&self) -> ApplicationFlags { + ApplicationFlags::from_bits(self.flags.to_owned()).unwrap() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct InstallParams { + pub scopes: Vec, + pub permissions: String, +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] + pub struct ApplicationFlags: u64 { + const APPLICATION_AUTO_MODERATION_RULE_CREATE_BADGE = 1 << 6; + const GATEWAY_PRESENCE = 1 << 12; + const GATEWAY_PRESENCE_LIMITED = 1 << 13; + const GATEWAY_GUILD_MEMBERS = 1 << 14; + const GATEWAY_GUILD_MEMBERS_LIMITED = 1 << 15; + const VERIFICATION_PENDING_GUILD_LIMIT = 1 << 16; + const EMBEDDED = 1 << 17; + const GATEWAY_MESSAGE_CONTENT = 1 << 18; + const GATEWAY_MESSAGE_CONTENT_LIMITED = 1 << 19; + const APPLICATION_COMMAND_BADGE = 1 << 23; + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ApplicationCommand { + pub id: Snowflake, + pub application_id: Snowflake, + pub name: String, + pub description: String, + pub options: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ApplicationCommandOption { + pub r#type: ApplicationCommandOptionType, + pub name: String, + pub description: String, + pub required: bool, + pub choices: Vec, + pub options: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ApplicationCommandOptionChoice { + pub name: String, + pub value: Value, +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum ApplicationCommandOptionType { + #[serde(rename = "SUB_COMMAND")] + SubCommand = 1, + #[serde(rename = "SUB_COMMAND_GROUP")] + SubCommandGroup = 2, + #[serde(rename = "STRING")] + String = 3, + #[serde(rename = "INTEGER")] + Integer = 4, + #[serde(rename = "BOOLEAN")] + Boolean = 5, + #[serde(rename = "USER")] + User = 6, + #[serde(rename = "CHANNEL")] + Channel = 7, + #[serde(rename = "ROLE")] + Role = 8, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ApplicationCommandInteractionData { + pub id: Snowflake, + pub name: String, + pub options: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ApplicationCommandInteractionDataOption { + pub name: String, + pub value: Value, + pub options: Vec, +} diff --git a/src/types/entities/attachment.rs b/src/types/entities/attachment.rs new file mode 100644 index 0000000..c93ab8f --- /dev/null +++ b/src/types/entities/attachment.rs @@ -0,0 +1,113 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::utils::Snowflake; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Attachment { + pub id: Snowflake, + pub filename: String, + pub description: Option, + pub content_type: Option, + pub size: u64, + pub url: String, + pub proxy_url: String, + pub height: Option, + pub width: Option, + pub message_id: Snowflake, + pub ephemeral: Option, + pub duration_secs: Option, + pub waveform: Option, + #[serde(skip_serializing)] + pub content: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] + +pub struct PartialDiscordFileAttachment { + pub id: Option, + pub filename: String, + pub description: Option, + pub content_type: Option, + pub size: Option, + pub url: Option, + pub proxy_url: Option, + pub height: Option, + pub width: Option, + pub ephemeral: Option, + pub duration_secs: Option, + pub waveform: Option, + #[serde(skip_serializing)] + pub content: Vec, +} + +impl PartialDiscordFileAttachment { + /** + Moves `self.content` out of `self` and returns it. + # Returns + Vec + */ + pub fn move_content(self) -> (Vec, PartialDiscordFileAttachment) { + let content = self.content; + let updated_struct = PartialDiscordFileAttachment { + id: self.id, + filename: self.filename, + description: self.description, + content_type: self.content_type, + size: self.size, + url: self.url, + proxy_url: self.proxy_url, + height: self.height, + width: self.width, + ephemeral: self.ephemeral, + duration_secs: self.duration_secs, + waveform: self.waveform, + content: Vec::new(), + }; + (content, updated_struct) + } + + pub fn move_filename(self) -> (String, PartialDiscordFileAttachment) { + let filename = self.filename; + let updated_struct = PartialDiscordFileAttachment { + id: self.id, + filename: String::new(), + description: self.description, + content_type: self.content_type, + size: self.size, + url: self.url, + proxy_url: self.proxy_url, + height: self.height, + width: self.width, + + ephemeral: self.ephemeral, + duration_secs: self.duration_secs, + waveform: self.waveform, + content: self.content, + }; + (filename, updated_struct) + } + + pub fn move_content_type(self) -> (Option, PartialDiscordFileAttachment) { + let content_type = self.content_type; + let updated_struct = PartialDiscordFileAttachment { + id: self.id, + filename: self.filename, + description: self.description, + content_type: None, + size: self.size, + url: self.url, + proxy_url: self.proxy_url, + height: self.height, + width: self.width, + ephemeral: self.ephemeral, + duration_secs: self.duration_secs, + waveform: self.waveform, + content: self.content, + }; + (content_type, updated_struct) + } + + pub fn set_id(&mut self, id: i16) { + self.id = Some(id); + } +} diff --git a/src/types/entities/channel.rs b/src/types/entities/channel.rs new file mode 100644 index 0000000..0b4d9f2 --- /dev/null +++ b/src/types/entities/channel.rs @@ -0,0 +1,116 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::{ + entities::{GuildMember, User}, + utils::Snowflake, +}; + +#[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct Channel { + pub id: Snowflake, + #[serde(rename = "type")] + pub channel_type: ChannelType, + pub guild_id: Option, + pub position: Option, + pub permission_overwrites: Option>, + pub name: Option, + pub topic: Option, + pub nsfw: Option, + pub last_message_id: Option, + pub bitrate: Option, + pub user_limit: Option, + pub rate_limit_per_user: Option, + pub recipients: Option>, + pub icon: Option, + pub owner_id: Option, + pub application_id: Option, + pub parent_id: Option, + pub last_pin_timestamp: Option, + pub rtc_region: Option, + pub video_quality_mode: Option, + pub message_count: Option, + pub member_count: Option, + pub thread_metadata: Option, + pub member: Option, + pub default_auto_archive_duration: Option, + pub permissions: Option, + pub flags: Option, + pub total_message_sent: Option, + pub available_tags: Option>, + pub applied_tags: Option>, + pub default_reaction_emoji: Option, + pub default_thread_rate_limit_per_user: Option, + pub default_sort_order: Option, + pub default_forum_layout: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct Tag { + pub id: u64, + pub name: String, + pub moderated: bool, + pub emoji_id: Option, + pub emoji_name: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct PermissionOverwrite { + pub id: String, + #[serde(rename = "type")] + pub overwrite_type: u8, + pub allow: String, + pub deny: String, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct ThreadMetadata { + pub archived: bool, + pub auto_archive_duration: i32, + pub archive_timestamp: String, + pub locked: bool, + pub invitable: Option, + pub create_timestamp: Option, +} + +#[derive(Default, Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct ThreadMember { + pub id: Option, + pub user_id: Option, + pub join_timestamp: Option, + pub flags: Option, + pub member: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct DefaultReaction { + pub emoji_id: Option, + pub emoji_name: Option, +} + +#[derive(Default, Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[repr(i32)] +pub enum ChannelType { + #[default] + GuildText = 0, + Dm = 1, + GuildVoice = 2, + GroupDm = 3, + GuildCategory = 4, + GuildNews = 5, + GuildStore = 6, + Encrypted = 7, + EncryptedThreads = 8, + Transactional = 9, + GuildNewsThread = 10, + GuildPublicThread = 11, + GuildPrivateThread = 12, + GuildStageVoice = 13, + Directory = 14, + GuildForum = 15, + TicketTracker = 33, + Kanban = 34, + VoicelessWhiteboard = 35, + CustomStart = 64, + Unhandled = 255, +} diff --git a/src/types/entities/config.rs b/src/types/entities/config.rs new file mode 100644 index 0000000..25b1ef1 --- /dev/null +++ b/src/types/entities/config.rs @@ -0,0 +1,34 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +#[cfg(feature = "sqlx")] +use sqlx::FromRow; + +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "sqlx", derive(FromRow))] +pub struct ConfigEntity { + pub key: String, + pub value: Option, +} + +impl ConfigEntity { + pub fn as_string(&self) -> Option { + let Some(v) = self.value.as_ref() else { + return None; + }; + Some(v.as_str().expect("value is not a string").to_string()) + } + + pub fn as_bool(&self) -> Option { + let Some(v) = self.value.as_ref() else { + return None; + }; + Some(v.as_bool().expect("value is not a boolean")) + } + + pub fn as_int(&self) -> Option { + let Some(v) = self.value.as_ref() else { + return None; + }; + Some(v.as_i64().expect("value is not a number")) + } +} diff --git a/src/types/entities/emoji.rs b/src/types/entities/emoji.rs new file mode 100644 index 0000000..cf56f1f --- /dev/null +++ b/src/types/entities/emoji.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::entities::User; + +#[derive(Debug, Deserialize, Serialize, Default, Clone)] +pub struct Emoji { + pub id: Option, + pub name: Option, + pub roles: Option>, + pub user: Option, + pub require_colons: Option, + pub managed: Option, + pub animated: Option, + pub available: Option, +} diff --git a/src/types/entities/guild.rs b/src/types/entities/guild.rs new file mode 100644 index 0000000..b2da5c1 --- /dev/null +++ b/src/types/entities/guild.rs @@ -0,0 +1,117 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::types::{ + entities::{Channel, Emoji, GuildTemplate, RoleObject, Sticker, User, VoiceState, Webhook}, + interfaces::WelcomeScreenObject, + utils::Snowflake, +}; + +/// See https://discord.com/developers/docs/resources/guild +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct Guild { + pub id: Snowflake, + pub name: String, + pub icon: Option, + pub icon_hash: Option, + pub splash: Option, + pub discovery_splash: Option, + pub owner: Option, + pub owner_id: Option, + pub permissions: Option, + pub afk_channel_id: Option, + pub afk_timeout: Option, + pub widget_enabled: Option, + pub widget_channel_id: Option, + pub widget_channel: Option, + pub verification_level: Option, + pub default_message_notifications: Option, + pub explicit_content_filter: Option, + pub roles: Vec, + pub emojis: Vec, + pub features: Vec, + pub application_id: Option, + pub system_channel_id: Option, + pub system_channel_flags: Option, + pub rules_channel_id: Option, + pub rules_channel: Option, + pub max_presences: Option, + pub max_members: Option, + pub vanity_url_code: Option, + pub description: Option, + pub banner: Option, + pub premium_tier: Option, + pub premium_subscription_count: Option, + pub preferred_locale: Option, + pub public_updates_channel_id: Option, + pub public_updates_channel: Option, + pub max_video_channel_users: Option, + pub max_stage_video_channel_users: Option, + pub approximate_member_count: Option, + pub approximate_presence_count: Option, + pub member_count: Option, + pub presence_count: Option, + pub welcome_screen: Option, + pub nsfw_level: u8, + pub nsfw: bool, + pub stickers: Option>, + pub premium_progress_bar_enabled: Option, + pub joined_at: String, + pub afk_channel: Option, + pub bans: Option>, + pub primary_category_id: Option, + pub large: Option, + pub channels: Option>, + pub template_id: Option, + pub template: Option, + pub invites: Option>, + pub voice_states: Option>, + pub webhooks: Option>, + pub mfa_level: Option, + pub region: Option, + pub unavailable: bool, + pub parent: Option, +} + +/// See https://docs.spacebar.chat/routes/#get-/guilds/-guild_id-/bans/-user- +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct GuildBan { + pub id: Snowflake, + pub user_id: Snowflake, + pub guild_id: Snowflake, + pub executor_id: Snowflake, + pub reason: Option, +} + +/// See https://docs.spacebar.chat/routes/#cmp--schemas-invite +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct GuildInvite { + pub code: String, + pub temporary: Option, + pub uses: Option, + pub max_uses: Option, + pub max_age: Option, + pub created_at: DateTime, + pub expires_at: Option>, + pub guild_id: String, + pub guild: Option, + pub channel_id: String, + pub channel: Option, + pub inviter_id: Option, + pub inviter: Option, + pub target_user_id: Option, + pub target_user: Option, + pub target_user_type: Option, + pub vanity_url: Option, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct UnavailableGuild { + id: String, + unavailable: bool, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct GuildCreateResponse { + pub id: String, +} diff --git a/src/types/entities/guild_member.rs b/src/types/entities/guild_member.rs new file mode 100644 index 0000000..50d75e4 --- /dev/null +++ b/src/types/entities/guild_member.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::entities::User; + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct GuildMember { + pub user: Option, + pub nick: Option, + pub avatar: Option, + pub roles: Vec, + pub joined_at: String, + pub premium_since: Option, + pub deaf: bool, + pub mute: bool, + pub flags: i32, + pub pending: Option, + pub permissions: Option, + pub communication_disabled_until: Option, +} diff --git a/src/types/entities/integration.rs b/src/types/entities/integration.rs new file mode 100644 index 0000000..d4ed911 --- /dev/null +++ b/src/types/entities/integration.rs @@ -0,0 +1,36 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::types::{ + entities::{Application, User}, + utils::Snowflake, +}; + +#[derive(Default, Debug, Deserialize, Serialize, Clone)] +/// See https://discord.com/developers/docs/resources/guild#integration-object-integration-structure +pub struct Integration { + pub id: Snowflake, + pub name: String, + #[serde(rename = "type")] + pub integration_type: String, + pub enabled: bool, + pub syncing: Option, + pub role_id: Option, + pub enabled_emoticons: Option, + pub expire_behaviour: Option, + pub expire_grace_period: Option, + pub user: Option, + pub account: IntegrationAccount, + pub synced_at: Option>, + pub subscriber_count: Option, + pub revoked: Option, + pub application: Option, + pub scopes: Option>, +} + +#[derive(Default, Debug, Deserialize, Serialize, Clone)] +/// See https://discord.com/developers/docs/resources/guild#integration-account-object-integration-account-structure +pub struct IntegrationAccount { + pub id: String, + pub name: String, +} diff --git a/src/types/entities/message.rs b/src/types/entities/message.rs new file mode 100644 index 0000000..e384e61 --- /dev/null +++ b/src/types/entities/message.rs @@ -0,0 +1,185 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::{ + entities::{ + Application, Attachment, Channel, Emoji, GuildMember, RoleSubscriptionData, Sticker, + StickerItem, User, + }, + utils::Snowflake, +}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Message { + id: Snowflake, + pub channel_id: Snowflake, + author: User, + content: String, + timestamp: String, + edited_timestamp: Option, + tts: bool, + mention_everyone: bool, + mentions: Vec, + mention_roles: Vec, + mention_channels: Option>, + pub attachments: Vec, + embeds: Vec, + reactions: Option>, + nonce: Option, + pinned: bool, + webhook_id: Option, + #[serde(rename = "type")] + message_type: i32, + activity: Option, + application: Option, + application_id: Option, + message_reference: Option, + flags: Option, + referenced_message: Option>, + interaction: Option, + thread: Option, + components: Option>, + sticker_items: Option>, + stickers: Option>, + position: Option, + role_subscription_data: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct MessageReference { + pub message_id: Snowflake, + pub channel_id: Snowflake, + pub guild_id: Option, + pub fail_if_not_exists: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct MessageInteraction { + pub id: Snowflake, + #[serde(rename = "type")] + pub interaction_type: u8, + pub name: String, + pub user: User, + pub member: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AllowedMention { + parse: Vec, + roles: Vec, + users: Vec, + replied_user: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AllowedMentionType { + Roles, + Users, + Everyone, +} + +#[derive(Debug, Serialize, Deserialize)] +struct ChannelMention { + id: Snowflake, + guild_id: Snowflake, + #[serde(rename = "type")] + channel_type: i32, + name: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Embed { + title: Option, + #[serde(rename = "type")] + embed_type: Option, + description: Option, + url: Option, + timestamp: Option, + color: Option, + footer: Option, + image: Option, + thumbnail: Option, + video: Option, + provider: Option, + author: Option, + fields: Option>, +} + +#[derive(Debug, Serialize, Deserialize)] +struct EmbedFooter { + text: String, + icon_url: Option, + proxy_icon_url: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct EmbedImage { + url: String, + proxy_url: String, + height: Option, + width: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct EmbedThumbnail { + url: String, + proxy_url: Option, + height: Option, + width: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct EmbedVideo { + url: Option, + proxy_url: Option, + height: Option, + width: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct EmbedProvider { + name: Option, + url: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct EmbedAuthor { + name: String, + url: Option, + icon_url: Option, + proxy_icon_url: Option, +} + +#[derive(Debug, Serialize, Deserialize)] + +struct EmbedField { + name: String, + value: String, + inline: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Reaction { + pub count: i32, + pub me: bool, + pub emoji: Emoji, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum Component { + ActionRow = 1, + Button = 2, + StringSelect = 3, + TextInput = 4, + UserSelect = 5, + RoleSelect = 6, + MentionableSelect = 7, + ChannelSelect = 8, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct MessageActivity { + #[serde(rename = "type")] + pub activity_type: i64, + pub party_id: Option, +} diff --git a/src/types/entities/mod.rs b/src/types/entities/mod.rs new file mode 100644 index 0000000..006aac1 --- /dev/null +++ b/src/types/entities/mod.rs @@ -0,0 +1,37 @@ +mod application; +mod attachment; +mod channel; +mod config; +mod emoji; +mod guild; +mod guild_member; +mod integration; +mod message; +mod role; +mod security_key; +mod sticker; +mod team; +mod template; +mod user; +mod user_settings; +mod voice_state; +mod webhook; + +pub use application::*; +pub use attachment::*; +pub use channel::*; +pub use config::*; +pub use emoji::*; +pub use guild::*; +pub use guild_member::*; +pub use integration::*; +pub use message::*; +pub use role::*; +pub use security_key::*; +pub use sticker::*; +pub use team::*; +pub use template::*; +pub use user::*; +pub use user_settings::*; +pub use voice_state::*; +pub use webhook::*; diff --git a/src/types/entities/role.rs b/src/types/entities/role.rs new file mode 100644 index 0000000..708be00 --- /dev/null +++ b/src/types/entities/role.rs @@ -0,0 +1,28 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::utils::Snowflake; + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +/// See https://discord.com/developers/docs/topics/permissions#role-object +pub struct RoleObject { + pub id: Snowflake, + pub name: String, + pub color: f64, + pub hoist: bool, + pub icon: Option, + pub unicode_emoji: Option, + pub position: u16, + pub permissions: String, + pub managed: bool, + pub mentionable: bool, + // to:do add role tags https://discord.com/developers/docs/topics/permissions#role-object-role-tags-structure + //pub tags: Option +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RoleSubscriptionData { + pub role_subscription_listing_id: Snowflake, + pub tier_name: String, + pub total_months_subscribed: u32, + pub is_renewal: bool, +} diff --git a/src/types/entities/security_key.rs b/src/types/entities/security_key.rs new file mode 100644 index 0000000..09112cc --- /dev/null +++ b/src/types/entities/security_key.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::utils::Snowflake; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SecurityKey { + pub id: String, + pub user_id: String, + pub key_id: String, + pub public_key: String, + pub counter: u64, + pub name: String, +} + +impl Default for SecurityKey { + fn default() -> Self { + Self { + id: Snowflake::generate().to_string(), + user_id: String::new(), + key_id: String::new(), + public_key: String::new(), + counter: 0, + name: String::new(), + } + } +} diff --git a/src/types/entities/sticker.rs b/src/types/entities/sticker.rs new file mode 100644 index 0000000..d8f2803 --- /dev/null +++ b/src/types/entities/sticker.rs @@ -0,0 +1,27 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::{entities::User, utils::Snowflake}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Sticker { + pub id: Snowflake, + pub pack_id: Option, + pub name: String, + pub description: Option, + pub tags: String, + pub asset: Option, + #[serde(rename = "type")] + pub sticker_type: u8, + pub format_type: u8, + pub available: Option, + pub guild_id: Option, + pub user: Option, + pub sort_value: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct StickerItem { + pub id: Snowflake, + pub name: String, + pub format_type: u8, +} diff --git a/src/types/entities/team.rs b/src/types/entities/team.rs new file mode 100644 index 0000000..7c381a6 --- /dev/null +++ b/src/types/entities/team.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::entities::User; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Team { + pub icon: Option, + pub id: u64, + pub members: Vec, + pub name: String, + pub owner_user_id: u64, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct TeamMember { + pub membership_state: u8, + pub permissions: Vec, + pub team_id: u64, + pub user: User, +} diff --git a/src/types/entities/template.rs b/src/types/entities/template.rs new file mode 100644 index 0000000..dfbd98a --- /dev/null +++ b/src/types/entities/template.rs @@ -0,0 +1,24 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::types::{ + entities::{Guild, User}, + utils::Snowflake, +}; + +/// See https://docs.spacebar.chat/routes/#cmp--schemas-template +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct GuildTemplate { + pub code: String, + pub name: String, + pub description: Option, + pub usage_count: Option, + pub creator_id: Snowflake, + pub creator: User, + pub created_at: DateTime, + pub updated_at: DateTime, + pub source_guild_id: String, + pub source_guild: Vec, // Unsure how a {recursive: Guild} looks like, might be a Vec? + pub serialized_source_guild: Vec, + id: Snowflake, +} diff --git a/src/types/entities/user.rs b/src/types/entities/user.rs new file mode 100644 index 0000000..fab42e0 --- /dev/null +++ b/src/types/entities/user.rs @@ -0,0 +1,238 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +#[cfg(feature = "sqlx")] +use sqlx::{FromRow, Type}; + +use crate::types::{ + errors::Error, + utils::Snowflake, //util::{email::adjust_email, entities::user_setting::UserSettings}, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "sqlx", derive(FromRow))] +pub struct User { + pub id: Snowflake, + pub username: String, + pub discriminator: String, + pub avatar: Option, + pub accent_color: Option, + pub banner: Option, + pub theme_colors: Option>, + pub pronouns: Option, + pub phone: Option, + pub desktop: bool, + pub mobile: bool, + pub premium: bool, + pub premium_type: u8, + pub bot: bool, + pub bio: String, + pub system: bool, + pub nsfw_allowed: bool, + pub mfa_enabled: bool, + pub webauthn_enabled: bool, + #[serde(skip)] + pub totp_secret: Option, + #[serde(skip)] + pub totp_last_ticket: Option, + pub created_at: DateTime, + pub premium_since: Option>, + pub verified: bool, + pub disabled: bool, + pub deleted: bool, + pub email: Option, + pub flags: String, + pub public_flags: u16, + pub purchased_flags: u16, + pub premium_usage_flags: u16, + pub rights: String, + #[cfg(feature = "sqlx")] + pub relationship_ids: sqlx::types::Json>, + #[cfg(not(feature = "sqlx"))] + pub relationship_ids: Vec, + #[cfg(feature = "sqlx")] + pub connected_account_ids: sqlx::types::Json>, + #[cfg(not(feature = "sqlx"))] + pub connected_account_ids: Vec, + #[cfg(feature = "sqlx")] + pub data: sqlx::types::Json, + #[cfg(not(feature = "sqlx"))] + pub data: UserData, + #[cfg(feature = "sqlx")] + pub fingerprints: sqlx::types::Json>, + #[cfg(not(feature = "sqlx"))] + pub fingerprints: Vec, + // pub settings: UserSettings, + pub extended_settings: Value, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "sqlx", derive(Type))] +pub struct UserData { + pub valid_tokens_since: DateTime, + pub hash: Option, +} + +impl Default for User { + fn default() -> Self { + Self { + id: Snowflake::generate(), + username: String::new(), + discriminator: String::new(), + avatar: None, + accent_color: None, + banner: None, + theme_colors: None, + pronouns: None, + phone: None, + desktop: false, + mobile: false, + premium: false, + premium_type: 0, + bot: false, + bio: String::new(), + system: false, + nsfw_allowed: false, + mfa_enabled: false, + webauthn_enabled: false, + totp_secret: None, + totp_last_ticket: None, + created_at: Utc::now(), + premium_since: None, + verified: false, + disabled: false, + deleted: false, + email: None, + flags: String::from("0"), + public_flags: 0, + purchased_flags: 0, + premium_usage_flags: 0, + rights: String::new(), + relationship_ids: Default::default(), + connected_account_ids: Default::default(), + data: Default::default(), + fingerprints: Default::default(), + //settings: UserSettings::default(), + extended_settings: Value::Object(Map::new()), + } + } +} + +impl User { + pub fn validate(&mut self) -> Result<(), Error> { + /*if let Some(email) = self.email.as_ref() { + self.email = Some(adjust_email(email)?); + } + + if self.discriminator.len() < 4 { + self.discriminator = format!("{:0>4}", self.discriminator); + } + Ok(())*/ + todo!() + } + + pub async fn generate_discriminator(_username: &str) -> Result { + todo!() + } +} + +impl User { + pub fn to_public_user(self) -> PublicUser { + PublicUser::from(self) + } +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct PrivateUser { + pub id: String, + username: String, + discriminator: String, + avatar: Option, + bot: bool, + system: Option, + mfa_enabled: Option, + accent_color: Option, + locale: Option, + verified: Option, + email: Option, + flags: String, + premium_since: Option, + premium_type: i8, + pronouns: Option, + public_flags: Option, + banner: Option, + bio: String, + theme_colors: Option>, + phone: Option, + nsfw_allowed: bool, + premium: bool, + purchased_flags: i32, + premium_usage_flags: i32, + disabled: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PublicUser { + pub id: Snowflake, + pub username: String, + pub discriminator: String, + pub avatar: Option, + pub accent_color: Option, + pub banner: Option, + pub theme_colors: Option>, + pub pronouns: Option, + pub bot: bool, + pub bio: String, + pub premium_type: u8, + pub premium_since: Option>, + pub public_flags: u16, +} + +impl From for PublicUser { + fn from(value: User) -> Self { + Self { + id: value.id, + username: value.username, + discriminator: value.discriminator, + avatar: value.avatar, + accent_color: value.accent_color, + banner: value.banner, + theme_colors: value.theme_colors, + pronouns: value.pronouns, + bot: value.bot, + bio: value.bio, + premium_type: value.premium_type, + premium_since: value.premium_since, + public_flags: value.public_flags, + } + } +} + +const CUSTOM_USER_FLAG_OFFSET: u64 = 1 << 32; + +bitflags::bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] + #[cfg_attr(feature = "sqlx", derive(Type))] + pub struct UserFlags: u64 { + const DISCORD_EMPLOYEE = 1 << 0; + const PARTNERED_SERVER_OWNER = 1 << 1; + const HYPESQUAD_EVENTS = 1 << 2; + const BUGHUNTER_LEVEL_1 =1 << 3; + const MFA_SMS = 1 << 4; + const PREMIUM_PROMO_DISMISSED = 1 << 5; + const HOUSE_BRAVERY = 1 << 6; + const HOUSE_BRILLIANCE = 1 << 7; + const HOUSE_BALANCE = 1 << 8; + const EARLY_SUPPORTER = 1 << 9; + const TEAM_USER = 1 << 10; + const TRUST_AND_SAFETY = 1 << 11; + const SYSTEM = 1 << 12; + const HAS_UNREAD_URGENT_MESSAGES = 1 << 13; + const BUGHUNTER_LEVEL_2 = 1 << 14; + const UNDERAGE_DELETED = 1 << 15; + const VERIFIED_BOT = 1 << 16; + const EARLY_VERIFIED_BOT_DEVELOPER = 1 << 17; + const CERTIFIED_MODERATOR = 1 << 18; + const BOT_HTTP_INTERACTIONS = 1 << 19; + } +} diff --git a/src/types/entities/user_settings.rs b/src/types/entities/user_settings.rs new file mode 100644 index 0000000..b7f4e4a --- /dev/null +++ b/src/types/entities/user_settings.rs @@ -0,0 +1,133 @@ +use chrono::{serde::ts_milliseconds_option, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::types::utils::Snowflake; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "lowercase")] +pub enum UserStatus { + #[default] + Online, + Offline, + Dnd, + Idle, + Invisible, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "lowercase")] +pub enum UserTheme { + #[default] + Dark, + Light, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserSettings { + #[serde(skip_serializing)] + pub index: String, // Database relevance only + pub afk_timeout: u16, + pub allow_accessibility_detection: bool, + pub animate_emoji: bool, + pub animate_stickers: u8, + pub contact_sync_enabled: bool, + pub convert_emoticons: bool, + pub custom_status: Option, + pub default_guilds_restricted: bool, + pub detect_platform_accounts: bool, + pub developer_mode: bool, + pub disable_games_tab: bool, + pub enable_tts_command: bool, + pub explicit_content_filter: u8, + pub friend_source_flags: FriendSourceFlags, + pub gateway_connected: bool, + pub gif_auto_play: bool, + pub guild_folders: Vec, + pub guild_positions: Vec, + pub inline_attachment_media: bool, + pub inline_embed_media: bool, + pub locale: String, + pub message_display_compact: bool, + pub native_phone_integration_enabled: bool, + pub render_embeds: bool, + pub render_reactions: bool, + pub restricted_guilds: Vec, + pub show_current_game: bool, + pub status: UserStatus, + pub stream_notifications_enabled: bool, + pub theme: UserTheme, + pub timezone_offset: i16, +} + +impl Default for UserSettings { + fn default() -> Self { + Self { + index: Snowflake::generate().to_string(), + afk_timeout: 3600, + allow_accessibility_detection: true, + animate_emoji: true, + animate_stickers: 0, + contact_sync_enabled: false, + convert_emoticons: false, + custom_status: None, + default_guilds_restricted: false, + detect_platform_accounts: false, + developer_mode: true, + disable_games_tab: true, + enable_tts_command: false, + explicit_content_filter: 0, + friend_source_flags: FriendSourceFlags::default(), + gateway_connected: false, + gif_auto_play: false, + guild_folders: Vec::new(), + guild_positions: Vec::new(), + inline_attachment_media: true, + inline_embed_media: true, + locale: "en-US".to_string(), + message_display_compact: false, + native_phone_integration_enabled: true, + render_embeds: true, + render_reactions: true, + restricted_guilds: Vec::new(), + show_current_game: true, + status: UserStatus::Online, + stream_notifications_enabled: false, + theme: UserTheme::Dark, + timezone_offset: 0, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CustomStatus { + pub emoji_id: Option, + pub emoji_name: Option, + #[serde(with = "ts_milliseconds_option")] + pub expires_at: Option>, + pub text: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct FriendSourceFlags { + pub all: bool, +} + +impl Default for FriendSourceFlags { + fn default() -> Self { + Self { all: true } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct GuildFolder { + pub color: u32, + pub guild_ids: Vec, + pub id: u16, + pub name: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct LoginResult { + pub token: String, + pub settings: UserSettings, +} diff --git a/src/types/entities/voice_state.rs b/src/types/entities/voice_state.rs new file mode 100644 index 0000000..68e2051 --- /dev/null +++ b/src/types/entities/voice_state.rs @@ -0,0 +1,30 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::types::{ + entities::{Channel, Guild, GuildMember, User}, + utils::Snowflake, +}; + +/// See https://docs.spacebar.chat/routes/#cmp--schemas-voicestate +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct VoiceState { + pub guild_id: Snowflake, + pub guild: Option, + pub channel_id: Snowflake, + pub channel: Option, + pub user_id: Snowflake, + pub user: Option, + pub member: Option, + pub session_id: Snowflake, + pub token: String, + pub deaf: bool, + pub mute: bool, + pub self_deaf: bool, + pub self_mute: bool, + pub self_stream: Option, + pub self_video: bool, + pub suppress: bool, + pub request_to_speak_timestamp: Option>, + pub id: Snowflake, +} diff --git a/src/types/entities/webhook.rs b/src/types/entities/webhook.rs new file mode 100644 index 0000000..26fa81f --- /dev/null +++ b/src/types/entities/webhook.rs @@ -0,0 +1,32 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::{ + entities::{Application, Channel, Guild, User}, + utils::Snowflake, +}; + +/// See https://docs.spacebar.chat/routes/#cmp--schemas-webhook +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct Webhook { + #[serde(rename = "type")] + pub webhook_type: i32, + pub name: String, + pub avatar: String, + pub token: String, + pub guild_id: Snowflake, + #[serde(skip_serializing_if = "Option::is_none")] + pub guild: Option, + pub channel_id: Snowflake, + #[serde(skip_serializing_if = "Option::is_none")] + pub channel: Option, + pub application_id: Snowflake, + #[serde(skip_serializing_if = "Option::is_none")] + pub application: Option, + pub user_id: Snowflake, + #[serde(skip_serializing_if = "Option::is_none")] + pub user: Option, + pub source_guild_id: Snowflake, + #[serde(skip_serializing_if = "Option::is_none")] + pub source_guild: Option, + pub id: Snowflake, +} diff --git a/src/types/errors.rs b/src/types/errors.rs new file mode 100644 index 0000000..63f6ceb --- /dev/null +++ b/src/types/errors.rs @@ -0,0 +1,54 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[cfg(feature = "sqlx")] + #[error("SQLX error: {0}")] + SQLX(#[from] sqlx::Error), + + #[error("serde: {0}")] + Serde(#[from] serde_json::Error), + + #[error(transparent)] + IO(#[from] std::io::Error), + + #[error(transparent)] + FieldFormat(#[from] FieldFormatError), +} + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +pub enum FieldFormatError { + #[error("Password must be between 1 and 72 characters.")] + PasswordError, + #[error("Username must be between 2 and 32 characters.")] + UsernameError, + #[error("Consent must be 'true' to register.")] + ConsentError, + #[error("The provided email address is in an invalid format.")] + EmailError, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ErrorResponse { + pub code: i32, + pub message: String, + pub errors: IntermittentError, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct IntermittentError { + #[serde(flatten)] + pub errors: std::collections::HashMap, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct ErrorField { + #[serde(default)] + pub _errors: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct APIError { + pub message: String, + pub code: String, +} diff --git a/src/types/events/channel.rs b/src/types/events/channel.rs new file mode 100644 index 0000000..a5db906 --- /dev/null +++ b/src/types/events/channel.rs @@ -0,0 +1,41 @@ +use crate::types::entities::Channel; +use crate::types::events::WebSocketEvent; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Deserialize, Serialize)] +/// See https://discord.com/developers/docs/topics/gateway-events#channel-pins-update +pub struct ChannelPinsUpdate { + pub guild_id: Option, + pub channel_id: String, + pub last_pin_timestamp: Option>, +} + +impl WebSocketEvent for ChannelPinsUpdate {} + +#[derive(Debug, Default, Deserialize, Serialize)] +/// See https://discord.com/developers/docs/topics/gateway-events#channel-create +/// Not directly serialized, as the inner payload is a channel object +pub struct ChannelCreate { + pub channel: Channel, +} + +impl WebSocketEvent for ChannelCreate {} + +#[derive(Debug, Default, Deserialize, Serialize)] +/// See https://discord.com/developers/docs/topics/gateway-events#channel-update +/// Not directly serialized, as the inner payload is a channel object +pub struct ChannelUpdate { + pub channel: Channel, +} + +impl WebSocketEvent for ChannelUpdate {} + +#[derive(Debug, Default, Deserialize, Serialize)] +/// See https://discord.com/developers/docs/topics/gateway-events#channel-delete +/// Not directly serialized, as the inner payload is a channel object +pub struct ChannelDelete { + pub channel: Channel, +} + +impl WebSocketEvent for ChannelDelete {} diff --git a/src/types/events/guild.rs b/src/types/events/guild.rs new file mode 100644 index 0000000..840fb8e --- /dev/null +++ b/src/types/events/guild.rs @@ -0,0 +1,41 @@ +use crate::types::entities::{Guild, UnavailableGuild, User}; +use crate::types::events::WebSocketEvent; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Default)] +/// See https://discord.com/developers/docs/topics/gateway-events#guild-create +/// This one is particularly painful, it can be a Guild object with extra field or an unavailbile guild object +pub struct GuildCreate { + pub d: GuildCreateDataOption, +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum GuildCreateDataOption { + UnavailableGuild(UnavailableGuild), + Guild(Guild), +} + +impl Default for GuildCreateDataOption { + fn default() -> Self { + GuildCreateDataOption::UnavailableGuild(UnavailableGuild::default()) + } +} +impl WebSocketEvent for GuildCreate {} + +#[derive(Debug, Default, Deserialize, Serialize)] +/// See https://discord.com/developers/docs/topics/gateway-events#guild-ban-add-guild-ban-add-event-fields +pub struct GuildBanAdd { + pub guild_id: String, + pub user: User, +} + +impl WebSocketEvent for GuildBanAdd {} + +#[derive(Debug, Default, Deserialize, Serialize)] +/// See https://discord.com/developers/docs/topics/gateway-events#guild-ban-remove +pub struct GuildBanRemove { + pub guild_id: String, + pub user: User, +} + +impl WebSocketEvent for GuildBanRemove {} diff --git a/src/types/events/heartbeat.rs b/src/types/events/heartbeat.rs new file mode 100644 index 0000000..be9e3f8 --- /dev/null +++ b/src/types/events/heartbeat.rs @@ -0,0 +1,17 @@ +use crate::types::events::WebSocketEvent; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct GatewayHeartbeat { + pub op: u8, + pub d: Option, +} + +impl WebSocketEvent for GatewayHeartbeat {} + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct GatewayHeartbeatAck { + pub op: i32, +} + +impl WebSocketEvent for GatewayHeartbeatAck {} diff --git a/src/types/events/hello.rs b/src/types/events/hello.rs new file mode 100644 index 0000000..214f211 --- /dev/null +++ b/src/types/events/hello.rs @@ -0,0 +1,17 @@ +use crate::types::events::WebSocketEvent; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct GatewayHello { + pub op: i32, + pub d: HelloData, +} + +impl WebSocketEvent for GatewayHello {} + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct HelloData { + pub heartbeat_interval: u128, +} + +impl WebSocketEvent for HelloData {} diff --git a/src/types/events/identify.rs b/src/types/events/identify.rs new file mode 100644 index 0000000..b9cacee --- /dev/null +++ b/src/types/events/identify.rs @@ -0,0 +1,22 @@ +use crate::types::events::{PresenceUpdate, WebSocketEvent}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct GatewayIdentifyPayload { + pub token: String, + pub properties: GatewayIdentifyConnectionProps, + pub compress: Option, + pub large_threshold: Option, //default: 50 + pub shard: Option>, + pub presence: Option, + pub intents: i32, +} + +impl WebSocketEvent for GatewayIdentifyPayload {} + +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct GatewayIdentifyConnectionProps { + pub os: String, + pub browser: String, + pub device: String, +} diff --git a/src/types/events/message.rs b/src/types/events/message.rs new file mode 100644 index 0000000..8cd6429 --- /dev/null +++ b/src/types/events/message.rs @@ -0,0 +1,101 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::{ + entities::{Emoji, GuildMember, Message, User}, + utils::Snowflake, +}; + +use super::WebSocketEvent; + +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct TypingStartEvent { + pub channel_id: String, + pub guild_id: Option, + pub user_id: String, + pub timestamp: i64, + pub member: Option, +} + +impl WebSocketEvent for TypingStartEvent {} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct MessageCreate { + #[serde(flatten)] + message: Message, + guild_id: Option, + member: Option, + mentions: Vec<(User, GuildMember)>, // Not sure if this is correct: https://discord.com/developers/docs/topics/gateway-events#message-create +} + +impl WebSocketEvent for MessageCreate {} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct MessageUpdate { + #[serde(flatten)] + message: Message, + guild_id: Option, + member: Option, + mentions: Vec<(User, GuildMember)>, // Not sure if this is correct: https://discord.com/developers/docs/topics/gateway-events#message-create +} + +impl WebSocketEvent for MessageUpdate {} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct MessageDelete { + id: String, + channel_id: String, + guild_id: Option, +} + +impl WebSocketEvent for MessageDelete {} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct MessageDeleteBulk { + ids: Vec, + channel_id: String, + guild_id: Option, +} + +impl WebSocketEvent for MessageDeleteBulk {} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct MessageReactionAdd { + user_id: String, + channel_id: String, + message_id: String, + guild_id: Option, + member: Option, + emoji: Emoji, +} + +impl WebSocketEvent for MessageReactionAdd {} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct MessageReactionRemove { + user_id: String, + channel_id: String, + message_id: String, + guild_id: Option, + emoji: Emoji, +} + +impl WebSocketEvent for MessageReactionRemove {} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct MessageReactionRemoveAll { + channel_id: String, + message_id: String, + guild_id: Option, +} + +impl WebSocketEvent for MessageReactionRemoveAll {} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct MessageReactionRemoveEmoji { + channel_id: String, + message_id: String, + guild_id: Option, + emoji: Emoji, +} + +impl WebSocketEvent for MessageReactionRemoveEmoji {} diff --git a/src/types/events/mod.rs b/src/types/events/mod.rs new file mode 100644 index 0000000..418cd31 --- /dev/null +++ b/src/types/events/mod.rs @@ -0,0 +1,41 @@ +use serde::{Deserialize, Serialize}; + +mod channel; +mod guild; +mod heartbeat; +mod hello; +mod identify; +mod message; +mod presence; +mod ready; +mod request_members; +mod resume; +mod thread; +mod user; +mod voice_status; + +pub use channel::*; +pub use guild::*; +pub use heartbeat::*; +pub use hello::*; +pub use identify::*; +pub use message::*; +pub use presence::*; +pub use ready::*; +pub use request_members::*; +pub use resume::*; +pub use thread::*; +pub use user::*; +pub use voice_status::*; + +pub trait WebSocketEvent {} + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct GatewayPayload { + pub op: u8, + pub d: Option, + pub s: Option, + pub t: Option, +} + +impl WebSocketEvent for GatewayPayload {} diff --git a/src/types/events/presence.rs b/src/types/events/presence.rs new file mode 100644 index 0000000..f8fea4c --- /dev/null +++ b/src/types/events/presence.rs @@ -0,0 +1,24 @@ +use crate::types::entities::User; +use crate::types::events::WebSocketEvent; +use crate::types::interfaces::Activity; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Default, Clone)] +/// See https://discord.com/developers/docs/topics/gateway-events#presence-update-presence-update-event-fields +pub struct PresenceUpdate { + pub user: User, + pub guild_id: String, + pub status: String, + pub activities: Vec, + pub client_status: ClientStatusObject, +} + +#[derive(Debug, Deserialize, Serialize, Default, Clone)] +/// See https://discord.com/developers/docs/topics/gateway-events#client-status-object +pub struct ClientStatusObject { + pub desktop: Option, + pub mobile: Option, + pub web: Option, +} + +impl WebSocketEvent for PresenceUpdate {} diff --git a/src/types/events/ready.rs b/src/types/events/ready.rs new file mode 100644 index 0000000..ff933c8 --- /dev/null +++ b/src/types/events/ready.rs @@ -0,0 +1,15 @@ +use crate::types::entities::{UnavailableGuild, User}; +use crate::types::events::WebSocketEvent; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct GatewayReady { + pub v: u8, + pub user: User, + pub guilds: Vec, + pub session_id: String, + pub resume_gateway_url: Option, + pub shard: Option<(u64, u64)>, +} + +impl WebSocketEvent for GatewayReady {} diff --git a/src/types/events/request_members.rs b/src/types/events/request_members.rs new file mode 100644 index 0000000..baf6d4a --- /dev/null +++ b/src/types/events/request_members.rs @@ -0,0 +1,15 @@ +use crate::types::events::WebSocketEvent; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Default)] +/// See https://discord.com/developers/docs/topics/gateway-events#request-guild-members-request-guild-members-structure +pub struct GatewayRequestGuildMembers { + pub guild_id: String, + pub query: Option, + pub limit: u64, + pub presence: Option, + pub user_ids: Option, + pub nonce: Option, +} + +impl WebSocketEvent for GatewayRequestGuildMembers {} diff --git a/src/types/events/resume.rs b/src/types/events/resume.rs new file mode 100644 index 0000000..362de98 --- /dev/null +++ b/src/types/events/resume.rs @@ -0,0 +1,11 @@ +use crate::types::events::WebSocketEvent; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct GatewayResume { + pub token: String, + pub session_id: String, + pub seq: String, +} + +impl WebSocketEvent for GatewayResume {} diff --git a/src/types/events/thread.rs b/src/types/events/thread.rs new file mode 100644 index 0000000..a9bf851 --- /dev/null +++ b/src/types/events/thread.rs @@ -0,0 +1,82 @@ +use crate::types::entities::{Channel, GuildMember, ThreadMember}; +use crate::types::events::WebSocketEvent; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Deserialize, Serialize)] +/// See https://discord.com/developers/docs/topics/gateway-events#thread-create +/// Not directly serialized, as the inner payload is a channel object +pub struct ThreadCreate { + pub thread: Channel, +} + +impl WebSocketEvent for ThreadCreate {} + +#[derive(Debug, Default, Deserialize, Serialize)] +/// See https://discord.com/developers/docs/topics/gateway-events#thread-update +/// Not directly serialized, as the inner payload is a channel object +pub struct ThreadUpdate { + pub thread: Channel, +} + +impl WebSocketEvent for ThreadUpdate {} + +#[derive(Debug, Default, Deserialize, Serialize)] +/// See https://discord.com/developers/docs/topics/gateway-events#thread-delete +/// Not directly serialized, as the inner payload is a channel object +pub struct ThreadDelete { + pub thread: Channel, +} + +impl WebSocketEvent for ThreadDelete {} + +#[derive(Debug, Default, Deserialize, Serialize)] +/// See https://discord.com/developers/docs/topics/gateway-events#thread-list-sync +pub struct ThreadListSync { + pub guild_id: String, + pub channel_ids: Option>, + pub threads: Vec, + pub members: Vec, +} + +impl WebSocketEvent for ThreadListSync {} + +#[derive(Debug, Default, Deserialize, Serialize)] +/// See https://discord.com/developers/docs/topics/gateway-events#thread-member-update +/// The inner payload is a thread member object with an extra field. +/// The extra field is a bit painful, because we can't just serialize a thread member object +pub struct ThreadMemberUpdate { + pub id: Option, + pub user_id: Option, + pub join_timestamp: Option, + pub flags: Option, + pub member: Option, + pub guild_id: String, +} + +impl ThreadMemberUpdate { + /// Convert self to a thread member, losing the added guild_id field + pub fn to_thread_member(self) -> ThreadMember { + ThreadMember { + id: self.id, + user_id: self.user_id, + join_timestamp: self.join_timestamp.clone(), + flags: self.flags, + member: self.member, + } + } +} + +impl WebSocketEvent for ThreadMemberUpdate {} + +#[derive(Debug, Default, Deserialize, Serialize)] +/// See https://discord.com/developers/docs/topics/gateway-events#thread-members-update +pub struct ThreadMembersUpdate { + pub id: String, + pub guild_id: String, + /// Capped at 50 + pub member_count: u8, + pub added_members: Option>, + pub removed_members: Option>, +} + +impl WebSocketEvent for ThreadMembersUpdate {} diff --git a/src/types/events/user.rs b/src/types/events/user.rs new file mode 100644 index 0000000..4762121 --- /dev/null +++ b/src/types/events/user.rs @@ -0,0 +1,12 @@ +use crate::types::entities::User; +use crate::types::events::WebSocketEvent; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Deserialize, Serialize)] +/// See https://discord.com/developers/docs/topics/gateway-events#user-update +/// Not directly serialized, as the inner payload is the user object +pub struct UserUpdate { + pub user: User, +} + +impl WebSocketEvent for UserUpdate {} diff --git a/src/types/events/voice_status.rs b/src/types/events/voice_status.rs new file mode 100644 index 0000000..aea7e0c --- /dev/null +++ b/src/types/events/voice_status.rs @@ -0,0 +1,13 @@ +use crate::types::events::WebSocketEvent; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Default)] +/// See https://discord.com/developers/docs/topics/gateway-events#update-voice-state-gateway-voice-state-update-structure +pub struct GatewayVoiceStateUpdate { + pub guild_id: String, + pub channel_id: Option, + pub self_mute: bool, + pub self_deaf: bool, +} + +impl WebSocketEvent for GatewayVoiceStateUpdate {} diff --git a/src/types/interfaces/activity.rs b/src/types/interfaces/activity.rs new file mode 100644 index 0000000..42ecc43 --- /dev/null +++ b/src/types/interfaces/activity.rs @@ -0,0 +1,56 @@ +use crate::types::entities::Emoji; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Activity { + name: String, + #[serde(rename = "type")] + activity_type: i32, + url: Option, + created_at: i64, + timestamps: Option, + application_id: Option, + details: Option, + state: Option, + emoji: Option, + party: Option, + assets: Option, + secrets: Option, + instance: Option, + flags: Option, + buttons: Option>, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct ActivityTimestamps { + start: Option, + end: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct ActivityParty { + id: Option, + size: Option>, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct ActivityAssets { + large_image: Option, + large_text: Option, + small_image: Option, + small_text: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct ActivitySecrets { + join: Option, + spectate: Option, + #[serde(rename = "match")] + match_string: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct ActivityButton { + label: String, + url: String, +} diff --git a/src/types/interfaces/connected_account.rs b/src/types/interfaces/connected_account.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/types/interfaces/guild_welcome_screen.rs b/src/types/interfaces/guild_welcome_screen.rs new file mode 100644 index 0000000..f799b1d --- /dev/null +++ b/src/types/interfaces/guild_welcome_screen.rs @@ -0,0 +1,17 @@ +use crate::types::utils::Snowflake; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct WelcomeScreenObject { + pub enabled: bool, + pub description: Option, + pub welcome_channels: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct WelcomeScreenChannel { + pub channel_id: Snowflake, + pub description: String, + pub emoji_id: Option, + pub emoji_name: Option, +} diff --git a/src/types/interfaces/interaction.rs b/src/types/interfaces/interaction.rs new file mode 100644 index 0000000..76b0361 --- /dev/null +++ b/src/types/interfaces/interaction.rs @@ -0,0 +1,39 @@ +use crate::types::entities::{AllowedMention, Embed}; +use crate::types::utils::Snowflake; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Interaction { + pub id: Snowflake, + pub r#type: InteractionType, + pub data: Value, + pub guild_id: Snowflake, + pub channel_id: Snowflake, + pub member_id: Snowflake, + pub token: String, + pub version: i32, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum InteractionType { + SelfCommand = 0, + Ping = 1, + ApplicationCommand = 2, +} + +pub enum InteractionResponseType { + SelfCommandResponse = 0, + Pong = 1, + Acknowledge = 2, + ChannelMessage = 3, + ChannelMessageWithSource = 4, + AcknowledgeWithSource = 5, +} + +pub struct InteractionApplicationCommandCallbackData { + pub tts: bool, + pub content: String, + pub embeds: Vec, + pub allowed_mentions: AllowedMention, +} diff --git a/src/types/interfaces/mod.rs b/src/types/interfaces/mod.rs new file mode 100644 index 0000000..6e0a7df --- /dev/null +++ b/src/types/interfaces/mod.rs @@ -0,0 +1,11 @@ +mod activity; +mod connected_account; +mod guild_welcome_screen; +mod interaction; +mod status; + +pub use activity::*; +pub use connected_account::*; +pub use guild_welcome_screen::*; +pub use interaction::*; +pub use status::*; \ No newline at end of file diff --git a/src/types/interfaces/status.rs b/src/types/interfaces/status.rs new file mode 100644 index 0000000..105dd87 --- /dev/null +++ b/src/types/interfaces/status.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Default, Clone)] +/// See https://discord.com/developers/docs/topics/gateway-events#client-status-object +pub struct ClientStatusObject { + pub desktop: Option, + pub mobile: Option, + pub web: Option, +} \ No newline at end of file diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..9148416 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,14 @@ +pub use config::*; +pub use errors::*; +pub use events::*; +pub use interfaces::*; +pub use schema::*; +pub use utils::*; + +mod config; +mod entities; +mod errors; +mod events; +mod interfaces; +mod schema; +mod utils; diff --git a/src/types/schema/apierror.rs b/src/types/schema/apierror.rs new file mode 100644 index 0000000..95e72a5 --- /dev/null +++ b/src/types/schema/apierror.rs @@ -0,0 +1,72 @@ +#[cfg(feature = "poem")] +use poem::{http::StatusCode, IntoResponse, Response}; +use serde_json::{json, Value}; + +#[derive(Debug, thiserror::Error)] +pub enum APIError { + #[error(transparent)] + Auth(#[from] AuthError), +} + +impl APIError { + pub fn error_payload(&self) -> Value { + match self { + APIError::Auth(auth_err) => auth_err.error_payload(), + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum AuthError { + #[error("INVALID_LOGIN")] + InvalidLogin, + #[error("INVALID_CAPTCHA")] + InvalidCaptcha, +} + +impl AuthError { + pub fn error_code(&self) -> &str { + match self { + AuthError::InvalidLogin => "INVALID_LOGIN", + AuthError::InvalidCaptcha => "INVALID_CATPCA", + } + } + + pub fn error_payload(&self) -> Value { + match self { + AuthError::InvalidLogin => json!({ + "login": { + "message": "auth:login.INVALID_LOGIN", + "code": self.error_code() + } + }), + AuthError::InvalidCaptcha => json!([json!({ + "captcha_key": "TODO", + "captcha_sitekey": "TODO", + "captcha_service": "TODO" + })]), + } + } +} + +#[cfg(feature = "poem")] +impl poem::error::ResponseError for APIError { + fn status(&self) -> StatusCode { + match self { + APIError::Auth(auth_err) => match auth_err { + AuthError::InvalidLogin => StatusCode::UNAUTHORIZED, + AuthError::InvalidCaptcha => StatusCode::BAD_REQUEST, + }, + } + } + + fn as_response(&self) -> Response + where + Self: std::error::Error + Send + Sync + 'static, + { + Response::builder() + .status(self.status()) + .body(self.error_payload().to_string()) + .into_response() + } +} diff --git a/src/types/schema/auth.rs b/src/types/schema/auth.rs new file mode 100644 index 0000000..073d8d4 --- /dev/null +++ b/src/types/schema/auth.rs @@ -0,0 +1,240 @@ +use crate::errors::FieldFormatError; +use regex::Regex; +use serde::{Deserialize, Serialize}; + +/** +A struct that represents a well-formed email address. + */ +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct AuthEmail { + pub email: String, +} + +impl AuthEmail { + /** + Returns a new [`Result`]. + ## Arguments + The email address you want to validate. + ## Errors + You will receive a [`FieldFormatError`], if: + - The email address is not in a valid format. + + */ + pub fn new(email: String) -> Result { + let regex = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap(); + if !regex.is_match(email.as_str()) { + return Err(FieldFormatError::EmailError); + } + Ok(AuthEmail { email }) + } +} + +/** +A struct that represents a well-formed username. +## Arguments +Please use new() to create a new instance of this struct. +## Errors +You will receive a [`FieldFormatError`], if: +- The username is not between 2 and 32 characters. + */ +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct AuthUsername { + pub username: String, +} + +impl AuthUsername { + /** + Returns a new [`Result`]. + ## Arguments + The username you want to validate. + ## Errors + You will receive a [`FieldFormatError`], if: + - The username is not between 2 and 32 characters. + */ + pub fn new(username: String) -> Result { + if username.len() < 2 || username.len() > 32 { + Err(FieldFormatError::UsernameError) + } else { + Ok(AuthUsername { username }) + } + } +} + +/** +A struct that represents a well-formed password. +## Arguments +Please use new() to create a new instance of this struct. +## Errors +You will receive a [`FieldFormatError`], if: +- The password is not between 1 and 72 characters. + */ +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct AuthPassword { + pub password: String, +} + +impl AuthPassword { + /** + Returns a new [`Result`]. + ## Arguments + The password you want to validate. + ## Errors + You will receive a [`FieldFormatError`], if: + - The password is not between 1 and 72 characters. + */ + pub fn new(password: String) -> Result { + if password.is_empty() || password.len() > 72 { + Err(FieldFormatError::PasswordError) + } else { + Ok(AuthPassword { password }) + } + } +} + +/** +A struct that represents a well-formed register request. +## Arguments +Please use new() to create a new instance of this struct. +## Errors +You will receive a [`FieldFormatError`], if: +- The username is not between 2 and 32 characters. +- The password is not between 1 and 72 characters. + */ + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub struct RegisterSchema { + username: String, + password: Option, + consent: bool, + email: Option, + fingerprint: Option, + invite: Option, + date_of_birth: Option, + gift_code_sku_id: Option, + captcha_key: Option, + promotional_email_opt_in: Option, +} + +impl RegisterSchema { + /** + Returns a new [`Result`]. + ## Arguments + All but "String::username" and "bool::consent" are optional. + + ## Errors + You will receive a [`FieldFormatError`], if: + - The username is less than 2 or more than 32 characters in length + - You supply a `password` which is less than 1 or more than 72 characters in length. + + These constraints have been defined [in the Spacebar-API](https://docs.spacebar.chat/routes/) + */ + pub fn new( + username: String, + password: Option, + consent: bool, + email: Option, + fingerprint: Option, + invite: Option, + date_of_birth: Option, + gift_code_sku_id: Option, + captcha_key: Option, + promotional_email_opt_in: Option, + ) -> Result { + let username = AuthUsername::new(username)?.username; + + let email = if let Some(email) = email { + Some(AuthEmail::new(email)?.email) + } else { + None + }; + + let password = if let Some(password) = password { + Some(AuthPassword::new(password)?.password) + } else { + None + }; + + if !consent { + return Err(FieldFormatError::ConsentError); + } + + Ok(RegisterSchema { + username, + password, + consent, + email, + fingerprint, + invite, + date_of_birth, + gift_code_sku_id, + captcha_key, + promotional_email_opt_in, + }) + } +} + +/** +A struct that represents a well-formed login request. +## Arguments +Please use new() to create a new instance of this struct. +## Errors +You will receive a [`FieldFormatError`], if: +- The username is not between 2 and 32 characters. +- The password is not between 1 and 72 characters. + */ +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub struct LoginSchema { + pub login: String, + pub password: String, + pub undelete: Option, + pub captcha_key: Option, + pub login_source: Option, + pub gift_code_sku_id: Option, +} + +impl LoginSchema { + /** + Returns a new [`Result`]. + ## Arguments + login: The username you want to login with. + password: The password you want to login with. + undelete: Honestly no idea what this is for. + captcha_key: The captcha key you want to login with. + login_source: The login source. + gift_code_sku_id: The gift code sku id. + ## Errors + You will receive a [`FieldFormatError`], if: + - The username is less than 2 or more than 32 characters in length + */ + pub fn new( + login: String, + password: String, + undelete: Option, + captcha_key: Option, + login_source: Option, + gift_code_sku_id: Option, + ) -> Result { + let login = AuthUsername::new(login)?.username; + let password = AuthPassword::new(password)?.password; + + Ok(LoginSchema { + login, + password, + undelete, + captcha_key, + login_source, + gift_code_sku_id, + }) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct TotpSchema { + code: String, + ticket: String, + gift_code_sku_id: Option, + login_source: Option, +} diff --git a/src/types/schema/channel.rs b/src/types/schema/channel.rs new file mode 100644 index 0000000..458c33c --- /dev/null +++ b/src/types/schema/channel.rs @@ -0,0 +1,27 @@ +use serde::{Deserialize, Serialize}; + +use crate::types::entities::PermissionOverwrite; + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct ChannelCreateSchema { + pub name: String, + #[serde(rename = "type")] + pub channel_type: Option, + pub topic: Option, + pub icon: Option, + pub bitrate: Option, + pub user_limit: Option, + pub rate_limit_per_user: Option, + pub position: Option, + pub permission_overwrites: Option>, + pub parent_id: Option, + pub id: Option, + pub nsfw: Option, + pub rtc_region: Option, + pub default_auto_archive_duration: Option, + pub default_reaction_emoji: Option, + pub flags: Option, + pub default_thread_rate_limit_per_user: Option, + pub video_quality_mode: Option, +} diff --git a/src/types/schema/guild.rs b/src/types/schema/guild.rs new file mode 100644 index 0000000..4b8b524 --- /dev/null +++ b/src/types/schema/guild.rs @@ -0,0 +1,14 @@ +use crate::types::entities::Channel; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct GuildCreateSchema { + pub name: Option, + pub region: Option, + pub icon: Option, + pub channels: Option>, + pub guild_template_code: Option, + pub system_channel_id: Option, + pub rules_channel_id: Option, +} diff --git a/src/types/schema/message.rs b/src/types/schema/message.rs new file mode 100644 index 0000000..6121f5f --- /dev/null +++ b/src/types/schema/message.rs @@ -0,0 +1,49 @@ +use crate::types::entities::{ + AllowedMention, Component, Embed, MessageReference, PartialDiscordFileAttachment, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct MessageSendSchema { + #[serde(rename = "type")] + message_type: Option, + content: Option, + nonce: Option, + tts: Option, + embeds: Option>, + allowed_mentions: Option, + message_reference: Option, + components: Option>, + sticker_ids: Option>, + pub attachments: Option>, +} + +// make a new() method for MessageSendSchema +impl MessageSendSchema { + pub fn new( + message_type: Option, + content: Option, + nonce: Option, + tts: Option, + embeds: Option>, + allowed_mentions: Option, + message_reference: Option, + components: Option>, + sticker_ids: Option>, + attachments: Option>, + ) -> MessageSendSchema { + MessageSendSchema { + message_type, + content, + nonce, + tts, + embeds, + allowed_mentions, + message_reference, + components, + sticker_ids, + attachments, + } + } +} diff --git a/src/types/schema/mod.rs b/src/types/schema/mod.rs new file mode 100644 index 0000000..6fe3e37 --- /dev/null +++ b/src/types/schema/mod.rs @@ -0,0 +1,103 @@ +mod apierror; +mod auth; +mod channel; +mod guild; +mod message; +mod user; + +pub use apierror::*; +pub use auth::*; +pub use channel::*; +pub use guild::*; +pub use message::*; +pub use user::*; + +#[cfg(test)] +mod schemas_tests { + use super::*; + use crate::errors::FieldFormatError; + + #[test] + fn password_too_short() { + assert_eq!( + AuthPassword::new("".to_string()), + Err(FieldFormatError::PasswordError) + ); + } + + #[test] + fn password_too_long() { + let mut long_pw = String::new(); + for _ in 0..73 { + long_pw += "a"; + } + assert_eq!( + AuthPassword::new(long_pw), + Err(FieldFormatError::PasswordError) + ); + } + + #[test] + fn username_too_short() { + assert_eq!( + AuthUsername::new("T".to_string()), + Err(FieldFormatError::UsernameError) + ); + } + + #[test] + fn username_too_long() { + let mut long_un = String::new(); + for _ in 0..33 { + long_un += "a"; + } + assert_eq!( + AuthUsername::new(long_un), + Err(FieldFormatError::UsernameError) + ); + } + + #[test] + fn consent_false() { + assert_eq!( + RegisterSchema::new( + "Test".to_string(), + None, + false, + None, + None, + None, + None, + None, + None, + None, + ), + Err(FieldFormatError::ConsentError) + ); + } + + #[test] + fn invalid_email() { + assert_eq!( + AuthEmail::new("p@p.p".to_string()), + Err(FieldFormatError::EmailError) + ) + } + + #[test] + fn valid_email() { + let reg = RegisterSchema::new( + "Testy".to_string(), + None, + true, + Some("me@mail.de".to_string()), + None, + None, + None, + None, + None, + None, + ); + assert_ne!(reg, Err(FieldFormatError::EmailError)); + } +} diff --git a/src/types/schema/user.rs b/src/types/schema/user.rs new file mode 100644 index 0000000..00f4962 --- /dev/null +++ b/src/types/schema/user.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct UserModifySchema { + pub username: Option, + pub avatar: Option, + pub bio: Option, + pub accent_color: Option, + pub banner: Option, + pub current_password: Option, + pub new_password: Option, + pub code: Option, + pub email: Option, + pub discriminator: Option, +} \ No newline at end of file diff --git a/src/types/utils/jwt.rs b/src/types/utils/jwt.rs new file mode 100644 index 0000000..c9f1aa5 --- /dev/null +++ b/src/types/utils/jwt.rs @@ -0,0 +1,44 @@ +use jsonwebtoken::{encode, EncodingKey, Header}; +use serde::{Deserialize, Serialize}; + +use crate::types::utils::Snowflake; + +pub fn generate_token(id: &Snowflake, email: String, jwt_key: &str) -> String { + let claims = Claims::new(&email, id); + + build_token(&claims, jwt_key).unwrap() +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Claims { + pub exp: i64, + pub iat: i64, + pub email: String, + pub id: String, +} + +impl Claims { + pub fn new(user: &str, id: &Snowflake) -> Self { + let unix = chrono::Utc::now().timestamp(); + Self { + exp: unix + (60 * 60 * 24), + id: id.to_string(), + iat: unix, + email: user.to_string(), + } + } +} + +pub fn build_token(claims: &Claims, jwt_key: &str) -> Result { + encode( + &Header::default(), + claims, + &EncodingKey::from_secret(jwt_key.as_bytes()), + ) +} + +/*pub fn decode_token(token: &str) -> Result, Error> { + let mut validation = Validation::new(Algorithm::HS256); + validation.sub = Some("quartzauth".to_string()); + decode(token, &DecodingKey::from_secret(JWT_SECRET), &validation) +}*/ diff --git a/src/types/utils/mod.rs b/src/types/utils/mod.rs new file mode 100644 index 0000000..ec2fd4a --- /dev/null +++ b/src/types/utils/mod.rs @@ -0,0 +1,8 @@ +pub mod jwt; +mod regexes; +mod rights; +mod snowflake; + +pub use regexes::*; +pub use rights::Rights; +pub use snowflake::{DeconstructedSnowflake, Snowflake}; diff --git a/src/types/utils/regexes.rs b/src/types/utils/regexes.rs new file mode 100644 index 0000000..0f160eb --- /dev/null +++ b/src/types/utils/regexes.rs @@ -0,0 +1,12 @@ +use lazy_static::lazy_static; +use regex::Regex; + +lazy_static! { + static ref DOUBLE_WHITE_SPACE_RE: Regex = Regex::new(r"\s\s+").unwrap(); + static ref SPECIAL_CHAR: Regex = Regex::new(r"@#`:\r\n\t\f\v\p{C}").unwrap(); + static ref CHANNEL_MENTION: Regex = Regex::new(r"<#(\d+)>").unwrap(); + static ref USER_MENTION: Regex = Regex::new(r"<@!?(\d+)>").unwrap(); + static ref ROLE_MENTION: Regex = Regex::new(r"<@&(\d+)>").unwrap(); + static ref EVERYONE_MENTION: Regex = Regex::new(r"@everyone").unwrap(); + static ref HERE_MENTION: Regex = Regex::new(r"@here").unwrap(); +} diff --git a/src/types/utils/rights.rs b/src/types/utils/rights.rs new file mode 100644 index 0000000..0198af6 --- /dev/null +++ b/src/types/utils/rights.rs @@ -0,0 +1,127 @@ +use bitflags::bitflags; + +bitflags! { + pub struct Rights: u64 { + const OPERATOR = 1 << 0; + const MANAGE_APPLICATIONS = 1 << 1; + const MANAGE_GUILDS = 1 << 2; + const MANAGE_MESSAGES = 1 << 3; + const MANAGE_RATE_LIMITS = 1 << 4; + const MANAGE_ROUTING = 1 << 5; + const MANAGE_TICKETS = 1 << 6; + const MANAGE_USERS = 1 << 7; + const ADD_MEMBERS = 1 << 8; + const BYPASS_RATE_LIMITS = 1 << 9; + const CREATE_APPLICATIONS = 1 << 10; + const CREATE_CHANNELS = 1 << 11; + const CREATE_DMS = 1 << 12; + const CREATE_DM_GROUPS = 1 << 13; + const CREATE_GUILDS = 1 << 14; + const CREATE_INVITES = 1 << 15; + const CREATE_ROLES = 1 << 16; + const CREATE_TEMPLATES = 1 << 17; + const CREATE_WEBHOOKS = 1 << 18; + const JOIN_GUILDS = 1 << 19; + const PIN_MESSAGES = 1 << 20; + const SELF_ADD_REACTIONS = 1 << 21; + const SELF_DELETE_MESSAGES = 1 << 22; + const SELF_EDIT_MESSAGES = 1 << 23; + const SELF_EDIT_NAME = 1 << 24; + const SEND_MESSAGES = 1 << 25; + const USE_ACTIVITIES = 1 << 26; + const USE_VIDEO = 1 << 27; + const USE_VOICE = 1 << 28; + const INVITE_USERS = 1 << 29; + const SELF_DELETE_DISABLE = 1 << 30; + const DEBTABLE = 1 << 31; + const CREDITABLE = 1 << 32; + const KICK_BAN_MEMBERS = 1 << 33; + const SELF_LEAVE_GROUPS = 1 << 34; + const PRESENCE = 1 << 35; + const SELF_ADD_DISCOVERABLE = 1 << 36; + const MANAGE_GUILD_DIRECTORY = 1 << 37; + const POGGERS = 1 << 38; + const USE_ACHIEVEMENTS = 1 << 39; + const INITIATE_INTERACTIONS = 1 << 40; + const RESPOND_TO_INTERACTIONS = 1 << 41; + const SEND_BACKDATED_EVENTS = 1 << 42; + const USE_MASS_INVITES = 1 << 43; + const ACCEPT_INVITES = 1 << 44; + const SELF_EDIT_FLAGS = 1 << 45; + const EDIT_FLAGS = 1 << 46; + const MANAGE_GROUPS = 1 << 47; + const VIEW_SERVER_STATS = 1 << 48; + const RESEND_VERIFICATION_EMAIL = 1 << 49; + } +} + +impl Rights { + pub fn any(&self, permission: Rights, check_operator: bool) -> bool { + (check_operator && self.contains(Rights::OPERATOR)) || self.contains(permission) + } + + pub fn has(&self, permission: Rights, check_operator: bool) -> bool { + (check_operator && self.contains(Rights::OPERATOR)) || self.contains(permission) + } + + pub fn has_throw(&self, permission: Rights) -> Result { + if self.has(permission, true) { + Ok(true) + } else { + Err("You are missing the following rights") + } + } +} + +fn all_rights() -> Rights { + Rights::OPERATOR + | Rights::MANAGE_APPLICATIONS + | Rights::MANAGE_GUILDS + | Rights::MANAGE_MESSAGES + | Rights::MANAGE_RATE_LIMITS + | Rights::MANAGE_ROUTING + | Rights::MANAGE_TICKETS + | Rights::MANAGE_USERS + | Rights::ADD_MEMBERS + | Rights::BYPASS_RATE_LIMITS + | Rights::CREATE_APPLICATIONS + | Rights::CREATE_CHANNELS + | Rights::CREATE_DMS + | Rights::CREATE_DM_GROUPS + | Rights::CREATE_GUILDS + | Rights::CREATE_INVITES + | Rights::CREATE_ROLES + | Rights::CREATE_TEMPLATES + | Rights::CREATE_WEBHOOKS + | Rights::JOIN_GUILDS + | Rights::PIN_MESSAGES + | Rights::SELF_ADD_REACTIONS + | Rights::SELF_DELETE_MESSAGES + | Rights::SELF_EDIT_MESSAGES + | Rights::SELF_EDIT_NAME + | Rights::SEND_MESSAGES + | Rights::USE_ACTIVITIES + | Rights::USE_VIDEO + | Rights::USE_VOICE + | Rights::INVITE_USERS + | Rights::SELF_DELETE_DISABLE + | Rights::DEBTABLE + | Rights::CREDITABLE + | Rights::KICK_BAN_MEMBERS + | Rights::SELF_LEAVE_GROUPS + | Rights::PRESENCE + | Rights::SELF_ADD_DISCOVERABLE + | Rights::MANAGE_GUILD_DIRECTORY + | Rights::POGGERS + | Rights::USE_ACHIEVEMENTS + | Rights::INITIATE_INTERACTIONS + | Rights::RESPOND_TO_INTERACTIONS + | Rights::SEND_BACKDATED_EVENTS + | Rights::USE_MASS_INVITES + | Rights::ACCEPT_INVITES + | Rights::SELF_EDIT_FLAGS + | Rights::EDIT_FLAGS + | Rights::MANAGE_GROUPS + | Rights::VIEW_SERVER_STATS + | Rights::RESEND_VERIFICATION_EMAIL +} diff --git a/src/types/utils/snowflake.rs b/src/types/utils/snowflake.rs new file mode 100644 index 0000000..7c756fa --- /dev/null +++ b/src/types/utils/snowflake.rs @@ -0,0 +1,160 @@ +use std::fmt::Display; + +use atomic::Atomic; +use bigdecimal::{Num, ToPrimitive, Zero}; +use num_bigint::{BigInt, ToBigInt}; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "sqlx")] +use sqlx::Type; + +const EPOCH: i64 = 1420070400000; +static WORKER_ID: u128 = 0; +static PROCESS_ID: u128 = 1; +lazy_static::lazy_static! { + static ref INCREMENT: Atomic = Atomic::default(); +} +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "sqlx", derive(Type))] +#[cfg_attr(feature = "sqlx", sqlx(transparent))] +pub struct Snowflake(String); + +impl Default for Snowflake { + fn default() -> Self { + Self::generate() + } +} + +impl Display for Snowflake { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Snowflake { + pub fn to_binary(&self) -> String { + let self_len = self.0.len(); + let high = self.0[..self_len - 10].parse::().unwrap_or(0); + let low = self.0[self_len - 10..].parse::().unwrap(); + let mut low = low; + let mut high = high; + let mut bin = Vec::with_capacity(64); + + while low > 0 || high > 0 { + bin.push((low & 1) as u8); + low >>= 1; + + if high > 0 { + low += 5_000_000_000 * (high % 2); + high >>= 1; + } + } + + bin.iter() + .rev() + .map(|b| char::from_digit(*b as u32, 10).unwrap()) + .collect() + } + + pub fn from_binary(num: &str) -> String { + let mut num = BigInt::from_str_radix(num, 2).unwrap(); + let mut dec = Vec::with_capacity(18); + + let ten = 10.to_bigint().unwrap(); + let _two = 2.to_bigint().unwrap(); + let _thirty_two = 32.to_bigint().unwrap(); + + while num.bits() > 50 { + let high: BigInt = &num >> 32; + let low: BigInt = (high.clone() % &ten) << 32 | &num & BigInt::from((1u64 << 32) - 1); + + let next: BigInt = low.clone() % &ten; + dec.push(next.to_u8().unwrap()); + num = (high / &ten) << 32 | (low / &ten); + } + + while !num.is_zero() { + dec.push((num.clone() % &ten).to_u8().unwrap()); + num /= &ten; + } + + dec.iter() + .rev() + .map(|d| char::from_digit(*d as u32, 10).unwrap()) + .collect() + } + + pub fn generate_worker_process() -> u128 { + let time = (chrono::Utc::now().naive_utc().timestamp_millis() - EPOCH) << 22; + let worker = WORKER_ID << 17; + let process = PROCESS_ID << 12; + let increment = INCREMENT.load(atomic::Ordering::Relaxed); + + INCREMENT.store(increment + 1, atomic::Ordering::Relaxed); + + time as u128 | worker | process | increment + } + + pub fn generate() -> Self { + Self(Self::generate_worker_process().to_string()) + } + + pub fn deconstruct(&self) -> DeconstructedSnowflake { + let binary = format!("{:0>64}", self.to_binary()); + + let ts = i64::from_str_radix(&binary[0..42], 2).unwrap() + EPOCH; + let wid = u64::from_str_radix(&binary[42..47], 2).unwrap(); + let pid = u64::from_str_radix(&binary[47..52], 2).unwrap(); + let increment = BigInt::from_str_radix(&binary[52..64], 2).unwrap(); + + DeconstructedSnowflake { + timestamp: ts, + worker_id: wid, + process_id: pid, + increment, + binary, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeconstructedSnowflake { + pub timestamp: i64, + pub worker_id: u64, + pub process_id: u64, + pub increment: BigInt, + pub binary: String, +} + +#[cfg(test)] +mod test { + use super::Snowflake; + + #[test] + fn test_new_snowflake() { + let snow = Snowflake::generate(); + println!("{snow}"); + } + + #[test] + fn snowflake_to_binary() { + let snowflake = super::Snowflake("1104339392517902336".to_string()); + + let bin = snowflake.to_binary(); + println!("{bin}"); + } + + #[test] + fn binary_to_snowflake() { + let snowflake = super::Snowflake::from_binary( + "111101010011011001101101001110010010100000000001000000000000", + ); + println!("{snowflake}"); + } + + #[test] + fn test_deconstruct() { + let new = super::Snowflake::generate(); + + println!("{:?}", new.deconstruct()); + } +}