From 76fb8838c2786e5b5a17aeeb0f44b6708f7afdeb Mon Sep 17 00:00:00 2001 From: nullishamy Date: Mon, 28 Apr 2025 00:48:36 +0100 Subject: [PATCH 1/3] feat: add user types into rewrite --- ferri-main/src/types_rewrite/convert.rs | 33 ++++++++++ ferri-main/src/types_rewrite/mod.rs | 86 ++++++++++++++++++++++++- 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/ferri-main/src/types_rewrite/convert.rs b/ferri-main/src/types_rewrite/convert.rs index 16ed63c..a4c869c 100644 --- a/ferri-main/src/types_rewrite/convert.rs +++ b/ferri-main/src/types_rewrite/convert.rs @@ -26,3 +26,36 @@ 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: "FIXME_api::Account::acct".to_string(), + display_name: val.display_name, + + locked: false, + bot: false, + + created_at: "FIXME_api::Account::created_at".to_string(), + attribution_domains: vec![], + + note: "".to_string(), + url: "FIXME_api::Account::url".to_string(), + + 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: "FIXME_api::Account::last_status_at".to_string(), + + emojis: vec![], + fields: vec![], + } + } +} diff --git a/ferri-main/src/types_rewrite/mod.rs b/ferri-main/src/types_rewrite/mod.rs index b1baa5a..4f023a9 100644 --- a/ferri-main/src/types_rewrite/mod.rs +++ b/ferri-main/src/types_rewrite/mod.rs @@ -18,6 +18,9 @@ pub enum ObjectContext { #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct ObjectUri(String); +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct ObjectUuid(String); + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct Object { #[serde(rename = "@context")] @@ -34,7 +37,15 @@ pub mod db { pub id: ObjectUri, pub inbox: String, pub outbox: String, - } + } + + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] + pub struct User { + pub id: ObjectUuid, + pub actor_id: ObjectUri, + pub username: String, + pub display_name: String + } } pub mod ap { @@ -49,6 +60,33 @@ 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 { @@ -58,6 +96,51 @@ 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: String, + + 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)] @@ -67,6 +150,7 @@ mod tests { #[test] fn ap_actor_to_db() { let domain = "https://example.com"; + let ap = ap::Actor { obj: Object { context: as_context(), From 62931ee20b0bc6a7abe6e87fa32c1ee4e0674176 Mon Sep 17 00:00:00 2001 From: nullishamy Date: Mon, 28 Apr 2025 19:23:56 +0100 Subject: [PATCH 2/3] feat: more fixes; finish account api types for now; add some more useful fields to it --- ferri-cli/src/main.rs | 17 +++- ferri-main/src/lib.rs | 2 +- ferri-main/src/types_rewrite/convert.rs | 8 +- ferri-main/src/types_rewrite/fetch.rs | 90 ++++++++++++++++++++++ ferri-main/src/types_rewrite/mod.rs | 34 +++++--- ferri-server/src/endpoints/api/timeline.rs | 1 + ferri-server/src/endpoints/custom.rs | 48 +++--------- ferri-server/src/endpoints/inbox.rs | 25 ++++-- ferri-server/src/endpoints/oauth.rs | 4 +- migrations/20250410121119_add_user.sql | 4 + 10 files changed, 168 insertions(+), 65 deletions(-) create mode 100644 ferri-main/src/types_rewrite/fetch.rs 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, From d252131e0da1ac51f34370c8f07c4a927c9991e1 Mon Sep 17 00:00:00 2001 From: nullishamy Date: Mon, 28 Apr 2025 21:06:17 +0100 Subject: [PATCH 3/3] feat: ! minor crimes ! timeline query --- ferri-server/src/endpoints/api/timeline.rs | 160 ++++++++++++--------- migrations/20250410182845_add_post.sql | 2 +- 2 files changed, 94 insertions(+), 68 deletions(-) diff --git a/ferri-server/src/endpoints/api/timeline.rs b/ferri-server/src/endpoints/api/timeline.rs index f6f7dcf..7748621 100644 --- a/ferri-server/src/endpoints/api/timeline.rs +++ b/ferri-server/src/endpoints/api/timeline.rs @@ -39,36 +39,60 @@ pub async fn home( config: &State, _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 - FROM post p - INNER JOIN user u on p.user_id = u.id - ORDER BY datetime(p.created_at) DESC + #[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; "# ) - .fetch_all(&mut **db) - .await - .unwrap(); + .bind("https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9") + .fetch_all(&mut **db) + .await + .unwrap(); + + dbg!(&posts); let mut out = Vec::::new(); - for record in posts { + for record in posts.iter() { 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(); - + 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(); + boost = Some(Box::new(TimelineStatus { id: record.post_id.clone(), created_at: record.created_at.clone(), @@ -111,49 +135,51 @@ pub async fn home( }, })) } - - 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(), - }, - }); + + 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(), + }, + }); + } } Json(out) diff --git a/migrations/20250410182845_add_post.sql b/migrations/20250410182845_add_post.sql index 04c66bd..95e2a2a 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, + uri TEXT NOT NULL UNIQUE, user_id TEXT NOT NULL, content TEXT NOT NULL, created_at TEXT NOT NULL,