From 2b62948447a305a47330d833280f24f7b4ec98d6 Mon Sep 17 00:00:00 2001 From: nullishamy Date: Sat, 12 Apr 2025 15:16:40 +0100 Subject: [PATCH] chore: tidy up ID usage; fmt --- Cargo.lock | 1 + ferri-cli/src/main.rs | 8 +- ferri-main/Cargo.toml | 3 +- ferri-main/src/ap/activity.rs | 136 +++++++++ ferri-main/src/ap/http.rs | 15 +- ferri-main/src/ap/mod.rs | 273 ++---------------- ferri-main/src/ap/post.rs | 113 ++++++++ ferri-main/src/ap/user.rs | 138 +++++++++ ferri-server/src/cors.rs | 6 +- ferri-server/src/endpoints/api/apps.rs | 2 +- ferri-server/src/endpoints/api/instance.rs | 7 +- ferri-server/src/endpoints/api/mod.rs | 6 +- ferri-server/src/endpoints/api/preferences.rs | 8 +- ferri-server/src/endpoints/api/status.rs | 198 +++---------- ferri-server/src/endpoints/api/timeline.rs | 9 +- ferri-server/src/endpoints/api/user.rs | 10 +- ferri-server/src/endpoints/custom.rs | 15 +- ferri-server/src/endpoints/inbox.rs | 60 +++- ferri-server/src/endpoints/mod.rs | 6 +- ferri-server/src/endpoints/oauth.rs | 6 +- ferri-server/src/endpoints/user.rs | 23 +- ferri-server/src/endpoints/well_known.rs | 5 +- ferri-server/src/lib.rs | 23 +- ferri-server/src/types/content.rs | 2 +- ferri-server/src/types/mod.rs | 4 +- ferri-server/src/types/oauth.rs | 7 +- ferri-server/src/types/webfinger.rs | 2 +- 27 files changed, 603 insertions(+), 483 deletions(-) create mode 100644 ferri-main/src/ap/activity.rs create mode 100644 ferri-main/src/ap/post.rs create mode 100644 ferri-main/src/ap/user.rs diff --git a/Cargo.lock b/Cargo.lock index a9d9ad2..388c0fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1264,6 +1264,7 @@ dependencies = [ "serde_json", "sqlx", "url", + "uuid", ] [[package]] diff --git a/ferri-cli/src/main.rs b/ferri-cli/src/main.rs index 2e65d5d..f3bfda4 100644 --- a/ferri-cli/src/main.rs +++ b/ferri-cli/src/main.rs @@ -28,9 +28,9 @@ async fn main() { INSERT INTO actor (id, inbox, outbox) VALUES (?1, ?2, ?3) "#, - "https://ferri.amy.mov/users/amy", - "https://ferri.amy.mov/users/amy/inbox", - "https://ferri.amy.mov/users/amy/outbox" + "https://ferri.amy.mov/users/c81db53f-d836-4283-a835-26606c9d14ff", + "https://ferri.amy.mov/users/c81db53f-d836-4283-a835-26606c9d14ff/inbox", + "https://ferri.amy.mov/users/c81db53f-d836-4283-a835-26606c9d14ff/outbox" ) .execute(&mut *conn) .await @@ -43,7 +43,7 @@ async fn main() { "#, "9b9d497b-2731-435f-a929-e609ca69dac9", "amy", - "https://ferri.amy.mov/users/amy", + "https://ferri.amy.mov/users/c81db53f-d836-4283-a835-26606c9d14ff", "amy" ) .execute(&mut *conn) diff --git a/ferri-main/Cargo.toml b/ferri-main/Cargo.toml index 282c128..855afc8 100644 --- a/ferri-main/Cargo.toml +++ b/ferri-main/Cargo.toml @@ -9,8 +9,9 @@ serde_json = "1.0.140" sqlx = { workspace = true } chrono = { workspace = true } reqwest = { workspace = true } +uuid = { workspace = true } base64 = "0.22.1" rsa = { version = "0.9.8", features = ["sha2"] } rand = "0.8" -url = "2.5.4" \ No newline at end of file +url = "2.5.4" diff --git a/ferri-main/src/ap/activity.rs b/ferri-main/src/ap/activity.rs new file mode 100644 index 0000000..bf834f1 --- /dev/null +++ b/ferri-main/src/ap/activity.rs @@ -0,0 +1,136 @@ +use crate::ap::{Actor, User, http}; +use chrono::{DateTime, Local}; +use serde::{Deserialize, Serialize}; +use sqlx::Sqlite; +use std::fmt::Debug; + +#[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 id: String, + pub ty: ActivityType, + 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 signed_by: KeyId, + 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(); + let actor_id = self.to.id(); + + sqlx::query!( + r#" + INSERT INTO activity (id, ty, actor_id) + VALUES (?1, ?2, ?3) + "#, + self.req.id, + ty, + actor_id + ) + .execute(conn) + .await + .unwrap(); + } +} + +#[derive(Serialize, Deserialize, Debug)] +struct RawActivity { + #[serde(rename = "@context")] + #[serde(skip_deserializing)] + context: String, + + id: String, + #[serde(rename = "type")] + ty: String, + + actor: String, + object: T, + published: String, +} + +type OutboxTransport = http::HttpClient; +pub struct Outbox<'a> { + user: User, + transport: &'a OutboxTransport, +} + +impl<'a> Outbox<'a> { + pub fn user(&self) -> &User { + &self.user + } + + pub async fn post(&self, activity: OutgoingActivity) { + dbg!(&activity); + let raw = RawActivity { + context: "https://www.w3.org/ns/activitystreams".to_string(), + id: activity.req.id, + ty: activity.req.ty.to_raw(), + actor: self.user.actor().id().to_string(), + object: activity.req.object, + published: activity.req.published.to_rfc3339(), + }; + + dbg!(&raw); + + let follow_res = self + .transport + .post(activity.to.inbox()) + .activity() + .json(&raw) + .sign(&activity.signed_by) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + dbg!(follow_res); + } + + pub fn for_user(user: User, transport: &'a OutboxTransport) -> Outbox<'a> { + Outbox { user, transport } + } +} diff --git a/ferri-main/src/ap/http.rs b/ferri-main/src/ap/http.rs index 1bc6860..5c45cca 100644 --- a/ferri-main/src/ap/http.rs +++ b/ferri-main/src/ap/http.rs @@ -51,7 +51,8 @@ impl RequestBuilder { } pub fn activity(mut self) -> RequestBuilder { - self.inner = self.inner + self.inner = self + .inner .header("Content-Type", "application/activity+json") .header("Accept", "application/activity+json"); self @@ -66,14 +67,16 @@ impl RequestBuilder { match self.verb { RequestVerb::GET => { let sig = self.sign_get_request(key_id); - self.inner = self.inner + self.inner = self + .inner .header("Date", sig.date) .header("Signature", sig.signature); self } RequestVerb::POST => { let sig = self.sign_post_request(key_id); - self.inner = self.inner + self.inner = self + .inner .header("Date", sig.date) .header("Digest", sig.digest) .header("Signature", sig.signature); @@ -87,7 +90,8 @@ impl RequestBuilder { let host = url.host_str().unwrap(); let path = url.path(); - let private_key = RsaPrivateKey::from_pkcs8_pem(include_str!("../../../private.pem")).unwrap(); + let private_key = + RsaPrivateKey::from_pkcs8_pem(include_str!("../../../private.pem")).unwrap(); let signing_key = SigningKey::::new(private_key); // UTC=GMT for our purposes, use it @@ -122,7 +126,8 @@ impl RequestBuilder { let host = url.host_str().unwrap(); let path = url.path(); - let private_key = RsaPrivateKey::from_pkcs8_pem(include_str!("../../../private.pem")).unwrap(); + let private_key = + RsaPrivateKey::from_pkcs8_pem(include_str!("../../../private.pem")).unwrap(); let signing_key = SigningKey::::new(private_key); let mut hasher = Sha256::new(); diff --git a/ferri-main/src/ap/mod.rs b/ferri-main/src/ap/mod.rs index b6a5914..44ae578 100644 --- a/ferri-main/src/ap/mod.rs +++ b/ferri-main/src/ap/mod.rs @@ -1,262 +1,27 @@ -use chrono::{DateTime, Local}; -use serde::{Deserialize, Serialize}; -use sqlx::Sqlite; +use chrono::{DateTime, Utc}; +use uuid::Uuid; -use std::fmt::Debug; pub mod http; -#[derive(Debug, Clone)] -pub struct Actor { - id: String, - inbox: String, - outbox: String, +mod activity; +pub use activity::*; + +mod user; +pub use user::*; + +mod post; +pub use post::*; + +pub const AS_CONTEXT: &'static str = "https://www.w3.org/ns/activitystreams"; + +pub fn new_id() -> String { + Uuid::new_v4().to_string() } -impl Actor { - pub fn from_raw(id: String, inbox: String, outbox: String) -> Self { - Self { id, inbox, outbox } - } +pub fn new_ts() -> String { + now().to_rfc3339() } -#[derive(Debug, Clone)] -pub struct User { - id: String, - username: String, - actor: Actor, - display_name: String, -} - -impl User { - pub fn id(&self) -> &str { - &self.id - } - - pub fn username(&self) -> &str { - &self.username - } - - pub fn actor_id(&self) -> &str { - &self.actor.id - } - - pub fn display_name(&self) -> &str { - &self.display_name - } - - pub fn actor(&self) -> &Actor { - &self.actor - } - - pub async fn from_id( - uuid: &str, - conn: impl sqlx::Executor<'_, Database = Sqlite>, - ) -> User { - let user = sqlx::query!( - r#" - SELECT u.*, a.id as "actor_own_id", a.inbox, a.outbox - FROM user u - INNER JOIN actor a ON u.actor_id = a.id - WHERE u.id = ?1 - "#, - uuid - ) - .fetch_one(conn) - .await - .unwrap(); - User { - id: user.id, - username: user.username, - actor: Actor { - id: user.actor_own_id, - inbox: user.inbox, - outbox: user.outbox, - }, - display_name: user.display_name, - } - } - - pub async fn from_username( - username: &str, - conn: impl sqlx::Executor<'_, Database = Sqlite>, - ) -> User { - let user = sqlx::query!( - r#" - SELECT u.*, a.id as "actor_own_id", a.inbox, a.outbox - FROM user u - INNER JOIN actor a ON u.actor_id = a.id - WHERE username = ?1 - "#, - username - ) - .fetch_one(conn) - .await - .unwrap(); - User { - id: user.id, - username: user.username, - actor: Actor { - id: user.actor_own_id, - inbox: user.inbox, - outbox: user.outbox, - }, - display_name: user.display_name, - } - } - - pub async fn from_actor_id( - actor_id: &str, - conn: impl sqlx::Executor<'_, Database = Sqlite>, - ) -> User { - let user = sqlx::query!( - r#" - SELECT u.*, a.id as "actor_own_id", a.inbox, a.outbox - FROM user u - INNER JOIN actor a ON u.actor_id = a.id - WHERE actor_id = ?1 - "#, - actor_id - ) - .fetch_one(conn) - .await - .unwrap(); - User { - id: user.id, - username: user.username, - actor: Actor { - id: user.actor_own_id, - inbox: user.inbox, - outbox: user.outbox, - }, - display_name: user.display_name, - } - } -} - -#[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 id: String, - pub ty: ActivityType, - 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 signed_by: KeyId, - 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 { - #[serde(rename = "@context")] - #[serde(skip_deserializing)] - context: String, - - id: String, - #[serde(rename = "type")] - ty: String, - - actor: String, - object: T, - published: String, -} - -type OutboxTransport = http::HttpClient; -pub struct Outbox<'a> { - user: User, - transport: &'a OutboxTransport, -} - -impl<'a> Outbox<'a> { - pub fn user(&self) -> &User { - &self.user - } - - pub async fn post(&self, activity: OutgoingActivity) { - dbg!(&activity); - let raw = RawActivity { - context: "https://www.w3.org/ns/activitystreams".to_string(), - id: activity.req.id, - ty: activity.req.ty.to_raw(), - actor: self.user.actor.id.clone(), - object: activity.req.object, - published: activity.req.published.to_rfc3339(), - }; - - dbg!(&raw); - - let follow_res = self - .transport - .post(activity.to.inbox) - .activity() - .json(&raw) - .sign(&activity.signed_by) - .send() - .await - .unwrap() - .text() - .await - .unwrap(); - - dbg!(follow_res); - } - - pub fn for_user(user: User, transport: &'a OutboxTransport) -> Outbox<'a> { - Outbox { user, transport } - } +pub fn now() -> DateTime { + Utc::now() } diff --git a/ferri-main/src/ap/post.rs b/ferri-main/src/ap/post.rs new file mode 100644 index 0000000..53bfe03 --- /dev/null +++ b/ferri-main/src/ap/post.rs @@ -0,0 +1,113 @@ +use crate::ap; +use chrono::{DateTime, Utc}; +use serde::Serialize; +use sqlx::Sqlite; + +const POST_TYPE: &'static str = "Post"; + +#[derive(Clone)] +pub struct Post { + id: String, + from: ap::User, + ts: DateTime, + content: String, + + to: Vec, + cc: Vec, +} + +impl Post { + pub fn from_parts(id: String, content: String, from: ap::User) -> Self { + Self { + id, + content, + from, + ts: ap::now(), + to: vec![], + cc: vec![], + } + } + + pub fn id(&self) -> &str { + &self.id + } + + pub fn content(&self) -> &str { + &self.content + } + + pub fn created_at(&self) -> String { + self.ts.to_rfc3339() + } + + pub fn uri(&self) -> String { + format!( + "https://ferri.amy.mov/users/{}/posts/{}", + self.from.id(), + self.id + ) + } + + pub async fn save(&self, conn: impl sqlx::Executor<'_, Database = Sqlite>) { + let ts = self.ts.to_rfc3339(); + let user_id = self.from.id(); + let post_id = self.id(); + let uri = self.uri(); + let content = self.content.clone(); + + sqlx::query!( + r#" + INSERT INTO post (id, uri, user_id, content, created_at) + VALUES (?1, ?2, ?3, ?4, ?5) + "#, + post_id, + uri, + user_id, + content, + ts + ) + .execute(conn) + .await + .unwrap(); + } + + pub fn to(mut self, recipient: String) -> Self { + self.to.push(recipient); + self + } + + pub fn cc(mut self, recipient: String) -> Self { + self.cc.push(recipient); + self + } + + pub fn to_ap(self) -> APPost { + APPost { + context: ap::AS_CONTEXT.to_string(), + id: self.uri(), + ty: POST_TYPE.to_string(), + ts: self.ts.to_rfc3339(), + content: self.content, + to: self.to, + cc: self.cc, + } + } +} + +#[derive(Serialize, Debug, Default)] +pub struct APPost { + #[serde(rename = "@context")] + #[serde(skip_deserializing)] + context: String, + id: String, + + #[serde(rename = "type")] + ty: String, + + #[serde(rename = "published")] + ts: String, + + content: String, + to: Vec, + cc: Vec, +} diff --git a/ferri-main/src/ap/user.rs b/ferri-main/src/ap/user.rs new file mode 100644 index 0000000..4178cfb --- /dev/null +++ b/ferri-main/src/ap/user.rs @@ -0,0 +1,138 @@ +use sqlx::Sqlite; +use std::fmt::Debug; + +#[derive(Debug, Clone)] +pub struct Actor { + id: String, + inbox: String, + outbox: String, +} + +impl Actor { + pub fn from_raw(id: String, inbox: String, outbox: String) -> Self { + Self { id, inbox, outbox } + } + + pub fn id(&self) -> &str { + &self.id + } + + pub fn inbox(&self) -> &str { + &self.inbox + } +} + +#[derive(Debug, Clone)] +pub struct User { + id: String, + username: String, + actor: Actor, + display_name: String, +} + +impl User { + pub fn id(&self) -> &str { + &self.id + } + + pub fn username(&self) -> &str { + &self.username + } + + pub fn actor_id(&self) -> &str { + &self.actor.id + } + + pub fn display_name(&self) -> &str { + &self.display_name + } + + pub fn actor(&self) -> &Actor { + &self.actor + } + + pub fn uri(&self) -> String { + format!("https://ferri.amy.mov/users/{}", self.id()) + } + + pub async fn from_id(uuid: &str, conn: impl sqlx::Executor<'_, Database = Sqlite>) -> User { + let user = sqlx::query!( + r#" + SELECT u.*, a.id as "actor_own_id", a.inbox, a.outbox + FROM user u + INNER JOIN actor a ON u.actor_id = a.id + WHERE u.id = ?1 + "#, + uuid + ) + .fetch_one(conn) + .await + .unwrap(); + User { + id: user.id, + username: user.username, + actor: Actor { + id: user.actor_own_id, + inbox: user.inbox, + outbox: user.outbox, + }, + display_name: user.display_name, + } + } + + pub async fn from_username( + username: &str, + conn: impl sqlx::Executor<'_, Database = Sqlite>, + ) -> User { + let user = sqlx::query!( + r#" + SELECT u.*, a.id as "actor_own_id", a.inbox, a.outbox + FROM user u + INNER JOIN actor a ON u.actor_id = a.id + WHERE username = ?1 + "#, + username + ) + .fetch_one(conn) + .await + .unwrap(); + User { + id: user.id, + username: user.username, + actor: Actor { + id: user.actor_own_id, + inbox: user.inbox, + outbox: user.outbox, + }, + display_name: user.display_name, + } + } + + pub async fn from_actor_id( + actor_id: &str, + conn: impl sqlx::Executor<'_, Database = Sqlite>, + ) -> User { + let user = sqlx::query!( + r#" + SELECT u.*, a.id as "actor_own_id", a.inbox, a.outbox + FROM user u + INNER JOIN actor a ON u.actor_id = a.id + WHERE actor_id = ?1 + "#, + actor_id + ) + .fetch_one(conn) + .await + .unwrap(); + User { + id: user.id, + username: user.username, + actor: Actor { + id: user.actor_own_id, + inbox: user.inbox, + outbox: user.outbox, + }, + display_name: user.display_name, + } + } +} diff --git a/ferri-server/src/cors.rs b/ferri-server/src/cors.rs index 731d64b..1293877 100644 --- a/ferri-server/src/cors.rs +++ b/ferri-server/src/cors.rs @@ -1,6 +1,8 @@ use rocket::{ - options, fairing::{Fairing, Info, Kind}, - Request, Response, http::{Method, Header} + Request, Response, + fairing::{Fairing, Info, Kind}, + http::{Header, Method}, + options, }; pub struct CORS; diff --git a/ferri-server/src/endpoints/api/apps.rs b/ferri-server/src/endpoints/api/apps.rs index 92cc1a1..40425cf 100644 --- a/ferri-server/src/endpoints/api/apps.rs +++ b/ferri-server/src/endpoints/api/apps.rs @@ -11,4 +11,4 @@ pub async fn new_app(app: Form) -> Json { client_id: format!("id-for-{}", app.client_name), client_secret: format!("secret-for-{}", app.client_name), }) -} \ 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 6fd927f..0a76ba6 100644 --- a/ferri-server/src/endpoints/api/instance.rs +++ b/ferri-server/src/endpoints/api/instance.rs @@ -1,6 +1,9 @@ use rocket::{get, serde::json::Json}; -use crate::types::instance::{Accounts, Configuration, Contact, Instance, MediaAttachments, Polls, Registrations, Statuses, Thumbnail, Translation, Urls}; +use crate::types::instance::{ + Accounts, Configuration, Contact, Instance, MediaAttachments, Polls, Registrations, Statuses, + Thumbnail, Translation, Urls, +}; #[get("/instance")] pub async fn instance() -> Json { @@ -59,4 +62,4 @@ pub async fn instance() -> Json { email: "no".to_string(), }, }) -} \ 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 ecc6713..02fe1da 100644 --- a/ferri-server/src/endpoints/api/mod.rs +++ b/ferri-server/src/endpoints/api/mod.rs @@ -1,6 +1,6 @@ -pub mod user; pub mod apps; pub mod instance; -pub mod status; pub mod preferences; -pub mod timeline; \ No newline at end of file +pub mod status; +pub mod timeline; +pub mod user; diff --git a/ferri-server/src/endpoints/api/preferences.rs b/ferri-server/src/endpoints/api/preferences.rs index 393d95d..a06db49 100644 --- a/ferri-server/src/endpoints/api/preferences.rs +++ b/ferri-server/src/endpoints/api/preferences.rs @@ -1,5 +1,7 @@ -use rocket::{get, serde::{json::Json, Deserialize, Serialize}}; - +use rocket::{ + get, + serde::{Deserialize, Serialize, json::Json}, +}; #[derive(Serialize, Deserialize)] #[serde(crate = "rocket::serde")] @@ -25,4 +27,4 @@ pub async fn preferences() -> Json { reading_expand_media: "default".to_string(), reading_expand_spoilers: false, }) -} \ 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 ea9bfa9..92cb6ec 100644 --- a/ferri-server/src/endpoints/api/status.rs +++ b/ferri-server/src/endpoints/api/status.rs @@ -1,3 +1,4 @@ +use crate::timeline::TimelineStatus; use chrono::Local; use main::ap::{self, http::HttpClient}; use rocket::{ @@ -8,10 +9,9 @@ use rocket::{ }; use rocket_db_pools::Connection; use uuid::Uuid; -use crate::timeline::TimelineStatus; -use crate::{AuthenticatedUser, Db, types::content}; use crate::api::user::CredentialAcount; +use crate::{AuthenticatedUser, Db, types::content}; #[derive(Serialize, Deserialize, Debug, FromForm)] #[serde(crate = "rocket::serde")] @@ -19,130 +19,23 @@ pub struct Status { status: String, } -#[post("/statuses", data = "")] -pub async fn new_status( - mut db: Connection, - http: &State, - status: Form, +async fn create_status( 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().to_string(); - - 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, uri, user_id, content, created_at) - VALUES (?1, ?2, ?3, ?4, ?5) - RETURNING * - "#, - post_id, - uri, - id, - status.status, - now - ) - .fetch_one(&mut **db) - .await - .unwrap(); - - 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; - } -} - -#[post("/statuses", data = "", rank = 2)] -pub async fn new_status_json( mut db: Connection, - http: &State, - status: Json, - user: AuthenticatedUser, -) -> Json { - dbg!(&user); + http: &HttpClient, + status: &Status, +) -> TimelineStatus { let user = ap::User::from_id(&user.username, &mut **db).await; let outbox = ap::Outbox::for_user(user.clone(), http); - let post_id = Uuid::new_v4().to_string(); + let post_id = ap::new_id(); + let now = ap::new_ts(); - let uri = format!( - "https://ferri.amy.mov/users/{}/posts/{}", - user.id(), - post_id - ); - let id = user.id(); - let now = Local::now().to_rfc3339(); + 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()); - let post = sqlx::query!( - r#" - INSERT INTO post (id, uri, user_id, content, created_at) - VALUES (?1, ?2, ?3, ?4, ?5) - RETURNING * - "#, - post_id, - uri, - id, - status.status, - now - ) - .fetch_one(&mut **db) - .await - .unwrap(); + post.save(&mut **db).await; let actors = sqlx::query!("SELECT * FROM actor") .fetch_all(&mut **db) @@ -151,8 +44,8 @@ pub async fn new_status_json( for record in actors { // Don't send to ourselves - if &record.id == user.actor_id() { - continue + if record.id == user.actor_id() { + continue; } let create_id = format!("https://ferri.amy.mov/activities/{}", Uuid::new_v4()); @@ -160,22 +53,8 @@ pub async fn new_status_json( 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() - )], + object: post.clone().to_ap(), + to: vec![format!("{}/followers", user.uri())], cc: vec!["https://www.w3.org/ns/activitystreams#Public".to_string()], ..Default::default() }; @@ -185,9 +64,10 @@ pub async fn new_status_json( record.inbox.clone(), record.outbox.clone(), ); + let req = ap::OutgoingActivity { req: activity, - signed_by: format!("https://ferri.amy.mov/users/{}#main-key", user.username()), + signed_by: format!("{}#main-key", user.uri()), to: actor, }; @@ -195,21 +75,17 @@ pub async fn new_status_json( outbox.post(req).await; } - let user_uri = format!( - "https://ferri.amy.mov/users/{}", - user.id(), - ); - Json(TimelineStatus { - id: post.id.clone(), - created_at: post.created_at.clone(), + 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.clone(), + content: post.content().to_string(), visibility: "public".to_string(), spoiler_text: "".to_string(), sensitive: false, - uri: post.uri.clone(), - url: post.uri.clone(), + uri: post.uri(), + url: post.uri(), replies_count: 0, reblogs_count: 0, favourites_count: 0, @@ -228,7 +104,7 @@ pub async fn new_status_json( created_at: "2025-04-10T22:12:09Z".to_string(), attribution_domains: vec![], note: "".to_string(), - url: user_uri, + 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(), @@ -237,6 +113,26 @@ pub async fn new_status_json( following_count: 1, statuses_count: 1, last_status_at: "2025-04-10T22:14:34Z".to_string(), - } - }) + }, + } +} + +#[post("/statuses", data = "")] +pub async fn new_status( + db: Connection, + http: &State, + status: Form, + user: AuthenticatedUser, +) -> Json { + Json(create_status(user, db, http.inner(), &status).await) +} + +#[post("/statuses", data = "", rank = 2)] +pub async fn new_status_json( + db: Connection, + http: &State, + status: Json, + user: AuthenticatedUser, +) -> Json { + Json(create_status(user, db, http.inner(), &status).await) } diff --git a/ferri-server/src/endpoints/api/timeline.rs b/ferri-server/src/endpoints/api/timeline.rs index 3d9e50c..385c0de 100644 --- a/ferri-server/src/endpoints/api/timeline.rs +++ b/ferri-server/src/endpoints/api/timeline.rs @@ -35,10 +35,11 @@ pub struct TimelineStatus { 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, p.uri as "post_uri", u.username, u.display_name, u.actor_id, p.created_at - FROM post p - INNER JOIN user u on p.user_id = u.id - "# + 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 + FROM post p + INNER JOIN user u on p.user_id = u.id + "# ) .fetch_all(&mut **db) .await diff --git a/ferri-server/src/endpoints/api/user.rs b/ferri-server/src/endpoints/api/user.rs index 2e4a4ac..a18a2f0 100644 --- a/ferri-server/src/endpoints/api/user.rs +++ b/ferri-server/src/endpoints/api/user.rs @@ -6,8 +6,8 @@ use rocket::{ use rocket_db_pools::Connection; use uuid::Uuid; +use crate::timeline::{TimelineAccount, TimelineStatus}; use crate::{AuthenticatedUser, Db, http::HttpClient}; -use crate::timeline::{TimelineStatus, TimelineAccount}; #[derive(Debug, Serialize, Deserialize)] #[serde(crate = "rocket::serde")] @@ -35,7 +35,7 @@ pub struct CredentialAcount { #[get("/accounts/verify_credentials")] pub async fn verify_credentials() -> Json { Json(CredentialAcount { - id: "https://ferri.amy.mov/users/amy".to_string(), + id: "9b9d497b-2731-435f-a929-e609ca69dac9".to_string(), username: "amy".to_string(), acct: "amy@ferri.amy.mov".to_string(), display_name: "amy".to_string(), @@ -89,7 +89,11 @@ pub async fn new_follow( } #[get("/accounts/")] -pub async fn account(mut db: Connection, uuid: &str, user: AuthenticatedUser) -> Json { +pub async fn account( + mut db: Connection, + uuid: &str, + user: AuthenticatedUser, +) -> Json { let user = ap::User::from_id(uuid, &mut **db).await; let user_uri = format!("https://ferri.amy.mov/users/{}", user.username()); Json(CredentialAcount { diff --git a/ferri-server/src/endpoints/custom.rs b/ferri-server/src/endpoints/custom.rs index 1306041..61a5aae 100644 --- a/ferri-server/src/endpoints/custom.rs +++ b/ferri-server/src/endpoints/custom.rs @@ -1,10 +1,13 @@ use main::ap::http::HttpClient; -use rocket::{get, response::status, State}; +use rocket::{State, get, response::status}; use rocket_db_pools::Connection; use uuid::Uuid; -use crate::{types::{self, activity, content, webfinger}, Db}; +use crate::{ + Db, + types::{self, activity, content, webfinger}, +}; #[get("/finger/")] pub async fn finger_account(mut db: Connection, account: &str) -> status::Accepted { @@ -109,11 +112,13 @@ pub async fn test(http: &State) -> &'static str { .sign(key_id) .activity() .send() - .await.unwrap() + .await + .unwrap() .text() - .await.unwrap(); + .await + .unwrap(); dbg!(follow); "Hello, world!" -} \ No newline at end of file +} diff --git a/ferri-server/src/endpoints/inbox.rs b/ferri-server/src/endpoints/inbox.rs index e87b83e..3291799 100644 --- a/ferri-server/src/endpoints/inbox.rs +++ b/ferri-server/src/endpoints/inbox.rs @@ -1,11 +1,11 @@ +use chrono::Local; use main::ap; +use rocket::serde::json::serde_json; 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, @@ -17,7 +17,11 @@ fn handle_delete_activity(activity: activity::DeleteActivity) { dbg!(activity); } -async fn create_actor(user: &Person, actor: String, conn: impl sqlx::Executor<'_, Database = Sqlite>) { +async fn create_actor( + user: &Person, + actor: String, + conn: impl sqlx::Executor<'_, Database = Sqlite>, +) { sqlx::query!( r#" INSERT INTO actor (id, inbox, outbox) @@ -33,7 +37,11 @@ async fn create_actor(user: &Person, actor: String, conn: impl sqlx::Executor<'_ .unwrap(); } -async fn create_user(user: &Person, actor: String, conn: impl sqlx::Executor<'_, Database = Sqlite>) { +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(); @@ -56,7 +64,10 @@ async fn create_user(user: &Person, actor: String, conn: impl sqlx::Executor<'_, .unwrap(); } -async fn create_follow(activity: &activity::FollowActivity, conn: impl sqlx::Executor<'_, Database = Sqlite>) { +async fn create_follow( + activity: &activity::FollowActivity, + conn: impl sqlx::Executor<'_, Database = Sqlite>, +) { sqlx::query!( r#" INSERT INTO follow (id, follower_id, followed_id) @@ -72,7 +83,12 @@ async fn create_follow(activity: &activity::FollowActivity, conn: impl sqlx::Exe .unwrap(); } -async fn handle_follow_activity(followed_account: String, activity: activity::FollowActivity, http: &HttpClient, mut db: Connection) { +async fn handle_follow_activity( + followed_account: String, + activity: activity::FollowActivity, + http: &HttpClient, + mut db: Connection, +) { let user = http .get(&activity.actor) .activity() @@ -114,11 +130,16 @@ async fn handle_follow_activity(followed_account: String, activity: activity::Fo async fn handle_like_activity(activity: activity::LikeActivity, mut db: Connection) { let target_post = sqlx::query!("SELECT * FROM post WHERE uri = ?1", activity.object) .fetch_one(&mut **db) - .await.unwrap(); + .await + .unwrap(); dbg!(&target_post); } -async fn handle_create_activity(activity: activity::CreateActivity,http: &HttpClient, mut db: Connection) { +async fn handle_create_activity( + activity: activity::CreateActivity, + http: &HttpClient, + mut db: Connection, +) { assert!(&activity.object.ty == "Note"); let user = http .get(&activity.actor) @@ -141,13 +162,20 @@ async fn handle_create_activity(activity: activity::CreateActivity,http: &HttpCl let post_id = Uuid::new_v4().to_string(); let uri = activity.id; - - sqlx::query!(r#" + sqlx::query!( + r#" INSERT INTO post (id, uri, user_id, content, created_at) VALUES (?1, ?2, ?3, ?4, ?5) - "#, post_id, uri, user_id, content, now) - .execute(&mut **db) - .await.unwrap(); + "#, + post_id, + uri, + user_id, + content, + now + ) + .execute(&mut **db) + .await + .unwrap(); } #[post("/users//inbox", data = "")] @@ -161,15 +189,15 @@ pub async fn inbox(db: Connection, http: &State, user: String, b "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; - }, + } unknown => { eprintln!("WARN: Unknown activity '{}' - {}", unknown, body); } diff --git a/ferri-server/src/endpoints/mod.rs b/ferri-server/src/endpoints/mod.rs index eb3a681..50f374c 100644 --- a/ferri-server/src/endpoints/mod.rs +++ b/ferri-server/src/endpoints/mod.rs @@ -1,13 +1,13 @@ use rocket::http::{ContentType, MediaType}; -pub mod user; pub mod oauth; +pub mod user; pub mod api; -pub mod well_known; pub mod custom; pub mod inbox; +pub mod well_known; fn activity_type() -> ContentType { ContentType(MediaType::new("application", "activity+json")) -} \ No newline at end of file +} diff --git a/ferri-server/src/endpoints/oauth.rs b/ferri-server/src/endpoints/oauth.rs index 7da8bf8..5b48ebc 100644 --- a/ferri-server/src/endpoints/oauth.rs +++ b/ferri-server/src/endpoints/oauth.rs @@ -1,4 +1,8 @@ -use rocket::{get, post, response::Redirect, serde::{json::Json, Deserialize, Serialize}}; +use rocket::{ + get, post, + response::Redirect, + serde::{Deserialize, Serialize, json::Json}, +}; #[get("/oauth/authorize?&&&")] pub async fn authorize( diff --git a/ferri-server/src/endpoints/user.rs b/ferri-server/src/endpoints/user.rs index 6cc0e05..7cc0550 100644 --- a/ferri-server/src/endpoints/user.rs +++ b/ferri-server/src/endpoints/user.rs @@ -7,14 +7,12 @@ use crate::{ types::{OrderedCollection, Person, UserKey, content}, }; - use super::activity_type; #[get("/users//inbox")] pub async fn inbox(user: String) -> Json { Json(OrderedCollection { ty: "OrderedCollection".to_string(), - summary: format!("Inbox for {}", user), total_items: 0, ordered_items: vec![], }) @@ -25,7 +23,6 @@ pub async fn outbox(user: String) -> Json { dbg!(&user); Json(OrderedCollection { ty: "OrderedCollection".to_string(), - summary: format!("Outbox for {}", user), total_items: 0, ordered_items: vec![], }) @@ -49,7 +46,6 @@ pub async fn followers(mut db: Connection, uuid: &str) -> Json, uuid: &str) -> Json, uuid: &str) -> Json/posts/")] -pub async fn post(mut db: Connection, uuid: &str, post: String) -> (ContentType, Json) { - let post = sqlx::query!(r#" +pub async fn post( + mut db: Connection, + uuid: &str, + post: String, +) -> (ContentType, Json) { + let post = sqlx::query!( + r#" SELECT * FROM post WHERE id = ?1 - "#, post) - .fetch_one(&mut **db) - .await.unwrap(); + "#, + post + ) + .fetch_one(&mut **db) + .await + .unwrap(); ( activity_type(), diff --git a/ferri-server/src/endpoints/well_known.rs b/ferri-server/src/endpoints/well_known.rs index 012ace6..9b07fc1 100644 --- a/ferri-server/src/endpoints/well_known.rs +++ b/ferri-server/src/endpoints/well_known.rs @@ -2,7 +2,10 @@ use main::ap; use rocket::{get, serde::json::Json}; use rocket_db_pools::Connection; -use crate::{types::webfinger::{Link, WebfingerResponse}, Db}; +use crate::{ + Db, + types::webfinger::{Link, WebfingerResponse}, +}; #[get("/.well-known/host-meta")] pub async fn host_meta() -> &'static str { diff --git a/ferri-server/src/lib.rs b/ferri-server/src/lib.rs index 96d8e83..13651ed 100644 --- a/ferri-server/src/lib.rs +++ b/ferri-server/src/lib.rs @@ -1,13 +1,19 @@ +use endpoints::{ + api::{self, timeline}, + custom, inbox, oauth, user, well_known, +}; use main::ap::http; use rocket::{ - build, get, http::ContentType, request::{FromRequest, Outcome}, routes, Build, Request, Rocket + Build, Request, Rocket, build, get, + http::ContentType, + request::{FromRequest, Outcome}, + routes, }; -use endpoints::{api::{self, timeline}, oauth, well_known, custom, user, inbox}; -use rocket_db_pools::{sqlx, Database}; +use rocket_db_pools::{Database, sqlx}; mod cors; -mod types; mod endpoints; +mod types; #[derive(Database)] #[database("sqlite_ferri")] @@ -26,7 +32,7 @@ async fn activity_endpoint(activity: String) { #[derive(Debug)] struct AuthenticatedUser { username: String, - actor_id: String + actor_id: String, } #[derive(Debug)] @@ -41,10 +47,13 @@ impl<'a> FromRequest<'a> for AuthenticatedUser { type Error = LoginError; async fn from_request(request: &'a Request<'_>) -> Outcome { let token = request.headers().get_one("Authorization").unwrap(); - let token = token.strip_prefix("Bearer").map(|s| s.trim()).unwrap_or(token); + let token = token + .strip_prefix("Bearer") + .map(|s| s.trim()) + .unwrap_or(token); Outcome::Success(AuthenticatedUser { username: token.to_string(), - actor_id: format!("https://ferri.amy.mov/users/{}", token) + actor_id: format!("https://ferri.amy.mov/users/{}", token), }) } } diff --git a/ferri-server/src/types/content.rs b/ferri-server/src/types/content.rs index ffd8922..c6da908 100644 --- a/ferri-server/src/types/content.rs +++ b/ferri-server/src/types/content.rs @@ -15,4 +15,4 @@ pub struct Post { pub content: String, pub to: Vec, pub cc: Vec, -} \ No newline at end of file +} diff --git a/ferri-server/src/types/mod.rs b/ferri-server/src/types/mod.rs index ce3e00f..65ed38e 100644 --- a/ferri-server/src/types/mod.rs +++ b/ferri-server/src/types/mod.rs @@ -6,7 +6,6 @@ pub mod webfinger; use rocket::serde::{Deserialize, Serialize}; - #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] #[serde(crate = "rocket::serde")] @@ -31,11 +30,11 @@ pub struct Person { pub ty: String, pub following: String, pub followers: String, + pub summary: String, pub inbox: String, pub outbox: String, pub preferred_username: String, pub name: String, - pub summary: String, pub public_key: Option, } @@ -53,7 +52,6 @@ pub struct Object { #[serde(rename_all = "camelCase")] #[serde(crate = "rocket::serde")] pub struct OrderedCollection { - pub summary: String, #[serde(rename = "type")] pub ty: String, pub total_items: u64, diff --git a/ferri-server/src/types/oauth.rs b/ferri-server/src/types/oauth.rs index 0517f75..567dd19 100644 --- a/ferri-server/src/types/oauth.rs +++ b/ferri-server/src/types/oauth.rs @@ -1,4 +1,7 @@ -use rocket::{serde::{Deserialize, Serialize}, FromForm}; +use rocket::{ + FromForm, + serde::{Deserialize, Serialize}, +}; #[derive(Serialize, Deserialize, Debug, FromForm)] #[serde(crate = "rocket::serde")] @@ -16,4 +19,4 @@ pub struct CredentialApplication { pub redirect_uris: Vec, pub client_id: String, pub client_secret: String, -} \ No newline at end of file +} diff --git a/ferri-server/src/types/webfinger.rs b/ferri-server/src/types/webfinger.rs index 33d8edd..a97c7dd 100644 --- a/ferri-server/src/types/webfinger.rs +++ b/ferri-server/src/types/webfinger.rs @@ -15,4 +15,4 @@ pub struct WebfingerResponse { pub subject: String, pub aliases: Vec, pub links: Vec, -} \ No newline at end of file +}