feat: db model for activity and object + routes

This commit is contained in:
əlemi 2024-03-16 05:45:58 +01:00
parent 40cee0fc87
commit 85c9b363f6
Signed by: alemi
GPG key ID: A4895B84D311642C
14 changed files with 365 additions and 73 deletions

View file

@ -1,4 +1,11 @@
pub trait Activity : super::Object { pub trait Activity : super::Object {
fn actor(&self) -> Option<&super::ObjectOrLink> { None } fn activity_type(&self) -> Option<super::types::ActivityType> { None }
fn object(&self) -> Option<&super::ObjectOrLink> { 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 }
} }

View file

@ -27,7 +27,7 @@ pub trait Link {
pub trait Object { pub trait Object {
fn id(&self) -> Option<&str> { None } fn id(&self) -> Option<&str> { None }
fn object_type(&self) -> Option<super::Type> { None } fn full_type(&self) -> Option<super::Type> { None }
fn attachment (&self) -> Option<&str> { None } fn attachment (&self) -> Option<&str> { None }
fn attributed_to (&self) -> Option<&str> { None } fn attributed_to (&self) -> Option<&str> { None }
fn audience (&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 in_reply_to (&self) -> Option<&str> { None }
fn location (&self) -> Option<&str> { None } fn location (&self) -> Option<&str> { None }
fn preview (&self) -> Option<&str> { None } fn preview (&self) -> Option<&str> { None }
fn published (&self) -> Option<&str> { None } fn published (&self) -> Option<chrono::DateTime<chrono::Utc>> { None }
fn replies (&self) -> Option<&str> { None } fn replies (&self) -> Option<&str> { None }
fn start_time (&self) -> Option<&str> { None } fn start_time (&self) -> Option<&str> { None }
fn summary (&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() self.get("id")?.as_str()
} }
fn object_type(&self) -> Option<super::Type> { fn full_type(&self) -> Option<super::Type> {
todo!() todo!()
} }
@ -88,11 +88,25 @@ pub trait ToJson : Object {
impl<T> ToJson for T where T : Object { impl<T> ToJson for T where T : Object {
fn json(&self) -> serde_json::Value { fn json(&self) -> serde_json::Value {
let mut map = serde_json::Map::new(); 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( map.insert(
"id".to_string(), "type".to_string(),
serde_json::Value::String(id.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<T> ToJson for T where T : Object {
serde_json::Value::Object(map) serde_json::Value::Object(map)
} }
} }
fn put_str(map: &mut serde_json::Map<String, serde_json::Value>, k: &str, v: Option<&str>) {
if let Some(v) = v {
map.insert(
k.to_string(),
serde_json::Value::String(v.to_string()),
);
}
}

View file

@ -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 { pub enum Type {
Object, Object,
ObjectType(ObjectType), ObjectType(ObjectType),
@ -13,56 +16,73 @@ pub enum Type {
ActorType(ActorType), 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 { pub enum ActivityType {
Accept, Accept = 1,
Add, Add = 2,
Announce, Announce = 3,
Arrive, Arrive = 4,
Block, Block = 5,
Create, Create = 6,
Delete, Delete = 7,
Dislike, Dislike = 8,
Flag, Flag = 9,
Follow, Follow = 10,
Ignore, Ignore = 11,
Invite, Invite = 12,
Join, Join = 13,
Leave, Leave = 14,
Like, Like = 15,
Listen, Listen = 16,
Move, Move = 17,
Offer, Offer = 18,
Question, Question = 19,
Reject, Reject = 20,
Read, Read = 21,
Remove, Remove = 22,
TentativeReject, TentativeReject = 23,
TentativeAccept, TentativeAccept = 24,
Travel, Travel = 25,
Undo, Undo = 26,
Update, Update = 27,
View, 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 { pub enum ActorType {
Application, Application = 1,
Group, Group = 2,
Organization, Organization = 3,
Person, Person = 4,
Service, 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 { pub enum ObjectType {
Article, Article = 1,
Audio, Audio = 2,
Document, Document = 3,
Event, Event = 4,
Image, Image = 5,
Note, Note = 6,
Page, Page = 7,
Place, Place = 8,
Profile, Profile = 9,
Relationship, Relationship = 10,
Tombstone, Tombstone = 11,
Video, Video = 12,
} }

View file

@ -1,11 +1,10 @@
pub mod model; pub mod model;
pub mod migrations; pub mod migrations;
pub mod activitystream; pub mod activitystream;
pub mod activitypub;
pub mod server; pub mod server;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use sea_orm::Database; use sea_orm::{ConnectOptions, Database};
use sea_orm_migration::MigratorTrait; use sea_orm_migration::MigratorTrait;
#[derive(Parser)] #[derive(Parser)]
@ -43,7 +42,11 @@ async fn main() {
.with_max_level(if args.debug { tracing::Level::DEBUG } else { tracing::Level::INFO }) .with_max_level(if args.debug { tracing::Level::DEBUG } else { tracing::Level::INFO })
.init(); .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"); .await.expect("error connecting to db");
match args.command { match args.command {

View file

@ -6,8 +6,8 @@ pub struct Migration;
#[async_trait::async_trait] #[async_trait::async_trait]
impl MigrationTrait for Migration { impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager. manager
create_table( .create_table(
Table::create() Table::create()
.table(Users::Table) .table(Users::Table)
.if_not_exists() .if_not_exists()
@ -17,17 +17,65 @@ impl MigrationTrait for Migration {
.not_null() .not_null()
.primary_key() .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() .to_owned()
) )
.await?; .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(()) Ok(())
} }
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager. manager
drop_table(Table::drop().table(Users::Table).to_owned()) .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?; .await?;
Ok(()) Ok(())
@ -38,5 +86,29 @@ impl MigrationTrait for Migration {
enum Users { enum Users {
Table, Table,
Id, Id,
ActorType,
Name, Name,
} }
#[derive(DeriveIden)]
enum Activities {
Table,
Id,
ActivityType,
Actor,
Object,
Target,
Published
}
#[derive(DeriveIden)]
enum Objects {
Table,
Id,
ObjectType,
Name,
Summary,
AttributedTo,
Content,
Published,
}

View file

@ -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<String>, // TODO relates to NOTES maybe????? maybe other tables??????
pub target: Option<String>, // 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<activitystream::Type> {
Some(activitystream::Type::ActivityType(self.activity_type))
}
fn published(&self) -> Option<chrono::DateTime<chrono::Utc>> {
Some(self.published)
}
}
impl activitystream::Activity for Model {
fn activity_type(&self) -> Option<activitystream::types::ActivityType> {
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()
}
}

View file

View file

@ -1,6 +1,34 @@
pub mod user; pub mod user;
pub mod status; pub mod object;
pub mod activity; 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(())
}

52
src/model/object.rs Normal file
View file

@ -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<String>,
pub name: Option<String>,
pub summary: Option<String>,
pub content: Option<String>,
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<crate::activitystream::Type> {
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<chrono::DateTime<chrono::Utc>> {
Some(self.published)
}
}

View file

View file

View file

@ -1,13 +1,18 @@
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use crate::activitystream::{self, types::ActorType};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "users")] #[sea_orm(table_name = "users")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
/// must be user@instance.org, even if local! TODO bad design... /// must be user@instance.org, even if local! TODO bad design...
pub id: String, pub id: String,
pub name: Option<String>,
pub actor_type: ActorType,
pub name: String,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@ -15,12 +20,16 @@ pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}
impl crate::activitystream::Object for Model { impl activitystream::Object for Model {
fn id(&self) -> Option<&str> { fn id(&self) -> Option<&str> {
Some(&self.id) Some(&self.id)
} }
fn full_type(&self) -> Option<activitystream::Type> {
Some(activitystream::Type::ActorType(self.actor_type))
}
fn name (&self) -> Option<&str> { fn name (&self) -> Option<&str> {
self.name.as_deref() Some(&self.name)
} }
} }

View file

View file

@ -3,7 +3,7 @@ use std::sync::Arc;
use crate::activitystream::object::ToJson; use crate::activitystream::object::ToJson;
use crate::activitystream::{types::ActivityType, Object, Type}; 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 axum::{extract::{Path, State}, http::StatusCode, routing::{get, post}, Json, Router};
use sea_orm::{DatabaseConnection, EntityTrait}; use sea_orm::{DatabaseConnection, EntityTrait};
@ -13,6 +13,7 @@ pub async fn serve(db: DatabaseConnection) {
.route("/inbox", post(inbox)) .route("/inbox", post(inbox))
.route("/outbox", get(|| async { todo!() })) .route("/outbox", get(|| async { todo!() }))
.route("/users/:id", get(user)) .route("/users/:id", get(user))
.route("/activities/:id", get(activity))
.route("/objects/:id", get(object)) .route("/objects/:id", get(object))
.with_state(Arc::new(db)); .with_state(Arc::new(db));
@ -25,7 +26,7 @@ pub async fn serve(db: DatabaseConnection) {
} }
async fn inbox(State(_db) : State<Arc<DatabaseConnection>>, Json(object): Json<serde_json::Value>) -> Result<Json<serde_json::Value>, StatusCode> { async fn inbox(State(_db) : State<Arc<DatabaseConnection>>, Json(object): Json<serde_json::Value>) -> Result<Json<serde_json::Value>, StatusCode> {
match object.object_type() { match object.full_type() {
None => { Err(StatusCode::BAD_REQUEST) }, None => { Err(StatusCode::BAD_REQUEST) },
Some(Type::Activity) => { Err(StatusCode::UNPROCESSABLE_ENTITY) }, Some(Type::Activity) => { Err(StatusCode::UNPROCESSABLE_ENTITY) },
Some(Type::ActivityType(ActivityType::Follow)) => { todo!() }, Some(Type::ActivityType(ActivityType::Follow)) => { todo!() },
@ -37,7 +38,8 @@ async fn inbox(State(_db) : State<Arc<DatabaseConnection>>, Json(object): Json<s
} }
async fn user(State(db) : State<Arc<DatabaseConnection>>, Path(id): Path<String>) -> Result<Json<serde_json::Value>, StatusCode> { async fn user(State(db) : State<Arc<DatabaseConnection>>, Path(id): Path<String>) -> Result<Json<serde_json::Value>, 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(Some(user)) => Ok(Json(user.json())),
Ok(None) => Err(StatusCode::NOT_FOUND), Ok(None) => Err(StatusCode::NOT_FOUND),
Err(e) => { Err(e) => {
@ -47,6 +49,26 @@ async fn user(State(db) : State<Arc<DatabaseConnection>>, Path(id): Path<String>
} }
} }
async fn object(State(_db) : State<Arc<DatabaseConnection>>, Path(_id): Path<String>) -> Result<Json<serde_json::Value>, StatusCode> { async fn activity(State(db) : State<Arc<DatabaseConnection>>, Path(id): Path<String>) -> Result<Json<serde_json::Value>, StatusCode> {
todo!() 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<Arc<DatabaseConnection>>, Path(id): Path<String>) -> Result<Json<serde_json::Value>, 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)
},
}
} }