diff --git a/ferri-cli/src/main.rs b/ferri-cli/src/main.rs index 3d415c8..9629282 100644 --- a/ferri-cli/src/main.rs +++ b/ferri-cli/src/main.rs @@ -47,17 +47,26 @@ async fn main() { ) .execute(&mut *conn) .await - .unwrap(); + .unwrap(); + + let ts = main::ap::new_ts(); sqlx::query!( r#" - INSERT INTO user (id, username, actor_id, display_name) - VALUES (?1, ?2, ?3, ?4) + INSERT INTO user ( + id, acct, url, remote, username, + actor_id, display_name, created_at + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) "#, "9b9d497b-2731-435f-a929-e609ca69dac9", "amy", + "https://ferri.amy.mov/@amy", + false, + "amy", "https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9", - "amy" + "amy", + ts ) .execute(&mut *conn) .await diff --git a/ferri-main/src/lib.rs b/ferri-main/src/lib.rs index 31cafd7..48ea0f2 100644 --- a/ferri-main/src/lib.rs +++ b/ferri-main/src/lib.rs @@ -1,6 +1,6 @@ pub mod ap; pub mod config; -mod types_rewrite; +pub 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 a4c869c..5daff6b 100644 --- a/ferri-main/src/types_rewrite/convert.rs +++ b/ferri-main/src/types_rewrite/convert.rs @@ -32,17 +32,17 @@ impl From for api::Account { api::Account { id: val.id, username: val.username, - acct: "FIXME_api::Account::acct".to_string(), + acct: val.acct, display_name: val.display_name, locked: false, bot: false, - created_at: "FIXME_api::Account::created_at".to_string(), + created_at: val.created_at.to_rfc3339(), attribution_domains: vec![], note: "".to_string(), - url: "FIXME_api::Account::url".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(), @@ -52,7 +52,7 @@ impl From for api::Account { followers_count: 0, following_count: 0, statuses_count: 0, - last_status_at: "FIXME_api::Account::last_status_at".to_string(), + 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 new file mode 100644 index 0000000..3e2cb69 --- /dev/null +++ b/ferri-main/src/types_rewrite/fetch.rs @@ -0,0 +1,90 @@ +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 4f023a9..1d03c12 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}; -mod convert; -pub use convert::*; +pub mod convert; +pub mod fetch; pub const AS_CONTEXT_RAW: &'static str = "https://www.w3.org/ns/activitystreams"; pub fn as_context() -> ObjectContext { @@ -16,10 +16,10 @@ pub enum ObjectContext { } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ObjectUri(String); +pub struct ObjectUri(pub String); #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ObjectUuid(String); +pub struct ObjectUuid(pub String); #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct Object { @@ -29,22 +29,34 @@ pub struct Object { } pub mod db { - use serde::{Serialize, Deserialize}; + use chrono::{DateTime, Utc}; use super::*; - - #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] + + #[derive(Debug, Eq, PartialEq)] pub struct Actor { pub id: ObjectUri, pub inbox: String, pub outbox: String, } - #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] + #[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_id: ObjectUri, + pub actor: Actor, pub username: String, - pub display_name: String + pub display_name: String, + pub acct: String, + pub remote: bool, + pub url: String, + pub created_at: DateTime, + + pub posts: UserPosts } } @@ -121,7 +133,7 @@ pub mod api { pub followers_count: i64, pub following_count: i64, pub statuses_count: i64, - pub last_status_at: String, + pub last_status_at: Option, pub emojis: Vec, pub fields: Vec, diff --git a/ferri-server/src/endpoints/api/timeline.rs b/ferri-server/src/endpoints/api/timeline.rs index 04920e1..f6f7dcf 100644 --- a/ferri-server/src/endpoints/api/timeline.rs +++ b/ferri-server/src/endpoints/api/timeline.rs @@ -45,6 +45,7 @@ pub async fn home( 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 + ORDER BY datetime(p.created_at) DESC "# ) .fetch_all(&mut **db) diff --git a/ferri-server/src/endpoints/custom.rs b/ferri-server/src/endpoints/custom.rs index 8d7c57f..b4a0f7c 100644 --- a/ferri-server/src/endpoints/custom.rs +++ b/ferri-server/src/endpoints/custom.rs @@ -1,4 +1,3 @@ -use main::ap::http::HttpClient; use rocket::{State, get, response::status}; use rocket_db_pools::Connection; use main::ap; @@ -8,7 +7,7 @@ use uuid::Uuid; use crate::{ Db, - types::{self, activity, content, webfinger}, + types::{self, webfinger}, }; #[get("/finger/")] @@ -86,44 +85,17 @@ pub async fn resolve_user(acct: &str, host: &str) -> types::Person { } #[get("/test")] -pub async fn test(http: &State, outbound: &State) -> &'static str { +pub async fn test( + outbound: &State, + mut db: Connection +) -> &'static str { + use main::types_rewrite::{ObjectUuid, fetch, api}; outbound.0.send(ap::QueueMessage::Heartbeat); - - let user = resolve_user("amy@fedi.amy.mov", "fedi.amy.mov").await; - 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); + 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); "Hello, world!" } diff --git a/ferri-server/src/endpoints/inbox.rs b/ferri-server/src/endpoints/inbox.rs index 924ae61..5d9971a 100644 --- a/ferri-server/src/endpoints/inbox.rs +++ b/ferri-server/src/endpoints/inbox.rs @@ -50,19 +50,34 @@ async fn create_user( let host = url.host_str().unwrap(); info!("creating user '{}'@'{}' ({:#?})", user.preferred_username, host, user); - let username = format!("{}@{}", user.preferred_username, host); + 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 uuid = Uuid::new_v4().to_string(); + // FIXME: Pull from user + let ts = main::ap::new_ts(); sqlx::query!( r#" - INSERT INTO user (id, username, actor_id, display_name) - VALUES (?1, ?2, ?3, ?4) + INSERT INTO user ( + id, acct, url, remote, username, + actor_id, display_name, created_at + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) ON CONFLICT(actor_id) DO NOTHING; "#, uuid, - username, + acct, + url, + remote, + user.preferred_username, actor, - user.name + user.name, + ts ) .execute(conn) .await diff --git a/ferri-server/src/endpoints/oauth.rs b/ferri-server/src/endpoints/oauth.rs index ae897fa..56ed5ce 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 diff --git a/migrations/20250410121119_add_user.sql b/migrations/20250410121119_add_user.sql index b1706e0..2dbadf1 100644 --- a/migrations/20250410121119_add_user.sql +++ b/migrations/20250410121119_add_user.sql @@ -2,6 +2,10 @@ 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,