1
0
Fork 0
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:
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 ( 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,

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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![] }
} }

View file

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

View file

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