mirror of
https://github.com/Nutomic/ibis.git
synced 2024-12-23 08:41:24 +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 (
|
||||
id serial primary key,
|
||||
domain text not null unique,
|
||||
ap_id varchar(255) not null unique,
|
||||
description text,
|
||||
inbox_url text not null,
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -41,6 +41,7 @@ diesel::table! {
|
|||
diesel::table! {
|
||||
instance (id) {
|
||||
id -> Int4,
|
||||
domain -> Text,
|
||||
#[max_length = 255]
|
||||
ap_id -> Varchar,
|
||||
description -> Nullable<Text>,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"))]
|
||||
|
|
|
@ -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<()> {
|
||||
|
|
|
@ -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! {
|
||||
|
|
|
@ -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(
|
||||
|| (),
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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![] }
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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?;
|
||||
|
|
Loading…
Reference in a new issue