diff --git a/Cargo.lock b/Cargo.lock index c9ca4d9..7136ebb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,17 +69,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "ahash" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - [[package]] name = "ahash" version = "0.8.7" @@ -308,18 +297,6 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -345,29 +322,6 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" -[[package]] -name = "bytecheck" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "simdutf8", - "uuid", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "bytecount" version = "0.6.7" @@ -536,26 +490,6 @@ dependencies = [ "toml 0.5.11", ] -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - -[[package]] -name = "console_log" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" -dependencies = [ - "log", - "web-sys", -] - [[package]] name = "const_format" version = "0.2.32" @@ -948,12 +882,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futures" version = "0.3.29" @@ -1141,9 +1069,6 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.7", -] [[package]] name = "hashbrown" @@ -1157,7 +1082,7 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" dependencies = [ - "ahash 0.8.7", + "ahash", "allocator-api2", ] @@ -1382,10 +1307,7 @@ dependencies = [ "axum", "axum-macros", "bcrypt", - "cfg-if", "chrono", - "console_error_panic_hook", - "console_log", "diesel", "diesel-derive-newtype", "diesel_migrations", @@ -1409,10 +1331,9 @@ dependencies = [ "sha2", "tokio", "tower-http", + "tracing", "url", "uuid", - "wasm-bindgen", - "web-sys", ] [[package]] @@ -1718,7 +1639,6 @@ dependencies = [ "js-sys", "paste", "pin-project", - "rkyv", "rustc-hash", "self_cell", "serde", @@ -2302,26 +2222,6 @@ dependencies = [ "yansi 1.0.0-rc.1", ] -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "pulldown-cmark" version = "0.9.3" @@ -2381,12 +2281,6 @@ dependencies = [ "syn 2.0.39", ] -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" version = "0.8.5" @@ -2464,15 +2358,6 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" -[[package]] -name = "rend" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2571463863a6bd50c32f94402933f03457a3fbaf697a707c5be741e459f08fd" -dependencies = [ - "bytecheck", -] - [[package]] name = "reqwest" version = "0.11.22" @@ -2543,35 +2428,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "rkyv" -version = "0.7.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527a97cdfef66f65998b5f3b637c26f5a5ec09cc52a3f9932313ac645f4190f5" -dependencies = [ - "bitvec", - "bytecheck", - "bytes", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5c462a1328c8e67e4d6dbad1eb0355dd43e8ab432c6e227a43657f16ade5033" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "rstml" version = "0.11.2" @@ -2656,12 +2512,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - [[package]] name = "security-framework" version = "2.9.2" @@ -2875,12 +2725,6 @@ dependencies = [ "libc", ] -[[package]] -name = "simdutf8" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" - [[package]] name = "simple_asn1" version = "0.6.2" @@ -3038,12 +2882,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "task-local-extensions" version = "0.1.4" @@ -3258,9 +3096,16 @@ dependencies = [ "http 0.2.11", "http-body 0.4.5", "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", "pin-project-lite", + "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -3672,15 +3517,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "xxhash-rust" version = "0.8.8" diff --git a/Cargo.toml b/Cargo.toml index d003333..e6a02f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,27 +4,21 @@ version = "0.1.0" edition = "2021" [features] +default = ["ssr"] ssr = [ - "activitypub_federation", "axum", "axum-macros", - "tower-http", + "tower-http", "diesel", "diesel-derive-newtype", "diesel_migrations", - "tokio", - "leptos_axum" + "tokio", + "leptos_axum", + "activitypub_federation" ] -csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] +csr = ["leptos/csr", "leptos_meta/csr"] [dependencies] -# shared -serde = { version = "1.0.192", features = ["derive"] } -url = { version = "2.4.1", features = ["serde"] } -reqwest = { version = "0.11.22", features = ["json"] } -log = "0.4" - -# backend activitypub_federation = { version = "0.5.0-beta.6", features = [ "axum", "diesel", @@ -33,6 +27,10 @@ anyhow = "1.0.75" async-trait = "0.1.74" axum = { version = "0.6.20", optional = true } axum-macros = { version = "0.3.8", optional = true } +leptos = "0.5.4" +leptos_meta = "0.5.4" +leptos_router = "0.5.4" +leptos_axum = { version = "0.5.4", optional = true } bcrypt = "0.15.0" chrono = { version = "0.4.31", features = ["serde"] } diesel = { version = "2.1.4", features = [ @@ -53,20 +51,15 @@ serde_json = "1.0.108" sha2 = "0.10.8" tokio = { version = "1.34.0", features = ["full"], optional = true } uuid = { version = "1.6.1", features = ["serde"] } -tower-http = { version = "0.4.0", features = ["cors"], optional = true } -leptos_axum = { version = "0.5.4", optional = true } - -# frontend -leptos = { version = "0.5.4", features = ["nightly"] } -leptos_meta = { version = "0.5.4", features = ["nightly"] } -leptos_router = { version = "0.5.4", features = ["nightly"] } -console_log = "1" -console_error_panic_hook = "0.1" -wasm-bindgen = "0.2" -web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] } +tower-http = { version = "0.4.0", features = ["cors", "fs"], optional = true } +serde = { version = "1.0.192", features = ["derive"] } +url = { version = "2.4.1", features = ["serde"] } +reqwest = { version = "0.11.22", features = ["json"] } +log = "0.4" +tracing = "0.1.40" +once_cell = "1.18.0" [dev-dependencies] -once_cell = "1.18.0" pretty_assertions = "1.4.0" reqwest = "0.11.22" @@ -74,6 +67,7 @@ reqwest = "0.11.22" output-name = "ibis" bin-features = ["ssr"] lib-features = ["csr"] +site-addr = "127.0.0.1:8131" [lib] crate-type = ["cdylib", "rlib"] diff --git a/src/backend/api/article.rs b/src/backend/api/article.rs index 2655497..e140641 100644 --- a/src/backend/api/article.rs +++ b/src/backend/api/article.rs @@ -1,14 +1,16 @@ -use crate::backend::database::article::{ArticleView, DbArticle, DbArticleForm}; +use crate::backend::database::article::DbArticleForm; use crate::backend::database::conflict::{ApiConflict, DbConflict, DbConflictForm}; -use crate::backend::database::edit::{DbEdit, DbEditForm}; +use crate::backend::database::edit::DbEditForm; use crate::backend::database::instance::DbInstance; use crate::backend::database::user::LocalUserView; -use crate::backend::database::version::EditVersion; use crate::backend::database::MyDataHandle; use crate::backend::error::MyResult; use crate::backend::federation::activities::create_article::CreateArticle; use crate::backend::federation::activities::submit_article_update; use crate::backend::utils::generate_article_version; +use crate::common::EditVersion; +use crate::common::GetArticleData; +use crate::common::{ArticleView, DbArticle, DbEdit}; use activitypub_federation::config::Data; use activitypub_federation::fetch::object_id::ObjectId; use axum::extract::Query; @@ -118,19 +120,14 @@ pub(in crate::backend::api) async fn edit_article( } } -#[derive(Deserialize, Serialize, Clone)] -pub struct GetArticleData { - pub article_id: i32, -} - /// Retrieve an article by ID. It must already be stored in the local database. #[debug_handler] pub(in crate::backend::api) async fn get_article( Query(query): Query, data: Data, ) -> MyResult> { - Ok(Json(DbArticle::read_view( - query.article_id, + Ok(Json(DbArticle::read_view_title( + &query.title, &data.db_connection, )?)) } diff --git a/src/backend/api/mod.rs b/src/backend/api/mod.rs index 9a1e604..236e7df 100644 --- a/src/backend/api/mod.rs +++ b/src/backend/api/mod.rs @@ -5,13 +5,13 @@ use crate::backend::api::instance::get_local_instance; use crate::backend::api::user::login_user; use crate::backend::api::user::register_user; use crate::backend::api::user::validate; -use crate::backend::database::article::{ArticleView, DbArticle}; use crate::backend::database::conflict::{ApiConflict, DbConflict}; -use crate::backend::database::edit::DbEdit; use crate::backend::database::instance::DbInstance; use crate::backend::database::user::LocalUserView; use crate::backend::database::MyDataHandle; use crate::backend::error::MyResult; +use crate::common::DbEdit; +use crate::common::{ArticleView, DbArticle}; use activitypub_federation::config::Data; use activitypub_federation::fetch::object_id::ObjectId; use axum::extract::Query; @@ -28,8 +28,8 @@ use axum::{ use axum::{Json, Router}; use axum_macros::debug_handler; use futures::future::try_join_all; -use serde::{Deserialize, Serialize}; use log::warn; +use serde::{Deserialize, Serialize}; use url::Url; pub mod article; @@ -83,7 +83,9 @@ async fn resolve_instance( Query(query): Query, data: Data, ) -> MyResult> { - let instance: DbInstance = ObjectId::from(query.id).dereference(&data).await?; + // TODO: workaround because axum makes it hard to have multiple routes on / + let id = format!("{}instance", query.id); + let instance: DbInstance = ObjectId::parse(&id)?.dereference(&data).await?; Ok(Json(instance)) } diff --git a/src/backend/database/article.rs b/src/backend/database/article.rs index ab0b89e..845721d 100644 --- a/src/backend/database/article.rs +++ b/src/backend/database/article.rs @@ -1,42 +1,20 @@ -use crate::backend::database::edit::DbEdit; - use crate::backend::database::schema::{article, edit}; use crate::backend::error::MyResult; use crate::backend::federation::objects::edits_collection::DbEditCollection; +use crate::common::DbEdit; +use crate::common::EditVersion; +use crate::common::{ArticleView, DbArticle}; use activitypub_federation::fetch::collection_id::CollectionId; use activitypub_federation::fetch::object_id::ObjectId; use diesel::pg::PgConnection; - use diesel::ExpressionMethods; use diesel::{ - insert_into, AsChangeset, BoolExpressionMethods, Identifiable, Insertable, - PgTextExpressionMethods, QueryDsl, Queryable, RunQueryDsl, Selectable, + insert_into, AsChangeset, BoolExpressionMethods, Insertable, PgTextExpressionMethods, QueryDsl, + RunQueryDsl, }; -use serde::{Deserialize, Serialize}; - -use crate::backend::database::version::EditVersion; use std::ops::DerefMut; use std::sync::Mutex; -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Queryable, Selectable, Identifiable)] -#[diesel(table_name = article, check_for_backend(diesel::pg::Pg), belongs_to(DbInstance, foreign_key = instance_id))] -pub struct DbArticle { - pub id: i32, - pub title: String, - pub text: String, - pub ap_id: ObjectId, - pub instance_id: i32, - pub local: bool, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Queryable)] -#[diesel(table_name = article, check_for_backend(diesel::pg::Pg))] -pub struct ArticleView { - pub article: DbArticle, - pub latest_version: EditVersion, - pub edits: Vec, -} - #[derive(Debug, Clone, Insertable, AsChangeset)] #[diesel(table_name = article, check_for_backend(diesel::pg::Pg))] pub struct DbArticleForm { @@ -47,6 +25,7 @@ pub struct DbArticleForm { pub local: bool, } +// TODO: get rid of unnecessary methods impl DbArticle { pub fn edits_id(&self) -> MyResult> { Ok(CollectionId::parse(&format!("{}/edits", self.ap_id))?) @@ -95,6 +74,22 @@ impl DbArticle { }) } + pub fn read_view_title(title: &str, conn: &Mutex) -> MyResult { + let article: DbArticle = { + let mut conn = conn.lock().unwrap(); + article::table + .filter(article::dsl::title.eq(title)) + .get_result(conn.deref_mut())? + }; + let latest_version = article.latest_edit_version(conn)?; + let edits: Vec = DbEdit::read_for_article(&article, conn)?; + Ok(ArticleView { + article, + edits, + latest_version, + }) + } + pub fn read_from_ap_id( ap_id: &ObjectId, conn: &Mutex, diff --git a/src/backend/database/conflict.rs b/src/backend/database/conflict.rs index af5e42b..55f4023 100644 --- a/src/backend/database/conflict.rs +++ b/src/backend/database/conflict.rs @@ -1,13 +1,14 @@ -use crate::backend::database::article::DbArticle; -use crate::backend::database::edit::DbEdit; use crate::backend::database::schema::conflict; use crate::backend::database::user::DbLocalUser; -use crate::backend::database::version::EditVersion; use crate::backend::database::MyDataHandle; use crate::backend::error::MyResult; use crate::backend::federation::activities::submit_article_update; use crate::backend::utils::generate_article_version; +use crate::common::DbArticle; +use crate::common::DbEdit; +use crate::common::EditVersion; use activitypub_federation::config::Data; +use activitypub_federation::fetch::object_id::ObjectId; use diesel::ExpressionMethods; use diesel::{ delete, insert_into, Identifiable, Insertable, PgConnection, QueryDsl, Queryable, RunQueryDsl, @@ -75,7 +76,9 @@ impl DbConflict { ) -> MyResult> { let article = DbArticle::read(self.article_id, &data.db_connection)?; // Make sure to get latest version from origin so that all conflicts can be resolved - let original_article = article.ap_id.dereference_forced(data).await?; + let original_article = ObjectId::parse(&article.ap_id)? + .dereference_forced(data) + .await?; // create common ancestor version let edits = DbEdit::read_for_article(&original_article, &data.db_connection)?; diff --git a/src/backend/database/edit.rs b/src/backend/database/edit.rs index bada8bd..9c53d73 100644 --- a/src/backend/database/edit.rs +++ b/src/backend/database/edit.rs @@ -1,35 +1,14 @@ use crate::backend::database::schema::edit; -use crate::backend::database::version::EditVersion; -use crate::backend::database::DbArticle; use crate::backend::error::MyResult; +use crate::common::EditVersion; +use crate::common::{DbArticle, DbEdit}; use activitypub_federation::fetch::object_id::ObjectId; use diesel::ExpressionMethods; -use diesel::{ - insert_into, AsChangeset, Insertable, PgConnection, QueryDsl, Queryable, RunQueryDsl, - Selectable, -}; +use diesel::{insert_into, AsChangeset, Insertable, PgConnection, QueryDsl, RunQueryDsl}; use diffy::create_patch; -use serde::{Deserialize, Serialize}; use std::ops::DerefMut; use std::sync::Mutex; -/// Represents a single change to the article. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Queryable, Selectable)] -#[diesel(table_name = edit, check_for_backend(diesel::pg::Pg))] -pub struct DbEdit { - // TODO: we could use hash as primary key, but that gives errors on forking because - // the same edit is used for multiple articles - pub id: i32, - pub creator_id: i32, - /// UUID built from sha224 hash of diff - pub hash: EditVersion, - pub ap_id: ObjectId, - pub diff: String, - pub article_id: i32, - /// First edit of an article always has `EditVersion::default()` here - pub previous_version_id: EditVersion, -} - #[derive(Debug, Clone, Insertable, AsChangeset)] #[diesel(table_name = edit, check_for_backend(diesel::pg::Pg))] pub struct DbEditForm { diff --git a/src/backend/database/mod.rs b/src/backend/database/mod.rs index 13725d0..253cc8f 100644 --- a/src/backend/database/mod.rs +++ b/src/backend/database/mod.rs @@ -1,4 +1,3 @@ -use crate::backend::database::article::DbArticle; use diesel::PgConnection; use std::ops::Deref; use std::sync::{Arc, Mutex}; @@ -12,7 +11,7 @@ pub mod article; pub mod conflict; pub mod edit; pub mod instance; -mod schema; +pub(crate) mod schema; pub mod user; pub mod version; diff --git a/src/backend/database/version.rs b/src/backend/database/version.rs index 791f863..2f57928 100644 --- a/src/backend/database/version.rs +++ b/src/backend/database/version.rs @@ -1,16 +1,8 @@ use crate::backend::error::MyResult; -use std::hash::Hash; - -use diesel_derive_newtype::DieselNewType; -use serde::{Deserialize, Serialize}; +use crate::common::EditVersion; use sha2::{Digest, Sha256}; use uuid::Uuid; -/// The version hash of a specific edit. Generated by taking an SHA256 hash of the diff -/// and using the first 16 bytes so that it fits into UUID. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, DieselNewType)] -pub struct EditVersion(Uuid); - impl EditVersion { pub fn new(diff: &str) -> MyResult { let mut sha256 = Sha256::new(); diff --git a/src/backend/error.rs b/src/backend/error.rs index 064f4a4..6839058 100644 --- a/src/backend/error.rs +++ b/src/backend/error.rs @@ -1,5 +1,3 @@ -use axum::http::StatusCode; -use axum::response::{IntoResponse, Response}; use std::fmt::{Display, Formatter}; pub type MyResult = Result; @@ -22,8 +20,13 @@ where } } -impl IntoResponse for Error { - fn into_response(self) -> Response { - (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", self.0)).into_response() +#[cfg(feature = "ssr")] +impl axum::response::IntoResponse for Error { + fn into_response(self) -> axum::response::Response { + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + format!("{}", self.0), + ) + .into_response() } } diff --git a/src/backend/federation/activities/create_article.rs b/src/backend/federation/activities/create_article.rs index b00c5f5..bb286ea 100644 --- a/src/backend/federation/activities/create_article.rs +++ b/src/backend/federation/activities/create_article.rs @@ -1,8 +1,9 @@ use crate::backend::database::instance::DbInstance; -use crate::backend::database::{article::DbArticle, MyDataHandle}; +use crate::backend::database::MyDataHandle; use crate::backend::error::MyResult; use crate::backend::federation::objects::article::ApubArticle; use crate::backend::utils::generate_activity_id; +use crate::common::DbArticle; use activitypub_federation::kinds::activity::CreateType; use activitypub_federation::{ config::Data, diff --git a/src/backend/federation/activities/follow.rs b/src/backend/federation/activities/follow.rs index 3eca565..34d893a 100644 --- a/src/backend/federation/activities/follow.rs +++ b/src/backend/federation/activities/follow.rs @@ -2,7 +2,9 @@ use crate::backend::database::instance::DbInstance; use crate::backend::database::user::DbPerson; use crate::backend::error::MyResult; use crate::backend::federation::send_activity; -use crate::backend::{database::MyDataHandle, federation::activities::accept::Accept, generate_activity_id}; +use crate::backend::{ + database::MyDataHandle, federation::activities::accept::Accept, generate_activity_id, +}; use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, diff --git a/src/backend/federation/activities/mod.rs b/src/backend/federation/activities/mod.rs index 72c2333..5e14afb 100644 --- a/src/backend/federation/activities/mod.rs +++ b/src/backend/federation/activities/mod.rs @@ -1,11 +1,11 @@ -use crate::backend::database::article::DbArticle; -use crate::backend::database::edit::{DbEdit, DbEditForm}; +use crate::backend::database::edit::DbEditForm; use crate::backend::database::instance::DbInstance; -use crate::backend::database::version::EditVersion; use crate::backend::database::MyDataHandle; use crate::backend::error::Error; use crate::backend::federation::activities::update_local_article::UpdateLocalArticle; use crate::backend::federation::activities::update_remote_article::UpdateRemoteArticle; +use crate::common::EditVersion; +use crate::common::{DbArticle, DbEdit}; use activitypub_federation::config::Data; pub mod accept; @@ -35,7 +35,7 @@ pub async fn submit_article_update( id: -1, creator_id, hash: form.hash, - ap_id: form.ap_id, + ap_id: form.ap_id.to_string(), diff: form.diff, article_id: form.article_id, previous_version_id: form.previous_version_id, diff --git a/src/backend/federation/activities/reject.rs b/src/backend/federation/activities/reject.rs index 7253b1b..068eae7 100644 --- a/src/backend/federation/activities/reject.rs +++ b/src/backend/federation/activities/reject.rs @@ -1,10 +1,10 @@ use crate::backend::database::conflict::{DbConflict, DbConflictForm}; use crate::backend::database::instance::DbInstance; -use crate::backend::database::version::EditVersion; use crate::backend::database::MyDataHandle; use crate::backend::error::MyResult; use crate::backend::federation::objects::edit::ApubEdit; use crate::backend::utils::generate_activity_id; +use crate::common::EditVersion; use activitypub_federation::kinds::activity::RejectType; use activitypub_federation::{ config::Data, fetch::object_id::ObjectId, protocol::helpers::deserialize_one_or_many, diff --git a/src/backend/federation/activities/update_local_article.rs b/src/backend/federation/activities/update_local_article.rs index fbf867d..2c4d2c0 100644 --- a/src/backend/federation/activities/update_local_article.rs +++ b/src/backend/federation/activities/update_local_article.rs @@ -1,4 +1,4 @@ -use crate::backend::database::{article::DbArticle, MyDataHandle}; +use crate::backend::database::MyDataHandle; use crate::backend::error::MyResult; use crate::backend::federation::objects::article::ApubArticle; @@ -12,6 +12,7 @@ use activitypub_federation::{ traits::{ActivityHandler, Object}, }; +use crate::common::DbArticle; use serde::{Deserialize, Serialize}; use url::Url; diff --git a/src/backend/federation/activities/update_remote_article.rs b/src/backend/federation/activities/update_remote_article.rs index cce2975..9e2d9fc 100644 --- a/src/backend/federation/activities/update_remote_article.rs +++ b/src/backend/federation/activities/update_remote_article.rs @@ -1,14 +1,14 @@ use crate::backend::database::MyDataHandle; use crate::backend::error::MyResult; -use crate::backend::database::article::DbArticle; -use crate::backend::database::edit::DbEdit; use crate::backend::database::instance::DbInstance; use crate::backend::federation::activities::reject::RejectEdit; use crate::backend::federation::activities::update_local_article::UpdateLocalArticle; use crate::backend::federation::objects::edit::ApubEdit; use crate::backend::federation::send_activity; use crate::backend::utils::generate_activity_id; +use crate::common::DbArticle; +use crate::common::DbEdit; use activitypub_federation::kinds::activity::UpdateType; use activitypub_federation::{ config::Data, diff --git a/src/backend/federation/mod.rs b/src/backend/federation/mod.rs index fec16e1..b9b3fbd 100644 --- a/src/backend/federation/mod.rs +++ b/src/backend/federation/mod.rs @@ -3,9 +3,9 @@ use activitypub_federation::activity_sending::SendActivityTask; use activitypub_federation::config::Data; use activitypub_federation::protocol::context::WithContext; use activitypub_federation::traits::{ActivityHandler, Actor}; +use log::warn; use serde::Serialize; use std::fmt::Debug; -use log::warn; use url::Url; pub mod activities; diff --git a/src/backend/federation/objects/article.rs b/src/backend/federation/objects/article.rs index cf91dcb..063e1e4 100644 --- a/src/backend/federation/objects/article.rs +++ b/src/backend/federation/objects/article.rs @@ -1,9 +1,10 @@ use crate::backend::database::article::DbArticleForm; use crate::backend::database::instance::DbInstance; -use crate::backend::database::version::EditVersion; -use crate::backend::database::{article::DbArticle, MyDataHandle}; +use crate::backend::database::MyDataHandle; use crate::backend::error::Error; use crate::backend::federation::objects::edits_collection::DbEditCollection; +use crate::common::DbArticle; +use crate::common::EditVersion; use activitypub_federation::config::Data; use activitypub_federation::fetch::collection_id::CollectionId; use activitypub_federation::kinds::object::ArticleType; @@ -48,7 +49,7 @@ impl Object for DbArticle { let local_instance = DbInstance::read_local_instance(&data.db_connection)?; Ok(ApubArticle { kind: Default::default(), - id: self.ap_id.clone(), + id: ObjectId::parse(&self.ap_id)?, attributed_to: local_instance.ap_id.clone(), to: vec![public(), local_instance.followers_url()?], edits: self.edits_id()?, diff --git a/src/backend/federation/objects/articles_collection.rs b/src/backend/federation/objects/articles_collection.rs index e636d87..1463ddf 100644 --- a/src/backend/federation/objects/articles_collection.rs +++ b/src/backend/federation/objects/articles_collection.rs @@ -1,8 +1,9 @@ use crate::backend::database::instance::DbInstance; -use crate::backend::database::{article::DbArticle, MyDataHandle}; +use crate::backend::database::MyDataHandle; use crate::backend::error::Error; use crate::backend::federation::objects::article::ApubArticle; +use crate::common::DbArticle; use activitypub_federation::kinds::collection::CollectionType; use activitypub_federation::{ config::Data, diff --git a/src/backend/federation/objects/edit.rs b/src/backend/federation/objects/edit.rs index d7a3b2a..05466b9 100644 --- a/src/backend/federation/objects/edit.rs +++ b/src/backend/federation/objects/edit.rs @@ -1,9 +1,9 @@ -use crate::backend::database::article::DbArticle; -use crate::backend::database::edit::{DbEdit, DbEditForm}; +use crate::backend::database::edit::DbEditForm; use crate::backend::database::user::DbPerson; -use crate::backend::database::version::EditVersion; use crate::backend::database::MyDataHandle; use crate::backend::error::Error; +use crate::common::EditVersion; +use crate::common::{DbArticle, DbEdit}; use activitypub_federation::config::Data; use activitypub_federation::fetch::object_id::ObjectId; use activitypub_federation::traits::Object; @@ -48,11 +48,11 @@ impl Object for DbEdit { let creator = DbPerson::read(self.creator_id, data)?; Ok(ApubEdit { kind: PatchType::Patch, - id: self.ap_id, + id: ObjectId::parse(&self.ap_id)?, content: self.diff, version: self.hash, previous_version: self.previous_version_id, - object: article.ap_id, + object: ObjectId::parse(&article.ap_id)?, attributed_to: creator.ap_id, }) } diff --git a/src/backend/federation/objects/edits_collection.rs b/src/backend/federation/objects/edits_collection.rs index a8c23c5..3336213 100644 --- a/src/backend/federation/objects/edits_collection.rs +++ b/src/backend/federation/objects/edits_collection.rs @@ -1,10 +1,10 @@ -use crate::backend::database::article::DbArticle; use crate::backend::database::MyDataHandle; use crate::backend::error::Error; use crate::backend::federation::objects::edit::ApubEdit; +use crate::common::DbArticle; -use crate::backend::database::edit::DbEdit; use crate::backend::database::instance::DbInstance; +use crate::common::DbEdit; use activitypub_federation::kinds::collection::OrderedCollectionType; use activitypub_federation::{ config::Data, diff --git a/src/backend/federation/objects/instance.rs b/src/backend/federation/objects/instance.rs index 6fc010f..a5ee0ef 100644 --- a/src/backend/federation/objects/instance.rs +++ b/src/backend/federation/objects/instance.rs @@ -1,12 +1,13 @@ use crate::backend::database::instance::{DbInstance, DbInstanceForm}; use crate::backend::database::MyDataHandle; -use crate::backend::error::{Error, MyResult}; +use crate::backend::error::Error; use crate::backend::federation::objects::articles_collection::DbArticleCollection; use crate::backend::federation::send_activity; use activitypub_federation::fetch::collection_id::CollectionId; use activitypub_federation::kinds::actor::ServiceType; +use crate::backend::error::MyResult; use activitypub_federation::traits::ActivityHandler; use activitypub_federation::{ config::Data, @@ -17,7 +18,6 @@ use activitypub_federation::{ use chrono::{DateTime, Local, Utc}; use serde::{Deserialize, Serialize}; use std::fmt::Debug; - use url::Url; #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/src/backend/federation/routes.rs b/src/backend/federation/routes.rs index 9eade4c..30bebf8 100644 --- a/src/backend/federation/routes.rs +++ b/src/backend/federation/routes.rs @@ -1,8 +1,8 @@ -use crate::backend::database::article::DbArticle; use crate::backend::database::instance::DbInstance; use crate::backend::database::user::DbPerson; use crate::backend::database::MyDataHandle; -use crate::backend::error::{Error, MyResult}; +use crate::backend::error::Error; +use crate::backend::error::MyResult; use crate::backend::federation::activities::accept::Accept; use crate::backend::federation::activities::create_article::CreateArticle; use crate::backend::federation::activities::follow::Follow; @@ -10,10 +10,13 @@ use crate::backend::federation::activities::reject::RejectEdit; use crate::backend::federation::activities::update_local_article::UpdateLocalArticle; use crate::backend::federation::activities::update_remote_article::UpdateRemoteArticle; use crate::backend::federation::objects::article::ApubArticle; -use crate::backend::federation::objects::articles_collection::{ArticleCollection, DbArticleCollection}; +use crate::backend::federation::objects::articles_collection::{ + ArticleCollection, DbArticleCollection, +}; use crate::backend::federation::objects::edits_collection::{ApubEditCollection, DbEditCollection}; use crate::backend::federation::objects::instance::ApubInstance; use crate::backend::federation::objects::user::ApubUser; +use crate::common::DbArticle; use activitypub_federation::axum::inbox::{receive_activity, ActivityData}; use activitypub_federation::axum::json::FederationJson; use activitypub_federation::config::Data; @@ -32,8 +35,9 @@ use url::Url; pub fn federation_routes() -> Router { Router::new() - // TODO - //.route("/", get(http_get_instance)) + // TODO: would be nice if this could be at / but axum doesnt properly support routing by headers + // https://github.com/tokio-rs/axum/issues/1654 + .route("/instance", get(http_get_instance)) .route("/user/:name", get(http_get_person)) .route("/all_articles", get(http_get_all_articles)) .route("/article/:title", get(http_get_article)) diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 100366e..cb2d9d1 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,8 +1,11 @@ +use crate::backend::database::article::DbArticleForm; use crate::backend::database::instance::{DbInstance, DbInstanceForm}; use crate::backend::database::MyData; use crate::backend::error::MyResult; use crate::backend::federation::routes::federation_routes; use crate::backend::utils::generate_activity_id; +use crate::common::DbArticle; +use crate::frontend::app::App; use activitypub_federation::config::{FederationConfig, FederationMiddleware}; use activitypub_federation::fetch::collection_id::CollectionId; use activitypub_federation::fetch::object_id::ObjectId; @@ -15,12 +18,12 @@ use diesel::PgConnection; use diesel_migrations::embed_migrations; use diesel_migrations::EmbeddedMigrations; use diesel_migrations::MigrationHarness; +use leptos::*; +use leptos_axum::{generate_route_list, LeptosRoutes}; use std::net::ToSocketAddrs; use std::sync::{Arc, Mutex}; use tower_http::cors::CorsLayer; -use log::info; -use leptos_axum::{generate_route_list, LeptosRoutes}; -use leptos::*;use leptos_meta::*;use leptos_router::*; +use tower_http::services::ServeFile; pub mod api; pub mod database; @@ -28,7 +31,6 @@ pub mod error; pub mod federation; mod utils; - const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); pub async fn start(hostname: &str, database_url: &str) -> MyResult<()> { @@ -47,57 +49,57 @@ pub async fn start(hostname: &str, database_url: &str) -> MyResult<()> { .build() .await?; + // Create local instance if it doesnt exist yet // TODO: Move this into setup api call - let ap_id = ObjectId::parse(&format!("http://{}", hostname))?; - let articles_url = CollectionId::parse(&format!("http://{}/all_articles", hostname))?; - let inbox_url = format!("http://{}/inbox", hostname); - let keypair = generate_actor_keypair()?; - let form = DbInstanceForm { - ap_id, - articles_url, - inbox_url, - public_key: keypair.public_key, - private_key: Some(keypair.private_key), - last_refreshed_at: Local::now().into(), - local: true, - }; - DbInstance::create(&form, &config.db_connection)?; + if DbInstance::read_local_instance(&config.db_connection).is_err() { + // TODO: workaround because axum makes it hard to have multiple routes on / + let ap_id = ObjectId::parse(&format!("http://{}/instance", hostname))?; + let articles_url = CollectionId::parse(&format!("http://{}/all_articles", hostname))?; + let inbox_url = format!("http://{}/inbox", hostname); + let keypair = generate_actor_keypair()?; + let form = DbInstanceForm { + ap_id, + articles_url, + inbox_url, + public_key: keypair.public_key, + private_key: Some(keypair.private_key), + last_refreshed_at: Local::now().into(), + local: true, + }; + let instance = DbInstance::create(&form, &config.db_connection)?; + + // Create the main page which is shown by default + let form = DbArticleForm { + title: "Main Page".to_string(), + text: "Hello world!".to_string(), + ap_id: ObjectId::parse("http://{hostname}/article/Main_Page")?, + instance_id: instance.id, + local: true, + }; + DbArticle::create(&form, &config.db_connection)?; + } let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); - let leptos_options = conf.leptos_options; - let addr = leptos_options.site_addr; + let mut leptos_options = conf.leptos_options; + let addr = hostname + .to_socket_addrs()? + .next() + .expect("Failed to lookup domain name"); + leptos_options.site_addr = addr; let routes = generate_route_list(App); - info!("Listening with axum on {hostname}"); let config = config.clone(); let app = Router::new() - .leptos_routes(&leptos_options, routes, || view! { } ) + .leptos_routes(&leptos_options, routes, || view! { }) .with_state(leptos_options) + .route_service("/style.css", ServeFile::new("style.css")) .nest("", federation_routes()) .nest("/api/v1", api_routes()) .layer(FederationMiddleware::new(config)) .layer(CorsLayer::permissive()); - /* - let addr = hostname - .to_socket_addrs()? - .next() - .expect("Failed to lookup domain name"); - */ + dbg!(&addr, &hostname); Server::bind(&addr).serve(app.into_make_service()).await?; Ok(()) } - -#[component] -pub fn App() -> impl IntoView { - provide_meta_context(); - view! { - <> - - - - test - - } -} \ No newline at end of file diff --git a/src/backend/utils.rs b/src/backend/utils.rs index 08bd8d1..0188104 100644 --- a/src/backend/utils.rs +++ b/src/backend/utils.rs @@ -1,6 +1,6 @@ -use crate::backend::database::edit::DbEdit; -use crate::backend::database::version::EditVersion; use crate::backend::error::MyResult; +use crate::common::DbEdit; +use crate::common::EditVersion; use anyhow::anyhow; use diffy::{apply, Patch}; use rand::{distributions::Alphanumeric, thread_rng, Rng}; diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 0000000..638805f --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,56 @@ +#[cfg(feature = "ssr")] +use crate::backend::database::schema::{article, edit}; +#[cfg(feature = "ssr")] +use diesel::{Identifiable, Queryable, Selectable}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Deserialize, Serialize, Clone)] +pub struct GetArticleData { + pub title: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[cfg_attr(feature = "ssr", derive(Queryable))] +#[cfg_attr(feature = "ssr", diesel(table_name = article, check_for_backend(diesel::pg::Pg)))] +pub struct ArticleView { + pub article: DbArticle, + pub latest_version: EditVersion, + pub edits: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Identifiable))] +#[cfg_attr(feature = "ssr", diesel(table_name = article, check_for_backend(diesel::pg::Pg), belongs_to(DbInstance, foreign_key = instance_id)))] +pub struct DbArticle { + pub id: i32, + pub title: String, + pub text: String, + pub ap_id: String, + pub instance_id: i32, + pub local: bool, +} + +/// Represents a single change to the article. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[cfg_attr(feature = "ssr", derive(Queryable, Selectable))] +#[cfg_attr(feature = "ssr", diesel(table_name = edit, check_for_backend(diesel::pg::Pg)))] +pub struct DbEdit { + // TODO: we could use hash as primary key, but that gives errors on forking because + // the same edit is used for multiple articles + pub id: i32, + pub creator_id: i32, + /// UUID built from sha224 hash of diff + pub hash: EditVersion, + pub ap_id: String, + pub diff: String, + pub article_id: i32, + /// First edit of an article always has `EditVersion::default()` here + pub previous_version_id: EditVersion, +} + +/// The version hash of a specific edit. Generated by taking an SHA256 hash of the diff +/// and using the first 16 bytes so that it fits into UUID. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "ssr", derive(diesel_derive_newtype::DieselNewType))] +pub struct EditVersion(pub(crate) Uuid); diff --git a/src/frontend/api.rs b/src/frontend/api.rs new file mode 100644 index 0000000..43ea9cb --- /dev/null +++ b/src/frontend/api.rs @@ -0,0 +1,41 @@ +use crate::common::ArticleView; +use crate::common::GetArticleData; +use anyhow::anyhow; +use once_cell::sync::Lazy; +use reqwest::{Client, RequestBuilder}; +use serde::{Deserialize, Serialize}; + +pub static CLIENT: Lazy = Lazy::new(Client::new); + +pub async fn get_article(hostname: &str, title: String) -> ArticleView { + let get_article = GetArticleData { title }; + get_query::(hostname, "article", Some(get_article.clone())).await +} + +pub async fn get_query(hostname: &str, endpoint: &str, query: Option) -> T +where + T: for<'de> Deserialize<'de>, + R: Serialize, +{ + let mut req = CLIENT.get(format!("http://{}/api/v1/{}", hostname, endpoint)); + if let Some(query) = query { + req = req.query(&query); + } + handle_json_res::(req).await +} + +pub async fn handle_json_res(req: RequestBuilder) -> T +where + T: for<'de> Deserialize<'de>, +{ + let res = req.send().await.unwrap(); + let status = res.status(); + let text = res.text().await.unwrap(); + if status == reqwest::StatusCode::OK { + serde_json::from_str(&text) + .map_err(|e| anyhow!("Json error on {text}: {e}")) + .unwrap() + } else { + Err(anyhow!("API error: {text}")).unwrap() + } +} diff --git a/src/frontend/app.rs b/src/frontend/app.rs new file mode 100644 index 0000000..71ff3d5 --- /dev/null +++ b/src/frontend/app.rs @@ -0,0 +1,29 @@ +use crate::frontend::article::Article; +use crate::frontend::nav::Nav; +use leptos::{component, view, IntoView}; +use leptos_meta::provide_meta_context; +use leptos_meta::*; +use leptos_router::Route; +use leptos_router::Router; +use leptos_router::Routes; + +#[component] +pub fn App() -> impl IntoView { + provide_meta_context(); + view! { + <> + + + + +