From 39e7f89c785310daa2a089c5f72c9b579a346ff8 Mon Sep 17 00:00:00 2001 From: Quat3rnion <81202811+Quat3rnion@users.noreply.github.com> Date: Thu, 27 Jun 2024 02:45:51 -0400 Subject: [PATCH] Backend/guilds (#509) * Fix SQL encode/decode for GuildFeatures * Use distinct PermissionFlags type * Add Emoji schema types, modify GuildBan with feature lock * Add Schemas for pruning guild members * Add schemas for interfacing with stickers backend routes * Add schemas for interfacing with vanity-url backend routes * Add schema for interfacing with guilds/id/welcome-screen route * Make all Option types Vec types with #[serde(default)] * Add various types to support guilds/* api routes * Add missing enums and structs for searching messages * Use proper distinct types * Add EmbedType enum * Use distinct PermissionFlags type * Changes supporting backend for VoiceState * Changes supporting backend for AuditLog's --- Cargo.lock | 19 ++ Cargo.toml | 2 +- src/types/config/types/guild_configuration.rs | 35 +-- src/types/entities/audit_log.rs | 177 +++++++++++++- src/types/entities/guild.rs | 32 ++- src/types/entities/guild_member.rs | 5 +- src/types/entities/integration.rs | 14 +- src/types/entities/invite.rs | 2 +- src/types/entities/message.rs | 24 +- src/types/entities/role.rs | 5 +- src/types/entities/sticker.rs | 79 ++++++- src/types/entities/voice_state.rs | 2 + src/types/events/channel.rs | 12 +- src/types/events/guild.rs | 12 +- src/types/schema/audit_log.rs | 23 ++ src/types/schema/guild.rs | 219 +++++++++++++++++- src/types/schema/invites.rs | 16 ++ src/types/schema/message.rs | 81 ++++++- src/types/schema/mod.rs | 10 + src/types/schema/role.rs | 7 +- src/types/schema/voice_state.rs | 15 ++ tests/common/mod.rs | 4 +- tests/gateway.rs | 18 +- tests/roles.rs | 3 +- 24 files changed, 712 insertions(+), 104 deletions(-) create mode 100644 src/types/schema/audit_log.rs create mode 100644 src/types/schema/voice_state.rs diff --git a/Cargo.lock b/Cargo.lock index bb74a32..f967a21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1272,6 +1272,24 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.1.0", + "httparse", + "memchr", + "mime", + "spin 0.9.8", + "tokio", + "version_check", +] + [[package]] name = "native-tls" version = "0.2.11" @@ -1609,6 +1627,7 @@ dependencies = [ "hyper 1.3.1", "hyper-util", "mime", + "multer", "nix 0.28.0", "parking_lot", "percent-encoding", diff --git a/Cargo.toml b/Cargo.toml index a1ab80d..f2e5f12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ http = "0.2.11" base64 = "0.21.7" bitflags = { version = "2.4.1", features = ["serde"] } lazy_static = "1.4.0" -poem = { version = "3.0.1", optional = true } +poem = { version = "3.0.1", features = ["multipart"], optional = true } thiserror = "1.0.56" jsonwebtoken = "8.3.0" log = "0.4.20" diff --git a/src/types/config/types/guild_configuration.rs b/src/types/config/types/guild_configuration.rs index d2b3f16..53a2d45 100644 --- a/src/types/config/types/guild_configuration.rs +++ b/src/types/config/types/guild_configuration.rs @@ -3,19 +3,10 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. use std::fmt::{Display, Formatter}; -#[cfg(feature = "sqlx")] -use std::io::Write; use std::ops::{Deref, DerefMut}; use std::str::FromStr; use serde::{Deserialize, Serialize}; -#[cfg(feature = "sqlx")] -use sqlx::{ - database::{HasArguments, HasValueRef}, - encode::IsNull, - error::BoxDynError, - Decode, MySql, -}; use crate::types::config::types::subconfigs::guild::{ autojoin::AutoJoinConfiguration, discovery::DiscoverConfiguration, @@ -172,8 +163,8 @@ impl Display for GuildFeaturesList { #[cfg(feature = "sqlx")] impl<'r> sqlx::Decode<'r, sqlx::MySql> for GuildFeaturesList { - fn decode(value: >::ValueRef) -> Result { - let v = <&str as Decode>::decode(value)?; + fn decode(value: >::ValueRef) -> Result { + let v = >::decode(value)?; Ok(Self( v.split(',') .filter(|f| !f.is_empty()) @@ -185,9 +176,9 @@ impl<'r> sqlx::Decode<'r, sqlx::MySql> for GuildFeaturesList { #[cfg(feature = "sqlx")] impl<'q> sqlx::Encode<'q, sqlx::MySql> for GuildFeaturesList { - fn encode_by_ref(&self, buf: &mut >::ArgumentBuffer) -> IsNull { + fn encode_by_ref(&self, buf: &mut >::ArgumentBuffer) -> sqlx::encode::IsNull { if self.is_empty() { - return IsNull::Yes; + return sqlx::encode::IsNull::Yes; } let features = self .iter() @@ -195,30 +186,18 @@ impl<'q> sqlx::Encode<'q, sqlx::MySql> for GuildFeaturesList { .collect::>() .join(","); - let _ = buf.write(features.as_bytes()); - IsNull::No + >::encode_by_ref(&features, buf) } } #[cfg(feature = "sqlx")] impl sqlx::Type for GuildFeaturesList { fn type_info() -> sqlx::mysql::MySqlTypeInfo { - <&str as sqlx::Type>::type_info() + >::type_info() } fn compatible(ty: &sqlx::mysql::MySqlTypeInfo) -> bool { - <&str as sqlx::Type>::compatible(ty) - } -} - -#[cfg(feature = "sqlx")] -impl sqlx::TypeInfo for GuildFeaturesList { - fn is_null(&self) -> bool { - false - } - - fn name(&self) -> &str { - "TEXT" + >::compatible(ty) } } diff --git a/src/types/entities/audit_log.rs b/src/types/entities/audit_log.rs index 566b223..48dc9d4 100644 --- a/src/types/entities/audit_log.rs +++ b/src/types/entities/audit_log.rs @@ -3,21 +3,27 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; -use crate::types::Shared; +use crate::types::{AutoModerationRuleTriggerType, IntegrationType, PermissionOverwriteType, Shared}; use crate::types::utils::Snowflake; #[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] /// See pub struct AuditLogEntry { pub target_id: Option, + #[cfg(feature = "sqlx")] + pub changes: sqlx::types::Json>>>, + #[cfg(not(feature = "sqlx"))] pub changes: Option>>, pub user_id: Option, pub id: Snowflake, - // to:do implement an enum for these types - pub action_type: u8, - // to:do add better options type - pub options: Option, + pub action_type: AuditLogActionType, + #[cfg(feature = "sqlx")] + pub options: Option>, + #[cfg(not(feature = "sqlx"))] + pub options: Option, pub reason: Option, } @@ -28,3 +34,164 @@ pub struct AuditLogChange { pub old_value: Option, pub key: String, } + + +#[derive(Default, Serialize_repr, Deserialize_repr, Debug, Clone, Copy)] +#[repr(u8)] +#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] +/// # Reference: +/// See +pub enum AuditLogActionType { + #[default] + /// Guild settings were updated + GuildUpdate = 1, + /// Channel was created + ChannelCreate = 10, + /// Channel settings were updated + ChannelUpdate = 11, + /// Channel was deleted + ChannelDelete = 12, + /// Permission overwrite was added to a channel + ChannelOverwriteCreate = 13, + /// Permission overwrite was updated for a channel + ChannelOverwriteUpdate = 14, + /// Permission overwrite was deleted from a channel + ChannelOverwriteDelete = 15, + /// Member was removed from guild + MemberKick = 20, + /// Members were pruned from guild + MemberPrune = 21, + /// Member was banned from guild + MemberBanAdd = 22, + /// Member was unbanned from guild + MemberBanRemove = 23, + /// Member was updated in guild + MemberUpdate = 24, + /// Member was added or removed from a role + MemberRoleUpdate = 25, + /// Member was moved to a different voice channel + MemberMove = 26, + /// Member was disconnected from a voice channel + MemberDisconnect = 27, + /// Bot user was added to guild + BotAdd = 28, + /// Role was created + RoleCreate = 30, + /// Role was edited + RoleUpdate = 31, + /// Role was deleted + RoleDelete = 32, + /// Guild invite was created + InviteCreate = 40, + /// Guild invite was updated + InviteUpdate = 41, + /// Guild invite was deleted + InviteDelete = 42, + /// Webhook was created + WebhookCreate = 50, + /// Webhook properties or channel were updated + WebhookUpdate = 51, + /// Webhook was deleted + WebhookDelete = 52, + /// Emoji was created + EmojiCreate = 60, + /// Emoji name was updated + EmojiUpdate = 61, + /// Emoji was deleted + EmojiDelete = 62, + /// Single message was deleted + MessageDelete = 72, + /// Multiple messages were deleted + MessageBulkDelete = 73, + /// Message was pinned to a channel + MessagePin = 74, + /// Message was unpinned from a channel + MessageUnpin = 75, + /// Interaction was added to guild + IntegrationCreate = 80, + /// Integration was updated (e.g. its scopes were updated) + IntegrationUpdate = 81, + /// Integration was removed from guild + IntegrationDelete = 82, + /// Stage instance was created (stage channel becomes live) + StageInstanceCreate = 83, + /// Stage instance details were updated + StageInstanceUpdate = 84, + /// Stage instance was deleted (stage channel no longer live) + StageInstanceDelete = 85, + /// Sticker was created + StickerCreate = 90, + /// Sticker details were updated + StickerUpdate = 91, + /// Sticker was deleted + StickerDelete = 92, + /// Event was created + GuildScheduledEventCreate = 100, + /// Event was updated + GuildScheduledEventUpdate = 101, + /// Event was cancelled + GuildScheduledEventDelete = 102, + /// Thread was created in a channel + ThreadCreate = 110, + /// Thread was updated + ThreadUpdate = 111, + /// Thread was deleted + ThreadDelete = 112, + /// Permissions were updated for a command + ApplicationCommandPermissionUpdate = 121, + /// AutoMod rule created + AutoModerationRuleCreate = 140, + /// AutoMod rule was updated + AutoModerationRuleUpdate = 141, + /// AutoMod rule was deleted + AutoModerationRuleDelete = 142, + /// Message was blocked by AutoMod + AutoModerationBlockMessage = 143, + /// Message was flagged by AutoMod + AutoModerationFlagToChannel = 144, + /// Member was timed out by AutoMod + AutoModerationUserCommunicationDisabled = 145, + /// Member was quarantined by AutoMod + AutoModerationQuarantineUser = 146, + /// Creator monetization request was created + CreatorMonetizationRequestCreated = 150, + /// Creator monetization terms were accepted + CreatorMonetizationTermsAccepted = 151, + /// Onboarding prompt was created + OnboardingPromptCreate = 163, + /// Onboarding prompt was updated + OnboardingPromptUpdate = 164, + /// Onboarding prompt was deleted + OnboardingPromptDelete = 165, + /// Onboarding was created + OnboardingCreate = 166, + /// Onboarding was updated + OnboardingUpdate = 167, + /// Voice channel status was updated + VoiceChannelStatusUpdate = 192, + /// Voice channel status was deleted + VoiceChannelStatusDelete = 193 +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct AuditEntryInfo { + pub application_id: Option, + pub auto_moderation_rule_name: Option, + pub auto_moderation_rule_trigger_type: Option, + pub channel_id: Option, + // #[serde(option_string)] + pub count: Option, + // #[serde(option_string)] + pub delete_member_days: Option, + /// The ID of the overwritten entity + pub id: Option, + pub integration_type: Option, + // #[serde(option_string)] + pub members_removed: Option, + // #[serde(option_string)] + pub message_id: Option, + pub role_name: Option, + #[serde(rename = "type")] + pub overwrite_type: Option, + pub status: Option +} \ No newline at end of file diff --git a/src/types/entities/guild.rs b/src/types/entities/guild.rs index 866b172..72e6a7e 100644 --- a/src/types/entities/guild.rs +++ b/src/types/entities/guild.rs @@ -23,7 +23,7 @@ use super::PublicUser; use crate::gateway::Updateable; #[cfg(feature = "client")] -use chorus_macros::{observe_option_vec, observe_vec, Composite, Updateable}; +use chorus_macros::{observe_vec, Composite, Updateable}; #[cfg(feature = "client")] use crate::types::Composite; @@ -46,10 +46,12 @@ pub struct Guild { pub approximate_presence_count: Option, pub banner: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub bans: Option>, + #[serde(default)] + pub bans: Vec, #[cfg_attr(feature = "sqlx", sqlx(skip))] - #[cfg_attr(feature = "client", observe_option_vec)] - pub channels: Option>>, + #[cfg_attr(feature = "client", observe_vec)] + #[serde(default)] + pub channels: Vec>, pub default_message_notifications: Option, pub description: Option, pub discovery_splash: Option, @@ -66,7 +68,8 @@ pub struct Guild { pub icon_hash: Option, pub id: Snowflake, #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub invites: Option>, + #[serde(default)] + pub invites: Vec, #[cfg_attr(feature = "sqlx", sqlx(skip))] pub joined_at: Option>, pub large: Option, @@ -92,25 +95,29 @@ pub struct Guild { pub public_updates_channel_id: Option, pub region: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] - #[cfg_attr(feature = "client", observe_option_vec)] - pub roles: Option>>, + #[cfg_attr(feature = "client", observe_vec)] + #[serde(default)] + pub roles: Vec>, #[cfg_attr(feature = "sqlx", sqlx(skip))] pub rules_channel: Option, pub rules_channel_id: Option, pub splash: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub stickers: Option>, + #[serde(default)] + pub stickers: Vec, pub system_channel_flags: Option, pub system_channel_id: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] pub vanity_url_code: Option, pub verification_level: Option, + #[serde(default)] #[cfg_attr(feature = "sqlx", sqlx(skip))] - #[cfg_attr(feature = "client", observe_option_vec)] - pub voice_states: Option>>, + #[cfg_attr(feature = "client", observe_vec)] + pub voice_states: Vec>, + #[serde(default)] #[cfg_attr(feature = "sqlx", sqlx(skip))] - #[cfg_attr(feature = "client", observe_option_vec)] - pub webhooks: Option>>, + #[cfg_attr(feature = "client", observe_vec)] + pub webhooks: Vec>, #[cfg(feature = "sqlx")] pub welcome_screen: sqlx::types::Json>, #[cfg(not(feature = "sqlx"))] @@ -226,6 +233,7 @@ impl std::cmp::PartialEq for Guild { #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] pub struct GuildBan { + #[cfg_attr(feature = "sqlx", sqlx(skip))] pub user: PublicUser, pub reason: Option, } diff --git a/src/types/entities/guild_member.rs b/src/types/entities/guild_member.rs index 522824c..5b1308d 100644 --- a/src/types/entities/guild_member.rs +++ b/src/types/entities/guild_member.rs @@ -5,7 +5,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use crate::types::{GuildMemberFlags, Shared}; +use crate::types::{GuildMemberFlags, PermissionFlags, Shared}; use crate::types::{entities::PublicUser, Snowflake}; #[derive(Debug, Deserialize, Default, Serialize, Clone)] @@ -27,6 +27,7 @@ pub struct GuildMember { pub mute: bool, pub flags: Option, pub pending: Option, - pub permissions: Option, + #[serde(default)] + pub permissions: PermissionFlags, pub communication_disabled_until: Option>, } diff --git a/src/types/entities/integration.rs b/src/types/entities/integration.rs index db4fecf..4b42f46 100644 --- a/src/types/entities/integration.rs +++ b/src/types/entities/integration.rs @@ -18,7 +18,7 @@ pub struct Integration { pub id: Snowflake, pub name: String, #[serde(rename = "type")] - pub integration_type: String, + pub integration_type: IntegrationType, pub enabled: bool, pub syncing: Option, pub role_id: Option, @@ -43,3 +43,15 @@ pub struct IntegrationAccount { pub id: String, pub name: String, } + +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] +#[cfg_attr(feature = "sqlx", sqlx(rename_all = "snake_case"))] +pub enum IntegrationType { + #[default] + Twitch, + Youtube, + Discord, + GuildSubscription, +} \ No newline at end of file diff --git a/src/types/entities/invite.rs b/src/types/entities/invite.rs index 388cc32..0160ac9 100644 --- a/src/types/entities/invite.rs +++ b/src/types/entities/invite.rs @@ -13,7 +13,7 @@ use super::{Application, Channel, GuildMember, NSFWLevel, User}; /// Represents a code that when used, adds a user to a guild or group DM channel, or creates a relationship between two users. /// See -#[derive(Debug, Serialize, Deserialize)] +#[derive(Default, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] pub struct Invite { #[cfg_attr(feature = "sqlx", sqlx(skip))] diff --git a/src/types/entities/message.rs b/src/types/entities/message.rs index fe7ff7b..0a8169b 100644 --- a/src/types/entities/message.rs +++ b/src/types/entities/message.rs @@ -177,7 +177,7 @@ pub struct ChannelMention { pub struct Embed { title: Option, #[serde(rename = "type")] - embed_type: Option, + embed_type: Option, description: Option, url: Option, timestamp: Option, @@ -191,6 +191,24 @@ pub struct Embed { fields: Option>, } +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "snake_case")] +pub enum EmbedType { + #[deprecated] + ApplicationNews, + Article, + AutoModerationMessage, + AutoModerationNotification, + Gift, + #[serde(rename = "gifv")] + GifVideo, + Image, + Link, + PostPreview, + Rich, + Video +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct EmbedFooter { text: String, @@ -291,7 +309,7 @@ pub enum MessageType { Default = 0, /// A message sent when a user is added to a group DM or thread RecipientAdd = 1, - /// A message sent when a user is removed from a group DM or thread + /// A message sent when a user is removed from a group DM or thread RecipientRemove = 2, /// A message sent when a user creates a call in a private channel Call = 3, @@ -335,7 +353,7 @@ pub enum MessageType { ThreadStarterMessage = 21, /// A message sent to remind users to invite friends to a guild GuildInviteReminder = 22, - /// A message sent when a user uses a context menu command + /// A message sent when a user uses a context menu command ContextMenuCommand = 23, /// A message sent when auto moderation takes an action AutoModerationAction = 24, diff --git a/src/types/entities/role.rs b/src/types/entities/role.rs index 7e804f5..8c00b41 100644 --- a/src/types/entities/role.rs +++ b/src/types/entities/role.rs @@ -4,7 +4,7 @@ use bitflags::bitflags; use serde::{Deserialize, Serialize}; -use serde_aux::prelude::{deserialize_option_number_from_string, deserialize_string_from_number}; +use serde_aux::prelude::deserialize_option_number_from_string; use std::fmt::Debug; use crate::types::utils::Snowflake; @@ -34,8 +34,7 @@ pub struct RoleObject { pub unicode_emoji: Option, pub position: u16, #[serde(default)] - #[serde(deserialize_with = "deserialize_string_from_number")] - pub permissions: String, + pub permissions: PermissionFlags, pub managed: bool, pub mentionable: bool, #[cfg(feature = "sqlx")] diff --git a/src/types/entities/sticker.rs b/src/types/entities/sticker.rs index 7048f82..22affcb 100644 --- a/src/types/entities/sticker.rs +++ b/src/types/entities/sticker.rs @@ -3,6 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::types::{entities::User, utils::Snowflake, Shared}; @@ -18,15 +19,17 @@ pub struct Sticker { pub pack_id: Option, pub name: String, pub description: Option, - pub tags: String, + pub tags: Option, + #[cfg_attr(feature = "sqlx", sqlx(skip))] pub asset: Option, #[serde(rename = "type")] - pub sticker_type: u8, - pub format_type: u8, + pub sticker_type: StickerType, + pub format_type: StickerFormatType, pub available: Option, pub guild_id: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] pub user: Option>, + #[cfg_attr(feature = "sqlx", sqlx(skip))] pub sort_value: Option, } @@ -108,6 +111,18 @@ impl PartialOrd for Sticker { } } +impl Sticker { + pub fn tags(&self) -> Vec { + self.tags + .as_ref() + .map_or(vec![], |s| + s.split(',') + .map(|tag| tag.trim().to_string()) + .collect() + ) + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] /// A partial sticker object. /// @@ -118,5 +133,61 @@ impl PartialOrd for Sticker { pub struct StickerItem { pub id: Snowflake, pub name: String, - pub format_type: u8, + pub format_type: StickerFormatType, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Hash, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] +#[serde(rename = "SCREAMING_SNAKE_CASE")] +/// # Reference +/// See +pub enum StickerType { + /// An official sticker in a current or legacy purchasable pack + Standard = 1, + #[default] + /// A sticker uploaded to a guild for the guild's members + Guild = 2, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Hash, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] +/// # Reference +/// See +pub enum StickerFormatType { + #[default] + /// A PNG image + PNG = 1, + /// An animated PNG image, using the APNG format - uses CDN + APNG = 2, + /// A lottie animation; requires the VERIFIED and/or PARTNERED guild feature - uses CDN + LOTTIE = 3, + /// An animated GIF image - does not use CDN + GIF = 4, +} + +impl StickerFormatType { + pub fn is_animated(&self) -> bool { + matches!(self, StickerFormatType::APNG | StickerFormatType::LOTTIE | StickerFormatType::GIF) + } + + pub const fn to_mime(&self) -> &'static str { + match self { + StickerFormatType::PNG => "image/png", + StickerFormatType::APNG => "image/apng", + StickerFormatType::LOTTIE => "application/json", + StickerFormatType::GIF => "image/gif", + } + } + + pub fn from_mime(mime: &str) -> Option { + match mime { + "image/png" => Some(StickerFormatType::PNG), + "image/apng" => Some(StickerFormatType::APNG), + "application/json" => Some(StickerFormatType::LOTTIE), + "image/gif" => Some(StickerFormatType::GIF), + _ => None, + } + } } diff --git a/src/types/entities/voice_state.rs b/src/types/entities/voice_state.rs index 7297456..9953b7b 100644 --- a/src/types/entities/voice_state.rs +++ b/src/types/entities/voice_state.rs @@ -34,9 +34,11 @@ use crate::types::{ #[cfg_attr(feature = "client", derive(Composite))] pub struct VoiceState { pub guild_id: Option, + #[cfg_attr(feature = "sqlx", sqlx(skip))] pub guild: Option, pub channel_id: Option, pub user_id: Snowflake, + #[cfg_attr(feature = "sqlx", sqlx(skip))] pub member: Option>, /// Includes alphanumeric characters, not a snowflake pub session_id: String, diff --git a/src/types/events/channel.rs b/src/types/events/channel.rs index dd16754..73d89f6 100644 --- a/src/types/events/channel.rs +++ b/src/types/events/channel.rs @@ -49,11 +49,7 @@ impl UpdateMessage for ChannelCreate { fn update(&mut self, object_to_update: Shared) { let mut write = object_to_update.write().unwrap(); let update = self.channel.clone().into_shared(); - if write.channels.is_some() { - write.channels.as_mut().unwrap().push(update); - } else { - write.channels = Some(Vec::from([update])); - } + write.channels.push(update); } } @@ -122,12 +118,12 @@ impl UpdateMessage for ChannelDelete { return; } let mut write = object_to_update.write().unwrap(); - if write.channels.is_none() { + if write.channels.is_empty() { return; } - for (iteration, item) in (0_u32..).zip(write.channels.as_mut().unwrap().iter()) { + for (iteration, item) in (0_u32..).zip(write.channels.iter()) { if item.read().unwrap().id == self.id().unwrap() { - write.channels.as_mut().unwrap().remove(iteration as usize); + write.channels.remove(iteration as usize); return; } } diff --git a/src/types/events/guild.rs b/src/types/events/guild.rs index 362b984..f2cc009 100644 --- a/src/types/events/guild.rs +++ b/src/types/events/guild.rs @@ -214,15 +214,9 @@ impl UpdateMessage for GuildRoleCreate { fn update(&mut self, object_to_update: Shared) { let mut object_to_update = object_to_update.write().unwrap(); - if object_to_update.roles.is_some() { - object_to_update - .roles - .as_mut() - .unwrap() - .push(self.role.clone().into_shared()); - } else { - object_to_update.roles = Some(Vec::from([self.role.clone().into_shared()])); - } + object_to_update + .roles + .push(self.role.clone().into_shared()); } } diff --git a/src/types/schema/audit_log.rs b/src/types/schema/audit_log.rs new file mode 100644 index 0000000..ddee832 --- /dev/null +++ b/src/types/schema/audit_log.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; +use crate::types::{ApplicationCommand, AuditLogActionType, AuditLogEntry, AutoModerationRule, Channel, GuildScheduledEvent, Integration, Snowflake, User, Webhook}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct AuditLogObject { + pub audit_log_entries: Vec, + pub application_commands: Vec, + pub auto_moderation_rules: Vec, + pub guild_scheduled_events: Vec, + pub integrations: Vec, + pub threads: Vec, + pub users: Vec, + pub webhooks: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct GetAuditLogsQuery { + pub before: Option, + pub after: Option, + pub limit: Option, + pub user_id: Option, + pub action_type: Option +} \ No newline at end of file diff --git a/src/types/schema/guild.rs b/src/types/schema/guild.rs index d820cd8..17c9d61 100644 --- a/src/types/schema/guild.rs +++ b/src/types/schema/guild.rs @@ -2,16 +2,14 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. +use std::collections::HashMap; use bitflags::bitflags; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::types::entities::Channel; use crate::types::types::guild_configuration::GuildFeatures; -use crate::types::{ - Emoji, ExplicitContentFilterLevel, MessageNotificationLevel, Snowflake, Sticker, - SystemChannelFlags, VerificationLevel, -}; +use crate::types::{Emoji, ExplicitContentFilterLevel, GenericSearchQueryWithLimit, MessageNotificationLevel, Snowflake, Sticker, StickerFormatType, SystemChannelFlags, VerificationLevel, WelcomeScreenChannel}; #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] @@ -32,10 +30,20 @@ pub struct GuildCreateSchema { /// Represents the schema which needs to be sent to create a Guild Ban. /// See: pub struct GuildBanCreateSchema { + /// Deprecated pub delete_message_days: Option, pub delete_message_seconds: Option, } +#[derive(Debug, Deserialize, Serialize, Default, Clone, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +/// Represents the schema which needs to be sent to create a Guild Ban. +/// See: +pub struct GuildBanBulkCreateSchema { + pub user_ids: Vec, + pub delete_message_seconds: Option, +} + #[derive(Debug, Deserialize, Serialize, Default, Clone, Eq, PartialEq)] #[serde(rename_all = "snake_case")] /// Represents the schema used to modify a guild. @@ -119,6 +127,12 @@ impl Default for GuildMemberSearchSchema { } } +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd, Eq, Ord)] +pub struct GuildGetMembersQuery { + pub limit: Option, + pub after: Option, +} + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd, Eq, Ord)] pub struct ModifyGuildMemberSchema { pub nick: Option, @@ -174,3 +188,200 @@ pub struct GuildBansQuery { pub after: Option, pub limit: Option, } + + +/// Max query length is 32 characters. +/// The limit argument is a number between 1 and 10, defaults to 10. +pub type GuildBansSearchQuery = GenericSearchQueryWithLimit; + +/// Query is partial or full, username or nickname. +/// Limit argument is a number between 1 and 1000, defaults to 1. +pub type GuildMembersSearchQuery = GenericSearchQueryWithLimit; + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +/// A guild's progress on meeting the requirements of joining discovery. +/// +/// Certain guilds, such as those that are verified, are exempt from discovery requirements. These guilds will not have a fully populated discovery requirements object, and are guaranteed to receive only sufficient and sufficient_without_grace_period. +/// +/// # Reference: +/// See +pub struct GuildDiscoveryRequirements { + pub guild_id: Option, + pub safe_environment: Option, + pub healthy: Option, + pub health_score_pending: Option, + pub size: Option, + pub nsfw_properties: Option, + pub protected: Option, + pub sufficient: Option, + pub sufficient_without_grace_period: Option, + pub valid_rules_channel: Option, + pub retention_healthy: Option, + pub engagement_healthy: Option, + pub age: Option, + pub minimum_age: Option, + pub health_score: Option, + pub minimum_size: Option, + pub grace_period_end_date: Option>, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// # Reference: +/// See +pub struct GuildDiscoveryNsfwProperties { + pub channels: Vec, + pub channel_banned_keywords: HashMap>, + pub name: Option, + pub name_banned_keywords: Vec, + pub description: Option, + pub description_banned_keywords: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +/// Activity metrics are recalculated weekly, as an 8-week rolling average. If they are not yet eligible to be calculated, all fields will be null. +/// +/// # Reference: +/// See +pub struct GuildDiscoveryHealthScore { + pub avg_nonnew_communicators: u64, + pub avg_nonnew_participators: u64, + pub num_intentful_joiners: u64, + pub perc_ret_w1_intentful: f64, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +/// # Reference: +/// See +pub struct EmojiCreateSchema { + pub name: Option, + /// # Reference: + /// See + pub image: String, + #[serde(default)] + pub roles: Vec +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +/// # Reference: +/// See +pub struct EmojiModifySchema { + pub name: Option, + pub roles: Option> +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +/// # Reference: +/// See +pub struct GuildPruneQuerySchema { + pub days: u8, + /// Only used on POST + #[serde(default, skip_serializing_if = "Option::is_none")] + pub compute_prune_count: Option, + #[serde(default)] + pub include_roles: Vec +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +/// # Reference: +/// See +pub struct GuildPruneResult { + /// Null if compute_prune_count is false + pub pruned: Option, +} + +#[derive(Default, Debug, Deserialize, Serialize, Clone, PartialEq)] +/// # Reference: +/// See +pub struct GuildCreateStickerSchema { + pub name: String, + #[serde(default)] + pub description: Option, + #[serde(default)] + pub tags: Option, + pub file_data: Vec, + #[serde(skip)] + pub sticker_format_type: StickerFormatType +} + +impl GuildCreateStickerSchema { + #[cfg(feature = "poem")] + pub async fn from_multipart(mut multipart: poem::web::Multipart) -> Result { + let mut _self = GuildCreateStickerSchema::default(); + while let Some(field) = multipart.next_field().await? { + let name = field.name().ok_or(poem::Error::from_string("All fields must be named", poem::http::StatusCode::BAD_REQUEST))?; + match name { + "name" => { + _self.name = field.text().await?; + } + "description" => { + _self.description = Some(field.text().await?); + } + "tags" => { + _self.tags = Some(field.text().await?); + } + "file_data" => { + if _self.name.is_empty() { + _self.name = field.file_name().map(String::from).ok_or(poem::Error::from_string("File name must be set", poem::http::StatusCode::BAD_REQUEST))?; + } + _self.sticker_format_type = StickerFormatType::from_mime(field.content_type().ok_or(poem::Error::from_string("Content type must be set", poem::http::StatusCode::BAD_REQUEST))?).ok_or(poem::Error::from_string("Unknown sticker format", poem::http::StatusCode::BAD_REQUEST))?; + _self.file_data = field.bytes().await?; + } + _ => {} + } + + } + if _self.name.is_empty() || _self.file_data.is_empty() { + return Err(poem::Error::from_string("At least the name and file_data are required", poem::http::StatusCode::BAD_REQUEST)); + } + + Ok(_self) + } + + // #[cfg(feature = "client")] + pub fn to_multipart(&self) -> reqwest::multipart::Form { + let mut form = reqwest::multipart::Form::new() + .text("name", self.name.clone()) + .part("file_data", reqwest::multipart::Part::bytes(self.file_data.clone()).mime_str(self.sticker_format_type.to_mime()).unwrap()); + + if let Some(description) = &self.description { + form = form.text("description", description.to_owned()); + } + + if let Some(tags) = &self.tags { + form = form.text("tags", tags.to_owned()) + } + form + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +/// # Reference: +/// See +pub struct GuildModifyStickerSchema { + #[serde(default)] + pub name: Option, + #[serde(default)] + pub description: Option, + #[serde(default)] + pub tags: Option +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +/// # Reference: +/// See +pub struct GuildModifyWelcomeScreenSchema { + pub enabled: Option, + pub description: Option, + /// Max of 5 + pub welcome_channels: Option>, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +/// # Reference: +/// See +pub struct GuildTemplateCreateSchema { + /// Name of the template (1-100 characters) + pub name: String, + /// Description of the template (max 120 characters) + pub description: Option +} \ No newline at end of file diff --git a/src/types/schema/invites.rs b/src/types/schema/invites.rs index 6bf2131..542c990 100644 --- a/src/types/schema/invites.rs +++ b/src/types/schema/invites.rs @@ -7,4 +7,20 @@ use serde::{Deserialize, Serialize}; /// Read: pub struct GetInvitesSchema { pub with_counts: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd, Eq, Ord)] +/// # Reference: +/// See +pub struct GuildVanityInviteResponse { + pub code: String, + #[serde(default)] + pub uses: Option +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd, Eq, Ord)] +/// # Reference: +/// See +pub struct GuildCreateVanitySchema { + pub code: String, } \ No newline at end of file diff --git a/src/types/schema/message.rs b/src/types/schema/message.rs index 4a7f2ab..4d50b25 100644 --- a/src/types/schema/message.rs +++ b/src/types/schema/message.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::types::entities::{ AllowedMention, Component, Embed, MessageReference, PartialDiscordFileAttachment, }; -use crate::types::{Attachment, MessageFlags, MessageType, ReactionType, Snowflake}; +use crate::types::{Attachment, EmbedType, Message, MessageFlags, MessageType, ReactionType, Snowflake}; #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] @@ -54,13 +54,13 @@ pub struct MessageSearchQuery { pub attachment_extension: Option>, pub attachment_filename: Option>, pub author_id: Option>, - pub author_type: Option>, + pub author_type: Option>, pub channel_id: Option>, pub command_id: Option>, pub content: Option, pub embed_provider: Option>, - pub embed_type: Option>, - pub has: Option>, + pub embed_type: Option>, + pub has: Option>, pub include_nsfw: Option, pub limit: Option, pub link_hostname: Option>, @@ -70,8 +70,8 @@ pub struct MessageSearchQuery { pub min_id: Option, pub offset: Option, pub pinned: Option, - pub sort_by: Option, - pub sort_order: Option, + pub sort_by: Option, + pub sort_order: Option, } impl std::default::Default for MessageSearchQuery { @@ -102,6 +102,75 @@ impl std::default::Default for MessageSearchQuery { } } +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "snake_case")] +pub enum AuthorType { + User, + #[serde(rename = "-user")] + NotUser, + Bot, + #[serde(rename = "-bot")] + NotBot, + Webhook, + #[serde(rename = "-webhook")] + NotWebhook, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "snake_case")] +pub enum HasType { + Image, + #[serde(rename = "-image")] + NotImage, + Sound, + #[serde(rename = "-sound")] + NotSound, + Video, + #[serde(rename = "-video")] + NotVideo, + File, + #[serde(rename = "-file")] + NotFile, + Sticker, + #[serde(rename = "-sticker")] + NotSticker, + Embed, + #[serde(rename = "-embed")] + NotEmbed, + Link, + #[serde(rename = "-link")] + NotLink, + Poll, + #[serde(rename = "-poll")] + NotPoll, + Snapshot, + #[serde(rename = "-snapshot")] + NotSnapshot, +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "snake_case")] +pub enum SortType { + #[default] + Timestamp, + Relevance +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum SortOrder { + #[default] + #[serde(rename = "desc")] + Descending, + #[serde(rename = "asc")] + Ascending, +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)] +pub struct MessageSearchResponse { + pub messages: Vec, + pub total_results: u64, +} + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct CreateGreetMessage { pub sticker_ids: Vec, diff --git a/src/types/schema/mod.rs b/src/types/schema/mod.rs index ef3233d..09e542e 100644 --- a/src/types/schema/mod.rs +++ b/src/types/schema/mod.rs @@ -3,6 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. pub use apierror::*; +pub use audit_log::*; pub use auth::*; pub use channel::*; pub use guild::*; @@ -11,8 +12,10 @@ pub use relationship::*; pub use role::*; pub use user::*; pub use invites::*; +pub use voice_state::*; mod apierror; +mod audit_log; mod auth; mod channel; mod guild; @@ -21,3 +24,10 @@ mod relationship; mod role; mod user; mod invites; +mod voice_state; + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, PartialOrd, Eq, Ord)] +pub struct GenericSearchQueryWithLimit { + pub query: String, + pub limit: Option, +} \ No newline at end of file diff --git a/src/types/schema/role.rs b/src/types/schema/role.rs index 168d999..5dce877 100644 --- a/src/types/schema/role.rs +++ b/src/types/schema/role.rs @@ -3,6 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. use serde::{Deserialize, Serialize}; +use crate::types::{PermissionFlags, Snowflake}; #[derive(Debug, Deserialize, Serialize, Clone)] #[serde(rename_all = "snake_case")] @@ -10,8 +11,8 @@ use serde::{Deserialize, Serialize}; /// See: [https://docs.spacebar.chat/routes/#cmp--schemas-rolemodifyschema](https://docs.spacebar.chat/routes/#cmp--schemas-rolemodifyschema) pub struct RoleCreateModifySchema { pub name: Option, - pub permissions: Option, - pub color: Option, + pub permissions: Option, + pub color: Option, pub hoist: Option, pub icon: Option>, pub unicode_emoji: Option, @@ -24,6 +25,6 @@ pub struct RoleCreateModifySchema { /// Represents the schema which needs to be sent to update a roles' position. /// See: [https://docs.spacebar.chat/routes/#cmp--schemas-rolepositionupdateschema](https://docs.spacebar.chat/routes/#cmp--schemas-rolepositionupdateschema) pub struct RolePositionUpdateSchema { - pub id: String, + pub id: Snowflake, pub position: u16, } diff --git a/src/types/schema/voice_state.rs b/src/types/schema/voice_state.rs new file mode 100644 index 0000000..d5576ad --- /dev/null +++ b/src/types/schema/voice_state.rs @@ -0,0 +1,15 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use crate::types::Snowflake; + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd)] +/// # Reference: +/// See +pub struct VoiceStateUpdateSchema { + /// The ID of the channel the user is currently in + pub channel_id: Option, + /// Whether to suppress the user + pub suppress: Option, + /// The time at which the user requested to speak + pub request_to_speak_timestamp: Option>, +} \ No newline at end of file diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 863e91f..f2f0663 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -5,7 +5,7 @@ use std::str::FromStr; use chorus::gateway::{Gateway, GatewayOptions}; -use chorus::types::IntoShared; +use chorus::types::{IntoShared, PermissionFlags}; use chorus::{ instance::{ChorusUser, Instance}, types::{ @@ -108,7 +108,7 @@ pub(crate) async fn setup() -> TestBundle { let role_create_schema: chorus::types::RoleCreateModifySchema = RoleCreateModifySchema { name: Some("Bundle role".to_string()), - permissions: Some("8".to_string()), // Administrator permissions + permissions: PermissionFlags::from_bits(8), // Administrator permissions hoist: Some(true), icon: None, unicode_emoji: Some("".to_string()), diff --git a/tests/gateway.rs b/tests/gateway.rs index 12626a6..9991124 100644 --- a/tests/gateway.rs +++ b/tests/gateway.rs @@ -81,7 +81,7 @@ async fn test_gateway_authenticate() { } // Success, we have received it Some(_) = ready_receive.recv() => {} - }; + } common::teardown(bundle).await } @@ -125,7 +125,7 @@ async fn test_self_updating_structs() { .gateway .observe_and_into_inner(bundle.guild.clone()) .await; - assert!(guild.channels.is_none()); + assert!(guild.channels.is_empty()); Channel::create( &mut bundle.user, @@ -145,8 +145,8 @@ async fn test_self_updating_structs() { .gateway .observe_and_into_inner(guild.into_shared()) .await; - assert!(guild.channels.is_some()); - assert!(guild.channels.as_ref().unwrap().len() == 1); + assert!(!guild.channels.is_empty()); + assert_eq!(guild.channels.len(), 1); common::teardown(bundle).await } @@ -160,13 +160,12 @@ async fn test_recursive_self_updating_structs() { // Observe Guild, make sure it has no channels let guild = bundle.user.gateway.observe(guild.clone()).await; let inner_guild = guild.read().unwrap().clone(); - assert!(inner_guild.roles.is_none()); + assert!(inner_guild.roles.is_empty()); // Create Role let permissions = types::PermissionFlags::CONNECT | types::PermissionFlags::MANAGE_EVENTS; - let permissions = Some(permissions.to_string()); let mut role_create_schema: types::RoleCreateModifySchema = RoleCreateModifySchema { name: Some("cool person".to_string()), - permissions, + permissions: Some(permissions), hoist: Some(true), icon: None, unicode_emoji: Some("".to_string()), @@ -186,7 +185,7 @@ async fn test_recursive_self_updating_structs() { .await; // Update Guild and check for Guild let inner_guild = guild.read().unwrap().clone(); - assert!(inner_guild.roles.is_some()); + assert!(!inner_guild.roles.is_empty()); // Update the Role role_create_schema.name = Some("yippieee".to_string()); RoleObject::modify(&mut bundle.user, guild_id, role.id, role_create_schema) @@ -202,8 +201,7 @@ async fn test_recursive_self_updating_structs() { let guild = bundle.user.gateway.observe(bundle.guild.clone()).await; let inner_guild = guild.read().unwrap().clone(); let guild_roles = inner_guild.roles; - let guild_role = guild_roles.unwrap(); - let guild_role_inner = guild_role.get(0).unwrap().read().unwrap().clone(); + let guild_role_inner = guild_roles.get(0).unwrap().read().unwrap().clone(); assert_eq!(guild_role_inner.name, "yippieee".to_string()); common::teardown(bundle).await; } diff --git a/tests/roles.rs b/tests/roles.rs index 3246140..faf2719 100644 --- a/tests/roles.rs +++ b/tests/roles.rs @@ -15,10 +15,9 @@ mod common; async fn create_and_get_roles() { let mut bundle = common::setup().await; let permissions = types::PermissionFlags::CONNECT | types::PermissionFlags::MANAGE_EVENTS; - let permissions = Some(permissions.to_string()); let role_create_schema: types::RoleCreateModifySchema = RoleCreateModifySchema { name: Some("cool person".to_string()), - permissions, + permissions: Some(permissions), hoist: Some(true), icon: None, unicode_emoji: Some("".to_string()),