diff --git a/ferri-cli/src/main.rs b/ferri-cli/src/main.rs index 9629282..3d415c8 100644 --- a/ferri-cli/src/main.rs +++ b/ferri-cli/src/main.rs @@ -47,26 +47,17 @@ async fn main() { ) .execute(&mut *conn) .await - .unwrap(); - - let ts = main::ap::new_ts(); + .unwrap(); sqlx::query!( r#" - INSERT INTO user ( - id, acct, url, remote, username, - actor_id, display_name, created_at - ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) + INSERT INTO user (id, username, actor_id, display_name) + VALUES (?1, ?2, ?3, ?4) "#, "9b9d497b-2731-435f-a929-e609ca69dac9", "amy", - "https://ferri.amy.mov/@amy", - false, - "amy", "https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9", - "amy", - ts + "amy" ) .execute(&mut *conn) .await diff --git a/ferri-main/src/lib.rs b/ferri-main/src/lib.rs index 48ea0f2..31cafd7 100644 --- a/ferri-main/src/lib.rs +++ b/ferri-main/src/lib.rs @@ -1,6 +1,6 @@ pub mod ap; pub mod config; -pub mod types_rewrite; +mod types_rewrite; use rand::{Rng, distributions::Alphanumeric}; diff --git a/ferri-main/src/types_rewrite/convert.rs b/ferri-main/src/types_rewrite/convert.rs index 5daff6b..16ed63c 100644 --- a/ferri-main/src/types_rewrite/convert.rs +++ b/ferri-main/src/types_rewrite/convert.rs @@ -26,36 +26,3 @@ impl From for db::Actor { } } } - -impl From for api::Account { - fn from(val: db::User) -> api::Account { - api::Account { - id: val.id, - username: val.username, - acct: val.acct, - display_name: val.display_name, - - locked: false, - bot: false, - - created_at: val.created_at.to_rfc3339(), - attribution_domains: vec![], - - note: "".to_string(), - url: val.url, - - 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: 0, - following_count: 0, - statuses_count: 0, - last_status_at: val.posts.last_post_at.map(|ts| ts.to_rfc3339()), - - emojis: vec![], - fields: vec![], - } - } -} diff --git a/ferri-main/src/types_rewrite/fetch.rs b/ferri-main/src/types_rewrite/fetch.rs deleted file mode 100644 index 3e2cb69..0000000 --- a/ferri-main/src/types_rewrite/fetch.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::types_rewrite::{ObjectUuid, ObjectUri, db}; -use sqlx::SqliteConnection; -use thiserror::Error; -use tracing::info; -use chrono::{NaiveDateTime, DateTime, Utc}; - -const SQLITE_TIME_FMT: &'static str = "%Y-%m-%d %H:%M:%S"; - -#[derive(Debug, Error)] -pub enum FetchError { - #[error("an unknown error occured when fetching: {0}")] - Unknown(String) -} - -fn parse_ts(ts: String) -> Option> { - NaiveDateTime::parse_from_str(&ts, SQLITE_TIME_FMT) - .ok() - .map(|nt| nt.and_utc()) -} - -pub async fn user_by_id(id: ObjectUuid, conn: &mut SqliteConnection) -> Result { - info!("fetching user by uuid '{:?}' from the database", id); - - let record = sqlx::query!(r#" - SELECT - u.id as "user_id", - u.username, - u.actor_id, - u.display_name, - a.inbox, - a.outbox, - u.url, - u.acct, - u.remote, - u.created_at - FROM "user" u - INNER JOIN "actor" a ON u.actor_id = a.id - WHERE u.id = ?1 - "#, id.0) - .fetch_one(&mut *conn) - .await - .map_err(|e| FetchError::Unknown(e.to_string()))?; - - let follower_count = sqlx::query_scalar!(r#" - SELECT COUNT(follower_id) - FROM "follow" - WHERE followed_id = ?1 - "#, record.actor_id) - .fetch_one(&mut *conn) - .await - .map_err(|e| FetchError::Unknown(e.to_string()))?; - - let last_post_at = sqlx::query_scalar!(r#" - SELECT datetime(p.created_at) - FROM post p - WHERE p.user_id = ?1 - ORDER BY datetime(p.created_at) DESC - LIMIT 1 - "#, record.user_id) - .fetch_one(&mut *conn) - .await - .map_err(|e| FetchError::Unknown(e.to_string()))? - .and_then(|ts| { - info!("parsing timestamp {}", ts); - parse_ts(ts) - }); - - let user_created = parse_ts(record.created_at).expect("no db corruption"); - - info!("user {:?} has {} followers", id, follower_count); - info!("user {:?} last posted {:?}", id, last_post_at); - - Ok(db::User { - id: ObjectUuid(record.user_id), - actor: db::Actor { - id: ObjectUri(record.actor_id), - inbox: record.inbox, - outbox: record.outbox - }, - acct: record.acct, - remote: record.remote, - username: record.username, - display_name: record.display_name, - created_at: user_created, - url: record.url, - posts: db::UserPosts { - last_post_at - } - }) - } diff --git a/ferri-main/src/types_rewrite/mod.rs b/ferri-main/src/types_rewrite/mod.rs index 1d03c12..b1baa5a 100644 --- a/ferri-main/src/types_rewrite/mod.rs +++ b/ferri-main/src/types_rewrite/mod.rs @@ -1,7 +1,7 @@ use serde::{Serialize, Deserialize}; -pub mod convert; -pub mod fetch; +mod convert; +pub use convert::*; pub const AS_CONTEXT_RAW: &'static str = "https://www.w3.org/ns/activitystreams"; pub fn as_context() -> ObjectContext { @@ -16,10 +16,7 @@ pub enum ObjectContext { } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ObjectUri(pub String); - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ObjectUuid(pub String); +pub struct ObjectUri(String); #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct Object { @@ -29,35 +26,15 @@ pub struct Object { } pub mod db { - use chrono::{DateTime, Utc}; + use serde::{Serialize, Deserialize}; use super::*; - - #[derive(Debug, Eq, PartialEq)] + + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct Actor { pub id: ObjectUri, pub inbox: String, pub outbox: String, - } - - #[derive(Debug, Eq, PartialEq)] - pub struct UserPosts { - // User may have no posts - pub last_post_at: Option> - } - - #[derive(Debug, Eq, PartialEq)] - pub struct User { - pub id: ObjectUuid, - pub actor: Actor, - pub username: String, - pub display_name: String, - pub acct: String, - pub remote: bool, - pub url: String, - pub created_at: DateTime, - - pub posts: UserPosts - } + } } pub mod ap { @@ -72,33 +49,6 @@ pub mod ap { pub inbox: String, pub outbox: String, } - - #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] - pub struct Person { - #[serde(flatten)] - pub obj: Object, - - pub following: String, - pub followers: String, - - pub summary: String, - pub inbox: String, - pub outbox: String, - - pub preferred_username: String, - pub name: String, - - pub public_key: Option, - } - - #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] - pub struct UserKey { - pub id: String, - pub owner: String, - - #[serde(rename = "publicKeyPem")] - pub public_key: String, - } } pub mod api { @@ -108,51 +58,6 @@ pub mod api { // API will not really use actors so treat them as DB actors // until we require specificity pub type Actor = db::Actor; - - #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] - pub struct Account { - pub id: ObjectUuid, - pub username: String, - pub acct: String, - pub display_name: String, - - pub locked: bool, - pub bot: bool, - - pub created_at: String, - pub attribution_domains: Vec, - - pub note: String, - pub url: String, - - pub avatar: String, - pub avatar_static: String, - pub header: String, - pub header_static: String, - - pub followers_count: i64, - pub following_count: i64, - pub statuses_count: i64, - pub last_status_at: Option, - - pub emojis: Vec, - pub fields: Vec, - } - - #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] - pub struct Emoji { - pub shortcode: String, - pub url: String, - pub static_url: String, - pub visible_in_picker: bool, - } - - #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] - pub struct CustomField { - pub name: String, - pub value: String, - pub verified_at: Option, - } } #[cfg(test)] @@ -162,7 +67,6 @@ mod tests { #[test] fn ap_actor_to_db() { let domain = "https://example.com"; - let ap = ap::Actor { obj: Object { context: as_context(), diff --git a/ferri-server/src/endpoints/api/timeline.rs b/ferri-server/src/endpoints/api/timeline.rs index 7748621..04920e1 100644 --- a/ferri-server/src/endpoints/api/timeline.rs +++ b/ferri-server/src/endpoints/api/timeline.rs @@ -39,60 +39,35 @@ pub async fn home( config: &State, _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 - } - - 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 - 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; + 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 + FROM post p + INNER JOIN user u on p.user_id = u.id "# ) - .bind("https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9") - .fetch_all(&mut **db) - .await - .unwrap(); - - dbg!(&posts); + .fetch_all(&mut **db) + .await + .unwrap(); let mut out = Vec::::new(); - for record in posts.iter() { + for record in posts { let mut boost: Option> = None; - if let Some(ref boosted_id) = record.boosted_post_id { - let user_uri = config.user_url(&record.user_id); - let record = posts.iter().find(|p| &p.post_id == boosted_id).unwrap(); + 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(), @@ -135,51 +110,49 @@ pub async fn home( }, })) } - - if !record.is_boost_source { - let user_uri = config.user_web_url(&record.username); - 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: 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); + 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: 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(), + }, + }); } Json(out) diff --git a/ferri-server/src/endpoints/custom.rs b/ferri-server/src/endpoints/custom.rs index b4a0f7c..8d7c57f 100644 --- a/ferri-server/src/endpoints/custom.rs +++ b/ferri-server/src/endpoints/custom.rs @@ -1,3 +1,4 @@ +use main::ap::http::HttpClient; use rocket::{State, get, response::status}; use rocket_db_pools::Connection; use main::ap; @@ -7,7 +8,7 @@ use uuid::Uuid; use crate::{ Db, - types::{self, webfinger}, + types::{self, activity, content, webfinger}, }; #[get("/finger/")] @@ -85,17 +86,44 @@ pub async fn resolve_user(acct: &str, host: &str) -> types::Person { } #[get("/test")] -pub async fn test( - outbound: &State, - mut db: Connection -) -> &'static str { - use main::types_rewrite::{ObjectUuid, fetch, api}; +pub async fn test(http: &State, outbound: &State) -> &'static str { outbound.0.send(ap::QueueMessage::Heartbeat); + + let user = resolve_user("amy@fedi.amy.mov", "fedi.amy.mov").await; - let id = ObjectUuid("9b9d497b-2731-435f-a929-e609ca69dac9".to_string()); - let user= dbg!(fetch::user_by_id(id, &mut **db).await.unwrap()); - let apu: api::Account = user.into(); - dbg!(apu); + let post = activity::CreateActivity { + id: "https://ferri.amy.mov/activities/amy/20".to_string(), + ty: "Create".to_string(), + actor: "https://ferri.amy.mov/users/amy".to_string(), + object: content::Post { + context: "https://www.w3.org/ns/activitystreams".to_string(), + id: "https://ferri.amy.mov/users/amy/posts/20".to_string(), + ty: "Note".to_string(), + content: "My first post".to_string(), + 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()], + cc: vec![], + }; + + let key_id = "https://ferri.amy.mov/users/amy#main-key"; + let follow = http + .post(user.inbox) + .json(&post) + .sign(key_id) + .activity() + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + + dbg!(follow); "Hello, world!" } diff --git a/ferri-server/src/endpoints/inbox.rs b/ferri-server/src/endpoints/inbox.rs index 5d9971a..924ae61 100644 --- a/ferri-server/src/endpoints/inbox.rs +++ b/ferri-server/src/endpoints/inbox.rs @@ -50,34 +50,19 @@ async fn create_user( let host = url.host_str().unwrap(); info!("creating user '{}'@'{}' ({:#?})", user.preferred_username, host, user); - let (acct, remote) = if host != "ferri.amy.mov" { - (format!("{}@{}", user.preferred_username, host), true) - } else { - (user.preferred_username.clone(), false) - }; - - let url = format!("https://ferri.amy.mov/{}", acct); + let username = format!("{}@{}", user.preferred_username, host); let uuid = Uuid::new_v4().to_string(); - // FIXME: Pull from user - let ts = main::ap::new_ts(); sqlx::query!( r#" - INSERT INTO user ( - id, acct, url, remote, username, - actor_id, display_name, created_at - ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) + INSERT INTO user (id, username, actor_id, display_name) + VALUES (?1, ?2, ?3, ?4) ON CONFLICT(actor_id) DO NOTHING; "#, uuid, - acct, - url, - remote, - user.preferred_username, + username, actor, - user.name, - ts + user.name ) .execute(conn) .await diff --git a/ferri-server/src/endpoints/oauth.rs b/ferri-server/src/endpoints/oauth.rs index 56ed5ce..ae897fa 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?&&&")] +#[get("/oauth/authorize?&&&<_response_type>")] 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 diff --git a/migrations/20250410121119_add_user.sql b/migrations/20250410121119_add_user.sql index 2dbadf1..b1706e0 100644 --- a/migrations/20250410121119_add_user.sql +++ b/migrations/20250410121119_add_user.sql @@ -2,10 +2,6 @@ CREATE TABLE IF NOT EXISTS user ( -- UUID id TEXT PRIMARY KEY NOT NULL, - acct TEXT NOT NULL, - url TEXT NOT NULL, - created_at TEXT NOT NULL, - remote BOOLEAN NOT NULL, username TEXT NOT NULL, actor_id TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, diff --git a/migrations/20250410182845_add_post.sql b/migrations/20250410182845_add_post.sql index 95e2a2a..04c66bd 100644 --- a/migrations/20250410182845_add_post.sql +++ b/migrations/20250410182845_add_post.sql @@ -1,7 +1,7 @@ CREATE TABLE IF NOT EXISTS post ( id TEXT PRIMARY KEY NOT NULL, - uri TEXT NOT NULL UNIQUE, + uri TEXT NOT NULL, user_id TEXT NOT NULL, content TEXT NOT NULL, created_at TEXT NOT NULL,