From a2b808ce5758d9720c4ffbe47ec967c229587918 Mon Sep 17 00:00:00 2001 From: Felix Ableitner Date: Mon, 12 Feb 2024 16:37:12 +0100 Subject: [PATCH] Add frontend button for instance follow, add instance.domain column --- .../2023-11-28-150402_ibis_setup/up.sql | 1 + src/backend/api/instance.rs | 2 +- src/backend/database/article.rs | 8 +--- src/backend/database/instance.rs | 1 + src/backend/database/schema.rs | 1 + src/backend/federation/activities/follow.rs | 2 +- src/backend/federation/objects/instance.rs | 5 +++ src/backend/mod.rs | 1 + src/common/mod.rs | 1 + src/frontend/api.rs | 14 +++++-- src/frontend/app.rs | 4 +- src/frontend/components/nav.rs | 4 +- src/frontend/pages/instance_details.rs | 35 +++++++++++++++-- src/frontend/pages/search.rs | 4 +- tests/common.rs | 2 +- tests/test.rs | 38 ++++++++++++------- 16 files changed, 86 insertions(+), 37 deletions(-) diff --git a/migrations/2023-11-28-150402_ibis_setup/up.sql b/migrations/2023-11-28-150402_ibis_setup/up.sql index 642a43c..9c46b6a 100644 --- a/migrations/2023-11-28-150402_ibis_setup/up.sql +++ b/migrations/2023-11-28-150402_ibis_setup/up.sql @@ -1,5 +1,6 @@ create table instance ( id serial primary key, + domain text not null unique, ap_id varchar(255) not null unique, description text, inbox_url text not null, diff --git a/src/backend/api/instance.rs b/src/backend/api/instance.rs index acdff0f..bd99aef 100644 --- a/src/backend/api/instance.rs +++ b/src/backend/api/instance.rs @@ -31,7 +31,7 @@ pub(in crate::backend::api) async fn follow_instance( let pending = !target.local; DbInstance::follow(&user.person, &target, pending, &data)?; let instance = DbInstance::read(query.id, &data.db_connection)?; - Follow::send(user.person, instance, &data).await?; + Follow::send(user.person, &instance, &data).await?; Ok(()) } diff --git a/src/backend/database/article.rs b/src/backend/database/article.rs index 3e47879..d1c14d0 100644 --- a/src/backend/database/article.rs +++ b/src/backend/database/article.rs @@ -86,13 +86,9 @@ impl DbArticle { .inner_join(instance::table) .filter(article::dsl::title.eq(title)) .into_boxed(); - let query = if let Some(mut instance_domain) = instance_domain { - // TODO: fragile - if !instance_domain.starts_with("http") { - instance_domain = format!("http://{instance_domain}/"); - } + let query = if let Some(instance_domain) = instance_domain { query - .filter(instance::dsl::ap_id.eq(instance_domain)) + .filter(instance::dsl::domain.eq(instance_domain)) .filter(instance::dsl::local.eq(false)) } else { query.filter(article::dsl::local.eq(true)) diff --git a/src/backend/database/instance.rs b/src/backend/database/instance.rs index 8c057ce..90eb6c5 100644 --- a/src/backend/database/instance.rs +++ b/src/backend/database/instance.rs @@ -18,6 +18,7 @@ use std::sync::Mutex; #[derive(Debug, Clone, Insertable, AsChangeset)] #[diesel(table_name = instance, check_for_backend(diesel::pg::Pg))] pub struct DbInstanceForm { + pub domain: String, pub ap_id: ObjectId, pub description: Option, pub articles_url: CollectionId, diff --git a/src/backend/database/schema.rs b/src/backend/database/schema.rs index d4ea8ce..d2997c3 100644 --- a/src/backend/database/schema.rs +++ b/src/backend/database/schema.rs @@ -41,6 +41,7 @@ diesel::table! { diesel::table! { instance (id) { id -> Int4, + domain -> Text, #[max_length = 255] ap_id -> Varchar, description -> Nullable, diff --git a/src/backend/federation/activities/follow.rs b/src/backend/federation/activities/follow.rs index 30ca8dc..3215557 100644 --- a/src/backend/federation/activities/follow.rs +++ b/src/backend/federation/activities/follow.rs @@ -26,7 +26,7 @@ pub struct Follow { } impl Follow { - pub async fn send(actor: DbPerson, to: DbInstance, data: &Data) -> MyResult<()> { + pub async fn send(actor: DbPerson, to: &DbInstance, data: &Data) -> MyResult<()> { let id = generate_activity_id(actor.ap_id.inner())?; let follow = Follow { actor: actor.ap_id.clone(), diff --git a/src/backend/federation/objects/instance.rs b/src/backend/federation/objects/instance.rs index 69e33f9..7c1c74e 100644 --- a/src/backend/federation/objects/instance.rs +++ b/src/backend/federation/objects/instance.rs @@ -108,7 +108,12 @@ impl Object for DbInstance { } async fn from_json(json: Self::Kind, data: &Data) -> Result { + let mut domain = json.id.inner().host_str().unwrap().to_string(); + if let Some(port) = json.id.inner().port() { + domain = format!("{domain}:{port}"); + } let form = DbInstanceForm { + domain, ap_id: json.id, description: json.content, articles_url: json.articles, diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 5e9a62c..36b98d4 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -117,6 +117,7 @@ async fn setup(data: &Data) -> Result<(), Error> { let inbox_url = format!("http://{domain}/inbox"); let keypair = generate_actor_keypair()?; let form = DbInstanceForm { + domain: domain.to_string(), ap_id, description: Some("New Ibis instance".to_string()), articles_url, diff --git a/src/common/mod.rs b/src/common/mod.rs index c79825a..7b6b471 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -216,6 +216,7 @@ pub struct ApiConflict { #[cfg_attr(feature = "ssr", diesel(table_name = instance, check_for_backend(diesel::pg::Pg)))] pub struct DbInstance { pub id: i32, + pub domain: String, #[cfg(feature = "ssr")] pub ap_id: ObjectId, #[cfg(not(feature = "ssr"))] diff --git a/src/frontend/api.rs b/src/frontend/api.rs index 3fe8907..7da6b54 100644 --- a/src/frontend/api.rs +++ b/src/frontend/api.rs @@ -98,7 +98,10 @@ impl ApiClient { self.get_query("instance", None::).await } - pub async fn follow_instance(&self, follow_instance: &str) -> MyResult { + pub async fn follow_instance_with_resolve( + &self, + follow_instance: &str, + ) -> MyResult { // fetch beta instance on alpha let resolve_form = ResolveObject { id: Url::parse(&format!("http://{}", follow_instance))?, @@ -111,6 +114,11 @@ impl ApiClient { let follow_form = FollowInstance { id: instance_resolved.id, }; + self.follow_instance(follow_form).await?; + Ok(instance_resolved) + } + + pub async fn follow_instance(&self, follow_form: FollowInstance) -> MyResult<()> { // cant use post helper because follow doesnt return json let res = self .client @@ -119,7 +127,7 @@ impl ApiClient { .send() .await?; if res.status() == StatusCode::OK { - Ok(instance_resolved) + Ok(()) } else { Err(anyhow!("API error: {}", res.text().await?).into()) } @@ -130,7 +138,7 @@ impl ApiClient { "http://{}/api/v1/account/my_profile", self.hostname )); - handle_json_res::(req).await + handle_json_res(req).await } pub async fn logout(&self) -> MyResult<()> { diff --git a/src/frontend/app.rs b/src/frontend/app.rs index 47e7595..b77c7e4 100644 --- a/src/frontend/app.rs +++ b/src/frontend/app.rs @@ -37,7 +37,7 @@ impl GlobalState { .api_client } - pub fn update_my_profile(&self) { + pub fn update_my_profile() { create_local_resource( move || (), |_| async move { @@ -78,7 +78,7 @@ pub fn App() -> impl IntoView { my_profile: None, }; // Load user profile in case we are already logged in - backend_hostname.update_my_profile(); + GlobalState::update_my_profile(); provide_context(create_rw_signal(backend_hostname)); view! { diff --git a/src/frontend/components/nav.rs b/src/frontend/components/nav.rs index 11afcf6..9cf269b 100644 --- a/src/frontend/components/nav.rs +++ b/src/frontend/components/nav.rs @@ -8,9 +8,7 @@ pub fn Nav() -> impl IntoView { let global_state = use_context::>().unwrap(); let logout_action = create_action(move |_| async move { GlobalState::api_client().logout().await.unwrap(); - expect_context::>() - .get_untracked() - .update_my_profile(); + GlobalState::update_my_profile(); }); let registration_open = create_local_resource( || (), diff --git a/src/frontend/pages/instance_details.rs b/src/frontend/pages/instance_details.rs index ed8bd0e..adb9760 100644 --- a/src/frontend/pages/instance_details.rs +++ b/src/frontend/pages/instance_details.rs @@ -1,4 +1,4 @@ -use crate::common::DbInstance; +use crate::common::{DbInstance, FollowInstance}; use crate::frontend::app::GlobalState; use leptos::*; use leptos_router::use_params_map; @@ -6,6 +6,7 @@ use url::Url; #[component] pub fn InstanceDetails() -> impl IntoView { + let global_state = use_context::>().unwrap(); let params = use_params_map(); let hostname = move || params.get().get("hostname").cloned().unwrap(); let instance_profile = create_resource(hostname, move |hostname| async move { @@ -15,16 +16,42 @@ pub fn InstanceDetails() -> impl IntoView { .await .unwrap() }); + let follow_action = create_action(move |instance_id: &i32| { + let instance_id = *instance_id; + async move { + let form = FollowInstance { id: instance_id }; + GlobalState::api_client() + .follow_instance(form) + .await + .unwrap(); + GlobalState::update_my_profile(); + } + }); view! { { move || instance_profile.get().map(|instance: DbInstance| { + let instance_ = instance.clone(); + let is_following = global_state.get().my_profile.map(|p| p.following.contains(&instance_)).unwrap_or_default(); + let follow_text = if is_following { + "Following" + } else { + "Follow" + }; view! { -

{instance.ap_id.to_string()}

- +

Follow the instance so that new edits are federated to your instance.

+

"TODO: show a list of articles from the instance. For now you can use the "Article list.

+
+

"Description:"

{instance.description}
-

TODO: show a list of articles from the instance. For now you can use the Article list.

} }) }
diff --git a/src/frontend/pages/search.rs b/src/frontend/pages/search.rs index b11f4aa..786bed3 100644 --- a/src/frontend/pages/search.rs +++ b/src/frontend/pages/search.rs @@ -71,9 +71,9 @@ pub fn Search() -> impl IntoView { { // render resolved instance if let Some(instance) = &search_results.instance { - let ap_id = instance.ap_id.to_string(); + let domain = &instance.domain; vec![view! {
  • - {ap_id} + {domain}
  • }] } else { vec![] } } diff --git a/tests/common.rs b/tests/common.rs index e1c3bab..6eb0917 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -39,7 +39,7 @@ impl TestData { // Give each test a moment to start its postgres databases sleep(Duration::from_millis(current_run as u64 * 2000)); - let first_port = 8000 + (current_run * 3); + let first_port = 8100 + (current_run * 3); let port_alpha = first_port; let port_beta = first_port + 1; let port_gamma = first_port + 2; diff --git a/tests/test.rs b/tests/test.rs index 741b3f0..045c3ed 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -103,7 +103,9 @@ async fn test_follow_instance() -> MyResult<()> { let beta_instance = data.beta.get_local_instance().await?; assert_eq!(0, beta_instance.followers.len()); - data.alpha.follow_instance(&data.beta.hostname).await?; + data.alpha + .follow_instance_with_resolve(&data.beta.hostname) + .await?; // check that follow was federated let alpha_user = data.alpha.my_profile().await?; @@ -159,7 +161,7 @@ async fn test_synchronize_articles() -> MyResult<()> { assert!(get_res.is_err()); // get the article with instance id and compare - get_article_data.instance_domain = Some(instance.ap_id.to_string()); + get_article_data.instance_domain = Some(instance.domain); let get_res = data.beta.get_article(get_article_data).await?; assert_eq!(create_res.article.ap_id, get_res.article.ap_id); assert_eq!(create_form.title, get_res.article.title); @@ -174,7 +176,10 @@ async fn test_synchronize_articles() -> MyResult<()> { async fn test_edit_local_article() -> MyResult<()> { let data = TestData::start().await; - let beta_instance = data.alpha.follow_instance(&data.beta.hostname).await?; + let beta_instance = data + .alpha + .follow_instance_with_resolve(&data.beta.hostname) + .await?; // create new article let create_form = CreateArticleData { @@ -189,8 +194,7 @@ async fn test_edit_local_article() -> MyResult<()> { // article should be federated to alpha let get_article_data = GetArticleData { title: Some(create_res.article.title.to_string()), - // TODO: this is wrong - instance_domain: Some(beta_instance.ap_id.to_string()), + instance_domain: Some(beta_instance.domain), id: None, }; let get_res = data.alpha.get_article(get_article_data.clone()).await?; @@ -228,8 +232,14 @@ async fn test_edit_local_article() -> MyResult<()> { async fn test_edit_remote_article() -> MyResult<()> { let data = TestData::start().await; - let beta_id_on_alpha = data.alpha.follow_instance(&data.beta.hostname).await?; - let beta_id_on_gamma = data.gamma.follow_instance(&data.beta.hostname).await?; + let beta_id_on_alpha = data + .alpha + .follow_instance_with_resolve(&data.beta.hostname) + .await?; + let beta_id_on_gamma = data + .gamma + .follow_instance_with_resolve(&data.beta.hostname) + .await?; // create new article let create_form = CreateArticleData { @@ -244,8 +254,7 @@ async fn test_edit_remote_article() -> MyResult<()> { // article should be federated to alpha and gamma let get_article_data_alpha = GetArticleData { title: Some(create_res.article.title.to_string()), - // TODO: wrong - instance_domain: Some(beta_id_on_alpha.ap_id.to_string()), + instance_domain: Some(beta_id_on_alpha.domain), id: None, }; let get_res = data @@ -258,8 +267,7 @@ async fn test_edit_remote_article() -> MyResult<()> { let get_article_data_gamma = GetArticleData { title: Some(create_res.article.title.to_string()), - // TODO: wrong - instance_domain: Some(beta_id_on_gamma.ap_id.to_string()), + instance_domain: Some(beta_id_on_gamma.domain), id: None, }; let get_res = data @@ -364,7 +372,10 @@ async fn test_local_edit_conflict() -> MyResult<()> { async fn test_federated_edit_conflict() -> MyResult<()> { let data = TestData::start().await; - let beta_id_on_alpha = data.alpha.follow_instance(&data.beta.hostname).await?; + let beta_id_on_alpha = data + .alpha + .follow_instance_with_resolve(&data.beta.hostname) + .await?; // create new article let create_form = CreateArticleData { @@ -386,8 +397,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> { // alpha edits article let get_article_data = GetArticleData { title: Some(create_form.title.to_string()), - // TODO: wrong - instance_domain: Some(beta_id_on_alpha.ap_id.to_string()), + instance_domain: Some(beta_id_on_alpha.domain), id: None, }; let get_res = data.alpha.get_article(get_article_data).await?;