1
0
Fork 0
mirror of https://github.com/Nutomic/ibis.git synced 2025-02-10 16:34:42 +00:00

Proper frontend error handling (#116)

* Rename to BackendError

* wip

* suspense error

* error handlign for read article

* error handling for discussion

* diff/history (with hydration error)

* actions

* edit

* article pages done

* return error from all api methods

* no unwrap in api client

* cleanup

* article 404 page with create article link

* get rid of articles list, rename instances to explore

* no unwrap on instance details

* more unwrap gone

* use bind

* more

* edits page

* instance details

* no more unwrap!! (fixes #16)

* timeout for error popup

* shear
This commit is contained in:
Nutomic 2025-01-31 11:56:10 +00:00 committed by GitHub
parent 494527eccf
commit c330061087
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
88 changed files with 1217 additions and 1116 deletions

View file

@ -27,6 +27,7 @@ panic = "abort"
[lints.clippy] [lints.clippy]
dbg_macro = "deny" dbg_macro = "deny"
unwrap_used = "deny" unwrap_used = "deny"
todo = "deny"
# frontend and shared deps # frontend and shared deps
[dependencies] [dependencies]

View file

@ -9,7 +9,7 @@ use crate::{
}, },
federation::activities::{create_article::CreateArticle, submit_article_update}, federation::activities::{create_article::CreateArticle, submit_article_update},
utils::{ utils::{
error::MyResult, error::BackendResult,
generate_article_version, generate_article_version,
validate::{validate_article_title, validate_not_empty}, validate::{validate_article_title, validate_not_empty},
}, },
@ -52,7 +52,7 @@ pub(in crate::backend::api) async fn create_article(
user: Extension<LocalUserView>, user: Extension<LocalUserView>,
context: Data<IbisContext>, context: Data<IbisContext>,
Form(mut params): Form<CreateArticleParams>, Form(mut params): Form<CreateArticleParams>,
) -> MyResult<Json<DbArticleView>> { ) -> BackendResult<Json<DbArticleView>> {
params.title = validate_article_title(&params.title)?; params.title = validate_article_title(&params.title)?;
validate_not_empty(&params.text)?; validate_not_empty(&params.text)?;
@ -105,7 +105,7 @@ pub(in crate::backend::api) async fn edit_article(
Extension(user): Extension<LocalUserView>, Extension(user): Extension<LocalUserView>,
context: Data<IbisContext>, context: Data<IbisContext>,
Form(mut params): Form<EditArticleParams>, Form(mut params): Form<EditArticleParams>,
) -> MyResult<Json<Option<ApiConflict>>> { ) -> BackendResult<Json<Option<ApiConflict>>> {
validate_not_empty(&params.new_text)?; validate_not_empty(&params.new_text)?;
// resolve conflict if any // resolve conflict if any
if let Some(resolve_conflict_id) = params.resolve_conflict_id { if let Some(resolve_conflict_id) = params.resolve_conflict_id {
@ -169,7 +169,7 @@ pub(in crate::backend::api) async fn edit_article(
pub(in crate::backend::api) async fn get_article( pub(in crate::backend::api) async fn get_article(
Query(query): Query<GetArticleParams>, Query(query): Query<GetArticleParams>,
context: Data<IbisContext>, context: Data<IbisContext>,
) -> MyResult<Json<DbArticleView>> { ) -> BackendResult<Json<DbArticleView>> {
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,
@ -191,7 +191,7 @@ pub(in crate::backend::api) async fn get_article(
pub(in crate::backend::api) async fn list_articles( pub(in crate::backend::api) async fn list_articles(
Query(query): Query<ListArticlesParams>, Query(query): Query<ListArticlesParams>,
context: Data<IbisContext>, context: Data<IbisContext>,
) -> MyResult<Json<Vec<DbArticle>>> { ) -> BackendResult<Json<Vec<DbArticle>>> {
Ok(Json(DbArticle::read_all( Ok(Json(DbArticle::read_all(
query.only_local, query.only_local,
query.instance_id, query.instance_id,
@ -206,7 +206,7 @@ pub(in crate::backend::api) async fn fork_article(
Extension(_user): Extension<LocalUserView>, Extension(_user): Extension<LocalUserView>,
context: Data<IbisContext>, context: Data<IbisContext>,
Form(mut params): Form<ForkArticleParams>, Form(mut params): Form<ForkArticleParams>,
) -> MyResult<Json<DbArticleView>> { ) -> BackendResult<Json<DbArticleView>> {
// TODO: lots of code duplicated from create_article(), can move it into helper // TODO: lots of code duplicated from create_article(), can move it into helper
let original_article = DbArticle::read_view(params.article_id, &context)?; let original_article = DbArticle::read_view(params.article_id, &context)?;
params.new_title = validate_article_title(&params.new_title)?; params.new_title = validate_article_title(&params.new_title)?;
@ -260,7 +260,7 @@ pub(in crate::backend::api) async fn fork_article(
pub(super) async fn resolve_article( pub(super) async fn resolve_article(
Query(query): Query<ResolveObjectParams>, Query(query): Query<ResolveObjectParams>,
context: Data<IbisContext>, context: Data<IbisContext>,
) -> MyResult<Json<DbArticleView>> { ) -> BackendResult<Json<DbArticleView>> {
let article: DbArticle = ObjectId::from(query.id).dereference(&context).await?; let article: DbArticle = ObjectId::from(query.id).dereference(&context).await?;
let instance = DbInstance::read(article.instance_id, &context)?; let instance = DbInstance::read(article.instance_id, &context)?;
let comments = DbComment::read_for_article(article.id, &context)?; let comments = DbComment::read_for_article(article.id, &context)?;
@ -278,7 +278,7 @@ pub(super) async fn resolve_article(
pub(super) async fn search_article( pub(super) async fn search_article(
Query(query): Query<SearchArticleParams>, Query(query): Query<SearchArticleParams>,
context: Data<IbisContext>, context: Data<IbisContext>,
) -> MyResult<Json<Vec<DbArticle>>> { ) -> BackendResult<Json<Vec<DbArticle>>> {
if query.query.is_empty() { if query.query.is_empty() {
return Err(anyhow!("Query is empty").into()); return Err(anyhow!("Query is empty").into());
} }
@ -291,7 +291,7 @@ pub(in crate::backend::api) async fn protect_article(
Extension(user): Extension<LocalUserView>, Extension(user): Extension<LocalUserView>,
context: Data<IbisContext>, context: Data<IbisContext>,
Form(params): Form<ProtectArticleParams>, Form(params): Form<ProtectArticleParams>,
) -> MyResult<Json<DbArticle>> { ) -> BackendResult<Json<DbArticle>> {
check_is_admin(&user)?; check_is_admin(&user)?;
let article = DbArticle::update_protected(params.article_id, params.protected, &context)?; let article = DbArticle::update_protected(params.article_id, params.protected, &context)?;
Ok(Json(article)) Ok(Json(article))
@ -303,7 +303,7 @@ pub async fn approve_article(
Extension(user): Extension<LocalUserView>, Extension(user): Extension<LocalUserView>,
context: Data<IbisContext>, context: Data<IbisContext>,
Form(params): Form<ApproveArticleParams>, Form(params): Form<ApproveArticleParams>,
) -> MyResult<Json<()>> { ) -> BackendResult<Json<()>> {
check_is_admin(&user)?; check_is_admin(&user)?;
if params.approve { if params.approve {
DbArticle::update_approved(params.article_id, true, &context)?; DbArticle::update_approved(params.article_id, true, &context)?;
@ -319,7 +319,7 @@ pub async fn delete_conflict(
Extension(user): Extension<LocalUserView>, Extension(user): Extension<LocalUserView>,
context: Data<IbisContext>, context: Data<IbisContext>,
Form(params): Form<DeleteConflictParams>, Form(params): Form<DeleteConflictParams>,
) -> MyResult<Json<()>> { ) -> BackendResult<Json<()>> {
DbConflict::delete(params.conflict_id, user.person.id, &context)?; DbConflict::delete(params.conflict_id, user.person.id, &context)?;
Ok(Json(())) Ok(Json(()))
} }

View file

@ -10,7 +10,7 @@ use crate::{
undo_delete_comment::UndoDeleteComment, undo_delete_comment::UndoDeleteComment,
}, },
utils::{ utils::{
error::MyResult, error::BackendResult,
validate::{validate_comment_max_depth, validate_not_empty}, validate::{validate_comment_max_depth, validate_not_empty},
}, },
}, },
@ -31,7 +31,7 @@ pub(in crate::backend::api) async fn create_comment(
user: Extension<LocalUserView>, user: Extension<LocalUserView>,
context: Data<IbisContext>, context: Data<IbisContext>,
Form(params): Form<CreateCommentParams>, Form(params): Form<CreateCommentParams>,
) -> MyResult<Json<DbCommentView>> { ) -> BackendResult<Json<DbCommentView>> {
validate_not_empty(&params.content)?; validate_not_empty(&params.content)?;
let mut depth = 0; let mut depth = 0;
if let Some(parent_id) = params.parent_id { if let Some(parent_id) = params.parent_id {
@ -78,7 +78,7 @@ pub(in crate::backend::api) async fn edit_comment(
user: Extension<LocalUserView>, user: Extension<LocalUserView>,
context: Data<IbisContext>, context: Data<IbisContext>,
Form(params): Form<EditCommentParams>, Form(params): Form<EditCommentParams>,
) -> MyResult<Json<DbCommentView>> { ) -> BackendResult<Json<DbCommentView>> {
if let Some(content) = &params.content { if let Some(content) = &params.content {
validate_not_empty(content)?; validate_not_empty(content)?;
} }

View file

@ -3,7 +3,7 @@ use crate::{
backend::{ backend::{
database::{instance::DbInstanceUpdateForm, IbisContext}, database::{instance::DbInstanceUpdateForm, IbisContext},
federation::activities::follow::Follow, federation::activities::follow::Follow,
utils::error::MyResult, utils::error::BackendResult,
}, },
common::{ common::{
instance::{ instance::{
@ -27,7 +27,7 @@ use axum_macros::debug_handler;
pub(in crate::backend::api) async fn get_instance( pub(in crate::backend::api) async fn get_instance(
context: Data<IbisContext>, context: Data<IbisContext>,
Form(params): Form<GetInstanceParams>, Form(params): Form<GetInstanceParams>,
) -> MyResult<Json<InstanceView>> { ) -> BackendResult<Json<InstanceView>> {
let local_instance = DbInstance::read_view(params.id, &context)?; let local_instance = DbInstance::read_view(params.id, &context)?;
Ok(Json(local_instance)) Ok(Json(local_instance))
} }
@ -35,7 +35,7 @@ pub(in crate::backend::api) async fn get_instance(
pub(in crate::backend::api) async fn update_instance( pub(in crate::backend::api) async fn update_instance(
context: Data<IbisContext>, context: Data<IbisContext>,
Form(mut params): Form<UpdateInstanceParams>, Form(mut params): Form<UpdateInstanceParams>,
) -> MyResult<Json<DbInstance>> { ) -> BackendResult<Json<DbInstance>> {
empty_to_none(&mut params.name); empty_to_none(&mut params.name);
empty_to_none(&mut params.topic); empty_to_none(&mut params.topic);
let form = DbInstanceUpdateForm { let form = DbInstanceUpdateForm {
@ -52,7 +52,7 @@ pub(in crate::backend::api) async fn follow_instance(
Extension(user): Extension<LocalUserView>, Extension(user): Extension<LocalUserView>,
context: Data<IbisContext>, context: Data<IbisContext>,
Form(params): Form<FollowInstanceParams>, Form(params): Form<FollowInstanceParams>,
) -> MyResult<Json<SuccessResponse>> { ) -> BackendResult<Json<SuccessResponse>> {
let target = DbInstance::read(params.id, &context)?; let target = DbInstance::read(params.id, &context)?;
let pending = !target.local; let pending = !target.local;
DbInstance::follow(&user.person, &target, pending, &context)?; DbInstance::follow(&user.person, &target, pending, &context)?;
@ -67,7 +67,7 @@ pub(in crate::backend::api) async fn follow_instance(
pub(super) async fn resolve_instance( pub(super) async fn resolve_instance(
Query(params): Query<ResolveObjectParams>, Query(params): Query<ResolveObjectParams>,
context: Data<IbisContext>, context: Data<IbisContext>,
) -> MyResult<Json<DbInstance>> { ) -> BackendResult<Json<DbInstance>> {
let instance: DbInstance = ObjectId::from(params.id).dereference(&context).await?; let instance: DbInstance = ObjectId::from(params.id).dereference(&context).await?;
Ok(Json(instance)) Ok(Json(instance))
} }
@ -75,7 +75,7 @@ pub(super) async fn resolve_instance(
#[debug_handler] #[debug_handler]
pub(in crate::backend::api) async fn list_instances( pub(in crate::backend::api) async fn list_instances(
context: Data<IbisContext>, context: Data<IbisContext>,
) -> MyResult<Json<Vec<DbInstance>>> { ) -> BackendResult<Json<Vec<DbInstance>>> {
let instances = DbInstance::list(false, &context)?; let instances = DbInstance::list(false, &context)?;
Ok(Json(instances)) Ok(Json(instances))
} }

View file

@ -17,7 +17,7 @@ use crate::{
user::{get_user, login_user, logout_user, register_user}, user::{get_user, login_user, logout_user, register_user},
}, },
database::IbisContext, database::IbisContext,
utils::error::MyResult, utils::error::BackendResult,
}, },
common::{ common::{
article::{DbEdit, EditView, GetEditList}, article::{DbEdit, EditView, GetEditList},
@ -75,7 +75,7 @@ pub fn api_routes() -> Router<()> {
.route("/site", get(site_view)) .route("/site", get(site_view))
} }
fn check_is_admin(user: &LocalUserView) -> MyResult<()> { fn check_is_admin(user: &LocalUserView) -> BackendResult<()> {
if !user.local_user.admin { if !user.local_user.admin {
return Err(anyhow!("Only admin can perform this action").into()); return Err(anyhow!("Only admin can perform this action").into());
} }
@ -86,7 +86,7 @@ fn check_is_admin(user: &LocalUserView) -> MyResult<()> {
pub(in crate::backend::api) async fn site_view( pub(in crate::backend::api) async fn site_view(
context: Data<IbisContext>, context: Data<IbisContext>,
user: Option<Extension<LocalUserView>>, user: Option<Extension<LocalUserView>>,
) -> MyResult<Json<SiteView>> { ) -> BackendResult<Json<SiteView>> {
Ok(Json(SiteView { Ok(Json(SiteView {
my_profile: user.map(|u| u.0), my_profile: user.map(|u| u.0),
config: context.config.options.clone(), config: context.config.options.clone(),
@ -99,7 +99,7 @@ pub async fn edit_list(
Query(query): Query<GetEditList>, Query(query): Query<GetEditList>,
user: Option<Extension<LocalUserView>>, user: Option<Extension<LocalUserView>>,
context: Data<IbisContext>, context: Data<IbisContext>,
) -> MyResult<Json<Vec<EditView>>> { ) -> BackendResult<Json<Vec<EditView>>> {
let params = if let Some(article_id) = query.article_id { let params = if let Some(article_id) = query.article_id {
ViewEditParams::ArticleId(article_id) ViewEditParams::ArticleId(article_id)
} else if let Some(person_id) = query.person_id { } else if let Some(person_id) = query.person_id {

View file

@ -3,7 +3,7 @@ use crate::{
backend::{ backend::{
database::{conflict::DbConflict, read_jwt_secret, IbisContext}, database::{conflict::DbConflict, read_jwt_secret, IbisContext},
utils::{ utils::{
error::MyResult, error::BackendResult,
validate::{validate_display_name, validate_user_name}, validate::{validate_display_name, validate_user_name},
}, },
}, },
@ -54,7 +54,7 @@ struct Claims {
pub exp: u64, pub exp: u64,
} }
fn generate_login_token(person: &DbPerson, context: &Data<IbisContext>) -> MyResult<String> { fn generate_login_token(person: &DbPerson, context: &Data<IbisContext>) -> BackendResult<String> {
let hostname = context.domain().to_string(); let hostname = context.domain().to_string();
let claims = Claims { let claims = Claims {
sub: person.username.clone(), sub: person.username.clone(),
@ -69,7 +69,7 @@ fn generate_login_token(person: &DbPerson, context: &Data<IbisContext>) -> MyRes
Ok(jwt) Ok(jwt)
} }
pub async fn validate(jwt: &str, context: &IbisContext) -> MyResult<LocalUserView> { pub async fn validate(jwt: &str, context: &IbisContext) -> BackendResult<LocalUserView> {
let validation = Validation::default(); let validation = Validation::default();
let secret = read_jwt_secret(context)?; let secret = read_jwt_secret(context)?;
let key = DecodingKey::from_secret(secret.as_bytes()); let key = DecodingKey::from_secret(secret.as_bytes());
@ -82,7 +82,7 @@ pub(in crate::backend::api) async fn register_user(
context: Data<IbisContext>, context: Data<IbisContext>,
jar: CookieJar, jar: CookieJar,
Form(params): Form<RegisterUserParams>, Form(params): Form<RegisterUserParams>,
) -> MyResult<(CookieJar, Json<LocalUserView>)> { ) -> BackendResult<(CookieJar, Json<LocalUserView>)> {
if !context.config.options.registration_open { if !context.config.options.registration_open {
return Err(anyhow!("Registration is closed").into()); return Err(anyhow!("Registration is closed").into());
} }
@ -98,7 +98,7 @@ pub(in crate::backend::api) async fn login_user(
context: Data<IbisContext>, context: Data<IbisContext>,
jar: CookieJar, jar: CookieJar,
Form(params): Form<LoginUserParams>, Form(params): Form<LoginUserParams>,
) -> MyResult<(CookieJar, Json<LocalUserView>)> { ) -> BackendResult<(CookieJar, Json<LocalUserView>)> {
let user = DbPerson::read_local_from_name(&params.username, &context)?; let user = DbPerson::read_local_from_name(&params.username, &context)?;
let valid = verify(&params.password, &user.local_user.password_encrypted)?; let valid = verify(&params.password, &user.local_user.password_encrypted)?;
if !valid { if !valid {
@ -133,7 +133,7 @@ fn create_cookie(jwt: String, context: &Data<IbisContext>) -> Cookie<'static> {
pub(in crate::backend::api) async fn logout_user( pub(in crate::backend::api) async fn logout_user(
context: Data<IbisContext>, context: Data<IbisContext>,
jar: CookieJar, jar: CookieJar,
) -> MyResult<(CookieJar, Json<SuccessResponse>)> { ) -> BackendResult<(CookieJar, Json<SuccessResponse>)> {
let jar = jar.remove(create_cookie(String::new(), &context)); let jar = jar.remove(create_cookie(String::new(), &context));
Ok((jar, Json(SuccessResponse::default()))) Ok((jar, Json(SuccessResponse::default())))
} }
@ -142,7 +142,7 @@ pub(in crate::backend::api) async fn logout_user(
pub(in crate::backend::api) async fn get_user( pub(in crate::backend::api) async fn get_user(
params: Query<GetUserParams>, params: Query<GetUserParams>,
context: Data<IbisContext>, context: Data<IbisContext>,
) -> MyResult<Json<DbPerson>> { ) -> BackendResult<Json<DbPerson>> {
Ok(Json(DbPerson::read_from_name( Ok(Json(DbPerson::read_from_name(
&params.name, &params.name,
&params.domain, &params.domain,
@ -154,7 +154,7 @@ pub(in crate::backend::api) async fn get_user(
pub(in crate::backend::api) async fn update_user_profile( pub(in crate::backend::api) async fn update_user_profile(
context: Data<IbisContext>, context: Data<IbisContext>,
Form(mut params): Form<UpdateUserParams>, Form(mut params): Form<UpdateUserParams>,
) -> MyResult<Json<SuccessResponse>> { ) -> BackendResult<Json<SuccessResponse>> {
empty_to_none(&mut params.display_name); empty_to_none(&mut params.display_name);
empty_to_none(&mut params.bio); empty_to_none(&mut params.bio);
validate_display_name(&params.display_name)?; validate_display_name(&params.display_name)?;
@ -166,7 +166,7 @@ pub(in crate::backend::api) async fn update_user_profile(
pub(crate) async fn list_notifications( pub(crate) async fn list_notifications(
Extension(user): Extension<LocalUserView>, Extension(user): Extension<LocalUserView>,
context: Data<IbisContext>, context: Data<IbisContext>,
) -> MyResult<Json<Vec<Notification>>> { ) -> BackendResult<Json<Vec<Notification>>> {
let conflicts = DbConflict::list(&user.person, &context)?; let conflicts = DbConflict::list(&user.person, &context)?;
let conflicts: Vec<_> = try_join_all(conflicts.into_iter().map(|c| { let conflicts: Vec<_> = try_join_all(conflicts.into_iter().map(|c| {
let data = context.reset_request_count(); let data = context.reset_request_count();
@ -194,9 +194,10 @@ pub(crate) async fn list_notifications(
#[debug_handler] #[debug_handler]
pub(crate) async fn count_notifications( pub(crate) async fn count_notifications(
Extension(user): Extension<LocalUserView>, user: Option<Extension<LocalUserView>>,
context: Data<IbisContext>, context: Data<IbisContext>,
) -> MyResult<Json<usize>> { ) -> BackendResult<Json<usize>> {
if let Some(user) = user {
let mut count = 0; let mut count = 0;
let conflicts = DbConflict::list(&user.person, &context)?; let conflicts = DbConflict::list(&user.person, &context)?;
count += conflicts.len(); count += conflicts.len();
@ -206,4 +207,7 @@ pub(crate) async fn count_notifications(
} }
Ok(Json(count)) Ok(Json(count))
} else {
Ok(Json(0))
}
} }

View file

@ -5,7 +5,7 @@ use crate::{
IbisContext, IbisContext,
}, },
federation::objects::edits_collection::DbEditCollection, federation::objects::edits_collection::DbEditCollection,
utils::error::MyResult, utils::error::BackendResult,
}, },
common::{ common::{
article::{DbArticle, DbArticleView, EditVersion}, article::{DbArticle, DbArticleView, EditVersion},
@ -42,18 +42,18 @@ pub struct DbArticleForm {
// TODO: get rid of unnecessary methods // TODO: get rid of unnecessary methods
impl DbArticle { impl DbArticle {
pub fn edits_id(&self) -> MyResult<CollectionId<DbEditCollection>> { pub fn edits_id(&self) -> BackendResult<CollectionId<DbEditCollection>> {
Ok(CollectionId::parse(&format!("{}/edits", self.ap_id))?) Ok(CollectionId::parse(&format!("{}/edits", self.ap_id))?)
} }
pub fn create(form: DbArticleForm, context: &IbisContext) -> MyResult<Self> { pub fn create(form: DbArticleForm, context: &IbisContext) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(insert_into(article::table) Ok(insert_into(article::table)
.values(form) .values(form)
.get_result(conn.deref_mut())?) .get_result(conn.deref_mut())?)
} }
pub fn create_or_update(form: DbArticleForm, context: &IbisContext) -> MyResult<Self> { pub fn create_or_update(form: DbArticleForm, context: &IbisContext) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(insert_into(article::table) Ok(insert_into(article::table)
.values(&form) .values(&form)
@ -63,40 +63,48 @@ impl DbArticle {
.get_result(conn.deref_mut())?) .get_result(conn.deref_mut())?)
} }
pub fn update_text(id: ArticleId, text: &str, context: &IbisContext) -> MyResult<Self> { pub fn update_text(id: ArticleId, text: &str, context: &IbisContext) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(diesel::update(article::dsl::article.find(id)) Ok(diesel::update(article::dsl::article.find(id))
.set(article::dsl::text.eq(text)) .set(article::dsl::text.eq(text))
.get_result::<Self>(conn.deref_mut())?) .get_result::<Self>(conn.deref_mut())?)
} }
pub fn update_protected(id: ArticleId, locked: bool, context: &IbisContext) -> MyResult<Self> { pub fn update_protected(
id: ArticleId,
locked: bool,
context: &IbisContext,
) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(diesel::update(article::dsl::article.find(id)) Ok(diesel::update(article::dsl::article.find(id))
.set(article::dsl::protected.eq(locked)) .set(article::dsl::protected.eq(locked))
.get_result::<Self>(conn.deref_mut())?) .get_result::<Self>(conn.deref_mut())?)
} }
pub fn update_approved(id: ArticleId, approved: bool, context: &IbisContext) -> MyResult<Self> { pub fn update_approved(
id: ArticleId,
approved: bool,
context: &IbisContext,
) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(diesel::update(article::dsl::article.find(id)) Ok(diesel::update(article::dsl::article.find(id))
.set(article::dsl::approved.eq(approved)) .set(article::dsl::approved.eq(approved))
.get_result::<Self>(conn.deref_mut())?) .get_result::<Self>(conn.deref_mut())?)
} }
pub fn delete(id: ArticleId, context: &IbisContext) -> MyResult<Self> { pub fn delete(id: ArticleId, context: &IbisContext) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(diesel::delete(article::dsl::article.find(id)).get_result::<Self>(conn.deref_mut())?) Ok(diesel::delete(article::dsl::article.find(id)).get_result::<Self>(conn.deref_mut())?)
} }
pub fn read(id: ArticleId, context: &IbisContext) -> MyResult<Self> { pub fn read(id: ArticleId, context: &IbisContext) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(article::table Ok(article::table
.find(id) .find(id)
.get_result::<Self>(conn.deref_mut())?) .get_result::<Self>(conn.deref_mut())?)
} }
pub fn read_view(id: ArticleId, context: &IbisContext) -> MyResult<DbArticleView> { pub fn read_view(id: ArticleId, context: &IbisContext) -> BackendResult<DbArticleView> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
let query = article::table let query = article::table
.find(id) .find(id)
@ -117,7 +125,7 @@ impl DbArticle {
title: &str, title: &str,
domain: Option<String>, domain: Option<String>,
context: &IbisContext, context: &IbisContext,
) -> MyResult<DbArticleView> { ) -> BackendResult<DbArticleView> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
let (article, instance): (DbArticle, DbInstance) = { let (article, instance): (DbArticle, DbInstance) = {
let query = article::table let query = article::table
@ -141,7 +149,10 @@ impl DbArticle {
}) })
} }
pub fn read_from_ap_id(ap_id: &ObjectId<DbArticle>, context: &IbisContext) -> MyResult<Self> { pub fn read_from_ap_id(
ap_id: &ObjectId<DbArticle>,
context: &IbisContext,
) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(article::table Ok(article::table
.filter(article::dsl::ap_id.eq(ap_id)) .filter(article::dsl::ap_id.eq(ap_id))
@ -155,7 +166,7 @@ impl DbArticle {
only_local: Option<bool>, only_local: Option<bool>,
instance_id: Option<InstanceId>, instance_id: Option<InstanceId>,
context: &IbisContext, context: &IbisContext,
) -> MyResult<Vec<Self>> { ) -> BackendResult<Vec<Self>> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
let mut query = article::table let mut query = article::table
.inner_join(edit::table) .inner_join(edit::table)
@ -175,7 +186,7 @@ impl DbArticle {
Ok(query.get_results(&mut conn)?) Ok(query.get_results(&mut conn)?)
} }
pub fn search(query: &str, context: &IbisContext) -> MyResult<Vec<Self>> { pub fn search(query: &str, context: &IbisContext) -> BackendResult<Vec<Self>> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
let replaced = query let replaced = query
.replace('%', "\\%") .replace('%', "\\%")
@ -191,7 +202,7 @@ impl DbArticle {
.get_results(conn.deref_mut())?) .get_results(conn.deref_mut())?)
} }
pub fn latest_edit_version(&self, context: &IbisContext) -> MyResult<EditVersion> { pub fn latest_edit_version(&self, context: &IbisContext) -> BackendResult<EditVersion> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
let latest_version: Option<EditVersion> = edit::table let latest_version: Option<EditVersion> = edit::table
.filter(edit::dsl::article_id.eq(self.id)) .filter(edit::dsl::article_id.eq(self.id))
@ -206,7 +217,7 @@ impl DbArticle {
} }
} }
pub fn list_approval_required(context: &IbisContext) -> MyResult<Vec<Self>> { pub fn list_approval_required(context: &IbisContext) -> BackendResult<Vec<Self>> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
let query = article::table let query = article::table
.group_by(article::dsl::id) .group_by(article::dsl::id)

View file

@ -3,7 +3,7 @@ use super::{
IbisContext, IbisContext,
}; };
use crate::{ use crate::{
backend::utils::error::MyResult, backend::utils::error::BackendResult,
common::{ common::{
comment::{DbComment, DbCommentView}, comment::{DbComment, DbCommentView},
newtypes::{ArticleId, CommentId, PersonId}, newtypes::{ArticleId, CommentId, PersonId},
@ -48,7 +48,7 @@ pub struct DbCommentUpdateForm {
} }
impl DbComment { impl DbComment {
pub fn create(form: DbCommentInsertForm, context: &IbisContext) -> MyResult<Self> { pub fn create(form: DbCommentInsertForm, context: &IbisContext) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(insert_into(comment::table) Ok(insert_into(comment::table)
.values(form) .values(form)
@ -59,7 +59,7 @@ impl DbComment {
form: DbCommentUpdateForm, form: DbCommentUpdateForm,
id: CommentId, id: CommentId,
context: &IbisContext, context: &IbisContext,
) -> MyResult<DbCommentView> { ) -> BackendResult<DbCommentView> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
let comment: DbComment = update(comment::table.find(id)) let comment: DbComment = update(comment::table.find(id))
.set(form) .set(form)
@ -68,7 +68,10 @@ impl DbComment {
Ok(DbCommentView { comment, creator }) Ok(DbCommentView { comment, creator })
} }
pub fn create_or_update(form: DbCommentInsertForm, context: &IbisContext) -> MyResult<Self> { pub fn create_or_update(
form: DbCommentInsertForm,
context: &IbisContext,
) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(insert_into(comment::table) Ok(insert_into(comment::table)
.values(&form) .values(&form)
@ -78,14 +81,14 @@ impl DbComment {
.get_result(conn.deref_mut())?) .get_result(conn.deref_mut())?)
} }
pub fn read(id: CommentId, context: &IbisContext) -> MyResult<Self> { pub fn read(id: CommentId, context: &IbisContext) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(comment::table Ok(comment::table
.find(id) .find(id)
.get_result::<Self>(conn.deref_mut())?) .get_result::<Self>(conn.deref_mut())?)
} }
pub fn read_view(id: CommentId, context: &IbisContext) -> MyResult<DbCommentView> { pub fn read_view(id: CommentId, context: &IbisContext) -> BackendResult<DbCommentView> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
let comment = comment::table let comment = comment::table
.find(id) .find(id)
@ -94,7 +97,10 @@ impl DbComment {
Ok(DbCommentView { comment, creator }) Ok(DbCommentView { comment, creator })
} }
pub fn read_from_ap_id(ap_id: &ObjectId<DbComment>, context: &IbisContext) -> MyResult<Self> { pub fn read_from_ap_id(
ap_id: &ObjectId<DbComment>,
context: &IbisContext,
) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(comment::table Ok(comment::table
.filter(comment::dsl::ap_id.eq(ap_id)) .filter(comment::dsl::ap_id.eq(ap_id))
@ -104,7 +110,7 @@ impl DbComment {
pub fn read_for_article( pub fn read_for_article(
article_id: ArticleId, article_id: ArticleId,
context: &IbisContext, context: &IbisContext,
) -> MyResult<Vec<DbCommentView>> { ) -> BackendResult<Vec<DbCommentView>> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
let comments = comment::table let comments = comment::table
.inner_join(person::table) .inner_join(person::table)

View file

@ -5,7 +5,7 @@ use crate::{
IbisContext, IbisContext,
}, },
federation::activities::submit_article_update, federation::activities::submit_article_update,
utils::{error::MyResult, generate_article_version}, utils::{error::BackendResult, generate_article_version},
}, },
common::{ common::{
article::{ApiConflict, DbArticle, DbEdit, EditVersion}, article::{ApiConflict, DbArticle, DbEdit, EditVersion},
@ -57,14 +57,14 @@ pub struct DbConflictForm {
} }
impl DbConflict { impl DbConflict {
pub fn create(form: &DbConflictForm, context: &IbisContext) -> MyResult<Self> { pub fn create(form: &DbConflictForm, context: &IbisContext) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(insert_into(conflict::table) Ok(insert_into(conflict::table)
.values(form) .values(form)
.get_result(conn.deref_mut())?) .get_result(conn.deref_mut())?)
} }
pub fn list(person: &DbPerson, context: &IbisContext) -> MyResult<Vec<Self>> { pub fn list(person: &DbPerson, context: &IbisContext) -> BackendResult<Vec<Self>> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(conflict::table Ok(conflict::table
.filter(conflict::dsl::creator_id.eq(person.id)) .filter(conflict::dsl::creator_id.eq(person.id))
@ -72,7 +72,11 @@ impl DbConflict {
} }
/// Delete merge conflict which was created by specific user /// Delete merge conflict which was created by specific user
pub fn delete(id: ConflictId, creator_id: PersonId, context: &IbisContext) -> MyResult<()> { pub fn delete(
id: ConflictId,
creator_id: PersonId,
context: &IbisContext,
) -> BackendResult<()> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
let conflict: Self = delete( let conflict: Self = delete(
conflict::table conflict::table
@ -92,7 +96,7 @@ impl DbConflict {
pub async fn to_api_conflict( pub async fn to_api_conflict(
&self, &self,
context: &Data<IbisContext>, context: &Data<IbisContext>,
) -> MyResult<Option<ApiConflict>> { ) -> BackendResult<Option<ApiConflict>> {
let article = DbArticle::read_view(self.article_id, context)?; let article = DbArticle::read_view(self.article_id, context)?;
// Make sure to get latest version from origin so that all conflicts can be resolved // Make sure to get latest version from origin so that all conflicts can be resolved
let original_article = article.article.ap_id.dereference_forced(context).await?; let original_article = article.article.ap_id.dereference_forced(context).await?;

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
backend::{ backend::{
database::schema::{article, edit, person}, database::schema::{article, edit, person},
utils::error::MyResult, utils::error::BackendResult,
IbisContext, IbisContext,
}, },
common::{ common::{
@ -47,7 +47,7 @@ impl DbEditForm {
summary: String, summary: String,
previous_version_id: EditVersion, previous_version_id: EditVersion,
pending: bool, pending: bool,
) -> MyResult<Self> { ) -> BackendResult<Self> {
let diff = create_patch(&original_article.text, updated_text); let diff = create_patch(&original_article.text, updated_text);
let version = EditVersion::new(&diff.to_string()); let version = EditVersion::new(&diff.to_string());
let ap_id = Self::generate_ap_id(original_article, &version)?; let ap_id = Self::generate_ap_id(original_article, &version)?;
@ -67,7 +67,7 @@ impl DbEditForm {
pub fn generate_ap_id( pub fn generate_ap_id(
article: &DbArticle, article: &DbArticle,
version: &EditVersion, version: &EditVersion,
) -> MyResult<ObjectId<DbEdit>> { ) -> BackendResult<ObjectId<DbEdit>> {
Ok(ObjectId::parse(&format!( Ok(ObjectId::parse(&format!(
"{}/{}", "{}/{}",
article.ap_id, article.ap_id,
@ -77,7 +77,7 @@ impl DbEditForm {
} }
impl DbEdit { impl DbEdit {
pub fn create(form: &DbEditForm, context: &IbisContext) -> MyResult<Self> { pub fn create(form: &DbEditForm, context: &IbisContext) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(insert_into(edit::table) Ok(insert_into(edit::table)
.values(form) .values(form)
@ -87,21 +87,21 @@ impl DbEdit {
.get_result(conn.deref_mut())?) .get_result(conn.deref_mut())?)
} }
pub fn read(version: &EditVersion, context: &IbisContext) -> MyResult<Self> { pub fn read(version: &EditVersion, context: &IbisContext) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(edit::table Ok(edit::table
.filter(edit::dsl::hash.eq(version)) .filter(edit::dsl::hash.eq(version))
.get_result(conn.deref_mut())?) .get_result(conn.deref_mut())?)
} }
pub fn read_from_ap_id(ap_id: &ObjectId<DbEdit>, context: &IbisContext) -> MyResult<Self> { pub fn read_from_ap_id(ap_id: &ObjectId<DbEdit>, context: &IbisContext) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(edit::table Ok(edit::table
.filter(edit::dsl::ap_id.eq(ap_id)) .filter(edit::dsl::ap_id.eq(ap_id))
.get_result(conn.deref_mut())?) .get_result(conn.deref_mut())?)
} }
pub fn list_for_article(id: ArticleId, context: &IbisContext) -> MyResult<Vec<Self>> { pub fn list_for_article(id: ArticleId, context: &IbisContext) -> BackendResult<Vec<Self>> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(edit::table Ok(edit::table
.filter(edit::article_id.eq(id)) .filter(edit::article_id.eq(id))
@ -113,7 +113,7 @@ impl DbEdit {
params: ViewEditParams, params: ViewEditParams,
user: &Option<LocalUserView>, user: &Option<LocalUserView>,
context: &IbisContext, context: &IbisContext,
) -> MyResult<Vec<EditView>> { ) -> BackendResult<Vec<EditView>> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
let person_id = user.as_ref().map(|u| u.person.id).unwrap_or(PersonId(-1)); let person_id = user.as_ref().map(|u| u.person.id).unwrap_or(PersonId(-1));
let query = edit::table let query = edit::table

View file

@ -8,7 +8,7 @@ use crate::{
articles_collection::DbArticleCollection, articles_collection::DbArticleCollection,
instance_collection::DbInstanceCollection, instance_collection::DbInstanceCollection,
}, },
utils::error::MyResult, utils::error::BackendResult,
}, },
common::{ common::{
instance::{DbInstance, InstanceView}, instance::{DbInstance, InstanceView},
@ -57,7 +57,7 @@ pub struct DbInstanceUpdateForm {
} }
impl DbInstance { impl DbInstance {
pub fn create(form: &DbInstanceForm, context: &IbisContext) -> MyResult<Self> { pub fn create(form: &DbInstanceForm, context: &IbisContext) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(insert_into(instance::table) Ok(insert_into(instance::table)
.values(form) .values(form)
@ -67,12 +67,12 @@ impl DbInstance {
.get_result(conn.deref_mut())?) .get_result(conn.deref_mut())?)
} }
pub fn read(id: InstanceId, context: &IbisContext) -> MyResult<Self> { pub fn read(id: InstanceId, context: &IbisContext) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(instance::table.find(id).get_result(conn.deref_mut())?) Ok(instance::table.find(id).get_result(conn.deref_mut())?)
} }
pub fn update(form: DbInstanceUpdateForm, context: &IbisContext) -> MyResult<Self> { pub fn update(form: DbInstanceUpdateForm, context: &IbisContext) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(update(instance::table) Ok(update(instance::table)
.filter(instance::local) .filter(instance::local)
@ -83,14 +83,14 @@ impl DbInstance {
pub fn read_from_ap_id( pub fn read_from_ap_id(
ap_id: &ObjectId<DbInstance>, ap_id: &ObjectId<DbInstance>,
context: &Data<IbisContext>, context: &Data<IbisContext>,
) -> MyResult<DbInstance> { ) -> BackendResult<DbInstance> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(instance::table Ok(instance::table
.filter(instance::ap_id.eq(ap_id)) .filter(instance::ap_id.eq(ap_id))
.get_result(conn.deref_mut())?) .get_result(conn.deref_mut())?)
} }
pub fn read_local(context: &IbisContext) -> MyResult<Self> { pub fn read_local(context: &IbisContext) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(instance::table Ok(instance::table
.filter(instance::local.eq(true)) .filter(instance::local.eq(true))
@ -100,7 +100,7 @@ impl DbInstance {
pub fn read_view( pub fn read_view(
id: Option<InstanceId>, id: Option<InstanceId>,
context: &Data<IbisContext>, context: &Data<IbisContext>,
) -> MyResult<InstanceView> { ) -> BackendResult<InstanceView> {
let instance = match id { let instance = match id {
Some(id) => DbInstance::read(id, context), Some(id) => DbInstance::read(id, context),
None => DbInstance::read_local(context), None => DbInstance::read_local(context),
@ -118,7 +118,7 @@ impl DbInstance {
instance: &DbInstance, instance: &DbInstance,
pending_: bool, pending_: bool,
context: &Data<IbisContext>, context: &Data<IbisContext>,
) -> MyResult<()> { ) -> BackendResult<()> {
use instance_follow::dsl::{follower_id, instance_id, pending}; use instance_follow::dsl::{follower_id, instance_id, pending};
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
let form = ( let form = (
@ -136,7 +136,7 @@ impl DbInstance {
Ok(()) Ok(())
} }
pub fn read_followers(id_: InstanceId, context: &IbisContext) -> MyResult<Vec<DbPerson>> { pub fn read_followers(id_: InstanceId, context: &IbisContext) -> BackendResult<Vec<DbPerson>> {
use crate::backend::database::schema::person; use crate::backend::database::schema::person;
use instance_follow::dsl::{follower_id, instance_id}; use instance_follow::dsl::{follower_id, instance_id};
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
@ -147,7 +147,7 @@ impl DbInstance {
.get_results(conn.deref_mut())?) .get_results(conn.deref_mut())?)
} }
pub fn list(only_remote: bool, context: &Data<IbisContext>) -> MyResult<Vec<DbInstance>> { pub fn list(only_remote: bool, context: &Data<IbisContext>) -> BackendResult<Vec<DbInstance>> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
let mut query = instance::table.into_boxed(); let mut query = instance::table.into_boxed();
if only_remote { if only_remote {
@ -161,7 +161,7 @@ impl DbInstance {
pub fn read_for_comment( pub fn read_for_comment(
comment_id: CommentId, comment_id: CommentId,
context: &Data<IbisContext>, context: &Data<IbisContext>,
) -> MyResult<DbInstance> { ) -> BackendResult<DbInstance> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(instance::table Ok(instance::table
.inner_join(article::table) .inner_join(article::table)

View file

@ -1,5 +1,5 @@
use super::schema::instance_stats; use super::schema::instance_stats;
use crate::backend::{IbisContext, MyResult}; use crate::backend::{BackendResult, IbisContext};
use diesel::{query_dsl::methods::FindDsl, Queryable, RunQueryDsl, Selectable}; use diesel::{query_dsl::methods::FindDsl, Queryable, RunQueryDsl, Selectable};
use std::ops::DerefMut; use std::ops::DerefMut;
@ -15,7 +15,7 @@ pub struct InstanceStats {
} }
impl InstanceStats { impl InstanceStats {
pub fn read(context: &IbisContext) -> MyResult<Self> { pub fn read(context: &IbisContext) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(instance_stats::table.find(1).get_result(conn.deref_mut())?) Ok(instance_stats::table.find(1).get_result(conn.deref_mut())?)
} }

View file

@ -1,6 +1,6 @@
use crate::backend::{ use crate::backend::{
database::schema::jwt_secret, database::schema::jwt_secret,
utils::{config::IbisConfig, error::MyResult}, utils::{config::IbisConfig, error::BackendResult},
}; };
use diesel::{ use diesel::{
r2d2::{ConnectionManager, Pool}, r2d2::{ConnectionManager, Pool},
@ -27,7 +27,7 @@ pub struct IbisContext {
pub config: IbisConfig, pub config: IbisConfig,
} }
pub fn read_jwt_secret(context: &IbisContext) -> MyResult<String> { pub fn read_jwt_secret(context: &IbisContext) -> BackendResult<String> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(jwt_secret::table Ok(jwt_secret::table
.select(jwt_secret::dsl::secret) .select(jwt_secret::dsl::secret)

View file

@ -4,7 +4,7 @@ use crate::{
schema::{instance, instance_follow, local_user, person}, schema::{instance, instance_follow, local_user, person},
IbisContext, IbisContext,
}, },
utils::{error::MyResult, generate_keypair}, utils::{error::BackendResult, generate_keypair},
}, },
common::{ common::{
instance::DbInstance, instance::DbInstance,
@ -51,7 +51,7 @@ pub struct DbPersonForm {
} }
impl DbPerson { impl DbPerson {
pub fn create(person_form: &DbPersonForm, context: &Data<IbisContext>) -> MyResult<Self> { pub fn create(person_form: &DbPersonForm, context: &Data<IbisContext>) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(insert_into(person::table) Ok(insert_into(person::table)
.values(person_form) .values(person_form)
@ -61,7 +61,7 @@ impl DbPerson {
.get_result::<DbPerson>(conn.deref_mut())?) .get_result::<DbPerson>(conn.deref_mut())?)
} }
pub fn read(id: PersonId, context: &IbisContext) -> MyResult<DbPerson> { pub fn read(id: PersonId, context: &IbisContext) -> BackendResult<DbPerson> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(person::table.find(id).get_result(conn.deref_mut())?) Ok(person::table.find(id).get_result(conn.deref_mut())?)
} }
@ -71,7 +71,7 @@ impl DbPerson {
password: String, password: String,
admin: bool, admin: bool,
context: &IbisContext, context: &IbisContext,
) -> MyResult<LocalUserView> { ) -> BackendResult<LocalUserView> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
let domain = &context.config.federation.domain; let domain = &context.config.federation.domain;
let ap_id = ObjectId::parse(&format!( let ap_id = ObjectId::parse(&format!(
@ -116,7 +116,7 @@ impl DbPerson {
pub fn read_from_ap_id( pub fn read_from_ap_id(
ap_id: &ObjectId<DbPerson>, ap_id: &ObjectId<DbPerson>,
context: &Data<IbisContext>, context: &Data<IbisContext>,
) -> MyResult<DbPerson> { ) -> BackendResult<DbPerson> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(person::table Ok(person::table
.filter(person::dsl::ap_id.eq(ap_id)) .filter(person::dsl::ap_id.eq(ap_id))
@ -127,7 +127,7 @@ impl DbPerson {
username: &str, username: &str,
domain: &Option<String>, domain: &Option<String>,
context: &Data<IbisContext>, context: &Data<IbisContext>,
) -> MyResult<DbPerson> { ) -> BackendResult<DbPerson> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
let mut query = person::table let mut query = person::table
.filter(person::username.eq(username)) .filter(person::username.eq(username))
@ -144,7 +144,10 @@ impl DbPerson {
Ok(query.get_result(conn.deref_mut())?) Ok(query.get_result(conn.deref_mut())?)
} }
pub fn update_profile(params: &UpdateUserParams, context: &Data<IbisContext>) -> MyResult<()> { pub fn update_profile(
params: &UpdateUserParams,
context: &Data<IbisContext>,
) -> BackendResult<()> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
diesel::update(person::table.find(params.person_id)) diesel::update(person::table.find(params.person_id))
.set(( .set((
@ -155,7 +158,10 @@ impl DbPerson {
Ok(()) Ok(())
} }
pub fn read_local_from_name(username: &str, context: &IbisContext) -> MyResult<LocalUserView> { pub fn read_local_from_name(
username: &str,
context: &IbisContext,
) -> BackendResult<LocalUserView> {
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
let (person, local_user) = person::table let (person, local_user) = person::table
.inner_join(local_user::table) .inner_join(local_user::table)
@ -171,7 +177,7 @@ impl DbPerson {
}) })
} }
fn read_following(id_: PersonId, context: &IbisContext) -> MyResult<Vec<DbInstance>> { fn read_following(id_: PersonId, context: &IbisContext) -> BackendResult<Vec<DbInstance>> {
use instance_follow::dsl::{follower_id, instance_id}; use instance_follow::dsl::{follower_id, instance_id};
let mut conn = context.db_pool.get()?; let mut conn = context.db_pool.get()?;
Ok(instance_follow::table Ok(instance_follow::table
@ -182,7 +188,7 @@ impl DbPerson {
} }
/// Ghost user serves as placeholder for deleted accounts /// Ghost user serves as placeholder for deleted accounts
pub fn ghost(context: &Data<IbisContext>) -> MyResult<DbPerson> { pub fn ghost(context: &Data<IbisContext>) -> BackendResult<DbPerson> {
let username = "ghost"; let username = "ghost";
let read = DbPerson::read_from_name(username, &None, context); let read = DbPerson::read_from_name(username, &None, context);
if read.is_ok() { if read.is_ok() {

View file

@ -3,7 +3,7 @@ use crate::{
database::IbisContext, database::IbisContext,
federation::{activities::follow::Follow, send_activity}, federation::{activities::follow::Follow, send_activity},
utils::{ utils::{
error::{Error, MyResult}, error::{BackendError, BackendResult},
generate_activity_id, generate_activity_id,
}, },
}, },
@ -33,7 +33,7 @@ impl Accept {
local_instance: DbInstance, local_instance: DbInstance,
object: Follow, object: Follow,
context: &Data<IbisContext>, context: &Data<IbisContext>,
) -> MyResult<()> { ) -> BackendResult<()> {
let id = generate_activity_id(context)?; let id = generate_activity_id(context)?;
let follower = object.actor.dereference(context).await?; let follower = object.actor.dereference(context).await?;
let accept = Accept { let accept = Accept {
@ -56,7 +56,7 @@ impl Accept {
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActivityHandler for Accept { impl ActivityHandler for Accept {
type DataType = IbisContext; type DataType = IbisContext;
type Error = Error; type Error = BackendError;
fn id(&self) -> &Url { fn id(&self) -> &Url {
&self.id &self.id

View file

@ -3,7 +3,7 @@ use crate::{
database::IbisContext, database::IbisContext,
federation::{routes::AnnouncableActivities, send_activity}, federation::{routes::AnnouncableActivities, send_activity},
utils::{ utils::{
error::{Error, MyResult}, error::{BackendError, BackendResult},
generate_activity_id, generate_activity_id,
}, },
}, },
@ -32,7 +32,10 @@ pub struct AnnounceActivity {
} }
impl AnnounceActivity { impl AnnounceActivity {
pub async fn send(object: AnnouncableActivities, context: &Data<IbisContext>) -> MyResult<()> { pub async fn send(
object: AnnouncableActivities,
context: &Data<IbisContext>,
) -> BackendResult<()> {
let id = generate_activity_id(context)?; let id = generate_activity_id(context)?;
let instance = DbInstance::read_local(context)?; let instance = DbInstance::read_local(context)?;
let announce = AnnounceActivity { let announce = AnnounceActivity {
@ -57,7 +60,7 @@ impl AnnounceActivity {
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActivityHandler for AnnounceActivity { impl ActivityHandler for AnnounceActivity {
type DataType = IbisContext; type DataType = IbisContext;
type Error = Error; type Error = BackendError;
fn id(&self) -> &Url { fn id(&self) -> &Url {
&self.id &self.id
@ -68,12 +71,12 @@ impl ActivityHandler for AnnounceActivity {
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
async fn verify(&self, _context: &Data<Self::DataType>) -> MyResult<()> { async fn verify(&self, _context: &Data<Self::DataType>) -> BackendResult<()> {
Ok(()) Ok(())
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<Self::DataType>) -> MyResult<()> { async fn receive(self, context: &Data<Self::DataType>) -> BackendResult<()> {
self.object.verify(context).await?; self.object.verify(context).await?;
self.object.receive(context).await self.object.receive(context).await
} }

View file

@ -8,7 +8,7 @@ use crate::{
send_activity_to_instance, send_activity_to_instance,
}, },
generate_activity_id, generate_activity_id,
utils::error::{Error, MyResult}, utils::error::{BackendError, BackendResult},
}, },
common::{comment::DbComment, instance::DbInstance, user::DbPerson}, common::{comment::DbComment, instance::DbInstance, user::DbPerson},
}; };
@ -40,7 +40,7 @@ pub struct CreateOrUpdateComment {
} }
impl CreateOrUpdateComment { impl CreateOrUpdateComment {
pub async fn send(comment: &DbComment, context: &Data<IbisContext>) -> MyResult<()> { pub async fn send(comment: &DbComment, context: &Data<IbisContext>) -> BackendResult<()> {
let instance = DbInstance::read_for_comment(comment.id, context)?; let instance = DbInstance::read_for_comment(comment.id, context)?;
let kind = if comment.updated.is_none() { let kind = if comment.updated.is_none() {
@ -67,7 +67,7 @@ impl CreateOrUpdateComment {
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActivityHandler for CreateOrUpdateComment { impl ActivityHandler for CreateOrUpdateComment {
type DataType = IbisContext; type DataType = IbisContext;
type Error = Error; type Error = BackendError;
fn id(&self) -> &Url { fn id(&self) -> &Url {
&self.id &self.id

View file

@ -4,7 +4,7 @@ use crate::{
database::{comment::DbCommentUpdateForm, IbisContext}, database::{comment::DbCommentUpdateForm, IbisContext},
federation::{routes::AnnouncableActivities, send_activity_to_instance}, federation::{routes::AnnouncableActivities, send_activity_to_instance},
utils::{ utils::{
error::{Error, MyResult}, error::{BackendError, BackendResult},
generate_activity_id, generate_activity_id,
}, },
}, },
@ -39,7 +39,7 @@ impl DeleteComment {
creator: &DbPerson, creator: &DbPerson,
instance: &DbInstance, instance: &DbInstance,
context: &Data<IbisContext>, context: &Data<IbisContext>,
) -> MyResult<Self> { ) -> BackendResult<Self> {
let id = generate_activity_id(context)?; let id = generate_activity_id(context)?;
Ok(DeleteComment { Ok(DeleteComment {
actor: creator.ap_id.clone(), actor: creator.ap_id.clone(),
@ -49,7 +49,7 @@ impl DeleteComment {
id, id,
}) })
} }
pub async fn send(comment: &DbComment, context: &Data<IbisContext>) -> MyResult<()> { pub async fn send(comment: &DbComment, context: &Data<IbisContext>) -> BackendResult<()> {
let instance = DbInstance::read_for_comment(comment.id, context)?; let instance = DbInstance::read_for_comment(comment.id, context)?;
let creator = DbPerson::read(comment.creator_id, context)?; let creator = DbPerson::read(comment.creator_id, context)?;
let activity = Self::new(comment, &creator, &instance, context)?; let activity = Self::new(comment, &creator, &instance, context)?;
@ -62,7 +62,7 @@ impl DeleteComment {
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActivityHandler for DeleteComment { impl ActivityHandler for DeleteComment {
type DataType = IbisContext; type DataType = IbisContext;
type Error = Error; type Error = BackendError;
fn id(&self) -> &Url { fn id(&self) -> &Url {
&self.id &self.id

View file

@ -1,4 +1,4 @@
use crate::{backend::utils::error::MyResult, common::instance::DbInstance}; use crate::{backend::utils::error::BackendResult, common::instance::DbInstance};
use activitypub_federation::kinds::public; use activitypub_federation::kinds::public;
use url::Url; use url::Url;
@ -7,7 +7,7 @@ pub mod delete_comment;
pub mod undo_delete_comment; pub mod undo_delete_comment;
/// Parameter is the return value from DbInstance::read_for_comment() for this comment. /// Parameter is the return value from DbInstance::read_for_comment() for this comment.
fn generate_comment_activity_to(instance: &DbInstance) -> MyResult<Vec<Url>> { fn generate_comment_activity_to(instance: &DbInstance) -> BackendResult<Vec<Url>> {
let followers_url = format!("{}/followers", &instance.ap_id); let followers_url = format!("{}/followers", &instance.ap_id);
Ok(vec![public(), followers_url.parse()?]) Ok(vec![public(), followers_url.parse()?])
} }

View file

@ -4,7 +4,7 @@ use crate::{
database::{comment::DbCommentUpdateForm, IbisContext}, database::{comment::DbCommentUpdateForm, IbisContext},
federation::{routes::AnnouncableActivities, send_activity_to_instance}, federation::{routes::AnnouncableActivities, send_activity_to_instance},
utils::{ utils::{
error::{Error, MyResult}, error::{BackendError, BackendResult},
generate_activity_id, generate_activity_id,
}, },
}, },
@ -37,7 +37,7 @@ pub struct UndoDeleteComment {
} }
impl UndoDeleteComment { impl UndoDeleteComment {
pub async fn send(comment: &DbComment, context: &Data<IbisContext>) -> MyResult<()> { pub async fn send(comment: &DbComment, context: &Data<IbisContext>) -> BackendResult<()> {
let instance = DbInstance::read_for_comment(comment.id, context)?; let instance = DbInstance::read_for_comment(comment.id, context)?;
let id = generate_activity_id(context)?; let id = generate_activity_id(context)?;
let creator = DbPerson::read(comment.creator_id, context)?; let creator = DbPerson::read(comment.creator_id, context)?;
@ -58,7 +58,7 @@ impl UndoDeleteComment {
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActivityHandler for UndoDeleteComment { impl ActivityHandler for UndoDeleteComment {
type DataType = IbisContext; type DataType = IbisContext;
type Error = Error; type Error = BackendError;
fn id(&self) -> &Url { fn id(&self) -> &Url {
&self.id &self.id

View file

@ -3,7 +3,7 @@ use crate::{
database::IbisContext, database::IbisContext,
federation::objects::article::ApubArticle, federation::objects::article::ApubArticle,
utils::{ utils::{
error::{Error, MyResult}, error::{BackendError, BackendResult},
generate_activity_id, generate_activity_id,
}, },
}, },
@ -35,7 +35,7 @@ impl CreateArticle {
pub async fn send_to_followers( pub async fn send_to_followers(
article: DbArticle, article: DbArticle,
context: &Data<IbisContext>, context: &Data<IbisContext>,
) -> MyResult<()> { ) -> BackendResult<()> {
let local_instance = DbInstance::read_local(context)?; let local_instance = DbInstance::read_local(context)?;
let object = article.clone().into_json(context).await?; let object = article.clone().into_json(context).await?;
let id = generate_activity_id(context)?; let id = generate_activity_id(context)?;
@ -56,7 +56,7 @@ impl CreateArticle {
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActivityHandler for CreateArticle { impl ActivityHandler for CreateArticle {
type DataType = IbisContext; type DataType = IbisContext;
type Error = Error; type Error = BackendError;
fn id(&self) -> &Url { fn id(&self) -> &Url {
&self.id &self.id

View file

@ -3,7 +3,7 @@ use crate::{
database::IbisContext, database::IbisContext,
federation::{activities::accept::Accept, send_activity}, federation::{activities::accept::Accept, send_activity},
generate_activity_id, generate_activity_id,
utils::error::{Error, MyResult}, utils::error::{BackendError, BackendResult},
}, },
common::{instance::DbInstance, user::DbPerson}, common::{instance::DbInstance, user::DbPerson},
}; };
@ -32,7 +32,7 @@ impl Follow {
actor: DbPerson, actor: DbPerson,
to: &DbInstance, to: &DbInstance,
context: &Data<IbisContext>, context: &Data<IbisContext>,
) -> MyResult<()> { ) -> BackendResult<()> {
let id = generate_activity_id(context)?; let id = generate_activity_id(context)?;
let follow = Follow { let follow = Follow {
actor: actor.ap_id.clone(), actor: actor.ap_id.clone(),
@ -48,7 +48,7 @@ impl Follow {
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActivityHandler for Follow { impl ActivityHandler for Follow {
type DataType = IbisContext; type DataType = IbisContext;
type Error = Error; type Error = BackendError;
fn id(&self) -> &Url { fn id(&self) -> &Url {
&self.id &self.id

View file

@ -5,7 +5,7 @@ use crate::{
update_local_article::UpdateLocalArticle, update_local_article::UpdateLocalArticle,
update_remote_article::UpdateRemoteArticle, update_remote_article::UpdateRemoteArticle,
}, },
utils::error::Error, utils::error::BackendError,
}, },
common::{ common::{
article::{DbArticle, DbEdit, EditVersion}, article::{DbArticle, DbEdit, EditVersion},
@ -31,7 +31,7 @@ pub async fn submit_article_update(
original_article: &DbArticle, original_article: &DbArticle,
creator_id: PersonId, creator_id: PersonId,
context: &Data<IbisContext>, context: &Data<IbisContext>,
) -> Result<(), Error> { ) -> Result<(), BackendError> {
let mut form = DbEditForm::new( let mut form = DbEditForm::new(
original_article, original_article,
creator_id, creator_id,

View file

@ -6,7 +6,7 @@ use crate::{
}, },
federation::{objects::edit::ApubEdit, send_activity}, federation::{objects::edit::ApubEdit, send_activity},
utils::{ utils::{
error::{Error, MyResult}, error::{BackendError, BackendResult},
generate_activity_id, generate_activity_id,
}, },
}, },
@ -39,7 +39,7 @@ impl RejectEdit {
edit: ApubEdit, edit: ApubEdit,
user_instance: DbInstance, user_instance: DbInstance,
context: &Data<IbisContext>, context: &Data<IbisContext>,
) -> MyResult<()> { ) -> BackendResult<()> {
let local_instance = DbInstance::read_local(context)?; let local_instance = DbInstance::read_local(context)?;
let id = generate_activity_id(context)?; let id = generate_activity_id(context)?;
let reject = RejectEdit { let reject = RejectEdit {
@ -63,7 +63,7 @@ impl RejectEdit {
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActivityHandler for RejectEdit { impl ActivityHandler for RejectEdit {
type DataType = IbisContext; type DataType = IbisContext;
type Error = Error; type Error = BackendError;
fn id(&self) -> &Url { fn id(&self) -> &Url {
&self.id &self.id

View file

@ -3,7 +3,7 @@ use crate::{
database::IbisContext, database::IbisContext,
federation::objects::article::ApubArticle, federation::objects::article::ApubArticle,
utils::{ utils::{
error::{Error, MyResult}, error::{BackendError, BackendResult},
generate_activity_id, generate_activity_id,
}, },
}, },
@ -37,7 +37,7 @@ impl UpdateLocalArticle {
article: DbArticle, article: DbArticle,
extra_recipients: Vec<DbInstance>, extra_recipients: Vec<DbInstance>,
context: &Data<IbisContext>, context: &Data<IbisContext>,
) -> MyResult<()> { ) -> BackendResult<()> {
debug_assert!(article.local); debug_assert!(article.local);
let local_instance = DbInstance::read_local(context)?; let local_instance = DbInstance::read_local(context)?;
let id = generate_activity_id(context)?; let id = generate_activity_id(context)?;
@ -60,7 +60,7 @@ impl UpdateLocalArticle {
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActivityHandler for UpdateLocalArticle { impl ActivityHandler for UpdateLocalArticle {
type DataType = IbisContext; type DataType = IbisContext;
type Error = Error; type Error = BackendError;
fn id(&self) -> &Url { fn id(&self) -> &Url {
&self.id &self.id

View file

@ -7,7 +7,7 @@ use crate::{
send_activity, send_activity,
}, },
utils::{ utils::{
error::{Error, MyResult}, error::{BackendError, BackendResult},
generate_activity_id, generate_activity_id,
}, },
}, },
@ -46,7 +46,7 @@ impl UpdateRemoteArticle {
edit: DbEdit, edit: DbEdit,
article_instance: DbInstance, article_instance: DbInstance,
context: &Data<IbisContext>, context: &Data<IbisContext>,
) -> MyResult<()> { ) -> BackendResult<()> {
let local_instance = DbInstance::read_local(context)?; let local_instance = DbInstance::read_local(context)?;
let id = generate_activity_id(context)?; let id = generate_activity_id(context)?;
let update = UpdateRemoteArticle { let update = UpdateRemoteArticle {
@ -70,7 +70,7 @@ impl UpdateRemoteArticle {
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActivityHandler for UpdateRemoteArticle { impl ActivityHandler for UpdateRemoteArticle {
type DataType = IbisContext; type DataType = IbisContext;
type Error = Error; type Error = BackendError;
fn id(&self) -> &Url { fn id(&self) -> &Url {
&self.id &self.id

View file

@ -1,4 +1,4 @@
use super::utils::error::MyResult; use super::utils::error::BackendResult;
use crate::{ use crate::{
backend::{database::IbisContext, utils::config::IbisConfig}, backend::{database::IbisContext, utils::config::IbisConfig},
common::{instance::DbInstance, user::DbPerson}, common::{instance::DbInstance, user::DbPerson},
@ -41,7 +41,7 @@ pub async fn send_activity_to_instance(
activity: AnnouncableActivities, activity: AnnouncableActivities,
instance: &DbInstance, instance: &DbInstance,
context: &Data<IbisContext>, context: &Data<IbisContext>,
) -> MyResult<()> { ) -> BackendResult<()> {
if instance.local { if instance.local {
AnnounceActivity::send(activity, context).await?; AnnounceActivity::send(activity, context).await?;
} else { } else {

View file

@ -2,7 +2,7 @@ use crate::{
backend::{ backend::{
database::{article::DbArticleForm, IbisContext}, database::{article::DbArticleForm, IbisContext},
federation::objects::edits_collection::DbEditCollection, federation::objects::edits_collection::DbEditCollection,
utils::{error::Error, validate::validate_article_title}, utils::{error::BackendError, validate::validate_article_title},
}, },
common::{ common::{
article::{DbArticle, EditVersion}, article::{DbArticle, EditVersion},
@ -42,7 +42,7 @@ pub struct ApubArticle {
impl Object for DbArticle { impl Object for DbArticle {
type DataType = IbisContext; type DataType = IbisContext;
type Kind = ApubArticle; type Kind = ApubArticle;
type Error = Error; type Error = BackendError;
async fn read_from_id( async fn read_from_id(
object_id: Url, object_id: Url,

View file

@ -2,7 +2,7 @@ use super::{article::ApubArticle, comment::ApubComment};
use crate::{ use crate::{
backend::{ backend::{
database::IbisContext, database::IbisContext,
utils::error::{Error, MyResult}, utils::error::{BackendError, BackendResult},
}, },
common::{article::DbArticle, comment::DbComment}, common::{article::DbArticle, comment::DbComment},
}; };
@ -28,7 +28,7 @@ pub enum ApubArticleOrComment {
impl Object for DbArticleOrComment { impl Object for DbArticleOrComment {
type DataType = IbisContext; type DataType = IbisContext;
type Kind = ApubArticleOrComment; type Kind = ApubArticleOrComment;
type Error = Error; type Error = BackendError;
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> { fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
None None
@ -37,7 +37,7 @@ impl Object for DbArticleOrComment {
async fn read_from_id( async fn read_from_id(
object_id: Url, object_id: Url,
context: &Data<Self::DataType>, context: &Data<Self::DataType>,
) -> MyResult<Option<Self>> { ) -> BackendResult<Option<Self>> {
let post = DbArticle::read_from_id(object_id.clone(), context).await?; let post = DbArticle::read_from_id(object_id.clone(), context).await?;
Ok(match post { Ok(match post {
Some(o) => Some(Self::Article(o)), Some(o) => Some(Self::Article(o)),
@ -47,14 +47,14 @@ impl Object for DbArticleOrComment {
}) })
} }
async fn delete(self, context: &Data<Self::DataType>) -> MyResult<()> { async fn delete(self, context: &Data<Self::DataType>) -> BackendResult<()> {
match self { match self {
Self::Article(p) => p.delete(context).await, Self::Article(p) => p.delete(context).await,
Self::Comment(c) => c.delete(context).await, Self::Comment(c) => c.delete(context).await,
} }
} }
async fn into_json(self, context: &Data<Self::DataType>) -> MyResult<Self::Kind> { async fn into_json(self, context: &Data<Self::DataType>) -> BackendResult<Self::Kind> {
Ok(match self { Ok(match self {
Self::Article(p) => Self::Kind::Article(Box::new(p.into_json(context).await?)), Self::Article(p) => Self::Kind::Article(Box::new(p.into_json(context).await?)),
Self::Comment(c) => Self::Kind::Comment(Box::new(c.into_json(context).await?)), Self::Comment(c) => Self::Kind::Comment(Box::new(c.into_json(context).await?)),
@ -65,14 +65,14 @@ impl Object for DbArticleOrComment {
apub: &Self::Kind, apub: &Self::Kind,
expected_domain: &Url, expected_domain: &Url,
context: &Data<Self::DataType>, context: &Data<Self::DataType>,
) -> MyResult<()> { ) -> BackendResult<()> {
match apub { match apub {
Self::Kind::Article(a) => DbArticle::verify(a, expected_domain, context).await, Self::Kind::Article(a) => DbArticle::verify(a, expected_domain, context).await,
Self::Kind::Comment(a) => DbComment::verify(a, expected_domain, context).await, Self::Kind::Comment(a) => DbComment::verify(a, expected_domain, context).await,
} }
} }
async fn from_json(apub: Self::Kind, context: &Data<Self::DataType>) -> MyResult<Self> { async fn from_json(apub: Self::Kind, context: &Data<Self::DataType>) -> BackendResult<Self> {
Ok(match apub { Ok(match apub {
Self::Kind::Article(p) => Self::Article(DbArticle::from_json(*p, context).await?), Self::Kind::Article(p) => Self::Article(DbArticle::from_json(*p, context).await?),
Self::Kind::Comment(n) => Self::Comment(DbComment::from_json(*n, context).await?), Self::Kind::Comment(n) => Self::Comment(DbComment::from_json(*n, context).await?),

View file

@ -2,7 +2,7 @@ use crate::{
backend::{ backend::{
database::IbisContext, database::IbisContext,
federation::objects::article::ApubArticle, federation::objects::article::ApubArticle,
utils::error::{Error, MyResult}, utils::error::{BackendError, BackendResult},
}, },
common::{article::DbArticle, utils::http_protocol_str}, common::{article::DbArticle, utils::http_protocol_str},
}; };
@ -30,7 +30,7 @@ pub struct ArticleCollection {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct DbArticleCollection(()); pub struct DbArticleCollection(());
pub fn local_articles_url(domain: &str) -> MyResult<CollectionId<DbArticleCollection>> { pub fn local_articles_url(domain: &str) -> BackendResult<CollectionId<DbArticleCollection>> {
Ok(CollectionId::parse(&format!( Ok(CollectionId::parse(&format!(
"{}://{domain}/all_articles", "{}://{domain}/all_articles",
http_protocol_str() http_protocol_str()
@ -42,7 +42,7 @@ impl Collection for DbArticleCollection {
type Owner = (); type Owner = ();
type DataType = IbisContext; type DataType = IbisContext;
type Kind = ArticleCollection; type Kind = ArticleCollection;
type Error = Error; type Error = BackendError;
async fn read_local( async fn read_local(
_owner: &Self::Owner, _owner: &Self::Owner,

View file

@ -2,7 +2,7 @@ use super::article_or_comment::DbArticleOrComment;
use crate::{ use crate::{
backend::{ backend::{
database::{comment::DbCommentInsertForm, IbisContext}, database::{comment::DbCommentInsertForm, IbisContext},
utils::{error::Error, validate::validate_comment_max_depth}, utils::{error::BackendError, validate::validate_comment_max_depth},
}, },
common::{article::DbArticle, comment::DbComment, user::DbPerson}, common::{article::DbArticle, comment::DbComment, user::DbPerson},
}; };
@ -39,7 +39,7 @@ pub struct ApubComment {
impl Object for DbComment { impl Object for DbComment {
type DataType = IbisContext; type DataType = IbisContext;
type Kind = ApubComment; type Kind = ApubComment;
type Error = Error; type Error = BackendError;
async fn read_from_id( async fn read_from_id(
object_id: Url, object_id: Url,

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
backend::{ backend::{
database::{edit::DbEditForm, IbisContext}, database::{edit::DbEditForm, IbisContext},
utils::error::Error, utils::error::BackendError,
}, },
common::{ common::{
article::{DbArticle, DbEdit, EditVersion}, article::{DbArticle, DbEdit, EditVersion},
@ -45,7 +45,7 @@ pub struct ApubEdit {
impl Object for DbEdit { impl Object for DbEdit {
type DataType = IbisContext; type DataType = IbisContext;
type Kind = ApubEdit; type Kind = ApubEdit;
type Error = Error; type Error = BackendError;
async fn read_from_id( async fn read_from_id(
object_id: Url, object_id: Url,

View file

@ -1,5 +1,9 @@
use crate::{ use crate::{
backend::{database::IbisContext, federation::objects::edit::ApubEdit, utils::error::Error}, backend::{
database::IbisContext,
federation::objects::edit::ApubEdit,
utils::error::BackendError,
},
common::article::{DbArticle, DbEdit}, common::article::{DbArticle, DbEdit},
}; };
use activitypub_federation::{ use activitypub_federation::{
@ -30,7 +34,7 @@ impl Collection for DbEditCollection {
type Owner = DbArticle; type Owner = DbArticle;
type DataType = IbisContext; type DataType = IbisContext;
type Kind = ApubEditCollection; type Kind = ApubEditCollection;
type Error = Error; type Error = BackendError;
async fn read_local( async fn read_local(
article: &Self::Owner, article: &Self::Owner,

View file

@ -3,7 +3,7 @@ use crate::{
backend::{ backend::{
database::{instance::DbInstanceForm, IbisContext}, database::{instance::DbInstanceForm, IbisContext},
federation::{objects::articles_collection::DbArticleCollection, send_activity}, federation::{objects::articles_collection::DbArticleCollection, send_activity},
utils::error::{Error, MyResult}, utils::error::{BackendError, BackendResult},
}, },
common::{instance::DbInstance, utils::extract_domain}, common::{instance::DbInstance, utils::extract_domain},
}; };
@ -37,11 +37,11 @@ pub struct ApubInstance {
} }
impl DbInstance { impl DbInstance {
pub fn followers_url(&self) -> MyResult<Url> { pub fn followers_url(&self) -> BackendResult<Url> {
Ok(Url::parse(&format!("{}/followers", self.ap_id.inner()))?) Ok(Url::parse(&format!("{}/followers", self.ap_id.inner()))?)
} }
pub fn follower_ids(&self, context: &Data<IbisContext>) -> MyResult<Vec<Url>> { pub fn follower_ids(&self, context: &Data<IbisContext>) -> BackendResult<Vec<Url>> {
Ok(DbInstance::read_followers(self.id, context)? Ok(DbInstance::read_followers(self.id, context)?
.into_iter() .into_iter()
.map(|f| f.ap_id.into()) .map(|f| f.ap_id.into())
@ -57,7 +57,7 @@ impl DbInstance {
where where
Activity: ActivityHandler + Serialize + Debug + Send + Sync, Activity: ActivityHandler + Serialize + Debug + Send + Sync,
<Activity as ActivityHandler>::Error: From<activitypub_federation::error::Error>, <Activity as ActivityHandler>::Error: From<activitypub_federation::error::Error>,
<Activity as ActivityHandler>::Error: From<Error>, <Activity as ActivityHandler>::Error: From<BackendError>,
{ {
let mut inboxes: Vec<_> = DbInstance::read_followers(self.id, context)? let mut inboxes: Vec<_> = DbInstance::read_followers(self.id, context)?
.iter() .iter()
@ -73,7 +73,7 @@ impl DbInstance {
impl Object for DbInstance { impl Object for DbInstance {
type DataType = IbisContext; type DataType = IbisContext;
type Kind = ApubInstance; type Kind = ApubInstance;
type Error = Error; type Error = BackendError;
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> { fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
Some(self.last_refreshed_at) Some(self.last_refreshed_at)

View file

@ -2,7 +2,7 @@ use super::instance::ApubInstance;
use crate::{ use crate::{
backend::{ backend::{
database::IbisContext, database::IbisContext,
utils::error::{Error, MyResult}, utils::error::{BackendError, BackendResult},
}, },
common::{instance::DbInstance, utils::http_protocol_str}, common::{instance::DbInstance, utils::http_protocol_str},
}; };
@ -30,7 +30,7 @@ pub struct InstanceCollection {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct DbInstanceCollection(()); pub struct DbInstanceCollection(());
pub fn linked_instances_url(domain: &str) -> MyResult<CollectionId<DbInstanceCollection>> { pub fn linked_instances_url(domain: &str) -> BackendResult<CollectionId<DbInstanceCollection>> {
Ok(CollectionId::parse(&format!( Ok(CollectionId::parse(&format!(
"{}://{domain}/linked_instances", "{}://{domain}/linked_instances",
http_protocol_str() http_protocol_str()
@ -42,7 +42,7 @@ impl Collection for DbInstanceCollection {
type Owner = (); type Owner = ();
type DataType = IbisContext; type DataType = IbisContext;
type Kind = InstanceCollection; type Kind = InstanceCollection;
type Error = Error; type Error = BackendError;
async fn read_local( async fn read_local(
_owner: &Self::Owner, _owner: &Self::Owner,

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
backend::{ backend::{
database::{user::DbPersonForm, IbisContext}, database::{user::DbPersonForm, IbisContext},
utils::error::Error, utils::error::BackendError,
}, },
common::user::DbPerson, common::user::DbPerson,
}; };
@ -35,7 +35,7 @@ pub struct ApubUser {
impl Object for DbPerson { impl Object for DbPerson {
type DataType = IbisContext; type DataType = IbisContext;
type Kind = ApubUser; type Kind = ApubUser;
type Error = Error; type Error = BackendError;
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> { fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
Some(self.last_refreshed_at) Some(self.last_refreshed_at)

View file

@ -30,7 +30,7 @@ use crate::{
user::ApubUser, user::ApubUser,
}, },
}, },
utils::error::{Error, MyResult}, utils::error::{BackendError, BackendResult},
}, },
common::{ common::{
article::DbArticle, article::DbArticle,
@ -75,7 +75,7 @@ pub fn federation_routes() -> Router<()> {
#[debug_handler] #[debug_handler]
async fn http_get_instance( async fn http_get_instance(
context: Data<IbisContext>, context: Data<IbisContext>,
) -> MyResult<FederationJson<WithContext<ApubInstance>>> { ) -> BackendResult<FederationJson<WithContext<ApubInstance>>> {
let local_instance = DbInstance::read_local(&context)?; let local_instance = DbInstance::read_local(&context)?;
let json_instance = local_instance.into_json(&context).await?; let json_instance = local_instance.into_json(&context).await?;
Ok(FederationJson(WithContext::new_default(json_instance))) Ok(FederationJson(WithContext::new_default(json_instance)))
@ -85,7 +85,7 @@ async fn http_get_instance(
async fn http_get_person( async fn http_get_person(
Path(name): Path<String>, Path(name): Path<String>,
context: Data<IbisContext>, context: Data<IbisContext>,
) -> MyResult<FederationJson<WithContext<ApubUser>>> { ) -> BackendResult<FederationJson<WithContext<ApubUser>>> {
let person = DbPerson::read_local_from_name(&name, &context)?.person; let person = DbPerson::read_local_from_name(&name, &context)?.person;
let json_person = person.into_json(&context).await?; let json_person = person.into_json(&context).await?;
Ok(FederationJson(WithContext::new_default(json_person))) Ok(FederationJson(WithContext::new_default(json_person)))
@ -94,7 +94,7 @@ async fn http_get_person(
#[debug_handler] #[debug_handler]
async fn http_get_all_articles( async fn http_get_all_articles(
context: Data<IbisContext>, context: Data<IbisContext>,
) -> MyResult<FederationJson<WithContext<ArticleCollection>>> { ) -> BackendResult<FederationJson<WithContext<ArticleCollection>>> {
let collection = DbArticleCollection::read_local(&(), &context).await?; let collection = DbArticleCollection::read_local(&(), &context).await?;
Ok(FederationJson(WithContext::new_default(collection))) Ok(FederationJson(WithContext::new_default(collection)))
} }
@ -102,7 +102,7 @@ async fn http_get_all_articles(
#[debug_handler] #[debug_handler]
async fn http_get_linked_instances( async fn http_get_linked_instances(
context: Data<IbisContext>, context: Data<IbisContext>,
) -> MyResult<FederationJson<WithContext<InstanceCollection>>> { ) -> BackendResult<FederationJson<WithContext<InstanceCollection>>> {
let collection = DbInstanceCollection::read_local(&(), &context).await?; let collection = DbInstanceCollection::read_local(&(), &context).await?;
Ok(FederationJson(WithContext::new_default(collection))) Ok(FederationJson(WithContext::new_default(collection)))
} }
@ -111,7 +111,7 @@ async fn http_get_linked_instances(
async fn http_get_article( async fn http_get_article(
Path(title): Path<String>, Path(title): Path<String>,
context: Data<IbisContext>, context: Data<IbisContext>,
) -> MyResult<FederationJson<WithContext<ApubArticle>>> { ) -> BackendResult<FederationJson<WithContext<ApubArticle>>> {
let article = DbArticle::read_view_title(&title, None, &context)?; let article = DbArticle::read_view_title(&title, None, &context)?;
let json = article.article.into_json(&context).await?; let json = article.article.into_json(&context).await?;
Ok(FederationJson(WithContext::new_default(json))) Ok(FederationJson(WithContext::new_default(json)))
@ -121,7 +121,7 @@ async fn http_get_article(
async fn http_get_article_edits( async fn http_get_article_edits(
Path(title): Path<String>, Path(title): Path<String>,
context: Data<IbisContext>, context: Data<IbisContext>,
) -> MyResult<FederationJson<WithContext<ApubEditCollection>>> { ) -> BackendResult<FederationJson<WithContext<ApubEditCollection>>> {
let article = DbArticle::read_view_title(&title, None, &context)?; let article = DbArticle::read_view_title(&title, None, &context)?;
let json = DbEditCollection::read_local(&article.article, &context).await?; let json = DbEditCollection::read_local(&article.article, &context).await?;
Ok(FederationJson(WithContext::new_default(json))) Ok(FederationJson(WithContext::new_default(json)))
@ -131,7 +131,7 @@ async fn http_get_article_edits(
async fn http_get_comment( async fn http_get_comment(
Path(id): Path<i32>, Path(id): Path<i32>,
context: Data<IbisContext>, context: Data<IbisContext>,
) -> MyResult<FederationJson<WithContext<ApubComment>>> { ) -> BackendResult<FederationJson<WithContext<ApubComment>>> {
let comment = DbComment::read(CommentId(id), &context)?; let comment = DbComment::read(CommentId(id), &context)?;
let json = comment.into_json(&context).await?; let json = comment.into_json(&context).await?;
Ok(FederationJson(WithContext::new_default(json))) Ok(FederationJson(WithContext::new_default(json)))
@ -193,7 +193,7 @@ pub enum PersonOrInstanceType {
impl Object for UserOrInstance { impl Object for UserOrInstance {
type DataType = IbisContext; type DataType = IbisContext;
type Kind = PersonOrInstance; type Kind = PersonOrInstance;
type Error = Error; type Error = BackendError;
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> { fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
Some(match self { Some(match self {
@ -205,7 +205,7 @@ impl Object for UserOrInstance {
async fn read_from_id( async fn read_from_id(
object_id: Url, object_id: Url,
data: &Data<Self::DataType>, data: &Data<Self::DataType>,
) -> Result<Option<Self>, Error> { ) -> Result<Option<Self>, BackendError> {
let person = DbPerson::read_from_id(object_id.clone(), data).await; let person = DbPerson::read_from_id(object_id.clone(), data).await;
Ok(match person { Ok(match person {
Ok(Some(o)) => Some(UserOrInstance::User(o)), Ok(Some(o)) => Some(UserOrInstance::User(o)),
@ -215,14 +215,14 @@ impl Object for UserOrInstance {
}) })
} }
async fn delete(self, data: &Data<Self::DataType>) -> Result<(), Error> { async fn delete(self, data: &Data<Self::DataType>) -> Result<(), BackendError> {
match self { match self {
UserOrInstance::User(p) => p.delete(data).await, UserOrInstance::User(p) => p.delete(data).await,
UserOrInstance::Instance(p) => p.delete(data).await, UserOrInstance::Instance(p) => p.delete(data).await,
} }
} }
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Error> { async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, BackendError> {
unimplemented!() unimplemented!()
} }
@ -230,14 +230,17 @@ impl Object for UserOrInstance {
apub: &Self::Kind, apub: &Self::Kind,
expected_domain: &Url, expected_domain: &Url,
data: &Data<Self::DataType>, data: &Data<Self::DataType>,
) -> Result<(), Error> { ) -> Result<(), BackendError> {
match apub { match apub {
PersonOrInstance::Person(a) => DbPerson::verify(a, expected_domain, data).await, PersonOrInstance::Person(a) => DbPerson::verify(a, expected_domain, data).await,
PersonOrInstance::Instance(a) => DbInstance::verify(a, expected_domain, data).await, PersonOrInstance::Instance(a) => DbInstance::verify(a, expected_domain, data).await,
} }
} }
async fn from_json(apub: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Error> { async fn from_json(
apub: Self::Kind,
data: &Data<Self::DataType>,
) -> Result<Self, BackendError> {
Ok(match apub { Ok(match apub {
PersonOrInstance::Person(p) => { PersonOrInstance::Person(p) => {
UserOrInstance::User(DbPerson::from_json(p, data).await?) UserOrInstance::User(DbPerson::from_json(p, data).await?)

View file

@ -2,7 +2,7 @@ use crate::{
backend::{ backend::{
database::IbisContext, database::IbisContext,
federation::VerifyUrlData, federation::VerifyUrlData,
utils::{config::IbisConfig, error::MyResult, generate_activity_id}, utils::{config::IbisConfig, error::BackendResult, generate_activity_id},
}, },
common::instance::DbInstance, common::instance::DbInstance,
}; };
@ -30,7 +30,7 @@ pub async fn start(
config: IbisConfig, config: IbisConfig,
override_hostname: Option<SocketAddr>, override_hostname: Option<SocketAddr>,
notify_start: Option<oneshot::Sender<()>>, notify_start: Option<oneshot::Sender<()>>,
) -> MyResult<()> { ) -> BackendResult<()> {
let manager = ConnectionManager::<PgConnection>::new(&config.database.connection_url); let manager = ConnectionManager::<PgConnection>::new(&config.database.connection_url);
let db_pool = Pool::builder() let db_pool = Pool::builder()
.max_size(config.database.pool_size) .max_size(config.database.pool_size)

View file

@ -1,4 +1,4 @@
use crate::backend::utils::error::MyResult; use crate::backend::utils::error::BackendResult;
use anyhow::anyhow; use anyhow::anyhow;
use axum::{ use axum::{
body::Body, body::Body,
@ -19,7 +19,7 @@ use tower_http::services::ServeDir;
pub async fn file_and_error_handler( pub async fn file_and_error_handler(
State(options): State<LeptosOptions>, State(options): State<LeptosOptions>,
request: Request<Body>, request: Request<Body>,
) -> MyResult<Response<Body>> { ) -> BackendResult<Response<Body>> {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
// in debug mode serve assets directly from local folder // in debug mode serve assets directly from local folder
Ok(ServeDir::new(options.site_root.as_ref()) Ok(ServeDir::new(options.site_root.as_ref())

View file

@ -1,4 +1,4 @@
use super::{database::IbisContext, utils::error::MyResult}; use super::{database::IbisContext, utils::error::BackendResult};
use crate::{ use crate::{
backend::{api::api_routes, federation::routes::federation_routes}, backend::{api::api_routes, federation::routes::federation_routes},
common::Auth, common::Auth,
@ -35,7 +35,7 @@ pub(super) async fn start_server(
context: FederationConfig<IbisContext>, context: FederationConfig<IbisContext>,
override_hostname: Option<SocketAddr>, override_hostname: Option<SocketAddr>,
notify_start: Option<oneshot::Sender<()>>, notify_start: Option<oneshot::Sender<()>>,
) -> MyResult<()> { ) -> BackendResult<()> {
let leptos_options = get_config_from_str(include_str!("../../../Cargo.toml"))?; let leptos_options = get_config_from_str(include_str!("../../../Cargo.toml"))?;
let mut addr = leptos_options.site_addr; let mut addr = leptos_options.site_addr;
if let Some(override_hostname) = override_hostname { if let Some(override_hostname) = override_hostname {
@ -76,8 +76,10 @@ async fn leptos_routes_handler(
State(leptos_options): State<LeptosOptions>, State(leptos_options): State<LeptosOptions>,
request: Request<Body>, request: Request<Body>,
) -> Response { ) -> Response {
let leptos_options_ = leptos_options.clone();
let handler = leptos_axum::render_app_async_with_context( let handler = leptos_axum::render_app_async_with_context(
move || { move || {
provide_context(leptos_options_.clone());
if let Some(auth) = &auth { if let Some(auth) = &auth {
provide_context(auth.0.clone()); provide_context(auth.0.clone());
} }

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
backend::{ backend::{
database::{instance_stats::InstanceStats, IbisContext}, database::{instance_stats::InstanceStats, IbisContext},
utils::error::MyResult, utils::error::BackendResult,
}, },
common::utils::http_protocol_str, common::utils::http_protocol_str,
}; };
@ -16,7 +16,9 @@ pub fn config() -> Router<()> {
.route("/.well-known/nodeinfo", get(node_info_well_known)) .route("/.well-known/nodeinfo", get(node_info_well_known))
} }
async fn node_info_well_known(context: Data<IbisContext>) -> MyResult<Json<NodeInfoWellKnown>> { async fn node_info_well_known(
context: Data<IbisContext>,
) -> BackendResult<Json<NodeInfoWellKnown>> {
Ok(Json(NodeInfoWellKnown { Ok(Json(NodeInfoWellKnown {
links: vec![NodeInfoWellKnownLinks { links: vec![NodeInfoWellKnownLinks {
rel: Url::parse("http://nodeinfo.diaspora.software/ns/schema/2.1")?, rel: Url::parse("http://nodeinfo.diaspora.software/ns/schema/2.1")?,
@ -29,7 +31,7 @@ async fn node_info_well_known(context: Data<IbisContext>) -> MyResult<Json<NodeI
})) }))
} }
async fn node_info(context: Data<IbisContext>) -> MyResult<Json<NodeInfo>> { async fn node_info(context: Data<IbisContext>) -> BackendResult<Json<NodeInfo>> {
let stats = InstanceStats::read(&context)?; let stats = InstanceStats::read(&context)?;
Ok(Json(NodeInfo { Ok(Json(NodeInfo {
version: "2.1".to_string(), version: "2.1".to_string(),

View file

@ -8,7 +8,7 @@ use crate::{
instance_collection::linked_instances_url, instance_collection::linked_instances_url,
}, },
}, },
utils::{error::Error, generate_keypair}, utils::{error::BackendError, generate_keypair},
}, },
common::{ common::{
article::{DbArticle, EditVersion}, article::{DbArticle, EditVersion},
@ -27,7 +27,7 @@ This main page can only be edited by the admin. Use it as an introduction for ne
and to list interesting articles. and to list interesting articles.
"; ";
pub async fn setup(context: &Data<IbisContext>) -> Result<(), Error> { pub async fn setup(context: &Data<IbisContext>) -> Result<(), BackendError> {
let domain = &context.config.federation.domain; let domain = &context.config.federation.domain;
let ap_id = ObjectId::parse(&format!("{}://{domain}", http_protocol_str()))?; let ap_id = ObjectId::parse(&format!("{}://{domain}", http_protocol_str()))?;
let inbox_url = format!("{}://{domain}/inbox", http_protocol_str()); let inbox_url = format!("{}://{domain}/inbox", http_protocol_str());

View file

@ -1,4 +1,4 @@
use crate::{backend::utils::error::MyResult, common::instance::Options}; use crate::{backend::utils::error::BackendResult, common::instance::Options};
use config::Config; use config::Config;
use doku::Document; use doku::Document;
use serde::Deserialize; use serde::Deserialize;
@ -17,7 +17,7 @@ pub struct IbisConfig {
} }
impl IbisConfig { impl IbisConfig {
pub fn read() -> MyResult<Self> { pub fn read() -> BackendResult<Self> {
let config = Config::builder() let config = Config::builder()
.add_source(config::File::with_name("config.toml")) .add_source(config::File::with_name("config.toml"))
// Cant use _ as separator due to https://github.com/mehcode/config-rs/issues/391 // Cant use _ as separator due to https://github.com/mehcode/config-rs/issues/391

View file

@ -1,27 +1,27 @@
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
pub type MyResult<T> = Result<T, Error>; pub type BackendResult<T> = Result<T, BackendError>;
#[derive(Debug)] #[derive(Debug)]
pub struct Error(pub anyhow::Error); pub struct BackendError(pub anyhow::Error);
impl Display for Error { impl Display for BackendError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f) std::fmt::Display::fmt(&self.0, f)
} }
} }
impl<T> From<T> for Error impl<T> From<T> for BackendError
where where
T: Into<anyhow::Error>, T: Into<anyhow::Error>,
{ {
fn from(t: T) -> Self { fn from(t: T) -> Self {
Error(t.into()) BackendError(t.into())
} }
} }
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
impl axum::response::IntoResponse for Error { impl axum::response::IntoResponse for BackendError {
fn into_response(self) -> axum::response::Response { fn into_response(self) -> axum::response::Response {
( (
axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::http::StatusCode::INTERNAL_SERVER_ERROR,

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
backend::{database::IbisContext, utils::error::MyResult}, backend::{database::IbisContext, utils::error::BackendResult},
common::{ common::{
article::{DbEdit, EditVersion}, article::{DbEdit, EditVersion},
utils, utils,
@ -43,7 +43,7 @@ pub(super) fn generate_activity_id(context: &Data<IbisContext>) -> Result<Url, P
pub(super) fn generate_article_version( pub(super) fn generate_article_version(
edits: &Vec<DbEdit>, edits: &Vec<DbEdit>,
version: &EditVersion, version: &EditVersion,
) -> MyResult<String> { ) -> BackendResult<String> {
let mut generated = String::new(); let mut generated = String::new();
if version == &EditVersion::default() { if version == &EditVersion::default() {
return Ok(generated); return Ok(generated);
@ -60,7 +60,7 @@ pub(super) fn generate_article_version(
/// Use a single static keypair during testing which is signficantly faster than /// Use a single static keypair during testing which is signficantly faster than
/// generating dozens of keys from scratch. /// generating dozens of keys from scratch.
pub fn generate_keypair() -> MyResult<Keypair> { pub fn generate_keypair() -> BackendResult<Keypair> {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
static KEYPAIR: LazyLock<Keypair> = static KEYPAIR: LazyLock<Keypair> =
LazyLock::new(|| generate_actor_keypair().expect("generate keypair")); LazyLock::new(|| generate_actor_keypair().expect("generate keypair"));
@ -81,8 +81,8 @@ mod test {
use chrono::Utc; use chrono::Utc;
use diffy::create_patch; use diffy::create_patch;
fn create_edits() -> MyResult<Vec<DbEdit>> { fn create_edits() -> BackendResult<Vec<DbEdit>> {
let generate_edit = |a, b| -> MyResult<DbEdit> { let generate_edit = |a, b| -> BackendResult<DbEdit> {
let diff = create_patch(a, b).to_string(); let diff = create_patch(a, b).to_string();
Ok(DbEdit { Ok(DbEdit {
id: EditId(0), id: EditId(0),
@ -106,7 +106,7 @@ mod test {
} }
#[test] #[test]
fn test_generate_article_version() -> MyResult<()> { fn test_generate_article_version() -> BackendResult<()> {
let edits = create_edits()?; let edits = create_edits()?;
let generated = generate_article_version(&edits, &edits[1].hash)?; let generated = generate_article_version(&edits, &edits[1].hash)?;
assert_eq!("sda\n", generated); assert_eq!("sda\n", generated);
@ -114,7 +114,7 @@ mod test {
} }
#[test] #[test]
fn test_generate_invalid_version() -> MyResult<()> { fn test_generate_invalid_version() -> BackendResult<()> {
let edits = create_edits()?; let edits = create_edits()?;
let generated = generate_article_version(&edits, &EditVersion::new("invalid")); let generated = generate_article_version(&edits, &EditVersion::new("invalid"));
assert!(generated.is_err()); assert!(generated.is_err());
@ -122,7 +122,7 @@ mod test {
} }
#[test] #[test]
fn test_generate_first_version() -> MyResult<()> { fn test_generate_first_version() -> BackendResult<()> {
let edits = create_edits()?; let edits = create_edits()?;
let generated = generate_article_version(&edits, &EditVersion::default())?; let generated = generate_article_version(&edits, &EditVersion::default())?;
assert_eq!("", generated); assert_eq!("", generated);

View file

@ -1,4 +1,4 @@
use crate::backend::{database::DbPool, utils::error::MyResult}; use crate::backend::{database::DbPool, utils::error::BackendResult};
use clokwerk::{Scheduler, TimeUnits}; use clokwerk::{Scheduler, TimeUnits};
use diesel::{sql_query, RunQueryDsl}; use diesel::{sql_query, RunQueryDsl};
use log::{error, info}; use log::{error, info};
@ -15,7 +15,7 @@ pub fn start(pool: DbPool) {
let _ = scheduler.watch_thread(Duration::from_secs(60)); let _ = scheduler.watch_thread(Duration::from_secs(60));
} }
fn active_counts(pool: &DbPool) -> MyResult<()> { fn active_counts(pool: &DbPool) -> BackendResult<()> {
info!("Updating active user count"); info!("Updating active user count");
let mut conn = pool.get()?; let mut conn = pool.get()?;

View file

@ -1,9 +1,9 @@
use super::error::MyResult; use super::error::BackendResult;
use anyhow::anyhow; use anyhow::anyhow;
use regex::Regex; use regex::Regex;
use std::sync::LazyLock; use std::sync::LazyLock;
pub fn validate_article_title(title: &str) -> MyResult<String> { pub fn validate_article_title(title: &str) -> BackendResult<String> {
#[expect(clippy::expect_used)] #[expect(clippy::expect_used)]
static TITLE_REGEX: LazyLock<Regex> = static TITLE_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9_]{3,100}$").expect("compile regex")); LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9_]{3,100}$").expect("compile regex"));
@ -14,7 +14,7 @@ pub fn validate_article_title(title: &str) -> MyResult<String> {
Ok(title) Ok(title)
} }
pub fn validate_user_name(name: &str) -> MyResult<()> { pub fn validate_user_name(name: &str) -> BackendResult<()> {
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
static VALID_ACTOR_NAME_REGEX: LazyLock<Regex> = static VALID_ACTOR_NAME_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9_]{3,20}$").expect("compile regex")); LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9_]{3,20}$").expect("compile regex"));
@ -26,7 +26,7 @@ pub fn validate_user_name(name: &str) -> MyResult<()> {
} }
} }
pub fn validate_display_name(name: &Option<String>) -> MyResult<()> { pub fn validate_display_name(name: &Option<String>) -> BackendResult<()> {
if let Some(name) = name { if let Some(name) = name {
if name.contains('@') || name.len() < 3 || name.len() > 20 { if name.contains('@') || name.len() < 3 || name.len() > 20 {
return Err(anyhow!("Invalid displayname").into()); return Err(anyhow!("Invalid displayname").into());
@ -35,14 +35,14 @@ pub fn validate_display_name(name: &Option<String>) -> MyResult<()> {
Ok(()) Ok(())
} }
pub fn validate_comment_max_depth(depth: i32) -> MyResult<()> { pub fn validate_comment_max_depth(depth: i32) -> BackendResult<()> {
if depth > 50 { if depth > 50 {
return Err(anyhow!("Max comment depth reached").into()); return Err(anyhow!("Max comment depth reached").into());
} }
Ok(()) Ok(())
} }
pub fn validate_not_empty(text: &str) -> MyResult<()> { pub fn validate_not_empty(text: &str) -> BackendResult<()> {
if text.trim().len() < 2 { if text.trim().len() < 2 {
return Err(anyhow!("Empty text submitted").into()); return Err(anyhow!("Empty text submitted").into());
} }

View file

@ -1,5 +1,6 @@
use super::{result_to_option, ApiClient}; use super::ApiClient;
use crate::common::{ use crate::{
common::{
article::{ article::{
ApiConflict, ApiConflict,
ApproveArticleParams, ApproveArticleParams,
@ -17,9 +18,10 @@ use crate::common::{
}, },
newtypes::{ArticleId, ConflictId}, newtypes::{ArticleId, ConflictId},
ResolveObjectParams, ResolveObjectParams,
},
frontend::utils::errors::FrontendResult,
}; };
use http::Method; use http::Method;
use leptos::prelude::ServerFnError;
use log::error; use log::error;
use url::Url; use url::Url;
@ -27,67 +29,67 @@ impl ApiClient {
pub async fn create_article( pub async fn create_article(
&self, &self,
data: &CreateArticleParams, data: &CreateArticleParams,
) -> Result<DbArticleView, ServerFnError> { ) -> FrontendResult<DbArticleView> {
self.post("/api/v1/article", Some(&data)).await self.post("/api/v1/article", Some(&data)).await
} }
pub async fn get_article(&self, data: GetArticleParams) -> Option<DbArticleView> { pub async fn get_article(&self, data: GetArticleParams) -> FrontendResult<DbArticleView> {
self.get("/api/v1/article", Some(data)).await self.send(Method::GET, "/api/v1/article", Some(data)).await
} }
pub async fn list_articles(&self, data: ListArticlesParams) -> Option<Vec<DbArticle>> { pub async fn list_articles(&self, data: ListArticlesParams) -> FrontendResult<Vec<DbArticle>> {
Some(self.get("/api/v1/article/list", Some(data)).await.unwrap()) self.get("/api/v1/article/list", Some(data)).await
} }
pub async fn edit_article( pub async fn edit_article(
&self, &self,
params: &EditArticleParams, params: &EditArticleParams,
) -> Result<Option<ApiConflict>, ServerFnError> { ) -> FrontendResult<Option<ApiConflict>> {
self.patch("/api/v1/article", Some(&params)).await self.patch("/api/v1/article", Some(&params)).await
} }
pub async fn fork_article( pub async fn fork_article(&self, params: &ForkArticleParams) -> FrontendResult<DbArticleView> {
&self,
params: &ForkArticleParams,
) -> Result<DbArticleView, ServerFnError> {
self.post("/api/v1/article/fork", Some(params)).await self.post("/api/v1/article/fork", Some(params)).await
} }
pub async fn protect_article( pub async fn protect_article(
&self, &self,
params: &ProtectArticleParams, params: &ProtectArticleParams,
) -> Result<DbArticle, ServerFnError> { ) -> FrontendResult<DbArticle> {
self.post("/api/v1/article/protect", Some(params)).await self.post("/api/v1/article/protect", Some(params)).await
} }
pub async fn resolve_article(&self, id: Url) -> Result<DbArticleView, ServerFnError> { pub async fn resolve_article(&self, id: Url) -> FrontendResult<DbArticleView> {
let resolve_object = ResolveObjectParams { id }; let resolve_object = ResolveObjectParams { id };
self.send(Method::GET, "/api/v1/article/resolve", Some(resolve_object)) self.send(Method::GET, "/api/v1/article/resolve", Some(resolve_object))
.await .await
} }
pub async fn get_article_edits(&self, article_id: ArticleId) -> Option<Vec<EditView>> { pub async fn get_article_edits(&self, article_id: ArticleId) -> FrontendResult<Vec<EditView>> {
let data = GetEditList { let data = GetEditList {
article_id: Some(article_id), article_id: Some(article_id),
..Default::default() ..Default::default()
}; };
self.get("/api/v1/edit/list", Some(data)).await self.send(Method::GET, "/api/v1/edit/list", Some(data))
.await
} }
pub async fn approve_article(&self, article_id: ArticleId, approve: bool) -> Option<()> { pub async fn approve_article(
&self,
article_id: ArticleId,
approve: bool,
) -> FrontendResult<()> {
let params = ApproveArticleParams { let params = ApproveArticleParams {
article_id, article_id,
approve, approve,
}; };
result_to_option(self.post("/api/v1/article/approve", Some(&params)).await) self.post("/api/v1/article/approve", Some(&params)).await
} }
pub async fn delete_conflict(&self, conflict_id: ConflictId) -> Option<()> { pub async fn delete_conflict(&self, conflict_id: ConflictId) -> FrontendResult<()> {
let params = DeleteConflictParams { conflict_id }; let params = DeleteConflictParams { conflict_id };
result_to_option(
self.send(Method::DELETE, "/api/v1/conflict", Some(params)) self.send(Method::DELETE, "/api/v1/conflict", Some(params))
.await, .await
)
} }
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@ -108,5 +110,6 @@ impl ApiClient {
id: Some(params.article_id), id: Some(params.article_id),
}) })
.await .await
.ok()
} }
} }

View file

@ -1,19 +1,18 @@
use super::ApiClient; use super::ApiClient;
use crate::common::comment::{CreateCommentParams, DbCommentView, EditCommentParams}; use crate::{
use leptos::prelude::ServerFnError; common::comment::{CreateCommentParams, DbCommentView, EditCommentParams},
frontend::utils::errors::FrontendResult,
};
impl ApiClient { impl ApiClient {
pub async fn create_comment( pub async fn create_comment(
&self, &self,
params: &CreateCommentParams, params: &CreateCommentParams,
) -> Result<DbCommentView, ServerFnError> { ) -> FrontendResult<DbCommentView> {
self.post("/api/v1/comment", Some(&params)).await self.post("/api/v1/comment", Some(&params)).await
} }
pub async fn edit_comment( pub async fn edit_comment(&self, params: &EditCommentParams) -> FrontendResult<DbCommentView> {
&self,
params: &EditCommentParams,
) -> Result<DbCommentView, ServerFnError> {
self.patch("/api/v1/comment", Some(&params)).await self.patch("/api/v1/comment", Some(&params)).await
} }
} }

View file

@ -1,5 +1,6 @@
use super::{result_to_option, ApiClient}; use super::ApiClient;
use crate::common::{ use crate::{
common::{
article::{DbArticle, SearchArticleParams}, article::{DbArticle, SearchArticleParams},
instance::{ instance::{
DbInstance, DbInstance,
@ -12,48 +13,46 @@ use crate::common::{
Notification, Notification,
ResolveObjectParams, ResolveObjectParams,
SuccessResponse, SuccessResponse,
},
frontend::utils::errors::FrontendResult,
}; };
use http::Method; use http::Method;
use leptos::prelude::ServerFnError;
use url::Url; use url::Url;
impl ApiClient { impl ApiClient {
pub async fn get_local_instance(&self) -> Option<InstanceView> { pub async fn get_local_instance(&self) -> FrontendResult<InstanceView> {
self.get("/api/v1/instance", None::<i32>).await self.get("/api/v1/instance", None::<i32>).await
} }
pub async fn get_instance(&self, params: &GetInstanceParams) -> Option<InstanceView> { pub async fn get_instance(&self, params: &GetInstanceParams) -> FrontendResult<InstanceView> {
self.get("/api/v1/instance", Some(&params)).await self.get("/api/v1/instance", Some(&params)).await
} }
pub async fn list_instances(&self) -> Option<Vec<DbInstance>> { pub async fn list_instances(&self) -> FrontendResult<Vec<DbInstance>> {
self.get("/api/v1/instance/list", None::<i32>).await self.get("/api/v1/instance/list", None::<i32>).await
} }
pub async fn update_local_instance( pub async fn update_local_instance(
&self, &self,
params: &UpdateInstanceParams, params: &UpdateInstanceParams,
) -> Result<DbInstance, ServerFnError> { ) -> FrontendResult<DbInstance> {
self.patch("/api/v1/instance", Some(params)).await self.patch("/api/v1/instance", Some(params)).await
} }
pub async fn notifications_list(&self) -> Option<Vec<Notification>> { pub async fn notifications_list(&self) -> FrontendResult<Vec<Notification>> {
self.get("/api/v1/user/notifications/list", None::<()>) self.get("/api/v1/user/notifications/list", None::<()>)
.await .await
} }
pub async fn notifications_count(&self) -> Option<usize> { pub async fn notifications_count(&self) -> FrontendResult<usize> {
self.get("/api/v1/user/notifications/count", None::<()>) self.get("/api/v1/user/notifications/count", None::<()>)
.await .await
} }
pub async fn search( pub async fn search(&self, params: &SearchArticleParams) -> FrontendResult<Vec<DbArticle>> {
&self,
params: &SearchArticleParams,
) -> Result<Vec<DbArticle>, ServerFnError> {
self.send(Method::GET, "/api/v1/search", Some(params)).await self.send(Method::GET, "/api/v1/search", Some(params)).await
} }
pub async fn resolve_instance(&self, id: Url) -> Result<DbInstance, ServerFnError> { pub async fn resolve_instance(&self, id: Url) -> FrontendResult<DbInstance> {
let resolve_object = ResolveObjectParams { id }; let resolve_object = ResolveObjectParams { id };
self.send( self.send(
Method::GET, Method::GET,
@ -63,23 +62,26 @@ impl ApiClient {
.await .await
} }
pub async fn follow_instance(&self, params: FollowInstanceParams) -> Option<SuccessResponse> { pub async fn follow_instance(
result_to_option(self.post("/api/v1/instance/follow", Some(params)).await) &self,
params: FollowInstanceParams,
) -> FrontendResult<SuccessResponse> {
self.post("/api/v1/instance/follow", Some(params)).await
} }
pub async fn site(&self) -> Option<SiteView> { pub async fn site(&self) -> FrontendResult<SiteView> {
self.get("/api/v1/site", None::<()>).await self.get("/api/v1/site", None::<()>).await
} }
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
pub async fn follow_instance_with_resolve(&self, follow_instance: &str) -> Option<DbInstance> { pub async fn follow_instance_with_resolve(
&self,
follow_instance: &str,
) -> FrontendResult<DbInstance> {
use crate::common::{utils::http_protocol_str, ResolveObjectParams}; use crate::common::{utils::http_protocol_str, ResolveObjectParams};
use log::error;
use url::Url; use url::Url;
let params = ResolveObjectParams { let params = ResolveObjectParams {
id: Url::parse(&format!("{}://{}", http_protocol_str(), follow_instance)) id: Url::parse(&format!("{}://{}", http_protocol_str(), follow_instance))?,
.map_err(|e| error!("invalid url {e}"))
.ok()?,
}; };
let instance_resolved: DbInstance = let instance_resolved: DbInstance =
self.get("/api/v1/instance/resolve", Some(params)).await?; self.get("/api/v1/instance/resolve", Some(params)).await?;
@ -89,6 +91,6 @@ impl ApiClient {
id: instance_resolved.id, id: instance_resolved.id,
}; };
self.follow_instance(params).await?; self.follow_instance(params).await?;
Some(instance_resolved) Ok(instance_resolved)
} }
} }

View file

@ -1,6 +1,6 @@
use crate::frontend::utils::errors::{FrontendError, FrontendResult};
use http::{Method, StatusCode}; use http::{Method, StatusCode};
use leptos::{prelude::ServerFnError, server_fn::error::NoCustomError}; use log::info;
use log::{error, info};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{fmt::Debug, sync::LazyLock}; use std::{fmt::Debug, sync::LazyLock};
@ -9,58 +9,45 @@ pub mod comment;
pub mod instance; pub mod instance;
pub mod user; pub mod user;
pub static CLIENT: LazyLock<ApiClient> = LazyLock::new(|| { pub static CLIENT: LazyLock<ApiClient> = LazyLock::new(|| ApiClient::new(None));
#[cfg(feature = "ssr")]
{
ApiClient::new(reqwest::Client::new(), None)
}
#[cfg(not(feature = "ssr"))]
{
ApiClient::new()
}
});
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ApiClient { pub struct ApiClient {
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
client: reqwest::Client, client: reqwest::Client,
pub hostname: String, #[cfg(feature = "ssr")]
ssl: bool, test_hostname: Option<String>,
} }
impl ApiClient { impl ApiClient {
pub fn new(#[allow(unused)] test_hostname: Option<String>) -> Self {
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
pub fn new(client: reqwest::Client, hostname_: Option<String>) -> Self { {
use leptos::config::get_config_from_str; // need cookie store for auth in tests
let leptos_options = get_config_from_str(include_str!("../../../Cargo.toml")).unwrap(); let client = reqwest::ClientBuilder::new()
let mut hostname = leptos_options.site_addr.to_string(); .cookie_store(true)
// required for tests .build()
if let Some(hostname_) = hostname_ { .expect("init reqwest");
hostname = hostname_;
}
Self { Self {
client, client,
hostname, test_hostname,
ssl: false,
} }
} }
#[cfg(not(feature = "ssr"))] #[cfg(not(feature = "ssr"))]
pub fn new() -> Self { {
use leptos_use::use_document; Self {}
let hostname = use_document().location().unwrap().host().unwrap(); }
let ssl = !cfg!(debug_assertions);
Self { hostname, ssl }
} }
async fn get<T, R>(&self, endpoint: &str, query: Option<R>) -> Option<T> async fn get<T, R>(&self, endpoint: &str, query: Option<R>) -> FrontendResult<T>
where where
T: for<'de> Deserialize<'de>, T: for<'de> Deserialize<'de>,
R: Serialize + Debug, R: Serialize + Debug,
{ {
result_to_option(self.send(Method::GET, endpoint, query).await) self.send(Method::GET, endpoint, query).await
} }
async fn post<T, R>(&self, endpoint: &str, query: Option<R>) -> Result<T, ServerFnError> async fn post<T, R>(&self, endpoint: &str, query: Option<R>) -> FrontendResult<T>
where where
T: for<'de> Deserialize<'de>, T: for<'de> Deserialize<'de>,
R: Serialize + Debug, R: Serialize + Debug,
@ -68,7 +55,7 @@ impl ApiClient {
self.send(Method::POST, endpoint, query).await self.send(Method::POST, endpoint, query).await
} }
async fn patch<T, R>(&self, endpoint: &str, query: Option<R>) -> Result<T, ServerFnError> async fn patch<T, R>(&self, endpoint: &str, query: Option<R>) -> FrontendResult<T>
where where
T: for<'de> Deserialize<'de>, T: for<'de> Deserialize<'de>,
R: Serialize + Debug, R: Serialize + Debug,
@ -77,12 +64,7 @@ impl ApiClient {
} }
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
async fn send<P, T>( async fn send<P, T>(&self, method: Method, path: &str, params: Option<P>) -> FrontendResult<T>
&self,
method: Method,
path: &str,
params: Option<P>,
) -> Result<T, ServerFnError>
where where
P: Serialize + Debug, P: Serialize + Debug,
T: for<'de> Deserialize<'de>, T: for<'de> Deserialize<'de>,
@ -90,9 +72,10 @@ impl ApiClient {
use crate::common::{Auth, AUTH_COOKIE}; use crate::common::{Auth, AUTH_COOKIE};
use leptos::prelude::use_context; use leptos::prelude::use_context;
use reqwest::header::HeaderName; use reqwest::header::HeaderName;
let mut req = self let mut req = self
.client .client
.request(method.clone(), self.request_endpoint(path)); .request(method.clone(), self.request_endpoint(path)?);
req = if method == Method::GET { req = if method == Method::GET {
req.query(&params) req.query(&params)
} else { } else {
@ -115,7 +98,7 @@ impl ApiClient {
method: Method, method: Method,
path: &'a str, path: &'a str,
params: Option<P>, params: Option<P>,
) -> impl std::future::Future<Output = Result<T, ServerFnError>> + Send + 'a ) -> impl std::future::Future<Output = FrontendResult<T>> + Send + 'a
where where
P: Serialize + Debug + 'a, P: Serialize + Debug + 'a,
T: for<'de> Deserialize<'de>, T: for<'de> Deserialize<'de>,
@ -136,8 +119,8 @@ impl ApiClient {
} }
}); });
let path_with_endpoint = self.request_endpoint(path); let path_with_endpoint = self.request_endpoint(path)?;
let params_encoded = serde_urlencoded::to_string(&params).unwrap(); let params_encoded = serde_urlencoded::to_string(&params)?;
let path = if method == Method::GET { let path = if method == Method::GET {
// Cannot pass the form data directly but need to convert it manually // Cannot pass the form data directly but need to convert it manually
// https://github.com/rustwasm/gloo/issues/378 // https://github.com/rustwasm/gloo/issues/378
@ -156,8 +139,7 @@ impl ApiClient {
.body(params_encoded) .body(params_encoded)
} else { } else {
builder.build() builder.build()
} }?;
.unwrap();
let res = req.send().await?; let res = req.send().await?;
let status = res.status(); let status = res.status();
let text = res.text().await?; let text = res.text().await?;
@ -165,34 +147,53 @@ impl ApiClient {
}) })
} }
fn response<T>(status: u16, text: String, url: &str) -> Result<T, ServerFnError> fn response<T>(status: u16, text: String, url: &str) -> FrontendResult<T>
where where
T: for<'de> Deserialize<'de>, T: for<'de> Deserialize<'de>,
{ {
let json = serde_json::from_str(&text).map_err(|e| { let json = serde_json::from_str(&text).map_err(|e| {
info!("Failed to deserialize api response: {e} from {text} on {url}"); info!("Failed to deserialize api response: {e} from {text} on {url}");
ServerFnError::<NoCustomError>::Deserialization(text.clone()) FrontendError::new(&text)
})?; })?;
if status == StatusCode::OK { if status == StatusCode::OK {
Ok(json) Ok(json)
} else { } else {
info!("API error: {text} on {url} status {status}"); info!("API error: {text} on {url} status {status}");
Err(ServerFnError::Response(text)) Err(FrontendError::new(text))
} }
} }
fn request_endpoint(&self, path: &str) -> String { fn request_endpoint(&self, path: &str) -> FrontendResult<String> {
let protocol = if self.ssl { "https" } else { "http" }; let protocol = if cfg!(debug_assertions) {
format!("{protocol}://{}{path}", &self.hostname) "http"
} } else {
} "https"
};
fn result_to_option<T>(val: Result<T, ServerFnError>) -> Option<T> { let hostname: String;
match val {
Ok(v) => Some(v), #[cfg(feature = "ssr")]
Err(e) => { {
error!("API error: {e}"); use leptos::{config::LeptosOptions, prelude::use_context};
None hostname = self
.test_hostname
.clone()
.or_else(|| use_context::<LeptosOptions>().map(|o| o.site_addr.to_string()))
// Needed because during tests App() gets initialized from backend
// generate_route_list() which attempts to load some resources without providing
// LeptosOptions. Returning an error results in hydration errors, but an invalid
// host seems fine.
// TODO: maybe can change this to Err after unwraps are all removed
.unwrap_or_else(|| "localhost".to_string());
} }
#[cfg(not(feature = "ssr"))]
{
use leptos::prelude::location;
hostname = location()
.host()
.map_err(|e| FrontendError::new(format!("Failed to get hostname: {:?}", e)))?;
}
Ok(format!("{protocol}://{}{path}", hostname))
} }
} }

View file

@ -1,5 +1,6 @@
use super::{result_to_option, ApiClient}; use super::ApiClient;
use crate::common::{ use crate::{
common::{
article::{EditView, GetEditList}, article::{EditView, GetEditList},
newtypes::PersonId, newtypes::PersonId,
user::{ user::{
@ -11,37 +12,35 @@ use crate::common::{
UpdateUserParams, UpdateUserParams,
}, },
SuccessResponse, SuccessResponse,
},
frontend::utils::errors::FrontendResult,
}; };
use leptos::prelude::ServerFnError;
impl ApiClient { impl ApiClient {
pub async fn register( pub async fn register(&self, params: RegisterUserParams) -> FrontendResult<LocalUserView> {
&self,
params: RegisterUserParams,
) -> Result<LocalUserView, ServerFnError> {
self.post("/api/v1/account/register", Some(&params)).await self.post("/api/v1/account/register", Some(&params)).await
} }
pub async fn login(&self, params: LoginUserParams) -> Result<LocalUserView, ServerFnError> { pub async fn login(&self, params: LoginUserParams) -> FrontendResult<LocalUserView> {
self.post("/api/v1/account/login", Some(&params)).await self.post("/api/v1/account/login", Some(&params)).await
} }
pub async fn logout(&self) -> Option<SuccessResponse> { pub async fn logout(&self) -> FrontendResult<SuccessResponse> {
result_to_option(self.post("/api/v1/account/logout", None::<()>).await) self.post("/api/v1/account/logout", None::<()>).await
} }
pub async fn get_user(&self, data: GetUserParams) -> Option<DbPerson> { pub async fn get_user(&self, data: GetUserParams) -> FrontendResult<DbPerson> {
self.get("/api/v1/user", Some(data)).await self.get("/api/v1/user", Some(data)).await
} }
pub async fn update_user_profile( pub async fn update_user_profile(
&self, &self,
data: UpdateUserParams, data: UpdateUserParams,
) -> Result<SuccessResponse, ServerFnError> { ) -> FrontendResult<SuccessResponse> {
self.post("/api/v1/account/update", Some(data)).await self.post("/api/v1/account/update", Some(data)).await
} }
pub async fn get_person_edits(&self, person_id: PersonId) -> Option<Vec<EditView>> { pub async fn get_person_edits(&self, person_id: PersonId) -> FrontendResult<Vec<EditView>> {
let data = GetEditList { let data = GetEditList {
person_id: Some(person_id), person_id: Some(person_id),
..Default::default() ..Default::default()

View file

@ -9,15 +9,10 @@ use crate::frontend::{
discussion::ArticleDiscussion, discussion::ArticleDiscussion,
edit::EditArticle, edit::EditArticle,
history::ArticleHistory, history::ArticleHistory,
list::ListArticles,
read::ReadArticle, read::ReadArticle,
}, },
instance::{ explore::Explore,
details::InstanceDetails, instance::{details::InstanceDetails, search::Search, settings::InstanceSettings},
list::ListInstances,
search::Search,
settings::InstanceSettings,
},
user::{ user::{
edit_profile::UserEditProfile, edit_profile::UserEditProfile,
login::Login, login::Login,
@ -26,7 +21,7 @@ use crate::frontend::{
register::Register, register::Register,
}, },
}, },
utils::{dark_mode::DarkMode, formatting::instance_title}, utils::{dark_mode::DarkMode, errors::ErrorPopup, formatting::instance_title},
}; };
use leptos::prelude::*; use leptos::prelude::*;
use leptos_meta::{provide_meta_context, *}; use leptos_meta::{provide_meta_context, *};
@ -57,16 +52,16 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
pub fn App() -> impl IntoView { pub fn App() -> impl IntoView {
provide_meta_context(); provide_meta_context();
let site_resource = Resource::new(|| (), |_| async move { CLIENT.site().await.unwrap() }); let site_resource = Resource::new(|| (), |_| async move { CLIENT.site().await });
provide_context(site_resource); provide_context(site_resource);
let instance = Resource::new(|| (), |_| async move { CLIENT.get_local_instance().await });
let darkmode = DarkMode::init(); let darkmode = DarkMode::init();
provide_context(darkmode.clone()); provide_context(darkmode.clone());
let instance = Resource::new( ErrorPopup::init();
|| (),
|_| async move { CLIENT.get_local_instance().await.unwrap() },
);
view! { view! {
<Html attr:data-theme=darkmode.theme {..} class="h-full" /> <Html attr:data-theme=darkmode.theme {..} class="h-full" />
<Body {..} class="h-full max-sm:flex max-sm:flex-col" /> <Body {..} class="h-full max-sm:flex max-sm:flex-col" />
@ -74,37 +69,40 @@ pub fn App() -> impl IntoView {
<Stylesheet id="ibis" href="/pkg/ibis.css" /> <Stylesheet id="ibis" href="/pkg/ibis.css" />
<Stylesheet id="katex" href="/katex.min.css" /> <Stylesheet id="katex" href="/katex.min.css" />
<Router> <Router>
<Nav />
<main class="p-4 md:ml-64">
<Suspense> <Suspense>
{move || { {move || Suspend::new(async move {
instance instance
.get() .await
.map(|i| { .map(|i| {
let formatter = move |text| { let formatter = move |text| {
format!("{text}{}", instance_title(&i.instance)) format!("{text}{}", instance_title(&i.instance))
}; };
view! { <Title formatter /> } view! { <Title formatter /> }
}) })
}} })}
</Suspense> </Suspense>
<Nav /> <Show when=move || ErrorPopup::get().is_some()>
<main class="p-4 md:ml-64"> <div class="toast">
<div class="alert alert-error">
<span>{ErrorPopup::get()}</span>
</div>
</div>
</Show>
<Routes fallback=|| "Page not found.".into_view()> <Routes fallback=|| "Page not found.".into_view()>
<Route path=path!("/") view=ReadArticle /> <Route path=path!("/") view=ReadArticle />
<Route path=path!("/article/:title") view=ReadArticle /> <Route path=path!("/article/:title") view=ReadArticle />
<Route path=path!("/article/:title/discussion") view=ArticleDiscussion /> <Route path=path!("/article/:title/discussion") view=ArticleDiscussion />
<Route path=path!("/article/:title/history") view=ArticleHistory /> <Route path=path!("/article/:title/history") view=ArticleHistory />
<IbisProtectedRoute <IbisProtectedRoute path=path!("/article/:title/edit") view=EditArticle />
path=path!("/article/:title/edit/:conflict_id?")
view=EditArticle
/>
<IbisProtectedRoute <IbisProtectedRoute
path=path!("/article/:title/actions") path=path!("/article/:title/actions")
view=ArticleActions view=ArticleActions
/> />
<Route path=path!("/article/:title/diff/:hash") view=EditDiff /> <Route path=path!("/article/:title/diff/:hash") view=EditDiff />
<IbisProtectedRoute path=path!("/create-article") view=CreateArticle /> <IbisProtectedRoute path=path!("/create-article") view=CreateArticle />
<Route path=path!("/articles") view=ListArticles /> <Route path=path!("/explore") view=Explore />
<Route path=path!("/instances") view=ListInstances />
<Route path=path!("/instance/:hostname") view=InstanceDetails /> <Route path=path!("/instance/:hostname") view=InstanceDetails />
<Route path=path!("/user/:name") view=UserProfile /> <Route path=path!("/user/:name") view=UserProfile />
<Route path=path!("/login") view=Login /> <Route path=path!("/login") view=Login />

View file

@ -1,6 +1,7 @@
use crate::{ use crate::{
common::{article::DbArticleView, validation::can_edit_article}, common::{article::DbArticleView, validation::can_edit_article},
frontend::utils::{ frontend::utils::{
errors::FrontendResult,
formatting::{article_path, article_title}, formatting::{article_path, article_title},
resources::{is_admin, is_logged_in}, resources::{is_admin, is_logged_in},
}, },
@ -9,6 +10,7 @@ use leptos::prelude::*;
use leptos_meta::Title; use leptos_meta::Title;
use leptos_router::components::A; use leptos_router::components::A;
#[derive(Clone, Copy)]
pub enum ActiveTab { pub enum ActiveTab {
Read, Read,
Discussion, Discussion,
@ -18,21 +20,24 @@ pub enum ActiveTab {
} }
#[component] #[component]
pub fn ArticleNav(article: Resource<DbArticleView>, active_tab: ActiveTab) -> impl IntoView { pub fn ArticleNav(
let tab_classes = tab_classes(&active_tab); article: Resource<FrontendResult<DbArticleView>>,
active_tab: ActiveTab,
) -> impl IntoView {
let tab_classes = tab_classes(active_tab);
view! { view! {
<Suspense> <Suspense>
{move || { {move || Suspend::new(async move {
article article
.get() .await
.map(|article_| { .map(|article_| {
let title = article_title(&article_.article); let title = article_title(&article_.article);
let article_link = article_path(&article_.article); let article_link = article_path(&article_.article);
let article_link_ = article_link.clone(); let article_link_ = article_link.clone();
let protected = article_.article.protected; let protected = article_.article.protected;
view! { view! {
<Title text=page_title(&active_tab, &title) /> <Title text=page_title(active_tab, &title) />
<div role="tablist" class="tabs tabs-lifted"> <div role="tablist" class="tabs tabs-lifted">
<A href=article_link.clone() {..} class=tab_classes.read> <A href=article_link.clone() {..} class=tab_classes.read>
"Read" "Read"
@ -90,13 +95,13 @@ pub fn ArticleNav(article: Resource<DbArticleView>, active_tab: ActiveTab) -> im
</div> </div>
} }
}) })
}} })}
</Suspense> </Suspense>
} }
} }
struct ActiveTabClasses { struct ActiveTab2Classes {
read: &'static str, read: &'static str,
discussion: &'static str, discussion: &'static str,
history: &'static str, history: &'static str,
@ -104,10 +109,10 @@ struct ActiveTabClasses {
actions: &'static str, actions: &'static str,
} }
fn tab_classes(active_tab: &ActiveTab) -> ActiveTabClasses { fn tab_classes(active_tab: ActiveTab) -> ActiveTab2Classes {
const TAB_INACTIVE: &str = "tab"; const TAB_INACTIVE: &str = "tab";
const TAB_ACTIVE: &str = "tab tab-active"; const TAB_ACTIVE: &str = "tab tab-active";
let mut classes = ActiveTabClasses { let mut classes = ActiveTab2Classes {
read: TAB_INACTIVE, read: TAB_INACTIVE,
discussion: TAB_INACTIVE, discussion: TAB_INACTIVE,
history: TAB_INACTIVE, history: TAB_INACTIVE,
@ -124,7 +129,7 @@ fn tab_classes(active_tab: &ActiveTab) -> ActiveTabClasses {
classes classes
} }
fn page_title(active_tab: &ActiveTab, article_title: &str) -> String { fn page_title(active_tab: ActiveTab, article_title: &str) -> String {
let active = match active_tab { let active = match active_tab {
ActiveTab::Read => return article_title.to_string(), ActiveTab::Read => return article_title.to_string(),
ActiveTab::Discussion => "Discuss", ActiveTab::Discussion => "Discuss",

View file

@ -9,8 +9,9 @@ use crate::{
components::comment_editor::{CommentEditorView, EditParams}, components::comment_editor::{CommentEditorView, EditParams},
markdown::render_comment_markdown, markdown::render_comment_markdown,
utils::{ utils::{
errors::{FrontendResult, FrontendResultExt},
formatting::{time_ago, user_link}, formatting::{time_ago, user_link},
resources::{site, DefaultResource}, resources::my_profile,
}, },
}, },
}; };
@ -18,7 +19,7 @@ use leptos::prelude::*;
#[component] #[component]
pub fn CommentView( pub fn CommentView(
article: Resource<DbArticleView>, article: Resource<FrontendResult<DbArticleView>>,
comment: DbCommentView, comment: DbCommentView,
show_editor: (ReadSignal<CommentId>, WriteSignal<CommentId>), show_editor: (ReadSignal<CommentId>, WriteSignal<CommentId>),
) -> impl IntoView { ) -> impl IntoView {
@ -36,6 +37,7 @@ pub fn CommentView(
"/article/{}/discussion#{comment_id}", "/article/{}/discussion#{comment_id}",
article article
.get() .get()
.and_then(|a| a.ok())
.map(|a| a.article.title.clone()) .map(|a| a.article.title.clone())
.unwrap_or_default(), .unwrap_or_default(),
); );
@ -46,12 +48,14 @@ pub fn CommentView(
deleted: Some(!comment_change_signal.0.get_untracked().deleted), deleted: Some(!comment_change_signal.0.get_untracked().deleted),
content: None, content: None,
}; };
let comment = CLIENT.edit_comment(&params).await.unwrap(); CLIENT
comment_change_signal.1.set(comment.comment); .edit_comment(&params)
.await
.error_popup(|comment| comment_change_signal.1.set(comment.comment));
}); });
let is_creator = site().with_default(|site| site.my_profile.as_ref().map(|p| p.person.id)) let is_creator =
== Some(comment.comment.creator_id); my_profile().map(|my_profile| my_profile.person.id) == Some(comment.comment.creator_id);
let edit_params = EditParams { let edit_params = EditParams {
comment: comment.comment.clone(), comment: comment.comment.clone(),

View file

@ -4,7 +4,10 @@ use crate::{
comment::{CreateCommentParams, DbComment, EditCommentParams}, comment::{CreateCommentParams, DbComment, EditCommentParams},
newtypes::CommentId, newtypes::CommentId,
}, },
frontend::api::CLIENT, frontend::{
api::CLIENT,
utils::errors::{FrontendResult, FrontendResultExt},
},
}; };
use leptos::{html::Textarea, prelude::*}; use leptos::{html::Textarea, prelude::*};
use leptos_use::{use_textarea_autosize, UseTextareaAutosizeReturn}; use leptos_use::{use_textarea_autosize, UseTextareaAutosizeReturn};
@ -18,7 +21,7 @@ pub struct EditParams {
#[component] #[component]
pub fn CommentEditorView( pub fn CommentEditorView(
article: Resource<DbArticleView>, article: Resource<FrontendResult<DbArticleView>>,
#[prop(optional)] parent_id: Option<CommentId>, #[prop(optional)] parent_id: Option<CommentId>,
/// Set this to CommentId(-1) to hide all editors /// Set this to CommentId(-1) to hide all editors
#[prop(optional)] #[prop(optional)]
@ -47,20 +50,22 @@ pub fn CommentEditorView(
content: Some(content.get_untracked()), content: Some(content.get_untracked()),
deleted: None, deleted: None,
}; };
let comment = CLIENT.edit_comment(&params).await.unwrap(); CLIENT.edit_comment(&params).await.error_popup(|comment| {
edit_params.set_comment.set(comment.comment); edit_params.set_comment.set(comment.comment);
edit_params.set_is_editing.set(false); edit_params.set_is_editing.set(false);
});
} else { } else {
let params = CreateCommentParams { let params = CreateCommentParams {
content: content.get_untracked(), content: content.get_untracked(),
article_id: article.await.article.id, article_id: article.await.map(|a| a.article.id).unwrap_or_default(),
parent_id, parent_id,
}; };
CLIENT.create_comment(&params).await.unwrap(); CLIENT.create_comment(&params).await.error_popup(|_| {
article.refetch(); article.refetch();
if let Some(set_show_editor) = set_show_editor { if let Some(set_show_editor) = set_show_editor {
set_show_editor.set(CommentId(-1)); set_show_editor.set(CommentId(-1));
} }
});
} }
} }
}); });

View file

@ -1,4 +1,4 @@
use crate::frontend::api::CLIENT; use crate::frontend::{api::CLIENT, utils::errors::FrontendResultExt};
use codee::{Decoder, Encoder}; use codee::{Decoder, Encoder};
use leptos::prelude::*; use leptos::prelude::*;
use std::fmt::Debug; use std::fmt::Debug;
@ -17,10 +17,9 @@ where
{ {
let connect_ibis_wiki = Action::new(move |_: &()| async move { let connect_ibis_wiki = Action::new(move |_: &()| async move {
CLIENT CLIENT
.resolve_instance(Url::parse("https://ibis.wiki").unwrap()) .resolve_instance(Url::parse("https://ibis.wiki").expect("parse ibis.wiki url"))
.await .await
.unwrap(); .error_popup(|_| res.refetch());
res.refetch();
}); });
view! { view! {

View file

@ -1,4 +1,4 @@
use leptos::{ev::KeyboardEvent, prelude::*}; use leptos::prelude::*;
#[component] #[component]
pub fn CredentialsForm( pub fn CredentialsForm(
@ -33,15 +33,8 @@ pub fn CredentialsForm(
class="input input-primary input-bordered" class="input input-primary input-bordered"
required required
placeholder="Username" placeholder="Username"
bind:value=(username, set_username)
prop:disabled=move || disabled.get() prop:disabled=move || disabled.get()
on:keyup=move |ev: KeyboardEvent| {
let val = event_target_value(&ev);
set_username.update(|v| *v = val);
}
on:change=move |ev| {
let val = event_target_value(&ev);
set_username.update(|v| *v = val);
}
/> />
<div class="h-2"></div> <div class="h-2"></div>
<input <input
@ -50,21 +43,7 @@ pub fn CredentialsForm(
required required
placeholder="Password" placeholder="Password"
prop:disabled=move || disabled.get() prop:disabled=move || disabled.get()
on:keyup=move |ev: KeyboardEvent| { bind:value=(password, set_password)
match &*ev.key() {
"Enter" => {
dispatch_action();
}
_ => {
let val = event_target_value(&ev);
set_password.update(|p| *p = val);
}
}
}
on:change=move |ev| {
let val = event_target_value(&ev);
set_password.update(|p| *p = val);
}
/> />
<div> <div>

View file

@ -5,7 +5,10 @@ use crate::{
}, },
frontend::{ frontend::{
api::CLIENT, api::CLIENT,
utils::resources::{site, DefaultResource}, utils::{
errors::FrontendResultExt,
resources::{my_profile, site},
},
}, },
}; };
use leptos::prelude::*; use leptos::prelude::*;
@ -16,16 +19,14 @@ pub fn InstanceFollowButton(instance: DbInstance) -> impl IntoView {
let instance_id = *instance_id; let instance_id = *instance_id;
async move { async move {
let params = FollowInstanceParams { id: instance_id }; let params = FollowInstanceParams { id: instance_id };
CLIENT.follow_instance(params).await.unwrap(); CLIENT
site().refetch(); .follow_instance(params)
.await
.error_popup(|_| site().refetch());
} }
}); });
let is_following = site() let is_following = my_profile()
.with_default(|site| { .map(|my_profile| my_profile.following.contains(&instance))
site.clone()
.my_profile
.map(|p| p.following.contains(&instance))
})
.unwrap_or(false); .unwrap_or(false);
let follow_text = if is_following { let follow_text = if is_following {
"Following instance" "Following instance"

View file

@ -8,3 +8,4 @@ pub mod edit_list;
pub mod instance_follow_button; pub mod instance_follow_button;
pub mod nav; pub mod nav;
pub mod protected_route; pub mod protected_route;
pub mod suspense_error;

View file

@ -2,8 +2,9 @@ use crate::frontend::{
api::CLIENT, api::CLIENT,
utils::{ utils::{
dark_mode::DarkMode, dark_mode::DarkMode,
errors::FrontendResultExt,
formatting::instance_title, formatting::instance_title,
resources::{is_admin, is_logged_in, site, DefaultResource}, resources::{config, is_admin, is_logged_in, my_profile, site},
}, },
}; };
use leptos::{component, prelude::*, view, IntoView, *}; use leptos::{component, prelude::*, view, IntoView, *};
@ -12,17 +13,13 @@ use leptos_router::hooks::use_navigate;
#[component] #[component]
pub fn Nav() -> impl IntoView { pub fn Nav() -> impl IntoView {
let logout_action = Action::new(move |_| async move { let logout_action = Action::new(move |_| async move {
CLIENT.logout().await.unwrap(); CLIENT.logout().await.error_popup(|_| site().refetch());
site().refetch();
}); });
let notification_count = Resource::new( let notification_count = Resource::new(
|| (), || (),
move |_| async move { CLIENT.notifications_count().await.unwrap_or_default() }, move |_| async move { CLIENT.notifications_count().await.unwrap_or_default() },
); );
let instance = Resource::new( let instance = Resource::new(|| (), |_| async move { CLIENT.get_local_instance().await });
|| (),
|_| async move { CLIENT.get_local_instance().await.unwrap() },
);
let (search_query, set_search_query) = signal(String::new()); let (search_query, set_search_query) = signal(String::new());
let mut dark_mode = expect_context::<DarkMode>(); let mut dark_mode = expect_context::<DarkMode>();
@ -41,17 +38,16 @@ pub fn Nav() -> impl IntoView {
<img src="/logo.png" class="m-auto max-sm:hidden" /> <img src="/logo.png" class="m-auto max-sm:hidden" />
</a> </a>
<h2 class="m-4 font-serif text-xl font-bold"> <h2 class="m-4 font-serif text-xl font-bold">
{move || { instance.get().map(|i| instance_title(&i.instance)) }} {move || Suspend::new(async move {
instance.await.map(|i| instance_title(&i.instance))
})}
</h2> </h2>
<ul> <ul>
<li> <li>
<a href="/">"Main Page"</a> <a href="/">"Main Page"</a>
</li> </li>
<li> <li>
<a href="/instances">"Instances"</a> <a href="/explore">"Explore"</a>
</li>
<li>
<a href="/articles">"Articles"</a>
</li> </li>
<Show when=is_logged_in> <Show when=is_logged_in>
<li> <li>
@ -109,9 +105,7 @@ pub fn Nav() -> impl IntoView {
<li> <li>
<a href="/login">"Login"</a> <a href="/login">"Login"</a>
</li> </li>
<Show when=move || { <Show when=move || config().registration_open>
site().with_default(|s| s.config.registration_open)
}>
<li> <li>
<a href="/register">"Register"</a> <a href="/register">"Register"</a>
</li> </li>
@ -120,10 +114,12 @@ pub fn Nav() -> impl IntoView {
} }
> >
{ {my_profile()
let my_profile = site() .map(|my_profile| {
.with_default(|site| site.clone().my_profile.unwrap()); let profile_link = format!(
let profile_link = format!("/user/{}", my_profile.person.username); "/user/{}",
my_profile.person.username,
);
view! { view! {
<p class="self-center"> <p class="self-center">
"Logged in as " <a class="link" href=profile_link> "Logged in as " <a class="link" href=profile_link>
@ -142,7 +138,7 @@ pub fn Nav() -> impl IntoView {
Logout Logout
</button> </button>
} }
} })}
</Show> </Show>
<div class="grow min-h-2"></div> <div class="grow min-h-2"></div>

View file

@ -0,0 +1,44 @@
use crate::frontend::{
pages::article_title_param,
utils::{errors::FrontendResult, resources::is_logged_in},
};
use leptos::{either::Either, prelude::*};
#[component]
pub fn SuspenseError<T>(children: ChildrenFn, result: Resource<FrontendResult<T>>) -> impl IntoView
where
T: Clone + Send + Sync + 'static,
{
view! {
<Suspense fallback=|| {
view! { "Loading..." }
}>
{move || {
if let Some(Err(e)) = result.get() {
let article_title = article_title_param();
let href = format!(
"/create-article?title={}",
article_title.clone().unwrap_or_default(),
);
Either::Left(
view! {
<div class="grid place-items-center h-screen">
<div>
<div class="alert alert-error w-fit">{e.message()}</div>
<Show when=move || article_title.is_some() && is_logged_in()>
<a class="mt-4 btn" href=href.clone()>
Create Article
</a>
</Show>
</div>
</div>
},
)
} else {
Either::Right(children())
}
}}
</Suspense>
}
}

View file

@ -1,5 +1,3 @@
#![deny(clippy::unwrap_used)]
use article_link::ArticleLinkScanner; use article_link::ArticleLinkScanner;
use markdown_it::{ use markdown_it::{
plugins::cmark::block::{heading::ATXHeading, lheading::SetextHeader}, plugins::cmark::block::{heading::ATXHeading, lheading::SetextHeader},

View file

@ -5,7 +5,10 @@ use crate::{
}, },
frontend::{ frontend::{
api::CLIENT, api::CLIENT,
components::article_nav::{ActiveTab, ArticleNav}, components::{
article_nav::{ActiveTab, ArticleNav},
suspense_error::SuspenseError,
},
pages::article_resource, pages::article_resource,
utils::{formatting::article_path, resources::is_admin}, utils::{formatting::article_path, resources::is_admin},
DbArticle, DbArticle,
@ -54,12 +57,10 @@ pub fn ArticleActions() -> impl IntoView {
}); });
view! { view! {
<ArticleNav article=article active_tab=ActiveTab::Actions /> <ArticleNav article=article active_tab=ActiveTab::Actions />
<Suspense fallback=|| { <SuspenseError result=article>
view! { "Loading..." } {move || Suspend::new(async move {
}>
{move || {
article article
.get() .await
.map(|article| { .map(|article| {
view! { view! {
<div> <div>
@ -108,12 +109,9 @@ pub fn ArticleActions() -> impl IntoView {
</div> </div>
} }
}) })
}} })}
{fork_response.get().map(|article| view! { <Redirect path=article_path(&article) /> })}
</Suspense> </SuspenseError>
<Show when=move || fork_response.get().is_some()>
<Redirect path=article_path(&fork_response.get().unwrap()) />
</Show>
<p>"TODO: add option for admin to delete article etc"</p> <p>"TODO: add option for admin to delete article etc"</p>
} }
} }

View file

@ -3,17 +3,24 @@ use crate::{
frontend::{ frontend::{
api::CLIENT, api::CLIENT,
components::article_editor::EditorView, components::article_editor::EditorView,
utils::resources::{is_admin, site, DefaultResource}, utils::resources::{config, is_admin},
}, },
}; };
use leptos::{html::Textarea, prelude::*}; use leptos::{html::Textarea, prelude::*};
use leptos_meta::Title; use leptos_meta::Title;
use leptos_router::components::Redirect; use leptos_router::{components::Redirect, hooks::use_query_map};
use leptos_use::{use_textarea_autosize, UseTextareaAutosizeReturn}; use leptos_use::{use_textarea_autosize, UseTextareaAutosizeReturn};
#[component] #[component]
pub fn CreateArticle() -> impl IntoView { pub fn CreateArticle() -> impl IntoView {
let (title, set_title) = signal(String::new()); let title = use_query_map()
.get()
.get("title")
.unwrap_or_default()
.replace('_', " ");
let title = title.split_once('@').map(|(t, _)| t).unwrap_or(&title);
let (title, set_title) = signal(title.to_string());
let textarea_ref = NodeRef::<Textarea>::new(); let textarea_ref = NodeRef::<Textarea>::new();
let UseTextareaAutosizeReturn { let UseTextareaAutosizeReturn {
content, content,
@ -52,9 +59,7 @@ pub fn CreateArticle() -> impl IntoView {
} }
} }
}); });
let show_approval_message = Signal::derive(move || { let show_approval_message = Signal::derive(move || config().article_approval && !is_admin());
site().with_default(|site| site.config.article_approval) && !is_admin()
});
view! { view! {
<Title text="Create new Article" /> <Title text="Create new Article" />
@ -76,6 +81,7 @@ pub fn CreateArticle() -> impl IntoView {
type="text" type="text"
required required
placeholder="Title" placeholder="Title"
value=title
prop:disabled=move || wait_for_response.get() prop:disabled=move || wait_for_response.get()
on:keyup=move |ev| { on:keyup=move |ev| {
let val = event_target_value(&ev); let val = event_target_value(&ev);

View file

@ -1,9 +1,12 @@
use crate::frontend::{ use crate::frontend::{
components::article_nav::{ActiveTab, ArticleNav}, components::{
article_nav::{ActiveTab, ArticleNav},
suspense_error::SuspenseError,
},
pages::{article_edits_resource, article_resource}, pages::{article_edits_resource, article_resource},
utils::formatting::{article_title, render_date_time, user_link}, utils::formatting::{article_title, render_date_time, user_link},
}; };
use leptos::prelude::*; use leptos::{either::Either, prelude::*};
use leptos_meta::Title; use leptos_meta::Title;
use leptos_router::hooks::use_params_map; use leptos_router::hooks::use_params_map;
@ -11,31 +14,36 @@ use leptos_router::hooks::use_params_map;
pub fn EditDiff() -> impl IntoView { pub fn EditDiff() -> impl IntoView {
let params = use_params_map(); let params = use_params_map();
let article = article_resource(); let article = article_resource();
let edits = article_edits_resource(article);
view! { view! {
<ArticleNav article=article active_tab=ActiveTab::History /> <ArticleNav article=article active_tab=ActiveTab::History />
<Suspense fallback=|| { <SuspenseError result=article>
view! { "Loading..." }
}>
{move || Suspend::new(async move { {move || Suspend::new(async move {
let edits = article_edits_resource(article).await; let article_title = article
let hash = params.get_untracked().get("hash").clone().unwrap(); .await
let edit = edits.iter().find(|e| e.edit.hash.0.to_string() == hash).unwrap(); .map(|a| article_title(&a.article))
.unwrap_or_default();
edits
.await
.map(|edits| {
let hash = params.get_untracked().get("hash").clone();
let edit = edits.iter().find(|e| Some(e.edit.hash.0.to_string()) == hash);
if let Some(edit) = edit {
let label = format!( let label = format!(
"{} ({})", "{} ({})",
edit.edit.summary, edit.edit.summary,
render_date_time(edit.edit.published), render_date_time(edit.edit.published),
); );
let pending = edit.edit.pending; let pending = edit.edit.pending;
let title = format!( let title = format!("Diff {}{}", edit.edit.summary, article_title);
"Diff {} — {}", Either::Left(
edit.edit.summary,
article_title(&article.await.article),
);
view! { view! {
<Title text=title /> <Title text=title />
<div class="flex w-full"> <div class="flex w-full">
<h2 class="my-2 font-serif text-xl font-bold grow">{label}</h2> <h2 class="my-2 font-serif text-xl font-bold grow">
{label}
</h2>
<Show when=move || pending> <Show when=move || pending>
<span class="p-1 w-min rounded border-2 border-rose-300 h-min"> <span class="p-1 w-min rounded border-2 border-rose-300 h-min">
Pending Pending
@ -48,9 +56,20 @@ pub fn EditDiff() -> impl IntoView {
<code>{edit.edit.diff.clone()}</code> <code>{edit.edit.diff.clone()}</code>
</pre> </pre>
</div> </div>
},
)
} else {
Either::Right(
view! {
<div class="grid place-items-center h-screen">
<div class="alert alert-error w-fit">Invalid edit</div>
</div>
},
)
} }
})
})} })}
</Suspense> </SuspenseError>
} }
} }

View file

@ -5,6 +5,7 @@ use crate::{
article_nav::{ActiveTab, ArticleNav}, article_nav::{ActiveTab, ArticleNav},
comment::CommentView, comment::CommentView,
comment_editor::CommentEditorView, comment_editor::CommentEditorView,
suspense_error::SuspenseError,
}, },
pages::article_resource, pages::article_resource,
}, },
@ -20,12 +21,18 @@ pub fn ArticleDiscussion() -> impl IntoView {
view! { view! {
<ArticleNav article=article active_tab=ActiveTab::Discussion /> <ArticleNav article=article active_tab=ActiveTab::Discussion />
<Suspense fallback=|| view! { "Loading..." }> <SuspenseError result=article>
{move || Suspend::new(async move {
let article2 = article.await;
view! {
<CommentEditorView article=article /> <CommentEditorView article=article />
<div> <div>
<For <For
each=move || { each=move || {
article.get().map(|a| build_comments_tree(a.comments)).unwrap_or_default() article2
.clone()
.map(|a| build_comments_tree(a.comments))
.unwrap_or_default()
} }
key=|comment| comment.comment.id key=|comment| comment.comment.id
children=move |comment: DbCommentView| { children=move |comment: DbCommentView| {
@ -33,7 +40,9 @@ pub fn ArticleDiscussion() -> impl IntoView {
} }
/> />
</div> </div>
</Suspense> }
})}
</SuspenseError>
} }
} }
@ -66,19 +75,24 @@ fn build_comments_tree(comments: Vec<DbCommentView>) -> Vec<DbCommentView> {
.iter() .iter()
.map(|v| (v.comment.id, CommentNode::new(v.clone()))) .map(|v| (v.comment.id, CommentNode::new(v.clone())))
.collect(); .collect();
debug_assert!(comments.len() == map.len());
// Move top-level comments directly into tree vec. For comments having parent_id, move them // Move top-level comments directly into tree vec. For comments having parent_id, move them
// `children` of respective parent. This preserves existing order. // `children` of respective parent. This preserves existing order.
let mut tree = Vec::<CommentNode>::new(); let mut tree = Vec::<CommentNode>::new();
for view in comments { for view in &comments {
let child = map.get(&view.comment.id).unwrap().clone(); let child = map
.get(&view.comment.id)
.expect("get comment by id")
.clone();
if let Some(parent_id) = &view.comment.parent_id { if let Some(parent_id) = &view.comment.parent_id {
let parent = map.get_mut(parent_id).unwrap(); let parent = map.get_mut(parent_id).expect("get parent comment by id");
parent.children.push(child); parent.children.push(child);
} else { } else {
tree.push(child); tree.push(child);
} }
} }
debug_assert!(comments.len() == map.len());
// Now convert it back to flat array with correct order for rendering // Now convert it back to flat array with correct order for rendering
tree.into_iter().flat_map(|t| t.flatten()).collect() tree.into_iter().flat_map(|t| t.flatten()).collect()

View file

@ -10,13 +10,17 @@ use crate::{
components::{ components::{
article_editor::EditorView, article_editor::EditorView,
article_nav::{ActiveTab, ArticleNav}, article_nav::{ActiveTab, ArticleNav},
suspense_error::SuspenseError,
}, },
pages::article_resource, pages::article_resource,
}, },
}; };
use chrono::{Days, Utc}; use chrono::{Days, Utc};
use leptos::{html::Textarea, prelude::*}; use leptos::{html::Textarea, prelude::*, task::spawn_local};
use leptos_router::{components::Redirect, hooks::use_params_map}; use leptos_router::{
components::Redirect,
hooks::{use_params_map, use_query_map},
};
use leptos_use::{use_textarea_autosize, UseTextareaAutosizeReturn}; use leptos_use::{use_textarea_autosize, UseTextareaAutosizeReturn};
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
@ -31,30 +35,30 @@ const CONFLICT_MESSAGE: &str = "There was an edit conflict. Resolve it manually
#[component] #[component]
pub fn EditArticle() -> impl IntoView { pub fn EditArticle() -> impl IntoView {
let article = article_resource(); let article = article_resource();
let (edit_response, set_edit_response) = signal(EditResponse::None); let (edit_response, set_edit_response) = signal(EditResponse::None);
let (edit_error, set_edit_error) = signal(None::<String>); let (edit_error, set_edit_error) = signal(None::<String>);
let conflict_id = move || use_params_map().get_untracked().get("conflict_id").clone(); let conflict_id = use_query_map().get_untracked().get("conflict_id").clone();
if let Some(conflict_id) = conflict_id() { if let Some(conflict_id) = conflict_id {
Action::new(move |conflict_id: &String| { let conflict_id = conflict_id.parse().map(ConflictId);
let conflict_id = ConflictId(conflict_id.parse().unwrap()); spawn_local(async move {
async move { CLIENT
let conflict = CLIENT
.notifications_list() .notifications_list()
.await .await
.unwrap() .ok()
.into_iter() .into_iter()
.flatten()
.filter_map(|n| match n { .filter_map(|n| match n {
Notification::EditConflict(c) => Some(c), Notification::EditConflict(c) => Some(c),
_ => None, _ => None,
}) })
.find(|c| c.id == conflict_id) .find(|c| Ok(c.id) == conflict_id)
.unwrap(); .map(|conflict| {
set_edit_response.set(EditResponse::Conflict(conflict)); set_edit_response.set(EditResponse::Conflict(conflict));
set_edit_error.set(Some(CONFLICT_MESSAGE.to_string())); set_edit_error.set(Some(CONFLICT_MESSAGE.to_string()));
} });
}) })
.dispatch(conflict_id);
} }
let textarea_ref = NodeRef::<Textarea>::new(); let textarea_ref = NodeRef::<Textarea>::new();
@ -121,12 +125,10 @@ pub fn EditArticle() -> impl IntoView {
when=move || edit_response.get() == EditResponse::Success when=move || edit_response.get() == EditResponse::Success
fallback=move || { fallback=move || {
view! { view! {
<Suspense fallback=|| { <SuspenseError result=article>
view! { "Loading..." } {move || Suspend::new(async move {
}>
{move || {
article article
.get() .await
.map(|mut article| { .map(|mut article| {
if let EditResponse::Conflict(conflict) = edit_response.get() { if let EditResponse::Conflict(conflict) = edit_response.get() {
article.article.text = conflict.three_way_merge; article.article.text = conflict.three_way_merge;
@ -188,9 +190,8 @@ pub fn EditArticle() -> impl IntoView {
</div> </div>
} }
}) })
}} })}
</SuspenseError>
</Suspense>
} }
} }
> >

View file

@ -2,6 +2,7 @@ use crate::frontend::{
components::{ components::{
article_nav::{ActiveTab, ArticleNav}, article_nav::{ActiveTab, ArticleNav},
edit_list::EditList, edit_list::EditList,
suspense_error::SuspenseError,
}, },
pages::{article_edits_resource, article_resource}, pages::{article_edits_resource, article_resource},
}; };
@ -10,20 +11,22 @@ use leptos::prelude::*;
#[component] #[component]
pub fn ArticleHistory() -> impl IntoView { pub fn ArticleHistory() -> impl IntoView {
let article = article_resource(); let article = article_resource();
let edits = article_edits_resource(article);
view! { view! {
<ArticleNav article=article active_tab=ActiveTab::History /> <ArticleNav article=article active_tab=ActiveTab::History />
<Suspense fallback=|| { <SuspenseError result=article>
view! { "Loading..." } {move || Suspend::new(async move {
}> edits
{move || { .await
article_edits_resource(article)
.get()
.map(|edits| { .map(|edits| {
view! { <EditList edits=edits for_article=true /> } view! {
// TODO: move edits resource here? but leads to strange crash
<EditList edits=edits for_article=true />
}
}) })
}} })}
</Suspense> </SuspenseError>
} }
} }

View file

@ -1,101 +0,0 @@
use crate::{
common::article::ListArticlesParams,
frontend::{
api::CLIENT,
components::connect::ConnectView,
utils::{
formatting::{article_path, article_title},
resources::DefaultResource,
},
},
};
use leptos::prelude::*;
use leptos_meta::Title;
#[component]
pub fn ListArticles() -> impl IntoView {
let (only_local, set_only_local) = signal(false);
let articles = Resource::new(
move || only_local.get(),
|only_local| async move {
CLIENT
.list_articles(ListArticlesParams {
only_local: Some(only_local),
instance_id: None,
})
.await
},
);
let only_local_class = Resource::new(
move || only_local.get(),
|only_local| async move {
if only_local {
"btn rounded-r-none btn-primary"
} else {
"btn rounded-r-none"
}
.to_string()
},
);
let all_class = Resource::new(
move || only_local.get(),
|only_local| async move {
if !only_local {
"btn rounded-l-none btn-primary"
} else {
"btn rounded-l-none"
}
.to_string()
},
);
view! {
<Title text="Recently edited Articles" />
<h1 class="my-4 font-serif text-4xl font-bold">"Recently edited Articles"</h1>
<Suspense fallback=|| view! { "Loading..." }>
<div class="divide-x">
<input
type="button"
value="Only Local"
class=move || only_local_class.get()
on:click=move |_| {
set_only_local.set(true);
}
/>
<input
type="button"
value="All"
class=move || all_class.get()
on:click=move |_| {
set_only_local.set(false);
}
/>
</div>
<Show
when=move || {
articles.get_default().unwrap_or_default().len() > 1 || only_local.get()
}
fallback=move || view! { <ConnectView res=articles /> }
>
<ul class="my-4 list-none">
<For
each=move || articles.get_default().unwrap_or_default()
key=|article| article.id
let:article
>
{
view! {
<li>
<a class="text-lg link" href=article_path(&article)>
{article_title(&article)}
</a>
</li>
}
}
</For>
</ul>
</Show>
</Suspense>
}
}

View file

@ -4,5 +4,4 @@ pub mod diff;
pub mod discussion; pub mod discussion;
pub mod edit; pub mod edit;
pub mod history; pub mod history;
pub mod list;
pub mod read; pub mod read;

View file

@ -1,9 +1,12 @@
use crate::frontend::{ use crate::frontend::{
components::article_nav::{ActiveTab, ArticleNav}, components::{
article_nav::{ActiveTab, ArticleNav},
suspense_error::SuspenseError,
},
markdown::render_article_markdown, markdown::render_article_markdown,
pages::article_resource, pages::article_resource,
}; };
use leptos::prelude::*; use leptos::{either::Either, prelude::*};
use leptos_router::hooks::use_query_map; use leptos_router::hooks::use_query_map;
#[component] #[component]
@ -14,26 +17,24 @@ pub fn ReadArticle() -> impl IntoView {
view! { view! {
<ArticleNav article=article active_tab=ActiveTab::Read /> <ArticleNav article=article active_tab=ActiveTab::Read />
<Suspense fallback=|| { <SuspenseError result=article>
view! { "Loading..." } {move || Suspend::new(async move {
}> let article = article.await;
let markdown = article.map(|a| render_article_markdown(&a.article.text));
{move || { if let Ok(markdown) = markdown {
article Either::Right(
.get()
.map(|article| {
view! { view! {
<div <div class="max-w-full prose prose-slate" inner_html=markdown></div>
class="max-w-full prose prose-slate" },
inner_html=render_article_markdown(&article.article.text) )
></div> } else {
Either::Left(markdown)
} }
}) })} <Show when=move || edit_successful>
}} <Show when=move || edit_successful>
<div class="toast toast-center"> <div class="toast toast-center">
<div class="alert alert-success">Edit successful</div> <div class="alert alert-success">Edit successful</div>
</div> </div>
</Show> </Show>
</Suspense> </SuspenseError>
} }
} }

View file

@ -0,0 +1,60 @@
use crate::{
common::instance::DbInstance,
frontend::{
api::CLIENT,
components::{connect::ConnectView, suspense_error::SuspenseError},
utils::formatting::{instance_title_with_domain, instance_updated},
},
};
use leptos::prelude::*;
use leptos_meta::Title;
#[component]
pub fn Explore() -> impl IntoView {
let instances = Resource::new(move || (), |_| async move { CLIENT.list_instances().await });
view! {
<Title text="Explore" />
<h1 class="my-4 font-serif text-4xl font-bold">Instances</h1>
<SuspenseError result=instances>
{move || Suspend::new(async move {
let instances_ = instances.await;
let is_empty = instances_.as_ref().map(|i| i.is_empty()).unwrap_or(true);
view! {
<Show
when=move || !is_empty
fallback=move || view! { <ConnectView res=instances /> }
>
<ul class="my-4 list-none">
{instances_
.clone()
.ok()
.iter()
.flatten()
.map(instance_card)
.collect::<Vec<_>>()}
</ul>
</Show>
}
})}
</SuspenseError>
}
}
pub fn instance_card(i: &DbInstance) -> impl IntoView {
view! {
<li>
<div class="m-4 shadow card bg-base-100">
<div class="p-4 card-body">
<div class="flex">
<a class="card-title grow" href=format!("/instance/{}", i.domain)>
{instance_title_with_domain(i)}
</a>
{instance_updated(i)}
</div>
<p>{i.topic.clone()}</p>
</div>
</div>
</li>
}
}

View file

@ -2,14 +2,17 @@ use crate::{
common::{article::ListArticlesParams, instance::DbInstance, utils::http_protocol_str}, common::{article::ListArticlesParams, instance::DbInstance, utils::http_protocol_str},
frontend::{ frontend::{
api::CLIENT, api::CLIENT,
components::instance_follow_button::InstanceFollowButton, components::{instance_follow_button::InstanceFollowButton, suspense_error::SuspenseError},
utils::formatting::{ utils::{
errors::FrontendError,
formatting::{
article_path, article_path,
article_title, article_title,
instance_title_with_domain, instance_title_with_domain,
instance_updated, instance_updated,
}, },
}, },
},
}; };
use leptos::prelude::*; use leptos::prelude::*;
use leptos_meta::Title; use leptos_meta::Title;
@ -19,19 +22,18 @@ use url::Url;
#[component] #[component]
pub fn InstanceDetails() -> impl IntoView { pub fn InstanceDetails() -> impl IntoView {
let params = use_params_map(); let params = use_params_map();
let hostname = move || params.get().get("hostname").clone().unwrap(); let hostname = move || params.get().get("hostname").clone();
let instance_profile = Resource::new(hostname, move |hostname| async move { let instance_profile = Resource::new(hostname, move |hostname| async move {
let url = Url::parse(&format!("{}://{hostname}", http_protocol_str())).unwrap(); let hostname = hostname.ok_or(FrontendError::new("No instance given"))?;
CLIENT.resolve_instance(url).await.unwrap() let url = Url::parse(&format!("{}://{hostname}", http_protocol_str()))?;
CLIENT.resolve_instance(url).await
}); });
view! { view! {
<Suspense fallback=|| { <SuspenseError result=instance_profile>
view! { "Loading..." } {move || Suspend::new(async move {
}>
{move || {
instance_profile instance_profile
.get() .await
.map(|instance: DbInstance| { .map(|instance: DbInstance| {
let articles = Resource::new( let articles = Resource::new(
move || instance.id, move || instance.id,
@ -42,7 +44,6 @@ pub fn InstanceDetails() -> impl IntoView {
instance_id: Some(instance_id), instance_id: Some(instance_id),
}) })
.await .await
.unwrap()
}, },
); );
let title = instance_title_with_domain(&instance); let title = instance_title_with_domain(&instance);
@ -60,10 +61,10 @@ pub fn InstanceDetails() -> impl IntoView {
<div>{instance.topic}</div> <div>{instance.topic}</div>
<h2 class="font-serif text-xl font-bold">Articles</h2> <h2 class="font-serif text-xl font-bold">Articles</h2>
<ul class="list-none"> <ul class="list-none">
<Suspense> <SuspenseError result=articles>
{move || { {move || Suspend::new(async move {
articles articles
.get() .await
.map(|a| { .map(|a| {
a.into_iter() a.into_iter()
.map(|a| { .map(|a| {
@ -77,14 +78,14 @@ pub fn InstanceDetails() -> impl IntoView {
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
}) })
}} })}
</Suspense> </SuspenseError>
</ul> </ul>
</div> </div>
} }
}) })
}} })}
</Suspense> </SuspenseError>
} }
} }

View file

@ -1,58 +0,0 @@
use crate::frontend::{
api::CLIENT,
components::connect::ConnectView,
utils::formatting::{instance_title_with_domain, instance_updated},
};
use leptos::prelude::*;
use leptos_meta::Title;
#[component]
pub fn ListInstances() -> impl IntoView {
let instances = Resource::new(
move || (),
|_| async move { CLIENT.list_instances().await.unwrap() },
);
view! {
<Title text="Instances" />
<h1 class="my-4 font-serif text-4xl font-bold">Instances</h1>
<Suspense fallback=|| view! { "Loading..." }>
<Show
when=move || { !instances.get().unwrap_or_default().is_empty() }
fallback=move || view! { <ConnectView res=instances /> }
>
<ul class="my-4 list-none">
{move || {
instances
.get()
.map(|a| {
a.into_iter()
.map(|ref i| {
view! {
<li>
<div class="m-4 shadow card bg-base-100">
<div class="p-4 card-body">
<div class="flex">
<a
class="card-title grow"
href=format!("/instance/{}", i.domain)
>
{instance_title_with_domain(i)}
</a>
{instance_updated(i)}
</div>
<p>{i.topic.clone()}</p>
</div>
</div>
</li>
}
})
.collect::<Vec<_>>()
})
}}
</ul>
</Show>
</Suspense>
}
}

View file

@ -1,4 +1,3 @@
pub mod details; pub mod details;
pub mod list;
pub mod search; pub mod search;
pub mod settings; pub mod settings;

View file

@ -1,32 +1,29 @@
use crate::{common::instance::UpdateInstanceParams, frontend::api::CLIENT}; use crate::{
common::instance::UpdateInstanceParams,
frontend::{
api::CLIENT,
components::suspense_error::SuspenseError,
utils::errors::FrontendResultExt,
},
};
use leptos::prelude::*; use leptos::prelude::*;
use leptos_meta::Title; use leptos_meta::Title;
#[component] #[component]
pub fn InstanceSettings() -> impl IntoView { pub fn InstanceSettings() -> impl IntoView {
let (saved, set_saved) = signal(false); let (saved, set_saved) = signal(false);
let (submit_error, set_submit_error) = signal(None::<String>); let instance = Resource::new(|| (), |_| async move { CLIENT.get_local_instance().await });
let instance = Resource::new(
|| (),
|_| async move { CLIENT.get_local_instance().await.unwrap() },
);
let submit_action = Action::new(move |params: &UpdateInstanceParams| { let submit_action = Action::new(move |params: &UpdateInstanceParams| {
let params = params.clone(); let params = params.clone();
async move { async move {
let result = CLIENT.update_local_instance(&params).await; CLIENT
match result { .update_local_instance(&params)
Ok(_res) => { .await
.error_popup(|_| {
instance.refetch(); instance.refetch();
set_saved.set(true); set_saved.set(true);
set_submit_error.set(None); });
}
Err(err) => {
let msg = err.to_string();
log::warn!("Unable to update profile: {msg}");
set_submit_error.set(Some(msg));
}
}
} }
}); });
@ -34,24 +31,19 @@ pub fn InstanceSettings() -> impl IntoView {
// that completely breaks reactivity. // that completely breaks reactivity.
view! { view! {
<Title text="Instance Settings" /> <Title text="Instance Settings" />
<Suspense fallback=|| { <SuspenseError result=instance>
view! { "Loading..." }
}>
{move || Suspend::new(async move { {move || Suspend::new(async move {
let instance = instance.await; instance
.await
.map(|instance| {
let (name, set_name) = signal(instance.instance.name.unwrap_or_default()); let (name, set_name) = signal(instance.instance.name.unwrap_or_default());
let (topic, set_topic) = signal(instance.instance.topic.unwrap_or_default()); let (topic, set_topic) = signal(
instance.instance.topic.unwrap_or_default(),
);
view! { view! {
<h1 class="flex-auto my-6 font-serif text-4xl font-bold grow"> <h1 class="flex-auto my-6 font-serif text-4xl font-bold grow">
"Instance Settings" "Instance Settings"
</h1> </h1>
{move || {
submit_error
.get()
.map(|err| {
view! { <p class="alert alert-error">{err}</p> }
})
}}
<div class="flex flex-row mb-2"> <div class="flex flex-row mb-2">
<label class="block w-20" for="name"> <label class="block w-20" for="name">
Name Name
@ -60,12 +52,7 @@ pub fn InstanceSettings() -> impl IntoView {
type="text" type="text"
id="name" id="name"
class="w-80 input input-secondary input-bordered" class="w-80 input input-secondary input-bordered"
prop:value=name bind:value=(name, set_name)
value=name
on:change=move |ev| {
let val = event_target_value(&ev);
set_name.set(val);
}
/> />
</div> </div>
<div class="flex flex-row mb-2"> <div class="flex flex-row mb-2">
@ -76,12 +63,7 @@ pub fn InstanceSettings() -> impl IntoView {
type="text" type="text"
id="name" id="name"
class="w-80 input input-secondary input-bordered" class="w-80 input input-secondary input-bordered"
prop:value=topic bind:value=(topic, set_topic)
value=topic
on:change=move |ev| {
let val = event_target_value(&ev);
set_topic.set(val);
}
/> />
</div> </div>
<button <button
@ -105,8 +87,9 @@ pub fn InstanceSettings() -> impl IntoView {
</div> </div>
</Show> </Show>
} }
})
})} })}
</Suspense> </SuspenseError>
} }
} }

View file

@ -1,3 +1,4 @@
use super::utils::errors::FrontendResult;
use crate::{ use crate::{
common::{ common::{
article::{DbArticleView, EditView, GetArticleParams}, article::{DbArticleView, EditView, GetArticleParams},
@ -9,13 +10,17 @@ use leptos::prelude::*;
use leptos_router::hooks::use_params_map; use leptos_router::hooks::use_params_map;
pub mod article; pub mod article;
pub mod explore;
pub mod instance; pub mod instance;
pub mod user; pub mod user;
fn article_resource() -> Resource<DbArticleView> { pub fn article_title_param() -> Option<String> {
let params = use_params_map(); let params = use_params_map();
let title = move || params.get().get("title").clone(); params.get().get("title").clone()
Resource::new(title, move |title| async move { }
fn article_resource() -> Resource<FrontendResult<DbArticleView>> {
Resource::new(article_title_param, move |title| async move {
let mut title = title.unwrap_or(MAIN_PAGE_NAME.to_string()); let mut title = title.unwrap_or(MAIN_PAGE_NAME.to_string());
let mut domain = None; let mut domain = None;
if let Some((title_, domain_)) = title.clone().split_once('@') { if let Some((title_, domain_)) = title.clone().split_once('@') {
@ -29,17 +34,17 @@ fn article_resource() -> Resource<DbArticleView> {
id: None, id: None,
}) })
.await .await
.unwrap()
}) })
} }
fn article_edits_resource(article: Resource<DbArticleView>) -> Resource<Vec<EditView>> {
fn article_edits_resource(
article: Resource<FrontendResult<DbArticleView>>,
) -> Resource<FrontendResult<Vec<EditView>>> {
Resource::new( Resource::new(
move || article.get(), move || article.get(),
move |_| async move { move |_| async move {
CLIENT let id = article.await.map(|a| a.article.id)?;
.get_article_edits(article.await.article.id) CLIENT.get_article_edits(id).await
.await
.unwrap_or_default()
}, },
) )
} }

View file

@ -2,7 +2,8 @@ use crate::{
common::user::UpdateUserParams, common::user::UpdateUserParams,
frontend::{ frontend::{
api::CLIENT, api::CLIENT,
utils::resources::{site, DefaultResource}, components::suspense_error::SuspenseError,
utils::{errors::FrontendResultExt, resources::site},
}, },
}; };
use leptos::prelude::*; use leptos::prelude::*;
@ -11,24 +12,14 @@ use leptos_meta::Title;
#[component] #[component]
pub fn UserEditProfile() -> impl IntoView { pub fn UserEditProfile() -> impl IntoView {
let (saved, set_saved) = signal(false); let (saved, set_saved) = signal(false);
let (submit_error, set_submit_error) = signal(None::<String>);
let submit_action = Action::new(move |params: &UpdateUserParams| { let submit_action = Action::new(move |params: &UpdateUserParams| {
let params = params.clone(); let params = params.clone();
async move { async move {
let result = CLIENT.update_user_profile(params).await; CLIENT.update_user_profile(params).await.error_popup(|_| {
match result {
Ok(_res) => {
site().refetch();
set_saved.set(true); set_saved.set(true);
set_submit_error.set(None); site().refetch();
} });
Err(err) => {
let msg = err.to_string();
log::warn!("Unable to update profile: {msg}");
set_submit_error.set(Some(msg));
}
}
} }
}); });
@ -36,36 +27,30 @@ pub fn UserEditProfile() -> impl IntoView {
// that completely breaks reactivity. // that completely breaks reactivity.
view! { view! {
<Title text="Edit Profile" /> <Title text="Edit Profile" />
<Suspense fallback=|| { <SuspenseError result=site()>
view! { "Loading..." } {Suspend::new(async move {
}> site()
{ .await
let my_profile = site().with_default(|site| site.clone().my_profile.unwrap()); .ok()
.and_then(|site| site.my_profile)
.map(|my_profile| {
let (display_name, set_display_name) = signal( let (display_name, set_display_name) = signal(
my_profile.person.display_name.clone().unwrap_or_default(), my_profile.person.display_name.clone().unwrap_or_default(),
); );
let (bio, set_bio) = signal(my_profile.person.bio.clone().unwrap_or_default()); let (bio, set_bio) = signal(
my_profile.person.bio.clone().unwrap_or_default(),
);
view! { view! {
<h1 class="flex-auto my-6 font-serif text-4xl font-bold grow">Edit Profile</h1> <h1 class="flex-auto my-6 font-serif text-4xl font-bold grow">
{move || { Edit Profile
submit_error </h1>
.get()
.map(|err| {
view! { <p class="alert alert-error">{err}</p> }
})
}}
<div class="flex flex-row mb-2"> <div class="flex flex-row mb-2">
<label class="block w-40">Displayname</label> <label class="block w-40">Displayname</label>
<input <input
type="text" type="text"
id="displayname" id="displayname"
class="w-80 input input-secondary input-bordered" class="w-80 input input-secondary input-bordered"
prop:value=display_name bind:value=(display_name, set_display_name)
value=display_name
on:change=move |ev| {
let val = event_target_value(&ev);
set_display_name.set(val);
}
/> />
</div> </div>
<div class="flex flex-row mb-2"> <div class="flex flex-row mb-2">
@ -75,11 +60,7 @@ pub fn UserEditProfile() -> impl IntoView {
<textarea <textarea
id="bio" id="bio"
class="w-80 text-base textarea textarea-secondary" class="w-80 text-base textarea textarea-secondary"
prop:value=move || bio.get() bind:value=(bio, set_bio)
on:input:target=move |evt| {
let val = evt.target().value();
set_bio.set(val);
}
> >
bio.get() bio.get()
</textarea> </textarea>
@ -106,8 +87,9 @@ pub fn UserEditProfile() -> impl IntoView {
</div> </div>
</Show> </Show>
} }
} })
})}
</Suspense> </SuspenseError>
} }
} }

View file

@ -2,7 +2,11 @@ use crate::{
common::Notification, common::Notification,
frontend::{ frontend::{
api::CLIENT, api::CLIENT,
utils::formatting::{article_path, article_title}, components::suspense_error::SuspenseError,
utils::{
errors::FrontendResultExt,
formatting::{article_path, article_title},
},
}, },
}; };
use leptos::prelude::*; use leptos::prelude::*;
@ -12,17 +16,17 @@ use leptos_meta::Title;
pub fn Notifications() -> impl IntoView { pub fn Notifications() -> impl IntoView {
let notifications = Resource::new( let notifications = Resource::new(
move || {}, move || {},
|_| async move { CLIENT.notifications_list().await.unwrap_or_default() }, |_| async move { CLIENT.notifications_list().await },
); );
view! { view! {
<Title text="Notifications" /> <Title text="Notifications" />
<h1 class="flex-auto my-6 font-serif text-4xl font-bold grow">Notifications</h1> <h1 class="flex-auto my-6 font-serif text-4xl font-bold grow">Notifications</h1>
<Suspense fallback=|| view! { "Loading..." }> <SuspenseError result=notifications>
<ul class="divide-y divide-solid"> <ul class="divide-y divide-solid">
{move || { {move || Suspend::new(async move {
notifications notifications
.get() .await
.map(|n| { .map(|n| {
n.into_iter() n.into_iter()
.map(|ref notif| { .map(|ref notif| {
@ -31,7 +35,11 @@ pub fn Notifications() -> impl IntoView {
EditConflict(c) => { EditConflict(c) => {
( (
"visibility: hidden", "visibility: hidden",
format!("{}/edit/{}", article_path(&c.article), c.id.0), format!(
"{}/edit?conflict_id={}",
article_path(&c.article),
c.id.0,
),
format!( format!(
"Conflict: {} - {}", "Conflict: {} - {}",
article_title(&c.article), article_title(&c.article),
@ -52,9 +60,11 @@ pub fn Notifications() -> impl IntoView {
let notif_ = notif_.clone(); let notif_ = notif_.clone();
async move { async move {
if let ArticleApprovalRequired(a) = notif_ { if let ArticleApprovalRequired(a) = notif_ {
CLIENT.approve_article(a.id, true).await.unwrap(); CLIENT
.approve_article(a.id, true)
.await
.error_popup(|_| notifications.refetch());
} }
notifications.refetch();
} }
}); });
let notif_ = notif.clone(); let notif_ = notif.clone();
@ -63,10 +73,13 @@ pub fn Notifications() -> impl IntoView {
async move { async move {
match notif_ { match notif_ {
EditConflict(c) => { EditConflict(c) => {
CLIENT.delete_conflict(c.id).await.unwrap(); CLIENT.delete_conflict(c.id).await.error_popup(|_| {});
} }
ArticleApprovalRequired(a) => { ArticleApprovalRequired(a) => {
CLIENT.approve_article(a.id, false).await.unwrap(); CLIENT
.approve_article(a.id, false)
.await
.error_popup(|_| {});
} }
} }
notifications.refetch(); notifications.refetch();
@ -101,9 +114,9 @@ pub fn Notifications() -> impl IntoView {
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
}) })
}} })}
</ul> </ul>
</Suspense> </SuspenseError>
} }
} }

View file

@ -2,7 +2,7 @@ use crate::{
common::user::GetUserParams, common::user::GetUserParams,
frontend::{ frontend::{
api::CLIENT, api::CLIENT,
components::edit_list::EditList, components::{edit_list::EditList, suspense_error::SuspenseError},
markdown::render_article_markdown, markdown::render_article_markdown,
utils::formatting::user_title, utils::formatting::user_title,
}, },
@ -15,43 +15,26 @@ use leptos_router::hooks::use_params_map;
pub fn UserProfile() -> impl IntoView { pub fn UserProfile() -> impl IntoView {
let params = use_params_map(); let params = use_params_map();
let name = move || params.get().get("name").clone().unwrap_or_default(); let name = move || params.get().get("name").clone().unwrap_or_default();
let (error, set_error) = signal(None::<String>);
let user_profile = Resource::new(name, move |mut name| async move { let user_profile = Resource::new(name, move |mut name| async move {
set_error.set(None);
let mut domain = None; let mut domain = None;
if let Some((title_, domain_)) = name.clone().split_once('@') { if let Some((title_, domain_)) = name.clone().split_once('@') {
name = title_.to_string(); name = title_.to_string();
domain = Some(domain_.to_string()); domain = Some(domain_.to_string());
} }
let params = GetUserParams { name, domain }; let params = GetUserParams { name, domain };
CLIENT.get_user(params).await.unwrap() CLIENT.get_user(params).await
}); });
view! {
<SuspenseError result=user_profile>
{move || Suspend::new(async move {
let edits = Resource::new( let edits = Resource::new(
move || user_profile.get(), move || user_profile.get(),
move |_| async move { move |_| async move { CLIENT.get_person_edits(user_profile.await?.id).await },
CLIENT
.get_person_edits(user_profile.await.id)
.await
.unwrap_or_default()
},
); );
user_profile
view! { .await
{move || { .map(|person| {
error
.get()
.map(|err| {
view! { <p style="color:red;">{err}</p> }
})
}}
<Suspense fallback=|| {
view! { "Loading..." }
}>
{move || Suspend::new(async move {
let edits = edits.await;
let person = user_profile.await;
view! { view! {
<Title text=user_title(&person) /> <Title text=user_title(&person) />
<h1 class="flex-auto my-6 font-serif text-4xl font-bold grow"> <h1 class="flex-auto my-6 font-serif text-4xl font-bold grow">
@ -63,11 +46,21 @@ pub fn UserProfile() -> impl IntoView {
inner_html=render_article_markdown(&person.bio.unwrap_or_default()) inner_html=render_article_markdown(&person.bio.unwrap_or_default())
></div> ></div>
<SuspenseError result=user_profile>
{move || Suspend::new(async move {
edits
.await
.map(|edits| {
view! {
<h2 class="font-serif text-xl font-bold">Edits</h2> <h2 class="font-serif text-xl font-bold">Edits</h2>
<EditList edits=edits for_article=false /> <EditList edits=edits for_article=false />
} }
})
})} })}
</SuspenseError>
</Suspense> }
})
})}
</SuspenseError>
} }
} }

View file

@ -0,0 +1,97 @@
use leptos::prelude::*;
use log::warn;
use serde::{Deserialize, Serialize};
use std::{error::Error, fmt::Display, time::Duration};
pub type FrontendResult<T> = Result<T, FrontendError>;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct FrontendError(String);
impl FrontendError {
pub fn new(message: impl Into<String>) -> Self {
Self(message.into())
}
pub fn message(self) -> String {
self.0
}
}
pub trait FrontendResultExt<T> {
fn error_popup<F>(self, on_success: F)
where
F: FnOnce(T);
}
impl<T> FrontendResultExt<T> for FrontendResult<T> {
fn error_popup<F>(self, on_success: F)
where
F: FnOnce(T),
{
match self {
Ok(o) => on_success(o),
Err(e) => {
warn!("{e}");
ErrorPopup::set(e.0);
}
}
}
}
#[derive(Clone)]
pub struct ErrorPopup {
read: ReadSignal<Option<String>>,
write: WriteSignal<Option<String>>,
}
impl ErrorPopup {
pub fn init() {
let (read, write) = signal(None::<String>);
provide_context(Self { read, write });
}
pub fn set(msg: String) {
if let Some(s) = use_context::<Self>() {
s.write.set(Some(msg));
set_timeout(move || s.write.set(None), Duration::from_secs(15));
}
}
pub fn get() -> Option<String> {
use_context::<Self>()?.read.get()
}
}
impl Display for FrontendError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl Error for FrontendError {}
#[cfg(feature = "ssr")]
impl From<reqwest::Error> for FrontendError {
fn from(value: reqwest::Error) -> Self {
Self(value.to_string())
}
}
#[cfg(not(feature = "ssr"))]
impl From<gloo_net::Error> for FrontendError {
fn from(value: gloo_net::Error) -> Self {
Self(value.to_string())
}
}
impl From<url::ParseError> for FrontendError {
fn from(value: url::ParseError) -> Self {
Self(value.to_string())
}
}
impl From<serde_urlencoded::ser::Error> for FrontendError {
fn from(value: serde_urlencoded::ser::Error) -> Self {
Self(value.to_string())
}
}

View file

@ -4,6 +4,7 @@ use leptos::prelude::*;
use leptos_use::{use_cookie_with_options, SameSite, UseCookieOptions}; use leptos_use::{use_cookie_with_options, SameSite, UseCookieOptions};
pub mod dark_mode; pub mod dark_mode;
pub mod errors;
pub mod formatting; pub mod formatting;
pub mod resources; pub mod resources;

View file

@ -1,37 +1,42 @@
use crate::common::instance::SiteView; use super::errors::FrontendResult;
use crate::{
common::{
instance::{Options, SiteView},
user::LocalUserView,
},
frontend::api::CLIENT,
};
use leptos::prelude::*; use leptos::prelude::*;
pub fn site() -> Resource<SiteView> { type SiteResource = Resource<FrontendResult<SiteView>>;
use_context::<Resource<SiteView>>().unwrap()
pub fn site() -> SiteResource {
site_internal().unwrap_or_else(|| Resource::new(|| (), |_| async move { CLIENT.site().await }))
}
fn site_internal() -> Option<SiteResource> {
use_context::<Resource<FrontendResult<SiteView>>>()
}
pub fn my_profile() -> Option<LocalUserView> {
match site_internal() {
Some(s) => s.map(|s| s.clone().ok().map(|s| s.my_profile))??,
None => None,
}
}
pub fn config() -> Options {
match site_internal() {
Some(s) => s.map(|s| s.clone().ok().map(|s| s.config)).flatten(),
None => None,
}
.unwrap_or_default()
} }
pub fn is_logged_in() -> bool { pub fn is_logged_in() -> bool {
site().with_default(|site| site.my_profile.is_some()) my_profile().is_some()
}
pub fn is_admin() -> bool {
site().with_default(|site| {
site.my_profile
.as_ref()
.map(|p| p.local_user.admin)
.unwrap_or(false)
})
}
pub trait DefaultResource<T> {
fn with_default<O>(&self, f: impl FnOnce(&T) -> O) -> O;
fn get_default(&self) -> T;
} }
impl<T: Default + Send + Sync + Clone> DefaultResource<T> for Resource<T> { pub fn is_admin() -> bool {
fn with_default<O>(&self, f: impl FnOnce(&T) -> O) -> O { my_profile().map(|p| p.local_user.admin).unwrap_or(false)
self.with(|x| match x {
Some(x) => f(x),
None => f(&T::default()),
})
}
fn get_default(&self) -> T {
match self.get() {
Some(x) => x.clone(),
None => T::default(),
}
}
} }

View file

@ -1,5 +1,4 @@
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
pub mod backend; pub mod backend;
pub mod common; pub mod common;
#[expect(clippy::unwrap_used)]
pub mod frontend; pub mod frontend;

View file

@ -1,6 +1,6 @@
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
#[tokio::main] #[tokio::main]
pub async fn main() -> ibis::backend::utils::error::MyResult<()> { pub async fn main() -> ibis::backend::utils::error::BackendResult<()> {
use ibis::backend::utils::config::IbisConfig; use ibis::backend::utils::config::IbisConfig;
use log::LevelFilter; use log::LevelFilter;

View file

@ -9,7 +9,6 @@ use ibis::{
common::{instance::Options, user::RegisterUserParams}, common::{instance::Options, user::RegisterUserParams},
frontend::api::ApiClient, frontend::api::ApiClient,
}; };
use reqwest::ClientBuilder;
use std::{ use std::{
env::current_dir, env::current_dir,
fs::{create_dir_all, remove_dir_all}, fs::{create_dir_all, remove_dir_all},
@ -90,6 +89,7 @@ pub struct IbisInstance {
pub api_client: ApiClient, pub api_client: ApiClient,
db_path: String, db_path: String,
db_handle: JoinHandle<()>, db_handle: JoinHandle<()>,
pub hostname: String,
} }
impl IbisInstance { impl IbisInstance {
@ -110,15 +110,15 @@ impl IbisInstance {
async fn start(db_path: String, port: i32, username: &str, article_approval: bool) -> Self { async fn start(db_path: String, port: i32, username: &str, article_approval: bool) -> Self {
let connection_url = format!("postgresql://ibis:password@/ibis?host={db_path}"); let connection_url = format!("postgresql://ibis:password@/ibis?host={db_path}");
let hostname = format!("127.0.0.1:{port}");
let domain = format!("localhost:{port}"); let hostname = format!("localhost:{port}");
let config = IbisConfig { let config = IbisConfig {
database: IbisConfigDatabase { database: IbisConfigDatabase {
connection_url, connection_url,
..Default::default() ..Default::default()
}, },
federation: IbisConfigFederation { federation: IbisConfigFederation {
domain: domain.clone(), domain: hostname.clone(),
..Default::default() ..Default::default()
}, },
options: Options { options: Options {
@ -127,10 +127,10 @@ impl IbisInstance {
}, },
..Default::default() ..Default::default()
}; };
let client = ClientBuilder::new().cookie_store(true).build().unwrap(); let api_client = ApiClient::new(Some(hostname.clone()));
let api_client = ApiClient::new(client, Some(domain));
let (tx, rx) = oneshot::channel::<()>(); let (tx, rx) = oneshot::channel::<()>();
let handle = tokio::task::spawn(async move { let db_handle = tokio::task::spawn(async move {
let hostname = format!("127.0.0.1:{port}");
start(config, Some(hostname.parse().unwrap()), Some(tx)) start(config, Some(hostname.parse().unwrap()), Some(tx))
.await .await
.unwrap(); .unwrap();
@ -145,7 +145,8 @@ impl IbisInstance {
Self { Self {
api_client, api_client,
db_path, db_path,
db_handle: handle, db_handle,
hostname,
} }
} }

View file

@ -54,7 +54,7 @@ async fn test_create_read_and_edit_local_article() -> Result<()> {
// error on article which wasnt federated // error on article which wasnt federated
let not_found = beta.get_article(get_article_data.clone()).await; let not_found = beta.get_article(get_article_data.clone()).await;
assert!(not_found.is_none()); assert!(not_found.is_err());
// edit article // edit article
let edit_params = EditArticleParams { let edit_params = EditArticleParams {
@ -185,7 +185,7 @@ async fn test_synchronize_articles() -> Result<()> {
// try to read remote article by name, fails without domain // try to read remote article by name, fails without domain
let get_res = beta.get_article(get_article_data.clone()).await; let get_res = beta.get_article(get_article_data.clone()).await;
assert!(get_res.is_none()); assert!(get_res.is_err());
// get the article with instance id and compare // get the article with instance id and compare
let get_res = RetryFuture::new( let get_res = RetryFuture::new(
@ -197,11 +197,11 @@ async fn test_synchronize_articles() -> Result<()> {
}; };
let res = beta.get_article(get_article_data).await; let res = beta.get_article(get_article_data).await;
match res { match res {
None => Err(RetryPolicy::<String>::Retry(None)), Err(_) => Err(RetryPolicy::<String>::Retry(None)),
Some(a) if a.latest_version != edit_res.latest_version => { Ok(a) if a.latest_version != edit_res.latest_version => {
Err(RetryPolicy::Retry(None)) Err(RetryPolicy::Retry(None))
} }
Some(a) => Ok(a), Ok(a) => Ok(a),
} }
}, },
LinearRetryStrategy::new(), LinearRetryStrategy::new(),
@ -805,9 +805,9 @@ async fn test_synchronize_instances() -> Result<()> {
|| async { || async {
let res = gamma.list_instances().await; let res = gamma.list_instances().await;
match res { match res {
None => Err(RetryPolicy::<String>::Retry(None)), Err(_) => Err(RetryPolicy::<String>::Retry(None)),
Some(i) if i.len() < 3 => Err(RetryPolicy::Retry(None)), Ok(i) if i.len() < 3 => Err(RetryPolicy::Retry(None)),
Some(i) => Ok(i), Ok(i) => Ok(i),
} }
}, },
LinearRetryStrategy::new(), LinearRetryStrategy::new(),