1
0
Fork 0
mirror of https://github.com/Nutomic/ibis.git synced 2024-11-24 14:31:09 +00:00

correctly link remote articles

This commit is contained in:
Felix Ableitner 2024-02-09 14:46:33 +01:00
parent 581d90c880
commit 0c247a8959
17 changed files with 122 additions and 57 deletions

View file

@ -6,12 +6,12 @@ set -e
# 127.0.0.1 ibis-alpha # 127.0.0.1 ibis-alpha
# 127.0.0.1 ibis-beta # 127.0.0.1 ibis-beta
# #
# Then run this script and open http://ibis-alpha:8070/, http://ibis-beta:8080/ in your browser. # Then run this script and open http://ibis-alpha:8090/, http://ibis-beta:8091/ in your browser.
function cleanup { function cleanup {
echo "stop postgres" echo "stop postgres"
./scripts/stop_dev_db.sh $ALPHA_DB_PATH ./scripts/stop_dev_db.sh $ALPHA_DB_PATH >/dev/null
./scripts/stop_dev_db.sh $BETA_DB_PATH ./scripts/stop_dev_db.sh $BETA_DB_PATH >/dev/null
} }
trap cleanup EXIT trap cleanup EXIT
@ -22,18 +22,19 @@ BETA_DB_PATH="$DB_FOLDER/beta"
# create db folders if they dont exist # create db folders if they dont exist
if [ ! -d $ALPHA_DB_PATH ]; then if [ ! -d $ALPHA_DB_PATH ]; then
./scripts/start_dev_db.sh $ALPHA_DB_PATH ./scripts/start_dev_db.sh $ALPHA_DB_PATH >/dev/null
else else
pg_ctl start --options="-c listen_addresses= -c unix_socket_directories=$ALPHA_DB_PATH" -D "$ALPHA_DB_PATH/dev_pgdata" pg_ctl start --options="-c listen_addresses= -c unix_socket_directories=$ALPHA_DB_PATH" -D "$ALPHA_DB_PATH/dev_pgdata" >/dev/null
fi fi
if [ ! -d $BETA_DB_PATH ]; then if [ ! -d $BETA_DB_PATH ]; then
./scripts/start_dev_db.sh $BETA_DB_PATH ./scripts/start_dev_db.sh $BETA_DB_PATH >/dev/null
else else
pg_ctl start --options="-c listen_addresses= -c unix_socket_directories=$BETA_DB_PATH" -D "$BETA_DB_PATH/dev_pgdata" pg_ctl start --options="-c listen_addresses= -c unix_socket_directories=$BETA_DB_PATH" -D "$BETA_DB_PATH/dev_pgdata" >/dev/null
fi fi
ALPHA_DB_URL="postgresql://ibis:password@/ibis?host=$ALPHA_DB_PATH" ALPHA_DB_URL="postgresql://ibis:password@/ibis?host=$ALPHA_DB_PATH"
BETA_DB_URL="postgresql://ibis:password@/ibis?host=$BETA_DB_PATH" BETA_DB_URL="postgresql://ibis:password@/ibis?host=$BETA_DB_PATH"
echo $ALPHA_DB_URL
# get rid of processes leftover from previous runs # get rid of processes leftover from previous runs
killall ibis || true killall ibis || true
@ -41,9 +42,9 @@ killall ibis || true
CARGO_TARGET_DIR=target/frontend trunk build CARGO_TARGET_DIR=target/frontend trunk build
# launch a couple of local instances to test federation, then wait for processes to finish # launch a couple of local instances to test federation, then wait for processes to finish
IBIS__BIND=127.0.0.1:8070 IBIS__FEDERATION__DOMAIN=ibis-alpha:8070 IBIS__DATABASE_URL=$ALPHA_DB_URL cargo run & IBIS__BIND=127.0.0.1:8090 IBIS__FEDERATION__DOMAIN=ibis-alpha:8090 IBIS__DATABASE_URL=$ALPHA_DB_URL cargo run &
PID_ALPHA=($!) PID_ALPHA=($!)
IBIS__BIND=127.0.0.1:8080 IBIS__FEDERATION__DOMAIN=ibis-beta:8080 IBIS__DATABASE_URL=$BETA_DB_URL cargo run & IBIS__BIND=127.0.0.1:8091 IBIS__FEDERATION__DOMAIN=ibis-beta:8091 IBIS__DATABASE_URL=$BETA_DB_URL cargo run &
PID_BETA=($!) PID_BETA=($!)
wait $PID_ALPHA wait $PID_ALPHA

View file

@ -140,12 +140,12 @@ pub(in crate::backend::api) async fn get_article(
match (query.title, query.id) { match (query.title, query.id) {
(Some(title), None) => Ok(Json(DbArticle::read_view_title( (Some(title), None) => Ok(Json(DbArticle::read_view_title(
&title, &title,
&query.instance_id, query.instance_domain,
&data.db_connection, &data.db_connection,
)?)), )?)),
(None, Some(id)) => { (None, Some(id)) => {
if query.instance_id.is_some() { if query.instance_domain.is_some() {
return Err(anyhow!("Cant combine id and instance_id").into()); return Err(anyhow!("Cant combine id and instance_domain").into());
} }
Ok(Json(DbArticle::read_view(id, &data.db_connection)?)) Ok(Json(DbArticle::read_view(id, &data.db_connection)?))
} }

View file

@ -1,3 +1,4 @@
use config::Config;
use doku::Document; use doku::Document;
use serde::Deserialize; use serde::Deserialize;
use smart_default::SmartDefault; use smart_default::SmartDefault;
@ -23,6 +24,19 @@ pub struct IbisConfig {
pub federation: IbisConfigFederation, pub federation: IbisConfigFederation,
} }
impl IbisConfig {
pub fn read() -> Self {
let config = Config::builder()
.add_source(config::File::with_name("config/config.toml"))
// Cant use _ as separator due to https://github.com/mehcode/config-rs/issues/391
.add_source(config::Environment::with_prefix("IBIS").separator("__"))
.build()
.unwrap();
config.try_deserialize().unwrap()
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Document, SmartDefault)] #[derive(Debug, Deserialize, PartialEq, Eq, Clone, Document, SmartDefault)]
#[serde(default)] #[serde(default)]
pub struct IbisConfigSetup { pub struct IbisConfigSetup {

View file

@ -1,4 +1,4 @@
use crate::backend::database::schema::{article, edit}; use crate::backend::database::schema::{article, edit, instance};
use crate::backend::error::MyResult; use crate::backend::error::MyResult;
use crate::backend::federation::objects::edits_collection::DbEditCollection; use crate::backend::federation::objects::edits_collection::DbEditCollection;
use crate::common::DbEdit; use crate::common::DbEdit;
@ -77,20 +77,29 @@ impl DbArticle {
pub fn read_view_title( pub fn read_view_title(
title: &str, title: &str,
instance_id: &Option<i32>, instance_domain: Option<String>,
conn: &Mutex<PgConnection>, conn: &Mutex<PgConnection>,
) -> MyResult<ArticleView> { ) -> MyResult<ArticleView> {
let article: DbArticle = { let article: DbArticle = {
let mut conn = conn.lock().unwrap(); let mut conn = conn.lock().unwrap();
let query = article::table let query = article::table
.into_boxed() .inner_join(instance::table)
.filter(article::dsl::title.eq(title)); .filter(article::dsl::title.eq(title))
let query = if let Some(instance_id) = instance_id { .into_boxed();
query.filter(article::dsl::instance_id.eq(instance_id)) let query = if let Some(mut instance_domain) = instance_domain {
// TODO: fragile
if !instance_domain.starts_with("http") {
instance_domain = format!("http://{instance_domain}/");
}
query
.filter(instance::dsl::ap_id.eq(instance_domain))
.filter(instance::dsl::local.eq(false))
} else { } else {
query.filter(article::dsl::local.eq(true)) query.filter(article::dsl::local.eq(true))
}; };
query.get_result(conn.deref_mut())? query
.select(article::all_columns)
.get_result(conn.deref_mut())?
}; };
let latest_version = article.latest_edit_version(conn)?; let latest_version = article.latest_edit_version(conn)?;
let edits = DbEdit::read_for_article(&article, conn)?; let edits = DbEdit::read_for_article(&article, conn)?;

View file

@ -18,10 +18,10 @@ use {
pub const MAIN_PAGE_NAME: &str = "Main_Page"; pub const MAIN_PAGE_NAME: &str = "Main_Page";
/// Should be an enum Title/Id but fails due to https://github.com/nox/serde_urlencoded/issues/66 /// Should be an enum Title/Id but fails due to https://github.com/nox/serde_urlencoded/issues/66
#[derive(Deserialize, Serialize, Clone)] #[derive(Deserialize, Serialize, Clone, Debug)]
pub struct GetArticleData { pub struct GetArticleData {
pub title: Option<String>, pub title: Option<String>,
pub instance_id: Option<i32>, pub instance_domain: Option<String>,
pub id: Option<i32>, pub id: Option<i32>,
} }
@ -54,12 +54,6 @@ pub struct DbArticle {
pub local: bool, pub local: bool,
} }
impl DbArticle {
pub fn title(&self) -> String {
self.title.replace('_', " ")
}
}
/// Represents a single change to the article. /// Represents a single change to the article.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable))] #[cfg_attr(feature = "ssr", derive(Queryable, Selectable))]

View file

@ -84,7 +84,7 @@ impl ApiClient {
self.get_article(GetArticleData { self.get_article(GetArticleData {
title: None, title: None,
instance_id: None, instance_domain: None,
id: Some(edit_form.article_id), id: Some(edit_form.article_id),
}) })
.await .await

View file

@ -62,12 +62,15 @@ impl GlobalState {
#[component] #[component]
pub fn App() -> impl IntoView { pub fn App() -> impl IntoView {
#[allow(unused_mut, unused_assignments)] let backend_hostname;
let mut backend_hostname = "127.0.0.1:8080".to_string();
#[cfg(not(feature = "ssr"))] #[cfg(not(feature = "ssr"))]
{ {
backend_hostname = web_sys::window().unwrap().location().host().unwrap(); backend_hostname = web_sys::window().unwrap().location().host().unwrap();
} }
#[cfg(feature = "ssr")]
{
backend_hostname = crate::backend::config::IbisConfig::read().bind.to_string();
}
provide_meta_context(); provide_meta_context();
let backend_hostname = GlobalState { let backend_hostname = GlobalState {

View file

@ -1,6 +1,7 @@
use crate::common::validation::can_edit_article; use crate::common::validation::can_edit_article;
use crate::common::ArticleView; use crate::common::ArticleView;
use crate::frontend::app::GlobalState; use crate::frontend::app::GlobalState;
use crate::frontend::article_link;
use leptos::*; use leptos::*;
use leptos_router::*; use leptos_router::*;
@ -10,16 +11,16 @@ pub fn ArticleNav(article: Resource<Option<String>, ArticleView>) -> impl IntoVi
view! { view! {
<Suspense> <Suspense>
{move || article.get().map(|article| { {move || article.get().map(|article| {
let title = article.article.title.clone(); let article_link = article_link(&article.article);
view!{ view!{
<nav class="inner"> <nav class="inner">
<A href={format!("/article/{title}")}>"Read"</A> <A href=article_link.clone()>"Read"</A>
<A href={format!("/article/{title}/history")}>"History"</A> <A href={format!("{article_link}/history")}>"History"</A>
<Show when=move || global_state.with(|state| { <Show when=move || global_state.with(|state| {
let is_admin = state.my_profile.as_ref().map(|p| p.local_user.admin).unwrap_or(false); let is_admin = state.my_profile.as_ref().map(|p| p.local_user.admin).unwrap_or(false);
state.my_profile.is_some() && can_edit_article(&article.article, is_admin).is_ok() state.my_profile.is_some() && can_edit_article(&article.article, is_admin).is_ok()
})> })>
<A href={format!("/article/{title}/edit")}>"Edit"</A> <A href={format!("{article_link}/edit")}>"Edit"</A>
</Show> </Show>
</nav> </nav>
}})} }})}

View file

@ -1,3 +1,6 @@
use crate::common::DbArticle;
use url::Url;
pub mod api; pub mod api;
pub mod app; pub mod app;
mod components; mod components;
@ -7,3 +10,37 @@ pub mod pages;
#[cfg(feature = "hydrate")] #[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen] #[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {} pub fn hydrate() {}
fn extract_hostname(article: &DbArticle) -> String {
let ap_id: Url;
#[cfg(not(feature = "ssr"))]
{
ap_id = article.ap_id.parse().unwrap();
}
#[cfg(feature = "ssr")]
{
ap_id = article.ap_id.inner().clone();
}
let mut port = String::new();
if let Some(port_) = ap_id.port() {
port = format!(":{port_}");
}
format!("{}{port}", ap_id.host_str().unwrap())
}
fn article_link(article: &DbArticle) -> String {
if article.local {
format!("/article/{}", article.title)
} else {
format!("/article/{}@{}", article.title, extract_hostname(article))
}
}
fn article_title(article: &DbArticle) -> String {
let title = article.title.replace('_', " ");
if article.local {
title
} else {
format!("{}@{}", title, extract_hostname(article))
}
}

View file

@ -1,5 +1,6 @@
use crate::common::{ArticleView, EditArticleData}; use crate::common::{ArticleView, EditArticleData};
use crate::frontend::app::GlobalState; use crate::frontend::app::GlobalState;
use crate::frontend::article_title;
use crate::frontend::components::article_nav::ArticleNav; use crate::frontend::components::article_nav::ArticleNav;
use crate::frontend::pages::article_resource; use crate::frontend::pages::article_resource;
use leptos::*; use leptos::*;
@ -61,7 +62,7 @@ pub fn EditArticle() -> impl IntoView {
set_text.set(article.article.text.clone()); set_text.set(article.article.text.clone());
view! { view! {
<div class="item-view"> <div class="item-view">
<h1>{article.article.title()}</h1> <h1>{article_title(&article.article)}</h1>
<textarea on:keyup=move |ev| { <textarea on:keyup=move |ev| {
let val = event_target_value(&ev); let val = event_target_value(&ev);
set_text.update(|p| *p = val); set_text.update(|p| *p = val);

View file

@ -1,3 +1,4 @@
use crate::frontend::article_title;
use crate::frontend::components::article_nav::ArticleNav; use crate::frontend::components::article_nav::ArticleNav;
use crate::frontend::pages::article_resource; use crate::frontend::pages::article_resource;
use leptos::*; use leptos::*;
@ -15,7 +16,7 @@ pub fn ArticleHistory() -> impl IntoView {
move || article.get().map(|article| { move || article.get().map(|article| {
view! { view! {
<div class="item-view"> <div class="item-view">
<h1>{article.article.title()}</h1> <h1>{article_title(&article.article)}</h1>
{ {
article.edits.into_iter().rev().map(|edit| { article.edits.into_iter().rev().map(|edit| {
let path = format!("/article/{}/diff/{}", article.article.title, edit.hash.0); let path = format!("/article/{}/diff/{}", article.article.title, edit.hash.0);

View file

@ -1,5 +1,6 @@
use crate::common::ListArticlesData; use crate::common::ListArticlesData;
use crate::frontend::app::GlobalState; use crate::frontend::app::GlobalState;
use crate::frontend::{article_link, article_title};
use leptos::*; use leptos::*;
use web_sys::wasm_bindgen::JsCast; use web_sys::wasm_bindgen::JsCast;
@ -38,7 +39,7 @@ pub fn ListArticles() -> impl IntoView {
<ul> { <ul> {
move || articles.get().map(|a| move || articles.get().map(|a|
a.into_iter().map(|a| view! { a.into_iter().map(|a| view! {
<li><a href=format!("/article/{}", a.title)>{a.title()}</a></li> <li><a href=article_link(&a)>{article_title(&a)}</a></li>
}).collect::<Vec<_>>()) }).collect::<Vec<_>>())
} </ul> } </ul>
</Suspense> </Suspense>

View file

@ -1,3 +1,4 @@
use crate::frontend::article_title;
use crate::frontend::components::article_nav::ArticleNav; use crate::frontend::components::article_nav::ArticleNav;
use crate::frontend::pages::article_resource; use crate::frontend::pages::article_resource;
use leptos::*; use leptos::*;
@ -17,7 +18,7 @@ pub fn ReadArticle() -> impl IntoView {
move || article.get().map(|article| move || article.get().map(|article|
view! { view! {
<div class="item-view"> <div class="item-view">
<h1>{article.article.title()}</h1> <h1>{article_title(&article.article)}</h1>
<div inner_html={parser.parse(&article.article.text).render()}/> <div inner_html={parser.parse(&article.article.text).render()}/>
</div> </div>
}) })

View file

@ -13,11 +13,16 @@ fn article_resource(
title: impl Fn() -> Option<String> + 'static, title: impl Fn() -> Option<String> + 'static,
) -> Resource<Option<String>, ArticleView> { ) -> Resource<Option<String>, ArticleView> {
create_resource(title, move |title| async move { create_resource(title, move |title| async move {
let title = title.unwrap_or(MAIN_PAGE_NAME.to_string()); let mut title = title.unwrap_or(MAIN_PAGE_NAME.to_string());
let mut domain = None;
if let Some((title_, domain_)) = title.clone().split_once('@') {
title = title_.to_string();
domain = Some(domain_.to_string());
}
GlobalState::api_client() GlobalState::api_client()
.get_article(GetArticleData { .get_article(GetArticleData {
title: Some(title), title: Some(title),
instance_id: None, instance_domain: domain,
id: None, id: None,
}) })
.await .await

View file

@ -1,5 +1,6 @@
use crate::common::{DbArticle, DbInstance, SearchArticleData}; use crate::common::{DbArticle, DbInstance, SearchArticleData};
use crate::frontend::app::GlobalState; use crate::frontend::app::GlobalState;
use crate::frontend::{article_link, article_title};
use futures::join; use futures::join;
use leptos::*; use leptos::*;
use leptos_router::use_query_map; use leptos_router::use_query_map;
@ -63,7 +64,7 @@ pub fn Search() -> impl IntoView {
search_results.articles search_results.articles
.iter() .iter()
.map(|a| view! { <li> .map(|a| view! { <li>
<a href={format!("/article/{}", a.title)}>{a.title()}</a> <a href={article_link(a)}>{article_title(a)}</a>
</li>}) </li>})
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }

View file

@ -1,7 +1,6 @@
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
#[tokio::main] #[tokio::main]
pub async fn main() -> ibis_lib::backend::error::MyResult<()> { pub async fn main() -> ibis_lib::backend::error::MyResult<()> {
use config::Config;
use ibis_lib::backend::config::IbisConfig; use ibis_lib::backend::config::IbisConfig;
use log::LevelFilter; use log::LevelFilter;
@ -16,14 +15,7 @@ pub async fn main() -> ibis_lib::backend::error::MyResult<()> {
.filter_module("ibis", LevelFilter::Info) .filter_module("ibis", LevelFilter::Info)
.init(); .init();
let config = Config::builder() let ibis_config = IbisConfig::read();
.add_source(config::File::with_name("config/config.toml"))
// Cant use _ as separator due to https://github.com/mehcode/config-rs/issues/391
.add_source(config::Environment::with_prefix("IBIS").separator("__"))
.build()
.unwrap();
let ibis_config: IbisConfig = config.try_deserialize().unwrap();
ibis_lib::backend::start(ibis_config).await?; ibis_lib::backend::start(ibis_config).await?;
Ok(()) Ok(())
} }

View file

@ -29,7 +29,7 @@ async fn test_create_read_and_edit_local_article() -> MyResult<()> {
// now article can be read // now article can be read
let get_article_data = GetArticleData { let get_article_data = GetArticleData {
title: Some(create_res.article.title.clone()), title: Some(create_res.article.title.clone()),
instance_id: None, instance_domain: None,
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?;
@ -150,7 +150,7 @@ async fn test_synchronize_articles() -> MyResult<()> {
let mut get_article_data = GetArticleData { let mut get_article_data = GetArticleData {
title: Some(create_res.article.title), title: Some(create_res.article.title),
instance_id: None, instance_domain: None,
id: None, id: None,
}; };
@ -159,7 +159,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_id = Some(instance.id); get_article_data.instance_domain = Some(instance.ap_id.to_string());
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);
@ -189,7 +189,8 @@ 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()),
instance_id: Some(beta_instance.id), // TODO: this is wrong
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?;
@ -243,7 +244,8 @@ 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()),
instance_id: Some(beta_id_on_alpha.id), // TODO: wrong
instance_domain: Some(beta_id_on_alpha.ap_id.to_string()),
id: None, id: None,
}; };
let get_res = data let get_res = data
@ -256,7 +258,8 @@ 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()),
instance_id: Some(beta_id_on_gamma.id), // TODO: wrong
instance_domain: Some(beta_id_on_gamma.ap_id.to_string()),
id: None, id: None,
}; };
let get_res = data let get_res = data
@ -383,7 +386,8 @@ 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()),
instance_id: Some(beta_id_on_alpha.id), // TODO: wrong
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?;