From dc50483e5cb1472b30a7f33eb5338ae8e9b1b61b Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Tue, 5 Dec 2023 15:50:10 +0100 Subject: [PATCH] initial user impl --- .../down.sql | 0 .../up.sql | 12 ++- src/database/edit.rs | 2 +- src/database/instance.rs | 12 +-- src/database/mod.rs | 1 + src/database/schema.rs | 24 ++++- src/database/user.rs | 63 +++++++++++++ src/error.rs | 2 +- src/federation/activities/follow.rs | 4 +- src/federation/objects/article.rs | 8 +- src/federation/objects/articles_collection.rs | 8 +- src/federation/objects/edits_collection.rs | 8 +- src/federation/objects/instance.rs | 1 + src/federation/objects/mod.rs | 1 + src/federation/objects/user.rs | 90 +++++++++++++++++++ 15 files changed, 212 insertions(+), 24 deletions(-) rename migrations/{2023-11-28-150402_article => 2023-11-28-150402_fediwiki_setup}/down.sql (100%) rename migrations/{2023-11-28-150402_article => 2023-11-28-150402_fediwiki_setup}/up.sql (77%) create mode 100644 src/database/user.rs create mode 100644 src/federation/objects/user.rs diff --git a/migrations/2023-11-28-150402_article/down.sql b/migrations/2023-11-28-150402_fediwiki_setup/down.sql similarity index 100% rename from migrations/2023-11-28-150402_article/down.sql rename to migrations/2023-11-28-150402_fediwiki_setup/down.sql diff --git a/migrations/2023-11-28-150402_article/up.sql b/migrations/2023-11-28-150402_fediwiki_setup/up.sql similarity index 77% rename from migrations/2023-11-28-150402_article/up.sql rename to migrations/2023-11-28-150402_fediwiki_setup/up.sql index 82801ea..b645549 100644 --- a/migrations/2023-11-28-150402_article/up.sql +++ b/migrations/2023-11-28-150402_fediwiki_setup/up.sql @@ -9,10 +9,20 @@ create table instance ( local bool not null ); +create table user_ ( + id serial primary key, + ap_id varchar(255) not null unique, + inbox_url text not null, + public_key text not null, + private_key text, + last_refreshed_at timestamptz not null default now(), + local bool not null + ); + create table instance_follow ( id serial primary key, instance_id int REFERENCES instance ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, - follower_id int REFERENCES instance ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + follower_id int REFERENCES user_ ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, pending boolean not null, unique(instance_id, follower_id) ); diff --git a/src/database/edit.rs b/src/database/edit.rs index 7e43497..e35ce9c 100644 --- a/src/database/edit.rs +++ b/src/database/edit.rs @@ -57,7 +57,7 @@ impl DbEditForm { }) } - pub(crate) fn generate_ap_id( + pub fn generate_ap_id( article: &DbArticle, version: &EditVersion, ) -> MyResult> { diff --git a/src/database/instance.rs b/src/database/instance.rs index 6bd9f95..9270679 100644 --- a/src/database/instance.rs +++ b/src/database/instance.rs @@ -24,11 +24,11 @@ pub struct DbInstance { pub articles_url: CollectionId, pub inbox_url: String, #[serde(skip)] - pub(crate) public_key: String, + pub public_key: String, #[serde(skip)] - pub(crate) private_key: Option, + pub private_key: Option, #[serde(skip)] - pub(crate) last_refreshed_at: DateTime, + pub last_refreshed_at: DateTime, pub local: bool, } @@ -38,9 +38,9 @@ pub struct DbInstanceForm { pub ap_id: ObjectId, pub articles_url: CollectionId, pub inbox_url: String, - pub(crate) public_key: String, - pub(crate) private_key: Option, - pub(crate) last_refreshed_at: DateTime, + pub public_key: String, + pub private_key: Option, + pub last_refreshed_at: DateTime, pub local: bool, } diff --git a/src/database/mod.rs b/src/database/mod.rs index fe457ea..ffa8c14 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -9,6 +9,7 @@ pub mod conflict; pub mod edit; pub mod instance; mod schema; +pub mod user; pub mod version; #[derive(Clone)] diff --git a/src/database/schema.rs b/src/database/schema.rs index dcb23de..f9b8fac 100644 --- a/src/database/schema.rs +++ b/src/database/schema.rs @@ -57,8 +57,30 @@ diesel::table! { } } +diesel::table! { + user_ (id) { + id -> Int4, + #[max_length = 255] + ap_id -> Varchar, + inbox_url -> Text, + public_key -> Text, + private_key -> Nullable, + last_refreshed_at -> Timestamptz, + local -> Bool, + } +} + diesel::joinable!(article -> instance (instance_id)); diesel::joinable!(conflict -> article (article_id)); diesel::joinable!(edit -> article (article_id)); +diesel::joinable!(instance_follow -> instance (instance_id)); +diesel::joinable!(instance_follow -> user_ (follower_id)); -diesel::allow_tables_to_appear_in_same_query!(article, conflict, edit, instance, instance_follow,); +diesel::allow_tables_to_appear_in_same_query!( + article, + conflict, + edit, + instance, + instance_follow, + user_, +); diff --git a/src/database/user.rs b/src/database/user.rs new file mode 100644 index 0000000..686bba8 --- /dev/null +++ b/src/database/user.rs @@ -0,0 +1,63 @@ +use crate::database::schema::user_; +use crate::database::MyDataHandle; +use crate::error::MyResult; +use activitypub_federation::config::Data; +use activitypub_federation::fetch::object_id::ObjectId; +use chrono::{DateTime, Utc}; +use diesel::ExpressionMethods; +use diesel::QueryDsl; +use diesel::{ + insert_into, AsChangeset, Identifiable, Insertable, PgConnection, Queryable, RunQueryDsl, + Selectable, +}; +use serde::{Deserialize, Serialize}; +use std::ops::DerefMut; +use std::sync::Mutex; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Queryable, Selectable, Identifiable)] +#[diesel(table_name = user_, check_for_backend(diesel::pg::Pg))] +pub struct DbUser { + pub id: i32, + pub ap_id: ObjectId, + pub inbox_url: String, + #[serde(skip)] + pub public_key: String, + #[serde(skip)] + pub private_key: Option, + #[serde(skip)] + pub last_refreshed_at: DateTime, + pub local: bool, +} + +#[derive(Debug, Clone, Insertable, AsChangeset)] +#[diesel(table_name = user_, check_for_backend(diesel::pg::Pg))] +pub struct DbUserForm { + pub ap_id: ObjectId, + pub inbox_url: String, + pub public_key: String, + pub private_key: Option, + pub last_refreshed_at: DateTime, + pub local: bool, +} + +impl DbUser { + pub fn create(form: &DbUserForm, conn: &Mutex) -> MyResult { + let mut conn = conn.lock().unwrap(); + Ok(insert_into(user_::table) + .values(form) + .on_conflict(user_::dsl::ap_id) + .do_update() + .set(form) + .get_result(conn.deref_mut())?) + } + + pub fn read_from_ap_id( + ap_id: &ObjectId, + data: &Data, + ) -> MyResult { + let mut conn = data.db_connection.lock().unwrap(); + Ok(user_::table + .filter(user_::dsl::ap_id.eq(ap_id)) + .get_result(conn.deref_mut())?) + } +} diff --git a/src/error.rs b/src/error.rs index a0b7a85..064f4a4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,7 +5,7 @@ use std::fmt::{Display, Formatter}; pub type MyResult = Result; #[derive(Debug)] -pub struct Error(pub(crate) anyhow::Error); +pub struct Error(pub anyhow::Error); impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { diff --git a/src/federation/activities/follow.rs b/src/federation/activities/follow.rs index f38a5dd..8a4f809 100644 --- a/src/federation/activities/follow.rs +++ b/src/federation/activities/follow.rs @@ -13,8 +13,8 @@ use url::Url; #[derive(Deserialize, Serialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct Follow { - pub(crate) actor: ObjectId, - pub(crate) object: ObjectId, + pub actor: ObjectId, + pub object: ObjectId, #[serde(rename = "type")] kind: FollowType, id: Url, diff --git a/src/federation/objects/article.rs b/src/federation/objects/article.rs index 45fe11d..59b67d2 100644 --- a/src/federation/objects/article.rs +++ b/src/federation/objects/article.rs @@ -19,11 +19,11 @@ use url::Url; #[serde(rename_all = "camelCase")] pub struct ApubArticle { #[serde(rename = "type")] - pub(crate) kind: ArticleType, - pub(crate) id: ObjectId, - pub(crate) attributed_to: ObjectId, + pub kind: ArticleType, + pub id: ObjectId, + pub attributed_to: ObjectId, #[serde(deserialize_with = "deserialize_one_or_many")] - pub(crate) to: Vec, + pub to: Vec, pub edits: CollectionId, latest_version: EditVersion, content: String, diff --git a/src/federation/objects/articles_collection.rs b/src/federation/objects/articles_collection.rs index d4efeb8..a53d4dd 100644 --- a/src/federation/objects/articles_collection.rs +++ b/src/federation/objects/articles_collection.rs @@ -16,10 +16,10 @@ use url::Url; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ArticleCollection { - pub(crate) r#type: CollectionType, - pub(crate) id: Url, - pub(crate) total_items: i32, - pub(crate) items: Vec, + pub r#type: CollectionType, + pub id: Url, + pub total_items: i32, + pub items: Vec, } #[derive(Clone, Debug)] diff --git a/src/federation/objects/edits_collection.rs b/src/federation/objects/edits_collection.rs index 3afe139..80b4c66 100644 --- a/src/federation/objects/edits_collection.rs +++ b/src/federation/objects/edits_collection.rs @@ -18,10 +18,10 @@ use url::Url; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ApubEditCollection { - pub(crate) r#type: OrderedCollectionType, - pub(crate) id: Url, - pub(crate) total_items: i32, - pub(crate) items: Vec, + pub r#type: OrderedCollectionType, + pub id: Url, + pub total_items: i32, + pub items: Vec, } #[derive(Clone, Debug)] diff --git a/src/federation/objects/instance.rs b/src/federation/objects/instance.rs index 6c824c5..5daa3a3 100644 --- a/src/federation/objects/instance.rs +++ b/src/federation/objects/instance.rs @@ -66,6 +66,7 @@ impl DbInstance { Ok(()) } + // TODO: move to user? pub async fn send( &self, activity: Activity, diff --git a/src/federation/objects/mod.rs b/src/federation/objects/mod.rs index 6d5b4b1..0b14474 100644 --- a/src/federation/objects/mod.rs +++ b/src/federation/objects/mod.rs @@ -3,3 +3,4 @@ pub mod articles_collection; pub mod edit; pub mod edits_collection; pub mod instance; +pub mod user; diff --git a/src/federation/objects/user.rs b/src/federation/objects/user.rs new file mode 100644 index 0000000..2522915 --- /dev/null +++ b/src/federation/objects/user.rs @@ -0,0 +1,90 @@ +use crate::database::user::{DbUser, DbUserForm}; +use crate::database::MyDataHandle; +use crate::error::Error; +use activitypub_federation::kinds::actor::PersonType; +use activitypub_federation::{ + config::Data, + fetch::object_id::ObjectId, + protocol::{public_key::PublicKey, verification::verify_domains_match}, + traits::{Actor, Object}, +}; +use chrono::{DateTime, Local, Utc}; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; +use url::Url; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApubUser { + #[serde(rename = "type")] + kind: PersonType, + id: ObjectId, + inbox: Url, + public_key: PublicKey, +} + +#[async_trait::async_trait] +impl Object for DbUser { + type DataType = MyDataHandle; + type Kind = ApubUser; + type Error = Error; + + fn last_refreshed_at(&self) -> Option> { + Some(self.last_refreshed_at) + } + + async fn read_from_id( + object_id: Url, + data: &Data, + ) -> Result, Self::Error> { + Ok(DbUser::read_from_ap_id(&object_id.into(), data).ok()) + } + + async fn into_json(self, _data: &Data) -> Result { + Ok(ApubUser { + kind: Default::default(), + id: self.ap_id.clone(), + inbox: Url::parse(&self.inbox_url)?, + public_key: self.public_key(), + }) + } + + async fn verify( + json: &Self::Kind, + expected_domain: &Url, + _data: &Data, + ) -> Result<(), Self::Error> { + verify_domains_match(json.id.inner(), expected_domain)?; + Ok(()) + } + + async fn from_json(json: Self::Kind, data: &Data) -> Result { + let form = DbUserForm { + ap_id: json.id, + inbox_url: json.inbox.to_string(), + public_key: json.public_key.public_key_pem, + private_key: None, + last_refreshed_at: Local::now().into(), + local: false, + }; + DbUser::create(&form, &data.db_connection) + } +} + +impl Actor for DbUser { + fn id(&self) -> Url { + self.ap_id.inner().clone() + } + + fn public_key_pem(&self) -> &str { + &self.public_key + } + + fn private_key_pem(&self) -> Option { + self.private_key.clone() + } + + fn inbox(&self) -> Url { + Url::parse(&self.inbox_url).unwrap() + } +}