diff --git a/Cargo.lock b/Cargo.lock index fe3d61d..fc47954 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2162,10 +2162,7 @@ dependencies = [ "reqwest", "rocket", "rocket_db_pools", - "serde", - "serde_json", "sqlx", - "thiserror 2.0.12", "tracing", "tracing-subscriber", "url", diff --git a/Cargo.toml b/Cargo.toml index 94415ed..0f7cb23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,6 @@ uuid = { version = "1.16.0", features = ["v4"] } chrono = "0.4.40" rand = "0.8" thiserror = "2.0.12" -serde_json = "1.0.140" tracing = "0.1.40" tracing-appender = "0.2.3" diff --git a/ferri-main/Cargo.toml b/ferri-main/Cargo.toml index 77dc8d9..cca1ac5 100644 --- a/ferri-main/Cargo.toml +++ b/ferri-main/Cargo.toml @@ -12,8 +12,8 @@ uuid = { workspace = true } rand = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } -serde_json = { workspace = true } base64 = "0.22.1" rsa = { version = "0.9.8", features = ["sha2"] } url = "2.5.4" +serde_json = "1.0.140" \ No newline at end of file diff --git a/ferri-main/src/ap/mod.rs b/ferri-main/src/ap/mod.rs index 98d7330..f86ba50 100644 --- a/ferri-main/src/ap/mod.rs +++ b/ferri-main/src/ap/mod.rs @@ -12,9 +12,6 @@ pub use user::*; mod post; pub use post::*; -mod request_queue; -pub use request_queue::*; - pub const AS_CONTEXT: &str = "https://www.w3.org/ns/activitystreams"; pub fn new_id() -> String { diff --git a/ferri-main/src/ap/post.rs b/ferri-main/src/ap/post.rs index a42a246..53c33e0 100644 --- a/ferri-main/src/ap/post.rs +++ b/ferri-main/src/ap/post.rs @@ -3,7 +3,7 @@ use chrono::{DateTime, Utc}; use serde::Serialize; use sqlx::Sqlite; -const POST_TYPE: &str = "Note"; +const POST_TYPE: &str = "Post"; #[derive(Clone)] pub struct Post { diff --git a/ferri-main/src/ap/request_queue.rs b/ferri-main/src/ap/request_queue.rs deleted file mode 100644 index 2a230df..0000000 --- a/ferri-main/src/ap/request_queue.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::sync::mpsc; -use std::thread; -use tracing::{info, span, Level}; - -#[derive(Debug)] -pub enum QueueMessage { - Heartbeat -} - -pub struct RequestQueue { - name: &'static str, - send: mpsc::Sender, - recv: mpsc::Receiver -} - -#[derive(Clone)] -pub struct QueueHandle { - send: mpsc::Sender -} - -impl QueueHandle { - pub fn send(&self, msg: QueueMessage) { - self.send.send(msg).unwrap(); - } -} - -impl RequestQueue { - pub fn new(name: &'static str) -> Self { - let (send, recv) = mpsc::channel(); - Self { - name, - send, - recv - } - } - - pub fn spawn(self) -> QueueHandle { - info!("starting up queue '{}'", self.name); - - thread::spawn(move || { - info!("queue '{}' up", self.name); - let recv = self.recv; - - while let Ok(req) = recv.recv() { - // FIXME: When we make this do async things we will need to add tokio and - // use proper async handled spans as the enter/drop won't work. - // See inbox.rs for how we handle that. - let s = span!(Level::INFO, "queue", queue_name = self.name); - let _enter = s.enter(); - - info!(?req, "got a message into the queue"); - - drop(_enter); - } - }); - - QueueHandle { send: self.send } - } -} diff --git a/ferri-main/src/ap/user.rs b/ferri-main/src/ap/user.rs index 361d0d8..b080181 100644 --- a/ferri-main/src/ap/user.rs +++ b/ferri-main/src/ap/user.rs @@ -106,10 +106,9 @@ impl User { "#, username ) - .fetch_one(conn) - .await - .unwrap(); - + .fetch_one(conn) + .await + .unwrap(); User { id: user.id, username: user.username, diff --git a/ferri-main/src/config/mod.rs b/ferri-main/src/config/mod.rs index 79305d2..5af1eef 100644 --- a/ferri-main/src/config/mod.rs +++ b/ferri-main/src/config/mod.rs @@ -9,41 +9,3 @@ pub struct ServerConfig { pub struct Config { pub server: ServerConfig, } - -impl Config { - pub fn host(&self) -> &str { - &self.server.host - } - - pub fn user_url(&self, user_uuid: &str) -> String { - format!("{}/users/{}", self.host(), user_uuid) - } - - pub fn user_web_url(&self, user_name: &str) -> String { - format!("{}/{}", self.host(), user_name) - } - - pub fn followers_url(&self, user_uuid: &str) -> String { - format!("{}/followers", self.user_url(user_uuid)) - } - - pub fn following_url(&self, user_uuid: &str) -> String { - format!("{}/following", self.user_url(user_uuid)) - } - - pub fn inbox_url(&self, user_uuid: &str) -> String { - format!("{}/inbox", self.user_url(user_uuid)) - } - - pub fn outbox_url(&self, user_uuid: &str) -> String { - format!("{}/outbox", self.user_url(user_uuid)) - } - - pub fn post_url(&self, poster_uuid: &str, post_uuid: &str) -> String { - format!("{}/{}", self.user_url(poster_uuid), post_uuid) - } - - pub fn activity_url(&self, activity_uuid: &str) -> String { - format!("{}/activities/{}", self.host(), activity_uuid) - } -} diff --git a/ferri-main/src/lib.rs b/ferri-main/src/lib.rs index 31cafd7..620300a 100644 --- a/ferri-main/src/lib.rs +++ b/ferri-main/src/lib.rs @@ -1,7 +1,5 @@ pub mod ap; pub mod config; -mod types_rewrite; - use rand::{Rng, distributions::Alphanumeric}; pub fn gen_token(len: usize) -> String { diff --git a/ferri-main/src/types_rewrite/convert.rs b/ferri-main/src/types_rewrite/convert.rs deleted file mode 100644 index 16ed63c..0000000 --- a/ferri-main/src/types_rewrite/convert.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::types_rewrite::api; -use crate::types_rewrite::ap; -use crate::types_rewrite::db; - -use crate::types_rewrite::{Object, as_context}; - -impl From for ap::Actor { - fn from(val: db::Actor) -> ap::Actor { - ap::Actor { - obj: Object { - context: as_context(), - id: val.id - }, - inbox: val.inbox, - outbox: val.outbox - } - } -} - -impl From for db::Actor { - fn from(val: ap::Actor) -> db::Actor { - db::Actor { - id: val.obj.id, - inbox: val.inbox, - outbox: val.outbox - } - } -} diff --git a/ferri-main/src/types_rewrite/mod.rs b/ferri-main/src/types_rewrite/mod.rs deleted file mode 100644 index b1baa5a..0000000 --- a/ferri-main/src/types_rewrite/mod.rs +++ /dev/null @@ -1,87 +0,0 @@ -use serde::{Serialize, Deserialize}; - -mod convert; -pub use convert::*; - -pub const AS_CONTEXT_RAW: &'static str = "https://www.w3.org/ns/activitystreams"; -pub fn as_context() -> ObjectContext { - ObjectContext::Str(AS_CONTEXT_RAW.to_string()) -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(untagged)] -pub enum ObjectContext { - Str(String), - Vec(Vec), -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ObjectUri(String); - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct Object { - #[serde(rename = "@context")] - context: ObjectContext, - id: ObjectUri, -} - -pub mod db { - use serde::{Serialize, Deserialize}; - use super::*; - - #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] - pub struct Actor { - pub id: ObjectUri, - pub inbox: String, - pub outbox: String, - } -} - -pub mod ap { - use serde::{Serialize, Deserialize}; - use super::*; - - #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] - pub struct Actor { - #[serde(flatten)] - pub obj: Object, - - pub inbox: String, - pub outbox: String, - } -} - -pub mod api { - use serde::{Serialize, Deserialize}; - use super::*; - - // API will not really use actors so treat them as DB actors - // until we require specificity - pub type Actor = db::Actor; -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn ap_actor_to_db() { - let domain = "https://example.com"; - let ap = ap::Actor { - obj: Object { - context: as_context(), - id: ObjectUri(format!("{}/users/sample", domain)), - }, - inbox: format!("{}/users/sample/inbox", domain), - outbox: format!("{}/users/sample/outbox", domain), - }; - - let db: db::Actor = ap.into(); - - assert_eq!(db, db::Actor { - id: ObjectUri("https://example.com/users/sample".to_string()), - inbox: "https://example.com/users/sample/inbox".to_string(), - outbox: "https://example.com/users/sample/outbox".to_string(), - }); - } -} diff --git a/ferri-server/Cargo.toml b/ferri-server/Cargo.toml index a378685..91b72fe 100644 --- a/ferri-server/Cargo.toml +++ b/ferri-server/Cargo.toml @@ -15,7 +15,4 @@ uuid = { workspace = true } chrono = { workspace = true } rand = { workspace = true } tracing = { workspace = true } -tracing-subscriber = { workspace = true } -thiserror = { workspace = true } -serde_json = { workspace = true } -serde = { workspace = true } \ No newline at end of file +tracing-subscriber = { workspace = true } \ No newline at end of file diff --git a/ferri-server/src/endpoints/api/instance.rs b/ferri-server/src/endpoints/api/instance.rs index 9fab90f..0a76ba6 100644 --- a/ferri-server/src/endpoints/api/instance.rs +++ b/ferri-server/src/endpoints/api/instance.rs @@ -1,6 +1,4 @@ -use rocket::{get, serde::json::Json, State}; - -use crate::Config; +use rocket::{get, serde::json::Json}; use crate::types::instance::{ Accounts, Configuration, Contact, Instance, MediaAttachments, Polls, Registrations, Statuses, @@ -8,9 +6,9 @@ use crate::types::instance::{ }; #[get("/instance")] -pub async fn instance(config: &State) -> Json { +pub async fn instance() -> Json { Json(Instance { - domain: config.host().to_string(), + domain: "ferri.amy.mov".to_string(), title: "Ferri".to_string(), version: "0.0.1".to_string(), source_url: "https://forge.amy.mov/amy/ferri".to_string(), diff --git a/ferri-server/src/endpoints/api/status.rs b/ferri-server/src/endpoints/api/status.rs index ca74a07..c0e6898 100644 --- a/ferri-server/src/endpoints/api/status.rs +++ b/ferri-server/src/endpoints/api/status.rs @@ -26,11 +26,11 @@ pub struct StatusContext { descendants: Vec } -#[get("/statuses/<_status>/context")] +#[get("/statuses//context")] pub async fn status_context( - _status: &str, - _user: AuthenticatedUser, - _db: Connection + status: &str, + user: AuthenticatedUser, + mut db: Connection ) -> Json { Json(StatusContext { ancestors: vec![], @@ -56,37 +56,44 @@ async fn create_status( 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) + let actors = sqlx::query!("SELECT * FROM actor") + .fetch_all(&mut **db) .await .unwrap(); - let create_id = format!("https://ferri.amy.mov/activities/{}", Uuid::new_v4()); + for record in actors { + // Don't send to ourselves + if record.id == user.actor_id() { + continue; + } - 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 create_id = format!("https://ferri.amy.mov/activities/{}", Uuid::new_v4()); - let actor = ap::Actor::from_raw( - actor.id.clone(), - actor.inbox.clone(), - actor.outbox.clone(), - ); + 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 req = ap::OutgoingActivity { - req: activity, - signed_by: format!("{}#main-key", user.uri()), - to: actor, - }; + let actor = ap::Actor::from_raw( + record.id.clone(), + record.inbox.clone(), + record.outbox.clone(), + ); - req.save(&mut **db).await; - outbox.post(req).await; + 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(), @@ -104,7 +111,6 @@ 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 04920e1..ae1f771 100644 --- a/ferri-server/src/endpoints/api/timeline.rs +++ b/ferri-server/src/endpoints/api/timeline.rs @@ -1,6 +1,5 @@ -use crate::{AuthenticatedUser, Db, endpoints::api::user::CredentialAcount, Config}; +use crate::{AuthenticatedUser, Db, endpoints::api::user::CredentialAcount}; use rocket::{ - State, get, serde::{Deserialize, Serialize, json::Json}, }; @@ -28,21 +27,20 @@ pub struct TimelineStatus { pub reblogged: bool, pub muted: bool, pub bookmarked: bool, - pub reblog: Option>, pub media_attachments: Vec<()>, pub account: TimelineAccount, } -#[get("/timelines/home")] +#[get("/timelines/home?")] pub async fn home( mut db: Connection, - config: &State, - _user: AuthenticatedUser, + limit: i64, + user: AuthenticatedUser, ) -> Json> { 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, p.boosted_post_id + u.username, u.display_name, u.actor_id, p.created_at FROM post p INNER JOIN user u on p.user_id = u.id "# @@ -53,65 +51,7 @@ 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 = config.user_url(&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 = config.user_web_url(&record.username); + let user_uri = format!("https://ferri.amy.mov/users/{}", record.username); out.push(TimelineStatus { id: record.post_id.clone(), created_at: record.created_at.clone(), @@ -128,7 +68,6 @@ 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 38428e2..a6be6aa 100644 --- a/ferri-server/src/endpoints/api/user.rs +++ b/ferri-server/src/endpoints/api/user.rs @@ -95,7 +95,7 @@ pub async fn new_follow( pub async fn account( mut db: Connection, uuid: &str, - _user: AuthenticatedUser, + user: AuthenticatedUser, ) -> Result, NotFound> { let user = ap::User::from_id(uuid, &mut **db) .await @@ -123,12 +123,12 @@ pub async fn account( })) } -#[get("/accounts//statuses?<_limit>")] +#[get("/accounts//statuses?")] pub async fn statuses( mut db: Connection, uuid: &str, - _limit: Option, - _user: AuthenticatedUser, + limit: Option, + user: AuthenticatedUser, ) -> Result>, NotFound> { let user = ap::User::from_id(uuid, &mut **db) .await @@ -165,7 +165,6 @@ 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 8d7c57f..ff3bea8 100644 --- a/ferri-server/src/endpoints/custom.rs +++ b/ferri-server/src/endpoints/custom.rs @@ -1,8 +1,6 @@ use main::ap::http::HttpClient; use rocket::{State, get, response::status}; use rocket_db_pools::Connection; -use main::ap; -use crate::OutboundQueue; use uuid::Uuid; @@ -86,9 +84,7 @@ pub async fn resolve_user(acct: &str, host: &str) -> types::Person { } #[get("/test")] -pub async fn test(http: &State, outbound: &State) -> &'static str { - outbound.0.send(ap::QueueMessage::Heartbeat); - +pub async fn test(http: &State) -> &'static str { let user = resolve_user("amy@fedi.amy.mov", "fedi.amy.mov").await; let post = activity::CreateActivity { @@ -103,7 +99,6 @@ pub async fn test(http: &State, outbound: &State) -> 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()], @@ -123,7 +118,5 @@ pub async fn test(http: &State, outbound: &State) -> .await .unwrap(); - dbg!(follow); - "Hello, world!" } diff --git a/ferri-server/src/endpoints/inbox.rs b/ferri-server/src/endpoints/inbox.rs index 924ae61..5d1972a 100644 --- a/ferri-server/src/endpoints/inbox.rs +++ b/ferri-server/src/endpoints/inbox.rs @@ -1,5 +1,4 @@ use chrono::Local; -use tracing::Instrument; use main::ap; use rocket::serde::json::serde_json; use rocket::{State, post}; @@ -7,13 +6,12 @@ use rocket_db_pools::Connection; use sqlx::Sqlite; use url::Url; use uuid::Uuid; -use tracing::{event, span, Level, debug, warn, info, error}; -use crate::http_wrapper::HttpWrapper; +use tracing::{event, span, Level, debug, warn, info}; use crate::{ Db, http::HttpClient, - types::{Person, content::Post, activity}, + types::{Person, activity}, }; fn handle_delete_activity(activity: activity::DeleteActivity) { @@ -48,9 +46,7 @@ async fn create_user( // 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(); - info!("creating user '{}'@'{}' ({:#?})", user.preferred_username, host, user); - - let username = format!("{}@{}", user.preferred_username, host); + let username = format!("{}@{}", user.name, host); let uuid = Uuid::new_v4().to_string(); sqlx::query!( @@ -62,7 +58,7 @@ async fn create_user( uuid, username, actor, - user.name + user.preferred_username ) .execute(conn) .await @@ -88,27 +84,29 @@ async fn create_follow( .unwrap(); } -async fn handle_follow_activity<'a>( +async fn handle_follow_activity( followed_account: &str, activity: activity::FollowActivity, - http: HttpWrapper<'a>, + http: &HttpClient, mut db: Connection, ) { - let user = http.get_person(&activity.actor).await; - if let Err(e) = user { - error!("could not load user {}: {}", activity.actor, e.to_string()); - return - } - - let user = user.unwrap(); + let user = http + .get(&activity.actor) + .activity() + .send() + .await + .unwrap() + .json::() + .await + .unwrap(); create_actor(&user, &activity.actor, &mut **db).await; create_user(&user, &activity.actor, &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_id(&followed_account, &mut **db).await.unwrap(); - let outbox = ap::Outbox::for_user(followed.clone(), http.client()); + 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()), @@ -144,109 +142,23 @@ async fn handle_like_activity(activity: activity::LikeActivity, mut db: Connecti } } -async fn handle_boost_activity<'a>( - activity: activity::BoostActivity, - http: HttpWrapper<'a>, - mut db: Connection, -) { - let key_id = "https://ferri.amy.mov/users/amy#main-key"; - dbg!(&activity); - let post = http - .client() - .get(&activity.object) - .activity() - .sign(&key_id) - .send() - .await - .unwrap() - .text() - .await - .unwrap(); - - info!("{}", post); - - let post = serde_json::from_str::(&post); - if let Err(e) = post { - error!(?e, "when decoding post"); - return - } - - let post = post.unwrap(); - - info!("{:#?}", post); - let attribution = post.attributed_to.unwrap(); - - let post_user = http.get_person(&attribution).await; - if let Err(e) = post_user { - error!("could not load post_user {}: {}", attribution, e.to_string()); - return - } - let post_user = post_user.unwrap(); - - let user = http.get_person(&activity.actor).await; - if let Err(e) = user { - error!("could not load actor {}: {}", activity.actor, e.to_string()); - return - } - let user = user.unwrap(); - - 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<'a>( +async fn handle_create_activity( activity: activity::CreateActivity, - http: HttpWrapper<'a>, + http: &HttpClient, mut db: Connection, ) { assert!(&activity.object.ty == "Note"); debug!("resolving user {}", activity.actor); - let user = http.get_person(&activity.actor).await; - if let Err(e) = user { - error!("could not load user {}: {}", activity.actor, e.to_string()); - return - } - - let user = user.unwrap(); + let user = http + .get(&activity.actor) + .activity() + .send() + .await + .unwrap() + .json::() + .await + .unwrap(); debug!("creating actor {}", activity.actor); create_actor(&user, &activity.actor, &mut **db).await; @@ -286,42 +198,31 @@ pub async fn inbox(db: Connection, http: &State, user: &str, bod let min = serde_json::from_str::(&body).unwrap(); let inbox_span = span!(Level::INFO, "inbox-post", user_id = user); - async move { - event!(Level::INFO, ?min, "received an activity"); - - let key_id = "https://ferri.amy.mov/users/amy#main-key"; - let wrapper = HttpWrapper::new(http.inner(), key_id); - - 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, wrapper, db).await; - } - "Create" => { - let activity = serde_json::from_str::(&body).unwrap(); - handle_create_activity(activity, wrapper, db).await; - } - "Like" => { - 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, wrapper, db).await; - } - - act => { - warn!(act, body, "unknown activity"); - } + let _enter = inbox_span.enter(); + event!(Level::INFO, ?min, "received an activity"); + + 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; + } + "Like" => { + let activity = serde_json::from_str::(&body).unwrap(); + handle_like_activity(activity, db).await; + } + act => { + warn!(act, body, "unknown activity"); } - - debug!("body in inbox: {}", body); } - // Allow the span to be used inside the async code - // https://docs.rs/tracing/latest/tracing/span/struct.EnteredSpan.html#deref-methods-Span - .instrument(inbox_span).await; + + debug!("body in inbox: {}", body); + drop(_enter) } diff --git a/ferri-server/src/endpoints/oauth.rs b/ferri-server/src/endpoints/oauth.rs index ae897fa..81c8d55 100644 --- a/ferri-server/src/endpoints/oauth.rs +++ b/ferri-server/src/endpoints/oauth.rs @@ -8,12 +8,12 @@ use rocket::{ }; use rocket_db_pools::Connection; -#[get("/oauth/authorize?&&&<_response_type>")] +#[get("/oauth/authorize?&&&")] pub async fn authorize( client_id: &str, scope: &str, redirect_uri: &str, - _response_type: &str, + response_type: &str, mut db: Connection, ) -> Redirect { // For now, we will always authorize the request and assign it to an admin user @@ -68,11 +68,11 @@ pub struct Token { #[derive(Deserialize, Debug, FromForm)] #[serde(crate = "rocket::serde")] pub struct NewTokenRequest { - // pub client_id: String, - // pub redirect_uri: String, - // pub grant_type: String, - pub code: String, - // pub client_secret: String, + client_id: String, + redirect_uri: String, + grant_type: String, + code: String, + client_secret: String, } #[post("/oauth/token", data = "")] diff --git a/ferri-server/src/endpoints/user.rs b/ferri-server/src/endpoints/user.rs index ce24ab5..3963926 100644 --- a/ferri-server/src/endpoints/user.rs +++ b/ferri-server/src/endpoints/user.rs @@ -1,19 +1,17 @@ use main::ap; -use rocket::{get, http::ContentType, serde::json::Json, State, Responder}; +use rocket::{get, http::ContentType, serde::json::Json}; use rocket_db_pools::Connection; -use rocket::response::Redirect; use rocket::response::status::NotFound; use crate::{ - Config, Db, types::{OrderedCollection, Person, UserKey, content}, }; use super::activity_type; -#[get("/users/<_user>/inbox")] -pub async fn inbox(_user: String) -> Json { +#[get("/users//inbox")] +pub async fn inbox(user: String) -> Json { Json(OrderedCollection { ty: "OrderedCollection".to_string(), total_items: 0, @@ -21,8 +19,8 @@ pub async fn inbox(_user: String) -> Json { }) } -#[get("/users/<_user>/outbox")] -pub async fn outbox(_user: String) -> Json { +#[get("/users//outbox")] +pub async fn outbox(user: String) -> Json { Json(OrderedCollection { ty: "OrderedCollection".to_string(), total_items: 0, @@ -91,7 +89,6 @@ pub async fn following(mut db: Connection, uuid: &str) -> Result/posts/")] pub async fn post( mut db: Connection, - config: &State, uuid: &str, post: String, ) -> (ContentType, Json) { @@ -109,67 +106,40 @@ pub async fn post( activity_type(), Json(content::Post { context: "https://www.w3.org/ns/activitystreams".to_string(), - id: config.post_url(uuid, &post.id), - attributed_to: Some(config.user_url(uuid)), + id: format!("https://ferri.amy.mov/users/{}/posts/{}", uuid, post.id), ty: "Note".to_string(), content: post.content, ts: post.created_at, - to: vec![config.followers_url(uuid)], + to: vec!["https://ferri.amy.mov/users/amy/followers".to_string()], cc: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()], }), ) } -#[derive(Debug, Responder)] -pub enum UserFetchError { - NotFound(NotFound), - Moved(Redirect), -} - -type ActivityResponse = (ContentType, T); -fn ap_response(t: T) -> ActivityResponse { - (activity_type(), t) -} - -fn ap_ok(t: T) -> Result, E> { - Ok(ap_response(t)) -} - #[get("/users/")] -pub async fn user( - mut db: Connection, - config: &State, - uuid: &str -) -> Result>, UserFetchError> { - if uuid == "amy" { - return Err( - UserFetchError::Moved( - Redirect::permanent("https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9") - ) - ) - } - +pub async fn user(mut db: Connection, uuid: &str) -> Result<(ContentType, Json), NotFound> { let user = ap::User::from_id(uuid, &mut **db) .await - .map_err(|e| UserFetchError::NotFound(NotFound(e.to_string())))?; + .map_err(|e| NotFound(e.to_string()))?; - let person = Person { - context: "https://www.w3.org/ns/activitystreams".to_string(), - ty: "Person".to_string(), - id: config.user_url(user.id()), - name: user.username().to_string(), - preferred_username: user.display_name().to_string(), - followers: config.followers_url(user.id()), - following: config.following_url(user.id()), - summary: format!("ferri {}", user.username()), - inbox: config.inbox_url(user.id()), - outbox: config.outbox_url(user.id()), - public_key: Some(UserKey { - id: format!("https://ferri.amy.mov/users/{}#main-key", uuid), - owner: config.user_url(user.id()), - public_key: include_str!("../../../public.pem").to_string(), + Ok(( + activity_type(), + Json(Person { + context: "https://www.w3.org/ns/activitystreams".to_string(), + ty: "Person".to_string(), + id: format!("https://ferri.amy.mov/users/{}", user.id()), + name: user.username().to_string(), + preferred_username: user.display_name().to_string(), + followers: format!("https://ferri.amy.mov/users/{}/followers", uuid), + following: format!("https://ferri.amy.mov/users/{}/following", uuid), + summary: format!("ferri {}", user.username()), + inbox: format!("https://ferri.amy.mov/users/{}/inbox", uuid), + outbox: format!("https://ferri.amy.mov/users/{}/outbox", uuid), + public_key: Some(UserKey { + id: format!("https://ferri.amy.mov/users/{}#main-key", uuid), + owner: format!("https://ferri.amy.mov/users/{}", uuid), + public_key: include_str!("../../../public.pem").to_string(), + }), }), - }; - - ap_ok(Json(person)) + )) } diff --git a/ferri-server/src/endpoints/well_known.rs b/ferri-server/src/endpoints/well_known.rs index fe03322..9692b61 100644 --- a/ferri-server/src/endpoints/well_known.rs +++ b/ferri-server/src/endpoints/well_known.rs @@ -1,10 +1,9 @@ use main::ap; -use rocket::{get, serde::json::Json, State}; +use rocket::{get, serde::json::Json}; use rocket_db_pools::Connection; use tracing::info; use crate::{ - Config, Db, types::webfinger::{Link, WebfingerResponse}, }; @@ -21,7 +20,7 @@ pub async fn host_meta() -> &'static str { // https://mastodon.social/.well-known/webfinger?resource=acct:gargron@mastodon.social #[get("/.well-known/webfinger?")] -pub async fn webfinger(mut db: Connection, config: &State, resource: &str) -> Json { +pub async fn webfinger(mut db: Connection, resource: &str) -> Json { info!(?resource, "incoming webfinger request"); let acct = resource.strip_prefix("acct:").unwrap(); @@ -31,19 +30,19 @@ pub async fn webfinger(mut db: Connection, config: &State, resource: Json(WebfingerResponse { subject: resource.to_string(), aliases: vec![ - config.user_url(user.id()), - config.user_web_url(user.username()) + format!("https://ferri.amy.mov/users/{}", user.id()), + format!("https://ferri.amy.mov/{}", user.username()), ], links: vec![ Link { rel: "http://webfinger.net/rel/profile-page".to_string(), ty: Some("text/html".to_string()), - href: Some(config.user_web_url(user.username())), + href: Some(format!("https://ferri.amy.mov/{}", user.username())), }, Link { rel: "self".to_string(), ty: Some("application/activity+json".to_string()), - href: Some(config.user_url(user.id())), + href: Some(format!("https://ferri.amy.mov/users/{}", user.id())), }, ], }) diff --git a/ferri-server/src/http_wrapper.rs b/ferri-server/src/http_wrapper.rs deleted file mode 100644 index 0470b3f..0000000 --- a/ferri-server/src/http_wrapper.rs +++ /dev/null @@ -1,71 +0,0 @@ -use thiserror::Error; -use tracing::{error, event, Level}; -use crate::http::HttpClient; -use crate::types::Person; -use std::fmt::Debug; - -pub struct HttpWrapper<'a> { - client: &'a HttpClient, - key_id: &'a str -} - -#[derive(Error, Debug)] -pub enum HttpError { - #[error("entity of type `{0}` @ URL `{1}` could not be loaded")] - LoadFailure(String, String), - #[error("entity of type `{0}` @ URL `{1}` could not be parsed ({2})")] - ParseFailure(String, String, String), -} - -impl <'a> HttpWrapper<'a> { - pub fn new(client: &'a HttpClient, key_id: &'a str) -> HttpWrapper<'a> { - Self { - client, - key_id - } - } - - pub fn client(&self) -> &'a HttpClient { - &self.client - } - - async fn get(&self, ty: &str, url: &str) -> Result { - let ty = ty.to_string(); - event!(Level::INFO, url, "loading {}", ty); - - let http_result = self.client - .get(url) - .sign(self.key_id) - .activity() - .send() - .await; - - if let Err(e) = http_result { - error!("could not load url {}: {:#?}", url, e); - return Err(HttpError::LoadFailure(ty, url.to_string())); - } - - let raw_body = http_result.unwrap().text().await; - if let Err(e) = raw_body { - error!("could not get text for url {}: {:#?}", url, e); - return Err(HttpError::LoadFailure(ty, url.to_string())); - } - - let decoded = serde_json::from_str::(&raw_body.unwrap()); - - if let Err(e) = decoded { - error!("could not parse {} for url {}: {:#?}", ty, url, e); - return Err(HttpError::ParseFailure( - ty, - url.to_string(), - e.to_string() - )); - } - - Ok(decoded.unwrap()) - } - - pub async fn get_person(&self, url: &str) -> Result { - self.get("Person", url).await - } -} diff --git a/ferri-server/src/lib.rs b/ferri-server/src/lib.rs index 520e213..cced509 100644 --- a/ferri-server/src/lib.rs +++ b/ferri-server/src/lib.rs @@ -3,10 +3,9 @@ use endpoints::{ custom, inbox, oauth, user, well_known, }; +use tracing::Level; use tracing_subscriber::fmt; -use main::ap; - use main::ap::http; use main::config::Config; use rocket::{ @@ -20,24 +19,23 @@ use rocket_db_pools::{Connection, Database, sqlx}; mod cors; mod endpoints; mod types; -mod http_wrapper; #[derive(Database)] #[database("sqlite_ferri")] pub struct Db(sqlx::SqlitePool); #[get("/")] -async fn user_profile() -> (ContentType, &'static str) { +async fn user_profile(cfg: &rocket::State) -> (ContentType, &'static str) { (ContentType::HTML, "

hello

") } -#[get("/activities/<_activity>")] -async fn activity_endpoint(_activity: String) { +#[get("/activities/")] +async fn activity_endpoint(activity: String) { } #[derive(Debug)] -pub struct AuthenticatedUser { +struct AuthenticatedUser { pub username: String, pub id: String, pub token: String, @@ -45,7 +43,10 @@ pub struct AuthenticatedUser { } #[derive(Debug)] -pub enum LoginError { +enum LoginError { + InvalidData, + UsernameDoesNotExist, + WrongPassword, } #[rocket::async_trait] @@ -87,8 +88,7 @@ impl<'a> FromRequest<'a> for AuthenticatedUser { } } -pub struct OutboundQueue(pub ap::QueueHandle); -pub struct InboundQueue(pub ap::QueueHandle); + pub fn launch(cfg: Config) -> Rocket { let format = fmt::format() @@ -104,19 +104,11 @@ pub fn launch(cfg: Config) -> Rocket { .event_format(format) .with_writer(std::io::stdout) .init(); - - let outbound = ap::RequestQueue::new("outbound"); - let outbound_handle = outbound.spawn(); - - let inbound = ap::RequestQueue::new("inbound"); - let inbound_handle = inbound.spawn(); - + let http_client = http::HttpClient::new(); build() .manage(cfg) .manage(http_client) - .manage(OutboundQueue(outbound_handle)) - .manage(InboundQueue(inbound_handle)) .attach(Db::init()) .attach(cors::CORS) .mount("/assets", rocket::fs::FileServer::from("./assets")) diff --git a/ferri-server/src/types/activity.rs b/ferri-server/src/types/activity.rs index 29b0151..76b9acc 100644 --- a/ferri-server/src/types/activity.rs +++ b/ferri-server/src/types/activity.rs @@ -1,4 +1,5 @@ use rocket::serde::{Deserialize, Serialize}; + use crate::types::content::Post; #[derive(Serialize, Deserialize, Debug)] @@ -59,17 +60,3 @@ 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 e35c5d9..c6da908 100644 --- a/ferri-server/src/types/content.rs +++ b/ferri-server/src/types/content.rs @@ -15,7 +15,4 @@ 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 7767a8d..65ed38e 100644 --- a/ferri-server/src/types/mod.rs +++ b/ferri-server/src/types/mod.rs @@ -34,7 +34,6 @@ 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/flake.nix b/flake.nix index 5a00eb6..15cf319 100644 --- a/flake.nix +++ b/flake.nix @@ -29,7 +29,6 @@ packages = with pkgs; [ sqlx-cli - cargo-nextest (rust-bin.stable.latest.default.override { extensions = [ "rust-src" "rust-analyzer" ]; targets = [ ]; diff --git a/migrations/20250410112325_add_follow.sql b/migrations/20250410112325_add_follow.sql index 4e8922b..ce518cd 100644 --- a/migrations/20250410112325_add_follow.sql +++ b/migrations/20250410112325_add_follow.sql @@ -4,7 +4,6 @@ CREATE TABLE IF NOT EXISTS follow id TEXT PRIMARY KEY NOT NULL, follower_id TEXT NOT NULL, followed_id TEXT NOT NULL, - FOREIGN KEY(follower_id) REFERENCES actor(id), FOREIGN KEY(followed_id) REFERENCES actor(id) ); diff --git a/migrations/20250410182845_add_post.sql b/migrations/20250410182845_add_post.sql index 04c66bd..96088c8 100644 --- a/migrations/20250410182845_add_post.sql +++ b/migrations/20250410182845_add_post.sql @@ -5,8 +5,6 @@ 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(boosted_post_id) REFERENCES post(id) + FOREIGN KEY(user_id) REFERENCES user(id) );