From e6b654a0b391e20d023d06533b53b2b7e7a17a52 Mon Sep 17 00:00:00 2001 From: nullishamy Date: Mon, 28 Apr 2025 23:21:59 +0100 Subject: [PATCH] feat: start on CRUD ops for new types --- .../src/types_rewrite/{fetch.rs => get.rs} | 16 +--- ferri-main/src/types_rewrite/make.rs | 30 ++++++ ferri-main/src/types_rewrite/mod.rs | 30 ++++-- ferri-server/src/endpoints/api/timeline.rs | 5 +- ferri-server/src/endpoints/custom.rs | 4 +- ferri-server/src/endpoints/inbox.rs | 96 +++++++++++++++++-- 6 files changed, 149 insertions(+), 32 deletions(-) rename ferri-main/src/types_rewrite/{fetch.rs => get.rs} (85%) create mode 100644 ferri-main/src/types_rewrite/make.rs diff --git a/ferri-main/src/types_rewrite/fetch.rs b/ferri-main/src/types_rewrite/get.rs similarity index 85% rename from ferri-main/src/types_rewrite/fetch.rs rename to ferri-main/src/types_rewrite/get.rs index 3e2cb69..b2ad17d 100644 --- a/ferri-main/src/types_rewrite/fetch.rs +++ b/ferri-main/src/types_rewrite/get.rs @@ -1,24 +1,18 @@ use crate::types_rewrite::{ObjectUuid, ObjectUri, db}; use sqlx::SqliteConnection; -use thiserror::Error; use tracing::info; use chrono::{NaiveDateTime, DateTime, Utc}; +use crate::types_rewrite::DbError; 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 { +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#" @@ -39,7 +33,7 @@ pub async fn user_by_id(id: ObjectUuid, conn: &mut SqliteConnection) -> Result Result Result Result { + let ts = user.created_at.to_rfc3339(); + sqlx::query!(r#" + INSERT INTO user (id, acct, url, created_at, remote, username, actor_id, display_name) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) + "#, user.id.0, user.acct, user.url, ts, + user.remote, user.username, user.actor.id.0, user.display_name + ) + .execute(conn) + .await + .map_err(|e| DbError::CreationError(e.to_string()))?; + + Ok(user) +} + +pub async fn new_actor(actor: db::Actor, conn: &mut SqliteConnection) -> Result { + sqlx::query!(r#" + INSERT INTO actor (id, inbox, outbox) + VALUES (?1, ?2, ?3) + "#, actor.id.0, actor.inbox, actor.outbox) + .execute(conn) + .await + .map_err(|e| DbError::CreationError(e.to_string()))?; + + Ok(actor) +} diff --git a/ferri-main/src/types_rewrite/mod.rs b/ferri-main/src/types_rewrite/mod.rs index 1d03c12..317c667 100644 --- a/ferri-main/src/types_rewrite/mod.rs +++ b/ferri-main/src/types_rewrite/mod.rs @@ -1,7 +1,19 @@ use serde::{Serialize, Deserialize}; +use thiserror::Error; +use std::fmt::Debug; +use uuid::Uuid; pub mod convert; -pub mod fetch; +pub mod get; +pub mod make; + +#[derive(Debug, Error)] +pub enum DbError { + #[error("an unknown error occured when creating: {0}")] + CreationError(String), + #[error("an unknown error occured when fetching: {0}")] + FetchError(String) +} pub const AS_CONTEXT_RAW: &'static str = "https://www.w3.org/ns/activitystreams"; pub fn as_context() -> ObjectContext { @@ -15,12 +27,18 @@ pub enum ObjectContext { Vec(Vec), } -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct ObjectUri(pub String); -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct ObjectUuid(pub String); +impl ObjectUuid { + pub fn new() -> Self { + Self(Uuid::new_v4().to_string()) + } +} + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct Object { #[serde(rename = "@context")] @@ -32,20 +50,20 @@ pub mod db { use chrono::{DateTime, Utc}; use super::*; - #[derive(Debug, Eq, PartialEq)] + #[derive(Debug, Eq, PartialEq, Clone)] pub struct Actor { pub id: ObjectUri, pub inbox: String, pub outbox: String, } - #[derive(Debug, Eq, PartialEq)] + #[derive(Debug, Eq, PartialEq, Clone)] pub struct UserPosts { // User may have no posts pub last_post_at: Option> } - #[derive(Debug, Eq, PartialEq)] + #[derive(Debug, Eq, PartialEq, Clone)] pub struct User { pub id: ObjectUuid, pub actor: Actor, diff --git a/ferri-server/src/endpoints/api/timeline.rs b/ferri-server/src/endpoints/api/timeline.rs index 7748621..c67f982 100644 --- a/ferri-server/src/endpoints/api/timeline.rs +++ b/ferri-server/src/endpoints/api/timeline.rs @@ -51,7 +51,8 @@ pub async fn home( display_name: String, username: String } - + + // FIXME: query! can't cope with this. returns a type error let posts = sqlx::query_as::<_, Post>( r#" WITH RECURSIVE get_home_timeline_with_boosts( @@ -84,8 +85,6 @@ pub async fn home( .await .unwrap(); - dbg!(&posts); - let mut out = Vec::::new(); for record in posts.iter() { let mut boost: Option> = None; diff --git a/ferri-server/src/endpoints/custom.rs b/ferri-server/src/endpoints/custom.rs index b4a0f7c..ff5150f 100644 --- a/ferri-server/src/endpoints/custom.rs +++ b/ferri-server/src/endpoints/custom.rs @@ -89,11 +89,11 @@ pub async fn test( outbound: &State, mut db: Connection ) -> &'static str { - use main::types_rewrite::{ObjectUuid, fetch, api}; + use main::types_rewrite::{ObjectUuid, get, api}; outbound.0.send(ap::QueueMessage::Heartbeat); let id = ObjectUuid("9b9d497b-2731-435f-a929-e609ca69dac9".to_string()); - let user= dbg!(fetch::user_by_id(id, &mut **db).await.unwrap()); + let user= dbg!(get::user_by_id(id, &mut **db).await.unwrap()); let apu: api::Account = user.into(); dbg!(apu); diff --git a/ferri-server/src/endpoints/inbox.rs b/ferri-server/src/endpoints/inbox.rs index 5d9971a..076db38 100644 --- a/ferri-server/src/endpoints/inbox.rs +++ b/ferri-server/src/endpoints/inbox.rs @@ -4,12 +4,15 @@ use main::ap; use rocket::serde::json::serde_json; use rocket::{State, post}; use rocket_db_pools::Connection; +use sqlx::SqliteConnection; use sqlx::Sqlite; use url::Url; use uuid::Uuid; use tracing::{event, span, Level, debug, warn, info, error}; use crate::http_wrapper::HttpWrapper; +use main::types_rewrite::{make, db, ObjectUuid, ObjectUri, self}; + use crate::{ Db, http::HttpClient, @@ -103,22 +106,92 @@ async fn create_follow( .unwrap(); } +struct RemoteInfo { + acct: String, + web_url: String, + is_remote: bool, +} + +fn get_remote_info(actor_url: &str, person: &Person) -> RemoteInfo { + let url = Url::parse(&actor_url).unwrap(); + let host = url.host_str().unwrap(); + + let (acct, remote) = if host != "ferri.amy.mov" { + (format!("{}@{}", person.preferred_username, host), true) + } else { + (person.preferred_username.clone(), false) + }; + + let url = format!("https://ferri.amy.mov/{}", acct); + + RemoteInfo { + acct: acct.to_string(), + web_url: url, + is_remote: remote + } +} + +async fn resolve_actor<'a>( + actor_url: &str, + http: &HttpWrapper<'a>, + conn: &mut SqliteConnection +) -> Result { + let person = { + let res = http.get_person(&actor_url).await; + if let Err(e) = res { + error!("could not load user {}: {}", actor_url, e.to_string()); + return Err(types_rewrite::DbError::FetchError( + format!("could not load user {}: {}", actor_url, e.to_string()) + )) + } + + res.unwrap() + }; + + let user_id = ObjectUuid::new(); + let remote_info = get_remote_info(actor_url, &person); + + let actor = db::Actor { + id: ObjectUri(actor_url.to_string()), + inbox: person.inbox.clone(), + outbox: person.outbox.clone(), + }; + + info!("creating actor {}", actor_url); + + let actor = make::new_actor(actor.clone(), conn).await.unwrap_or(actor); + + info!("creating user {} ({:#?})", remote_info.acct, person); + + let user = db::User { + id: user_id, + actor, + username: person.name, + display_name: person.preferred_username, + acct: remote_info.acct, + remote: remote_info.is_remote, + url: remote_info.web_url, + created_at: main::ap::now(), + + posts: db::UserPosts { + last_post_at: None + } + }; + + Ok(make::new_user(user.clone(), conn).await.unwrap_or(user)) +} + async fn handle_follow_activity<'a>( followed_account: &str, activity: activity::FollowActivity, http: HttpWrapper<'a>, 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 actor = resolve_actor(&activity.actor, &http, &mut **db) + .await.unwrap(); - create_actor(&user, &activity.actor, &mut **db).await; - create_user(&user, &activity.actor, &mut **db).await; + info!("{:?} follows {}", actor, followed_account); + create_follow(&activity, &mut **db).await; let follower = ap::User::from_actor_id(&activity.actor, &mut **db).await; @@ -226,15 +299,18 @@ async fn handle_boost_activity<'a>( let reblog_id = ap::new_id(); let attr_id = attributed_user.id(); + // HACK: ON CONFLICT is to avoid duplicate remote posts coming in + // check this better in future sqlx::query!(" INSERT INTO post (id, uri, user_id, content, created_at) VALUES (?1, ?2, ?3, ?4, ?5) + ON CONFLICT(uri) DO NOTHING ", 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 uri = format!("https://ferri.amy.mov/users/{}/posts/{}", actor_user.id(), base_id); let user_id = actor_user.id(); sqlx::query!("