diff --git a/src/migrations/m20240316_000001_create_table.rs b/src/migrations/m20240316_000001_create_table.rs index 1dd0b7b..34f4ab0 100644 --- a/src/migrations/m20240316_000001_create_table.rs +++ b/src/migrations/m20240316_000001_create_table.rs @@ -18,7 +18,19 @@ impl MigrationTrait for Migration { .primary_key() ) .col(ColumnDef::new(Users::ActorType).string().not_null()) + .col(ColumnDef::new(Users::Domain).string().not_null()) .col(ColumnDef::new(Users::Name).string().not_null()) + .col(ColumnDef::new(Users::Summary).string().null()) + .col(ColumnDef::new(Users::Image).string().null()) + .col(ColumnDef::new(Users::Icon).string().null()) + .col(ColumnDef::new(Users::PreferredUsername).string().null()) + .col(ColumnDef::new(Users::Inbox).string().null()) + .col(ColumnDef::new(Users::SharedInbox).string().null()) + .col(ColumnDef::new(Users::Outbox).string().null()) + .col(ColumnDef::new(Users::Following).string().null()) + .col(ColumnDef::new(Users::Followers).string().null()) + .col(ColumnDef::new(Users::Created).date_time().not_null()) + .col(ColumnDef::new(Users::Updated).date_time().not_null()) .to_owned() ) .await?; @@ -38,7 +50,11 @@ impl MigrationTrait for Migration { .col(ColumnDef::new(Activities::Actor).string().not_null()) .col(ColumnDef::new(Activities::Object).string().null()) .col(ColumnDef::new(Activities::Target).string().null()) - .col(ColumnDef::new(Activities::Published).string().null()) + .col(ColumnDef::new(Activities::To).json().null()) + .col(ColumnDef::new(Activities::Bto).json().null()) + .col(ColumnDef::new(Activities::Cc).json().null()) + .col(ColumnDef::new(Activities::Bcc).json().null()) + .col(ColumnDef::new(Activities::Published).date_time().not_null()) .to_owned() ).await?; @@ -58,7 +74,7 @@ impl MigrationTrait for Migration { .col(ColumnDef::new(Objects::Name).string().null()) .col(ColumnDef::new(Objects::Summary).string().null()) .col(ColumnDef::new(Objects::Content).string().null()) - .col(ColumnDef::new(Objects::Published).string().null()) + .col(ColumnDef::new(Objects::Published).string().not_null()) .to_owned() ).await?; @@ -86,8 +102,20 @@ impl MigrationTrait for Migration { enum Users { Table, Id, + Domain, ActorType, Name, + Summary, + Image, + Icon, + PreferredUsername, + Inbox, + SharedInbox, + Outbox, + Following, + Followers, + Created, + Updated, } #[derive(DeriveIden)] @@ -98,7 +126,11 @@ enum Activities { Actor, Object, Target, - Published + Cc, + Bcc, + To, + Bto, + Published, } #[derive(DeriveIden)] @@ -106,9 +138,9 @@ enum Objects { Table, Id, ObjectType, + AttributedTo, Name, Summary, - AttributedTo, Content, Published, } diff --git a/src/model/activity.rs b/src/model/activity.rs index 90243be..0e64950 100644 --- a/src/model/activity.rs +++ b/src/model/activity.rs @@ -1,6 +1,22 @@ -use sea_orm::entity::prelude::*; +use sea_orm::{entity::prelude::*, FromJsonQueryResult}; -use crate::activitystream::{Node, macros::InsertValue, object::{activity::{Activity, ActivityType}, actor::Actor, Object, ObjectType}, Base, BaseType}; +use crate::activitystream::{self, link::Link, object::{activity::{Activity, ActivityMut, ActivityType}, actor::Actor, Object, ObjectMut, ObjectType}, Base, BaseMut, BaseType, Node}; + +#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, FromJsonQueryResult)] +pub struct Audience(pub Vec); + +impl From> for Audience { + fn from(value: Node) -> Self { + Audience( + match value { + Node::Empty => vec![], + Node::Link(l) => vec![l.href().to_string()], + Node::Object(o) => if let Some(id) = o.id() { vec![id.to_string()] } else { vec![] }, + Node::Array(arr) => arr.into_iter().filter_map(|l| Some(l.id()?.to_string())).collect(), + } + ) + } +} #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "activities")] @@ -12,14 +28,45 @@ pub struct Model { pub activity_type: ActivityType, pub actor: String, // TODO relates to USER pub object: Option, // TODO relates to NOTES maybe????? maybe other tables?????? + pub target: Option, // TODO relates to USER maybe?? + pub cc: Audience, + pub bcc: Audience, + pub to: Audience, + pub bto: Audience, pub published: ChronoDateTimeUtc, // TODO: origin, result, instrument } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::Actor", + to = "super::user::Column::Id" + )] + User, + + #[sea_orm( + belongs_to = "super::object::Entity", + from = "Column::Object", + to = "super::object::Column::Id" + )] + Object, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Object.def() + } +} impl ActiveModelBehavior for ActiveModel {} @@ -33,21 +80,44 @@ impl Base for Model { } fn underlying_json_object(self) -> serde_json::Value { - let mut map = serde_json::Map::new(); - map.insert_str("id", Some(&self.id)); - map.insert_str("type", Some(self.activity_type.as_ref())); - map.insert_str("actor", Some(&self.actor)); - map.insert_str("object", self.object.as_deref()); - map.insert_str("target", self.target.as_deref()); - map.insert_timestr("published", Some(self.published)); - serde_json::Value::Object(map) + activitystream::object() + .set_id(Some(&self.id)) + .set_activity_type(Some(self.activity_type)) + .set_actor(Node::link(self.actor)) + .set_object(Node::maybe_link(self.object)) + .set_target(Node::maybe_link(self.target)) + .set_published(Some(self.published)) + .set_to(Node::links(self.to.0.clone())) + .set_bto(Node::empty()) + .set_cc(Node::links(self.cc.0.clone())) + .set_bcc(Node::empty()) } } impl Object for Model { + fn object_type(&self) -> Option { + Some(ObjectType::Activity(self.activity_type)) + } + fn published(&self) -> Option> { Some(self.published) } + + fn to(&self) -> Node { + Node::links(self.to.0.clone()) + } + + fn bto(&self) -> Node { + Node::links(self.bto.0.clone()) + } + + fn cc(&self) -> Node { + Node::links(self.cc.0.clone()) + } + + fn bcc(&self) -> Node { + Node::links(self.bcc.0.clone()) + } } impl Activity for Model { @@ -83,6 +153,10 @@ impl Model { object: activity.object().id().map(|x| x.to_string()), target: activity.target().id().map(|x| x.to_string()), published: activity.published().ok_or(super::FieldError("published"))?, + to: activity.to().into(), + bto: activity.bto().into(), + cc: activity.cc().into(), + bcc: activity.bcc().into(), }) } } diff --git a/src/model/mod.rs b/src/model/mod.rs index c959335..9921718 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,3 +1,7 @@ +use sea_orm::IntoActiveModel; + +use crate::{activitypub::PUBLIC_TARGET, activitystream::object::actor::Actor, model::activity::Audience}; + pub mod user; pub mod object; pub mod activity; @@ -7,34 +11,52 @@ pub mod activity; pub struct FieldError(pub &'static str); pub async fn faker(db: &sea_orm::DatabaseConnection, domain: String) -> Result<(), sea_orm::DbErr> { - use sea_orm::EntityTrait; + use sea_orm::{EntityTrait, Set}; - user::Entity::insert(user::ActiveModel { - id: sea_orm::Set(format!("{domain}/users/root")), - name: sea_orm::Set("root".into()), - actor_type: sea_orm::Set(super::activitystream::object::actor::ActorType::Person), - }).exec(db).await?; + let root = user::Model { + id: format!("{domain}/users/root"), + name: "root".into(), + domain: crate::activitypub::domain(&domain), + preferred_username: Some("Administrator".to_string()), + summary: Some("hello world! i'm manually generated but served dynamically from db!".to_string()), + following: None, + followers: 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()), + inbox: None, + shared_inbox: None, + outbox: None, + actor_type: super::activitystream::object::actor::ActorType::Person, + created: chrono::Utc::now(), + updated: chrono::Utc::now(), + }; + + user::Entity::insert(root.clone().into_active_model()).exec(db).await?; for i in (0..100).rev() { let oid = uuid::Uuid::new_v4(); let aid = uuid::Uuid::new_v4(); object::Entity::insert(object::ActiveModel { - id: sea_orm::Set(format!("{domain}/objects/{oid}")), - name: sea_orm::Set(None), - object_type: sea_orm::Set(crate::activitystream::object::ObjectType::Note), - attributed_to: sea_orm::Set(Some(format!("{domain}/users/root"))), - summary: sea_orm::Set(None), - content: sea_orm::Set(Some(format!("[{i}] Tic(k). Quasiparticle of intensive multiplicity. Tics (or ticks) are intrinsically several components of autonomously numbering anorganic populations, propagating by contagion between segmentary divisions in the order of nature. Ticks - as nonqualitative differentially-decomposable counting marks - each designate a multitude comprehended as a singular variation in tic(k)-density."))), - published: sea_orm::Set(chrono::Utc::now() - std::time::Duration::from_secs(60*i)), + id: Set(format!("{domain}/objects/{oid}")), + name: Set(None), + object_type: Set(crate::activitystream::object::ObjectType::Note), + attributed_to: Set(Some(format!("{domain}/users/root"))), + summary: Set(None), + content: Set(Some(format!("[{i}] Tic(k). Quasiparticle of intensive multiplicity. Tics (or ticks) are intrinsically several components of autonomously numbering anorganic populations, propagating by contagion between segmentary divisions in the order of nature. Ticks - as nonqualitative differentially-decomposable counting marks - each designate a multitude comprehended as a singular variation in tic(k)-density."))), + published: Set(chrono::Utc::now() - std::time::Duration::from_secs(60*i)), }).exec(db).await?; activity::Entity::insert(activity::ActiveModel { - id: sea_orm::Set(format!("{domain}/activities/{aid}")), - activity_type: sea_orm::Set(crate::activitystream::object::activity::ActivityType::Create), - actor: sea_orm::Set(format!("{domain}/users/root")), - object: sea_orm::Set(Some(format!("{domain}/objects/{oid}"))), - target: sea_orm::Set(None), - published: sea_orm::Set(chrono::Utc::now() - std::time::Duration::from_secs(60*i)), + id: Set(format!("{domain}/activities/{aid}")), + activity_type: Set(crate::activitystream::object::activity::ActivityType::Create), + actor: Set(format!("{domain}/users/root")), + object: Set(Some(format!("{domain}/objects/{oid}"))), + target: Set(None), + published: Set(chrono::Utc::now() - std::time::Duration::from_secs(60*i)), + to: Set(Audience(vec![PUBLIC_TARGET.to_string()])), + bto: Set(Audience::default()), + cc: Set(Audience(vec![root.followers().id().unwrap_or("").to_string()])), + bcc: Set(Audience::default()), }).exec(db).await?; } diff --git a/src/model/object.rs b/src/model/object.rs index 08ffa44..91ca3ae 100644 --- a/src/model/object.rs +++ b/src/model/object.rs @@ -1,6 +1,7 @@ use sea_orm::entity::prelude::*; +use crate::activitystream::prelude::*; -use crate::activitystream::{macros::InsertValue, object::{actor::Actor, Object, ObjectType}, Base, BaseType, Node}; +use crate::activitystream::{object::ObjectType, BaseType, Node}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "objects")] @@ -17,11 +18,33 @@ pub struct Model { } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm(has_many = "super::activity::Entity")] + Activity, + + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::AttributedTo", + to = "super::user::Column::Id", + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Activity.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} impl ActiveModelBehavior for ActiveModel {} -impl Base for Model { +impl crate::activitystream::Base for Model { fn id(&self) -> Option<&str> { Some(&self.id) } @@ -31,24 +54,23 @@ impl Base for Model { } fn underlying_json_object(self) -> serde_json::Value { - let mut map = serde_json::Map::new(); - map.insert_str("id", Some(&self.id)); - map.insert_str("type", Some(self.object_type.as_ref())); - map.insert_str("attributedTo", self.attributed_to.as_deref()); - map.insert_str("name", self.name.as_deref()); - map.insert_str("summary", self.summary.as_deref()); - map.insert_str("content", self.content.as_deref()); - map.insert_timestr("published", Some(self.published)); - serde_json::Value::Object(map) + crate::activitystream::object() + .set_id(Some(&self.id)) + .set_object_type(Some(self.object_type)) + .set_attributed_to(Node::maybe_link(self.attributed_to)) + .set_name(self.name.as_deref()) + .set_summary(self.summary.as_deref()) + .set_content(self.content.as_deref()) + .set_published(Some(self.published)) } } -impl Object for Model { +impl crate::activitystream::object::Object for Model { fn object_type(&self) -> Option { Some(self.object_type) } - fn attributed_to(&self) -> Node { + fn attributed_to(&self) -> Node { Node::::from(self.attributed_to.as_deref()) } @@ -70,7 +92,7 @@ impl Object for Model { } impl Model { - pub fn new(object: &impl Object) -> Result { + pub fn new(object: &impl crate::activitystream::object::Object) -> Result { Ok(Model { id: object.id().ok_or(super::FieldError("id"))?.to_string(), object_type: object.object_type().ok_or(super::FieldError("type"))?, diff --git a/src/model/user.rs b/src/model/user.rs index 02b9222..9f053f2 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -1,25 +1,63 @@ use sea_orm::entity::prelude::*; +use crate::activitystream::prelude::*; -use crate::activitystream::{macros::InsertValue, object::actor::{Actor, ActorType}, Base, BaseType, Object, ObjectType}; +use crate::{activitypub, activitystream::{object::actor::ActorType, BaseType, Node, ObjectType}}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "users")] pub struct Model { #[sea_orm(primary_key)] - /// must be user@instance.org, even if local! TODO bad design... + /// must be full AP ID, since they are unique over the network pub id: String, - + pub domain: String, pub actor_type: ActorType, pub name: String, + pub summary: Option, + pub image: Option, + pub icon: Option, + + pub preferred_username: Option, + pub inbox: Option, + pub shared_inbox: Option, + pub outbox: Option, + pub following: Option, + pub followers: Option, + + pub created: ChronoDateTimeUtc, + pub updated: ChronoDateTimeUtc, + + // TODO these are also suggested + // pub liked: Option, + // pub streams: Option, } +use crate::activitystream::object::{actor::Actor, collection::Collection, document::Image}; + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm(has_many = "super::activity::Entity")] + Activity, + + #[sea_orm(has_many = "super::object::Entity")] + Object, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Activity.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Object.def() + } +} impl ActiveModelBehavior for ActiveModel {} -impl Base for Model { +impl crate::activitystream::Base for Model { fn id(&self) -> Option<&str> { Some(&self.id) } @@ -29,32 +67,94 @@ impl Base for Model { } fn underlying_json_object(self) -> serde_json::Value { - let mut map = serde_json::Map::new(); - map.insert_str("id", Some(&self.id)); - map.insert_str("type", Some(self.actor_type.as_ref())); - map.insert_str("name", Some(&self.name)); - serde_json::Value::Object(map) + crate::activitystream::object() + .set_id(Some(&self.id)) + .set_actor_type(Some(self.actor_type)) + .set_name(Some(&self.name)) + .set_summary(self.summary.as_deref()) + .set_icon(self.icon()) + .set_image(self.image()) + .set_preferred_username(self.preferred_username.as_deref()) + .set_inbox(self.inbox()) + .set_outbox(self.outbox()) + .set_following(self.following()) + .set_followers(self.followers()) + .underlying_json_object() } } -impl Object for Model { - fn name (&self) -> Option<&str> { +impl crate::activitystream::object::Object for Model { + fn name(&self) -> Option<&str> { Some(&self.name) } + + fn summary(&self) -> Option<&str> { + self.summary.as_deref() + } + + fn icon(&self) -> Node { + match &self.icon { + Some(x) => Node::link(x.to_string()), + None => Node::Empty, + } + } + + fn image(&self) -> Node { + match &self.image { + Some(x) => Node::link(x.to_string()), + None => Node::Empty, + } + } + + fn published(&self) -> Option> { + Some(self.created) + } } -impl Actor for Model { +impl crate::activitystream::object::actor::Actor for Model { fn actor_type(&self) -> Option { Some(self.actor_type) } + + fn preferred_username(&self) -> Option<&str> { + self.preferred_username.as_deref() + } + + fn inbox(&self) -> Node { + Node::link(self.inbox.clone().unwrap_or(format!("https://{}/users/{}/inbox", self.domain, self.name))) + } + + fn outbox(&self) -> Node { + Node::link(self.outbox.clone().unwrap_or(format!("https://{}/users/{}/outbox", self.domain, self.name))) + } + + fn following(&self) -> Node { + Node::link(self.following.clone().unwrap_or(format!("https://{}/users/{}/following", self.domain, self.name))) + } + + fn followers(&self) -> Node { + Node::link(self.following.clone().unwrap_or(format!("https://{}/users/{}/followers", self.domain, self.name))) + } } impl Model { pub fn new(object: &impl Actor) -> Result { + let ap_id = object.id().ok_or(super::FieldError("id"))?.to_string(); + let (domain, name) = activitypub::split_id(&ap_id); Ok(Model { - id: object.id().ok_or(super::FieldError("id"))?.to_string(), + id: ap_id, name, domain, actor_type: object.actor_type().ok_or(super::FieldError("type"))?, - name: object.name().ok_or(super::FieldError("name"))?.to_string(), + preferred_username: object.preferred_username().map(|x| x.to_string()), + summary: object.summary().map(|x| x.to_string()), + icon: object.icon().id().map(|x| x.to_string()), + image: object.image().id().map(|x| x.to_string()), + inbox: object.inbox().id().map(|x| x.to_string()), + outbox: object.inbox().id().map(|x| x.to_string()), + shared_inbox: None, // TODO!!! parse endpoints + followers: object.followers().id().map(|x| x.to_string()), + following: object.following().id().map(|x| x.to_string()), + created: object.published().unwrap_or(chrono::Utc::now()), + updated: chrono::Utc::now(), }) } }