From fafaf243c54700a54450638e6abfb113ab9ebff6 Mon Sep 17 00:00:00 2001 From: nullishamy Date: Tue, 6 May 2025 18:36:31 +0100 Subject: [PATCH 1/2] fix: misc cleanup --- ferri-cli/src/main.rs | 61 +- ferri-main/src/types/ap.rs | 223 ++++++++ ferri-main/src/types/api.rs | 231 ++++++++ ferri-main/src/types/db.rs | 59 ++ ferri-main/src/types/mod.rs | 541 +----------------- ferri-server/src/endpoints/admin/mod.rs | 10 +- ferri-server/src/endpoints/api/preferences.rs | 22 +- ferri-server/src/endpoints/api/timeline.rs | 50 +- ferri-server/src/endpoints/api/user.rs | 105 +--- ferri-server/src/http_wrapper.rs | 4 - ferri-server/src/lib.rs | 1 - ferri-server/templates/_layout.html | 19 + ferri-server/templates/index.html | 64 ++- 13 files changed, 640 insertions(+), 750 deletions(-) create mode 100644 ferri-main/src/types/ap.rs create mode 100644 ferri-main/src/types/api.rs create mode 100644 ferri-main/src/types/db.rs create mode 100644 ferri-server/templates/_layout.html diff --git a/ferri-cli/src/main.rs b/ferri-cli/src/main.rs index b67ad9e..afbe020 100644 --- a/ferri-cli/src/main.rs +++ b/ferri-cli/src/main.rs @@ -1,3 +1,4 @@ +use main::types::{db, make, ObjectUri, ObjectUuid}; use server::launch; extern crate rocket; @@ -24,6 +25,10 @@ pub fn read_config(path: impl AsRef) -> config::Config { toml::from_str(&content).unwrap() } +fn s(st: &'static str) -> String { + st.to_string() +} + #[rocket::main] async fn main() { let cli = Cli::parse(); @@ -36,42 +41,30 @@ async fn main() { .unwrap(); let mut conn = pool.acquire().await.unwrap(); - sqlx::query!( - r#" - INSERT INTO actor (id, inbox, outbox) - VALUES (?1, ?2, ?3) - "#, - "https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9", - "https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9/inbox", - "https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9/outbox" - ) - .execute(&mut *conn) - .await - .unwrap(); + let actor = db::Actor { + id: ObjectUri(s("https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9")), + inbox: s("https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9/inbox"), + outbox: s("https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9/outbox") + }; - let ts = main::ap::new_ts(); + make::new_actor(actor.clone(), &mut *conn).await.unwrap(); - sqlx::query!( - r#" - INSERT INTO user ( - id, acct, url, remote, username, - actor_id, display_name, created_at, icon_url - ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) - "#, - "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, - "https://ferri.amy.mov/assets/pfp.png" - ) - .execute(&mut *conn) - .await - .unwrap(); + let user = db::User { + id: ObjectUuid(s("9b9d497b-2731-435f-a929-e609ca69dac9")), + actor, + username: s("amy"), + display_name: s("amy (display)"), + acct: s("amy"), + remote: false, + url: s("https://ferri.amy.mov/@amy"), + created_at: main::ap::now(), + icon_url: s("https://ferri.amy.mov/assets/pfp.png"), + posts: db::UserPosts { + last_post_at: None + } + }; + + make::new_user(user, &mut *conn).await.unwrap(); } else { let _ = launch(config).launch().await; } diff --git a/ferri-main/src/types/ap.rs b/ferri-main/src/types/ap.rs new file mode 100644 index 0000000..a7a4ef2 --- /dev/null +++ b/ferri-main/src/types/ap.rs @@ -0,0 +1,223 @@ +use super::*; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub enum ActivityType { + Reject, + Create, + Note, + Delete, + Undo, + Accept, + Announce, + Person, + Like, + Follow, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct MinimalActivity { + #[serde(flatten)] + pub obj: Object, + + #[serde(rename = "type")] + pub ty: ActivityType, +} + +pub type DeleteActivity = BasicActivity; +pub type LikeActivity = BasicActivity; + +#[derive(Serialize, Deserialize, Debug)] +pub struct BasicActivity { + #[serde(flatten)] + pub obj: Object, + + pub object: String, + pub actor: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct CreateActivity { + #[serde(flatten)] + pub obj: Object, + + #[serde(rename = "type")] + pub ty: ActivityType, + + pub object: Post, + pub actor: String, + pub to: Vec, + pub cc: Vec, + + #[serde(rename = "published")] + pub ts: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct FollowActivity { + #[serde(flatten)] + pub obj: Object, + + #[serde(rename = "type")] + pub ty: ActivityType, + + pub object: String, + pub actor: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct AcceptActivity { + #[serde(flatten)] + pub obj: Object, + + #[serde(rename = "type")] + pub ty: ActivityType, + + pub object: String, + pub actor: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct BoostActivity { + #[serde(flatten)] + pub obj: Object, + + #[serde(rename = "type")] + pub ty: ActivityType, + + pub actor: String, + pub published: String, + pub to: Vec, + pub cc: Vec, + pub object: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum PostAttachmentType { + Document +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct PostAttachment { + #[serde(rename = "type")] + pub ty: PostAttachmentType, + + pub media_type: String, + pub url: String, + #[serde(deserialize_with = "deserialize_null_default")] + pub name: String, + + pub summary: Option, + #[serde(default)] + pub sensitive: bool +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Post { + #[serde(flatten)] + pub obj: Object, + + #[serde(rename = "type")] + pub ty: ActivityType, + + #[serde(rename = "published")] + pub ts: String, + pub content: String, + pub to: Vec, + pub cc: Vec, + + pub attachment: Vec, + + #[serde(rename = "attributedTo")] + pub attributed_to: Option, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct Actor { + #[serde(flatten)] + pub obj: Object, + + pub inbox: String, + pub outbox: String, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub enum IconType { + Image +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct PersonIcon { + #[serde(rename = "type")] + pub ty: IconType, + pub url: String, + + #[serde(default)] + pub summary: String, + #[serde(default)] + pub width: i64, + #[serde(default)] + pub height: i64 +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Person { + #[serde(flatten)] + pub obj: Object, + + #[serde(rename = "type")] + pub ty: ActivityType, + + 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, + + pub icon: Option +} + +pub struct RemoteInfo { + pub is_remote: bool, + pub web_url: String, + pub acct: String +} + +impl Person { + pub fn remote_info(&self) -> RemoteInfo { + let url = Url::parse(&self.obj.id.0).unwrap(); + let host = url.host_str().unwrap(); + + let (acct, remote) = if host != "ferri.amy.mov" { + (format!("{}@{}", self.preferred_username, host), true) + } else { + (self.preferred_username.clone(), false) + }; + + let url = format!("https://ferri.amy.mov/{}", acct); + + RemoteInfo { + acct: acct.to_string(), + web_url: url, + is_remote: remote, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct UserKey { + pub id: String, + pub owner: String, + + #[serde(rename = "publicKeyPem")] + pub public_key: String, +} diff --git a/ferri-main/src/types/api.rs b/ferri-main/src/types/api.rs new file mode 100644 index 0000000..81594a5 --- /dev/null +++ b/ferri-main/src/types/api.rs @@ -0,0 +1,231 @@ +use super::*; +use serde::{Deserialize, Serialize}; + +// 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)] +pub struct CredentialApplication { + pub name: String, + pub scopes: String, + pub redirect_uris: Vec, + pub client_id: String, + pub client_secret: String, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct WebfingerLink { + pub rel: String, + #[serde(rename = "type")] + pub ty: Option, + pub href: Option, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct WebfingerHit { + pub subject: String, + pub aliases: Vec, + pub links: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct StatusAttachment { + pub id: ObjectUuid, + #[serde(rename = "type")] + pub ty: String, + + pub url: String, + pub description: String +} + +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct Status { + pub id: ObjectUuid, + pub created_at: String, + pub in_reply_to_id: Option, + pub in_reply_to_account_id: Option, + pub sensitive: bool, + pub spoiler_text: String, + pub visibility: String, + pub language: String, + pub uri: ObjectUri, + pub url: String, + pub replies_count: i64, + pub reblogs_count: i64, + pub favourites_count: i64, + pub favourited: bool, + pub reblogged: bool, + pub muted: bool, + pub bookmarked: bool, + pub content: String, + pub reblog: Option>, + pub application: Option<()>, + pub account: Account, + pub media_attachments: Vec, + pub mentions: Vec>, + pub tags: Vec>, + pub emojis: Vec>, + pub card: Option<()>, + pub poll: Option<()>, +} + +#[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, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Instance { + pub domain: String, + pub title: String, + pub version: String, + pub source_url: String, + pub description: String, + pub thumbnail: Thumbnail, + pub icon: Vec, + pub languages: Vec, + pub configuration: Configuration, + pub registrations: Registrations, + pub contact: Contact, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Configuration { + pub urls: Urls, + pub accounts: Accounts, + pub statuses: Statuses, + pub media_attachments: MediaAttachments, + pub polls: Polls, + pub translation: Translation, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Accounts { + pub max_featured_tags: i64, + pub max_pinned_statuses: i64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct MediaAttachments { + pub supported_mime_types: Vec, + pub description_limit: i64, + pub image_size_limit: i64, + pub image_matrix_limit: i64, + pub video_size_limit: i64, + pub video_frame_rate_limit: i64, + pub video_matrix_limit: i64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Polls { + pub max_options: i64, + pub max_characters_per_option: i64, + pub min_expiration: i64, + pub max_expiration: i64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Statuses { + pub max_characters: i64, + pub max_media_attachments: i64, + pub characters_reserved_per_url: i64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Translation { + pub enabled: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Urls { + pub streaming: String, + pub about: String, + pub privacy_policy: String, + pub terms_of_service: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Contact { + pub email: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Field { + pub name: String, + pub value: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Icon { + pub src: String, + pub size: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Registrations { + pub enabled: bool, + pub approval_required: bool, + pub reason_required: bool, + pub message: Option, + pub min_age: i64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Thumbnail { + pub url: String, +} + +#[derive(Serialize, Deserialize)] +pub struct Preferences { + #[serde(rename = "posting:default:visibility")] + pub posting_default_visibility: String, + #[serde(rename = "posting:default:sensitive")] + pub posting_default_sensitive: bool, + #[serde(rename = "posting:default:language")] + pub posting_default_language: Option, + #[serde(rename = "reading:expand:media")] + pub reading_expand_media: String, + #[serde(rename = "reading:expand:spoilers")] + pub reading_expand_spoilers: bool, +} diff --git a/ferri-main/src/types/db.rs b/ferri-main/src/types/db.rs new file mode 100644 index 0000000..2c8ce0e --- /dev/null +++ b/ferri-main/src/types/db.rs @@ -0,0 +1,59 @@ +use super::*; +use chrono::{DateTime, Utc}; + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct Follow { + pub id: ObjectUri, + pub follower: ObjectUri, + pub followed: ObjectUri, +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct Actor { + pub id: ObjectUri, + pub inbox: String, + pub outbox: String, +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct UserPosts { + // User may have no posts + pub last_post_at: Option>, +} + +#[derive(Debug, Eq, PartialEq, Clone)] +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 icon_url: String, + + pub posts: UserPosts, +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct Attachment { + pub id: ObjectUuid, + pub post_id: ObjectUuid, + pub url: String, + pub media_type: Option, + pub sensitive: bool, + pub alt: Option +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct Post { + pub id: ObjectUuid, + pub uri: ObjectUri, + pub user: User, + pub content: String, + pub created_at: DateTime, + pub boosted_post: Option>, + pub attachments: Vec +} + diff --git a/ferri-main/src/types/mod.rs b/ferri-main/src/types/mod.rs index 35c4a43..61487dd 100644 --- a/ferri-main/src/types/mod.rs +++ b/ferri-main/src/types/mod.rs @@ -7,6 +7,10 @@ pub mod convert; pub mod get; pub mod make; +pub mod db; +pub mod ap; +pub mod api; + fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result where T: Default + Deserialize<'de>, @@ -67,540 +71,3 @@ pub struct Object { pub context: ObjectContext, pub id: ObjectUri, } - -pub mod db { - use super::*; - use chrono::{DateTime, Utc}; - - #[derive(Debug, Eq, PartialEq, Clone)] - pub struct Follow { - pub id: ObjectUri, - pub follower: ObjectUri, - pub followed: ObjectUri, - } - - #[derive(Debug, Eq, PartialEq, Clone)] - pub struct Actor { - pub id: ObjectUri, - pub inbox: String, - pub outbox: String, - } - - #[derive(Debug, Eq, PartialEq, Clone)] - pub struct UserPosts { - // User may have no posts - pub last_post_at: Option>, - } - - #[derive(Debug, Eq, PartialEq, Clone)] - 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 icon_url: String, - - pub posts: UserPosts, - } - - #[derive(Debug, Eq, PartialEq, Clone)] - pub struct Attachment { - pub id: ObjectUuid, - pub post_id: ObjectUuid, - pub url: String, - pub media_type: Option, - pub sensitive: bool, - pub alt: Option - } - - #[derive(Debug, Eq, PartialEq, Clone)] - pub struct Post { - pub id: ObjectUuid, - pub uri: ObjectUri, - pub user: User, - pub content: String, - pub created_at: DateTime, - pub boosted_post: Option>, - pub attachments: Vec - } -} - -pub mod ap { - use super::*; - use serde::{Deserialize, Serialize}; - use url::Url; - - #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] - pub enum ActivityType { - Reject, - Create, - Note, - Delete, - Undo, - Accept, - Announce, - Person, - Like, - Follow, - } - - #[derive(Serialize, Deserialize, Debug)] - pub struct MinimalActivity { - #[serde(flatten)] - pub obj: Object, - - #[serde(rename = "type")] - pub ty: ActivityType, - } - - pub type DeleteActivity = BasicActivity; - pub type LikeActivity = BasicActivity; - - #[derive(Serialize, Deserialize, Debug)] - pub struct BasicActivity { - #[serde(flatten)] - pub obj: Object, - - pub object: String, - pub actor: String, - } - - #[derive(Serialize, Deserialize, Debug)] - pub struct CreateActivity { - #[serde(flatten)] - pub obj: Object, - - #[serde(rename = "type")] - pub ty: ActivityType, - - pub object: Post, - pub actor: String, - pub to: Vec, - pub cc: Vec, - - #[serde(rename = "published")] - pub ts: String, - } - - #[derive(Serialize, Deserialize, Debug)] - pub struct FollowActivity { - #[serde(flatten)] - pub obj: Object, - - #[serde(rename = "type")] - pub ty: ActivityType, - - pub object: String, - pub actor: String, - } - - #[derive(Serialize, Deserialize, Debug)] - pub struct AcceptActivity { - #[serde(flatten)] - pub obj: Object, - - #[serde(rename = "type")] - pub ty: ActivityType, - - pub object: String, - pub actor: String, - } - - #[derive(Serialize, Deserialize, Debug)] - pub struct BoostActivity { - #[serde(flatten)] - pub obj: Object, - - #[serde(rename = "type")] - pub ty: ActivityType, - - pub actor: String, - pub published: String, - pub to: Vec, - pub cc: Vec, - pub object: String, - } - - #[derive(Serialize, Deserialize, Debug)] - pub enum PostAttachmentType { - Document - } - - #[derive(Serialize, Deserialize, Debug)] - #[serde(rename_all = "camelCase")] - pub struct PostAttachment { - #[serde(rename = "type")] - pub ty: PostAttachmentType, - - pub media_type: String, - pub url: String, - #[serde(deserialize_with = "deserialize_null_default")] - pub name: String, - - pub summary: Option, - #[serde(default)] - pub sensitive: bool - } - - #[derive(Serialize, Deserialize, Debug)] - pub struct Post { - #[serde(flatten)] - pub obj: Object, - - #[serde(rename = "type")] - pub ty: ActivityType, - - #[serde(rename = "published")] - pub ts: String, - pub content: String, - pub to: Vec, - pub cc: Vec, - - pub attachment: Vec, - - #[serde(rename = "attributedTo")] - pub attributed_to: Option, - } - - #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] - pub struct Actor { - #[serde(flatten)] - pub obj: Object, - - pub inbox: String, - pub outbox: String, - } - - #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] - pub enum IconType { - Image - } - - #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] - pub struct PersonIcon { - #[serde(rename = "type")] - pub ty: IconType, - pub url: String, - - #[serde(default)] - pub summary: String, - #[serde(default)] - pub width: i64, - #[serde(default)] - pub height: i64 - } - - #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] - #[serde(rename_all = "camelCase")] - pub struct Person { - #[serde(flatten)] - pub obj: Object, - - #[serde(rename = "type")] - pub ty: ActivityType, - - 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, - - pub icon: Option - } - - pub struct RemoteInfo { - pub is_remote: bool, - pub web_url: String, - pub acct: String - } - - impl Person { - pub fn remote_info(&self) -> RemoteInfo { - let url = Url::parse(&self.obj.id.0).unwrap(); - let host = url.host_str().unwrap(); - - let (acct, remote) = if host != "ferri.amy.mov" { - (format!("{}@{}", self.preferred_username, host), true) - } else { - (self.preferred_username.clone(), false) - }; - - let url = format!("https://ferri.amy.mov/{}", acct); - - RemoteInfo { - acct: acct.to_string(), - web_url: url, - is_remote: remote, - } - } - } - - #[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 { - use super::*; - use serde::{Deserialize, Serialize}; - - // 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)] - pub struct CredentialApplication { - pub name: String, - pub scopes: String, - pub redirect_uris: Vec, - pub client_id: String, - pub client_secret: String, - } - - #[derive(Deserialize, Serialize, Debug)] - pub struct WebfingerLink { - pub rel: String, - #[serde(rename = "type")] - pub ty: Option, - pub href: Option, - } - - #[derive(Deserialize, Serialize, Debug)] - pub struct WebfingerHit { - pub subject: String, - pub aliases: Vec, - pub links: Vec, - } - - #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] - pub struct StatusAttachment { - pub id: ObjectUuid, - #[serde(rename = "type")] - pub ty: String, - - pub url: String, - pub description: String - } - - #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] - pub struct Status { - pub id: ObjectUuid, - pub created_at: String, - pub in_reply_to_id: Option, - pub in_reply_to_account_id: Option, - pub sensitive: bool, - pub spoiler_text: String, - pub visibility: String, - pub language: String, - pub uri: ObjectUri, - pub url: String, - pub replies_count: i64, - pub reblogs_count: i64, - pub favourites_count: i64, - pub favourited: bool, - pub reblogged: bool, - pub muted: bool, - pub bookmarked: bool, - pub content: String, - pub reblog: Option>, - pub application: Option<()>, - pub account: Account, - pub media_attachments: Vec, - pub mentions: Vec>, - pub tags: Vec>, - pub emojis: Vec>, - pub card: Option<()>, - pub poll: Option<()>, - } - - #[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, - } - - #[derive(Debug, Serialize, Deserialize)] - pub struct Instance { - pub domain: String, - pub title: String, - pub version: String, - pub source_url: String, - pub description: String, - pub thumbnail: Thumbnail, - pub icon: Vec, - pub languages: Vec, - pub configuration: Configuration, - pub registrations: Registrations, - pub contact: Contact, - } - - #[derive(Debug, Serialize, Deserialize)] - pub struct Configuration { - pub urls: Urls, - pub accounts: Accounts, - pub statuses: Statuses, - pub media_attachments: MediaAttachments, - pub polls: Polls, - pub translation: Translation, - } - - #[derive(Debug, Serialize, Deserialize)] - pub struct Accounts { - pub max_featured_tags: i64, - pub max_pinned_statuses: i64, - } - - #[derive(Debug, Serialize, Deserialize)] - pub struct MediaAttachments { - pub supported_mime_types: Vec, - pub description_limit: i64, - pub image_size_limit: i64, - pub image_matrix_limit: i64, - pub video_size_limit: i64, - pub video_frame_rate_limit: i64, - pub video_matrix_limit: i64, - } - - #[derive(Debug, Serialize, Deserialize)] - pub struct Polls { - pub max_options: i64, - pub max_characters_per_option: i64, - pub min_expiration: i64, - pub max_expiration: i64, - } - - #[derive(Debug, Serialize, Deserialize)] - pub struct Statuses { - pub max_characters: i64, - pub max_media_attachments: i64, - pub characters_reserved_per_url: i64, - } - - #[derive(Debug, Serialize, Deserialize)] - pub struct Translation { - pub enabled: bool, - } - - #[derive(Debug, Serialize, Deserialize)] - pub struct Urls { - pub streaming: String, - pub about: String, - pub privacy_policy: String, - pub terms_of_service: String, - } - - #[derive(Debug, Serialize, Deserialize)] - pub struct Contact { - pub email: String, - } - - #[derive(Debug, Serialize, Deserialize)] - pub struct Field { - pub name: String, - pub value: String, - } - - #[derive(Debug, Serialize, Deserialize)] - pub struct Icon { - pub src: String, - pub size: String, - } - - #[derive(Debug, Serialize, Deserialize)] - pub struct Registrations { - pub enabled: bool, - pub approval_required: bool, - pub reason_required: bool, - pub message: Option, - pub min_age: i64, - } - - #[derive(Debug, Serialize, Deserialize)] - pub struct Thumbnail { - pub url: String, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn ap_actor_to_db() { - let domain = "https://example.com"; - - let ap = ap::Actor { - obj: Object { - context: as_context(), - id: ObjectUri(format!("{}/users/sample", domain)), - }, - inbox: format!("{}/users/sample/inbox", domain), - outbox: format!("{}/users/sample/outbox", domain), - }; - - let db: db::Actor = ap.into(); - - assert_eq!( - db, - db::Actor { - id: ObjectUri("https://example.com/users/sample".to_string()), - inbox: "https://example.com/users/sample/inbox".to_string(), - outbox: "https://example.com/users/sample/outbox".to_string(), - } - ); - } -} diff --git a/ferri-server/src/endpoints/admin/mod.rs b/ferri-server/src/endpoints/admin/mod.rs index 51e6ec7..174b968 100644 --- a/ferri-server/src/endpoints/admin/mod.rs +++ b/ferri-server/src/endpoints/admin/mod.rs @@ -1,20 +1,14 @@ -use rocket::{get, post, response::content::RawHtml}; +use rocket::{get, response::content::RawHtml}; use askama::Template; #[derive(Template)] #[template(path = "index.html")] struct IndexTemplate { - val: String -} -#[post("/clicked")] -pub async fn button_clicked() -> RawHtml { - let tmpl = IndexTemplate { val: "clicked".to_string() }; - RawHtml(tmpl.render().unwrap()) } #[get("/")] pub async fn index() -> RawHtml { - let tmpl = IndexTemplate { val: "test".to_string() }; + let tmpl = IndexTemplate { }; RawHtml(tmpl.render().unwrap()) } diff --git a/ferri-server/src/endpoints/api/preferences.rs b/ferri-server/src/endpoints/api/preferences.rs index a06db49..afe4e56 100644 --- a/ferri-server/src/endpoints/api/preferences.rs +++ b/ferri-server/src/endpoints/api/preferences.rs @@ -1,26 +1,12 @@ +use main::types::api; use rocket::{ get, - serde::{Deserialize, Serialize, json::Json}, + serde::json::Json, }; -#[derive(Serialize, Deserialize)] -#[serde(crate = "rocket::serde")] -pub struct Preferences { - #[serde(rename = "posting:default:visibility")] - pub posting_default_visibility: String, - #[serde(rename = "posting:default:sensitive")] - pub posting_default_sensitive: bool, - #[serde(rename = "posting:default:language")] - pub posting_default_language: Option, - #[serde(rename = "reading:expand:media")] - pub reading_expand_media: String, - #[serde(rename = "reading:expand:spoilers")] - pub reading_expand_spoilers: bool, -} - #[get("/preferences")] -pub async fn preferences() -> Json { - Json(Preferences { +pub async fn preferences() -> Json { + Json(api::Preferences { posting_default_visibility: "public".to_string(), posting_default_sensitive: false, posting_default_language: None, diff --git a/ferri-server/src/endpoints/api/timeline.rs b/ferri-server/src/endpoints/api/timeline.rs index 582b397..f6c82d0 100644 --- a/ferri-server/src/endpoints/api/timeline.rs +++ b/ferri-server/src/endpoints/api/timeline.rs @@ -1,48 +1,11 @@ -use crate::{AuthenticatedUser, Db, endpoints::api::user::CredentialAcount}; -use main::types::{api, get, ObjectUuid}; +use crate::{AuthenticatedUser, Db}; +use main::types::{api, get}; use rocket::{ get, - serde::{Deserialize, Serialize, json::Json}, + serde::json::Json, }; use rocket_db_pools::Connection; -pub type TimelineAccount = CredentialAcount; - -#[derive(Debug, Serialize, Deserialize)] -#[serde(crate = "rocket::serde")] -pub struct TimelineStatusAttachment { - id: ObjectUuid, - #[serde(rename = "type")] - ty: String, - url: String, - description: String -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(crate = "rocket::serde")] -pub struct TimelineStatus { - pub id: String, - pub created_at: String, - pub in_reply_to_id: Option, - pub in_reply_to_account_id: Option, - pub content: String, - pub visibility: String, - pub spoiler_text: String, - pub sensitive: bool, - pub uri: String, - pub url: String, - pub replies_count: i64, - pub reblogs_count: i64, - pub favourites_count: i64, - pub favourited: bool, - pub reblogged: bool, - pub muted: bool, - pub bookmarked: bool, - pub reblog: Option>, - pub media_attachments: Vec, - pub account: TimelineAccount, -} - #[get("/timelines/home")] pub async fn home( mut db: Connection, @@ -50,7 +13,10 @@ pub async fn home( ) -> Json> { let posts = get::home_timeline(user.actor_id, &mut **db) .await - .unwrap(); + .unwrap() + .into_iter() + .map(|p| p.into()) + .collect(); - Json(posts.into_iter().map(Into::into).collect()) + Json(posts) } diff --git a/ferri-server/src/endpoints/api/user.rs b/ferri-server/src/endpoints/api/user.rs index 3f60f86..6cb86a0 100644 --- a/ferri-server/src/endpoints/api/user.rs +++ b/ferri-server/src/endpoints/api/user.rs @@ -1,4 +1,5 @@ use main::ap; +use main::types::{api, get, ObjectUuid}; use rocket::response::status::NotFound; use rocket::{ State, get, post, @@ -6,8 +7,8 @@ use rocket::{ }; use rocket_db_pools::Connection; use uuid::Uuid; +use tracing::info; -use crate::timeline::{TimelineAccount, TimelineStatus}; use crate::{AuthenticatedUser, Db}; #[derive(Debug, Serialize, Deserialize)] @@ -34,7 +35,8 @@ pub struct CredentialAcount { } #[get("/accounts/verify_credentials")] -pub async fn verify_credentials() -> Json { +pub async fn verify_credentials(user: AuthenticatedUser) -> Json { + info!("verifying creds for {:#?}", user); Json(CredentialAcount { id: "9b9d497b-2731-435f-a929-e609ca69dac9".to_string(), username: "amy".to_string(), @@ -98,31 +100,12 @@ pub async fn account( mut db: Connection, uuid: &str, _user: AuthenticatedUser, -) -> Result, NotFound> { - let user = ap::User::from_id(uuid, &mut **db) +) -> Result, NotFound> { + let user = get::user_by_id(ObjectUuid(uuid.to_string()), &mut **db) .await .map_err(|e| NotFound(e.to_string()))?; - let user_uri = format!("https://ferri.amy.mov/users/{}", user.username()); - Ok(Json(CredentialAcount { - id: user.id().to_string(), - username: user.username().to_string(), - acct: user.username().to_string(), - display_name: user.display_name().to_string(), - 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(), - })) + + Ok(Json(user.into())) } #[get("/accounts//statuses?<_limit>")] @@ -131,69 +114,17 @@ pub async fn statuses( uuid: &str, _limit: Option, _user: AuthenticatedUser, -) -> Result>, NotFound> { - let user = ap::User::from_id(uuid, &mut **db) +) -> Result>, NotFound> { + let user = get::user_by_id(ObjectUuid(uuid.to_string()), &mut **db) .await .map_err(|e| NotFound(e.to_string()))?; - let uid = 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 - FROM post p - INNER JOIN user u on p.user_id = u.id - WHERE u.id = ?1 - ORDER BY p.created_at DESC - "#, uid) - .fetch_all(&mut **db) - .await - .unwrap(); - - let mut out = Vec::::new(); - for record in posts { - let user_uri = format!("https://ferri.amy.mov/users/{}", 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: None, - 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(), - }, - }); - } - - Ok(Json(out)) + let posts = get::posts_for_user_id(user.id, &mut **db) + .await + .unwrap() + .into_iter() + .map(|p| p.into()) + .collect(); + + Ok(Json(posts)) } diff --git a/ferri-server/src/http_wrapper.rs b/ferri-server/src/http_wrapper.rs index 38c51be..e065010 100644 --- a/ferri-server/src/http_wrapper.rs +++ b/ferri-server/src/http_wrapper.rs @@ -22,10 +22,6 @@ impl<'a> HttpWrapper<'a> { Self { client, key_id } } - pub fn client(&self) -> &'a HttpClient { - self.client - } - async fn get( &self, ty: &str, diff --git a/ferri-server/src/lib.rs b/ferri-server/src/lib.rs index 5b8b59e..0154ded 100644 --- a/ferri-server/src/lib.rs +++ b/ferri-server/src/lib.rs @@ -134,7 +134,6 @@ pub fn launch(cfg: Config) -> Rocket { "/admin", routes![ admin::index, - admin::button_clicked ] ) .mount( diff --git a/ferri-server/templates/_layout.html b/ferri-server/templates/_layout.html new file mode 100644 index 0000000..9cc9117 --- /dev/null +++ b/ferri-server/templates/_layout.html @@ -0,0 +1,19 @@ + + + + + + + + + {% block title %}{% endblock %} + + {%~ block styles ~%} {% endblock ~%} + + + + + + {%~ block content %}{% endblock ~%} + + diff --git a/ferri-server/templates/index.html b/ferri-server/templates/index.html index f912bcf..50db608 100644 --- a/ferri-server/templates/index.html +++ b/ferri-server/templates/index.html @@ -1,19 +1,45 @@ - - - - - - - - - Ferri Test - - - - - - - - +{% extends "_layout.html" %} + +{%- block title -%} +Control panel +{%- endblock -%} + +{%- block styles -%} + +{%- endblock -%} + +{%- block content -%} +
+
+

Test

+
+ +
+

Test

+
+ +
+

Test

+
+ +
+

Test

+
+
+{%- endblock -%} From ab9836293e3762a60606d310c01d6ab6acd1fea9 Mon Sep 17 00:00:00 2001 From: nullishamy Date: Tue, 6 May 2025 22:25:15 +0100 Subject: [PATCH 2/2] refactor: cleanup; add missing APIs; reorganise --- ferri-cli/src/main.rs | 6 +- ferri-main/src/ap/activity.rs | 141 --------------- ferri-main/src/ap/http.rs | 199 --------------------- ferri-main/src/ap/mod.rs | 30 ---- ferri-main/src/ap/post.rs | 113 ------------ ferri-main/src/ap/request_queue.rs | 55 ------ ferri-main/src/ap/user.rs | 154 ---------------- ferri-main/src/federation/http.rs | 199 ++++++++++++++++++++- ferri-main/src/federation/inbox.rs | 38 ++-- ferri-main/src/federation/outbox.rs | 51 +++++- ferri-main/src/federation/request_queue.rs | 2 +- ferri-main/src/lib.rs | 10 +- ferri-main/src/types/api.rs | 18 ++ ferri-main/src/types/db.rs | 1 + ferri-main/src/types/get.rs | 175 +++++++++++++++++- ferri-server/src/endpoints/api/search.rs | 4 +- ferri-server/src/endpoints/api/status.rs | 2 +- ferri-server/src/endpoints/api/user.rs | 53 +++--- ferri-server/src/endpoints/user.rs | 56 ++---- ferri-server/src/endpoints/well_known.rs | 17 +- ferri-server/src/http_wrapper.rs | 70 -------- ferri-server/src/lib.rs | 5 +- 22 files changed, 529 insertions(+), 870 deletions(-) delete mode 100644 ferri-main/src/ap/activity.rs delete mode 100644 ferri-main/src/ap/http.rs delete mode 100644 ferri-main/src/ap/mod.rs delete mode 100644 ferri-main/src/ap/post.rs delete mode 100644 ferri-main/src/ap/request_queue.rs delete mode 100644 ferri-main/src/ap/user.rs diff --git a/ferri-cli/src/main.rs b/ferri-cli/src/main.rs index afbe020..e785c88 100644 --- a/ferri-cli/src/main.rs +++ b/ferri-cli/src/main.rs @@ -57,11 +57,13 @@ async fn main() { acct: s("amy"), remote: false, url: s("https://ferri.amy.mov/@amy"), - created_at: main::ap::now(), + created_at: main::now(), icon_url: s("https://ferri.amy.mov/assets/pfp.png"), posts: db::UserPosts { last_post_at: None - } + }, + key_id: s("https://ferri.amy.mov/users/9b9d497b-2731-435f-a929-e609ca69dac9#main-key") + }; make::new_user(user, &mut *conn).await.unwrap(); diff --git a/ferri-main/src/ap/activity.rs b/ferri-main/src/ap/activity.rs deleted file mode 100644 index 80f45e7..0000000 --- a/ferri-main/src/ap/activity.rs +++ /dev/null @@ -1,141 +0,0 @@ -use crate::ap::{Actor, User, http}; -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use sqlx::Sqlite; -use std::fmt::Debug; -use tracing::{Level, event}; - -#[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: Utc::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) { - event!(Level::INFO, ?activity, "activity in outbox"); - - let raw = RawActivity { - context: "https://www.w3.org/ns/activitystreams".to_string(), - id: activity.req.id.clone(), - ty: activity.req.ty.to_raw(), - actor: self.user.actor().id().to_string(), - object: activity.req.object, - published: activity.req.published.to_rfc3339(), - }; - - let outbox_res = self - .transport - .post(activity.to.inbox()) - .activity() - .json(&raw) - .sign(&activity.signed_by) - .send() - .await - .unwrap() - .text() - .await - .unwrap(); - - event!( - Level::DEBUG, - outbox_res, - activity = activity.req.id, - "got response for outbox dispatch" - ); - } - - 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 deleted file mode 100644 index 5b8f20b..0000000 --- a/ferri-main/src/ap/http.rs +++ /dev/null @@ -1,199 +0,0 @@ -use reqwest::{IntoUrl, Response}; -use serde::Serialize; -use url::Url; - -use rsa::{ - RsaPrivateKey, - pkcs1v15::SigningKey, - pkcs8::DecodePrivateKey, - sha2::{Digest, Sha256}, - signature::{RandomizedSigner, SignatureEncoding}, -}; - -use base64::prelude::*; -use chrono::Utc; -use tracing::{Level, event}; - -pub struct HttpClient { - client: reqwest::Client, -} - -#[derive(Debug)] -pub struct PostSignature { - date: String, - digest: String, - signature: String, -} - -#[derive(Debug)] -struct GetSignature { - date: String, - signature: String, -} - -enum RequestVerb { - GET, - POST, -} - -pub struct RequestBuilder { - verb: RequestVerb, - url: Url, - body: String, - inner: reqwest::RequestBuilder, -} - -impl RequestBuilder { - pub fn json(mut self, json: impl Serialize + Sized) -> RequestBuilder { - let body = serde_json::to_string(&json).unwrap(); - self.inner = self.inner.body(body.clone()); - self.body = body; - self - } - - pub fn activity(mut self) -> RequestBuilder { - self.inner = self - .inner - .header("Content-Type", "application/activity+json") - .header("Accept", "application/activity+json"); - self - } - - pub async fn send(self) -> Result { - event!(Level::DEBUG, ?self.inner, "sending an http request"); - - self.inner.send().await - } - - pub fn sign(mut self, key_id: &str) -> RequestBuilder { - match self.verb { - RequestVerb::GET => { - let sig = self.sign_get_request(key_id); - 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 - .header("Date", sig.date) - .header("Digest", sig.digest) - .header("Signature", sig.signature); - self - } - } - } - - fn sign_get_request(&self, key_id: &str) -> GetSignature { - let url = &self.url; - let host = url.host_str().unwrap(); - let path = url.path(); - - 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 - // RFC7231 is hardcoded to use GMT for.. some reason - let ts = Utc::now(); - - // RFC7231 string - let date = ts.format("%a, %d %b %Y %H:%M:%S GMT").to_string(); - - let to_sign = format!( - "(request-target): get {}\nhost: {}\ndate: {}", - path, host, date - ); - - let signature = signing_key.sign_with_rng(&mut rand::rngs::OsRng, &to_sign.into_bytes()); - let header = format!( - "keyId=\"{}\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date\",signature=\"{}\"", - key_id, - BASE64_STANDARD.encode(signature.to_bytes()) - ); - - GetSignature { - date, - signature: header, - } - } - - fn sign_post_request(&self, key_id: &str) -> PostSignature { - let body = &self.body; - let url = &self.url; - - let host = url.host_str().unwrap(); - let path = url.path(); - - let private_key = - RsaPrivateKey::from_pkcs8_pem(include_str!("../../../private.pem")).unwrap(); - let signing_key = SigningKey::::new(private_key); - - let mut hasher = Sha256::new(); - hasher.update(body); - let sha256 = hasher.finalize(); - - let b64 = BASE64_STANDARD.encode(sha256); - let digest = format!("SHA-256={}", b64); - - // UTC=GMT for our purposes, use it - // RFC7231 is hardcoded to use GMT for.. some reason - let ts = Utc::now(); - - // RFC7231 string - let date = ts.format("%a, %d %b %Y %H:%M:%S GMT").to_string(); - - let to_sign = format!( - "(request-target): post {}\nhost: {}\ndate: {}\ndigest: {}", - path, host, date, digest - ); - - let signature = signing_key.sign_with_rng(&mut rand::rngs::OsRng, &to_sign.into_bytes()); - let header = format!( - "keyId=\"{}\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest\",signature=\"{}\"", - key_id, - BASE64_STANDARD.encode(signature.to_bytes()) - ); - - PostSignature { - date, - digest, - signature: header, - } - } -} - -impl Default for HttpClient { - fn default() -> Self { - Self::new() - } -} - -impl HttpClient { - pub fn new() -> Self { - Self { - client: reqwest::Client::new(), - } - } - - pub fn get(&self, url: impl IntoUrl + Clone) -> RequestBuilder { - RequestBuilder { - verb: RequestVerb::GET, - url: url.clone().into_url().unwrap(), - body: String::new(), - inner: self.client.get(url), - } - } - - pub fn post(&self, url: impl IntoUrl + Clone) -> RequestBuilder { - RequestBuilder { - verb: RequestVerb::POST, - url: url.clone().into_url().unwrap(), - body: String::new(), - inner: self.client.post(url), - } - } -} diff --git a/ferri-main/src/ap/mod.rs b/ferri-main/src/ap/mod.rs deleted file mode 100644 index 98d7330..0000000 --- a/ferri-main/src/ap/mod.rs +++ /dev/null @@ -1,30 +0,0 @@ -use chrono::{DateTime, Utc}; -use uuid::Uuid; - -pub mod http; - -mod activity; -pub use activity::*; - -mod user; -pub use user::*; - -mod post; -pub use post::*; - -mod request_queue; -pub use request_queue::*; - -pub const AS_CONTEXT: &str = "https://www.w3.org/ns/activitystreams"; - -pub fn new_id() -> String { - Uuid::new_v4().to_string() -} - -pub fn new_ts() -> String { - now().to_rfc3339() -} - -pub fn now() -> DateTime { - Utc::now() -} diff --git a/ferri-main/src/ap/post.rs b/ferri-main/src/ap/post.rs deleted file mode 100644 index a42a246..0000000 --- a/ferri-main/src/ap/post.rs +++ /dev/null @@ -1,113 +0,0 @@ -use crate::ap; -use chrono::{DateTime, Utc}; -use serde::Serialize; -use sqlx::Sqlite; - -const POST_TYPE: &str = "Note"; - -#[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/request_queue.rs b/ferri-main/src/ap/request_queue.rs deleted file mode 100644 index fc5ebeb..0000000 --- a/ferri-main/src/ap/request_queue.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::sync::mpsc; -use std::thread; -use tracing::{Level, info, span}; - -#[derive(Debug)] -pub enum QueueMessage { - Heartbeat, -} - -pub struct RequestQueue { - name: &'static str, - send: mpsc::Sender, - recv: mpsc::Receiver, -} - -#[derive(Clone)] -pub struct QueueHandle { - send: mpsc::Sender, -} - -impl QueueHandle { - pub fn send(&self, msg: QueueMessage) { - self.send.send(msg).unwrap(); - } -} - -impl RequestQueue { - pub fn new(name: &'static str) -> Self { - let (send, recv) = mpsc::channel(); - Self { name, send, recv } - } - - pub fn spawn(self) -> QueueHandle { - info!("starting up queue '{}'", self.name); - - thread::spawn(move || { - info!("queue '{}' up", self.name); - let recv = self.recv; - - while let Ok(req) = recv.recv() { - // FIXME: When we make this do async things we will need to add tokio and - // use proper async handled spans as the enter/drop won't work. - // See inbox.rs for how we handle that. - let s = span!(Level::INFO, "queue", queue_name = self.name); - let _enter = s.enter(); - - info!(?req, "got a message into the queue"); - - drop(_enter); - } - }); - - QueueHandle { send: self.send } - } -} diff --git a/ferri-main/src/ap/user.rs b/ferri-main/src/ap/user.rs deleted file mode 100644 index 3edd614..0000000 --- a/ferri-main/src/ap/user.rs +++ /dev/null @@ -1,154 +0,0 @@ -use sqlx::Sqlite; -use std::fmt::Debug; -use thiserror::Error; - -#[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 - } - - pub fn outbox(&self) -> &str { - &self.outbox - } -} - -#[derive(Debug, Clone)] -pub struct User { - id: String, - username: String, - actor: Actor, - display_name: String, -} - -#[derive(Error, Debug)] -pub enum UserError { - #[error("user `{0}` not found")] - NotFound(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>, - ) -> Result { - 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 - .map_err(|_| UserError::NotFound(uuid.to_string()))?; - - Ok(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-main/src/federation/http.rs b/ferri-main/src/federation/http.rs index d6e592d..65fcc52 100644 --- a/ferri-main/src/federation/http.rs +++ b/ferri-main/src/federation/http.rs @@ -1,10 +1,23 @@ -use crate::ap::http::HttpClient; use crate::types::ap; use std::fmt::Debug; use serde::Serialize; use thiserror::Error; use tracing::{Level, error, event, info}; +use reqwest::{IntoUrl, Response}; +use url::Url; + +use rsa::{ + RsaPrivateKey, + pkcs1v15::SigningKey, + pkcs8::DecodePrivateKey, + sha2::{Digest, Sha256}, + signature::{RandomizedSigner, SignatureEncoding}, +}; + +use base64::prelude::*; +use chrono::Utc; + use super::outbox::PreparedActivity; pub struct HttpWrapper<'a> { @@ -108,3 +121,187 @@ impl<'a> HttpWrapper<'a> { Ok(raw_body.to_string()) } } + +pub struct HttpClient { + client: reqwest::Client, +} + +#[derive(Debug)] +pub struct PostSignature { + date: String, + digest: String, + signature: String, +} + +#[derive(Debug)] +struct GetSignature { + date: String, + signature: String, +} + +enum RequestVerb { + GET, + POST, +} + +pub struct RequestBuilder { + verb: RequestVerb, + url: Url, + body: String, + inner: reqwest::RequestBuilder, +} + +impl RequestBuilder { + pub fn json(mut self, json: impl Serialize + Sized) -> RequestBuilder { + let body = serde_json::to_string(&json).unwrap(); + self.inner = self.inner.body(body.clone()); + self.body = body; + self + } + + pub fn activity(mut self) -> RequestBuilder { + self.inner = self + .inner + .header("Content-Type", "application/activity+json") + .header("Accept", "application/activity+json"); + self + } + + pub async fn send(self) -> Result { + event!(Level::DEBUG, ?self.inner, "sending an http request"); + + self.inner.send().await + } + + pub fn sign(mut self, key_id: &str) -> RequestBuilder { + match self.verb { + RequestVerb::GET => { + let sig = self.sign_get_request(key_id); + 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 + .header("Date", sig.date) + .header("Digest", sig.digest) + .header("Signature", sig.signature); + self + } + } + } + + fn sign_get_request(&self, key_id: &str) -> GetSignature { + let url = &self.url; + let host = url.host_str().unwrap(); + let path = url.path(); + + 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 + // RFC7231 is hardcoded to use GMT for.. some reason + let ts = Utc::now(); + + // RFC7231 string + let date = ts.format("%a, %d %b %Y %H:%M:%S GMT").to_string(); + + let to_sign = format!( + "(request-target): get {}\nhost: {}\ndate: {}", + path, host, date + ); + + let signature = signing_key.sign_with_rng(&mut rand::rngs::OsRng, &to_sign.into_bytes()); + let header = format!( + "keyId=\"{}\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date\",signature=\"{}\"", + key_id, + BASE64_STANDARD.encode(signature.to_bytes()) + ); + + GetSignature { + date, + signature: header, + } + } + + fn sign_post_request(&self, key_id: &str) -> PostSignature { + let body = &self.body; + let url = &self.url; + + let host = url.host_str().unwrap(); + let path = url.path(); + + let private_key = + RsaPrivateKey::from_pkcs8_pem(include_str!("../../../private.pem")).unwrap(); + let signing_key = SigningKey::::new(private_key); + + let mut hasher = Sha256::new(); + hasher.update(body); + let sha256 = hasher.finalize(); + + let b64 = BASE64_STANDARD.encode(sha256); + let digest = format!("SHA-256={}", b64); + + // UTC=GMT for our purposes, use it + // RFC7231 is hardcoded to use GMT for.. some reason + let ts = Utc::now(); + + // RFC7231 string + let date = ts.format("%a, %d %b %Y %H:%M:%S GMT").to_string(); + + let to_sign = format!( + "(request-target): post {}\nhost: {}\ndate: {}\ndigest: {}", + path, host, date, digest + ); + + let signature = signing_key.sign_with_rng(&mut rand::rngs::OsRng, &to_sign.into_bytes()); + let header = format!( + "keyId=\"{}\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest\",signature=\"{}\"", + key_id, + BASE64_STANDARD.encode(signature.to_bytes()) + ); + + PostSignature { + date, + digest, + signature: header, + } + } +} + +impl Default for HttpClient { + fn default() -> Self { + Self::new() + } +} + +impl HttpClient { + pub fn new() -> Self { + Self { + client: reqwest::Client::new(), + } + } + + pub fn get(&self, url: impl IntoUrl + Clone) -> RequestBuilder { + RequestBuilder { + verb: RequestVerb::GET, + url: url.clone().into_url().unwrap(), + body: String::new(), + inner: self.client.get(url), + } + } + + pub fn post(&self, url: impl IntoUrl + Clone) -> RequestBuilder { + RequestBuilder { + verb: RequestVerb::POST, + url: url.clone().into_url().unwrap(), + body: String::new(), + inner: self.client.post(url), + } + } +} diff --git a/ferri-main/src/federation/inbox.rs b/ferri-main/src/federation/inbox.rs index 68dd742..23d6fd1 100644 --- a/ferri-main/src/federation/inbox.rs +++ b/ferri-main/src/federation/inbox.rs @@ -1,7 +1,6 @@ use crate::types::{ap, as_context, db, get, make, Object, ObjectUri, ObjectUuid}; -use crate::ap::http::HttpClient; -use super::http::HttpWrapper; +use super::http::{HttpClient, HttpWrapper}; use super::outbox::OutboxRequest; use super::QueueMessage; @@ -83,7 +82,7 @@ pub async fn handle_inbox_request( let rmt = person.remote_info(); let post = activity.object; - let post_id = crate::ap::new_id(); + let post_id = crate::new_id(); let created_at = DateTime::parse_from_rfc3339(&activity.ts) .map(|dt| dt.to_utc()) @@ -104,20 +103,25 @@ pub async fn handle_inbox_request( let user = get::user_by_actor_uri(actor.id.clone(), &mut conn) .await .unwrap_or_else(|_| { + let id = crate::new_id(); db::User { - id: ObjectUuid(crate::new_id()), + id: ObjectUuid(id.clone()), actor, username: person.preferred_username, display_name: person.name, acct: rmt.acct, remote: rmt.is_remote, url: rmt.web_url, - created_at: crate::ap::now(), + created_at: crate::now(), icon_url: person.icon.map(|ic| ic.url) .unwrap_or("https//ferri.amy.mov/assets/pfp.png".to_string()), posts: db::UserPosts { last_post_at: None - } + }, + key_id: format!( + "https://ferri.amy.mov/users/{}#main-key", + id + ) } }); @@ -199,8 +203,9 @@ pub async fn handle_inbox_request( let user = get::user_by_actor_uri(actor.id.clone(), &mut conn) .await .unwrap_or_else(|_| { + let id = crate::new_id(); db::User { - id: ObjectUuid(crate::new_id()), + id: ObjectUuid(id.clone()), actor, username: boosted_author.preferred_username, display_name: boosted_author.name, @@ -208,12 +213,16 @@ pub async fn handle_inbox_request( remote: boosted_rmt.is_remote, url: boosted_rmt.web_url, // FIXME: Come from boosted_author - created_at: crate::ap::now(), + created_at: crate::now(), icon_url: boosted_author.icon.map(|ic| ic.url) .unwrap_or("https//ferri.amy.mov/assets/pfp.png".to_string()), posts: db::UserPosts { last_post_at: None - } + }, + key_id: format!( + "https://ferri.amy.mov/users/{}#main-key", + id + ) } }); @@ -270,20 +279,25 @@ pub async fn handle_inbox_request( let user = get::user_by_actor_uri(actor.id.clone(), &mut conn) .await .unwrap_or_else(|_| { + let id = crate::new_id(); db::User { - id: ObjectUuid(crate::new_id()), + id: ObjectUuid(id.clone()), actor, username: person.preferred_username, display_name: person.name, acct: rmt.acct, remote: rmt.is_remote, url: rmt.web_url, - created_at: crate::ap::now(), + created_at: crate::now(), icon_url: person.icon.map(|ic| ic.url) .unwrap_or("https//ferri.amy.mov/assets/pfp.png".to_string()), posts: db::UserPosts { last_post_at: None - } + }, + key_id: format!( + "https://ferri.amy.mov/users/{}#main-key", + id + ) } }); diff --git a/ferri-main/src/federation/outbox.rs b/ferri-main/src/federation/outbox.rs index 0ffa2ad..9a08af0 100644 --- a/ferri-main/src/federation/outbox.rs +++ b/ferri-main/src/federation/outbox.rs @@ -1,14 +1,22 @@ use serde::{Deserialize, Serialize}; +use sqlx::SqliteConnection; use tracing::info; use std::fmt::Debug; -use crate::{ap::http::HttpClient, federation::http::HttpWrapper, types::{ap::{self, ActivityType}, as_context, db, Object, ObjectContext, ObjectUri}}; +use crate::{federation::http::HttpWrapper, types::{ap::{self, ActivityType}, as_context, db, make, Object, ObjectContext, ObjectUri}}; + +use super::http::HttpClient; #[derive(Debug)] pub enum OutboxRequest { // FIXME: Make the String (key_id) nicer // Probably store it in the DB and pass a db::User here Accept(ap::AcceptActivity, String, ap::Person), - Status(db::Post, String) + Status(db::Post, String), + Follow { + follower: db::User, + followed: db::User, + conn: SqliteConnection + } } #[derive(Serialize, Deserialize, Debug)] @@ -40,7 +48,7 @@ pub async fn handle_outbox_request( ty: activity.ty, actor: activity.actor, object: activity.object, - published: crate::ap::new_ts() + published: crate::now_str(), }; let res = http @@ -80,7 +88,7 @@ pub async fn handle_outbox_request( attachment: vec![], attributed_to: Some(post.user.actor.id.0) }, - published: crate::ap::new_ts() + published: crate::now_str(), }; let res = http @@ -90,5 +98,40 @@ pub async fn handle_outbox_request( info!("status res {}", res); } + OutboxRequest::Follow { follower, followed, mut conn } => { + let follow = db::Follow { + id: ObjectUri(format!( + "https://ferri.amy.mov/activities/{}", + crate::new_id()) + ), + follower: follower.actor.id.clone(), + followed: followed.actor.id.clone(), + }; + + make::new_follow(follow, &mut conn) + .await + .unwrap(); + + let http = HttpWrapper::new(http, &follower.key_id); + + let activity = PreparedActivity { + context: as_context(), + id: format!( + "https://ferri.amy.mov/activities/{}", + crate::new_id() + ), + ty: ActivityType::Follow, + actor: follower.actor.id.0, + object: followed.actor.id.0, + published: crate::now_str(), + }; + + let res = http + .post_activity(&followed.actor.inbox, activity) + .await + .unwrap(); + + info!("follow res {}", res); + }, } } diff --git a/ferri-main/src/federation/request_queue.rs b/ferri-main/src/federation/request_queue.rs index facf294..014af49 100644 --- a/ferri-main/src/federation/request_queue.rs +++ b/ferri-main/src/federation/request_queue.rs @@ -1,8 +1,8 @@ use tokio::sync::mpsc; use tracing::{info, span, Instrument, Level}; -use crate::ap::http::HttpClient; use crate::config::Config; +use crate::federation::http::HttpClient; use crate::federation::inbox::handle_inbox_request; use crate::federation::outbox::handle_outbox_request; diff --git a/ferri-main/src/lib.rs b/ferri-main/src/lib.rs index c9a8744..d18d33e 100644 --- a/ferri-main/src/lib.rs +++ b/ferri-main/src/lib.rs @@ -1,8 +1,8 @@ -pub mod ap; pub mod config; pub mod types; pub mod federation; +use chrono::{DateTime, Utc}; use rand::{Rng, distributions::Alphanumeric}; pub fn gen_token(len: usize) -> String { @@ -16,3 +16,11 @@ pub fn gen_token(len: usize) -> String { pub fn new_id() -> String { uuid::Uuid::new_v4().to_string() } + +pub fn now() -> DateTime { + Utc::now() +} + +pub fn now_str() -> String { + now().to_rfc3339() +} diff --git a/ferri-main/src/types/api.rs b/ferri-main/src/types/api.rs index 81594a5..ab71ead 100644 --- a/ferri-main/src/types/api.rs +++ b/ferri-main/src/types/api.rs @@ -70,6 +70,24 @@ pub struct Status { pub poll: Option<()>, } +#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct Relationship { + id: ObjectUuid, + following: bool, + showing_reblogs: bool, + notifying: bool, + followed_by: bool, + blocking: bool, + blocked_by: bool, + muting: bool, + muting_notifications: bool, + requested: bool, + requested_by: bool, + domain_blocking: bool, + endorsed: bool, + note: String +} + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] pub struct Account { pub id: ObjectUuid, diff --git a/ferri-main/src/types/db.rs b/ferri-main/src/types/db.rs index 2c8ce0e..4b9dece 100644 --- a/ferri-main/src/types/db.rs +++ b/ferri-main/src/types/db.rs @@ -34,6 +34,7 @@ pub struct User { pub icon_url: String, pub posts: UserPosts, + pub key_id: String } #[derive(Debug, Eq, PartialEq, Clone)] diff --git a/ferri-main/src/types/get.rs b/ferri-main/src/types/get.rs index 0db4500..acab381 100644 --- a/ferri-main/src/types/get.rs +++ b/ferri-main/src/types/get.rs @@ -25,7 +25,10 @@ fn parse_ts(ts: String) -> Option> { Some(dt.unwrap()) } -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!( @@ -89,7 +92,7 @@ pub async fn user_by_id(id: ObjectUuid, conn: &mut SqliteConnection) -> Result Result Result { + info!("fetching user by username '{}' from the database", username); + + 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, + u.icon_url + FROM "user" u + INNER JOIN "actor" a ON u.actor_id = a.id + WHERE u.username = ?1 + "#, + username + ) + .fetch_one(&mut *conn) + .await + .map_err(|e| DbError::FetchError(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| DbError::FetchError(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_optional(&mut *conn) + .await + .map_err(|e| DbError::FetchError(e.to_string()))? + .flatten() + .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", record.user_id, follower_count); + info!("user {} last posted {:?}", record.user_id, last_post_at); + + Ok(db::User { + id: ObjectUuid(record.user_id.clone()), + 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 }, + icon_url: record.icon_url, + key_id: format!( + "https://ferri.amy.mov/users/{}#main-key", + record.user_id + ) }) } @@ -170,7 +265,7 @@ pub async fn user_by_actor_uri(uri: ObjectUri, conn: &mut SqliteConnection) -> R info!("user {:?} last posted {:?}", record.user_id, last_post_at); Ok(db::User { - id: ObjectUuid(record.user_id), + id: ObjectUuid(record.user_id.clone()), actor: db::Actor { id: ObjectUri(record.actor_id), inbox: record.inbox, @@ -183,7 +278,11 @@ pub async fn user_by_actor_uri(uri: ObjectUri, conn: &mut SqliteConnection) -> R created_at: user_created, url: record.url, posts: db::UserPosts { last_post_at }, - icon_url: record.icon_url + icon_url: record.icon_url, + key_id: format!( + "https://ferri.amy.mov/users/{}#main-key", + record.user_id + ) }) } @@ -248,7 +347,7 @@ pub async fn posts_for_user_id( id: ObjectUuid(record.post_id), uri: ObjectUri(record.post_uri), user: db::User { - id: ObjectUuid(record.user_id), + id: ObjectUuid(record.user_id.clone()), actor: db::Actor { id: ObjectUri(record.actor_id), inbox: record.inbox, @@ -263,7 +362,11 @@ pub async fn posts_for_user_id( icon_url: record.icon_url, posts: db::UserPosts { last_post_at: None - } + }, + key_id: format!( + "https://ferri.amy.mov/users/{}#main-key", + record.user_id + ) }, attachments, content: record.content, @@ -305,7 +408,7 @@ pub async fn home_timeline( id: ObjectUuid(p.post_id), uri: ObjectUri(p.post_uri), user: db::User { - id: ObjectUuid(p.user_id), + id: ObjectUuid(p.user_id.clone()), actor: db::Actor { id: ObjectUri(p.actor_id), inbox: p.inbox, @@ -320,7 +423,11 @@ pub async fn home_timeline( icon_url: p.icon_url, posts: db::UserPosts { last_post_at: None - } + }, + key_id: format!( + "https://ferri.amy.mov/users/{}#main-key", + p.user_id + ) }, content: p.content, created_at: parse_ts(p.post_created).unwrap(), @@ -391,3 +498,53 @@ pub async fn home_timeline( Ok(out) } + +pub async fn followers_for_user( + user_id: ObjectUuid, + conn: &mut SqliteConnection +) -> Result, DbError> { + let followers = sqlx::query!( + "SELECT * FROM follow WHERE followed_id = ?", + user_id.0 + ) + .fetch_all(&mut *conn) + .await + .unwrap(); + + let followers = followers.into_iter() + .map(|f| { + db::Follow { + id: ObjectUri(f.id), + follower: ObjectUri(f.follower_id), + followed: ObjectUri(f.followed_id) + } + }) + .collect::>(); + + Ok(followers) +} + +pub async fn following_for_user( + user_id: ObjectUuid, + conn: &mut SqliteConnection +) -> Result, DbError> { + let followers = sqlx::query!( + "SELECT * FROM follow WHERE follower_id = ?", + user_id.0 + ) + .fetch_all(&mut *conn) + .await + .unwrap(); + + let followers = followers.into_iter() + .map(|f| { + db::Follow { + id: ObjectUri(f.id), + follower: ObjectUri(f.follower_id), + followed: ObjectUri(f.followed_id) + } + }) + .collect::>(); + + Ok(followers) +} diff --git a/ferri-server/src/endpoints/api/search.rs b/ferri-server/src/endpoints/api/search.rs index f332fc7..8750f2d 100644 --- a/ferri-server/src/endpoints/api/search.rs +++ b/ferri-server/src/endpoints/api/search.rs @@ -1,12 +1,12 @@ use rocket::{ get, serde::json::Json, FromFormField, State, }; -use main::types::{api, get}; +use main::{federation::http::HttpWrapper, types::{api, get}}; use rocket_db_pools::Connection; use serde::{Deserialize, Serialize}; use tracing::{info, error}; -use crate::{http_wrapper::HttpWrapper, AuthenticatedUser, Db}; +use crate::{AuthenticatedUser, Db}; #[derive(Serialize, Deserialize, FromFormField, Debug)] #[serde(rename_all = "lowercase")] diff --git a/ferri-server/src/endpoints/api/status.rs b/ferri-server/src/endpoints/api/status.rs index 2ce38be..e884812 100644 --- a/ferri-server/src/endpoints/api/status.rs +++ b/ferri-server/src/endpoints/api/status.rs @@ -43,7 +43,7 @@ fn to_db_post(req: &CreateStatus, user: &AuthenticatedUser, config: &Config) -> uri: ObjectUri(config.post_url(&user.id.0, &post_id)), user: user.user.clone(), content: req.status.clone(), - created_at: main::ap::now(), + created_at: main::now(), boosted_post: None, attachments: vec![] } diff --git a/ferri-server/src/endpoints/api/user.rs b/ferri-server/src/endpoints/api/user.rs index 6cb86a0..63d824f 100644 --- a/ferri-server/src/endpoints/api/user.rs +++ b/ferri-server/src/endpoints/api/user.rs @@ -1,4 +1,5 @@ -use main::ap; +use main::federation::outbox::OutboxRequest; +use main::federation::QueueMessage; use main::types::{api, get, ObjectUuid}; use rocket::response::status::NotFound; use rocket::{ @@ -6,10 +7,9 @@ use rocket::{ serde::{Deserialize, Serialize, json::Json}, }; use rocket_db_pools::Connection; -use uuid::Uuid; use tracing::info; -use crate::{AuthenticatedUser, Db}; +use crate::{AuthenticatedUser, Db, OutboundQueue}; #[derive(Debug, Serialize, Deserialize)] #[serde(crate = "rocket::serde")] @@ -62,39 +62,38 @@ pub async fn verify_credentials(user: AuthenticatedUser) -> Json/follow")] pub async fn new_follow( mut db: Connection, - helpers: &State, + outbound: &State, uuid: &str, user: AuthenticatedUser, ) -> Result<(), NotFound> { - let http = &helpers.http; - - let follower = ap::User::from_actor_id(&user.actor_id.0, &mut **db).await; - - let followed = ap::User::from_id(uuid, &mut **db) + let follower = user.user; + let followed = get::user_by_id(ObjectUuid(uuid.to_string()), &mut **db) .await - .map_err(|e| NotFound(e.to_string()))?; + .unwrap(); - let outbox = ap::Outbox::for_user(follower.clone(), http); - - let activity = ap::Activity { - id: format!("https://ferri.amy.mov/activities/{}", Uuid::new_v4()), - ty: ap::ActivityType::Follow, - object: followed.actor_id().to_string(), - ..Default::default() - }; - - let req = ap::OutgoingActivity { - signed_by: format!("{}#main-key", follower.uri()), - req: activity, - to: followed.actor().clone(), - }; - - req.save(&mut **db).await; - outbox.post(req).await; + let conn = db.into_inner(); + let conn = conn.detach(); + let msg = QueueMessage::Outbound(OutboxRequest::Follow { + follower, + followed, + conn + }); + + outbound.0.send(msg).await; + Ok(()) } +#[get("/accounts/relationships?")] +pub async fn relationships( + id: Vec, + user: AuthenticatedUser +) -> Result>, ()> { + info!("{} looking up relationships for {:#?}", user.username, id); + Ok(Json(vec![])) +} + #[get("/accounts/")] pub async fn account( mut db: Connection, diff --git a/ferri-server/src/endpoints/user.rs b/ferri-server/src/endpoints/user.rs index 61ff21d..6dcf5c3 100644 --- a/ferri-server/src/endpoints/user.rs +++ b/ferri-server/src/endpoints/user.rs @@ -50,23 +50,14 @@ pub async fn followers( mut db: Connection, uuid: &str, ) -> Result>, NotFound> { - let target = main::ap::User::from_id(uuid, &mut **db) + let user = get::user_by_id(ObjectUuid(uuid.to_string()), &mut **db) .await - .map_err(|e| NotFound(e.to_string()))?; - - let actor_id = target.actor_id(); - - let followers = sqlx::query!( - r#" - SELECT follower_id FROM follow - WHERE followed_id = ? - "#, - actor_id - ) - .fetch_all(&mut **db) - .await - .unwrap(); - + .unwrap(); + + let followers = get::followers_for_user(user.id.clone(), &mut **db) + .await + .unwrap(); + ap_ok(Json(OrderedCollection { context: as_context(), ty: "OrderedCollection".to_string(), @@ -74,8 +65,8 @@ pub async fn followers( id: format!("https://ferri.amy.mov/users/{}/followers", uuid), ordered_items: followers .into_iter() - .map(|f| f.follower_id) - .collect::>(), + .map(|f| f.follower.0) + .collect(), })) } @@ -84,32 +75,23 @@ pub async fn following( mut db: Connection, uuid: &str, ) -> Result>, NotFound> { - let target = main::ap::User::from_id(uuid, &mut **db) + let user = get::user_by_id(ObjectUuid(uuid.to_string()), &mut **db) .await - .map_err(|e| NotFound(e.to_string()))?; - - let actor_id = target.actor_id(); - - let following = sqlx::query!( - r#" - SELECT followed_id FROM follow - WHERE follower_id = ? - "#, - actor_id - ) - .fetch_all(&mut **db) - .await - .unwrap(); - + .unwrap(); + + let followers = get::following_for_user(user.id.clone(), &mut **db) + .await + .unwrap(); + ap_ok(Json(OrderedCollection { context: as_context(), ty: "OrderedCollection".to_string(), total_items: 1, id: format!("https://ferri.amy.mov/users/{}/following", uuid), - ordered_items: following + ordered_items: followers .into_iter() - .map(|f| f.followed_id) - .collect::>(), + .map(|f| f.followed.0) + .collect(), })) } diff --git a/ferri-server/src/endpoints/well_known.rs b/ferri-server/src/endpoints/well_known.rs index 42d0373..d73c99c 100644 --- a/ferri-server/src/endpoints/well_known.rs +++ b/ferri-server/src/endpoints/well_known.rs @@ -1,7 +1,6 @@ use crate::Db; -use main::ap; -use main::types::api; -use rocket::{State, get, serde::json::Json}; +use main::types::{api, get}; +use rocket::{get, serde::json::Json, State}; use rocket_db_pools::Connection; use tracing::info; @@ -27,24 +26,26 @@ pub async fn webfinger( let acct = resource.strip_prefix("acct:").unwrap(); let (user, _) = acct.split_once("@").unwrap(); - let user = ap::User::from_username(user, &mut **db).await; + let user = get::user_by_username(user, &mut **db) + .await + .unwrap(); Json(api::WebfingerHit { subject: resource.to_string(), aliases: vec![ - config.user_url(user.id()), - config.user_web_url(user.username()), + config.user_url(&user.id.0), + config.user_web_url(&user.username), ], links: vec![ api::WebfingerLink { rel: "http://webfinger.net/rel/profile-page".to_string(), ty: Some("text/html".to_string()), - href: Some(config.user_web_url(user.username())), + href: Some(config.user_web_url(&user.username)), }, api::WebfingerLink { rel: "self".to_string(), ty: Some("application/activity+json".to_string()), - href: Some(config.user_url(user.id())), + href: Some(config.user_url(&user.id.0)), }, ], }) diff --git a/ferri-server/src/http_wrapper.rs b/ferri-server/src/http_wrapper.rs index e065010..e69de29 100644 --- a/ferri-server/src/http_wrapper.rs +++ b/ferri-server/src/http_wrapper.rs @@ -1,70 +0,0 @@ -use crate::http::HttpClient; -use main::types::ap; -use std::fmt::Debug; -use thiserror::Error; -use tracing::{Level, error, event, info}; - -pub struct HttpWrapper<'a> { - client: &'a HttpClient, - key_id: &'a str, -} - -#[derive(Error, Debug)] -pub enum HttpError { - #[error("entity of type `{0}` @ URL `{1}` could not be loaded")] - LoadFailure(String, String), - #[error("entity of type `{0}` @ URL `{1}` could not be parsed ({2})")] - ParseFailure(String, String, String), -} - -impl<'a> HttpWrapper<'a> { - pub fn new(client: &'a HttpClient, key_id: &'a str) -> HttpWrapper<'a> { - Self { client, key_id } - } - - async fn get( - &self, - ty: &str, - url: &str, - ) -> Result { - let ty = ty.to_string(); - event!(Level::INFO, url, "loading {}", ty); - - let http_result = self - .client - .get(url) - .sign(self.key_id) - .activity() - .send() - .await; - - if let Err(e) = http_result { - error!("could not load url {}: {:#?}", url, e); - return Err(HttpError::LoadFailure(ty, url.to_string())); - } - - let raw_body = http_result.unwrap().text().await; - if let Err(e) = raw_body { - error!("could not get text for url {}: {:#?}", url, e); - return Err(HttpError::LoadFailure(ty, url.to_string())); - } - - let raw_body = raw_body.unwrap(); - info!("raw body {}", raw_body); - let decoded = serde_json::from_str::(&raw_body); - - if let Err(e) = decoded { - error!( - "could not parse {} for url {}: {:#?} {}", - ty, url, e, &raw_body - ); - return Err(HttpError::ParseFailure(ty, url.to_string(), e.to_string())); - } - - Ok(decoded.unwrap()) - } - - pub async fn get_person(&self, url: &str) -> Result { - self.get("Person", url).await - } -} diff --git a/ferri-server/src/lib.rs b/ferri-server/src/lib.rs index 0154ded..420d05a 100644 --- a/ferri-server/src/lib.rs +++ b/ferri-server/src/lib.rs @@ -4,10 +4,8 @@ use endpoints::{ }; use tracing_subscriber::fmt; +use main::{federation::{self, http}, types::{db, get, ObjectUri, ObjectUuid}}; -use main::{federation, types::{db, get, ObjectUri, ObjectUuid}}; - -use main::ap::http; use main::config::Config; use rocket::{ Build, Request, Rocket, build, get, @@ -170,6 +168,7 @@ pub fn launch(cfg: Config) -> Rocket { api::user::new_follow, api::user::statuses, api::user::account, + api::user::relationships, api::apps::new_app, api::preferences::preferences, api::user::verify_credentials,