diff --git a/ferri-server/src/endpoints/api/status.rs b/ferri-server/src/endpoints/api/status.rs index c0e6898..3cf1eb2 100644 --- a/ferri-server/src/endpoints/api/status.rs +++ b/ferri-server/src/endpoints/api/status.rs @@ -111,6 +111,7 @@ async fn create_status( favourites_count: 0, favourited: false, reblogged: false, + reblog: None, muted: false, bookmarked: false, media_attachments: vec![], diff --git a/ferri-server/src/endpoints/api/timeline.rs b/ferri-server/src/endpoints/api/timeline.rs index ae1f771..c81e387 100644 --- a/ferri-server/src/endpoints/api/timeline.rs +++ b/ferri-server/src/endpoints/api/timeline.rs @@ -27,6 +27,7 @@ pub struct TimelineStatus { pub reblogged: bool, pub muted: bool, pub bookmarked: bool, + pub reblog: Option>, pub media_attachments: Vec<()>, pub account: TimelineAccount, } @@ -40,7 +41,7 @@ pub async fn home( let posts = sqlx::query!( r#" SELECT 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 + u.username, u.display_name, u.actor_id, p.created_at, p.boosted_post_id FROM post p INNER JOIN user u on p.user_id = u.id "# @@ -51,6 +52,64 @@ pub async fn home( let mut out = Vec::::new(); for record in posts { + let mut boost: Option> = None; + if let Some(boosted_id) = record.boosted_post_id { + let record = sqlx::query!( + r#" + SELECT 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 + FROM post p + INNER JOIN user u on p.user_id = u.id + WHERE p.id = ?1 + "#, boosted_id) + .fetch_one(&mut **db) + .await + .unwrap(); + + let user_uri = format!("https://ferri.amy.mov/users/{}", record.user_id); + 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: 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: 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(), + }, + })) + } + let user_uri = format!("https://ferri.amy.mov/users/{}", record.username); out.push(TimelineStatus { id: record.post_id.clone(), @@ -68,6 +127,7 @@ pub async fn home( favourites_count: 0, favourited: false, reblogged: false, + reblog: boost, muted: false, bookmarked: false, media_attachments: vec![], diff --git a/ferri-server/src/endpoints/api/user.rs b/ferri-server/src/endpoints/api/user.rs index a6be6aa..2a686d4 100644 --- a/ferri-server/src/endpoints/api/user.rs +++ b/ferri-server/src/endpoints/api/user.rs @@ -165,6 +165,7 @@ pub async fn statuses( favourites_count: 0, favourited: false, reblogged: false, + reblog: None, muted: false, bookmarked: false, media_attachments: vec![], diff --git a/ferri-server/src/endpoints/custom.rs b/ferri-server/src/endpoints/custom.rs index ff3bea8..9bf268f 100644 --- a/ferri-server/src/endpoints/custom.rs +++ b/ferri-server/src/endpoints/custom.rs @@ -99,6 +99,7 @@ pub async fn test(http: &State) -> &'static str { ts: "2025-04-10T10:48:11Z".to_string(), to: vec!["https://ferri.amy.mov/users/amy/followers".to_string()], cc: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()], + attributed_to: None }, ts: "2025-04-10T10:48:11Z".to_string(), to: vec!["https://ferri.amy.mov/users/amy/followers".to_string()], diff --git a/ferri-server/src/endpoints/inbox.rs b/ferri-server/src/endpoints/inbox.rs index 5d1972a..bb95091 100644 --- a/ferri-server/src/endpoints/inbox.rs +++ b/ferri-server/src/endpoints/inbox.rs @@ -11,7 +11,7 @@ use tracing::{event, span, Level, debug, warn, info}; use crate::{ Db, http::HttpClient, - types::{Person, activity}, + types::{Person, content::Post, activity}, }; fn handle_delete_activity(activity: activity::DeleteActivity) { @@ -142,6 +142,92 @@ async fn handle_like_activity(activity: activity::LikeActivity, mut db: Connecti } } +async fn handle_boost_activity( + activity: activity::BoostActivity, + http: &HttpClient, + mut db: Connection, +) { + let key_id = "https://ferri.amy.mov/users/amy#main-key"; + dbg!(&activity); + let post = http + .get(&activity.object) + .activity() + .sign(&key_id) + .send() + .await + .unwrap() + .json::() + .await + .unwrap(); + + dbg!(&post); + let attribution = post.attributed_to.unwrap(); + let post_user = http + .get(&attribution) + .activity() + .sign(&key_id) + .send() + .await + .unwrap() + .json::() + .await + .unwrap(); + + let user = http + .get(&activity.actor) + .activity() + .sign(&key_id) + .send() + .await + .unwrap() + .json::() + .await + .unwrap(); + + dbg!(&post_user); + + debug!("creating actor {}", activity.actor); + create_actor(&user, &activity.actor, &mut **db).await; + + debug!("creating user {}", activity.actor); + create_user(&user, &activity.actor, &mut **db).await; + + debug!("creating actor {}", attribution); + create_actor(&post_user, &attribution, &mut **db).await; + + debug!("creating user {}", attribution); + create_user(&post_user, &attribution, &mut **db).await; + + let attributed_user = ap::User::from_actor_id(&attribution, &mut **db).await; + let actor_user = ap::User::from_actor_id(&activity.actor, &mut **db).await; + + let base_id = ap::new_id(); + let now = ap::new_ts(); + + let reblog_id = ap::new_id(); + + let attr_id = attributed_user.id(); + sqlx::query!(" + INSERT INTO post (id, uri, user_id, content, created_at) + VALUES (?1, ?2, ?3, ?4, ?5) + ", reblog_id, post.id, attr_id, post.content, post.ts) + .execute(&mut **db) + .await + .unwrap(); + + let uri = format!("https://ferri.amy.mov/users/{}/posts/{}", actor_user.id(), post.id); + let user_id = actor_user.id(); + + sqlx::query!(" + INSERT INTO post (id, uri, user_id, content, created_at, boosted_post_id) + VALUES (?1, ?2, ?3, ?4, ?5, ?6) + ", base_id, uri, user_id, "", now, reblog_id) + .execute(&mut **db) + .await + .unwrap(); + +} + async fn handle_create_activity( activity: activity::CreateActivity, http: &HttpClient, @@ -218,6 +304,11 @@ pub async fn inbox(db: Connection, http: &State, user: &str, bod let activity = serde_json::from_str::(&body).unwrap(); handle_like_activity(activity, db).await; } + "Announce" => { + let activity = serde_json::from_str::(&body).unwrap(); + handle_boost_activity(activity, http.inner(), db).await; + } + act => { warn!(act, body, "unknown activity"); } diff --git a/ferri-server/src/endpoints/user.rs b/ferri-server/src/endpoints/user.rs index 3963926..a205e60 100644 --- a/ferri-server/src/endpoints/user.rs +++ b/ferri-server/src/endpoints/user.rs @@ -107,6 +107,7 @@ pub async fn post( Json(content::Post { context: "https://www.w3.org/ns/activitystreams".to_string(), id: format!("https://ferri.amy.mov/users/{}/posts/{}", uuid, post.id), + attributed_to: Some(format!("https://ferri.amy.mov/users/{}/posts/{}", uuid, post.id)), ty: "Note".to_string(), content: post.content, ts: post.created_at, diff --git a/ferri-server/src/types/activity.rs b/ferri-server/src/types/activity.rs index 76b9acc..29b0151 100644 --- a/ferri-server/src/types/activity.rs +++ b/ferri-server/src/types/activity.rs @@ -1,5 +1,4 @@ use rocket::serde::{Deserialize, Serialize}; - use crate::types::content::Post; #[derive(Serialize, Deserialize, Debug)] @@ -60,3 +59,17 @@ pub struct AcceptActivity { pub object: String, pub actor: String, } + +#[derive(Serialize, Deserialize, Debug)] +#[serde(crate = "rocket::serde")] +pub struct BoostActivity { + #[serde(rename = "type")] + pub ty: String, + + pub id: String, + pub actor: String, + pub published: String, + pub to: Vec, + pub cc: Vec, + pub object: String, +} diff --git a/ferri-server/src/types/content.rs b/ferri-server/src/types/content.rs index c6da908..e35c5d9 100644 --- a/ferri-server/src/types/content.rs +++ b/ferri-server/src/types/content.rs @@ -15,4 +15,7 @@ pub struct Post { pub content: String, pub to: Vec, pub cc: Vec, + + #[serde(rename = "attributedTo")] + pub attributed_to: Option } diff --git a/ferri-server/src/types/mod.rs b/ferri-server/src/types/mod.rs index 65ed38e..7767a8d 100644 --- a/ferri-server/src/types/mod.rs +++ b/ferri-server/src/types/mod.rs @@ -34,6 +34,7 @@ pub struct Person { pub inbox: String, pub outbox: String, pub preferred_username: String, + #[serde(default)] pub name: String, pub public_key: Option, } diff --git a/migrations/20250410182845_add_post.sql b/migrations/20250410182845_add_post.sql index 96088c8..04c66bd 100644 --- a/migrations/20250410182845_add_post.sql +++ b/migrations/20250410182845_add_post.sql @@ -5,6 +5,8 @@ CREATE TABLE IF NOT EXISTS post user_id TEXT NOT NULL, content TEXT NOT NULL, created_at TEXT NOT NULL, + boosted_post_id TEXT, - FOREIGN KEY(user_id) REFERENCES user(id) + FOREIGN KEY(user_id) REFERENCES user(id), + FOREIGN KEY(boosted_post_id) REFERENCES post(id) );