mirror of
https://github.com/Nutomic/ibis.git
synced 2024-11-25 23:01:08 +00:00
Add frontend button for instance follow, add instance.domain column
This commit is contained in:
parent
575ef14a23
commit
a2b808ce57
16 changed files with 86 additions and 37 deletions
|
@ -1,5 +1,6 @@
|
||||||
create table instance (
|
create table instance (
|
||||||
id serial primary key,
|
id serial primary key,
|
||||||
|
domain text not null unique,
|
||||||
ap_id varchar(255) not null unique,
|
ap_id varchar(255) not null unique,
|
||||||
description text,
|
description text,
|
||||||
inbox_url text not null,
|
inbox_url text not null,
|
||||||
|
|
|
@ -31,7 +31,7 @@ pub(in crate::backend::api) async fn follow_instance(
|
||||||
let pending = !target.local;
|
let pending = !target.local;
|
||||||
DbInstance::follow(&user.person, &target, pending, &data)?;
|
DbInstance::follow(&user.person, &target, pending, &data)?;
|
||||||
let instance = DbInstance::read(query.id, &data.db_connection)?;
|
let instance = DbInstance::read(query.id, &data.db_connection)?;
|
||||||
Follow::send(user.person, instance, &data).await?;
|
Follow::send(user.person, &instance, &data).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -86,13 +86,9 @@ impl DbArticle {
|
||||||
.inner_join(instance::table)
|
.inner_join(instance::table)
|
||||||
.filter(article::dsl::title.eq(title))
|
.filter(article::dsl::title.eq(title))
|
||||||
.into_boxed();
|
.into_boxed();
|
||||||
let query = if let Some(mut instance_domain) = instance_domain {
|
let query = if let Some(instance_domain) = instance_domain {
|
||||||
// TODO: fragile
|
|
||||||
if !instance_domain.starts_with("http") {
|
|
||||||
instance_domain = format!("http://{instance_domain}/");
|
|
||||||
}
|
|
||||||
query
|
query
|
||||||
.filter(instance::dsl::ap_id.eq(instance_domain))
|
.filter(instance::dsl::domain.eq(instance_domain))
|
||||||
.filter(instance::dsl::local.eq(false))
|
.filter(instance::dsl::local.eq(false))
|
||||||
} else {
|
} else {
|
||||||
query.filter(article::dsl::local.eq(true))
|
query.filter(article::dsl::local.eq(true))
|
||||||
|
|
|
@ -18,6 +18,7 @@ use std::sync::Mutex;
|
||||||
#[derive(Debug, Clone, Insertable, AsChangeset)]
|
#[derive(Debug, Clone, Insertable, AsChangeset)]
|
||||||
#[diesel(table_name = instance, check_for_backend(diesel::pg::Pg))]
|
#[diesel(table_name = instance, check_for_backend(diesel::pg::Pg))]
|
||||||
pub struct DbInstanceForm {
|
pub struct DbInstanceForm {
|
||||||
|
pub domain: String,
|
||||||
pub ap_id: ObjectId<DbInstance>,
|
pub ap_id: ObjectId<DbInstance>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub articles_url: CollectionId<DbArticleCollection>,
|
pub articles_url: CollectionId<DbArticleCollection>,
|
||||||
|
|
|
@ -41,6 +41,7 @@ diesel::table! {
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
instance (id) {
|
instance (id) {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
|
domain -> Text,
|
||||||
#[max_length = 255]
|
#[max_length = 255]
|
||||||
ap_id -> Varchar,
|
ap_id -> Varchar,
|
||||||
description -> Nullable<Text>,
|
description -> Nullable<Text>,
|
||||||
|
|
|
@ -26,7 +26,7 @@ pub struct Follow {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 id = generate_activity_id(actor.ap_id.inner())?;
|
||||||
let follow = Follow {
|
let follow = Follow {
|
||||||
actor: actor.ap_id.clone(),
|
actor: actor.ap_id.clone(),
|
||||||
|
|
|
@ -108,7 +108,12 @@ impl Object for DbInstance {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error> {
|
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 {
|
let form = DbInstanceForm {
|
||||||
|
domain,
|
||||||
ap_id: json.id,
|
ap_id: json.id,
|
||||||
description: json.content,
|
description: json.content,
|
||||||
articles_url: json.articles,
|
articles_url: json.articles,
|
||||||
|
|
|
@ -117,6 +117,7 @@ async fn setup(data: &Data<IbisData>) -> Result<(), Error> {
|
||||||
let inbox_url = format!("http://{domain}/inbox");
|
let inbox_url = format!("http://{domain}/inbox");
|
||||||
let keypair = generate_actor_keypair()?;
|
let keypair = generate_actor_keypair()?;
|
||||||
let form = DbInstanceForm {
|
let form = DbInstanceForm {
|
||||||
|
domain: domain.to_string(),
|
||||||
ap_id,
|
ap_id,
|
||||||
description: Some("New Ibis instance".to_string()),
|
description: Some("New Ibis instance".to_string()),
|
||||||
articles_url,
|
articles_url,
|
||||||
|
|
|
@ -216,6 +216,7 @@ pub struct ApiConflict {
|
||||||
#[cfg_attr(feature = "ssr", diesel(table_name = instance, check_for_backend(diesel::pg::Pg)))]
|
#[cfg_attr(feature = "ssr", diesel(table_name = instance, check_for_backend(diesel::pg::Pg)))]
|
||||||
pub struct DbInstance {
|
pub struct DbInstance {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
|
pub domain: String,
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub ap_id: ObjectId<DbInstance>,
|
pub ap_id: ObjectId<DbInstance>,
|
||||||
#[cfg(not(feature = "ssr"))]
|
#[cfg(not(feature = "ssr"))]
|
||||||
|
|
|
@ -98,7 +98,10 @@ impl ApiClient {
|
||||||
self.get_query("instance", None::<i32>).await
|
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
|
// fetch beta instance on alpha
|
||||||
let resolve_form = ResolveObject {
|
let resolve_form = ResolveObject {
|
||||||
id: Url::parse(&format!("http://{}", follow_instance))?,
|
id: Url::parse(&format!("http://{}", follow_instance))?,
|
||||||
|
@ -111,6 +114,11 @@ impl ApiClient {
|
||||||
let follow_form = FollowInstance {
|
let follow_form = FollowInstance {
|
||||||
id: instance_resolved.id,
|
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
|
// cant use post helper because follow doesnt return json
|
||||||
let res = self
|
let res = self
|
||||||
.client
|
.client
|
||||||
|
@ -119,7 +127,7 @@ impl ApiClient {
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
if res.status() == StatusCode::OK {
|
if res.status() == StatusCode::OK {
|
||||||
Ok(instance_resolved)
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!("API error: {}", res.text().await?).into())
|
Err(anyhow!("API error: {}", res.text().await?).into())
|
||||||
}
|
}
|
||||||
|
@ -130,7 +138,7 @@ impl ApiClient {
|
||||||
"http://{}/api/v1/account/my_profile",
|
"http://{}/api/v1/account/my_profile",
|
||||||
self.hostname
|
self.hostname
|
||||||
));
|
));
|
||||||
handle_json_res::<LocalUserView>(req).await
|
handle_json_res(req).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn logout(&self) -> MyResult<()> {
|
pub async fn logout(&self) -> MyResult<()> {
|
||||||
|
|
|
@ -37,7 +37,7 @@ impl GlobalState {
|
||||||
.api_client
|
.api_client
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_my_profile(&self) {
|
pub fn update_my_profile() {
|
||||||
create_local_resource(
|
create_local_resource(
|
||||||
move || (),
|
move || (),
|
||||||
|_| async move {
|
|_| async move {
|
||||||
|
@ -78,7 +78,7 @@ pub fn App() -> impl IntoView {
|
||||||
my_profile: None,
|
my_profile: None,
|
||||||
};
|
};
|
||||||
// Load user profile in case we are already logged in
|
// 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));
|
provide_context(create_rw_signal(backend_hostname));
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
|
|
|
@ -8,9 +8,7 @@ pub fn Nav() -> impl IntoView {
|
||||||
let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
|
let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
|
||||||
let logout_action = create_action(move |_| async move {
|
let logout_action = create_action(move |_| async move {
|
||||||
GlobalState::api_client().logout().await.unwrap();
|
GlobalState::api_client().logout().await.unwrap();
|
||||||
expect_context::<RwSignal<GlobalState>>()
|
GlobalState::update_my_profile();
|
||||||
.get_untracked()
|
|
||||||
.update_my_profile();
|
|
||||||
});
|
});
|
||||||
let registration_open = create_local_resource(
|
let registration_open = create_local_resource(
|
||||||
|| (),
|
|| (),
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::common::DbInstance;
|
use crate::common::{DbInstance, FollowInstance};
|
||||||
use crate::frontend::app::GlobalState;
|
use crate::frontend::app::GlobalState;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_router::use_params_map;
|
use leptos_router::use_params_map;
|
||||||
|
@ -6,6 +6,7 @@ use url::Url;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn InstanceDetails() -> impl IntoView {
|
pub fn InstanceDetails() -> impl IntoView {
|
||||||
|
let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
|
||||||
let params = use_params_map();
|
let params = use_params_map();
|
||||||
let hostname = move || params.get().get("hostname").cloned().unwrap();
|
let hostname = move || params.get().get("hostname").cloned().unwrap();
|
||||||
let instance_profile = create_resource(hostname, move |hostname| async move {
|
let instance_profile = create_resource(hostname, move |hostname| async move {
|
||||||
|
@ -15,16 +16,42 @@ pub fn InstanceDetails() -> impl IntoView {
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.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! {
|
view! {
|
||||||
<Suspense fallback=|| view! { "Loading..." }> {
|
<Suspense fallback=|| view! { "Loading..." }> {
|
||||||
move || instance_profile.get().map(|instance: DbInstance| {
|
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! {
|
view! {
|
||||||
<h1>{instance.ap_id.to_string()}</h1>
|
<h1>{instance.domain}</h1>
|
||||||
<button text="Follow"/>
|
|
||||||
|
<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>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>
|
<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>
|
}</Suspense>
|
||||||
|
|
|
@ -71,9 +71,9 @@ pub fn Search() -> impl IntoView {
|
||||||
{
|
{
|
||||||
// render resolved instance
|
// render resolved instance
|
||||||
if let Some(instance) = &search_results.instance {
|
if let Some(instance) = &search_results.instance {
|
||||||
let ap_id = instance.ap_id.to_string();
|
let domain = &instance.domain;
|
||||||
vec![view! { <li>
|
vec![view! { <li>
|
||||||
<a href={format!("/instance/{ap_id}")}>{ap_id}</a>
|
<a href={format!("/instance/{domain}")}>{domain}</a>
|
||||||
</li>}]
|
</li>}]
|
||||||
} else { vec![] }
|
} else { vec![] }
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ impl TestData {
|
||||||
// Give each test a moment to start its postgres databases
|
// Give each test a moment to start its postgres databases
|
||||||
sleep(Duration::from_millis(current_run as u64 * 2000));
|
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_alpha = first_port;
|
||||||
let port_beta = first_port + 1;
|
let port_beta = first_port + 1;
|
||||||
let port_gamma = first_port + 2;
|
let port_gamma = first_port + 2;
|
||||||
|
|
|
@ -103,7 +103,9 @@ async fn test_follow_instance() -> MyResult<()> {
|
||||||
let beta_instance = data.beta.get_local_instance().await?;
|
let beta_instance = data.beta.get_local_instance().await?;
|
||||||
assert_eq!(0, beta_instance.followers.len());
|
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
|
// check that follow was federated
|
||||||
let alpha_user = data.alpha.my_profile().await?;
|
let alpha_user = data.alpha.my_profile().await?;
|
||||||
|
@ -159,7 +161,7 @@ async fn test_synchronize_articles() -> MyResult<()> {
|
||||||
assert!(get_res.is_err());
|
assert!(get_res.is_err());
|
||||||
|
|
||||||
// get the article with instance id and compare
|
// 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?;
|
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_res.article.ap_id, get_res.article.ap_id);
|
||||||
assert_eq!(create_form.title, get_res.article.title);
|
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<()> {
|
async fn test_edit_local_article() -> MyResult<()> {
|
||||||
let data = TestData::start().await;
|
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
|
// create new article
|
||||||
let create_form = CreateArticleData {
|
let create_form = CreateArticleData {
|
||||||
|
@ -189,8 +194,7 @@ async fn test_edit_local_article() -> MyResult<()> {
|
||||||
// article should be federated to alpha
|
// article should be federated to alpha
|
||||||
let get_article_data = GetArticleData {
|
let get_article_data = GetArticleData {
|
||||||
title: Some(create_res.article.title.to_string()),
|
title: Some(create_res.article.title.to_string()),
|
||||||
// TODO: this is wrong
|
instance_domain: Some(beta_instance.domain),
|
||||||
instance_domain: Some(beta_instance.ap_id.to_string()),
|
|
||||||
id: None,
|
id: None,
|
||||||
};
|
};
|
||||||
let get_res = data.alpha.get_article(get_article_data.clone()).await?;
|
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<()> {
|
async fn test_edit_remote_article() -> MyResult<()> {
|
||||||
let data = TestData::start().await;
|
let data = TestData::start().await;
|
||||||
|
|
||||||
let beta_id_on_alpha = data.alpha.follow_instance(&data.beta.hostname).await?;
|
let beta_id_on_alpha = data
|
||||||
let beta_id_on_gamma = data.gamma.follow_instance(&data.beta.hostname).await?;
|
.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
|
// create new article
|
||||||
let create_form = CreateArticleData {
|
let create_form = CreateArticleData {
|
||||||
|
@ -244,8 +254,7 @@ async fn test_edit_remote_article() -> MyResult<()> {
|
||||||
// article should be federated to alpha and gamma
|
// article should be federated to alpha and gamma
|
||||||
let get_article_data_alpha = GetArticleData {
|
let get_article_data_alpha = GetArticleData {
|
||||||
title: Some(create_res.article.title.to_string()),
|
title: Some(create_res.article.title.to_string()),
|
||||||
// TODO: wrong
|
instance_domain: Some(beta_id_on_alpha.domain),
|
||||||
instance_domain: Some(beta_id_on_alpha.ap_id.to_string()),
|
|
||||||
id: None,
|
id: None,
|
||||||
};
|
};
|
||||||
let get_res = data
|
let get_res = data
|
||||||
|
@ -258,8 +267,7 @@ async fn test_edit_remote_article() -> MyResult<()> {
|
||||||
|
|
||||||
let get_article_data_gamma = GetArticleData {
|
let get_article_data_gamma = GetArticleData {
|
||||||
title: Some(create_res.article.title.to_string()),
|
title: Some(create_res.article.title.to_string()),
|
||||||
// TODO: wrong
|
instance_domain: Some(beta_id_on_gamma.domain),
|
||||||
instance_domain: Some(beta_id_on_gamma.ap_id.to_string()),
|
|
||||||
id: None,
|
id: None,
|
||||||
};
|
};
|
||||||
let get_res = data
|
let get_res = data
|
||||||
|
@ -364,7 +372,10 @@ async fn test_local_edit_conflict() -> MyResult<()> {
|
||||||
async fn test_federated_edit_conflict() -> MyResult<()> {
|
async fn test_federated_edit_conflict() -> MyResult<()> {
|
||||||
let data = TestData::start().await;
|
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
|
// create new article
|
||||||
let create_form = CreateArticleData {
|
let create_form = CreateArticleData {
|
||||||
|
@ -386,8 +397,7 @@ async fn test_federated_edit_conflict() -> MyResult<()> {
|
||||||
// alpha edits article
|
// alpha edits article
|
||||||
let get_article_data = GetArticleData {
|
let get_article_data = GetArticleData {
|
||||||
title: Some(create_form.title.to_string()),
|
title: Some(create_form.title.to_string()),
|
||||||
// TODO: wrong
|
instance_domain: Some(beta_id_on_alpha.domain),
|
||||||
instance_domain: Some(beta_id_on_alpha.ap_id.to_string()),
|
|
||||||
id: None,
|
id: None,
|
||||||
};
|
};
|
||||||
let get_res = data.alpha.get_article(get_article_data).await?;
|
let get_res = data.alpha.get_article(get_article_data).await?;
|
||||||
|
|
Loading…
Reference in a new issue