Avoid panics (#392)

- Use `Result` as much as possible
- Display errors instead of panicking

TODO (maybe in another PR? this one is already quite big):
- Find a way to merge Ructe/ErrorPage types, so that we can have routes returning `Result<X, ErrorPage>` instead of panicking when we have an `Error`
- Display more details about the error, to make it easier to debug

(sorry, this isn't going to be fun to review, the diff is huge, but it is always the same changes)
This commit is contained in:
Baptiste Gelez
2018-12-29 09:36:07 +01:00
committed by GitHub
parent 4059a840be
commit 80a4dae8bd
50 changed files with 1879 additions and 1990 deletions
+87 -79
View File
@@ -21,20 +21,19 @@ use plume_models::{
tags::*,
users::User
};
use routes::comments::NewCommentForm;
use routes::{errors::ErrorPage, comments::NewCommentForm};
use template_utils::Ructe;
use Worker;
use Searcher;
#[get("/~/<blog>/<slug>?<responding_to>", rank = 4)]
pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, responding_to: Option<i32>, intl: I18n) -> Result<Ructe, Ructe> {
let blog = Blog::find_by_fqn(&*conn, &blog).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, user.clone()))))?;
let post = Post::find_by_slug(&*conn, &slug, blog.id).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, user.clone()))))?;
if post.published || post.get_authors(&*conn).into_iter().any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)) {
let comments = CommentTree::from_post(&*conn, &post, user.as_ref());
pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, responding_to: Option<i32>, intl: I18n) -> Result<Ructe, ErrorPage> {
let blog = Blog::find_by_fqn(&*conn, &blog)?;
let post = Post::find_by_slug(&*conn, &slug, blog.id)?;
if post.published || post.get_authors(&*conn)?.into_iter().any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)) {
let comments = CommentTree::from_post(&*conn, &post, user.as_ref())?;
let previous = responding_to.map(|r| Comment::get(&*conn, r)
.expect("posts::details_reponse: Error retrieving previous comment"));
let previous = responding_to.and_then(|r| Comment::get(&*conn, r).ok());
Ok(render!(posts::details(
&(&*conn, &intl.catalog, user.clone()),
@@ -42,14 +41,14 @@ pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, res
blog,
&NewCommentForm {
warning: previous.clone().map(|p| p.spoiler_text).unwrap_or_default(),
content: previous.clone().map(|p| format!(
content: previous.clone().and_then(|p| Some(format!(
"@{} {}",
p.get_author(&*conn).get_fqn(&*conn),
Mention::list_for_comment(&*conn, p.id)
p.get_author(&*conn).ok()?.get_fqn(&*conn),
Mention::list_for_comment(&*conn, p.id).ok()?
.into_iter()
.filter_map(|m| {
let user = user.clone();
if let Some(mentioned) = m.get_mentioned(&*conn) {
if let Ok(mentioned) = m.get_mentioned(&*conn) {
if user.is_none() || mentioned.id != user.expect("posts::details_response: user error while listing mentions").id {
Some(format!("@{}", mentioned.get_fqn(&*conn)))
} else {
@@ -59,22 +58,22 @@ pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, res
None
}
}).collect::<Vec<String>>().join(" "))
).unwrap_or_default(),
)).unwrap_or_default(),
..NewCommentForm::default()
},
ValidationErrors::default(),
Tag::for_post(&*conn, post.id),
Tag::for_post(&*conn, post.id)?,
comments,
previous,
post.count_likes(&*conn),
post.count_reshares(&*conn),
user.clone().map(|u| u.has_liked(&*conn, &post)).unwrap_or(false),
user.clone().map(|u| u.has_reshared(&*conn, &post)).unwrap_or(false),
user.map(|u| u.is_following(&*conn, post.get_authors(&*conn)[0].id)).unwrap_or(false),
post.get_authors(&*conn)[0].clone()
post.count_likes(&*conn)?,
post.count_reshares(&*conn)?,
user.clone().and_then(|u| u.has_liked(&*conn, &post).ok()).unwrap_or(false),
user.clone().and_then(|u| u.has_reshared(&*conn, &post).ok()).unwrap_or(false),
user.and_then(|u| u.is_following(&*conn, post.get_authors(&*conn).ok()?[0].id).ok()).unwrap_or(false),
post.get_authors(&*conn)?[0].clone()
)))
} else {
Err(render!(errors::not_authorized(
Ok(render!(errors::not_authorized(
&(&*conn, &intl.catalog, user.clone()),
"This post isn't published yet."
)))
@@ -83,10 +82,10 @@ pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, res
#[get("/~/<blog>/<slug>", rank = 3)]
pub fn activity_details(blog: String, slug: String, conn: DbConn, _ap: ApRequest) -> Result<ActivityStream<LicensedArticle>, Option<String>> {
let blog = Blog::find_by_fqn(&*conn, &blog).ok_or(None)?;
let post = Post::find_by_slug(&*conn, &slug, blog.id).ok_or(None)?;
let blog = Blog::find_by_fqn(&*conn, &blog).map_err(|_| None)?;
let post = Post::find_by_slug(&*conn, &slug, blog.id).map_err(|_| None)?;
if post.published {
Ok(ActivityStream::new(post.to_activity(&*conn)))
Ok(ActivityStream::new(post.to_activity(&*conn).map_err(|_| String::from("Post serialization error"))?))
} else {
Err(Some(String::from("Not published yet.")))
}
@@ -101,23 +100,23 @@ pub fn new_auth(blog: String, i18n: I18n) -> Flash<Redirect> {
}
#[get("/~/<blog>/new", rank = 1)]
pub fn new(blog: String, user: User, conn: DbConn, intl: I18n) -> Option<Ructe> {
pub fn new(blog: String, user: User, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
let b = Blog::find_by_fqn(&*conn, &blog)?;
if !user.is_author_in(&*conn, &b) {
if !user.is_author_in(&*conn, &b)? {
// TODO actually return 403 error code
Some(render!(errors::not_authorized(
Ok(render!(errors::not_authorized(
&(&*conn, &intl.catalog, Some(user)),
"You are not author in this blog."
)))
} else {
let medias = Media::for_user(&*conn, user.id);
Some(render!(posts::new(
let medias = Media::for_user(&*conn, user.id)?;
Ok(render!(posts::new(
&(&*conn, &intl.catalog, Some(user)),
b,
false,
&NewPostForm {
license: Instance::get_local(&*conn).map(|i| i.default_license).unwrap_or_else(||String::from("CC-BY-SA")),
license: Instance::get_local(&*conn)?.default_license,
..NewPostForm::default()
},
true,
@@ -129,12 +128,12 @@ pub fn new(blog: String, user: User, conn: DbConn, intl: I18n) -> Option<Ructe>
}
#[get("/~/<blog>/<slug>/edit")]
pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) -> Option<Ructe> {
pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
let b = Blog::find_by_fqn(&*conn, &blog)?;
let post = Post::find_by_slug(&*conn, &slug, b.id)?;
if !user.is_author_in(&*conn, &b) {
Some(render!(errors::not_authorized(
if !user.is_author_in(&*conn, &b)? {
Ok(render!(errors::not_authorized(
&(&*conn, &intl.catalog, Some(user)),
"You are not author in this blog."
)))
@@ -145,8 +144,8 @@ pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) ->
post.content.get().clone() // fallback to HTML if the markdown was not stored
};
let medias = Media::for_user(&*conn, user.id);
Some(render!(posts::new(
let medias = Media::for_user(&*conn, user.id)?;
Ok(render!(posts::new(
&(&*conn, &intl.catalog, Some(user)),
b,
true,
@@ -154,7 +153,7 @@ pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) ->
title: post.title.clone(),
subtitle: post.subtitle.clone(),
content: source,
tags: Tag::for_post(&*conn, post.id)
tags: Tag::for_post(&*conn, post.id)?
.into_iter()
.filter_map(|t| if !t.is_hashtag {Some(t.tag)} else {None})
.collect::<Vec<String>>()
@@ -173,9 +172,9 @@ pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) ->
#[post("/~/<blog>/<slug>/edit", data = "<form>")]
pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: LenientForm<NewPostForm>, worker: Worker, intl: I18n, searcher: Searcher)
-> Result<Redirect, Option<Ructe>> {
let b = Blog::find_by_fqn(&*conn, &blog).ok_or(None)?;
let mut post = Post::find_by_slug(&*conn, &slug, b.id).ok_or(None)?;
-> Result<Redirect, Ructe> {
let b = Blog::find_by_fqn(&*conn, &blog).expect("post::update: blog error");
let mut post = Post::find_by_slug(&*conn, &slug, b.id).expect("post::update: find by slug error");
let new_slug = if !post.published {
form.title.to_string().to_kebab_case()
@@ -188,7 +187,7 @@ pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: Lenien
Err(e) => e
};
if new_slug != slug && Post::find_by_slug(&*conn, &new_slug, b.id).is_some() {
if new_slug != slug && Post::find_by_slug(&*conn, &new_slug, b.id).is_ok() {
errors.add("title", ValidationError {
code: Cow::from("existing_slug"),
message: Some(Cow::from("A post with the same title already exists.")),
@@ -197,7 +196,7 @@ pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: Lenien
}
if errors.is_empty() {
if !user.is_author_in(&*conn, &b) {
if !user.is_author_in(&*conn, &b).expect("posts::update: is author in error") {
// actually it's not "Ok"…
Ok(Redirect::to(uri!(super::blogs::details: name = blog, page = _)))
} else {
@@ -219,29 +218,30 @@ pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: Lenien
post.source = form.content.clone();
post.license = form.license.clone();
post.cover_id = form.cover;
post.update(&*conn, &searcher);
let post = post.update_ap_url(&*conn);
post.update(&*conn, &searcher).expect("post::update: update error");;
let post = post.update_ap_url(&*conn).expect("post::update: update ap url error");
if post.published {
post.update_mentions(&conn, mentions.into_iter().map(|m| Mention::build_activity(&conn, &m)).collect());
post.update_mentions(&conn, mentions.into_iter().filter_map(|m| Mention::build_activity(&conn, &m).ok()).collect())
.expect("post::update: mentions error");;
}
let tags = form.tags.split(',').map(|t| t.trim().to_camel_case()).filter(|t| !t.is_empty())
.collect::<HashSet<_>>().into_iter().map(|t| Tag::build_activity(&conn, t)).collect::<Vec<_>>();
post.update_tags(&conn, tags);
.collect::<HashSet<_>>().into_iter().filter_map(|t| Tag::build_activity(&conn, t).ok()).collect::<Vec<_>>();
post.update_tags(&conn, tags).expect("post::update: tags error");
let hashtags = hashtags.into_iter().map(|h| h.to_camel_case()).collect::<HashSet<_>>()
.into_iter().map(|t| Tag::build_activity(&conn, t)).collect::<Vec<_>>();
post.update_hashtags(&conn, hashtags);
.into_iter().filter_map(|t| Tag::build_activity(&conn, t).ok()).collect::<Vec<_>>();
post.update_hashtags(&conn, hashtags).expect("post::update: hashtags error");
if post.published {
if newly_published {
let act = post.create_activity(&conn);
let dest = User::one_by_instance(&*conn);
let act = post.create_activity(&conn).expect("post::update: act error");
let dest = User::one_by_instance(&*conn).expect("post::update: dest error");
worker.execute(move || broadcast(&user, act, dest));
} else {
let act = post.update_activity(&*conn);
let dest = User::one_by_instance(&*conn);
let act = post.update_activity(&*conn).expect("post::update: act error");
let dest = User::one_by_instance(&*conn).expect("posts::update: dest error");
worker.execute(move || broadcast(&user, act, dest));
}
}
@@ -249,8 +249,8 @@ pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: Lenien
Ok(Redirect::to(uri!(details: blog = blog, slug = new_slug, responding_to = _)))
}
} else {
let medias = Media::for_user(&*conn, user.id);
let temp = render!(posts::new(
let medias = Media::for_user(&*conn, user.id).expect("posts:update: medias error");
Err(render!(posts::new(
&(&*conn, &intl.catalog, Some(user)),
b,
true,
@@ -259,8 +259,7 @@ pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: Lenien
Some(post),
errors.clone(),
medias.clone()
));
Err(Some(temp))
)))
}
}
@@ -288,15 +287,15 @@ pub fn valid_slug(title: &str) -> Result<(), ValidationError> {
}
#[post("/~/<blog_name>/new", data = "<form>")]
pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, conn: DbConn, worker: Worker, intl: I18n, searcher: Searcher) -> Result<Redirect, Option<Ructe>> {
let blog = Blog::find_by_fqn(&*conn, &blog_name).ok_or(None)?;
pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, conn: DbConn, worker: Worker, intl: I18n, searcher: Searcher) -> Result<Redirect, Result<Ructe, ErrorPage>> {
let blog = Blog::find_by_fqn(&*conn, &blog_name).expect("post::create: blog error");;
let slug = form.title.to_string().to_kebab_case();
let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(),
Err(e) => e
};
if Post::find_by_slug(&*conn, &slug, blog.id).is_some() {
if Post::find_by_slug(&*conn, &slug, blog.id).is_ok() {
errors.add("title", ValidationError {
code: Cow::from("existing_slug"),
message: Some(Cow::from("A post with the same title already exists.")),
@@ -305,11 +304,14 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, con
}
if errors.is_empty() {
if !user.is_author_in(&*conn, &blog) {
if !user.is_author_in(&*conn, &blog).expect("post::create: is author in error") {
// actually it's not "Ok"…
Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)))
} else {
let (content, mentions, hashtags) = utils::md_to_html(form.content.to_string().as_ref(), &Instance::get_local(&conn).expect("posts::create: Error getting l ocal instance").public_domain);
let (content, mentions, hashtags) = utils::md_to_html(
form.content.to_string().as_ref(),
&Instance::get_local(&conn).expect("post::create: local instance error").public_domain
);
let post = Post::insert(&*conn, NewPost {
blog_id: blog.id,
@@ -325,12 +327,12 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, con
cover_id: form.cover,
},
&searcher,
);
let post = post.update_ap_url(&*conn);
).expect("post::create: post save error");
let post = post.update_ap_url(&*conn).expect("post::create: update ap url error");
PostAuthor::insert(&*conn, NewPostAuthor {
post_id: post.id,
author_id: user.id
});
}).expect("post::create: author save error");
let tags = form.tags.split(',')
.map(|t| t.trim().to_camel_case())
@@ -341,31 +343,37 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, con
tag,
is_hashtag: false,
post_id: post.id
});
}).expect("post::create: tags save error");
}
for hashtag in hashtags {
Tag::insert(&*conn, NewTag {
tag: hashtag.to_camel_case(),
is_hashtag: true,
post_id: post.id
});
}).expect("post::create: hashtags save error");
}
if post.published {
for m in mentions {
Mention::from_activity(&*conn, &Mention::build_activity(&*conn, &m), post.id, true, true);
Mention::from_activity(
&*conn,
&Mention::build_activity(&*conn, &m).expect("post::create: mention build error"),
post.id,
true,
true
).expect("post::create: mention save error");
}
let act = post.create_activity(&*conn);
let dest = User::one_by_instance(&*conn);
let act = post.create_activity(&*conn).expect("posts::create: activity error");
let dest = User::one_by_instance(&*conn).expect("posts::create: dest error");
worker.execute(move || broadcast(&user, act, dest));
}
Ok(Redirect::to(uri!(details: blog = blog_name, slug = slug, responding_to = _)))
}
} else {
let medias = Media::for_user(&*conn, user.id);
Err(Some(render!(posts::new(
let medias = Media::for_user(&*conn, user.id).expect("posts::create: medias error");
Err(Ok(render!(posts::new(
&(&*conn, &intl.catalog, Some(user)),
blog,
false,
@@ -379,21 +387,21 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, con
}
#[post("/~/<blog_name>/<slug>/delete")]
pub fn delete(blog_name: String, slug: String, conn: DbConn, user: User, worker: Worker, searcher: Searcher) -> Redirect {
pub fn delete(blog_name: String, slug: String, conn: DbConn, user: User, worker: Worker, searcher: Searcher) -> Result<Redirect, ErrorPage> {
let post = Blog::find_by_fqn(&*conn, &blog_name)
.and_then(|blog| Post::find_by_slug(&*conn, &slug, blog.id));
if let Some(post) = post {
if !post.get_authors(&*conn).into_iter().any(|a| a.id == user.id) {
Redirect::to(uri!(details: blog = blog_name.clone(), slug = slug.clone(), responding_to = _))
if let Ok(post) = post {
if !post.get_authors(&*conn)?.into_iter().any(|a| a.id == user.id) {
Ok(Redirect::to(uri!(details: blog = blog_name.clone(), slug = slug.clone(), responding_to = _)))
} else {
let dest = User::one_by_instance(&*conn);
let delete_activity = post.delete(&(&conn, &searcher));
let dest = User::one_by_instance(&*conn)?;
let delete_activity = post.delete(&(&conn, &searcher))?;
worker.execute(move || broadcast(&user, delete_activity, dest));
Redirect::to(uri!(super::blogs::details: name = blog_name, page = _))
Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)))
}
} else {
Redirect::to(uri!(super::blogs::details: name = blog_name, page = _))
Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)))
}
}