1
0
Fork 0
mirror of https://github.com/Nutomic/ibis.git synced 2025-02-09 07:24:41 +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]
dbg_macro = "deny"
unwrap_used = "deny"
todo = "deny"
# frontend and shared deps
[dependencies]

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ use crate::{
backend::{
database::{conflict::DbConflict, read_jwt_secret, IbisContext},
utils::{
error::MyResult,
error::BackendResult,
validate::{validate_display_name, validate_user_name},
},
},
@ -54,7 +54,7 @@ struct Claims {
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 claims = Claims {
sub: person.username.clone(),
@ -69,7 +69,7 @@ fn generate_login_token(person: &DbPerson, context: &Data<IbisContext>) -> MyRes
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 secret = read_jwt_secret(context)?;
let key = DecodingKey::from_secret(secret.as_bytes());
@ -82,7 +82,7 @@ pub(in crate::backend::api) async fn register_user(
context: Data<IbisContext>,
jar: CookieJar,
Form(params): Form<RegisterUserParams>,
) -> MyResult<(CookieJar, Json<LocalUserView>)> {
) -> BackendResult<(CookieJar, Json<LocalUserView>)> {
if !context.config.options.registration_open {
return Err(anyhow!("Registration is closed").into());
}
@ -98,7 +98,7 @@ pub(in crate::backend::api) async fn login_user(
context: Data<IbisContext>,
jar: CookieJar,
Form(params): Form<LoginUserParams>,
) -> MyResult<(CookieJar, Json<LocalUserView>)> {
) -> BackendResult<(CookieJar, Json<LocalUserView>)> {
let user = DbPerson::read_local_from_name(&params.username, &context)?;
let valid = verify(&params.password, &user.local_user.password_encrypted)?;
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(
context: Data<IbisContext>,
jar: CookieJar,
) -> MyResult<(CookieJar, Json<SuccessResponse>)> {
) -> BackendResult<(CookieJar, Json<SuccessResponse>)> {
let jar = jar.remove(create_cookie(String::new(), &context));
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(
params: Query<GetUserParams>,
context: Data<IbisContext>,
) -> MyResult<Json<DbPerson>> {
) -> BackendResult<Json<DbPerson>> {
Ok(Json(DbPerson::read_from_name(
&params.name,
&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(
context: Data<IbisContext>,
Form(mut params): Form<UpdateUserParams>,
) -> MyResult<Json<SuccessResponse>> {
) -> BackendResult<Json<SuccessResponse>> {
empty_to_none(&mut params.display_name);
empty_to_none(&mut params.bio);
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(
Extension(user): Extension<LocalUserView>,
context: Data<IbisContext>,
) -> MyResult<Json<Vec<Notification>>> {
) -> BackendResult<Json<Vec<Notification>>> {
let conflicts = DbConflict::list(&user.person, &context)?;
let conflicts: Vec<_> = try_join_all(conflicts.into_iter().map(|c| {
let data = context.reset_request_count();
@ -194,16 +194,20 @@ pub(crate) async fn list_notifications(
#[debug_handler]
pub(crate) async fn count_notifications(
Extension(user): Extension<LocalUserView>,
user: Option<Extension<LocalUserView>>,
context: Data<IbisContext>,
) -> MyResult<Json<usize>> {
let mut count = 0;
let conflicts = DbConflict::list(&user.person, &context)?;
count += conflicts.len();
if check_is_admin(&user).is_ok() {
let articles = DbArticle::list_approval_required(&context)?;
count += articles.len();
}
) -> BackendResult<Json<usize>> {
if let Some(user) = user {
let mut count = 0;
let conflicts = DbConflict::list(&user.person, &context)?;
count += conflicts.len();
if check_is_admin(&user).is_ok() {
let articles = DbArticle::list_approval_required(&context)?;
count += articles.len();
}
Ok(Json(count))
Ok(Json(count))
} else {
Ok(Json(0))
}
}

View file

@ -5,7 +5,7 @@ use crate::{
IbisContext,
},
federation::objects::edits_collection::DbEditCollection,
utils::error::MyResult,
utils::error::BackendResult,
},
common::{
article::{DbArticle, DbArticleView, EditVersion},
@ -42,18 +42,18 @@ pub struct DbArticleForm {
// TODO: get rid of unnecessary methods
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))?)
}
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()?;
Ok(insert_into(article::table)
.values(form)
.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()?;
Ok(insert_into(article::table)
.values(&form)
@ -63,40 +63,48 @@ impl DbArticle {
.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()?;
Ok(diesel::update(article::dsl::article.find(id))
.set(article::dsl::text.eq(text))
.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()?;
Ok(diesel::update(article::dsl::article.find(id))
.set(article::dsl::protected.eq(locked))
.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()?;
Ok(diesel::update(article::dsl::article.find(id))
.set(article::dsl::approved.eq(approved))
.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()?;
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()?;
Ok(article::table
.find(id)
.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 query = article::table
.find(id)
@ -117,7 +125,7 @@ impl DbArticle {
title: &str,
domain: Option<String>,
context: &IbisContext,
) -> MyResult<DbArticleView> {
) -> BackendResult<DbArticleView> {
let mut conn = context.db_pool.get()?;
let (article, instance): (DbArticle, DbInstance) = {
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()?;
Ok(article::table
.filter(article::dsl::ap_id.eq(ap_id))
@ -155,7 +166,7 @@ impl DbArticle {
only_local: Option<bool>,
instance_id: Option<InstanceId>,
context: &IbisContext,
) -> MyResult<Vec<Self>> {
) -> BackendResult<Vec<Self>> {
let mut conn = context.db_pool.get()?;
let mut query = article::table
.inner_join(edit::table)
@ -175,7 +186,7 @@ impl DbArticle {
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 replaced = query
.replace('%', "\\%")
@ -191,7 +202,7 @@ impl DbArticle {
.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 latest_version: Option<EditVersion> = edit::table
.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 query = article::table
.group_by(article::dsl::id)

View file

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

View file

@ -5,7 +5,7 @@ use crate::{
IbisContext,
},
federation::activities::submit_article_update,
utils::{error::MyResult, generate_article_version},
utils::{error::BackendResult, generate_article_version},
},
common::{
article::{ApiConflict, DbArticle, DbEdit, EditVersion},
@ -57,14 +57,14 @@ pub struct DbConflictForm {
}
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()?;
Ok(insert_into(conflict::table)
.values(form)
.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()?;
Ok(conflict::table
.filter(conflict::dsl::creator_id.eq(person.id))
@ -72,7 +72,11 @@ impl DbConflict {
}
/// 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 conflict: Self = delete(
conflict::table
@ -92,7 +96,7 @@ impl DbConflict {
pub async fn to_api_conflict(
&self,
context: &Data<IbisContext>,
) -> MyResult<Option<ApiConflict>> {
) -> BackendResult<Option<ApiConflict>> {
let article = DbArticle::read_view(self.article_id, context)?;
// 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?;

View file

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

View file

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

View file

@ -1,5 +1,5 @@
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 std::ops::DerefMut;
@ -15,7 +15,7 @@ pub struct InstanceStats {
}
impl InstanceStats {
pub fn read(context: &IbisContext) -> MyResult<Self> {
pub fn read(context: &IbisContext) -> BackendResult<Self> {
let mut conn = context.db_pool.get()?;
Ok(instance_stats::table.find(1).get_result(conn.deref_mut())?)
}

View file

@ -1,6 +1,6 @@
use crate::backend::{
database::schema::jwt_secret,
utils::{config::IbisConfig, error::MyResult},
utils::{config::IbisConfig, error::BackendResult},
};
use diesel::{
r2d2::{ConnectionManager, Pool},
@ -27,7 +27,7 @@ pub struct IbisContext {
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()?;
Ok(jwt_secret::table
.select(jwt_secret::dsl::secret)

View file

@ -4,7 +4,7 @@ use crate::{
schema::{instance, instance_follow, local_user, person},
IbisContext,
},
utils::{error::MyResult, generate_keypair},
utils::{error::BackendResult, generate_keypair},
},
common::{
instance::DbInstance,
@ -51,7 +51,7 @@ pub struct DbPersonForm {
}
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()?;
Ok(insert_into(person::table)
.values(person_form)
@ -61,7 +61,7 @@ impl DbPerson {
.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()?;
Ok(person::table.find(id).get_result(conn.deref_mut())?)
}
@ -71,7 +71,7 @@ impl DbPerson {
password: String,
admin: bool,
context: &IbisContext,
) -> MyResult<LocalUserView> {
) -> BackendResult<LocalUserView> {
let mut conn = context.db_pool.get()?;
let domain = &context.config.federation.domain;
let ap_id = ObjectId::parse(&format!(
@ -116,7 +116,7 @@ impl DbPerson {
pub fn read_from_ap_id(
ap_id: &ObjectId<DbPerson>,
context: &Data<IbisContext>,
) -> MyResult<DbPerson> {
) -> BackendResult<DbPerson> {
let mut conn = context.db_pool.get()?;
Ok(person::table
.filter(person::dsl::ap_id.eq(ap_id))
@ -127,7 +127,7 @@ impl DbPerson {
username: &str,
domain: &Option<String>,
context: &Data<IbisContext>,
) -> MyResult<DbPerson> {
) -> BackendResult<DbPerson> {
let mut conn = context.db_pool.get()?;
let mut query = person::table
.filter(person::username.eq(username))
@ -144,7 +144,10 @@ impl DbPerson {
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()?;
diesel::update(person::table.find(params.person_id))
.set((
@ -155,7 +158,10 @@ impl DbPerson {
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 (person, local_user) = person::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};
let mut conn = context.db_pool.get()?;
Ok(instance_follow::table
@ -182,7 +188,7 @@ impl DbPerson {
}
/// 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 read = DbPerson::read_from_name(username, &None, context);
if read.is_ok() {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ use super::article_or_comment::DbArticleOrComment;
use crate::{
backend::{
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},
};
@ -39,7 +39,7 @@ pub struct ApubComment {
impl Object for DbComment {
type DataType = IbisContext;
type Kind = ApubComment;
type Error = Error;
type Error = BackendError;
async fn read_from_id(
object_id: Url,

View file

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

View file

@ -1,5 +1,9 @@
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},
};
use activitypub_federation::{
@ -30,7 +34,7 @@ impl Collection for DbEditCollection {
type Owner = DbArticle;
type DataType = IbisContext;
type Kind = ApubEditCollection;
type Error = Error;
type Error = BackendError;
async fn read_local(
article: &Self::Owner,

View file

@ -3,7 +3,7 @@ use crate::{
backend::{
database::{instance::DbInstanceForm, IbisContext},
federation::{objects::articles_collection::DbArticleCollection, send_activity},
utils::error::{Error, MyResult},
utils::error::{BackendError, BackendResult},
},
common::{instance::DbInstance, utils::extract_domain},
};
@ -37,11 +37,11 @@ pub struct ApubInstance {
}
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()))?)
}
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)?
.into_iter()
.map(|f| f.ap_id.into())
@ -57,7 +57,7 @@ impl DbInstance {
where
Activity: ActivityHandler + Serialize + Debug + Send + Sync,
<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)?
.iter()
@ -73,7 +73,7 @@ impl DbInstance {
impl Object for DbInstance {
type DataType = IbisContext;
type Kind = ApubInstance;
type Error = Error;
type Error = BackendError;
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
Some(self.last_refreshed_at)

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ use crate::{
backend::{
database::IbisContext,
federation::VerifyUrlData,
utils::{config::IbisConfig, error::MyResult, generate_activity_id},
utils::{config::IbisConfig, error::BackendResult, generate_activity_id},
},
common::instance::DbInstance,
};
@ -30,7 +30,7 @@ pub async fn start(
config: IbisConfig,
override_hostname: Option<SocketAddr>,
notify_start: Option<oneshot::Sender<()>>,
) -> MyResult<()> {
) -> BackendResult<()> {
let manager = ConnectionManager::<PgConnection>::new(&config.database.connection_url);
let db_pool = Pool::builder()
.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 axum::{
body::Body,
@ -19,7 +19,7 @@ use tower_http::services::ServeDir;
pub async fn file_and_error_handler(
State(options): State<LeptosOptions>,
request: Request<Body>,
) -> MyResult<Response<Body>> {
) -> BackendResult<Response<Body>> {
if cfg!(debug_assertions) {
// in debug mode serve assets directly from local folder
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::{
backend::{api::api_routes, federation::routes::federation_routes},
common::Auth,
@ -35,7 +35,7 @@ pub(super) async fn start_server(
context: FederationConfig<IbisContext>,
override_hostname: Option<SocketAddr>,
notify_start: Option<oneshot::Sender<()>>,
) -> MyResult<()> {
) -> BackendResult<()> {
let leptos_options = get_config_from_str(include_str!("../../../Cargo.toml"))?;
let mut addr = leptos_options.site_addr;
if let Some(override_hostname) = override_hostname {
@ -76,8 +76,10 @@ async fn leptos_routes_handler(
State(leptos_options): State<LeptosOptions>,
request: Request<Body>,
) -> Response {
let leptos_options_ = leptos_options.clone();
let handler = leptos_axum::render_app_async_with_context(
move || {
provide_context(leptos_options_.clone());
if let Some(auth) = &auth {
provide_context(auth.0.clone());
}

View file

@ -1,7 +1,7 @@
use crate::{
backend::{
database::{instance_stats::InstanceStats, IbisContext},
utils::error::MyResult,
utils::error::BackendResult,
},
common::utils::http_protocol_str,
};
@ -16,7 +16,9 @@ pub fn config() -> Router<()> {
.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 {
links: vec![NodeInfoWellKnownLinks {
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)?;
Ok(Json(NodeInfo {
version: "2.1".to_string(),

View file

@ -8,7 +8,7 @@ use crate::{
instance_collection::linked_instances_url,
},
},
utils::{error::Error, generate_keypair},
utils::{error::BackendError, generate_keypair},
},
common::{
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.
";
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 ap_id = ObjectId::parse(&format!("{}://{domain}", 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 doku::Document;
use serde::Deserialize;
@ -17,7 +17,7 @@ pub struct IbisConfig {
}
impl IbisConfig {
pub fn read() -> MyResult<Self> {
pub fn read() -> BackendResult<Self> {
let config = Config::builder()
.add_source(config::File::with_name("config.toml"))
// 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};
pub type MyResult<T> = Result<T, Error>;
pub type BackendResult<T> = Result<T, BackendError>;
#[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 {
std::fmt::Display::fmt(&self.0, f)
}
}
impl<T> From<T> for Error
impl<T> From<T> for BackendError
where
T: Into<anyhow::Error>,
{
fn from(t: T) -> Self {
Error(t.into())
BackendError(t.into())
}
}
#[cfg(feature = "ssr")]
impl axum::response::IntoResponse for Error {
impl axum::response::IntoResponse for BackendError {
fn into_response(self) -> axum::response::Response {
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,

View file

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

View file

@ -1,9 +1,9 @@
use super::error::MyResult;
use super::error::BackendResult;
use anyhow::anyhow;
use regex::Regex;
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)]
static TITLE_REGEX: LazyLock<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)
}
pub fn validate_user_name(name: &str) -> MyResult<()> {
pub fn validate_user_name(name: &str) -> BackendResult<()> {
#[allow(clippy::expect_used)]
static VALID_ACTOR_NAME_REGEX: LazyLock<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 name.contains('@') || name.len() < 3 || name.len() > 20 {
return Err(anyhow!("Invalid displayname").into());
@ -35,14 +35,14 @@ pub fn validate_display_name(name: &Option<String>) -> MyResult<()> {
Ok(())
}
pub fn validate_comment_max_depth(depth: i32) -> MyResult<()> {
pub fn validate_comment_max_depth(depth: i32) -> BackendResult<()> {
if depth > 50 {
return Err(anyhow!("Max comment depth reached").into());
}
Ok(())
}
pub fn validate_not_empty(text: &str) -> MyResult<()> {
pub fn validate_not_empty(text: &str) -> BackendResult<()> {
if text.trim().len() < 2 {
return Err(anyhow!("Empty text submitted").into());
}

View file

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

View file

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

View file

@ -1,59 +1,58 @@
use super::{result_to_option, ApiClient};
use crate::common::{
article::{DbArticle, SearchArticleParams},
instance::{
DbInstance,
FollowInstanceParams,
GetInstanceParams,
InstanceView,
SiteView,
UpdateInstanceParams,
use super::ApiClient;
use crate::{
common::{
article::{DbArticle, SearchArticleParams},
instance::{
DbInstance,
FollowInstanceParams,
GetInstanceParams,
InstanceView,
SiteView,
UpdateInstanceParams,
},
Notification,
ResolveObjectParams,
SuccessResponse,
},
Notification,
ResolveObjectParams,
SuccessResponse,
frontend::utils::errors::FrontendResult,
};
use http::Method;
use leptos::prelude::ServerFnError;
use url::Url;
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
}
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
}
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
}
pub async fn update_local_instance(
&self,
params: &UpdateInstanceParams,
) -> Result<DbInstance, ServerFnError> {
) -> FrontendResult<DbInstance> {
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::<()>)
.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::<()>)
.await
}
pub async fn search(
&self,
params: &SearchArticleParams,
) -> Result<Vec<DbArticle>, ServerFnError> {
pub async fn search(&self, params: &SearchArticleParams) -> FrontendResult<Vec<DbArticle>> {
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 };
self.send(
Method::GET,
@ -63,23 +62,26 @@ impl ApiClient {
.await
}
pub async fn follow_instance(&self, params: FollowInstanceParams) -> Option<SuccessResponse> {
result_to_option(self.post("/api/v1/instance/follow", Some(params)).await)
pub async fn follow_instance(
&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
}
#[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 log::error;
use url::Url;
let params = ResolveObjectParams {
id: Url::parse(&format!("{}://{}", http_protocol_str(), follow_instance))
.map_err(|e| error!("invalid url {e}"))
.ok()?,
id: Url::parse(&format!("{}://{}", http_protocol_str(), follow_instance))?,
};
let instance_resolved: DbInstance =
self.get("/api/v1/instance/resolve", Some(params)).await?;
@ -89,6 +91,6 @@ impl ApiClient {
id: instance_resolved.id,
};
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 leptos::{prelude::ServerFnError, server_fn::error::NoCustomError};
use log::{error, info};
use log::info;
use serde::{Deserialize, Serialize};
use std::{fmt::Debug, sync::LazyLock};
@ -9,58 +9,45 @@ pub mod comment;
pub mod instance;
pub mod user;
pub static CLIENT: LazyLock<ApiClient> = LazyLock::new(|| {
#[cfg(feature = "ssr")]
{
ApiClient::new(reqwest::Client::new(), None)
}
#[cfg(not(feature = "ssr"))]
{
ApiClient::new()
}
});
pub static CLIENT: LazyLock<ApiClient> = LazyLock::new(|| ApiClient::new(None));
#[derive(Clone, Debug)]
pub struct ApiClient {
#[cfg(feature = "ssr")]
client: reqwest::Client,
pub hostname: String,
ssl: bool,
#[cfg(feature = "ssr")]
test_hostname: Option<String>,
}
impl ApiClient {
#[cfg(feature = "ssr")]
pub fn new(client: reqwest::Client, hostname_: Option<String>) -> Self {
use leptos::config::get_config_from_str;
let leptos_options = get_config_from_str(include_str!("../../../Cargo.toml")).unwrap();
let mut hostname = leptos_options.site_addr.to_string();
// required for tests
if let Some(hostname_) = hostname_ {
hostname = hostname_;
pub fn new(#[allow(unused)] test_hostname: Option<String>) -> Self {
#[cfg(feature = "ssr")]
{
// need cookie store for auth in tests
let client = reqwest::ClientBuilder::new()
.cookie_store(true)
.build()
.expect("init reqwest");
Self {
client,
test_hostname,
}
}
Self {
client,
hostname,
ssl: false,
#[cfg(not(feature = "ssr"))]
{
Self {}
}
}
#[cfg(not(feature = "ssr"))]
pub fn new() -> Self {
use leptos_use::use_document;
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
T: for<'de> Deserialize<'de>,
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
T: for<'de> Deserialize<'de>,
R: Serialize + Debug,
@ -68,7 +55,7 @@ impl ApiClient {
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
T: for<'de> Deserialize<'de>,
R: Serialize + Debug,
@ -77,12 +64,7 @@ impl ApiClient {
}
#[cfg(feature = "ssr")]
async fn send<P, T>(
&self,
method: Method,
path: &str,
params: Option<P>,
) -> Result<T, ServerFnError>
async fn send<P, T>(&self, method: Method, path: &str, params: Option<P>) -> FrontendResult<T>
where
P: Serialize + Debug,
T: for<'de> Deserialize<'de>,
@ -90,9 +72,10 @@ impl ApiClient {
use crate::common::{Auth, AUTH_COOKIE};
use leptos::prelude::use_context;
use reqwest::header::HeaderName;
let mut req = self
.client
.request(method.clone(), self.request_endpoint(path));
.request(method.clone(), self.request_endpoint(path)?);
req = if method == Method::GET {
req.query(&params)
} else {
@ -115,7 +98,7 @@ impl ApiClient {
method: Method,
path: &'a str,
params: Option<P>,
) -> impl std::future::Future<Output = Result<T, ServerFnError>> + Send + 'a
) -> impl std::future::Future<Output = FrontendResult<T>> + Send + 'a
where
P: Serialize + Debug + 'a,
T: for<'de> Deserialize<'de>,
@ -136,8 +119,8 @@ impl ApiClient {
}
});
let path_with_endpoint = self.request_endpoint(path);
let params_encoded = serde_urlencoded::to_string(&params).unwrap();
let path_with_endpoint = self.request_endpoint(path)?;
let params_encoded = serde_urlencoded::to_string(&params)?;
let path = if method == Method::GET {
// Cannot pass the form data directly but need to convert it manually
// https://github.com/rustwasm/gloo/issues/378
@ -156,8 +139,7 @@ impl ApiClient {
.body(params_encoded)
} else {
builder.build()
}
.unwrap();
}?;
let res = req.send().await?;
let status = res.status();
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
T: for<'de> Deserialize<'de>,
{
let json = serde_json::from_str(&text).map_err(|e| {
info!("Failed to deserialize api response: {e} from {text} on {url}");
ServerFnError::<NoCustomError>::Deserialization(text.clone())
FrontendError::new(&text)
})?;
if status == StatusCode::OK {
Ok(json)
} else {
info!("API error: {text} on {url} status {status}");
Err(ServerFnError::Response(text))
Err(FrontendError::new(text))
}
}
fn request_endpoint(&self, path: &str) -> String {
let protocol = if self.ssl { "https" } else { "http" };
format!("{protocol}://{}{path}", &self.hostname)
}
}
fn request_endpoint(&self, path: &str) -> FrontendResult<String> {
let protocol = if cfg!(debug_assertions) {
"http"
} else {
"https"
};
fn result_to_option<T>(val: Result<T, ServerFnError>) -> Option<T> {
match val {
Ok(v) => Some(v),
Err(e) => {
error!("API error: {e}");
None
let hostname: String;
#[cfg(feature = "ssr")]
{
use leptos::{config::LeptosOptions, prelude::use_context};
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,47 +1,46 @@
use super::{result_to_option, ApiClient};
use crate::common::{
article::{EditView, GetEditList},
newtypes::PersonId,
user::{
DbPerson,
GetUserParams,
LocalUserView,
LoginUserParams,
RegisterUserParams,
UpdateUserParams,
use super::ApiClient;
use crate::{
common::{
article::{EditView, GetEditList},
newtypes::PersonId,
user::{
DbPerson,
GetUserParams,
LocalUserView,
LoginUserParams,
RegisterUserParams,
UpdateUserParams,
},
SuccessResponse,
},
SuccessResponse,
frontend::utils::errors::FrontendResult,
};
use leptos::prelude::ServerFnError;
impl ApiClient {
pub async fn register(
&self,
params: RegisterUserParams,
) -> Result<LocalUserView, ServerFnError> {
pub async fn register(&self, params: RegisterUserParams) -> FrontendResult<LocalUserView> {
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
}
pub async fn logout(&self) -> Option<SuccessResponse> {
result_to_option(self.post("/api/v1/account/logout", None::<()>).await)
pub async fn logout(&self) -> FrontendResult<SuccessResponse> {
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
}
pub async fn update_user_profile(
&self,
data: UpdateUserParams,
) -> Result<SuccessResponse, ServerFnError> {
) -> FrontendResult<SuccessResponse> {
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 {
person_id: Some(person_id),
..Default::default()

View file

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

View file

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

View file

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

View file

@ -4,7 +4,10 @@ use crate::{
comment::{CreateCommentParams, DbComment, EditCommentParams},
newtypes::CommentId,
},
frontend::api::CLIENT,
frontend::{
api::CLIENT,
utils::errors::{FrontendResult, FrontendResultExt},
},
};
use leptos::{html::Textarea, prelude::*};
use leptos_use::{use_textarea_autosize, UseTextareaAutosizeReturn};
@ -18,7 +21,7 @@ pub struct EditParams {
#[component]
pub fn CommentEditorView(
article: Resource<DbArticleView>,
article: Resource<FrontendResult<DbArticleView>>,
#[prop(optional)] parent_id: Option<CommentId>,
/// Set this to CommentId(-1) to hide all editors
#[prop(optional)]
@ -47,20 +50,22 @@ pub fn CommentEditorView(
content: Some(content.get_untracked()),
deleted: None,
};
let comment = CLIENT.edit_comment(&params).await.unwrap();
edit_params.set_comment.set(comment.comment);
edit_params.set_is_editing.set(false);
CLIENT.edit_comment(&params).await.error_popup(|comment| {
edit_params.set_comment.set(comment.comment);
edit_params.set_is_editing.set(false);
});
} else {
let params = CreateCommentParams {
content: content.get_untracked(),
article_id: article.await.article.id,
article_id: article.await.map(|a| a.article.id).unwrap_or_default(),
parent_id,
};
CLIENT.create_comment(&params).await.unwrap();
article.refetch();
if let Some(set_show_editor) = set_show_editor {
set_show_editor.set(CommentId(-1));
}
CLIENT.create_comment(&params).await.error_popup(|_| {
article.refetch();
if let Some(set_show_editor) = set_show_editor {
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 leptos::prelude::*;
use std::fmt::Debug;
@ -17,10 +17,9 @@ where
{
let connect_ibis_wiki = Action::new(move |_: &()| async move {
CLIENT
.resolve_instance(Url::parse("https://ibis.wiki").unwrap())
.resolve_instance(Url::parse("https://ibis.wiki").expect("parse ibis.wiki url"))
.await
.unwrap();
res.refetch();
.error_popup(|_| res.refetch());
});
view! {

View file

@ -1,4 +1,4 @@
use leptos::{ev::KeyboardEvent, prelude::*};
use leptos::prelude::*;
#[component]
pub fn CredentialsForm(
@ -33,15 +33,8 @@ pub fn CredentialsForm(
class="input input-primary input-bordered"
required
placeholder="Username"
bind:value=(username, set_username)
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>
<input
@ -50,21 +43,7 @@ pub fn CredentialsForm(
required
placeholder="Password"
prop:disabled=move || disabled.get()
on:keyup=move |ev: KeyboardEvent| {
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);
}
bind:value=(password, set_password)
/>
<div>

View file

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

View file

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

View file

@ -2,8 +2,9 @@ use crate::frontend::{
api::CLIENT,
utils::{
dark_mode::DarkMode,
errors::FrontendResultExt,
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, *};
@ -12,17 +13,13 @@ use leptos_router::hooks::use_navigate;
#[component]
pub fn Nav() -> impl IntoView {
let logout_action = Action::new(move |_| async move {
CLIENT.logout().await.unwrap();
site().refetch();
CLIENT.logout().await.error_popup(|_| site().refetch());
});
let notification_count = Resource::new(
|| (),
move |_| async move { CLIENT.notifications_count().await.unwrap_or_default() },
);
let instance = Resource::new(
|| (),
|_| async move { CLIENT.get_local_instance().await.unwrap() },
);
let instance = Resource::new(|| (), |_| async move { CLIENT.get_local_instance().await });
let (search_query, set_search_query) = signal(String::new());
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" />
</a>
<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>
<ul>
<li>
<a href="/">"Main Page"</a>
</li>
<li>
<a href="/instances">"Instances"</a>
</li>
<li>
<a href="/articles">"Articles"</a>
<a href="/explore">"Explore"</a>
</li>
<Show when=is_logged_in>
<li>
@ -109,9 +105,7 @@ pub fn Nav() -> impl IntoView {
<li>
<a href="/login">"Login"</a>
</li>
<Show when=move || {
site().with_default(|s| s.config.registration_open)
}>
<Show when=move || config().registration_open>
<li>
<a href="/register">"Register"</a>
</li>
@ -120,29 +114,31 @@ pub fn Nav() -> impl IntoView {
}
>
{
let my_profile = site()
.with_default(|site| site.clone().my_profile.unwrap());
let profile_link = format!("/user/{}", my_profile.person.username);
view! {
<p class="self-center">
"Logged in as " <a class="link" href=profile_link>
{my_profile.person.username}
{my_profile()
.map(|my_profile| {
let profile_link = format!(
"/user/{}",
my_profile.person.username,
);
view! {
<p class="self-center">
"Logged in as " <a class="link" href=profile_link>
{my_profile.person.username}
</a>
</p>
<a class="self-center py-2 link" href="/edit_profile">
Edit Profile
</a>
</p>
<a class="self-center py-2 link" href="/edit_profile">
Edit Profile
</a>
<button
class="self-center w-min btn btn-outline btn-xs"
on:click=move |_| {
logout_action.dispatch(());
}
>
Logout
</button>
}
}
<button
class="self-center w-min btn btn-outline btn-xs"
on:click=move |_| {
logout_action.dispatch(());
}
>
Logout
</button>
}
})}
</Show>
<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 markdown_it::{
plugins::cmark::block::{heading::ATXHeading, lheading::SetextHeader},

View file

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

View file

@ -3,17 +3,24 @@ use crate::{
frontend::{
api::CLIENT,
components::article_editor::EditorView,
utils::resources::{is_admin, site, DefaultResource},
utils::resources::{config, is_admin},
},
};
use leptos::{html::Textarea, prelude::*};
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};
#[component]
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 UseTextareaAutosizeReturn {
content,
@ -52,9 +59,7 @@ pub fn CreateArticle() -> impl IntoView {
}
}
});
let show_approval_message = Signal::derive(move || {
site().with_default(|site| site.config.article_approval) && !is_admin()
});
let show_approval_message = Signal::derive(move || config().article_approval && !is_admin());
view! {
<Title text="Create new Article" />
@ -76,6 +81,7 @@ pub fn CreateArticle() -> impl IntoView {
type="text"
required
placeholder="Title"
value=title
prop:disabled=move || wait_for_response.get()
on:keyup=move |ev| {
let val = event_target_value(&ev);

View file

@ -1,9 +1,12 @@
use crate::frontend::{
components::article_nav::{ActiveTab, ArticleNav},
components::{
article_nav::{ActiveTab, ArticleNav},
suspense_error::SuspenseError,
},
pages::{article_edits_resource, article_resource},
utils::formatting::{article_title, render_date_time, user_link},
};
use leptos::prelude::*;
use leptos::{either::Either, prelude::*};
use leptos_meta::Title;
use leptos_router::hooks::use_params_map;
@ -11,46 +14,62 @@ use leptos_router::hooks::use_params_map;
pub fn EditDiff() -> impl IntoView {
let params = use_params_map();
let article = article_resource();
let edits = article_edits_resource(article);
view! {
<ArticleNav article=article active_tab=ActiveTab::History />
<Suspense fallback=|| {
view! { "Loading..." }
}>
<SuspenseError result=article>
{move || Suspend::new(async move {
let edits = article_edits_resource(article).await;
let hash = params.get_untracked().get("hash").clone().unwrap();
let edit = edits.iter().find(|e| e.edit.hash.0.to_string() == hash).unwrap();
let label = format!(
"{} ({})",
edit.edit.summary,
render_date_time(edit.edit.published),
);
let pending = edit.edit.pending;
let title = format!(
"Diff {} — {}",
edit.edit.summary,
article_title(&article.await.article),
);
view! {
<Title text=title />
<div class="flex w-full">
<h2 class="my-2 font-serif text-xl font-bold grow">{label}</h2>
<Show when=move || pending>
<span class="p-1 w-min rounded border-2 border-rose-300 h-min">
Pending
</span>
</Show>
</div>
<p>"by " {user_link(&edit.creator)}</p>
<div class="max-w-full prose prose-slate">
<pre class="text-wrap">
<code>{edit.edit.diff.clone()}</code>
</pre>
</div>
}
let article_title = article
.await
.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!(
"{} ({})",
edit.edit.summary,
render_date_time(edit.edit.published),
);
let pending = edit.edit.pending;
let title = format!("Diff {}{}", edit.edit.summary, article_title);
Either::Left(
view! {
<Title text=title />
<div class="flex w-full">
<h2 class="my-2 font-serif text-xl font-bold grow">
{label}
</h2>
<Show when=move || pending>
<span class="p-1 w-min rounded border-2 border-rose-300 h-min">
Pending
</span>
</Show>
</div>
<p>"by " {user_link(&edit.creator)}</p>
<div class="max-w-full prose prose-slate">
<pre class="text-wrap">
<code>{edit.edit.diff.clone()}</code>
</pre>
</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},
comment::CommentView,
comment_editor::CommentEditorView,
suspense_error::SuspenseError,
},
pages::article_resource,
},
@ -20,20 +21,28 @@ pub fn ArticleDiscussion() -> impl IntoView {
view! {
<ArticleNav article=article active_tab=ActiveTab::Discussion />
<Suspense fallback=|| view! { "Loading..." }>
<CommentEditorView article=article />
<div>
<For
each=move || {
article.get().map(|a| build_comments_tree(a.comments)).unwrap_or_default()
}
key=|comment| comment.comment.id
children=move |comment: DbCommentView| {
view! { <CommentView article comment show_editor /> }
}
/>
</div>
</Suspense>
<SuspenseError result=article>
{move || Suspend::new(async move {
let article2 = article.await;
view! {
<CommentEditorView article=article />
<div>
<For
each=move || {
article2
.clone()
.map(|a| build_comments_tree(a.comments))
.unwrap_or_default()
}
key=|comment| comment.comment.id
children=move |comment: DbCommentView| {
view! { <CommentView article comment show_editor /> }
}
/>
</div>
}
})}
</SuspenseError>
}
}
@ -66,19 +75,24 @@ fn build_comments_tree(comments: Vec<DbCommentView>) -> Vec<DbCommentView> {
.iter()
.map(|v| (v.comment.id, CommentNode::new(v.clone())))
.collect();
debug_assert!(comments.len() == map.len());
// Move top-level comments directly into tree vec. For comments having parent_id, move them
// `children` of respective parent. This preserves existing order.
let mut tree = Vec::<CommentNode>::new();
for view in comments {
let child = map.get(&view.comment.id).unwrap().clone();
for view in &comments {
let child = map
.get(&view.comment.id)
.expect("get comment by id")
.clone();
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);
} else {
tree.push(child);
}
}
debug_assert!(comments.len() == map.len());
// Now convert it back to flat array with correct order for rendering
tree.into_iter().flat_map(|t| t.flatten()).collect()

View file

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

View file

@ -2,6 +2,7 @@ use crate::frontend::{
components::{
article_nav::{ActiveTab, ArticleNav},
edit_list::EditList,
suspense_error::SuspenseError,
},
pages::{article_edits_resource, article_resource},
};
@ -10,20 +11,22 @@ use leptos::prelude::*;
#[component]
pub fn ArticleHistory() -> impl IntoView {
let article = article_resource();
let edits = article_edits_resource(article);
view! {
<ArticleNav article=article active_tab=ActiveTab::History />
<Suspense fallback=|| {
view! { "Loading..." }
}>
{move || {
article_edits_resource(article)
.get()
<SuspenseError result=article>
{move || Suspend::new(async move {
edits
.await
.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 edit;
pub mod history;
pub mod list;
pub mod read;

View file

@ -1,9 +1,12 @@
use crate::frontend::{
components::article_nav::{ActiveTab, ArticleNav},
components::{
article_nav::{ActiveTab, ArticleNav},
suspense_error::SuspenseError,
},
markdown::render_article_markdown,
pages::article_resource,
};
use leptos::prelude::*;
use leptos::{either::Either, prelude::*};
use leptos_router::hooks::use_query_map;
#[component]
@ -14,26 +17,24 @@ pub fn ReadArticle() -> impl IntoView {
view! {
<ArticleNav article=article active_tab=ActiveTab::Read />
<Suspense fallback=|| {
view! { "Loading..." }
}>
{move || {
article
.get()
.map(|article| {
<SuspenseError result=article>
{move || Suspend::new(async move {
let article = article.await;
let markdown = article.map(|a| render_article_markdown(&a.article.text));
if let Ok(markdown) = markdown {
Either::Right(
view! {
<div
class="max-w-full prose prose-slate"
inner_html=render_article_markdown(&article.article.text)
></div>
}
})
}} <Show when=move || edit_successful>
<div class="max-w-full prose prose-slate" inner_html=markdown></div>
},
)
} else {
Either::Left(markdown)
}
})} <Show when=move || edit_successful>
<div class="toast toast-center">
<div class="alert alert-success">Edit successful</div>
</div>
</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,12 +2,15 @@ use crate::{
common::{article::ListArticlesParams, instance::DbInstance, utils::http_protocol_str},
frontend::{
api::CLIENT,
components::instance_follow_button::InstanceFollowButton,
utils::formatting::{
article_path,
article_title,
instance_title_with_domain,
instance_updated,
components::{instance_follow_button::InstanceFollowButton, suspense_error::SuspenseError},
utils::{
errors::FrontendError,
formatting::{
article_path,
article_title,
instance_title_with_domain,
instance_updated,
},
},
},
};
@ -19,19 +22,18 @@ use url::Url;
#[component]
pub fn InstanceDetails() -> impl IntoView {
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 url = Url::parse(&format!("{}://{hostname}", http_protocol_str())).unwrap();
CLIENT.resolve_instance(url).await.unwrap()
let hostname = hostname.ok_or(FrontendError::new("No instance given"))?;
let url = Url::parse(&format!("{}://{hostname}", http_protocol_str()))?;
CLIENT.resolve_instance(url).await
});
view! {
<Suspense fallback=|| {
view! { "Loading..." }
}>
{move || {
<SuspenseError result=instance_profile>
{move || Suspend::new(async move {
instance_profile
.get()
.await
.map(|instance: DbInstance| {
let articles = Resource::new(
move || instance.id,
@ -42,7 +44,6 @@ pub fn InstanceDetails() -> impl IntoView {
instance_id: Some(instance_id),
})
.await
.unwrap()
},
);
let title = instance_title_with_domain(&instance);
@ -60,10 +61,10 @@ pub fn InstanceDetails() -> impl IntoView {
<div>{instance.topic}</div>
<h2 class="font-serif text-xl font-bold">Articles</h2>
<ul class="list-none">
<Suspense>
{move || {
<SuspenseError result=articles>
{move || Suspend::new(async move {
articles
.get()
.await
.map(|a| {
a.into_iter()
.map(|a| {
@ -77,14 +78,14 @@ pub fn InstanceDetails() -> impl IntoView {
})
.collect::<Vec<_>>()
})
}}
</Suspense>
})}
</SuspenseError>
</ul>
</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 list;
pub mod search;
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_meta::Title;
#[component]
pub fn InstanceSettings() -> impl IntoView {
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.unwrap() },
);
let instance = Resource::new(|| (), |_| async move { CLIENT.get_local_instance().await });
let submit_action = Action::new(move |params: &UpdateInstanceParams| {
let params = params.clone();
async move {
let result = CLIENT.update_local_instance(&params).await;
match result {
Ok(_res) => {
CLIENT
.update_local_instance(&params)
.await
.error_popup(|_| {
instance.refetch();
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,79 +31,65 @@ pub fn InstanceSettings() -> impl IntoView {
// that completely breaks reactivity.
view! {
<Title text="Instance Settings" />
<Suspense fallback=|| {
view! { "Loading..." }
}>
<SuspenseError result=instance>
{move || Suspend::new(async move {
let instance = instance.await;
let (name, set_name) = signal(instance.instance.name.unwrap_or_default());
let (topic, set_topic) = signal(instance.instance.topic.unwrap_or_default());
view! {
<h1 class="flex-auto my-6 font-serif text-4xl font-bold grow">
"Instance Settings"
</h1>
{move || {
submit_error
.get()
.map(|err| {
view! { <p class="alert alert-error">{err}</p> }
})
}}
<div class="flex flex-row mb-2">
<label class="block w-20" for="name">
Name
</label>
<input
type="text"
id="name"
class="w-80 input input-secondary input-bordered"
prop:value=name
value=name
on:change=move |ev| {
let val = event_target_value(&ev);
set_name.set(val);
}
/>
</div>
<div class="flex flex-row mb-2">
<label class="block w-20" for="topic">
"Topic"
</label>
<input
type="text"
id="name"
class="w-80 input input-secondary input-bordered"
prop:value=topic
value=topic
on:change=move |ev| {
let val = event_target_value(&ev);
set_topic.set(val);
}
/>
</div>
<button
class="btn btn-primary"
on:click=move |_| {
let form = UpdateInstanceParams {
name: Some(name.get()),
topic: Some(topic.get()),
};
submit_action.dispatch(form);
}
>
Submit
</button>
<Show when=move || saved.get()>
<div class="toast">
<div class="alert alert-info">
<span>Saved!</span>
instance
.await
.map(|instance| {
let (name, set_name) = signal(instance.instance.name.unwrap_or_default());
let (topic, set_topic) = signal(
instance.instance.topic.unwrap_or_default(),
);
view! {
<h1 class="flex-auto my-6 font-serif text-4xl font-bold grow">
"Instance Settings"
</h1>
<div class="flex flex-row mb-2">
<label class="block w-20" for="name">
Name
</label>
<input
type="text"
id="name"
class="w-80 input input-secondary input-bordered"
bind:value=(name, set_name)
/>
</div>
</div>
</Show>
}
<div class="flex flex-row mb-2">
<label class="block w-20" for="topic">
"Topic"
</label>
<input
type="text"
id="name"
class="w-80 input input-secondary input-bordered"
bind:value=(topic, set_topic)
/>
</div>
<button
class="btn btn-primary"
on:click=move |_| {
let form = UpdateInstanceParams {
name: Some(name.get()),
topic: Some(topic.get()),
};
submit_action.dispatch(form);
}
>
Submit
</button>
<Show when=move || saved.get()>
<div class="toast">
<div class="alert alert-info">
<span>Saved!</span>
</div>
</div>
</Show>
}
})
})}
</Suspense>
</SuspenseError>
}
}

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ use crate::{
common::user::GetUserParams,
frontend::{
api::CLIENT,
components::edit_list::EditList,
components::{edit_list::EditList, suspense_error::SuspenseError},
markdown::render_article_markdown,
utils::formatting::user_title,
},
@ -15,59 +15,52 @@ use leptos_router::hooks::use_params_map;
pub fn UserProfile() -> impl IntoView {
let params = use_params_map();
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 {
set_error.set(None);
let mut domain = None;
if let Some((title_, domain_)) = name.clone().split_once('@') {
name = title_.to_string();
domain = Some(domain_.to_string());
}
let params = GetUserParams { name, domain };
CLIENT.get_user(params).await.unwrap()
CLIENT.get_user(params).await
});
let edits = Resource::new(
move || user_profile.get(),
move |_| async move {
CLIENT
.get_person_edits(user_profile.await.id)
.await
.unwrap_or_default()
},
);
view! {
{move || {
error
.get()
.map(|err| {
view! { <p style="color:red;">{err}</p> }
})
}}
<Suspense fallback=|| {
view! { "Loading..." }
}>
<SuspenseError result=user_profile>
{move || Suspend::new(async move {
let edits = edits.await;
let person = user_profile.await;
view! {
<Title text=user_title(&person) />
<h1 class="flex-auto my-6 font-serif text-4xl font-bold grow">
{user_title(&person)}
</h1>
let edits = Resource::new(
move || user_profile.get(),
move |_| async move { CLIENT.get_person_edits(user_profile.await?.id).await },
);
user_profile
.await
.map(|person| {
view! {
<Title text=user_title(&person) />
<h1 class="flex-auto my-6 font-serif text-4xl font-bold grow">
{user_title(&person)}
</h1>
<div
class="mb-2 max-w-full prose prose-slate"
inner_html=render_article_markdown(&person.bio.unwrap_or_default())
></div>
<div
class="mb-2 max-w-full prose prose-slate"
inner_html=render_article_markdown(&person.bio.unwrap_or_default())
></div>
<h2 class="font-serif text-xl font-bold">Edits</h2>
<EditList edits=edits for_article=false />
}
<SuspenseError result=user_profile>
{move || Suspend::new(async move {
edits
.await
.map(|edits| {
view! {
<h2 class="font-serif text-xl font-bold">Edits</h2>
<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};
pub mod dark_mode;
pub mod errors;
pub mod formatting;
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::*;
pub fn site() -> Resource<SiteView> {
use_context::<Resource<SiteView>>().unwrap()
type SiteResource = Resource<FrontendResult<SiteView>>;
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 {
site().with_default(|site| site.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;
my_profile().is_some()
}
impl<T: Default + Send + Sync + Clone> DefaultResource<T> for Resource<T> {
fn with_default<O>(&self, f: impl FnOnce(&T) -> O) -> O {
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(),
}
}
pub fn is_admin() -> bool {
my_profile().map(|p| p.local_user.admin).unwrap_or(false)
}

View file

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

View file

@ -1,6 +1,6 @@
#[cfg(feature = "ssr")]
#[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 log::LevelFilter;

View file

@ -9,7 +9,6 @@ use ibis::{
common::{instance::Options, user::RegisterUserParams},
frontend::api::ApiClient,
};
use reqwest::ClientBuilder;
use std::{
env::current_dir,
fs::{create_dir_all, remove_dir_all},
@ -90,6 +89,7 @@ pub struct IbisInstance {
pub api_client: ApiClient,
db_path: String,
db_handle: JoinHandle<()>,
pub hostname: String,
}
impl IbisInstance {
@ -110,15 +110,15 @@ impl IbisInstance {
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 hostname = format!("127.0.0.1:{port}");
let domain = format!("localhost:{port}");
let hostname = format!("localhost:{port}");
let config = IbisConfig {
database: IbisConfigDatabase {
connection_url,
..Default::default()
},
federation: IbisConfigFederation {
domain: domain.clone(),
domain: hostname.clone(),
..Default::default()
},
options: Options {
@ -127,10 +127,10 @@ impl IbisInstance {
},
..Default::default()
};
let client = ClientBuilder::new().cookie_store(true).build().unwrap();
let api_client = ApiClient::new(client, Some(domain));
let api_client = ApiClient::new(Some(hostname.clone()));
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))
.await
.unwrap();
@ -145,7 +145,8 @@ impl IbisInstance {
Self {
api_client,
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
let not_found = beta.get_article(get_article_data.clone()).await;
assert!(not_found.is_none());
assert!(not_found.is_err());
// edit article
let edit_params = EditArticleParams {
@ -185,7 +185,7 @@ async fn test_synchronize_articles() -> Result<()> {
// try to read remote article by name, fails without domain
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
let get_res = RetryFuture::new(
@ -197,11 +197,11 @@ async fn test_synchronize_articles() -> Result<()> {
};
let res = beta.get_article(get_article_data).await;
match res {
None => Err(RetryPolicy::<String>::Retry(None)),
Some(a) if a.latest_version != edit_res.latest_version => {
Err(_) => Err(RetryPolicy::<String>::Retry(None)),
Ok(a) if a.latest_version != edit_res.latest_version => {
Err(RetryPolicy::Retry(None))
}
Some(a) => Ok(a),
Ok(a) => Ok(a),
}
},
LinearRetryStrategy::new(),
@ -805,9 +805,9 @@ async fn test_synchronize_instances() -> Result<()> {
|| async {
let res = gamma.list_instances().await;
match res {
None => Err(RetryPolicy::<String>::Retry(None)),
Some(i) if i.len() < 3 => Err(RetryPolicy::Retry(None)),
Some(i) => Ok(i),
Err(_) => Err(RetryPolicy::<String>::Retry(None)),
Ok(i) if i.len() < 3 => Err(RetryPolicy::Retry(None)),
Ok(i) => Ok(i),
}
},
LinearRetryStrategy::new(),