From 85c9b363f60acee2612eccc4b9c3ee2ec4ca7cda Mon Sep 17 00:00:00 2001 From: alemi Date: Sat, 16 Mar 2024 05:45:58 +0100 Subject: [PATCH] feat: db model for activity and object + routes --- src/activitystream/activity.rs | 11 +- src/activitystream/object.rs | 35 +++++- src/activitystream/types.rs | 110 +++++++++++------- src/main.rs | 9 +- .../m20240316_000001_create_table.rs | 82 ++++++++++++- src/model/activity.rs | 56 +++++++++ src/model/like.rs | 0 src/model/mod.rs | 36 +++++- src/model/object.rs | 52 +++++++++ src/model/relation.rs | 0 src/model/status.rs | 0 src/model/user.rs | 15 ++- src/model/vote.rs | 0 src/server.rs | 32 ++++- 14 files changed, 365 insertions(+), 73 deletions(-) delete mode 100644 src/model/like.rs create mode 100644 src/model/object.rs delete mode 100644 src/model/relation.rs delete mode 100644 src/model/status.rs delete mode 100644 src/model/vote.rs diff --git a/src/activitystream/activity.rs b/src/activitystream/activity.rs index 10586e6..4b58039 100644 --- a/src/activitystream/activity.rs +++ b/src/activitystream/activity.rs @@ -1,4 +1,11 @@ pub trait Activity : super::Object { - fn actor(&self) -> Option<&super::ObjectOrLink> { None } - fn object(&self) -> Option<&super::ObjectOrLink> { None } + fn activity_type(&self) -> Option { None } + + fn actor_id(&self) -> Option<&str> { None } + fn actor(&self) -> Option<&impl super::Object> { None::<&()> } + + fn object_id(&self) -> Option<&str> { None } + fn object(&self) -> Option<&impl super::Object> { None::<&()> } + + fn target(&self) -> Option<&str> { None } } diff --git a/src/activitystream/object.rs b/src/activitystream/object.rs index accddcd..dd151cd 100644 --- a/src/activitystream/object.rs +++ b/src/activitystream/object.rs @@ -27,7 +27,7 @@ pub trait Link { pub trait Object { fn id(&self) -> Option<&str> { None } - fn object_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 } @@ -41,7 +41,7 @@ pub trait Object { fn in_reply_to (&self) -> Option<&str> { None } fn location (&self) -> Option<&str> { None } fn preview (&self) -> Option<&str> { None } - fn published (&self) -> Option<&str> { None } + fn published (&self) -> Option> { None } fn replies (&self) -> Option<&str> { None } fn start_time (&self) -> Option<&str> { None } fn summary (&self) -> Option<&str> { None } @@ -66,7 +66,7 @@ impl Object for serde_json::Value { self.get("id")?.as_str() } - fn object_type(&self) -> Option { + fn full_type(&self) -> Option { todo!() } @@ -88,11 +88,25 @@ pub trait ToJson : Object { impl ToJson for T where T : Object { fn json(&self) -> serde_json::Value { let mut map = serde_json::Map::new(); + let mp = &mut map; - if let Some(id) = self.id() { + 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( - "id".to_string(), - serde_json::Value::String(id.to_string()) + "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()), ); } @@ -101,3 +115,12 @@ impl ToJson for T where T : Object { 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 ebcab58..98130f3 100644 --- a/src/activitystream/types.rs +++ b/src/activitystream/types.rs @@ -1,3 +1,6 @@ +// 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), @@ -13,56 +16,73 @@ pub enum Type { ActorType(ActorType), } +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), + } + } +} + +#[derive(sea_orm::EnumIter, sea_orm::DeriveActiveEnum, PartialEq, Eq, Debug, Clone, Copy)] +#[sea_orm(rs_type = "i32", db_type = "Integer")] pub enum ActivityType { - Accept, - Add, - Announce, - Arrive, - Block, - Create, - Delete, - Dislike, - Flag, - Follow, - Ignore, - Invite, - Join, - Leave, - Like, - Listen, - Move, - Offer, - Question, - Reject, - Read, - Remove, - TentativeReject, - TentativeAccept, - Travel, - Undo, - Update, - View, + 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, } +#[derive(sea_orm::EnumIter, sea_orm::DeriveActiveEnum, PartialEq, Eq, Debug, Clone, Copy)] +#[sea_orm(rs_type = "i32", db_type = "Integer")] pub enum ActorType { - Application, - Group, - Organization, - Person, - Service, + Application = 1, + Group = 2, + Organization = 3, + Person = 4, + Service = 5, } +#[derive(sea_orm::EnumIter, sea_orm::DeriveActiveEnum, PartialEq, Eq, Debug, Clone, Copy)] +#[sea_orm(rs_type = "i32", db_type = "Integer")] pub enum ObjectType { - Article, - Audio, - Document, - Event, - Image, - Note, - Page, - Place, - Profile, - Relationship, - Tombstone, - Video, + Article = 1, + Audio = 2, + Document = 3, + Event = 4, + Image = 5, + Note = 6, + Page = 7, + Place = 8, + Profile = 9, + Relationship = 10, + Tombstone = 11, + Video = 12, } diff --git a/src/main.rs b/src/main.rs index e84c9ee..09942a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,10 @@ pub mod model; pub mod migrations; pub mod activitystream; -pub mod activitypub; pub mod server; use clap::{Parser, Subcommand}; -use sea_orm::Database; +use sea_orm::{ConnectOptions, Database}; use sea_orm_migration::MigratorTrait; #[derive(Parser)] @@ -43,7 +42,11 @@ async fn main() { .with_max_level(if args.debug { tracing::Level::DEBUG } else { tracing::Level::INFO }) .init(); - let db = Database::connect(&args.database) + let mut opts = ConnectOptions::new(&args.database); + opts + .max_connections(1); + + let db = Database::connect(opts) .await.expect("error connecting to db"); match args.command { diff --git a/src/migrations/m20240316_000001_create_table.rs b/src/migrations/m20240316_000001_create_table.rs index ba286fd..0c37045 100644 --- a/src/migrations/m20240316_000001_create_table.rs +++ b/src/migrations/m20240316_000001_create_table.rs @@ -6,8 +6,8 @@ pub struct Migration; #[async_trait::async_trait] impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager. - create_table( + manager + .create_table( Table::create() .table(Users::Table) .if_not_exists() @@ -17,17 +17,65 @@ impl MigrationTrait for Migration { .not_null() .primary_key() ) - .col(ColumnDef::new(Users::Name).string().null()) + .col(ColumnDef::new(Users::ActorType).integer().not_null()) + .col(ColumnDef::new(Users::Name).string().not_null()) .to_owned() ) .await?; + manager + .create_table( + Table::create() + .table(Activities::Table) + .if_not_exists() + .col( + ColumnDef::new(Activities::Id) + .string() + .not_null() + .primary_key() + ) + .col(ColumnDef::new(Activities::ActivityType).integer().not_null()) + .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()) + .to_owned() + ).await?; + + manager + .create_table( + Table::create() + .table(Objects::Table) + .if_not_exists() + .col( + ColumnDef::new(Objects::Id) + .string() + .not_null() + .primary_key() + ) + .col(ColumnDef::new(Objects::ObjectType).integer().not_null()) + .col(ColumnDef::new(Objects::AttributedTo).string().null()) + .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()) + .to_owned() + ).await?; + Ok(()) } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager. - drop_table(Table::drop().table(Users::Table).to_owned()) + manager + .drop_table(Table::drop().table(Users::Table).to_owned()) + .await?; + + manager + .drop_table(Table::drop().table(Activities::Table).to_owned()) + .await?; + + manager + .drop_table(Table::drop().table(Objects::Table).to_owned()) .await?; Ok(()) @@ -38,5 +86,29 @@ impl MigrationTrait for Migration { enum Users { Table, Id, + ActorType, Name, } + +#[derive(DeriveIden)] +enum Activities { + Table, + Id, + ActivityType, + Actor, + Object, + Target, + Published +} + +#[derive(DeriveIden)] +enum Objects { + Table, + Id, + ObjectType, + Name, + Summary, + AttributedTo, + Content, + Published, +} diff --git a/src/model/activity.rs b/src/model/activity.rs index e69de29..44f437d 100644 --- a/src/model/activity.rs +++ b/src/model/activity.rs @@ -0,0 +1,56 @@ +use sea_orm::entity::prelude::*; + +use crate::activitystream; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "activities")] +pub struct Model { + #[sea_orm(primary_key)] + /// must be https://instance.org/users/:user , even if local! TODO bad design... + pub id: String, + + pub activity_type: activitystream::types::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 published: ChronoDateTimeUtc, + + // TODO: origin, result, instrument +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} + +impl activitystream::Object for Model { + fn id(&self) -> Option<&str> { + Some(&self.id) + } + + fn full_type(&self) -> Option { + Some(activitystream::Type::ActivityType(self.activity_type)) + } + + fn published(&self) -> Option> { + Some(self.published) + } +} + +impl activitystream::Activity for Model { + fn activity_type(&self) -> Option { + Some(self.activity_type) + } + + fn actor_id(&self) -> Option<&str> { + Some(&self.actor) + } + + fn object_id(&self) -> Option<&str> { + self.object.as_deref() + } + + fn target(&self) -> Option<&str> { + self.target.as_deref() + } +} diff --git a/src/model/like.rs b/src/model/like.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/model/mod.rs b/src/model/mod.rs index 464b35d..6e07293 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,6 +1,34 @@ - pub mod user; -pub mod status; +pub mod object; pub mod activity; -pub mod like; -pub mod relation; + +pub async fn faker(db: &sea_orm::DatabaseConnection) -> Result<(), sea_orm::DbErr> { + use sea_orm::EntityTrait; + + user::Entity::insert(user::ActiveModel { + id: sea_orm::Set("http://localhost:3000/users/root".into()), + name: sea_orm::Set("root".into()), + actor_type: sea_orm::Set(super::activitystream::types::ActorType::Person), + }).exec(db).await?; + + 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), + 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())), + published: sea_orm::Set(chrono::Utc::now()), + }).exec(db).await?; + + activity::Entity::insert(activity::ActiveModel { + id: sea_orm::Set("http://localhost:3000/activities/ebac57e1-9828-438c-be34-a44a52de7641".into()), + activity_type: sea_orm::Set(crate::activitystream::types::ActivityType::Create), + actor: sea_orm::Set("http://localhost:3000/users/root".into()), + object: sea_orm::Set(Some("http://localhost:3000/obkects/4e28d30b-33c1-4336-918b-6fbe592bdd44".into())), + target: sea_orm::Set(None), + published: sea_orm::Set(chrono::Utc::now()), + }).exec(db).await?; + + Ok(()) +} diff --git a/src/model/object.rs b/src/model/object.rs new file mode 100644 index 0000000..49abbed --- /dev/null +++ b/src/model/object.rs @@ -0,0 +1,52 @@ +use sea_orm::entity::prelude::*; + +use crate::activitystream::types::ObjectType; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "objects")] +pub struct Model { + #[sea_orm(primary_key)] + /// must be full uri!!! maybe not great? + pub id: String, + pub object_type: ObjectType, + pub attributed_to: Option, + pub name: Option, + pub summary: Option, + pub content: Option, + pub published: ChronoDateTimeUtc, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} + +impl crate::activitystream::Object for Model { + fn id(&self) -> Option<&str> { + Some(&self.id) + } + + fn full_type(&self) -> Option { + Some(crate::activitystream::Type::ObjectType(self.object_type)) + } + + fn attributed_to (&self) -> Option<&str> { + self.attributed_to.as_deref() + } + + fn name (&self) -> Option<&str> { + self.name.as_deref() + } + + fn summary (&self) -> Option<&str> { + self.summary.as_deref() + } + + fn content(&self) -> Option<&str> { + self.content.as_deref() + } + + fn published (&self) -> Option> { + Some(self.published) + } +} diff --git a/src/model/relation.rs b/src/model/relation.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/model/status.rs b/src/model/status.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/model/user.rs b/src/model/user.rs index 341695c..755fef3 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -1,13 +1,18 @@ use sea_orm::entity::prelude::*; +use crate::activitystream::{self, types::ActorType}; + #[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... pub id: String, - pub name: Option, + + pub actor_type: ActorType, + + pub name: String, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] @@ -15,12 +20,16 @@ pub enum Relation {} impl ActiveModelBehavior for ActiveModel {} -impl crate::activitystream::Object for Model { +impl activitystream::Object for Model { fn id(&self) -> Option<&str> { Some(&self.id) } + fn full_type(&self) -> Option { + Some(activitystream::Type::ActorType(self.actor_type)) + } + fn name (&self) -> Option<&str> { - self.name.as_deref() + Some(&self.name) } } diff --git a/src/model/vote.rs b/src/model/vote.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/server.rs b/src/server.rs index 138ccb4..3f2a4f2 100644 --- a/src/server.rs +++ b/src/server.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use crate::activitystream::object::ToJson; use crate::activitystream::{types::ActivityType, Object, Type}; -use crate::model::user; +use crate::model::{activity, object, user}; use axum::{extract::{Path, State}, http::StatusCode, routing::{get, post}, Json, Router}; use sea_orm::{DatabaseConnection, EntityTrait}; @@ -13,6 +13,7 @@ pub async fn serve(db: DatabaseConnection) { .route("/inbox", post(inbox)) .route("/outbox", get(|| async { todo!() })) .route("/users/:id", get(user)) + .route("/activities/:id", get(activity)) .route("/objects/:id", get(object)) .with_state(Arc::new(db)); @@ -25,7 +26,7 @@ pub async fn serve(db: DatabaseConnection) { } async fn inbox(State(_db) : State>, Json(object): Json) -> Result, StatusCode> { - match object.object_type() { + match object.full_type() { None => { Err(StatusCode::BAD_REQUEST) }, Some(Type::Activity) => { Err(StatusCode::UNPROCESSABLE_ENTITY) }, Some(Type::ActivityType(ActivityType::Follow)) => { todo!() }, @@ -37,7 +38,8 @@ async fn inbox(State(_db) : State>, Json(object): Json>, Path(id): Path) -> Result, StatusCode> { - match user::Entity::find_by_id(id).one(db.deref()).await { + let uri = format!("http://localhost:3000/users/{id}"); + match user::Entity::find_by_id(uri).one(db.deref()).await { Ok(Some(user)) => Ok(Json(user.json())), Ok(None) => Err(StatusCode::NOT_FOUND), Err(e) => { @@ -47,6 +49,26 @@ async fn user(State(db) : State>, Path(id): Path } } -async fn object(State(_db) : State>, Path(_id): Path) -> Result, StatusCode> { - todo!() +async fn activity(State(db) : State>, Path(id): Path) -> Result, StatusCode> { + let uri = format!("http://localhost:3000/activities/{id}"); + match activity::Entity::find_by_id(uri).one(db.deref()).await { + Ok(Some(activity)) => Ok(Json(activity.json())), + Ok(None) => Err(StatusCode::NOT_FOUND), + Err(e) => { + tracing::error!("error querying for activity: {e}"); + Err(StatusCode::INTERNAL_SERVER_ERROR) + }, + } +} + +async fn object(State(db) : State>, Path(id): Path) -> Result, StatusCode> { + let uri = format!("http://localhost:3000/objects/{id}"); + match object::Entity::find_by_id(uri).one(db.deref()).await { + Ok(Some(object)) => Ok(Json(object.json())), + Ok(None) => Err(StatusCode::NOT_FOUND), + Err(e) => { + tracing::error!("error querying for object: {e}"); + Err(StatusCode::INTERNAL_SERVER_ERROR) + }, + } }