diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml index b92bb4fa..0d821e11 100644 --- a/crates/core/database/Cargo.toml +++ b/crates/core/database/Cargo.toml @@ -15,6 +15,7 @@ mongodb = [ "dep:mongodb", "bson" ] # ... Other async-std-runtime = [ "async-std" ] rocket-impl = [ "rocket", "schemars" ] +redis-is-patched = [ "revolt-presence/redis-is-patched" ] # Default Features default = [ "mongodb", "async-std-runtime" ] @@ -22,6 +23,8 @@ default = [ "mongodb", "async-std-runtime" ] [dependencies] # Core revolt-result = { version = "0.0.2", path = "../result" } +revolt-models = { version = "0.0.2", path = "../models" } +revolt-presence = { version = "0.0.2", path = "../presence" } revolt-permissions = { version = "0.0.2", path = "../permissions", features = [ "serde" ] } # Utility @@ -36,6 +39,9 @@ revolt_optional_struct = "0.2.0" serde = { version = "1", features = ["derive"] } iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] } +# Events +redis-kiss = { version = "0.1.4" } + # Database bson = { optional = true, version = "2.1.0" } mongodb = { optional = true, version = "2.1.0", default-features = false } diff --git a/crates/core/database/src/drivers/reference.rs b/crates/core/database/src/drivers/reference.rs index 9f562b36..ef7e6adb 100644 --- a/crates/core/database/src/drivers/reference.rs +++ b/crates/core/database/src/drivers/reference.rs @@ -2,7 +2,9 @@ use std::{collections::HashMap, sync::Arc}; use futures::lock::Mutex; -use crate::{AccountStrike, Bot, File, Member, MemberCompositeKey, Server, User, UserSettings}; +use crate::{ + AccountStrike, Bot, File, Member, MemberCompositeKey, Server, User, UserSettings, Webhook, +}; database_derived!( /// Reference implementation @@ -10,6 +12,7 @@ database_derived!( pub struct ReferenceDb { pub account_strikes: Arc>>, pub bots: Arc>>, + pub channel_webhooks: Arc>>, pub user_settings: Arc>>, pub users: Arc>>, pub server_members: Arc>>, diff --git a/crates/core/database/src/events/client.rs b/crates/core/database/src/events/client.rs new file mode 100644 index 00000000..38ed1f9d --- /dev/null +++ b/crates/core/database/src/events/client.rs @@ -0,0 +1,267 @@ +use authifier::AuthifierEvent; +use serde::{Deserialize, Serialize}; + +use revolt_models::v0::{FieldsWebhook, PartialWebhook, Webhook}; +use revolt_result::Error; + +use crate::Database; + +/// WebSocket Client Errors +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(tag = "error")] +pub enum WebSocketError { + LabelMe, + InternalError { at: String }, + InvalidSession, + OnboardingNotFinished, + AlreadyAuthenticated, + MalformedData { msg: String }, +} + +/// Ping Packet +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum Ping { + Binary(Vec), + Number(usize), +} + +/// Untagged Error +#[derive(Serialize)] +#[serde(untagged)] +pub enum ErrorEvent { + Error(WebSocketError), + APIError(Error), +} + +/// Protocol Events +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(tag = "type")] +pub enum EventV1 { + /// Multiple events + Bulk { v: Vec }, + + /// Successfully authenticated + Authenticated, + /* /// Basic data to cache + Ready { + users: Vec, + servers: Vec, + channels: Vec, + members: Vec, + emojis: Option>, + }, + + /// Ping response + Pong { data: Ping }, + + /// New message + Message(Message), + + /// Update existing message + MessageUpdate { + id: String, + channel: String, + data: PartialMessage, + }, + + /// Append information to existing message + MessageAppend { + id: String, + channel: String, + append: AppendMessage, + }, + + /// Delete message + MessageDelete { id: String, channel: String }, + + /// New reaction to a message + MessageReact { + id: String, + channel_id: String, + user_id: String, + emoji_id: String, + }, + + /// Remove user's reaction from message + MessageUnreact { + id: String, + channel_id: String, + user_id: String, + emoji_id: String, + }, + + /// Remove a reaction from message + MessageRemoveReaction { + id: String, + channel_id: String, + emoji_id: String, + }, + + /// Bulk delete messages + BulkMessageDelete { channel: String, ids: Vec }, + + /// New channel + ChannelCreate(Channel), + + /// Update existing channel + ChannelUpdate { + id: String, + data: PartialChannel, + clear: Vec, + }, + + /// Delete channel + ChannelDelete { id: String }, + + /// User joins a group + ChannelGroupJoin { id: String, user: String }, + + /// User leaves a group + ChannelGroupLeave { id: String, user: String }, + + /// User started typing in a channel + ChannelStartTyping { id: String, user: String }, + + /// User stopped typing in a channel + ChannelStopTyping { id: String, user: String }, + + /// User acknowledged message in channel + ChannelAck { + id: String, + user: String, + message_id: String, + }, + + /// New server + ServerCreate { + id: String, + server: Server, + channels: Vec, + }, + + /// Update existing server + ServerUpdate { + id: String, + data: PartialServer, + clear: Vec, + }, + + /// Delete server + ServerDelete { id: String }, + + /// Update existing server member + ServerMemberUpdate { + id: MemberCompositeKey, + data: PartialMember, + clear: Vec, + }, + + /// User joins server + ServerMemberJoin { id: String, user: String }, + + /// User left server + ServerMemberLeave { id: String, user: String }, + + /// Server role created or updated + ServerRoleUpdate { + id: String, + role_id: String, + data: PartialRole, + clear: Vec, + }, + + /// Server role deleted + ServerRoleDelete { id: String, role_id: String }, + + /// Update existing user + UserUpdate { + id: String, + data: PartialUser, + clear: Vec, + event_id: Option, + }, + + /// Relationship with another user changed + UserRelationship { + id: String, + user: User, + // ! this field can be deprecated + status: RelationshipStatus, + }, + + /// Settings updated remotely + UserSettingsUpdate { id: String, update: UserSettings }, + + /// User has been platform banned or deleted their account + /// + /// Clients should remove the following associated data: + /// - Messages + /// - DM Channels + /// - Relationships + /// - Server Memberships + /// + /// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear. + UserPlatformWipe { user_id: String, flags: i32 }, + + /// New emoji + EmojiCreate(Emoji), + + /// Delete emoji + EmojiDelete { id: String }, + + /// New report + ReportCreate(Report), */ + /// New webhook + WebhookCreate(Webhook), + + /// Update existing webhook + WebhookUpdate { + id: String, + data: PartialWebhook, + remove: Vec, + }, + + /// Delete webhook + WebhookDelete { id: String }, + + /// Auth events + Auth(AuthifierEvent), +} + +impl EventV1 { + /// Publish helper wrapper + pub async fn p(self, channel: String) { + #[cfg(not(debug_assertions))] + redis_kiss::p(channel, self).await; + + #[cfg(debug_assertions)] + info!("Publishing event to {channel}: {self:?}"); + + #[cfg(debug_assertions)] + redis_kiss::publish(channel, self).await.unwrap(); + } + + /// Publish user event + pub async fn p_user(self, id: String, db: &Database) { + self.clone().p(id.clone()).await; + + // ! FIXME: this should be captured by member list in the future + // ! and not immediately fanned out to users + if let Ok(members) = db.fetch_all_memberships(&id).await { + for member in members { + self.clone().p(member.id.server).await; + } + } + } + + /// Publish private event + pub async fn private(self, id: String) { + self.p(format!("{id}!")).await; + } + + /// Publish internal global event + pub async fn global(self) { + self.p("global".to_string()).await; + } +} diff --git a/crates/core/database/src/events/mod.rs b/crates/core/database/src/events/mod.rs new file mode 100644 index 00000000..b9babe5b --- /dev/null +++ b/crates/core/database/src/events/mod.rs @@ -0,0 +1 @@ +pub mod client; diff --git a/crates/core/database/src/lib.rs b/crates/core/database/src/lib.rs index 18d5db63..03f0df41 100644 --- a/crates/core/database/src/lib.rs +++ b/crates/core/database/src/lib.rs @@ -80,6 +80,8 @@ mod models; pub mod util; pub use models::*; +pub mod events; + /// Utility function to check if a boolean value is false pub fn if_false(t: &bool) -> bool { !t diff --git a/crates/core/database/src/models/channel_webhooks/mod.rs b/crates/core/database/src/models/channel_webhooks/mod.rs new file mode 100644 index 00000000..4d801b73 --- /dev/null +++ b/crates/core/database/src/models/channel_webhooks/mod.rs @@ -0,0 +1,5 @@ +mod model; +mod ops; + +pub use model::*; +pub use ops::*; diff --git a/crates/core/database/src/models/channel_webhooks/model.rs b/crates/core/database/src/models/channel_webhooks/model.rs new file mode 100644 index 00000000..dba91ff8 --- /dev/null +++ b/crates/core/database/src/models/channel_webhooks/model.rs @@ -0,0 +1,158 @@ +use revolt_result::Result; + +use crate::events::client::EventV1; +use crate::{Database, File}; + +auto_derived_partial!( + /// Webhook + pub struct Webhook { + /// Webhook Id + #[serde(rename = "_id")] + pub id: String, + + /// The name of the webhook + pub name: String, + + /// The avatar of the webhook + #[serde(skip_serializing_if = "Option::is_none")] + pub avatar: Option, + + /// The channel this webhook belongs to + pub channel_id: String, + + /// The private token for the webhook + pub token: Option, + }, + "PartialWebhook" +); + +auto_derived!( + /// Optional fields on webhook object + pub enum FieldsWebhook { + Avatar, + } +); + +#[allow(clippy::disallowed_methods)] +impl Webhook { + pub async fn create(&self, db: &Database) -> Result<()> { + db.insert_webhook(self).await?; + + // Avoid leaking the token to people who receive the event + let mut webhook = self.clone(); + webhook.token = None; + + EventV1::WebhookCreate(webhook.into()) + .p(self.channel_id.clone()) + .await; + + Ok(()) + } + + pub fn assert_token(&self, token: &str) -> Result<()> { + if self.token.as_deref() == Some(token) { + Ok(()) + } else { + Err(create_error!(InvalidCredentials)) + } + } + + pub async fn update( + &mut self, + db: &Database, + mut partial: PartialWebhook, + remove: Vec, + ) -> Result<()> { + for field in &remove { + self.remove_field(field) + } + + self.apply_options(partial.clone()); + + db.update_webhook(&self.id, &partial, &remove).await?; + + partial.token = None; // Avoid leaking the token to people who receive the event + + EventV1::WebhookUpdate { + id: self.id.clone(), + data: partial.into(), + remove: remove.into_iter().map(|v| v.into()).collect(), + } + .p(self.channel_id.clone()) + .await; + + Ok(()) + } + + pub fn remove_field(&mut self, field: &FieldsWebhook) { + match field { + FieldsWebhook::Avatar => self.avatar = None, + } + } + + pub async fn delete(&self, db: &Database) -> Result<()> { + db.delete_webhook(&self.id).await?; + + EventV1::WebhookDelete { + id: self.id.clone(), + } + .p(self.channel_id.clone()) + .await; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::{FieldsWebhook, PartialWebhook, Webhook}; + + #[async_std::test] + async fn crud() { + database_test!(|db| async move { + let webhook_id = "webhook"; + let channel_id = "channel"; + + let webhook = Webhook { + id: webhook_id.to_string(), + name: "Webhook Name".to_string(), + channel_id: channel_id.to_string(), + avatar: Some(Default::default()), + ..Default::default() + }; + + db.insert_webhook(&webhook).await.unwrap(); + + let mut updated_webhook = webhook.clone(); + updated_webhook + .update( + &db, + PartialWebhook { + name: Some("New Name".to_string()), + ..Default::default() + }, + vec![FieldsWebhook::Avatar], + ) + .await + .unwrap(); + + let fetched_webhook = db.fetch_webhook(webhook_id).await.unwrap(); + let fetched_webhooks = db.fetch_webhooks_for_channel(channel_id).await.unwrap(); + + assert_eq!(updated_webhook, fetched_webhook); + assert_ne!(webhook, fetched_webhook); + assert_eq!(1, fetched_webhooks.len()); + assert_eq!(fetched_webhook, fetched_webhooks[0]); + + webhook.delete(&db).await.unwrap(); + assert!(db.fetch_webhook(webhook_id).await.is_err()); + assert_eq!( + 0, + db.fetch_webhooks_for_channel(channel_id) + .await + .unwrap() + .len() + ) + }); + } +} diff --git a/crates/core/database/src/models/channel_webhooks/ops.rs b/crates/core/database/src/models/channel_webhooks/ops.rs new file mode 100644 index 00000000..d7561a99 --- /dev/null +++ b/crates/core/database/src/models/channel_webhooks/ops.rs @@ -0,0 +1,29 @@ +use revolt_result::Result; + +use crate::{FieldsWebhook, PartialWebhook, Webhook}; + +mod mongodb; +mod reference; + +#[async_trait] +pub trait AbstractWebhooks: Sync + Send { + /// Insert new webhook into the database + async fn insert_webhook(&self, webhook: &Webhook) -> Result<()>; + + /// Fetch webhook by id + async fn fetch_webhook(&self, webhook_id: &str) -> Result; + + /// Fetch webhooks for channel + async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result>; + + /// Update webhook with new information + async fn update_webhook( + &self, + webhook_id: &str, + partial: &PartialWebhook, + remove: &[FieldsWebhook], + ) -> Result<()>; + + /// Delete webhook by id + async fn delete_webhook(&self, webhook_id: &str) -> Result<()>; +} diff --git a/crates/core/database/src/models/channel_webhooks/ops/mongodb.rs b/crates/core/database/src/models/channel_webhooks/ops/mongodb.rs new file mode 100644 index 00000000..457e3b68 --- /dev/null +++ b/crates/core/database/src/models/channel_webhooks/ops/mongodb.rs @@ -0,0 +1,77 @@ +use futures::StreamExt; +use revolt_result::Result; + +use crate::{FieldsWebhook, PartialWebhook, Webhook}; +use crate::{IntoDocumentPath, MongoDb}; + +use super::AbstractWebhooks; + +static COL: &str = "channel_webhooks"; + +#[async_trait] +impl AbstractWebhooks for MongoDb { + /// Insert new webhook into the database + async fn insert_webhook(&self, webhook: &Webhook) -> Result<()> { + query!(self, insert_one, COL, &webhook).map(|_| ()) + } + + /// Fetch webhook by id + async fn fetch_webhook(&self, webhook_id: &str) -> Result { + query!(self, find_one_by_id, COL, webhook_id)?.ok_or_else(|| create_error!(NotFound)) + } + + /// Fetch webhooks for channel + async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result> { + Ok(self + .col::(COL) + .find( + doc! { + "channel_id": channel_id, + }, + None, + ) + .await + .map_err(|_| create_database_error!("find", COL))? + .filter_map(|s| async { + if cfg!(debug_assertions) { + Some(s.unwrap()) + } else { + s.ok() + } + }) + .collect() + .await) + } + + /// Update webhook with new information + async fn update_webhook( + &self, + webhook_id: &str, + partial: &PartialWebhook, + remove: &[FieldsWebhook], + ) -> Result<()> { + query!( + self, + update_one_by_id, + COL, + webhook_id, + partial, + remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(), + None + ) + .map(|_| ()) + } + + /// Delete webhook by id + async fn delete_webhook(&self, webhook_id: &str) -> Result<()> { + query!(self, delete_one_by_id, COL, webhook_id).map(|_| ()) + } +} + +impl IntoDocumentPath for FieldsWebhook { + fn as_path(&self) -> Option<&'static str> { + Some(match self { + FieldsWebhook::Avatar => "avatar", + }) + } +} diff --git a/crates/core/database/src/models/channel_webhooks/ops/reference.rs b/crates/core/database/src/models/channel_webhooks/ops/reference.rs new file mode 100644 index 00000000..fbf1a508 --- /dev/null +++ b/crates/core/database/src/models/channel_webhooks/ops/reference.rs @@ -0,0 +1,70 @@ +use revolt_result::Result; + +use crate::ReferenceDb; +use crate::{FieldsWebhook, PartialWebhook, Webhook}; + +use super::AbstractWebhooks; + +#[async_trait] +impl AbstractWebhooks for ReferenceDb { + /// Insert new webhook into the database + async fn insert_webhook(&self, webhook: &Webhook) -> Result<()> { + let mut webhooks = self.channel_webhooks.lock().await; + if webhooks.contains_key(&webhook.id) { + Err(create_database_error!("insert", "webhook")) + } else { + webhooks.insert(webhook.id.to_string(), webhook.clone()); + Ok(()) + } + } + + /// Fetch webhook by id + async fn fetch_webhook(&self, webhook_id: &str) -> Result { + let webhooks = self.channel_webhooks.lock().await; + webhooks + .get(webhook_id) + .cloned() + .ok_or_else(|| create_error!(NotFound)) + } + + /// Fetch webhooks for channel + async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result> { + let webhooks = self.channel_webhooks.lock().await; + Ok(webhooks + .values() + .filter(|webhook| webhook.channel_id == channel_id) + .cloned() + .collect()) + } + + /// Update webhook with new information + async fn update_webhook( + &self, + webhook_id: &str, + partial: &PartialWebhook, + remove: &[FieldsWebhook], + ) -> Result<()> { + let mut webhooks = self.channel_webhooks.lock().await; + if let Some(webhook) = webhooks.get_mut(webhook_id) { + for field in remove { + #[allow(clippy::disallowed_methods)] + webhook.remove_field(field); + } + + webhook.apply_options(partial.clone()); + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + /// Delete webhook by id + async fn delete_webhook(&self, webhook_id: &str) -> Result<()> { + let mut webhooks = self.channel_webhooks.lock().await; + if webhooks.remove(webhook_id).is_some() { + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } +} diff --git a/crates/core/database/src/models/mod.rs b/crates/core/database/src/models/mod.rs index badc2c24..786c4559 100644 --- a/crates/core/database/src/models/mod.rs +++ b/crates/core/database/src/models/mod.rs @@ -1,5 +1,6 @@ mod admin_migrations; mod bots; +mod channel_webhooks; mod files; mod safety_strikes; mod server_members; @@ -9,6 +10,7 @@ mod users; pub use admin_migrations::*; pub use bots::*; +pub use channel_webhooks::*; pub use files::*; pub use safety_strikes::*; pub use server_members::*; @@ -29,6 +31,7 @@ pub trait AbstractDatabase: + servers::AbstractServers + user_settings::AbstractUserSettings + users::AbstractUsers + + channel_webhooks::AbstractWebhooks { } diff --git a/crates/core/database/src/util/bridge/mod.rs b/crates/core/database/src/util/bridge/mod.rs new file mode 100644 index 00000000..2d24cd45 --- /dev/null +++ b/crates/core/database/src/util/bridge/mod.rs @@ -0,0 +1 @@ +pub mod v0; diff --git a/crates/core/database/src/util/bridge/v0.rs b/crates/core/database/src/util/bridge/v0.rs new file mode 100644 index 00000000..a35d40a6 --- /dev/null +++ b/crates/core/database/src/util/bridge/v0.rs @@ -0,0 +1,217 @@ +use revolt_models::v0::*; + +impl From for AccountStrike { + fn from(value: crate::AccountStrike) -> Self { + AccountStrike { + id: value.id, + user_id: value.user_id, + reason: value.reason, + } + } +} + +impl crate::Bot { + pub fn into_public_bot(self, user: crate::User) -> PublicBot { + #[cfg(debug_assertions)] + assert_eq!(self.id, user.id); + + PublicBot { + id: self.id, + username: user.username, + avatar: user.avatar.map(|x| x.id).unwrap_or_default(), + description: user + .profile + .map(|profile| profile.content) + .unwrap_or_default(), + } + } +} + +impl From for Bot { + fn from(value: crate::Bot) -> Self { + Bot { + id: value.id, + owner_id: value.owner, + token: value.token, + public: value.public, + analytics: value.analytics, + discoverable: value.discoverable, + interactions_url: value.interactions_url, + terms_of_service_url: value.terms_of_service_url, + privacy_policy_url: value.privacy_policy_url, + flags: value.flags.unwrap_or_default() as u32, + } + } +} + +impl From for Webhook { + fn from(value: crate::Webhook) -> Self { + Webhook { + id: value.id, + name: value.name, + avatar: value.avatar.map(|file| file.into()), + channel_id: value.channel_id, + token: value.token, + } + } +} + +impl From for PartialWebhook { + fn from(value: crate::PartialWebhook) -> Self { + PartialWebhook { + id: value.id, + name: value.name, + avatar: value.avatar.map(|file| file.into()), + channel_id: value.channel_id, + token: value.token, + } + } +} + +impl From for crate::FieldsWebhook { + fn from(_value: FieldsWebhook) -> Self { + Self::Avatar + } +} + +impl From for FieldsWebhook { + fn from(_value: crate::FieldsWebhook) -> Self { + Self::Avatar + } +} + +impl From for File { + fn from(value: crate::File) -> Self { + File { + id: value.id, + tag: value.tag, + filename: value.filename, + metadata: value.metadata.into(), + content_type: value.content_type, + size: value.size, + deleted: value.deleted, + reported: value.reported, + message_id: value.message_id, + user_id: value.user_id, + server_id: value.server_id, + object_id: value.object_id, + } + } +} + +impl From for Metadata { + fn from(value: crate::Metadata) -> Self { + match value { + crate::Metadata::File => Metadata::File, + crate::Metadata::Text => Metadata::Text, + crate::Metadata::Image { width, height } => Metadata::Image { + width: width as usize, + height: height as usize, + }, + crate::Metadata::Video { width, height } => Metadata::Video { + width: width as usize, + height: height as usize, + }, + crate::Metadata::Audio => Metadata::Audio, + } + } +} + +impl crate::User { + pub async fn into

(self, perspective: P) -> User + where + P: Into>, + { + let relationship = if let Some(perspective) = perspective.into() { + perspective + .relations + .unwrap_or_default() + .into_iter() + .find(|relationship| relationship.id == self.id) + .map(|relationship| relationship.status.into()) + .unwrap_or_default() + } else { + RelationshipStatus::None + }; + + // do permission stuff here + // TODO: implement permissions =) + let can_see_profile = false; + + User { + username: self.username, + avatar: self.avatar.map(|file| file.into()), + relations: vec![], + badges: self.badges.unwrap_or_default() as u32, + status: None, + profile: None, + flags: self.flags.unwrap_or_default() as u32, + privileged: self.privileged, + bot: self.bot.map(|bot| bot.into()), + relationship, + online: can_see_profile && revolt_presence::is_online(&self.id).await, + id: self.id, + } + } +} + +impl From for RelationshipStatus { + fn from(value: crate::RelationshipStatus) -> Self { + match value { + crate::RelationshipStatus::None => RelationshipStatus::None, + crate::RelationshipStatus::User => RelationshipStatus::User, + crate::RelationshipStatus::Friend => RelationshipStatus::Friend, + crate::RelationshipStatus::Outgoing => RelationshipStatus::Outgoing, + crate::RelationshipStatus::Incoming => RelationshipStatus::Incoming, + crate::RelationshipStatus::Blocked => RelationshipStatus::Blocked, + crate::RelationshipStatus::BlockedOther => RelationshipStatus::BlockedOther, + } + } +} + +impl From for Relationship { + fn from(value: crate::Relationship) -> Self { + Self { + user_id: value.id, + status: value.status.into(), + } + } +} + +impl From for Presence { + fn from(value: crate::Presence) -> Self { + match value { + crate::Presence::Online => Presence::Online, + crate::Presence::Idle => Presence::Idle, + crate::Presence::Focus => Presence::Focus, + crate::Presence::Busy => Presence::Busy, + crate::Presence::Invisible => Presence::Invisible, + } + } +} + +impl From for UserStatus { + fn from(value: crate::UserStatus) -> Self { + UserStatus { + text: value.text, + presence: value.presence.map(|presence| presence.into()), + } + } +} + +impl From for UserProfile { + fn from(value: crate::UserProfile) -> Self { + UserProfile { + content: value.content, + background: value.background.map(|file| file.into()), + } + } +} + +impl From for BotInformation { + fn from(value: crate::BotInformation) -> Self { + BotInformation { + owner_id: value.owner, + } + } +} diff --git a/crates/core/database/src/util/mod.rs b/crates/core/database/src/util/mod.rs index 7b1c1337..e7efee98 100644 --- a/crates/core/database/src/util/mod.rs +++ b/crates/core/database/src/util/mod.rs @@ -1,2 +1,3 @@ +pub mod bridge; pub mod permissions; pub mod reference;