diff --git a/Cargo.lock b/Cargo.lock index 8eb4999..a9d9ad2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2158,6 +2158,7 @@ dependencies = [ "rocket", "rocket_db_pools", "sqlx", + "url", "uuid", ] diff --git a/assets/pfp.png b/assets/pfp.png new file mode 100644 index 0000000..9e0a6c6 Binary files /dev/null and b/assets/pfp.png differ diff --git a/ferri-main/src/ap/mod.rs b/ferri-main/src/ap/mod.rs index a4f8436..09d20cb 100644 --- a/ferri-main/src/ap/mod.rs +++ b/ferri-main/src/ap/mod.rs @@ -2,6 +2,7 @@ use chrono::{DateTime, Local}; use serde::{Deserialize, Serialize}; use sqlx::Sqlite; +use std::fmt::Debug; pub mod http; #[derive(Debug, Clone)] @@ -11,6 +12,12 @@ pub struct Actor { outbox: String, } +impl Actor { + pub fn from_raw(id: String, inbox: String, outbox: String) -> Self { + Self { id, inbox, outbox } + } +} + #[derive(Debug, Clone)] pub struct User { id: String, @@ -100,35 +107,74 @@ impl User { #[derive(Debug, Clone)] pub enum ActivityType { Follow, + Accept, + Create, + Unknown } impl ActivityType { fn to_raw(self) -> String { match self { ActivityType::Follow => "Follow".to_string(), + ActivityType::Accept => "Accept".to_string(), + ActivityType::Create => "Create".to_string(), + ActivityType::Unknown => "FIXME".to_string() } } } #[derive(Debug, Clone)] -pub struct Activity { +pub struct Activity { pub id: String, pub ty: ActivityType, - pub object: String, + pub object: T, pub published: DateTime, + pub to: Vec, + pub cc: Vec, +} + +impl Default for Activity { + fn default() -> Self { + Self { + id: Default::default(), + ty: ActivityType::Unknown, + object: Default::default(), + published: Local::now(), + to: Default::default(), + cc: Default::default(), + } + } } pub type KeyId = String; #[derive(Debug, Clone)] -pub struct OutgoingActivity { +pub struct OutgoingActivity { pub signed_by: KeyId, - pub req: Activity, + pub req: Activity, pub to: Actor, } +impl OutgoingActivity { + pub async fn save(&self, conn: impl sqlx::Executor<'_, Database = Sqlite>) { + let ty = self.req.ty.clone().to_raw(); + sqlx::query!( + r#" + INSERT INTO activity (id, ty, actor_id) + VALUES (?1, ?2, ?3) + "#, + self.req.id, + ty, + self.to.id + ) + .execute(conn) + .await + .unwrap(); + } +} + #[derive(Serialize, Deserialize, Debug)] -struct RawActivity { +struct RawActivity { #[serde(rename = "@context")] #[serde(skip_deserializing)] context: String, @@ -138,7 +184,7 @@ struct RawActivity { ty: String, actor: String, - object: String, + object: T, published: String, } @@ -153,7 +199,7 @@ impl<'a> Outbox<'a> { &self.user } - pub async fn post(&self, activity: OutgoingActivity) { + pub async fn post(&self, activity: OutgoingActivity) { dbg!(&activity); let raw = RawActivity { context: "https://www.w3.org/ns/activitystreams".to_string(), diff --git a/ferri-server/Cargo.toml b/ferri-server/Cargo.toml index 36574fa..5ff5b60 100644 --- a/ferri-server/Cargo.toml +++ b/ferri-server/Cargo.toml @@ -10,4 +10,6 @@ rocket_db_pools = { version = "0.2.0", features = ["sqlx_sqlite"] } reqwest = { workspace = true } sqlx = { workspace = true } uuid = { workspace = true } -chrono = { workspace = true } \ No newline at end of file +chrono = { workspace = true } + +url = "2.5.4" \ No newline at end of file diff --git a/ferri-server/src/endpoints/api/mod.rs b/ferri-server/src/endpoints/api/mod.rs index c79b726..ecc6713 100644 --- a/ferri-server/src/endpoints/api/mod.rs +++ b/ferri-server/src/endpoints/api/mod.rs @@ -2,4 +2,5 @@ pub mod user; pub mod apps; pub mod instance; pub mod status; -pub mod preferences; \ No newline at end of file +pub mod preferences; +pub mod timeline; \ No newline at end of file diff --git a/ferri-server/src/endpoints/api/status.rs b/ferri-server/src/endpoints/api/status.rs index 55347ba..507d82d 100644 --- a/ferri-server/src/endpoints/api/status.rs +++ b/ferri-server/src/endpoints/api/status.rs @@ -1,14 +1,15 @@ +use chrono::Local; +use main::ap::{self, http::HttpClient}; use rocket::{ - FromForm, + FromForm, State, form::Form, post, serde::{Deserialize, Serialize}, }; use rocket_db_pools::Connection; use uuid::Uuid; -use main::ap; -use crate::{AuthenticatedUser, Db}; +use crate::{AuthenticatedUser, Db, types::content}; #[derive(Serialize, Deserialize, Debug, FromForm)] #[serde(crate = "rocket::serde")] pub struct Status { @@ -16,25 +17,88 @@ pub struct Status { } #[post("/statuses", data = "")] -pub async fn new_status(mut db: Connection, status: Form, user: AuthenticatedUser) { +pub async fn new_status( + mut db: Connection, + http: &State, + status: Form, + user: AuthenticatedUser, +) { let user = ap::User::from_actor_id(&user.actor_id, &mut **db).await; + let outbox = ap::Outbox::for_user(user.clone(), http); + let post_id = Uuid::new_v4(); - let uri = format!("https://ferri.amy.mov/users/{}/posts/{}", user.username(), post_id); + + let uri = format!( + "https://ferri.amy.mov/users/{}/posts/{}", + user.username(), + post_id + ); let id = user.id(); + let now = Local::now().to_rfc3339(); let post = sqlx::query!( r#" - INSERT INTO post (id, user_id, content) - VALUES (?1, ?2, ?3) + INSERT INTO post (id, user_id, content, created_at) + VALUES (?1, ?2, ?3, ?4) RETURNING * "#, uri, id, - status.status + status.status, + now ) .fetch_one(&mut **db) .await .unwrap(); - dbg!(user, status, post); + let actors = sqlx::query!("SELECT * FROM actor") + .fetch_all(&mut **db) + .await + .unwrap(); + + for record in actors { + // Don't send to ourselves + if &record.id == user.actor_id() { + continue + } + + let create_id = format!("https://ferri.amy.mov/activities/{}", Uuid::new_v4()); + + let activity = ap::Activity { + id: create_id, + ty: ap::ActivityType::Create, + object: content::Post { + context: "https://www.w3.org/ns/activitystreams".to_string(), + id: uri.clone(), + content: status.status.clone(), + ty: "Note".to_string(), + ts: Local::now().to_rfc3339(), + to: vec![format!( + "https://ferri.amy.mov/users/{}/followers", + user.username() + )], + cc: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()], + }, + to: vec![format!( + "https://ferri.amy.mov/users/{}/followers", + user.username() + )], + cc: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()], + ..Default::default() + }; + + let actor = ap::Actor::from_raw( + record.id.clone(), + record.inbox.clone(), + record.outbox.clone(), + ); + let req = ap::OutgoingActivity { + req: activity, + signed_by: format!("https://ferri.amy.mov/users/{}#main-key", user.username()), + to: actor, + }; + + req.save(&mut **db).await; + outbox.post(req).await; + } } diff --git a/ferri-server/src/endpoints/api/timeline.rs b/ferri-server/src/endpoints/api/timeline.rs new file mode 100644 index 0000000..c120dc1 --- /dev/null +++ b/ferri-server/src/endpoints/api/timeline.rs @@ -0,0 +1,91 @@ +use crate::{Db, endpoints::api::user::CredentialAcount}; +use rocket::{ + get, + serde::{Deserialize, Serialize, json::Json}, +}; +use rocket_db_pools::Connection; + +pub type TimelineAccount = CredentialAcount; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct TimelineStatus { + id: String, + created_at: String, + in_reply_to_id: Option, + in_reply_to_account_id: Option, + content: String, + visibility: String, + spoiler_text: String, + sensitive: bool, + uri: String, + url: String, + replies_count: i64, + reblogs_count: i64, + favourites_count: i64, + favourited: bool, + reblogged: bool, + muted: bool, + bookmarked: bool, + media_attachments: Vec<()>, + account: TimelineAccount, +} + +#[get("/timelines/home?")] +pub async fn home(mut db: Connection, limit: i64) -> Json> { + let posts = sqlx::query!( + r#" + SELECT p.id as "post_id", u.id as "user_id", p.content, u.username, u.display_name, u.actor_id FROM post p + INNER JOIN user u on p.user_id = u.id + "# + ) + .fetch_all(&mut **db) + .await + .unwrap(); + + let mut out = Vec::::new(); + for record in posts { + out.push(TimelineStatus { + id: record.post_id.clone(), + created_at: "2025-04-10T22:12:09Z".to_string(), + in_reply_to_id: None, + in_reply_to_account_id: None, + content: record.content.clone(), + visibility: "public".to_string(), + spoiler_text: "".to_string(), + sensitive: false, + uri: record.post_id.clone(), + url: record.post_id.clone(), + replies_count: 0, + reblogs_count: 0, + favourites_count: 0, + favourited: false, + reblogged: false, + muted: false, + bookmarked: false, + media_attachments: vec![], + account: CredentialAcount { + id: record.actor_id.clone(), + username: record.username.clone(), + acct: record.username.clone(), + display_name: record.display_name.clone(), + locked: false, + bot: false, + created_at: "2025-04-10T22:12:09Z".to_string(), + attribution_domains: vec![], + note: "".to_string(), + url: record.actor_id.clone(), + avatar: "https://ferri.amy.mov/assets/pfp.png".to_string(), + avatar_static: "https://ferri.amy.mov/assets/pfp.png".to_string(), + header: "https://ferri.amy.mov/assets/pfp.png".to_string(), + header_static: "https://ferri.amy.mov/assets/pfp.png".to_string(), + followers_count: 1, + following_count: 1, + statuses_count: 1, + last_status_at: "2025-04-10T22:14:34Z".to_string(), + }, + }); + } + + Json(out) +} diff --git a/ferri-server/src/endpoints/api/user.rs b/ferri-server/src/endpoints/api/user.rs index 78103ad..ea5d43f 100644 --- a/ferri-server/src/endpoints/api/user.rs +++ b/ferri-server/src/endpoints/api/user.rs @@ -1,4 +1,3 @@ -use chrono::Local; use main::ap; use rocket::{ State, get, post, @@ -72,7 +71,7 @@ pub async fn new_follow( id: format!("https://ferri.amy.mov/activities/{}", Uuid::new_v4()), ty: ap::ActivityType::Follow, object: followed.actor_id().to_string(), - published: Local::now(), + ..Default::default() }; let req = ap::OutgoingActivity { @@ -84,5 +83,6 @@ pub async fn new_follow( to: followed.actor().clone(), }; + req.save(&mut **db).await; outbox.post(req).await; } diff --git a/ferri-server/src/endpoints/custom.rs b/ferri-server/src/endpoints/custom.rs index 55dc93b..1306041 100644 --- a/ferri-server/src/endpoints/custom.rs +++ b/ferri-server/src/endpoints/custom.rs @@ -87,7 +87,6 @@ pub async fn test(http: &State) -> &'static str { let post = activity::CreateActivity { id: "https://ferri.amy.mov/activities/amy/20".to_string(), ty: "Create".to_string(), - summary: "Amy create a note".to_string(), actor: "https://ferri.amy.mov/users/amy".to_string(), object: content::Post { context: "https://www.w3.org/ns/activitystreams".to_string(), diff --git a/ferri-server/src/endpoints/inbox.rs b/ferri-server/src/endpoints/inbox.rs new file mode 100644 index 0000000..543d652 --- /dev/null +++ b/ferri-server/src/endpoints/inbox.rs @@ -0,0 +1,173 @@ +use main::ap; +use rocket::{State, post}; +use rocket_db_pools::Connection; +use rocket::serde::json::serde_json; +use sqlx::Sqlite; +use url::Url; +use uuid::Uuid; +use chrono::Local; + +use crate::{ + Db, + http::HttpClient, + types::{Person, activity}, +}; + +fn handle_delete_activity(activity: activity::DeleteActivity) { + dbg!(activity); +} + +async fn create_actor(user: &Person, actor: String, conn: impl sqlx::Executor<'_, Database = Sqlite>) { + sqlx::query!( + r#" + INSERT INTO actor (id, inbox, outbox) + VALUES ( ?1, ?2, ?3 ) + ON CONFLICT(id) DO NOTHING; + "#, + actor, + user.inbox, + user.outbox + ) + .execute(conn) + .await + .unwrap(); +} + +async fn create_user(user: &Person, actor: String, conn: impl sqlx::Executor<'_, Database = Sqlite>) { + // HACK: Allow us to formulate a `user@host` username by assuming the actor is on the same host as the user + let url = Url::parse(&actor).unwrap(); + let host = url.host_str().unwrap(); + let username = format!("{}@{}", user.name, host); + + let uuid = Uuid::new_v4().to_string(); + sqlx::query!( + r#" + INSERT INTO user (id, username, actor_id, display_name) + VALUES (?1, ?2, ?3, ?4) + ON CONFLICT(actor_id) DO NOTHING; + "#, + uuid, + username, + actor, + user.preferred_username + ) + .execute(conn) + .await + .unwrap(); +} + +async fn create_follow(activity: &activity::FollowActivity, conn: impl sqlx::Executor<'_, Database = Sqlite>) { + sqlx::query!( + r#" + INSERT INTO follow (id, follower_id, followed_id) + VALUES ( ?1, ?2, ?3 ) + ON CONFLICT(id) DO NOTHING; + "#, + activity.id, + activity.actor, + activity.object + ) + .execute(conn) + .await + .unwrap(); +} + +async fn handle_follow_activity(followed_account: String, activity: activity::FollowActivity, http: &HttpClient, mut db: Connection) { + let user = http + .get(&activity.actor) + .activity() + .send() + .await + .unwrap() + .json::() + .await + .unwrap(); + + create_actor(&user, activity.actor.clone(), &mut **db).await; + create_user(&user, activity.actor.clone(), &mut **db).await; + create_follow(&activity, &mut **db).await; + + let follower = ap::User::from_actor_id(&activity.actor, &mut **db).await; + let followed = ap::User::from_username(&followed_account, &mut **db).await; + let outbox = ap::Outbox::for_user(followed.clone(), http); + + let activity = ap::Activity { + id: format!("https://ferri.amy.mov/activities/{}", Uuid::new_v4()), + ty: ap::ActivityType::Accept, + object: activity.id, + ..Default::default() + }; + + let req = ap::OutgoingActivity { + signed_by: format!( + "https://ferri.amy.mov/users/{}#main-key", + followed.username() + ), + req: activity, + to: follower.actor().clone(), + }; + + req.save(&mut **db).await; + outbox.post(req).await; +} + +async fn handle_create_activity(activity: activity::CreateActivity,http: &HttpClient, mut db: Connection) { + assert!(&activity.object.ty == "Note"); + let user = http + .get(&activity.actor) + .activity() + .send() + .await + .unwrap() + .json::() + .await + .unwrap(); + + create_actor(&user, activity.actor.clone(), &mut **db).await; + create_user(&user, activity.actor.clone(), &mut **db).await; + + let user = ap::User::from_actor_id(&activity.actor, &mut **db).await; + + let post_id = Uuid::new_v4(); + + let uri = format!( + "https://ferri.amy.mov/users/{}/posts/{}", + user.username(), + post_id + ); + let id = user.id(); + let now = Local::now().to_rfc3339(); + let content = activity.object.content.clone(); + + sqlx::query!(r#" + INSERT INTO post (id, user_id, content, created_at) + VALUES (?1, ?2, ?3, ?4) + "#, uri, id, content, now) + .execute(&mut **db) + .await.unwrap(); +} + +#[post("/users//inbox", data = "")] +pub async fn inbox(db: Connection, http: &State, user: String, body: String) { + let min = serde_json::from_str::(&body).unwrap(); + match min.ty.as_str() { + "Delete" => { + let activity = serde_json::from_str::(&body).unwrap(); + handle_delete_activity(activity); + } + "Follow" => { + let activity = serde_json::from_str::(&body).unwrap(); + handle_follow_activity(user, activity, http.inner(), db).await; + }, + "Create" => { + let activity = serde_json::from_str::(&body).unwrap(); + handle_create_activity(activity, http.inner(), db).await; + }, + unknown => { + eprintln!("WARN: Unknown activity '{}' - {}", unknown, body); + } + } + + dbg!(min); + println!("Body in inbox: {}", body); +} diff --git a/ferri-server/src/endpoints/mod.rs b/ferri-server/src/endpoints/mod.rs index 048601e..eb3a681 100644 --- a/ferri-server/src/endpoints/mod.rs +++ b/ferri-server/src/endpoints/mod.rs @@ -6,6 +6,7 @@ pub mod oauth; pub mod api; pub mod well_known; pub mod custom; +pub mod inbox; fn activity_type() -> ContentType { ContentType(MediaType::new("application", "activity+json")) diff --git a/ferri-server/src/endpoints/user.rs b/ferri-server/src/endpoints/user.rs index be52f61..36e3ccc 100644 --- a/ferri-server/src/endpoints/user.rs +++ b/ferri-server/src/endpoints/user.rs @@ -1,14 +1,12 @@ use main::ap; -use rocket::{State, get, http::ContentType, post, serde::json::Json}; +use rocket::{get, http::ContentType, serde::json::Json}; use rocket_db_pools::Connection; use crate::{ Db, - http::HttpClient, - types::{OrderedCollection, Person, UserKey, activity, content}, + types::{OrderedCollection, Person, UserKey, content}, }; -use rocket::serde::json::serde_json; use super::activity_type; @@ -22,91 +20,6 @@ pub async fn inbox(user: String) -> Json { }) } -#[post("/users//inbox", data = "")] -pub async fn post_inbox( - mut db: Connection, - http: &State, - user: String, - body: String, -) { - let min = serde_json::from_str::(&body).unwrap(); - match min.ty.as_str() { - "Delete" => { - let activity = serde_json::from_str::(&body); - dbg!(activity.unwrap()); - } - "Follow" => { - let activity = serde_json::from_str::(&body).unwrap(); - dbg!(&activity); - - let user = http - .get(&activity.actor) - .activity() - .send() - .await - .unwrap() - .json::() - .await - .unwrap(); - - sqlx::query!( - r#" - INSERT INTO actor (id, inbox, outbox) - VALUES ( ?1, ?2, ?3 ) - ON CONFLICT(id) DO NOTHING; - "#, - activity.actor, - user.inbox, - user.outbox - ) - .execute(&mut **db) - .await - .unwrap(); - - sqlx::query!( - r#" - INSERT INTO follow (id, follower_id, followed_id) - VALUES ( ?1, ?2, ?3 ) - ON CONFLICT(id) DO NOTHING; - "#, - activity.id, - activity.actor, - activity.object - ) - .execute(&mut **db) - .await - .unwrap(); - - let accept = activity::AcceptActivity { - ty: "Accept".to_string(), - actor: "https://ferri.amy.mov/users/amy".to_string(), - object: activity.id, - }; - - let key_id = "https://ferri.amy.mov/users/amy#main-key"; - let accept_res = http - .post(user.inbox) - .json(&accept) - .sign(key_id) - .activity() - .send() - .await - .unwrap() - .text() - .await - .unwrap(); - - dbg!(accept_res); - } - unknown => { - eprintln!("WARN: Unknown activity '{}' - {}", unknown, body); - } - } - - dbg!(min); - println!("Body in inbox: {}", body); -} - #[get("/users//outbox")] pub async fn outbox(user: String) -> Json { dbg!(&user); diff --git a/ferri-server/src/lib.rs b/ferri-server/src/lib.rs index 898da07..f9ddbfe 100644 --- a/ferri-server/src/lib.rs +++ b/ferri-server/src/lib.rs @@ -1,20 +1,14 @@ -use main::ap::{http}; +use main::ap::http; use rocket::{ - build, get, http::ContentType, request::{FromRequest, Outcome}, routes, serde::{ - json::Json, Deserialize, Serialize - }, Build, Request, Rocket + build, get, http::ContentType, request::{FromRequest, Outcome}, routes, Build, Request, Rocket }; - +use endpoints::{api::{self, timeline}, oauth, well_known, custom, user, inbox}; +use rocket_db_pools::{sqlx, Database}; mod cors; mod types; mod endpoints; -use endpoints::{api::{self, user::CredentialAcount}, oauth, well_known, custom, user}; - -use rocket_db_pools::sqlx; -use rocket_db_pools::Database; - #[derive(Database)] #[database("sqlite_ferri")] pub struct Db(sqlx::SqlitePool); @@ -54,88 +48,18 @@ impl<'a> FromRequest<'a> for AuthenticatedUser { } } -pub type TimelineAccount = CredentialAcount; - -#[derive(Debug, Serialize, Deserialize)] -#[serde(crate = "rocket::serde")] -struct TimelineStatus { - id: String, - created_at: String, - in_reply_to_id: Option, - in_reply_to_account_id: Option, - content: String, - visibility: String, - spoiler_text: String, - sensitive: bool, - uri: String, - url: String, - replies_count: i64, - reblogs_count: i64, - favourites_count: i64, - favourited: bool, - reblogged: bool, - muted: bool, - bookmarked: bool, - media_attachments: Vec<()>, - account: TimelineAccount, -} - -#[get("/timelines/home?")] -async fn home_timeline(limit: i64) -> Json> { - Json(vec![TimelineStatus { - id: "1".to_string(), - created_at: "2025-04-10T22:12:09Z".to_string(), - in_reply_to_id: None, - in_reply_to_account_id: None, - content: "My first post".to_string(), - visibility: "public".to_string(), - spoiler_text: "".to_string(), - sensitive: false, - uri: "https://ferri.amy.mov/users/amy/posts/1".to_string(), - url: "https://ferri.amy.mov/users/amy/posts/1".to_string(), - replies_count: 0, - reblogs_count: 0, - favourites_count: 0, - favourited: false, - reblogged: false, - muted: false, - bookmarked: false, - media_attachments: vec![], - account: CredentialAcount { - id: "https://ferri.amy.mov/users/amy".to_string(), - username: "amy".to_string(), - acct: "amy@ferri.amy.mov".to_string(), - display_name: "amy".to_string(), - locked: false, - bot: false, - created_at: "2025-04-10T22:12:09Z".to_string(), - attribution_domains: vec![], - note: "".to_string(), - url: "https://ferri.amy.mov/@amy".to_string(), - avatar: "https://i.sstatic.net/l60Hf.png".to_string(), - avatar_static: "https://i.sstatic.net/l60Hf.png".to_string(), - header: "https://i.sstatic.net/l60Hf.png".to_string(), - header_static: "https://i.sstatic.net/l60Hf.png".to_string(), - followers_count: 1, - following_count: 1, - statuses_count: 1, - last_status_at: "2025-04-10T22:14:34Z".to_string(), - }, - }]) -} - pub fn launch() -> Rocket { let http_client = http::HttpClient::new(); build() .manage(http_client) .attach(Db::init()) .attach(cors::CORS) + .mount("/assets", rocket::fs::FileServer::from("./assets")) .mount( "/", routes![ custom::test, user::inbox, - user::post_inbox, user::outbox, user::user, user::followers, @@ -147,6 +71,7 @@ pub fn launch() -> Rocket { activity_endpoint, well_known::webfinger, well_known::host_meta, + inbox::inbox, user_profile, ], ) @@ -160,7 +85,7 @@ pub fn launch() -> Rocket { api::preferences::preferences, api::user::verify_credentials, custom::finger_account, - home_timeline + timeline::home ], ) } diff --git a/ferri-server/src/types/activity.rs b/ferri-server/src/types/activity.rs index 73f13b6..165925d 100644 --- a/ferri-server/src/types/activity.rs +++ b/ferri-server/src/types/activity.rs @@ -2,19 +2,6 @@ use rocket::serde::{Deserialize, Serialize}; use crate::types::content::Post; -#[derive(Serialize, Deserialize)] -#[serde(crate = "rocket::serde")] -pub struct Activity { - pub id: String, - #[serde(rename = "type")] - pub ty: String, - - pub summary: String, - pub actor: String, - pub object: String, - pub published: String, -} - #[derive(Serialize, Deserialize, Debug)] #[serde(crate = "rocket::serde")] pub struct MinimalActivity { @@ -45,9 +32,9 @@ pub struct CreateActivity { pub actor: String, pub to: Vec, pub cc: Vec, + #[serde(rename = "published")] pub ts: String, - pub summary: String, } #[derive(Serialize, Deserialize, Debug)] diff --git a/ferri-server/src/types/content.rs b/ferri-server/src/types/content.rs index b6b8263..ffd8922 100644 --- a/ferri-server/src/types/content.rs +++ b/ferri-server/src/types/content.rs @@ -1,6 +1,6 @@ use rocket::serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Default)] #[serde(crate = "rocket::serde")] pub struct Post { // FIXME: This is because Masto sends an array but we don't care diff --git a/migrations/20250410112325_add_follow.sql b/migrations/20250410112325_add_follow.sql index 81a4511..ce518cd 100644 --- a/migrations/20250410112325_add_follow.sql +++ b/migrations/20250410112325_add_follow.sql @@ -1,7 +1,7 @@ CREATE TABLE IF NOT EXISTS follow ( - -- Activity ID - id TEXT PRIMARY KEY NOT NULL, + -- Activity ID + id TEXT PRIMARY KEY NOT NULL, follower_id TEXT NOT NULL, followed_id TEXT NOT NULL, FOREIGN KEY(follower_id) REFERENCES actor(id), diff --git a/migrations/20250410121119_add_user.sql b/migrations/20250410121119_add_user.sql index ada5bab..b1706e0 100644 --- a/migrations/20250410121119_add_user.sql +++ b/migrations/20250410121119_add_user.sql @@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS user -- UUID id TEXT PRIMARY KEY NOT NULL, username TEXT NOT NULL, - actor_id TEXT NOT NULL, + actor_id TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, FOREIGN KEY(actor_id) REFERENCES actor(id) diff --git a/migrations/20250410182845_add_post.sql b/migrations/20250410182845_add_post.sql index 8790cf6..f2e47bd 100644 --- a/migrations/20250410182845_add_post.sql +++ b/migrations/20250410182845_add_post.sql @@ -1,9 +1,10 @@ CREATE TABLE IF NOT EXISTS post ( -- Uri - id TEXT PRIMARY KEY NOT NULL, + id TEXT PRIMARY KEY NOT NULL, user_id TEXT NOT NULL, content TEXT NOT NULL, + created_at TEXT NOT NULL, FOREIGN KEY(user_id) REFERENCES user(id) ); diff --git a/migrations/20250411125138_add_activity.sql b/migrations/20250411125138_add_activity.sql new file mode 100644 index 0000000..2ccf07d --- /dev/null +++ b/migrations/20250411125138_add_activity.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS activity +( + -- UUID + id TEXT PRIMARY KEY NOT NULL, + ty TEXT NOT NULL, + actor_id TEXT NOT NULL, + + FOREIGN KEY(actor_id) REFERENCES actor(id) +); +