Add frontend button for instance follow, add instance.domain column

This commit is contained in:
Felix Ableitner 2024-02-12 16:37:12 +01:00
parent 575ef14a23
commit a2b808ce57
16 changed files with 86 additions and 37 deletions

View File

@ -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,

View File

@ -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(())
}

View File

@ -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))

View File

@ -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<DbInstance>,
pub description: Option<String>,
pub articles_url: CollectionId<DbArticleCollection>,

View File

@ -41,6 +41,7 @@ diesel::table! {
diesel::table! {
instance (id) {
id -> Int4,
domain -> Text,
#[max_length = 255]
ap_id -> Varchar,
description -> Nullable<Text>,

View File

@ -26,7 +26,7 @@ pub struct Follow {
}
impl Follow {
pub async fn send(actor: DbPerson, to: DbInstance, data: &Data<IbisData>) -> MyResult<()> {
pub async fn send(actor: DbPerson, to: &DbInstance, data: &Data<IbisData>) -> MyResult<()> {
let id = generate_activity_id(actor.ap_id.inner())?;
let follow = Follow {
actor: actor.ap_id.clone(),

View File

@ -108,7 +108,12 @@ impl Object for DbInstance {
}
async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> {
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,

View File

@ -117,6 +117,7 @@ async fn setup(data: &Data<IbisData>) -> 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,

View File

@ -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<DbInstance>,
#[cfg(not(feature = "ssr"))]

View File

@ -98,7 +98,10 @@ impl ApiClient {
self.get_query("instance", None::<i32>).await
}
pub async fn follow_instance(&self, follow_instance: &str) -> MyResult<DbInstance> {
pub async fn follow_instance_with_resolve(
&self,
follow_instance: &str,
) -> MyResult<DbInstance> {
// 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::<LocalUserView>(req).await
handle_json_res(req).await
}
pub async fn logout(&self) -> MyResult<()> {

View File

@ -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! {

View File

@ -8,9 +8,7 @@ pub fn Nav() -> impl IntoView {
let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
let logout_action = create_action(move |_| async move {
GlobalState::api_client().logout().await.unwrap();
expect_context::<RwSignal<GlobalState>>()
.get_untracked()
.update_my_profile();
GlobalState::update_my_profile();
});
let registration_open = create_local_resource(
|| (),

View File

@ -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::<RwSignal<GlobalState>>().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! {
<Suspense fallback=|| view! { "Loading..." }> {
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! {
<h1>{instance.ap_id.to_string()}</h1>
<button text="Follow"/>
<h1>{instance.domain}</h1>
<Show when=move || global_state.with(|state| state.my_profile.is_some())>
<button on:click=move |_| follow_action.dispatch(instance.id)
prop:disabled=move || is_following>
{follow_text}
</button>
</Show>
<p>Follow the instance so that new edits are federated to your instance.</p>
<p>"TODO: show a list of articles from the instance. For now you can use the "<a href="/article/list">Article list</a>.</p>
<hr/>
<h2>"Description:"</h2>
<div>{instance.description}</div>
<p>TODO: show a list of articles from the instance. For now you can use the <a href="/article/list">Article list</a>.</p>
}
})
}</Suspense>

View File

@ -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! { <li>
<a href={format!("/instance/{ap_id}")}>{ap_id}</a>
<a href={format!("/instance/{domain}")}>{domain}</a>
</li>}]
} else { vec![] }
}

View File

@ -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;

View File

@ -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?;