diff --git a/src/activitystream/activity.rs b/src/activitystream/activity.rs index 4b580391..004feba7 100644 --- a/src/activitystream/activity.rs +++ b/src/activitystream/activity.rs @@ -2,10 +2,55 @@ pub trait Activity : super::Object { fn activity_type(&self) -> Option { None } fn actor_id(&self) -> Option<&str> { None } - fn actor(&self) -> Option<&impl super::Object> { None::<&()> } + fn actor(&self) -> Option> { None::> } fn object_id(&self) -> Option<&str> { None } - fn object(&self) -> Option<&impl super::Object> { None::<&()> } + fn object(&self) -> Option> { None::> } fn target(&self) -> Option<&str> { None } } + +impl Activity for serde_json::Value { + fn activity_type(&self) -> Option { + let serde_json::Value::String(t) = self.get("type")? else { return None }; + super::types::ActivityType::try_from(t.as_str()).ok() + } + + fn object(&self) -> Option> { + let obj = self.get("object")?; + match obj { + serde_json::Value::Object(_) => Some(obj.clone().into()), + _ => None, + } + } + + fn object_id(&self) -> Option<&str> { + match self.get("object")? { + serde_json::Value::Object(map) => match map.get("id")? { + serde_json::Value::String(id) => Some(id), + _ => None, + }, + serde_json::Value::String(id) => Some(id), + _ => None, + } + } + + fn actor(&self) -> Option> { + let obj = self.get("actor")?; + match obj { + serde_json::Value::Object(_) => Some(obj.clone().into()), + _ => None, + } + } + + fn actor_id(&self) -> Option<&str> { + match self.get("actor")? { + serde_json::Value::Object(map) => match map.get("id")? { + serde_json::Value::String(id) => Some(id), + _ => None, + }, + serde_json::Value::String(id) => Some(id), + _ => None, + } + } +} diff --git a/src/activitystream/actor.rs b/src/activitystream/actor.rs index e69de29b..70a2a75d 100644 --- a/src/activitystream/actor.rs +++ b/src/activitystream/actor.rs @@ -0,0 +1,3 @@ +pub trait Actor : super::Object { + fn actor_type(&self) -> Option { None } +} diff --git a/src/activitystream/link.rs b/src/activitystream/link.rs new file mode 100644 index 00000000..4aa65b5c --- /dev/null +++ b/src/activitystream/link.rs @@ -0,0 +1,66 @@ +pub trait Link { + fn href(&self) -> &str; + fn rel(&self) -> Option<&str> { None } + fn media_type(&self) -> Option<&str> { None } // also in obj + fn name(&self) -> Option<&str> { None } // also in obj + fn hreflang(&self) -> Option<&str> { None } + fn height(&self) -> Option<&str> { None } + fn width(&self) -> Option<&str> { None } + fn preview(&self) -> Option<&str> { None } // also in obj +} + +pub enum LinkedObject { + Object(T), + Link(Box), +} + +impl LinkedObject +where + T : for<'de> serde::Deserialize<'de>, +{ + pub async fn resolve(self) -> T { + match self { + LinkedObject::Object(o) => o, + LinkedObject::Link(l) => + reqwest::get(l.href()) + .await.unwrap() + .json::() + .await.unwrap(), + } + } +} + +impl Link for String { + fn href(&self) -> &str { + self + } +} + +impl Link for serde_json::Value { + // TODO this is unchecked and can panic + fn href(&self) -> &str { + match self { + serde_json::Value::String(x) => x, + serde_json::Value::Object(map) => + map.get("href") + .unwrap() + .as_str() + .unwrap(), + _ => panic!("invalid value for Link"), + + } + } + + // ... TODO! +} + +impl From for LinkedObject { + fn from(value: serde_json::Value) -> Self { + if value.is_string() || value.get("href").is_some() { + Self::Link(Box::new(value)) + } else { + Self::Object(value) + } + } +} + diff --git a/src/activitystream/mod.rs b/src/activitystream/mod.rs index 8c9161b1..6f2c4df5 100644 --- a/src/activitystream/mod.rs +++ b/src/activitystream/mod.rs @@ -1,6 +1,9 @@ - pub mod object; -pub use object::{Object, Link, ObjectOrLink}; +pub use object::Object; + + +pub mod actor; +pub use actor::Actor; pub mod activity; @@ -8,4 +11,52 @@ pub use activity::Activity; pub mod types; -pub use types::Type; +pub use types::{BaseType, ObjectType, ActivityType, ActorType}; + + +pub mod link; +pub use link::{Link, LinkedObject}; + +pub trait ToJson : Object { + fn json(&self) -> serde_json::Value; +} + +impl ToJson for T where T : Object { + fn json(&self) -> serde_json::Value { + let mut map = serde_json::Map::new(); + let mp = &mut map; + + put_str(mp, "id", self.id()); + put_str(mp, "attributedTo", self.attributed_to()); + put_str(mp, "name", self.name()); + put_str(mp, "summary", self.summary()); + put_str(mp, "content", self.content()); + + if let Some(t) = self.full_type() { + map.insert( + "type".to_string(), + serde_json::Value::String(format!("{t:?}")), + ); + } + + if let Some(published) = self.published() { + map.insert( + "published".to_string(), + serde_json::Value::String(published.to_rfc3339()), + ); + } + + // ... TODO! + + serde_json::Value::Object(map) + } +} + +fn put_str(map: &mut serde_json::Map, k: &str, v: Option<&str>) { + if let Some(v) = v { + map.insert( + k.to_string(), + serde_json::Value::String(v.to_string()), + ); + } +} diff --git a/src/activitystream/object.rs b/src/activitystream/object.rs index dd151cd5..35d8b100 100644 --- a/src/activitystream/object.rs +++ b/src/activitystream/object.rs @@ -1,33 +1,6 @@ -pub enum ObjectOrLink { - Object(Box), - Link(Box), -} - -impl From for ObjectOrLink { - fn from(value: serde_json::Value) -> Self { - if value.get("href").is_some() { - Self::Link(Box::new(value)) - } else { - Self::Object(Box::new(value)) - } - } -} - -pub trait Link { - fn href(&self) -> Option<&str> { None } - fn rel(&self) -> Option<&str> { None } - fn media_type(&self) -> Option<&str> { None } // also in obj - fn name(&self) -> Option<&str> { None } // also in obj - fn hreflang(&self) -> Option<&str> { None } - fn height(&self) -> Option<&str> { None } - fn width(&self) -> Option<&str> { None } - fn preview(&self) -> Option<&str> { None } // also in obj -} - - pub trait Object { fn id(&self) -> Option<&str> { None } - fn full_type(&self) -> Option { None } + fn full_type(&self) -> Option { None } fn attachment (&self) -> Option<&str> { None } fn attributed_to (&self) -> Option<&str> { None } fn audience (&self) -> Option<&str> { None } @@ -66,61 +39,9 @@ impl Object for serde_json::Value { self.get("id")?.as_str() } - fn full_type(&self) -> Option { - todo!() + fn full_type(&self) -> Option { + self.get("type")?.as_str()?.try_into().ok() } // ... TODO! } - -impl Link for serde_json::Value { - fn href(&self) -> Option<&str> { - self.get("href")?.as_str() - } - - // ... TODO! -} - -pub trait ToJson : Object { - fn json(&self) -> serde_json::Value; -} - -impl ToJson for T where T : Object { - fn json(&self) -> serde_json::Value { - let mut map = serde_json::Map::new(); - let mp = &mut map; - - put_str(mp, "id", self.id()); - put_str(mp, "attributedTo", self.attributed_to()); - put_str(mp, "name", self.name()); - put_str(mp, "summary", self.summary()); - put_str(mp, "content", self.content()); - - if let Some(t) = self.full_type() { - map.insert( - "type".to_string(), - serde_json::Value::String(format!("{t}")), - ); - } - - if let Some(published) = self.published() { - map.insert( - "published".to_string(), - serde_json::Value::String(published.to_rfc3339()), - ); - } - - // ... TODO! - - serde_json::Value::Object(map) - } -} - -fn put_str(map: &mut serde_json::Map, k: &str, v: Option<&str>) { - if let Some(v) = v { - map.insert( - k.to_string(), - serde_json::Value::String(v.to_string()), - ); - } -} diff --git a/src/activitystream/types.rs b/src/activitystream/types.rs index 98130f37..fcee7800 100644 --- a/src/activitystream/types.rs +++ b/src/activitystream/types.rs @@ -1,88 +1,228 @@ -// TODO merge these flat maybe? -// but then db could theoretically hold an actor with type "Like" ... idk! -#[derive(Debug, Clone)] -pub enum Type { - Object, - ObjectType(ObjectType), - Link, - Mention, // TODO what about this??? - Activity, - IntransitiveActivity, - ActivityType(ActivityType), - Collection, - OrderedCollection, - CollectionPage, - OrderedCollectionPage, - ActorType(ActorType), -} +#[derive(Debug, thiserror::Error)] +#[error("invalid type value")] +pub struct TypeValueError; -impl std::fmt::Display for Type { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::ObjectType(x) => write!(f, "{:?}", x), - Self::ActivityType(x) => write!(f, "{:?}", x), - Self::ActorType(x) => write!(f, "{:?}", x), - _ => write!(f, "{:?}", self), - } +impl From for sea_orm::sea_query::ValueTypeErr { + fn from(_: TypeValueError) -> Self { + sea_orm::sea_query::ValueTypeErr } } -#[derive(sea_orm::EnumIter, sea_orm::DeriveActiveEnum, PartialEq, Eq, Debug, Clone, Copy)] -#[sea_orm(rs_type = "i32", db_type = "Integer")] -pub enum ActivityType { - Accept = 1, - Add = 2, - Announce = 3, - Arrive = 4, - Block = 5, - Create = 6, - Delete = 7, - Dislike = 8, - Flag = 9, - Follow = 10, - Ignore = 11, - Invite = 12, - Join = 13, - Leave = 14, - Like = 15, - Listen = 16, - Move = 17, - Offer = 18, - Question = 19, - Reject = 20, - Read = 21, - Remove = 22, - TentativeReject = 23, - TentativeAccept = 24, - Travel = 25, - Undo = 26, - Update = 27, - View = 28, +impl From for sea_orm::TryGetError { + fn from(_: TypeValueError) -> Self { + sea_orm::TryGetError::Null("value is not a valid type".into()) + } } -#[derive(sea_orm::EnumIter, sea_orm::DeriveActiveEnum, PartialEq, Eq, Debug, Clone, Copy)] -#[sea_orm(rs_type = "i32", db_type = "Integer")] -pub enum ActorType { - Application = 1, - Group = 2, - Organization = 3, - Person = 4, - Service = 5, +macro_rules! strenum { + ( $(pub enum $enum_name:ident { $($flat:ident),+ $($deep:ident($inner:ident)),*};)+ ) => { + $( + #[derive(PartialEq, Eq, Debug, Clone, Copy)] + pub enum $enum_name { + $($flat,)* + $($deep($inner),)* + } + + impl AsRef for $enum_name { + fn as_ref(&self) -> &str { + match self { + $(Self::$flat => stringify!($flat),)* + $(Self::$deep(x) => x.as_ref(),)* + } + } + } + + impl TryFrom<&str> for $enum_name { + type Error = TypeValueError; + + fn try_from(value:&str) -> Result { + match value { + $(stringify!($flat) => Ok(Self::$flat),)* + _ => { + $( + if let Ok(x) = $inner::try_from(value) { + return Ok(Self::$deep(x)); + } + )* + Err(TypeValueError) + }, + } + } + } + + impl From<$enum_name> for sea_orm::Value { + fn from(value: $enum_name) -> sea_orm::Value { + sea_orm::Value::String(Some(Box::new(value.as_ref().to_string()))) + } + } + + impl sea_orm::sea_query::ValueType for $enum_name { + fn try_from(v: sea_orm::Value) -> Result { + match v { + sea_orm::Value::String(Some(x)) => + Ok(>::try_from(x.as_str())?), + _ => Err(sea_orm::sea_query::ValueTypeErr), + } + } + + fn type_name() -> String { + stringify!($enum_name).to_string() + } + + fn array_type() -> sea_orm::sea_query::ArrayType { + sea_orm::sea_query::ArrayType::String + } + + fn column_type() -> sea_orm::sea_query::ColumnType { + sea_orm::sea_query::ColumnType::String(Some(24)) + } + } + + impl sea_orm::TryGetable for $enum_name { + fn try_get_by(res: &sea_orm::prelude::QueryResult, index: I) -> Result { + let x : String = res.try_get_by(index)?; + Ok(Self::try_from(x.as_str())?) + } + } + )* + }; } -#[derive(sea_orm::EnumIter, sea_orm::DeriveActiveEnum, PartialEq, Eq, Debug, Clone, Copy)] -#[sea_orm(rs_type = "i32", db_type = "Integer")] -pub enum ObjectType { - Article = 1, - Audio = 2, - Document = 3, - Event = 4, - Image = 5, - Note = 6, - Page = 7, - Place = 8, - Profile = 9, - Relationship = 10, - Tombstone = 11, - Video = 12, +strenum! { + pub enum BaseType { + Invalid + + Object(ObjectType), + Link(LinkType) + }; + + pub enum LinkType { + Base, + Mention + }; + + pub enum ObjectType { + Object, + Relationship, + Tombstone + + Activity(ActivityType), + Actor(ActorType), + Collection(CollectionType), + Status(StatusType) + }; + + pub enum ActorType { + Application, + Group, + Organization, + Person, + Object + }; + + pub enum StatusType { + Article, + Event, + Note, + Place, + Profile + + Document(DocumentType) + }; + + pub enum CollectionType { + Collection, + CollectionPage, + OrderedCollection, + OrderedCollectionPage + }; + + pub enum AcceptType { + Accept, + TentativeAccept + }; + + pub enum DocumentType { + Document, + Audio, + Image, + Page, + Video + }; + + pub enum ActivityType { + Activity, + Add, + Announce, + Create, + Delete, + Dislike, + Flag, + Follow, + Join, + Leave, + Like, + Listen, + Move, + Read, + Remove, + Undo, + Update, + View + + IntransitiveActivity(IntransitiveActivityType), + Accept(AcceptType), + Ignore(IgnoreType), + Offer(OfferType), + Reject(RejectType) + }; + + pub enum IntransitiveActivityType { + IntransitiveActivity, + Arrive, + Question, + Travel + }; + + pub enum IgnoreType { + Ignore, + Block + }; + + pub enum OfferType { + Offer, + Invite + }; + + pub enum RejectType { + Reject, + TentativeReject + }; +} + +#[cfg(test)] +mod test { + #[test] + fn assert_flat_types_serialize() { + let x = super::IgnoreType::Block; + assert_eq!("Block", >::as_ref(&x)); + } + + #[test] + fn assert_deep_types_serialize() { + let x = super::StatusType::Document(super::DocumentType::Page); + assert_eq!("Page", >::as_ref(&x)); + } + + #[test] + fn assert_flat_types_deserialize() { + let x = super::ActorType::try_from("Person").expect("could not deserialize"); + assert_eq!(super::ActorType::Person, x); + } + + #[test] + fn assert_deep_types_deserialize() { + let x = super::ActivityType::try_from("Invite").expect("could not deserialize"); + assert_eq!(super::ActivityType::Offer(super::OfferType::Invite), x); + } } diff --git a/src/migrations/m20240316_000001_create_table.rs b/src/migrations/m20240316_000001_create_table.rs index 0c37045b..1dd0b7bc 100644 --- a/src/migrations/m20240316_000001_create_table.rs +++ b/src/migrations/m20240316_000001_create_table.rs @@ -17,7 +17,7 @@ impl MigrationTrait for Migration { .not_null() .primary_key() ) - .col(ColumnDef::new(Users::ActorType).integer().not_null()) + .col(ColumnDef::new(Users::ActorType).string().not_null()) .col(ColumnDef::new(Users::Name).string().not_null()) .to_owned() ) @@ -34,7 +34,7 @@ impl MigrationTrait for Migration { .not_null() .primary_key() ) - .col(ColumnDef::new(Activities::ActivityType).integer().not_null()) + .col(ColumnDef::new(Activities::ActivityType).string().not_null()) .col(ColumnDef::new(Activities::Actor).string().not_null()) .col(ColumnDef::new(Activities::Object).string().null()) .col(ColumnDef::new(Activities::Target).string().null()) @@ -53,7 +53,7 @@ impl MigrationTrait for Migration { .not_null() .primary_key() ) - .col(ColumnDef::new(Objects::ObjectType).integer().not_null()) + .col(ColumnDef::new(Objects::ObjectType).string().not_null()) .col(ColumnDef::new(Objects::AttributedTo).string().null()) .col(ColumnDef::new(Objects::Name).string().null()) .col(ColumnDef::new(Objects::Summary).string().null()) diff --git a/src/model/activity.rs b/src/model/activity.rs index 72207e56..d7753d4e 100644 --- a/src/model/activity.rs +++ b/src/model/activity.rs @@ -1,6 +1,6 @@ use sea_orm::entity::prelude::*; -use crate::activitystream; +use crate::activitystream::{self, ObjectType, BaseType}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "activities")] @@ -28,8 +28,8 @@ impl activitystream::Object for Model { Some(&self.id) } - fn full_type(&self) -> Option { - Some(activitystream::Type::ActivityType(self.activity_type)) + fn full_type(&self) -> Option { + Some(BaseType::Object(ObjectType::Activity(self.activity_type))) } fn published(&self) -> Option> { diff --git a/src/model/mod.rs b/src/model/mod.rs index 4d5cf444..c015798a 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -4,7 +4,7 @@ pub mod activity; #[derive(Debug, Clone, thiserror::Error)] #[error("missing required field: '{0}'")] -pub struct FieldError(&'static str); +pub struct FieldError(pub &'static str); pub async fn faker(db: &sea_orm::DatabaseConnection) -> Result<(), sea_orm::DbErr> { use sea_orm::EntityTrait; @@ -18,7 +18,7 @@ pub async fn faker(db: &sea_orm::DatabaseConnection) -> Result<(), sea_orm::DbEr object::Entity::insert(object::ActiveModel { id: sea_orm::Set("http://localhost:3000/objects/4e28d30b-33c1-4336-918b-6fbe592bdd44".into()), name: sea_orm::Set(None), - object_type: sea_orm::Set(crate::activitystream::types::ObjectType::Note), + object_type: sea_orm::Set(crate::activitystream::types::StatusType::Note), attributed_to: sea_orm::Set(Some("http://localhost:3000/users/root".into())), summary: sea_orm::Set(None), content: sea_orm::Set(Some("Hello world!".into())), diff --git a/src/model/object.rs b/src/model/object.rs index d6c05a8f..cd458896 100644 --- a/src/model/object.rs +++ b/src/model/object.rs @@ -1,6 +1,6 @@ use sea_orm::entity::prelude::*; -use crate::activitystream::{self, types::ObjectType}; +use crate::activitystream::{Object, types::{BaseType, ObjectType, StatusType}}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "objects")] @@ -8,7 +8,7 @@ pub struct Model { #[sea_orm(primary_key)] /// must be full uri!!! maybe not great? pub id: String, - pub object_type: ObjectType, + pub object_type: StatusType, pub attributed_to: Option, pub name: Option, pub summary: Option, @@ -26,8 +26,8 @@ impl crate::activitystream::Object for Model { Some(&self.id) } - fn full_type(&self) -> Option { - Some(crate::activitystream::Type::ObjectType(self.object_type)) + fn full_type(&self) -> Option { + Some(BaseType::Object(ObjectType::Status(self.object_type))) } fn attributed_to (&self) -> Option<&str> { @@ -52,8 +52,8 @@ impl crate::activitystream::Object for Model { } impl Model { - pub fn new(object: &impl activitystream::Object) -> Result { - let Some(activitystream::Type::ObjectType(t)) = object.full_type() else { + pub fn new(object: &impl Object) -> Result { + let Some(BaseType::Object(ObjectType::Status(t))) = object.full_type() else { return Err(super::FieldError("type")); // TODO maybe just wrong? better errors! }; Ok(Model { diff --git a/src/model/user.rs b/src/model/user.rs index 2b8475fe..00a64320 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -24,8 +24,8 @@ impl activitystream::Object for Model { Some(&self.id) } - fn full_type(&self) -> Option { - Some(activitystream::Type::ActorType(self.actor_type)) + fn full_type(&self) -> Option { + Some(activitystream::BaseType::Object(activitystream::ObjectType::Actor(self.actor_type))) } fn name (&self) -> Option<&str> { @@ -35,7 +35,7 @@ impl activitystream::Object for Model { impl Model { pub fn new(object: &impl activitystream::Object) -> Result { - let Some(activitystream::Type::ActorType(t)) = object.full_type() else { + let Some(activitystream::BaseType::Object(activitystream::ObjectType::Actor(t))) = object.full_type() else { return Err(super::FieldError("type")); // TODO maybe just wrong? better errors! }; Ok(Model { diff --git a/src/server.rs b/src/server.rs index 3f2a4f23..6444541f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,11 +1,13 @@ use std::ops::Deref; use std::sync::Arc; -use crate::activitystream::object::ToJson; -use crate::activitystream::{types::ActivityType, Object, Type}; +use crate::activitystream::ObjectType; +use crate::activitystream::ToJson; +use crate::activitystream::Activity; +use crate::activitystream::{types::ActivityType, Object, BaseType, LinkedObject}; use crate::model::{activity, object, user}; use axum::{extract::{Path, State}, http::StatusCode, routing::{get, post}, Json, Router}; -use sea_orm::{DatabaseConnection, EntityTrait}; +use sea_orm::{DatabaseConnection, EntityTrait, IntoActiveModel}; pub async fn serve(db: DatabaseConnection) { // build our application with a single route @@ -25,14 +27,33 @@ pub async fn serve(db: DatabaseConnection) { .unwrap(); } -async fn inbox(State(_db) : State>, Json(object): Json) -> Result, StatusCode> { +async fn inbox(State(db) : State>, Json(object): Json) -> Result, StatusCode> { match object.full_type() { None => { Err(StatusCode::BAD_REQUEST) }, - Some(Type::Activity) => { Err(StatusCode::UNPROCESSABLE_ENTITY) }, - Some(Type::ActivityType(ActivityType::Follow)) => { todo!() }, - Some(Type::ActivityType(ActivityType::Create)) => { todo!() }, - Some(Type::ActivityType(ActivityType::Like)) => { todo!() }, - Some(Type::ActivityType(_x)) => { Err(StatusCode::NOT_IMPLEMENTED) }, + Some(BaseType::Link(_x)) => Err(StatusCode::UNPROCESSABLE_ENTITY), // we could but not yet + Some(BaseType::Object(ObjectType::Activity(ActivityType::Activity))) => Err(StatusCode::UNPROCESSABLE_ENTITY), + Some(BaseType::Object(ObjectType::Activity(ActivityType::Follow))) => { todo!() }, + Some(BaseType::Object(ObjectType::Activity(ActivityType::Like))) => { todo!() }, + Some(BaseType::Object(ObjectType::Activity(ActivityType::Create))) => { + let Ok(activity_entity) = activity::Model::new(&object) else { + return Err(StatusCode::UNPROCESSABLE_ENTITY); + }; + let Some(LinkedObject::Object(obj)) = object.object() else { + // TODO we could process non-embedded activities but im lazy rn + return Err(StatusCode::UNPROCESSABLE_ENTITY); + }; + let Ok(obj_entity) = object::Model::new(&obj) else { + return Err(StatusCode::UNPROCESSABLE_ENTITY); + }; + object::Entity::insert(obj_entity.into_active_model()) + .exec(db.deref()) + .await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + activity::Entity::insert(activity_entity.into_active_model()) + .exec(db.deref()) + .await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(Json(serde_json::Value::Null)) // TODO hmmmmmmmmmmm not the best value to return.... + }, + Some(BaseType::Object(ObjectType::Activity(_x))) => { Err(StatusCode::NOT_IMPLEMENTED) }, Some(_x) => { Err(StatusCode::UNPROCESSABLE_ENTITY) } } }