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 {
fn actor(&self) -> Option<&super::ObjectOrLink> { None }
fn object(&self) -> Option<&super::ObjectOrLink> { None }
fn activity_type(&self) -> Option<super::types::ActivityType> { 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 {
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 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<chrono::DateTime<chrono::Utc>> { 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<super::Type> {
fn full_type(&self) -> Option<super::Type> {
todo!()
}
@ -88,11 +88,25 @@ pub trait ToJson : Object {
impl<T> 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<T> ToJson for T where T : Object {
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 {
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,
}

View file

@ -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 {

View file

@ -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,
}

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 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(())
}

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 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<String>,
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<activitystream::Type> {
Some(activitystream::Type::ActorType(self.actor_type))
}
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::{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<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) },
Some(Type::Activity) => { Err(StatusCode::UNPROCESSABLE_ENTITY) },
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> {
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<Arc<DatabaseConnection>>, Path(id): Path<String>
}
}
async fn object(State(_db) : State<Arc<DatabaseConnection>>, Path(_id): Path<String>) -> Result<Json<serde_json::Value>, StatusCode> {
todo!()
async fn activity(State(db) : State<Arc<DatabaseConnection>>, Path(id): Path<String>) -> Result<Json<serde_json::Value>, 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<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)
},
}
}