From fafe5307c5bd7d91c3f67f1d8c13503568031763 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 15 Jul 2024 20:20:43 +0200 Subject: [PATCH] fix: generic JsonVec that accepts null not the cleanest solution but should be generic and transparent --- upub/cli/src/faker.rs | 22 +++++------ upub/core/src/ext.rs | 63 +++++++++++++++++++++++++++++++ upub/core/src/init.rs | 6 +-- upub/core/src/model/activity.rs | 10 ++--- upub/core/src/model/actor.rs | 18 ++++++--- upub/core/src/model/mod.rs | 20 ---------- upub/core/src/model/object.rs | 10 ++--- upub/core/src/traits/admin.rs | 6 ++- upub/core/src/traits/normalize.rs | 25 +++++++----- 9 files changed, 119 insertions(+), 61 deletions(-) diff --git a/upub/cli/src/faker.rs b/upub/cli/src/faker.rs index 1ba9f91..ce4bd69 100644 --- a/upub/cli/src/faker.rs +++ b/upub/cli/src/faker.rs @@ -1,4 +1,4 @@ -use upub::model::{addressing, config, credential, activity, object, actor, Audience}; +use upub::{ext::JsonVec, model::{activity, actor, addressing, config, credential, object}}; use openssl::rsa::Rsa; use sea_orm::{ActiveValue::NotSet, IntoActiveModel}; @@ -21,8 +21,8 @@ pub async fn faker(ctx: upub::Context, count: i64) -> Result<(), sea_orm::DbErr> followers: None, followers_count: 0, statuses_count: count as i32, - fields: vec![], - also_known_as: vec![], + fields: JsonVec::default(), + also_known_as: JsonVec::default(), moved_to: None, icon: Some("https://cdn.alemi.dev/social/circle-square.png".to_string()), image: Some("https://cdn.alemi.dev/social/someriver-xs.jpg".to_string()), @@ -90,10 +90,10 @@ pub async fn faker(ctx: upub::Context, count: i64) -> Result<(), sea_orm::DbErr> likes: Set(0), announces: Set(0), audience: Set(None), - to: Set(Audience(vec![apb::target::PUBLIC.to_string()])), - bto: Set(Audience::default()), - cc: Set(Audience(vec![])), - bcc: Set(Audience::default()), + to: Set(JsonVec(vec![apb::target::PUBLIC.to_string()])), + bto: Set(JsonVec::default()), + cc: Set(JsonVec(vec![])), + bcc: Set(JsonVec::default()), url: Set(None), sensitive: Set(false), }).exec(db).await?; @@ -106,10 +106,10 @@ pub async fn faker(ctx: upub::Context, count: i64) -> Result<(), sea_orm::DbErr> object: Set(Some(format!("{domain}/objects/{oid}"))), target: Set(None), published: Set(chrono::Utc::now() - std::time::Duration::from_secs(60*i as u64)), - to: Set(Audience(vec![apb::target::PUBLIC.to_string()])), - bto: Set(Audience::default()), - cc: Set(Audience(vec![])), - bcc: Set(Audience::default()), + to: Set(JsonVec(vec![apb::target::PUBLIC.to_string()])), + bto: Set(JsonVec::default()), + cc: Set(JsonVec(vec![])), + bcc: Set(JsonVec::default()), }).exec(db).await?; } diff --git a/upub/core/src/ext.rs b/upub/core/src/ext.rs index b08651f..2b02a15 100644 --- a/upub/core/src/ext.rs +++ b/upub/core/src/ext.rs @@ -48,3 +48,66 @@ impl LoggableError for Result { } } } + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct JsonVec(pub Vec); + +impl From> for JsonVec { + fn from(value: Vec) -> Self { + JsonVec(value) + } +} + +impl Default for JsonVec { + fn default() -> Self { + JsonVec(Vec::new()) + } +} + +impl sea_orm::TryGetableFromJson for JsonVec {} + +impl std::convert::From> for sea_orm::Value { + fn from(source: JsonVec) -> Self { + sea_orm::Value::Json(serde_json::to_value(&source).ok().map(std::boxed::Box::new)) + } +} + +impl sea_orm::sea_query::ValueType for JsonVec { + fn try_from(v: sea_orm::Value) -> Result { + match v { + sea_orm::Value::Json(Some(json)) => Ok( + serde_json::from_value(*json).map_err(|_| sea_orm::sea_query::ValueTypeErr)?, + ), + sea_orm::Value::Json(None) => Ok(JsonVec::default()), + _ => Err(sea_orm::sea_query::ValueTypeErr), + } + } + + fn type_name() -> String { + format!("JsonVec_{}", T::type_name()) + } + + fn array_type() -> sea_orm::sea_query::ArrayType { + sea_orm::sea_query::ArrayType::Json + } + + fn column_type() -> sea_orm::sea_query::ColumnType { + sea_orm::sea_query::ColumnType::Json + } +} + +impl sea_orm::sea_query::Nullable for JsonVec { + fn null() -> sea_orm::Value { + sea_orm::Value::Json(None) + } +} + +pub trait TypeName { + fn type_name() -> String; +} + +impl TypeName for String { + fn type_name() -> String { + "String".to_string() + } +} diff --git a/upub/core/src/init.rs b/upub/core/src/init.rs index 69b01ef..447429e 100644 --- a/upub/core/src/init.rs +++ b/upub/core/src/init.rs @@ -1,7 +1,7 @@ use openssl::rsa::Rsa; use sea_orm::{ActiveValue::{NotSet, Set}, DatabaseConnection, EntityTrait}; -use crate::model; +use crate::{ext::JsonVec, model}; #[derive(Debug, thiserror::Error)] pub enum InitError { @@ -34,9 +34,9 @@ pub async fn application( domain: Set(domain.clone()), preferred_username: Set(domain.clone()), actor_type: Set(apb::ActorType::Application), - also_known_as: Set(vec![]), + also_known_as: Set(JsonVec::default()), moved_to: Set(None), - fields: Set(vec![]), // TODO we could put some useful things here actually + fields: Set(JsonVec::default()), // TODO we could put some useful things here actually private_key: Set(Some(privk)), public_key: Set(pubk), following: Set(None), diff --git a/upub/core/src/model/activity.rs b/upub/core/src/model/activity.rs index 7916dc3..21946e2 100644 --- a/upub/core/src/model/activity.rs +++ b/upub/core/src/model/activity.rs @@ -1,7 +1,7 @@ use apb::{ActivityMut, ActivityType, BaseMut, ObjectMut}; use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns}; -use crate::model::Audience; +use crate::ext::JsonVec; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "activities")] @@ -14,10 +14,10 @@ pub struct Model { pub actor: String, pub object: Option, pub target: Option, - pub to: Audience, - pub bto: Audience, - pub cc: Audience, - pub bcc: Audience, + pub to: JsonVec, + pub bto: JsonVec, + pub cc: JsonVec, + pub bcc: JsonVec, pub published: ChronoDateTimeUtc, } diff --git a/upub/core/src/model/actor.rs b/upub/core/src/model/actor.rs index fea8675..93d0902 100644 --- a/upub/core/src/model/actor.rs +++ b/upub/core/src/model/actor.rs @@ -2,7 +2,9 @@ use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns}; use apb::{field::OptionalString, ActorMut, ActorType, BaseMut, DocumentMut, EndpointsMut, ObjectMut, PublicKeyMut}; -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, sea_orm::FromJsonQueryResult)] +use crate::ext::{JsonVec, TypeName}; + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct Field { pub name: String, pub content: String, @@ -12,6 +14,12 @@ pub struct Field { pub field_type: String, } +impl TypeName for Field { + fn type_name() -> String { + "Field".to_string() + } +} + impl From for Field { fn from(value: T) -> Self { Field { @@ -37,7 +45,7 @@ pub struct Model { pub image: Option, pub icon: Option, pub preferred_username: String, - pub fields: Vec, + pub fields: JsonVec, pub inbox: Option, pub shared_inbox: Option, pub outbox: Option, @@ -50,7 +58,7 @@ pub struct Model { pub private_key: Option, pub published: ChronoDateTimeUtc, pub updated: ChronoDateTimeUtc, - pub also_known_as: Vec, + pub also_known_as: JsonVec, pub moved_to: Option, } @@ -208,7 +216,7 @@ impl Model { .set_url(apb::Node::link(i.clone())) ))) .set_attachment(apb::Node::array( - self.fields + self.fields.0 .into_iter() .filter_map(|x| serde_json::to_value(x).ok()) .collect() @@ -233,7 +241,7 @@ impl Model { apb::new() .set_shared_inbox(self.shared_inbox.as_deref()) )) - .set_also_known_as(apb::Node::links(self.also_known_as)) + .set_also_known_as(apb::Node::links(self.also_known_as.0)) .set_moved_to(apb::Node::maybe_link(self.moved_to)) .set_discoverable(Some(true)) } diff --git a/upub/core/src/model/mod.rs b/upub/core/src/model/mod.rs index 9320ee5..8eabaa2 100644 --- a/upub/core/src/model/mod.rs +++ b/upub/core/src/model/mod.rs @@ -19,23 +19,3 @@ pub mod dislike; pub mod hashtag; pub mod mention; pub mod attachment; - - -#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, sea_orm::FromJsonQueryResult)] -pub struct Audience(pub Vec); - -impl From> for Audience { - fn from(value: apb::Node) -> Self { - use apb::field::OptionalString; - - Audience( - match value { - apb::Node::Empty => vec![], - apb::Node::Link(l) => l.href().map(|x| vec![x.to_string()]).unwrap_or_default(), - apb::Node::Object(o) => if let Ok(id) = o.id() { vec![id.to_string()] } else { vec![] }, - apb::Node::Array(arr) => arr.into_iter().filter_map(|l| l.id().str()).collect(), - } - ) - } -} - diff --git a/upub/core/src/model/object.rs b/upub/core/src/model/object.rs index f109584..bf5e045 100644 --- a/upub/core/src/model/object.rs +++ b/upub/core/src/model/object.rs @@ -1,7 +1,7 @@ use apb::{BaseMut, CollectionMut, DocumentMut, ObjectMut, ObjectType}; use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns}; -use super::Audience; +use crate::ext::JsonVec; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "objects")] @@ -24,10 +24,10 @@ pub struct Model { pub announces: i32, pub replies: i32, pub context: Option, - pub to: Audience, - pub bto: Audience, - pub cc: Audience, - pub bcc: Audience, + pub to: JsonVec, + pub bto: JsonVec, + pub cc: JsonVec, + pub bcc: JsonVec, pub published: ChronoDateTimeUtc, pub updated: ChronoDateTimeUtc, diff --git a/upub/core/src/traits/admin.rs b/upub/core/src/traits/admin.rs index bedaaea..a9063c4 100644 --- a/upub/core/src/traits/admin.rs +++ b/upub/core/src/traits/admin.rs @@ -1,5 +1,7 @@ use sea_orm::{ActiveValue::{NotSet, Set}, DbErr, EntityTrait}; +use crate::ext::JsonVec; + #[async_trait::async_trait] pub trait Administrable { async fn register_user( @@ -35,13 +37,13 @@ impl Administrable for crate::Context { domain: Set(domain), summary: Set(summary), preferred_username: Set(username.clone()), - fields: Set(vec![]), + fields: Set(JsonVec::default()), following: Set(None), following_count: Set(0), followers: Set(None), followers_count: Set(0), statuses_count: Set(0), - also_known_as: Set(vec![]), + also_known_as: Set(JsonVec::default()), moved_to: Set(None), icon: Set(avatar_url), image: Set(banner_url), diff --git a/upub/core/src/traits/normalize.rs b/upub/core/src/traits/normalize.rs index d100390..2c1135e 100644 --- a/upub/core/src/traits/normalize.rs +++ b/upub/core/src/traits/normalize.rs @@ -203,10 +203,10 @@ impl AP { object: activity.object().id().str(), target: activity.target().id().str(), published: activity.published().unwrap_or(chrono::Utc::now()), - to: activity.to().into(), - bto: activity.bto().into(), - cc: activity.cc().into(), - bcc: activity.bcc().into(), + to: activity.to().all_ids().into(), + bto: activity.bto().all_ids().into(), + cc: activity.cc().all_ids().into(), + bcc: activity.bcc().all_ids().into(), }) } @@ -287,10 +287,10 @@ impl AP { announces: object.shares().get() .map_or(0, |x| x.total_items().unwrap_or(0)) as i32, audience: object.audience().id().str(), - to: object.to().into(), - bto: object.bto().into(), - cc: object.cc().into(), - bcc: object.bcc().into(), + to: object.to().all_ids().into(), + bto: object.bto().all_ids().into(), + cc: object.cc().all_ids().into(), + bcc: object.bcc().all_ids().into(), sensitive: object.sensitive().unwrap_or(false), }) @@ -333,13 +333,12 @@ impl AP { summary: actor.summary().str(), icon: actor.icon().get().and_then(|x| x.url().id().str()), image: actor.image().get().and_then(|x| x.url().id().str()), - fields: actor.attachment().flat().into_iter().filter_map(|x| Some(crate::model::actor::Field::from(x.extract()?))).collect(), inbox: actor.inbox().id().str(), outbox: actor.outbox().id().str(), shared_inbox: actor.endpoints().get().and_then(|x| x.shared_inbox().str()), followers: actor.followers().id().str(), following: actor.following().id().str(), - also_known_as: actor.also_known_as().flat().into_iter().filter_map(|x| x.id().str()).collect(), + also_known_as: actor.also_known_as().flat().into_iter().filter_map(|x| x.id().str()).collect::>().into(), moved_to: actor.moved_to().id().str(), published: actor.published().unwrap_or(chrono::Utc::now()), updated: chrono::Utc::now(), @@ -348,6 +347,12 @@ impl AP { statuses_count: actor.statuses_count().unwrap_or(0) as i32, public_key: actor.public_key().get().ok_or(apb::FieldErr("publicKey"))?.public_key_pem().to_string(), private_key: None, // there's no way to transport privkey over AP json, must come from DB + fields: actor.attachment() + .flat() + .into_iter() + .filter_map(|x| Some(crate::model::actor::Field::from(x.extract()?))) + .collect::>() + .into(), }) }