From 90be7d570ed3a0d4c3561d5505f157a9e0570b71 Mon Sep 17 00:00:00 2001 From: nullishamy Date: Mon, 5 May 2025 15:54:23 +0100 Subject: [PATCH] feat: timeline cleanup --- ferri-main/src/federation/http.rs | 4 +- ferri-main/src/federation/inbox.rs | 28 +++- ferri-main/src/federation/outbox.rs | 45 +++++- ferri-main/src/types/convert.rs | 20 ++- ferri-main/src/types/get.rs | 164 +++++++++++++++++--- ferri-main/src/types/make.rs | 2 +- ferri-main/src/types/mod.rs | 32 +++- ferri-server/src/endpoints/api/status.rs | 162 +++++++------------- ferri-server/src/endpoints/api/timeline.rs | 168 +-------------------- ferri-server/src/endpoints/inbox.rs | 17 ++- ferri-server/src/endpoints/user.rs | 30 ++-- ferri-server/src/lib.rs | 26 ++-- 12 files changed, 372 insertions(+), 326 deletions(-) diff --git a/ferri-main/src/federation/http.rs b/ferri-main/src/federation/http.rs index 4d30b32..d6e592d 100644 --- a/ferri-main/src/federation/http.rs +++ b/ferri-main/src/federation/http.rs @@ -87,9 +87,9 @@ impl<'a> HttpWrapper<'a> { let http_result = self .client .post(inbox) - .sign(self.key_id) - .json(activity) .activity() + .json(activity) + .sign(self.key_id) .send() .await; diff --git a/ferri-main/src/federation/inbox.rs b/ferri-main/src/federation/inbox.rs index c41c096..68dd742 100644 --- a/ferri-main/src/federation/inbox.rs +++ b/ferri-main/src/federation/inbox.rs @@ -40,6 +40,14 @@ pub async fn handle_inbox_request( let follower = http.get_person(&activity.actor).await.unwrap(); + let actor = db::Actor { + id: follower.obj.id.clone(), + inbox: follower.inbox.clone(), + outbox: follower.outbox.clone() + }; + + make::new_actor(actor, &mut conn).await.unwrap(); + let follow = db::Follow { id: ObjectUri( format!("https://ferri.amy.mov/activities/{}", crate::new_id()) @@ -117,16 +125,29 @@ pub async fn handle_inbox_request( .await .unwrap(); + let attachments = post.attachment + .into_iter() + .map(|at| { + db::Attachment { + id: ObjectUuid(crate::new_id()), + post_id: ObjectUuid(post_id.clone()), + url: at.url, + media_type: Some(at.media_type), + sensitive: at.sensitive, + alt: at.summary + } + }) + .collect::>(); + let post = db::Post { id: ObjectUuid(post_id), uri: post.obj.id, user, content: post.content, created_at, - attachments: vec![], + attachments, boosted_post: None }; - make::new_post(post, &mut conn) .await @@ -288,8 +309,7 @@ pub async fn handle_inbox_request( attachments: vec![], content: String::new(), created_at, - boosted_post: Some(boosted_post.id.clone()) - + boosted_post: Some(Box::new(boosted_post.clone())) } }; diff --git a/ferri-main/src/federation/outbox.rs b/ferri-main/src/federation/outbox.rs index 072e5af..0ffa2ad 100644 --- a/ferri-main/src/federation/outbox.rs +++ b/ferri-main/src/federation/outbox.rs @@ -1,13 +1,14 @@ use serde::{Deserialize, Serialize}; use tracing::info; use std::fmt::Debug; -use crate::{ap::http::HttpClient, federation::http::HttpWrapper, types::{ap, ObjectContext}}; +use crate::{ap::http::HttpClient, federation::http::HttpWrapper, types::{ap::{self, ActivityType}, as_context, db, Object, ObjectContext, ObjectUri}}; #[derive(Debug)] pub enum OutboxRequest { // FIXME: Make the String (key_id) nicer // Probably store it in the DB and pass a db::User here - Accept(ap::AcceptActivity, String, ap::Person) + Accept(ap::AcceptActivity, String, ap::Person), + Status(db::Post, String) } #[derive(Serialize, Deserialize, Debug)] @@ -49,5 +50,45 @@ pub async fn handle_outbox_request( info!("accept res {}", res); }, + OutboxRequest::Status(post, key_id) => { + // FIXME: Take a list of who we should send to + // for now we only propogate to my main instance + + let http = HttpWrapper::new(http, &key_id); + + let activity = PreparedActivity { + context: as_context(), + id: format!("https://ferri.amy.mov/activities/{}", crate::new_id()), + ty: ActivityType::Create, + actor: post.user.actor.id.0.clone(), + object: ap::Post { + obj: Object { + id: ObjectUri( + format!( + "https://ferri.amy.mov/users/{}/posts/{}", + post.user.id.0, + post.id.0 + ) + ), + context: as_context() + }, + ty: ActivityType::Note, + ts: post.created_at.to_rfc3339(), + content: post.content, + to: vec![format!("https://ferri.amy.mov/users/{}/followers", post.user.id.0)], + cc: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()], + attachment: vec![], + attributed_to: Some(post.user.actor.id.0) + }, + published: crate::ap::new_ts() + }; + + let res = http + .post_activity("https://fedi.amy.mov/users/9zkygethkdw60001/inbox", activity) + .await + .unwrap(); + + info!("status res {}", res); + } } } diff --git a/ferri-main/src/types/convert.rs b/ferri-main/src/types/convert.rs index c473954..26ea91c 100644 --- a/ferri-main/src/types/convert.rs +++ b/ferri-main/src/types/convert.rs @@ -4,6 +4,8 @@ use crate::types::db; use crate::types::{Object, ObjectUri, as_context}; +use super::ap::ActivityType; + impl From for ap::Actor { fn from(val: db::Actor) -> ap::Actor { ap::Actor { @@ -67,6 +69,7 @@ impl From for ap::Person { context: as_context(), id: ObjectUri(format!("https://ferri.amy.mov/users/{}", val.id.0)), }, + ty: ActivityType::Person, following: format!("https://ferri.amy.mov/users/{}/following", val.id.0), followers: format!("https://ferri.amy.mov/users/{}/followers", val.id.0), summary: format!("ferri {}", val.username), @@ -105,10 +108,23 @@ impl From for api::Status { muted: false, bookmarked: false, content: value.content, - reblog: None, + reblog: value.boosted_post.map(|p| { + // Probably a better way to do this without reboxing but whatever... + let p: db::Post = *p; + let p: api::Status = p.into(); + Box::new(p) + }), application: None, account: value.user.into(), - media_attachments: vec![], + media_attachments: value.attachments + .into_iter() + .map(|at| api::StatusAttachment { + id: at.id, + ty: "image".to_string(), + url: at.url, + description: at.alt.unwrap_or(String::new()) + }) + .collect(), mentions: vec![], tags: vec![], emojis: vec![], diff --git a/ferri-main/src/types/get.rs b/ferri-main/src/types/get.rs index 4115e98..0db4500 100644 --- a/ferri-main/src/types/get.rs +++ b/ferri-main/src/types/get.rs @@ -187,6 +187,34 @@ pub async fn user_by_actor_uri(uri: ObjectUri, conn: &mut SqliteConnection) -> R }) } +pub async fn attachments_for_post( + post_id: ObjectUuid, + conn: &mut SqliteConnection +)-> Result, DbError> { + let attachments = sqlx::query!( + "SELECT * FROM attachment WHERE post_id = ?", + post_id.0 + ) + .fetch_all(&mut *conn) + .await + .unwrap(); + + let attachments = attachments.into_iter() + .map(|at| { + db::Attachment { + id: ObjectUuid(at.id), + post_id: ObjectUuid(at.post_id), + url: at.url, + media_type: Some(at.media_type), + sensitive: at.marked_sensitive, + alt: at.alt + } + }) + .collect::>(); + + Ok(attachments) +} + pub async fn posts_for_user_id( id: ObjectUuid, conn: &mut SqliteConnection @@ -209,26 +237,9 @@ pub async fn posts_for_user_id( .unwrap(); for record in posts { - let attachments = sqlx::query!( - "SELECT * FROM attachment WHERE post_id = ?", - record.post_id - ) - .fetch_all(&mut *conn) + let attachments = attachments_for_post(ObjectUuid(record.post_id.clone()), conn) .await .unwrap(); - - let attachments = attachments.into_iter() - .map(|at| { - db::Attachment { - id: ObjectUuid(at.id), - post_id: ObjectUuid(at.post_id), - url: at.url, - media_type: Some(at.media_type), - sensitive: at.marked_sensitive, - alt: at.alt - } - }) - .collect::>(); let user_created = parse_ts(record.user_created) .expect("no db corruption"); @@ -263,3 +274,120 @@ pub async fn posts_for_user_id( Ok(out) } + +pub async fn home_timeline( + actor: ObjectUri, + conn: &mut SqliteConnection +) -> Result, DbError> { + #[derive(sqlx::FromRow, Debug, Clone)] + struct Post { + is_boost_source: bool, + post_id: String, + user_id: String, + post_uri: String, + content: String, + post_created: String, + user_created: String, + actor_id: String, + acct: String, + remote: bool, + boosted_post_id: Option, + display_name: String, + username: String, + icon_url: String, + user_url: String, + inbox: String, + outbox: String + } + + fn make_into_db(p: Post, attachments: Vec) -> db::Post { + db::Post { + id: ObjectUuid(p.post_id), + uri: ObjectUri(p.post_uri), + user: db::User { + id: ObjectUuid(p.user_id), + actor: db::Actor { + id: ObjectUri(p.actor_id), + inbox: p.inbox, + outbox: p.outbox + }, + username: p.username, + display_name: p.display_name, + acct: p.acct, + remote: p.remote, + url: p.user_url, + created_at: parse_ts(p.user_created).unwrap(), + icon_url: p.icon_url, + posts: db::UserPosts { + last_post_at: None + } + }, + content: p.content, + created_at: parse_ts(p.post_created).unwrap(), + boosted_post: None, + attachments + } + } + + // FIXME: query! can't cope with this. returns a type error + let posts = sqlx::query_as::<_, Post>( + r#" + WITH RECURSIVE get_home_timeline_with_boosts( + id, boosted_post_id, is_boost_source + ) AS + ( + SELECT p.id, p.boosted_post_id, 0 as is_boost_source + FROM post p + WHERE p.user_id IN ( + SELECT u.id + FROM follow f + INNER JOIN user u ON u.actor_id = f.followed_id + WHERE f.follower_id = $1 + ) + UNION + SELECT p.id, p.boosted_post_id, 1 as is_boost_source + FROM post p + JOIN get_home_timeline_with_boosts tl ON tl.boosted_post_id = p.id + ) + SELECT is_boost_source, p.id as "post_id", u.id as "user_id", + p.content, p.uri as "post_uri", u.username, u.display_name, + u.actor_id, p.created_at as "post_created", p.boosted_post_id, u.icon_url, u.url as "user_url", + a.inbox, a.outbox, u.acct, u.remote, u.created_at as "user_created" + FROM get_home_timeline_with_boosts + JOIN post p ON p.id = get_home_timeline_with_boosts.id + JOIN actor a ON u.actor_id = a.id + JOIN user u ON u.id = p.user_id; + "#, + ) + .bind(actor.0) + .fetch_all(&mut *conn) + .await + .unwrap(); + + let mut out = vec![]; + for post in posts.iter() { + let boost_id = post.boosted_post_id.clone(); + let is_boost_base = post.is_boost_source; + let attachments = attachments_for_post(ObjectUuid(post.post_id.clone()), &mut *conn) + .await + .unwrap(); + + let mut base = make_into_db(post.clone(), attachments); + if let Some(boost_id) = boost_id { + + let attachments = attachments_for_post(ObjectUuid(boost_id.clone()), &mut *conn) + .await + .unwrap(); + + let boost = posts.iter().find(|p| p.post_id == boost_id).unwrap(); + let boost = make_into_db(boost.clone(), attachments); + base.boosted_post = Some(Box::new(boost)); + } + + if !is_boost_base { + out.push(base); + } + } + + Ok(out) +} diff --git a/ferri-main/src/types/make.rs b/ferri-main/src/types/make.rs index a905dc9..b961bf3 100644 --- a/ferri-main/src/types/make.rs +++ b/ferri-main/src/types/make.rs @@ -96,7 +96,7 @@ pub async fn new_post( conn: &mut SqliteConnection, ) -> Result { let ts = post.created_at.to_rfc3339(); - let boosted = post.boosted_post.as_ref().map(|b| &b.0); + let boosted = post.boosted_post.as_ref().map(|b| &b.id.0); sqlx::query!( r#" diff --git a/ferri-main/src/types/mod.rs b/ferri-main/src/types/mod.rs index 326c8f2..35c4a43 100644 --- a/ferri-main/src/types/mod.rs +++ b/ferri-main/src/types/mod.rs @@ -1,4 +1,4 @@ -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use std::fmt::Debug; use thiserror::Error; use uuid::Uuid; @@ -7,6 +7,15 @@ pub mod convert; pub mod get; pub mod make; +fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result +where + T: Default + Deserialize<'de>, + D: Deserializer<'de>, +{ + let opt = Option::deserialize(deserializer)?; + Ok(opt.unwrap_or_default()) +} + #[derive(Debug, Error)] pub enum DbError { #[error("an unknown error occured when creating: {0}")] @@ -115,7 +124,7 @@ pub mod db { pub user: User, pub content: String, pub created_at: DateTime, - pub boosted_post: Option, + pub boosted_post: Option>, pub attachments: Vec } } @@ -127,12 +136,14 @@ pub mod ap { #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub enum ActivityType { + Reject, Create, Note, Delete, Undo, Accept, Announce, + Person, Like, Follow, } @@ -227,7 +238,9 @@ pub mod ap { pub media_type: String, pub url: String, + #[serde(deserialize_with = "deserialize_null_default")] pub name: String, + pub summary: Option, #[serde(default)] pub sensitive: bool @@ -287,6 +300,9 @@ pub mod ap { #[serde(flatten)] pub obj: Object, + #[serde(rename = "type")] + pub ty: ActivityType, + pub following: String, pub followers: String, @@ -371,6 +387,16 @@ pub mod api { pub links: Vec, } + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] + pub struct StatusAttachment { + pub id: ObjectUuid, + #[serde(rename = "type")] + pub ty: String, + + pub url: String, + pub description: String + } + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct Status { pub id: ObjectUuid, @@ -394,7 +420,7 @@ pub mod api { pub reblog: Option>, pub application: Option<()>, pub account: Account, - pub media_attachments: Vec>, + pub media_attachments: Vec, pub mentions: Vec>, pub tags: Vec>, pub emojis: Vec>, diff --git a/ferri-server/src/endpoints/api/status.rs b/ferri-server/src/endpoints/api/status.rs index 90b766c..2ce38be 100644 --- a/ferri-server/src/endpoints/api/status.rs +++ b/ferri-server/src/endpoints/api/status.rs @@ -1,5 +1,4 @@ -use crate::timeline::TimelineStatus; -use main::ap::{self, http::HttpClient}; +use main::{config::Config, federation::{outbox::OutboxRequest, QueueMessage}, types::{api, make, ObjectUri, ObjectUuid}}; use rocket::{ FromForm, State, form::Form, @@ -7,22 +6,15 @@ use rocket::{ serde::{Deserialize, Serialize, json::Json}, }; use rocket_db_pools::Connection; -use uuid::Uuid; +use main::types::db; -use crate::api::user::CredentialAcount; -use crate::{AuthenticatedUser, Db}; - -#[derive(Serialize, Deserialize, Debug, FromForm)] -#[serde(crate = "rocket::serde")] -pub struct Status { - status: String, -} +use crate::{AuthenticatedUser, Db, OutboundQueue}; #[derive(Serialize, Deserialize, Debug, FromForm)] #[serde(crate = "rocket::serde")] pub struct StatusContext { - ancestors: Vec, - descendants: Vec, + ancestors: Vec, + descendants: Vec, } #[get("/statuses/<_status>/context")] @@ -37,114 +29,64 @@ pub async fn status_context( }) } -async fn create_status( - user: AuthenticatedUser, - mut db: Connection, - http: &HttpClient, - status: &Status, -) -> TimelineStatus { - let user = ap::User::from_id(&user.id.0, &mut **db).await.unwrap(); - let outbox = ap::Outbox::for_user(user.clone(), http); +#[derive(Serialize, Deserialize, Debug, FromForm)] +#[serde(crate = "rocket::serde")] +pub struct CreateStatus { + status: String, +} - let post_id = ap::new_id(); - let now = ap::now(); - - let post = ap::Post::from_parts(post_id, status.status.clone(), user.clone()) - .to(format!("{}/followers", user.uri())) - .cc("https://www.w3.org/ns/activitystreams#Public".to_string()); - - post.save(&mut **db).await; - - let actor = sqlx::query!( - "SELECT * FROM actor WHERE id = ?1", - "https://fedi.amy.mov/users/9zkygethkdw60001" - ) - .fetch_one(&mut **db) - .await - .unwrap(); - - let create_id = format!("https://ferri.amy.mov/activities/{}", Uuid::new_v4()); - - let activity = ap::Activity { - id: create_id, - ty: ap::ActivityType::Create, - object: post.clone().to_ap(), - to: vec![format!("{}/followers", user.uri())], - published: now, - cc: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()], - ..Default::default() - }; - - let actor = ap::Actor::from_raw(actor.id.clone(), actor.inbox.clone(), actor.outbox.clone()); - - let req = ap::OutgoingActivity { - req: activity, - signed_by: format!("{}#main-key", user.uri()), - to: actor, - }; - - req.save(&mut **db).await; - outbox.post(req).await; - - TimelineStatus { - id: post.id().to_string(), - created_at: post.created_at(), - in_reply_to_id: None, - in_reply_to_account_id: None, - content: post.content().to_string(), - visibility: "public".to_string(), - spoiler_text: "".to_string(), - sensitive: false, - uri: post.uri(), - url: post.uri(), - replies_count: 0, - reblogs_count: 0, - favourites_count: 0, - favourited: false, - reblogged: false, - reblog: None, - muted: false, - bookmarked: false, - media_attachments: vec![], - account: CredentialAcount { - id: user.id().to_string(), - username: user.username().to_string(), - acct: user.username().to_string(), - display_name: user.display_name().to_string(), - locked: false, - bot: false, - created_at: "2025-04-10T22:12:09Z".to_string(), - attribution_domains: vec![], - note: "".to_string(), - url: user.uri(), - 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(), - }, +fn to_db_post(req: &CreateStatus, user: &AuthenticatedUser, config: &Config) -> db::Post { + let post_id = main::new_id(); + + db::Post { + id: ObjectUuid(post_id.clone()), + uri: ObjectUri(config.post_url(&user.id.0, &post_id)), + user: user.user.clone(), + content: req.status.clone(), + created_at: main::ap::now(), + boosted_post: None, + attachments: vec![] } } #[post("/statuses", data = "")] pub async fn new_status( - db: Connection, + mut db: Connection, helpers: &State, - status: Form, + status: Form, user: AuthenticatedUser, -) -> Json { - Json(create_status(user, db, &helpers.http, &status).await) -} +) -> Json { + let post = make::new_post( + to_db_post(&status, &user, &helpers.config), + &mut **db + ) + .await + .unwrap(); + + Json(post.into()) +} #[post("/statuses", data = "", rank = 2)] pub async fn new_status_json( - db: Connection, + mut db: Connection, helpers: &State, - status: Json, + outbound: &State, + status: Json, user: AuthenticatedUser, -) -> Json { - Json(create_status(user, db, &helpers.http, &status).await) +) -> Json { + let post = make::new_post( + to_db_post(&status, &user, &helpers.config), + &mut **db + ) + .await + .unwrap(); + + + let key_id = "https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9#main-key"; + outbound.0.send(QueueMessage::Outbound( + OutboxRequest::Status(post.clone(), key_id.to_string())) + ) + .await; + + Json(post.into()) } diff --git a/ferri-server/src/endpoints/api/timeline.rs b/ferri-server/src/endpoints/api/timeline.rs index c6c8fb4..582b397 100644 --- a/ferri-server/src/endpoints/api/timeline.rs +++ b/ferri-server/src/endpoints/api/timeline.rs @@ -1,5 +1,5 @@ use crate::{AuthenticatedUser, Db, endpoints::api::user::CredentialAcount}; -use main::types::ObjectUuid; +use main::types::{api, get, ObjectUuid}; use rocket::{ get, serde::{Deserialize, Serialize, json::Json}, @@ -47,166 +47,10 @@ pub struct TimelineStatus { pub async fn home( mut db: Connection, user: AuthenticatedUser, -) -> Json> { - #[derive(sqlx::FromRow, Debug)] - struct Post { - is_boost_source: bool, - post_id: String, - user_id: String, - post_uri: String, - content: String, - created_at: String, - boosted_post_id: Option, - display_name: String, - username: String, - icon_url: String, - user_url: String - } +) -> Json> { + let posts = get::home_timeline(user.actor_id, &mut **db) + .await + .unwrap(); - // FIXME: query! can't cope with this. returns a type error - let posts = sqlx::query_as::<_, Post>( - r#" - WITH RECURSIVE get_home_timeline_with_boosts( - id, boosted_post_id, is_boost_source - ) AS - ( - SELECT p.id, p.boosted_post_id, 0 as is_boost_source - FROM post p - WHERE p.user_id IN ( - SELECT u.id - FROM follow f - INNER JOIN user u ON u.actor_id = f.followed_id - WHERE f.follower_id = $1 - ) - UNION - SELECT p.id, p.boosted_post_id, 1 as is_boost_source - FROM post p - JOIN get_home_timeline_with_boosts tl ON tl.boosted_post_id = p.id - ) - SELECT is_boost_source, p.id as "post_id", u.id as "user_id", - p.content, p.uri as "post_uri", u.username, u.display_name, - u.actor_id, p.created_at, p.boosted_post_id, u.icon_url, u.url as "user_url" - FROM get_home_timeline_with_boosts - JOIN post p ON p.id = get_home_timeline_with_boosts.id - JOIN user u ON u.id = p.user_id; - "#, - ) - .bind(user.actor_id.0) - .fetch_all(&mut **db) - .await - .unwrap(); - - let mut out = Vec::::new(); - for record in posts.iter() { - let mut boost: Option> = None; - if let Some(ref boosted_id) = record.boosted_post_id { - let record = posts.iter().find(|p| &p.post_id == boosted_id).unwrap(); - let attachments = sqlx::query!( - "SELECT * FROM attachment WHERE post_id = ?1", - boosted_id - ) - .fetch_all(&mut **db) - .await - .unwrap() - .into_iter() - .map(|at| { - TimelineStatusAttachment { - id: ObjectUuid(at.id), - url: at.url, - ty: "image".to_string(), - description: at.alt.unwrap_or(String::new()) - } - }) - .collect::>(); - - boost = Some(Box::new(TimelineStatus { - id: record.post_id.clone(), - created_at: record.created_at.clone(), - 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_uri.clone(), - url: record.post_uri.clone(), - replies_count: 0, - reblogs_count: 0, - favourites_count: 0, - favourited: false, - reblogged: false, - reblog: boost, - muted: false, - bookmarked: false, - media_attachments: attachments, - account: CredentialAcount { - id: record.user_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.user_url.clone(), - avatar: record.icon_url.clone(), - avatar_static: record.icon_url.clone(), - header: record.icon_url.clone(), - header_static: record.icon_url.clone(), - followers_count: 1, - following_count: 1, - statuses_count: 1, - last_status_at: "2025-04-10T22:14:34Z".to_string(), - }, - })) - } - - // Don't send the empty boost source posts - if !record.is_boost_source { - out.push(TimelineStatus { - id: record.post_id.clone(), - created_at: record.created_at.clone(), - 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_uri.clone(), - url: record.post_uri.clone(), - replies_count: 0, - reblogs_count: 0, - favourites_count: 0, - favourited: false, - reblogged: false, - reblog: boost, - muted: false, - bookmarked: false, - media_attachments: vec![], - account: CredentialAcount { - id: record.user_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.user_url.clone(), - avatar: record.icon_url.clone(), - avatar_static: record.icon_url.clone(), - header: record.icon_url.clone(), - header_static: record.icon_url.clone(), - followers_count: 1, - following_count: 1, - statuses_count: 1, - last_status_at: "2025-04-10T22:14:34Z".to_string(), - }, - }); - } - } - - Json(out) + Json(posts.into_iter().map(Into::into).collect()) } diff --git a/ferri-server/src/endpoints/inbox.rs b/ferri-server/src/endpoints/inbox.rs index f712e38..0a0661b 100644 --- a/ferri-server/src/endpoints/inbox.rs +++ b/ferri-server/src/endpoints/inbox.rs @@ -5,10 +5,10 @@ use main::{ }, types::{ap, get, ObjectUuid} }; -use rocket::{State, post, serde::json::serde_json}; +use rocket::{post, response::Redirect, serde::json::serde_json, State}; use rocket_db_pools::Connection; use serde::de::DeserializeOwned; -use tracing::{debug, event, span, warn, Instrument, Level}; +use tracing::{debug, event, info, span, warn, Instrument, Level}; use crate::{Db, InboundQueue, OutboundQueue}; @@ -23,7 +23,13 @@ pub async fn inbox( outbound: &State, user_uuid: &str, body: String -) { +) -> Result<(), Redirect> { + if user_uuid == "amy" { + return Err(Redirect::permanent( + "https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9/inbox", + )); + } + let user = get::user_by_id( ObjectUuid(user_uuid.to_string()), &mut db @@ -65,6 +71,7 @@ pub async fn inbox( queue.0.send(msg).await; } ap::ActivityType::Create => { + info!("{}", body); let activity = deser::(&body); let msg = QueueMessage::Inbound( InboxRequest::Create(activity, user, conn) @@ -94,5 +101,7 @@ pub async fn inbox( } } .instrument(span) - .await; + .await; + + Ok(()) } diff --git a/ferri-server/src/endpoints/user.rs b/ferri-server/src/endpoints/user.rs index adc89a6..61ff21d 100644 --- a/ferri-server/src/endpoints/user.rs +++ b/ferri-server/src/endpoints/user.rs @@ -7,31 +7,39 @@ use rocket::{ use rocket_db_pools::Connection; use serde::{Deserialize, Serialize}; -use main::types::{Object, ObjectUri, ObjectUuid, ap, as_context, get}; +use main::types::{ap, as_context, get, Object, ObjectContext, ObjectUri, ObjectUuid}; use super::activity_type; use crate::Db; #[derive(Serialize, Deserialize)] pub struct OrderedCollection { + #[serde(rename = "@context")] + context: ObjectContext, + #[serde(rename = "type")] ty: String, + id: String, total_items: i64, ordered_items: Vec, } -#[get("/users/<_user>/inbox")] -pub async fn inbox(_user: String) -> Json { +#[get("/users//inbox")] +pub async fn inbox(user: String) -> Json { Json(OrderedCollection { + context: as_context(), ty: "OrderedCollection".to_string(), + id: format!("https://ferri.amy.mov/users/{}/inbox", user), total_items: 0, ordered_items: vec![], }) } -#[get("/users/<_user>/outbox")] -pub async fn outbox(_user: String) -> Json { +#[get("/users//outbox")] +pub async fn outbox(user: String) -> Json { Json(OrderedCollection { + context: as_context(), ty: "OrderedCollection".to_string(), + id: format!("https://ferri.amy.mov/users/{}/outbox", user), total_items: 0, ordered_items: vec![], }) @@ -41,7 +49,7 @@ pub async fn outbox(_user: String) -> Json { pub async fn followers( mut db: Connection, uuid: &str, -) -> Result, NotFound> { +) -> Result>, NotFound> { let target = main::ap::User::from_id(uuid, &mut **db) .await .map_err(|e| NotFound(e.to_string()))?; @@ -59,9 +67,11 @@ pub async fn followers( .await .unwrap(); - Ok(Json(OrderedCollection { + ap_ok(Json(OrderedCollection { + context: as_context(), ty: "OrderedCollection".to_string(), total_items: 1, + id: format!("https://ferri.amy.mov/users/{}/followers", uuid), ordered_items: followers .into_iter() .map(|f| f.follower_id) @@ -73,7 +83,7 @@ pub async fn followers( pub async fn following( mut db: Connection, uuid: &str, -) -> Result, NotFound> { +) -> Result>, NotFound> { let target = main::ap::User::from_id(uuid, &mut **db) .await .map_err(|e| NotFound(e.to_string()))?; @@ -91,9 +101,11 @@ pub async fn following( .await .unwrap(); - Ok(Json(OrderedCollection { + ap_ok(Json(OrderedCollection { + context: as_context(), ty: "OrderedCollection".to_string(), total_items: 1, + id: format!("https://ferri.amy.mov/users/{}/following", uuid), ordered_items: following .into_iter() .map(|f| f.followed_id) diff --git a/ferri-server/src/lib.rs b/ferri-server/src/lib.rs index 614061a..5b8b59e 100644 --- a/ferri-server/src/lib.rs +++ b/ferri-server/src/lib.rs @@ -5,7 +5,7 @@ use endpoints::{ use tracing_subscriber::fmt; -use main::{federation, types::{ObjectUri, ObjectUuid}}; +use main::{federation, types::{db, get, ObjectUri, ObjectUuid}}; use main::ap::http; use main::config::Config; @@ -35,10 +35,12 @@ async fn activity_endpoint(_activity: String) {} #[derive(Debug)] pub struct AuthenticatedUser { - pub username: String, pub id: ObjectUuid, - pub token: String, pub actor_id: ObjectUri, + pub user: db::User, + pub username: String, + pub token: String, + } #[derive(Debug)] @@ -70,12 +72,18 @@ impl<'a> FromRequest<'a> for AuthenticatedUser { .await; if let Ok(auth) = auth { - return Outcome::Success(AuthenticatedUser { - token: auth.token, - id: ObjectUuid(auth.id), - username: auth.display_name, - actor_id: ObjectUri(auth.actor_id), - }); + let uid = ObjectUuid(auth.id); + let user = get::user_by_id(uid.clone(), &mut **conn).await; + + if let Ok(user) = user { + return Outcome::Success(AuthenticatedUser { + id: uid, + actor_id: ObjectUri(auth.actor_id), + user, + token: auth.token, + username: auth.display_name, + }); + } } }