From 80a4dae8bda18139da71ff678a50d4cc6baa27b3 Mon Sep 17 00:00:00 2001 From: Baptiste Gelez Date: Sat, 29 Dec 2018 09:36:07 +0100 Subject: [PATCH] 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` 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) --- plume-cli/src/instance.rs | 2 +- plume-cli/src/search.rs | 2 +- plume-cli/src/users.rs | 5 +- plume-common/src/activity_pub/inbox.rs | 35 +- plume-common/src/activity_pub/mod.rs | 4 +- plume-common/src/activity_pub/request.rs | 6 +- plume-common/src/activity_pub/sign.rs | 18 +- plume-models/src/api_tokens.rs | 34 +- plume-models/src/apps.rs | 12 +- plume-models/src/blog_authors.rs | 1 + plume-models/src/blogs.rs | 464 +++++++--------- plume-models/src/comment_seers.rs | 10 +- plume-models/src/comments.rs | 236 ++++----- plume-models/src/db_conn.rs | 4 +- plume-models/src/follows.rs | 132 ++--- plume-models/src/instance.rs | 85 ++- plume-models/src/lib.rs | 136 ++++- plume-models/src/likes.rs | 111 ++-- plume-models/src/medias.rs | 92 ++-- plume-models/src/mentions.rs | 94 ++-- plume-models/src/notifications.rs | 65 ++- plume-models/src/post_authors.rs | 1 + plume-models/src/posts.rs | 513 +++++++++--------- plume-models/src/reshares.rs | 119 ++--- plume-models/src/search/mod.rs | 10 +- plume-models/src/search/searcher.rs | 44 +- plume-models/src/tags.rs | 37 +- plume-models/src/users.rs | 648 +++++++++-------------- src/api/mod.rs | 54 +- src/inbox.rs | 36 +- src/main.rs | 24 +- src/routes/blogs.rs | 56 +- src/routes/comments.rs | 65 ++- src/routes/errors.rs | 38 +- src/routes/instance.rs | 182 +++---- src/routes/likes.rs | 21 +- src/routes/medias.rs | 51 +- src/routes/mod.rs | 2 +- src/routes/notifications.rs | 12 +- src/routes/posts.rs | 166 +++--- src/routes/reshares.rs | 22 +- src/routes/session.rs | 19 +- src/routes/tags.rs | 12 +- src/routes/user.rs | 153 +++--- src/routes/well_known.rs | 12 +- templates/medias/details.rs.html | 5 +- templates/medias/index.rs.html | 3 +- templates/notifications/index.rs.html | 6 +- templates/partials/comment.rs.html | 2 +- templates/partials/post_card.rs.html | 8 +- 50 files changed, 1879 insertions(+), 1990 deletions(-) diff --git a/plume-cli/src/instance.rs b/plume-cli/src/instance.rs index 0bd45db1..38b1af47 100644 --- a/plume-cli/src/instance.rs +++ b/plume-cli/src/instance.rs @@ -59,5 +59,5 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) { open_registrations: open_reg, short_description_html: String::new(), long_description_html: String::new() - }); + }).expect("Couldn't save instance"); } diff --git a/plume-cli/src/search.rs b/plume-cli/src/search.rs index 67cbd58d..7083e552 100644 --- a/plume-cli/src/search.rs +++ b/plume-cli/src/search.rs @@ -94,7 +94,7 @@ fn refill<'a>(args: &ArgMatches<'a>, conn: &Connection, searcher: Option(args: &ArgMatches<'a>, conn: &Connection) { admin, &bio, email, - User::hash_pass(&password), - ).update_boxes(conn); + User::hash_pass(&password).expect("Couldn't hash password"), + ).expect("Couldn't save new user") + .update_boxes(conn).expect("Couldn't update ActivityPub informations for new user"); } diff --git a/plume-common/src/activity_pub/inbox.rs b/plume-common/src/activity_pub/inbox.rs index 2820b14f..9b3155e7 100644 --- a/plume-common/src/activity_pub/inbox.rs +++ b/plume-common/src/activity_pub/inbox.rs @@ -1,4 +1,4 @@ -use activitypub::{activity::Create, Object}; +use activitypub::{activity::Create, Error as ApError, Object}; use activity_pub::Id; @@ -13,31 +13,30 @@ pub enum InboxError { } pub trait FromActivity: Sized { - fn from_activity(conn: &C, obj: T, actor: Id) -> Self; + type Error: From; - fn try_from_activity(conn: &C, act: Create) -> bool { - if let Ok(obj) = act.create_props.object_object() { - Self::from_activity( - conn, - obj, - act.create_props - .actor_link::() - .expect("FromActivity::try_from_activity: id not found error"), - ); - true - } else { - false - } + fn from_activity(conn: &C, obj: T, actor: Id) -> Result; + + fn try_from_activity(conn: &C, act: Create) -> Result { + Self::from_activity( + conn, + act.create_props.object_object()?, + act.create_props.actor_link::()?, + ) } } pub trait Notify { - fn notify(&self, conn: &C); + type Error; + + fn notify(&self, conn: &C) -> Result<(), Self::Error>; } pub trait Deletable { - fn delete(&self, conn: &C) -> A; - fn delete_id(id: &str, actor_id: &str, conn: &C); + type Error; + + fn delete(&self, conn: &C) -> Result; + fn delete_id(id: &str, actor_id: &str, conn: &C) -> Result; } pub trait WithInbox { diff --git a/plume-common/src/activity_pub/mod.rs b/plume-common/src/activity_pub/mod.rs index 01836afc..0f5b3d3e 100644 --- a/plume-common/src/activity_pub/mod.rs +++ b/plume-common/src/activity_pub/mod.rs @@ -120,7 +120,7 @@ pub fn broadcast( let mut act = serde_json::to_value(act).expect("activity_pub::broadcast: serialization error"); act["@context"] = context(); - let signed = act.sign(sender); + let signed = act.sign(sender).expect("activity_pub::broadcast: signature error"); for inbox in boxes { // TODO: run it in Sidekiq or something like that @@ -130,7 +130,7 @@ pub fn broadcast( let res = Client::new() .post(&inbox) .headers(headers.clone()) - .header("Signature", request::signature(sender, &headers)) + .header("Signature", request::signature(sender, &headers).expect("activity_pub::broadcast: request signature error")) .body(body) .send(); match res { diff --git a/plume-common/src/activity_pub/request.rs b/plume-common/src/activity_pub/request.rs index e569d2bc..78059dd3 100644 --- a/plume-common/src/activity_pub/request.rs +++ b/plume-common/src/activity_pub/request.rs @@ -105,7 +105,7 @@ pub fn headers() -> HeaderMap { headers } -pub fn signature(signer: &S, headers: &HeaderMap) -> HeaderValue { +pub fn signature(signer: &S, headers: &HeaderMap) -> Result { let signed_string = headers .iter() .map(|(h, v)| { @@ -125,7 +125,7 @@ pub fn signature(signer: &S, headers: &HeaderMap) -> HeaderValue { .join(" ") .to_lowercase(); - let data = signer.sign(&signed_string); + let data = signer.sign(&signed_string).map_err(|_| ())?; let sign = base64::encode(&data); HeaderValue::from_str(&format!( @@ -133,5 +133,5 @@ pub fn signature(signer: &S, headers: &HeaderMap) -> HeaderValue { key_id = signer.get_key_id(), signed_headers = signed_headers, signature = sign - )).expect("request::signature: signature header error") + )).map_err(|_| ()) } diff --git a/plume-common/src/activity_pub/sign.rs b/plume-common/src/activity_pub/sign.rs index 94368ce0..9d00a2e1 100644 --- a/plume-common/src/activity_pub/sign.rs +++ b/plume-common/src/activity_pub/sign.rs @@ -22,16 +22,18 @@ pub fn gen_keypair() -> (Vec, Vec) { } pub trait Signer { + type Error; + fn get_key_id(&self) -> String; /// Sign some data with the signer keypair - fn sign(&self, to_sign: &str) -> Vec; + fn sign(&self, to_sign: &str) -> Result, Self::Error>; /// Verify if the signature is valid - fn verify(&self, data: &str, signature: &[u8]) -> bool; + fn verify(&self, data: &str, signature: &[u8]) -> Result; } pub trait Signable { - fn sign(&mut self, creator: &T) -> &mut Self + fn sign(&mut self, creator: &T) -> Result<&mut Self, ()> where T: Signer; fn verify(self, creator: &T) -> bool @@ -45,7 +47,7 @@ pub trait Signable { } impl Signable for serde_json::Value { - fn sign(&mut self, creator: &T) -> &mut serde_json::Value { + fn sign(&mut self, creator: &T) -> Result<&mut serde_json::Value, ()> { let creation_date = Utc::now().to_rfc3339(); let mut options = json!({ "type": "RsaSignature2017", @@ -62,11 +64,11 @@ impl Signable for serde_json::Value { let document_hash = Self::hash(&self.to_string()); let to_be_signed = options_hash + &document_hash; - let signature = base64::encode(&creator.sign(&to_be_signed)); + let signature = base64::encode(&creator.sign(&to_be_signed).map_err(|_| ())?); options["signatureValue"] = serde_json::Value::String(signature); self["signature"] = options; - self + Ok(self) } fn verify(mut self, creator: &T) -> bool { @@ -107,7 +109,7 @@ impl Signable for serde_json::Value { } let document_hash = Self::hash(&self.to_string()); let to_be_signed = options_hash + &document_hash; - creator.verify(&to_be_signed, &signature) + creator.verify(&to_be_signed, &signature).unwrap_or(false) } } @@ -167,7 +169,7 @@ pub fn verify_http_headers( .collect::>() .join("\n"); - if !sender.verify(&h, &base64::decode(signature).unwrap_or_default()) { + if !sender.verify(&h, &base64::decode(signature).unwrap_or_default()).unwrap_or(false) { return SignatureValidity::Invalid; } if !headers.contains(&"digest") { diff --git a/plume-models/src/api_tokens.rs b/plume-models/src/api_tokens.rs index b3f45773..b19bc126 100644 --- a/plume-models/src/api_tokens.rs +++ b/plume-models/src/api_tokens.rs @@ -8,6 +8,7 @@ use rocket::{ use db_conn::DbConn; use schema::api_tokens; +use {Error, Result}; #[derive(Clone, Queryable)] pub struct ApiToken { @@ -63,22 +64,39 @@ impl ApiToken { } } -impl<'a, 'r> FromRequest<'a, 'r> for ApiToken { - type Error = (); +#[derive(Debug)] +pub enum TokenError { + /// The Authorization header was not present + NoHeader, - fn from_request(request: &'a Request<'r>) -> request::Outcome { + /// The type of the token was not specified ("Basic" or "Bearer" for instance) + NoType, + + /// No value was provided + NoValue, + + /// Error while connecting to the database to retrieve all the token metadata + DbError, +} + +impl<'a, 'r> FromRequest<'a, 'r> for ApiToken { + type Error = TokenError; + + fn from_request(request: &'a Request<'r>) -> request::Outcome { let headers: Vec<_> = request.headers().get("Authorization").collect(); if headers.len() != 1 { - return Outcome::Failure((Status::BadRequest, ())); + return Outcome::Failure((Status::BadRequest, TokenError::NoHeader)); } let mut parsed_header = headers[0].split(' '); - let auth_type = parsed_header.next().expect("Expect a token type"); - let val = parsed_header.next().expect("Expect a token value"); + let auth_type = parsed_header.next() + .map_or_else(|| Outcome::Failure((Status::BadRequest, TokenError::NoType)), |t| Outcome::Success(t))?; + let val = parsed_header.next() + .map_or_else(|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)), |t| Outcome::Success(t))?; if auth_type == "Bearer" { - let conn = request.guard::().expect("Couldn't connect to DB"); - if let Some(token) = ApiToken::find_by_value(&*conn, val) { + let conn = request.guard::().map_failure(|_| (Status::InternalServerError, TokenError::DbError))?; + if let Ok(token) = ApiToken::find_by_value(&*conn, val) { return Outcome::Success(token); } } diff --git a/plume-models/src/apps.rs b/plume-models/src/apps.rs index d3883930..aae3af96 100644 --- a/plume-models/src/apps.rs +++ b/plume-models/src/apps.rs @@ -1,11 +1,11 @@ -use canapi::{Error, Provider}; +use canapi::{Error as ApiError, Provider}; use chrono::NaiveDateTime; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use plume_api::apps::AppEndpoint; use plume_common::utils::random_hex; use schema::apps; -use Connection; +use {Connection, Error, Result, ApiResult}; #[derive(Clone, Queryable)] pub struct App { @@ -31,7 +31,7 @@ pub struct NewApp { impl Provider for App { type Data = AppEndpoint; - fn get(_conn: &Connection, _id: i32) -> Result { + fn get(_conn: &Connection, _id: i32) -> ApiResult { unimplemented!() } @@ -39,7 +39,7 @@ impl Provider for App { unimplemented!() } - fn create(conn: &Connection, data: AppEndpoint) -> Result { + fn create(conn: &Connection, data: AppEndpoint) -> ApiResult { let client_id = random_hex(); let client_secret = random_hex(); @@ -52,7 +52,7 @@ impl Provider for App { redirect_uri: data.redirect_uri, website: data.website, }, - ); + ).map_err(|_| ApiError::NotFound("Couldn't register app".into()))?; Ok(AppEndpoint { id: Some(app.id), @@ -64,7 +64,7 @@ impl Provider for App { }) } - fn update(_conn: &Connection, _id: i32, _new_data: AppEndpoint) -> Result { + fn update(_conn: &Connection, _id: i32, _new_data: AppEndpoint) -> ApiResult { unimplemented!() } diff --git a/plume-models/src/blog_authors.rs b/plume-models/src/blog_authors.rs index 64e82c9f..20b99b13 100644 --- a/plume-models/src/blog_authors.rs +++ b/plume-models/src/blog_authors.rs @@ -1,6 +1,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use schema::blog_authors; +use {Error, Result}; #[derive(Clone, Queryable, Identifiable)] pub struct BlogAuthor { diff --git a/plume-models/src/blogs.rs b/plume-models/src/blogs.rs index dc6a9c6c..0a8e19ad 100644 --- a/plume-models/src/blogs.rs +++ b/plume-models/src/blogs.rs @@ -26,7 +26,7 @@ use safe_string::SafeString; use schema::blogs; use search::Searcher; use users::User; -use {Connection, BASE_URL, USE_HTTPS}; +use {Connection, BASE_URL, USE_HTTPS, Error, Result}; pub type CustomGroup = CustomObject; @@ -67,11 +67,11 @@ impl Blog { find_by!(blogs, find_by_ap_url, ap_url as &str); find_by!(blogs, find_by_name, actor_id as &str, instance_id as i32); - pub fn get_instance(&self, conn: &Connection) -> Instance { - Instance::get(conn, self.instance_id).expect("Blog::get_instance: instance not found error") + pub fn get_instance(&self, conn: &Connection) -> Result { + Instance::get(conn, self.instance_id) } - pub fn list_authors(&self, conn: &Connection) -> Vec { + pub fn list_authors(&self, conn: &Connection) -> Result> { use schema::blog_authors; use schema::users; let authors_ids = blog_authors::table @@ -80,19 +80,19 @@ impl Blog { users::table .filter(users::id.eq_any(authors_ids)) .load::(conn) - .expect("Blog::list_authors: author loading error") + .map_err(Error::from) } - pub fn count_authors(&self, conn: &Connection) -> i64 { + pub fn count_authors(&self, conn: &Connection) -> Result { use schema::blog_authors; blog_authors::table .filter(blog_authors::blog_id.eq(self.id)) .count() .get_result(conn) - .expect("Blog::count_authors: count loading error") + .map_err(Error::from) } - pub fn find_for_author(conn: &Connection, author: &User) -> Vec { + pub fn find_for_author(conn: &Connection, author: &User) -> Result> { use schema::blog_authors; let author_ids = blog_authors::table .filter(blog_authors::author_id.eq(author.id)) @@ -100,62 +100,40 @@ impl Blog { blogs::table .filter(blogs::id.eq_any(author_ids)) .load::(conn) - .expect("Blog::find_for_author: blog loading error") + .map_err(Error::from) } - pub fn find_local(conn: &Connection, name: &str) -> Option { - Blog::find_by_name(conn, name, Instance::local_id(conn)) + pub fn find_local(conn: &Connection, name: &str) -> Result { + Blog::find_by_name(conn, name, Instance::get_local(conn)?.id) } - pub fn find_by_fqn(conn: &Connection, fqn: &str) -> Option { - if fqn.contains('@') { - // remote blog - match Instance::find_by_domain( - conn, - fqn.split('@') - .last() - .expect("Blog::find_by_fqn: unreachable"), - ) { - Some(instance) => match Blog::find_by_name( + pub fn find_by_fqn(conn: &Connection, fqn: &str) -> Result { + let mut split_fqn = fqn.split('@'); + let actor = split_fqn.next().ok_or(Error::InvalidValue)?; + if let Some(domain) = split_fqn.next() { // remote blog + Instance::find_by_domain(conn, domain) + .and_then(|instance| Blog::find_by_name(conn, actor, instance.id)) + .or_else(|_| Blog::fetch_from_webfinger(conn, fqn)) + } else { // local blog + Blog::find_local(conn, actor) + } + } + + fn fetch_from_webfinger(conn: &Connection, acct: &str) -> Result { + resolve(acct.to_owned(), *USE_HTTPS)?.links + .into_iter() + .find(|l| l.mime_type == Some(String::from("application/activity+json"))) + .ok_or(Error::Webfinger) + .and_then(|l| { + Blog::fetch_from_url( conn, - fqn.split('@') - .nth(0) - .expect("Blog::find_by_fqn: unreachable"), - instance.id, - ) { - Some(u) => Some(u), - None => Blog::fetch_from_webfinger(conn, fqn), - }, - None => Blog::fetch_from_webfinger(conn, fqn), - } - } else { - // local blog - Blog::find_local(conn, fqn) - } + &l.href? + ) + }) } - fn fetch_from_webfinger(conn: &Connection, acct: &str) -> Option { - match resolve(acct.to_owned(), *USE_HTTPS) { - Ok(wf) => wf - .links - .into_iter() - .find(|l| l.mime_type == Some(String::from("application/activity+json"))) - .and_then(|l| { - Blog::fetch_from_url( - conn, - &l.href - .expect("Blog::fetch_from_webfinger: href not found error"), - ) - }), - Err(details) => { - println!("{:?}", details); - None - } - } - } - - fn fetch_from_url(conn: &Connection, url: &str) -> Option { - let req = Client::new() + fn fetch_from_url(conn: &Connection, url: &str) -> Result { + let mut res = Client::new() .get(url) .header( ACCEPT, @@ -164,139 +142,109 @@ impl Blog { .into_iter() .collect::>() .join(", "), - ).expect("Blog::fetch_from_url: accept_header generation error"), + )?, ) - .send(); - match req { - Ok(mut res) => { - let text = &res - .text() - .expect("Blog::fetch_from_url: body reading error"); - let ap_sign: ApSignature = - serde_json::from_str(text).expect("Blog::fetch_from_url: body parsing error"); - let mut json: CustomGroup = - serde_json::from_str(text).expect("Blog::fetch_from_url: body parsing error"); - json.custom_props = ap_sign; // without this workaround, publicKey is not correctly deserialized - Some(Blog::from_activity( - conn, - &json, - Url::parse(url) - .expect("Blog::fetch_from_url: url parsing error") - .host_str() - .expect("Blog::fetch_from_url: host extraction error"), - )) - } - Err(_) => None, - } + .send()?; + + let text = &res.text()?; + let ap_sign: ApSignature = + serde_json::from_str(text)?; + let mut json: CustomGroup = + serde_json::from_str(text)?; + json.custom_props = ap_sign; // without this workaround, publicKey is not correctly deserialized + Blog::from_activity( + conn, + &json, + Url::parse(url)?.host_str()?, + ) } - fn from_activity(conn: &Connection, acct: &CustomGroup, inst: &str) -> Blog { - let instance = match Instance::find_by_domain(conn, inst) { - Some(instance) => instance, - None => { - Instance::insert( - conn, - NewInstance { - public_domain: inst.to_owned(), - name: inst.to_owned(), - local: false, - // We don't really care about all the following for remote instances - long_description: SafeString::new(""), - short_description: SafeString::new(""), - default_license: String::new(), - open_registrations: true, - short_description_html: String::new(), - long_description_html: String::new(), - }, - ) - } - }; + fn from_activity(conn: &Connection, acct: &CustomGroup, inst: &str) -> Result { + let instance = Instance::find_by_domain(conn, inst).or_else(|_| + Instance::insert( + conn, + NewInstance { + public_domain: inst.to_owned(), + name: inst.to_owned(), + local: false, + // We don't really care about all the following for remote instances + long_description: SafeString::new(""), + short_description: SafeString::new(""), + default_license: String::new(), + open_registrations: true, + short_description_html: String::new(), + long_description_html: String::new(), + }, + ) + )?; Blog::insert( conn, NewBlog { actor_id: acct .object .ap_actor_props - .preferred_username_string() - .expect("Blog::from_activity: preferredUsername error"), + .preferred_username_string()?, title: acct .object .object_props - .name_string() - .expect("Blog::from_activity: name error"), + .name_string()?, outbox_url: acct .object .ap_actor_props - .outbox_string() - .expect("Blog::from_activity: outbox error"), + .outbox_string()?, inbox_url: acct .object .ap_actor_props - .inbox_string() - .expect("Blog::from_activity: inbox error"), + .inbox_string()?, summary: acct .object .object_props - .summary_string() - .expect("Blog::from_activity: summary error"), + .summary_string()?, instance_id: instance.id, ap_url: acct .object .object_props - .id_string() - .expect("Blog::from_activity: id error"), + .id_string()?, public_key: acct .custom_props - .public_key_publickey() - .expect("Blog::from_activity: publicKey error") - .public_key_pem_string() - .expect("Blog::from_activity: publicKey.publicKeyPem error"), + .public_key_publickey()? + .public_key_pem_string()?, private_key: None, }, ) } - pub fn to_activity(&self, _conn: &Connection) -> CustomGroup { + pub fn to_activity(&self, _conn: &Connection) -> Result { let mut blog = Group::default(); blog.ap_actor_props - .set_preferred_username_string(self.actor_id.clone()) - .expect("Blog::to_activity: preferredUsername error"); + .set_preferred_username_string(self.actor_id.clone())?; blog.object_props - .set_name_string(self.title.clone()) - .expect("Blog::to_activity: name error"); + .set_name_string(self.title.clone())?; blog.ap_actor_props - .set_outbox_string(self.outbox_url.clone()) - .expect("Blog::to_activity: outbox error"); + .set_outbox_string(self.outbox_url.clone())?; blog.ap_actor_props - .set_inbox_string(self.inbox_url.clone()) - .expect("Blog::to_activity: inbox error"); + .set_inbox_string(self.inbox_url.clone())?; blog.object_props - .set_summary_string(self.summary.clone()) - .expect("Blog::to_activity: summary error"); + .set_summary_string(self.summary.clone())?; blog.object_props - .set_id_string(self.ap_url.clone()) - .expect("Blog::to_activity: id error"); + .set_id_string(self.ap_url.clone())?; let mut public_key = PublicKey::default(); public_key - .set_id_string(format!("{}#main-key", self.ap_url)) - .expect("Blog::to_activity: publicKey.id error"); + .set_id_string(format!("{}#main-key", self.ap_url))?; public_key - .set_owner_string(self.ap_url.clone()) - .expect("Blog::to_activity: publicKey.owner error"); + .set_owner_string(self.ap_url.clone())?; public_key - .set_public_key_pem_string(self.public_key.clone()) - .expect("Blog::to_activity: publicKey.publicKeyPem error"); + .set_public_key_pem_string(self.public_key.clone())?; let mut ap_signature = ApSignature::default(); ap_signature - .set_public_key_publickey(public_key) - .expect("Blog::to_activity: publicKey error"); + .set_public_key_publickey(public_key)?; - CustomGroup::new(blog, ap_signature) + Ok(CustomGroup::new(blog, ap_signature)) } - pub fn update_boxes(&self, conn: &Connection) { - let instance = self.get_instance(conn); + pub fn update_boxes(&self, conn: &Connection) -> Result<()> { + let instance = self.get_instance(conn)?; if self.outbox_url.is_empty() { diesel::update(self) .set(blogs::outbox_url.eq(instance.compute_box( @@ -304,8 +252,7 @@ impl Blog { &self.actor_id, "outbox", ))) - .execute(conn) - .expect("Blog::update_boxes: outbox update error"); + .execute(conn)?; } if self.inbox_url.is_empty() { @@ -315,49 +262,45 @@ impl Blog { &self.actor_id, "inbox", ))) - .execute(conn) - .expect("Blog::update_boxes: inbox update error"); + .execute(conn)?; } if self.ap_url.is_empty() { diesel::update(self) .set(blogs::ap_url.eq(instance.compute_box(BLOG_PREFIX, &self.actor_id, ""))) - .execute(conn) - .expect("Blog::update_boxes: ap_url update error"); + .execute(conn)?; } + Ok(()) } - pub fn outbox(&self, conn: &Connection) -> ActivityStream { + pub fn outbox(&self, conn: &Connection) -> Result> { let mut coll = OrderedCollection::default(); - coll.collection_props.items = serde_json::to_value(self.get_activities(conn)) - .expect("Blog::outbox: activity serialization error"); + coll.collection_props.items = serde_json::to_value(self.get_activities(conn)?)?; coll.collection_props - .set_total_items_u64(self.get_activities(conn).len() as u64) - .expect("Blog::outbox: count serialization error"); - ActivityStream::new(coll) + .set_total_items_u64(self.get_activities(conn)?.len() as u64)?; + Ok(ActivityStream::new(coll)) } - fn get_activities(&self, _conn: &Connection) -> Vec { - vec![] + fn get_activities(&self, _conn: &Connection) -> Result> { + Ok(vec![]) } - pub fn get_keypair(&self) -> PKey { + pub fn get_keypair(&self) -> Result> { PKey::from_rsa( Rsa::private_key_from_pem( self.private_key - .clone() - .expect("Blog::get_keypair: private key not found error") + .clone()? .as_ref(), - ).expect("Blog::get_keypair: pem parsing error"), - ).expect("Blog::get_keypair: private key deserialization error") + )?, + ).map_err(Error::from) } - pub fn webfinger(&self, conn: &Connection) -> Webfinger { - Webfinger { + pub fn webfinger(&self, conn: &Connection) -> Result { + Ok(Webfinger { subject: format!( "acct:{}@{}", self.actor_id, - self.get_instance(conn).public_domain + self.get_instance(conn)?.public_domain ), aliases: vec![self.ap_url.clone()], links: vec![ @@ -370,7 +313,7 @@ impl Blog { Link { rel: String::from("http://schemas.google.com/g/2010#updates-from"), mime_type: Some(String::from("application/atom+xml")), - href: Some(self.get_instance(conn).compute_box( + href: Some(self.get_instance(conn)?.compute_box( BLOG_PREFIX, &self.actor_id, "feed.atom", @@ -384,50 +327,41 @@ impl Blog { template: None, }, ], - } + }) } - pub fn from_url(conn: &Connection, url: &str) -> Option { - Blog::find_by_ap_url(conn, url).or_else(|| { + pub fn from_url(conn: &Connection, url: &str) -> Result { + Blog::find_by_ap_url(conn, url).or_else(|_| { // The requested blog was not in the DB // We try to fetch it if it is remote - if Url::parse(url) - .expect("Blog::from_url: ap_url parsing error") - .host_str() - .expect("Blog::from_url: host extraction error") != BASE_URL.as_str() - { + if Url::parse(url)?.host_str()? != BASE_URL.as_str() { Blog::fetch_from_url(conn, url) } else { - None + Err(Error::NotFound) } }) } pub fn get_fqn(&self, conn: &Connection) -> String { - if self.instance_id == Instance::local_id(conn) { + if self.instance_id == Instance::get_local(conn).ok().expect("Blog::get_fqn: local instance error").id { self.actor_id.clone() } else { format!( "{}@{}", self.actor_id, - self.get_instance(conn).public_domain + self.get_instance(conn).ok().expect("Blog::get_fqn: instance error").public_domain ) } } - pub fn to_json(&self, conn: &Connection) -> serde_json::Value { - let mut json = serde_json::to_value(self).expect("Blog::to_json: serialization error"); - json["fqn"] = json!(self.get_fqn(conn)); - json - } - - pub fn delete(&self, conn: &Connection, searcher: &Searcher) { - for post in Post::get_for_blog(conn, &self) { - post.delete(&(conn, searcher)); + pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> { + for post in Post::get_for_blog(conn, &self)? { + post.delete(&(conn, searcher))?; } diesel::delete(self) .execute(conn) - .expect("Blog::delete: blog deletion error"); + .map(|_| ()) + .map_err(Error::from) } } @@ -455,35 +389,33 @@ impl WithInbox for Blog { } impl sign::Signer for Blog { + type Error = Error; + fn get_key_id(&self) -> String { format!("{}#main-key", self.ap_url) } - fn sign(&self, to_sign: &str) -> Vec { - let key = self.get_keypair(); + fn sign(&self, to_sign: &str) -> Result> { + let key = self.get_keypair()?; let mut signer = - Signer::new(MessageDigest::sha256(), &key).expect("Blog::sign: initialization error"); + Signer::new(MessageDigest::sha256(), &key)?; signer - .update(to_sign.as_bytes()) - .expect("Blog::sign: content insertion error"); + .update(to_sign.as_bytes())?; signer .sign_to_vec() - .expect("Blog::sign: finalization error") + .map_err(Error::from) } - fn verify(&self, data: &str, signature: &[u8]) -> bool { + fn verify(&self, data: &str, signature: &[u8]) -> Result { let key = PKey::from_rsa( - Rsa::public_key_from_pem(self.public_key.as_ref()) - .expect("Blog::verify: pem parsing error"), - ).expect("Blog::verify: deserialization error"); - let mut verifier = Verifier::new(MessageDigest::sha256(), &key) - .expect("Blog::verify: initialization error"); + Rsa::public_key_from_pem(self.public_key.as_ref())? + )?; + let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?; verifier - .update(data.as_bytes()) - .expect("Blog::verify: content insertion error"); + .update(data.as_bytes())?; verifier .verify(&signature) - .expect("Blog::verify: finalization error") + .map_err(Error::from) } } @@ -493,9 +425,9 @@ impl NewBlog { title: String, summary: String, instance_id: i32, - ) -> NewBlog { + ) -> Result { let (pub_key, priv_key) = sign::gen_keypair(); - NewBlog { + Ok(NewBlog { actor_id, title, summary, @@ -503,11 +435,9 @@ impl NewBlog { inbox_url: String::from(""), instance_id, ap_url: String::from(""), - public_key: String::from_utf8(pub_key).expect("NewBlog::new_local: public key error"), - private_key: Some( - String::from_utf8(priv_key).expect("NewBlog::new_local: private key error"), - ), - } + public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?, + private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?), + }) } } @@ -529,23 +459,23 @@ pub(crate) mod tests { "BlogName".to_owned(), "Blog name".to_owned(), "This is a small blog".to_owned(), - Instance::local_id(conn), - )); - blog1.update_boxes(conn); + Instance::get_local(conn).unwrap().id + ).unwrap()).unwrap(); + blog1.update_boxes(conn).unwrap(); let blog2 = Blog::insert(conn, NewBlog::new_local( "MyBlog".to_owned(), "My blog".to_owned(), "Welcome to my blog".to_owned(), - Instance::local_id(conn), - )); - blog2.update_boxes(conn); + Instance::get_local(conn).unwrap().id + ).unwrap()).unwrap(); + blog2.update_boxes(conn).unwrap(); let blog3 = Blog::insert(conn, NewBlog::new_local( "WhyILikePlume".to_owned(), "Why I like Plume".to_owned(), "In this blog I will explay you why I like Plume so much".to_owned(), - Instance::local_id(conn), - )); - blog3.update_boxes(conn); + Instance::get_local(conn).unwrap().id + ).unwrap()).unwrap(); + blog3.update_boxes(conn).unwrap(); BlogAuthor::insert( conn, @@ -554,7 +484,7 @@ pub(crate) mod tests { author_id: users[0].id, is_owner: true, }, - ); + ).unwrap(); BlogAuthor::insert( conn, @@ -563,7 +493,7 @@ pub(crate) mod tests { author_id: users[1].id, is_owner: false, }, - ); + ).unwrap(); BlogAuthor::insert( conn, @@ -572,7 +502,7 @@ pub(crate) mod tests { author_id: users[1].id, is_owner: true, }, - ); + ).unwrap(); BlogAuthor::insert( conn, @@ -581,7 +511,7 @@ pub(crate) mod tests { author_id: users[2].id, is_owner: true, }, - ); + ).unwrap(); (users, vec![ blog1, blog2, blog3 ]) } @@ -597,11 +527,11 @@ pub(crate) mod tests { "SomeName".to_owned(), "Some name".to_owned(), "This is some blog".to_owned(), - Instance::local_id(conn), - ), - ); + Instance::get_local(conn).unwrap().id + ).unwrap(), + ).unwrap(); - assert_eq!(blog.get_instance(conn).id, Instance::local_id(conn)); + assert_eq!(blog.get_instance(conn).unwrap().id, Instance::get_local(conn).unwrap().id); // TODO add tests for remote instance Ok(()) @@ -620,20 +550,20 @@ pub(crate) mod tests { "SomeName".to_owned(), "Some name".to_owned(), "This is some blog".to_owned(), - Instance::local_id(conn), - ), - ); - b1.update_boxes(conn); + Instance::get_local(conn).unwrap().id, + ).unwrap(), + ).unwrap(); + b1.update_boxes(conn).unwrap(); let b2 = Blog::insert( conn, NewBlog::new_local( "Blog".to_owned(), "Blog".to_owned(), "I've named my blog Blog".to_owned(), - Instance::local_id(conn), - ), - ); - b2.update_boxes(conn); + Instance::get_local(conn).unwrap().id + ).unwrap(), + ).unwrap(); + b2.update_boxes(conn).unwrap(); let blog = vec![ b1, b2 ]; BlogAuthor::insert( @@ -643,7 +573,7 @@ pub(crate) mod tests { author_id: user[0].id, is_owner: true, }, - ); + ).unwrap(); BlogAuthor::insert( conn, @@ -652,7 +582,7 @@ pub(crate) mod tests { author_id: user[1].id, is_owner: false, }, - ); + ).unwrap(); BlogAuthor::insert( conn, @@ -661,50 +591,50 @@ pub(crate) mod tests { author_id: user[0].id, is_owner: true, }, - ); + ).unwrap(); assert!( blog[0] - .list_authors(conn) + .list_authors(conn).unwrap() .iter() .any(|a| a.id == user[0].id) ); assert!( blog[0] - .list_authors(conn) + .list_authors(conn).unwrap() .iter() .any(|a| a.id == user[1].id) ); assert!( blog[1] - .list_authors(conn) + .list_authors(conn).unwrap() .iter() .any(|a| a.id == user[0].id) ); assert!( !blog[1] - .list_authors(conn) + .list_authors(conn).unwrap() .iter() .any(|a| a.id == user[1].id) ); assert!( - Blog::find_for_author(conn, &user[0]) + Blog::find_for_author(conn, &user[0]).unwrap() .iter() .any(|b| b.id == blog[0].id) ); assert!( - Blog::find_for_author(conn, &user[1]) + Blog::find_for_author(conn, &user[1]).unwrap() .iter() .any(|b| b.id == blog[0].id) ); assert!( - Blog::find_for_author(conn, &user[0]) + Blog::find_for_author(conn, &user[0]).unwrap() .iter() .any(|b| b.id == blog[1].id) ); assert!( - !Blog::find_for_author(conn, &user[1]) + !Blog::find_for_author(conn, &user[1]).unwrap() .iter() .any(|b| b.id == blog[1].id) ); @@ -725,9 +655,9 @@ pub(crate) mod tests { "SomeName".to_owned(), "Some name".to_owned(), "This is some blog".to_owned(), - Instance::local_id(conn), - ), - ); + Instance::get_local(conn).unwrap().id, + ).unwrap(), + ).unwrap(); assert_eq!( Blog::find_local(conn, "SomeName").unwrap().id, @@ -750,9 +680,9 @@ pub(crate) mod tests { "SomeName".to_owned(), "Some name".to_owned(), "This is some blog".to_owned(), - Instance::local_id(conn), - ), - ); + Instance::get_local(conn).unwrap().id, + ).unwrap(), + ).unwrap(); assert_eq!(blog.get_fqn(conn), "SomeName"); @@ -766,8 +696,8 @@ pub(crate) mod tests { conn.test_transaction::<_, (), _>(|| { let (_, blogs) = fill_database(conn); - blogs[0].delete(conn, &get_searcher()); - assert!(Blog::get(conn, blogs[0].id).is_none()); + blogs[0].delete(conn, &get_searcher()).unwrap(); + assert!(Blog::get(conn, blogs[0].id).is_err()); Ok(()) }); @@ -786,20 +716,20 @@ pub(crate) mod tests { "SomeName".to_owned(), "Some name".to_owned(), "This is some blog".to_owned(), - Instance::local_id(conn), - ), - ); - b1.update_boxes(conn); + Instance::get_local(conn).unwrap().id, + ).unwrap(), + ).unwrap(); + b1.update_boxes(conn).unwrap(); let b2 = Blog::insert( conn, NewBlog::new_local( "Blog".to_owned(), "Blog".to_owned(), "I've named my blog Blog".to_owned(), - Instance::local_id(conn), - ), - ); - b2.update_boxes(conn); + Instance::get_local(conn).unwrap().id, + ).unwrap(), + ).unwrap(); + b2.update_boxes(conn).unwrap(); let blog = vec![ b1, b2 ]; BlogAuthor::insert( @@ -809,7 +739,7 @@ pub(crate) mod tests { author_id: user[0].id, is_owner: true, }, - ); + ).unwrap(); BlogAuthor::insert( conn, @@ -818,7 +748,7 @@ pub(crate) mod tests { author_id: user[1].id, is_owner: false, }, - ); + ).unwrap(); BlogAuthor::insert( conn, @@ -827,13 +757,13 @@ pub(crate) mod tests { author_id: user[0].id, is_owner: true, }, - ); + ).unwrap(); - user[0].delete(conn, &searcher); - assert!(Blog::get(conn, blog[0].id).is_some()); - assert!(Blog::get(conn, blog[1].id).is_none()); - user[1].delete(conn, &searcher); - assert!(Blog::get(conn, blog[0].id).is_none()); + user[0].delete(conn, &searcher).unwrap(); + assert!(Blog::get(conn, blog[0].id).is_ok()); + assert!(Blog::get(conn, blog[1].id).is_err()); + user[1].delete(conn, &searcher).unwrap(); + assert!(Blog::get(conn, blog[0].id).is_err()); Ok(()) }); diff --git a/plume-models/src/comment_seers.rs b/plume-models/src/comment_seers.rs index 9083eecd..89c5d283 100644 --- a/plume-models/src/comment_seers.rs +++ b/plume-models/src/comment_seers.rs @@ -3,7 +3,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use comments::Comment; use schema::comment_seers; use users::User; -use Connection; +use {Connection, Error, Result}; #[derive(Queryable, Serialize, Clone)] pub struct CommentSeers { @@ -22,11 +22,11 @@ pub struct NewCommentSeers { impl CommentSeers { insert!(comment_seers, NewCommentSeers); - pub fn can_see(conn: &Connection, c: &Comment, u: &User) -> bool { - !comment_seers::table.filter(comment_seers::comment_id.eq(c.id)) + pub fn can_see(conn: &Connection, c: &Comment, u: &User) -> Result { + comment_seers::table.filter(comment_seers::comment_id.eq(c.id)) .filter(comment_seers::user_id.eq(u.id)) .load::(conn) - .expect("Comment::get_responses: loading error") - .is_empty() + .map_err(Error::from) + .map(|r| !r.is_empty()) } } diff --git a/plume-models/src/comments.rs b/plume-models/src/comments.rs index abcf9618..90204825 100644 --- a/plume-models/src/comments.rs +++ b/plume-models/src/comments.rs @@ -18,7 +18,7 @@ use posts::Post; use safe_string::SafeString; use schema::comments; use users::User; -use Connection; +use {Connection, Error, Result}; #[derive(Queryable, Identifiable, Serialize, Clone)] pub struct Comment { @@ -53,150 +53,125 @@ impl Comment { list_by!(comments, list_by_post, post_id as i32); find_by!(comments, find_by_ap_url, ap_url as &str); - pub fn get_author(&self, conn: &Connection) -> User { - User::get(conn, self.author_id).expect("Comment::get_author: author error") + pub fn get_author(&self, conn: &Connection) -> Result { + User::get(conn, self.author_id) } - pub fn get_post(&self, conn: &Connection) -> Post { - Post::get(conn, self.post_id).expect("Comment::get_post: post error") + pub fn get_post(&self, conn: &Connection) -> Result { + Post::get(conn, self.post_id) } - pub fn count_local(conn: &Connection) -> i64 { + pub fn count_local(conn: &Connection) -> Result { use schema::users; let local_authors = users::table - .filter(users::instance_id.eq(Instance::local_id(conn))) + .filter(users::instance_id.eq(Instance::get_local(conn)?.id)) .select(users::id); comments::table .filter(comments::author_id.eq_any(local_authors)) .count() .get_result(conn) - .expect("Comment::count_local: loading error") + .map_err(Error::from) } - pub fn get_responses(&self, conn: &Connection) -> Vec { + pub fn get_responses(&self, conn: &Connection) -> Result> { comments::table.filter(comments::in_response_to_id.eq(self.id)) .load::(conn) - .expect("Comment::get_responses: loading error") + .map_err(Error::from) } - pub fn update_ap_url(&self, conn: &Connection) -> Comment { + pub fn update_ap_url(&self, conn: &Connection) -> Result { if self.ap_url.is_none() { diesel::update(self) - .set(comments::ap_url.eq(self.compute_id(conn))) - .execute(conn) - .expect("Comment::update_ap_url: update error"); - Comment::get(conn, self.id).expect("Comment::update_ap_url: get error") + .set(comments::ap_url.eq(self.compute_id(conn)?)) + .execute(conn)?; + Comment::get(conn, self.id) } else { - self.clone() + Ok(self.clone()) } } - pub fn compute_id(&self, conn: &Connection) -> String { - format!("{}comment/{}", self.get_post(conn).ap_url, self.id) + pub fn compute_id(&self, conn: &Connection) -> Result { + Ok(format!("{}comment/{}", self.get_post(conn)?.ap_url, self.id)) } pub fn can_see(&self, conn: &Connection, user: Option<&User>) -> bool { self.public_visibility || - user.as_ref().map(|u| CommentSeers::can_see(conn, self, u)).unwrap_or(false) + user.as_ref().map(|u| CommentSeers::can_see(conn, self, u).unwrap_or(false)) + .unwrap_or(false) } - pub fn to_activity(&self, conn: &Connection) -> Note { + pub fn to_activity(&self, conn: &Connection) -> Result { let (html, mentions, _hashtags) = utils::md_to_html(self.content.get().as_ref(), - &Instance::get_local(conn) - .expect("Comment::to_activity: instance error") - .public_domain); + &Instance::get_local(conn)?.public_domain); - let author = User::get(conn, self.author_id).expect("Comment::to_activity: author error"); + let author = User::get(conn, self.author_id)?; let mut note = Note::default(); let to = vec![Id::new(PUBLIC_VISIBILTY.to_string())]; note.object_props - .set_id_string(self.ap_url.clone().unwrap_or_default()) - .expect("Comment::to_activity: id error"); + .set_id_string(self.ap_url.clone().unwrap_or_default())?; note.object_props - .set_summary_string(self.spoiler_text.clone()) - .expect("Comment::to_activity: summary error"); + .set_summary_string(self.spoiler_text.clone())?; note.object_props - .set_content_string(html) - .expect("Comment::to_activity: content error"); + .set_content_string(html)?; note.object_props .set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else( - || { - Post::get(conn, self.post_id) - .expect("Comment::to_activity: post error") - .ap_url - }, - |id| { - let comm = - Comment::get(conn, id).expect("Comment::to_activity: comment error"); - comm.ap_url.clone().unwrap_or_else(|| comm.compute_id(conn)) - }, - ))) - .expect("Comment::to_activity: in_reply_to error"); + || Ok(Post::get(conn, self.post_id)?.ap_url), + |id| Ok(Comment::get(conn, id)?.compute_id(conn)?) as Result, + )?))?; note.object_props - .set_published_string(chrono::Utc::now().to_rfc3339()) - .expect("Comment::to_activity: published error"); + .set_published_string(chrono::Utc::now().to_rfc3339())?; note.object_props - .set_attributed_to_link(author.clone().into_id()) - .expect("Comment::to_activity: attributed_to error"); + .set_attributed_to_link(author.clone().into_id())?; note.object_props - .set_to_link_vec(to.clone()) - .expect("Comment::to_activity: to error"); + .set_to_link_vec(to.clone())?; note.object_props .set_tag_link_vec( mentions .into_iter() - .map(|m| Mention::build_activity(conn, &m)) + .filter_map(|m| Mention::build_activity(conn, &m).ok()) .collect::>(), - ) - .expect("Comment::to_activity: tag error"); - note + )?; + Ok(note) } - pub fn create_activity(&self, conn: &Connection) -> Create { + pub fn create_activity(&self, conn: &Connection) -> Result { let author = - User::get(conn, self.author_id).expect("Comment::create_activity: author error"); + User::get(conn, self.author_id)?; - let note = self.to_activity(conn); + let note = self.to_activity(conn)?; let mut act = Create::default(); act.create_props - .set_actor_link(author.into_id()) - .expect("Comment::create_activity: actor error"); + .set_actor_link(author.into_id())?; act.create_props - .set_object_object(note.clone()) - .expect("Comment::create_activity: object error"); + .set_object_object(note.clone())?; act.object_props .set_id_string(format!( "{}/activity", self.ap_url - .clone() - .expect("Comment::create_activity: ap_url error") - )) - .expect("Comment::create_activity: id error"); + .clone()?, + ))?; act.object_props .set_to_link_vec( note.object_props - .to_link_vec::() - .expect("Comment::create_activity: id error"), - ) - .expect("Comment::create_activity: to error"); + .to_link_vec::()?, + )?; act.object_props - .set_cc_link_vec::(vec![]) - .expect("Comment::create_activity: cc error"); - act + .set_cc_link_vec::(vec![])?; + Ok(act) } } impl FromActivity for Comment { - fn from_activity(conn: &Connection, note: Note, actor: Id) -> Comment { + type Error = Error; + + fn from_activity(conn: &Connection, note: Note, actor: Id) -> Result { let comm = { let previous_url = note .object_props .in_reply_to - .as_ref() - .expect("Comment::from_activity: not an answer error") - .as_str() - .expect("Comment::from_activity: in_reply_to parsing error"); + .as_ref()? + .as_str()?; let previous_comment = Comment::find_by_ap_url(conn, previous_url); let is_public = |v: &Option| match v.as_ref().unwrap_or(&serde_json::Value::Null) { @@ -216,42 +191,35 @@ impl FromActivity for Comment { content: SafeString::new( ¬e .object_props - .content_string() - .expect("Comment::from_activity: content deserialization error"), + .content_string()? ), spoiler_text: note .object_props .summary_string() .unwrap_or_default(), ap_url: note.object_props.id_string().ok(), - in_response_to_id: previous_comment.clone().map(|c| c.id), - post_id: previous_comment.map(|c| c.post_id).unwrap_or_else(|| { - Post::find_by_ap_url(conn, previous_url) - .expect("Comment::from_activity: post error") - .id - }), - author_id: User::from_url(conn, actor.as_ref()) - .expect("Comment::from_activity: author error") - .id, + in_response_to_id: previous_comment.iter().map(|c| c.id).next(), + post_id: previous_comment.map(|c| c.post_id) + .or_else(|_| Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result)?, + author_id: User::from_url(conn, actor.as_ref())?.id, sensitive: false, // "sensitive" is not a standard property, we need to think about how to support it with the activitypub crate public_visibility }, - ); + )?; // save mentions if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() { for tag in tags { serde_json::from_value::(tag) - .map(|m| { - let author = &Post::get(conn, comm.post_id) - .expect("Comment::from_activity: error") - .get_authors(conn)[0]; + .map_err(Error::from) + .and_then(|m| { + let author = &Post::get(conn, comm.post_id)? + .get_authors(conn)?[0]; let not_author = m .link_props - .href_string() - .expect("Comment::from_activity: no href error") + .href_string()? != author.ap_url.clone(); - Mention::from_activity(conn, &m, comm.id, false, not_author) + Ok(Mention::from_activity(conn, &m, comm.id, false, not_author)?) }) .ok(); } @@ -279,13 +247,13 @@ impl FromActivity for Comment { let receivers_ap_url = to.chain(cc).chain(bto).chain(bcc) .collect::>()//remove duplicates (don't do a query more than once) .into_iter() - .map(|v| if let Some(user) = User::from_url(conn,&v) { + .map(|v| if let Ok(user) = User::from_url(conn,&v) { vec![user] } else { vec![]// TODO try to fetch collection }) .flatten() - .filter(|u| u.get_instance(conn).local) + .filter(|u| u.get_instance(conn).map(|i| i.local).unwrap_or(false)) .collect::>();//remove duplicates (prevent db error) for user in &receivers_ap_url { @@ -295,18 +263,20 @@ impl FromActivity for Comment { comment_id: comm.id, user_id: user.id } - ); + )?; } } - comm.notify(conn); - comm + comm.notify(conn)?; + Ok(comm) } } impl Notify for Comment { - fn notify(&self, conn: &Connection) { - for author in self.get_post(conn).get_authors(conn) { + type Error = Error; + + fn notify(&self, conn: &Connection) -> Result<()> { + for author in self.get_post(conn)?.get_authors(conn)? { Notification::insert( conn, NewNotification { @@ -314,8 +284,9 @@ impl Notify for Comment { object_id: self.id, user_id: author.id, }, - ); + )?; } + Ok(()) } } @@ -325,67 +296,64 @@ pub struct CommentTree { } impl CommentTree { - pub fn from_post(conn: &Connection, p: &Post, user: Option<&User>) -> Vec { - Comment::list_by_post(conn, p.id).into_iter() + pub fn from_post(conn: &Connection, p: &Post, user: Option<&User>) -> Result> { + Ok(Comment::list_by_post(conn, p.id)?.into_iter() .filter(|c| c.in_response_to_id.is_none()) .filter(|c| c.can_see(conn, user)) - .map(|c| Self::from_comment(conn, c, user)) - .collect() + .filter_map(|c| Self::from_comment(conn, c, user).ok()) + .collect()) } - pub fn from_comment(conn: &Connection, comment: Comment, user: Option<&User>) -> Self { - let responses = comment.get_responses(conn).into_iter() + pub fn from_comment(conn: &Connection, comment: Comment, user: Option<&User>) -> Result { + let responses = comment.get_responses(conn)?.into_iter() .filter(|c| c.can_see(conn, user)) - .map(|c| Self::from_comment(conn, c, user)) + .filter_map(|c| Self::from_comment(conn, c, user).ok()) .collect(); - CommentTree { + Ok(CommentTree { comment, responses, - } + }) } } impl<'a> Deletable for Comment { - fn delete(&self, conn: &Connection) -> Delete { + type Error = Error; + + fn delete(&self, conn: &Connection) -> Result { let mut act = Delete::default(); act.delete_props - .set_actor_link(self.get_author(conn).into_id()) - .expect("Comment::delete: actor error"); + .set_actor_link(self.get_author(conn)?.into_id())?; let mut tombstone = Tombstone::default(); tombstone .object_props - .set_id_string(self.ap_url.clone().expect("Comment::delete: no ap_url")) - .expect("Comment::delete: object.id error"); + .set_id_string(self.ap_url.clone()?)?; act.delete_props - .set_object_object(tombstone) - .expect("Comment::delete: object error"); + .set_object_object(tombstone)?; act.object_props - .set_id_string(format!("{}#delete", self.ap_url.clone().unwrap())) - .expect("Comment::delete: id error"); + .set_id_string(format!("{}#delete", self.ap_url.clone().unwrap()))?; act.object_props - .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILTY)]) - .expect("Comment::delete: to error"); + .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILTY)])?; - for m in Mention::list_for_comment(&conn, self.id) { - m.delete(conn); + for m in Mention::list_for_comment(&conn, self.id)? { + m.delete(conn)?; } diesel::update(comments::table).filter(comments::in_response_to_id.eq(self.id)) .set(comments::in_response_to_id.eq(self.in_response_to_id)) - .execute(conn) - .expect("Comment::delete: DB error could not update other comments"); + .execute(conn)?; diesel::delete(self) - .execute(conn) - .expect("Comment::delete: DB error"); - act + .execute(conn)?; + Ok(act) } - fn delete_id(id: &str, actor_id: &str, conn: &Connection) { - let actor = User::find_by_ap_url(conn, actor_id); - let comment = Comment::find_by_ap_url(conn, id); - if let Some(comment) = comment.filter(|c| c.author_id == actor.unwrap().id) { - comment.delete(conn); + fn delete_id(id: &str, actor_id: &str, conn: &Connection) -> Result { + let actor = User::find_by_ap_url(conn, actor_id)?; + let comment = Comment::find_by_ap_url(conn, id)?; + if comment.author_id == actor.id { + comment.delete(conn) + } else { + Err(Error::Unauthorized) } } } diff --git a/plume-models/src/db_conn.rs b/plume-models/src/db_conn.rs index 9bdd4c9a..05378a63 100644 --- a/plume-models/src/db_conn.rs +++ b/plume-models/src/db_conn.rs @@ -1,4 +1,6 @@ -use diesel::{dsl::sql_query, r2d2::{ConnectionManager, CustomizeConnection, Error as ConnError, Pool, PooledConnection}, ConnectionError, RunQueryDsl}; +use diesel::{r2d2::{ConnectionManager, CustomizeConnection, Error as ConnError, Pool, PooledConnection}}; +#[cfg(feature = "sqlite")] +use diesel::{dsl::sql_query, ConnectionError, RunQueryDsl}; use rocket::{ http::Status, request::{self, FromRequest}, diff --git a/plume-models/src/follows.rs b/plume-models/src/follows.rs index c7b14a5d..9c194a93 100644 --- a/plume-models/src/follows.rs +++ b/plume-models/src/follows.rs @@ -15,7 +15,7 @@ use plume_common::activity_pub::{ }; use schema::follows; use users::User; -use {ap_url, Connection, BASE_URL}; +use {ap_url, Connection, BASE_URL, Error, Result}; #[derive(Clone, Queryable, Identifiable, Associations)] #[belongs_to(User, foreign_key = "following_id")] @@ -39,37 +39,30 @@ impl Follow { get!(follows); find_by!(follows, find_by_ap_url, ap_url as &str); - pub fn find(conn: &Connection, from: i32, to: i32) -> Option { + pub fn find(conn: &Connection, from: i32, to: i32) -> Result { follows::table .filter(follows::follower_id.eq(from)) .filter(follows::following_id.eq(to)) .get_result(conn) - .ok() + .map_err(Error::from) } - pub fn to_activity(&self, conn: &Connection) -> FollowAct { - let user = User::get(conn, self.follower_id) - .expect("Follow::to_activity: actor not found error"); - let target = User::get(conn, self.following_id) - .expect("Follow::to_activity: target not found error"); + pub fn to_activity(&self, conn: &Connection) -> Result { + let user = User::get(conn, self.follower_id)?; + let target = User::get(conn, self.following_id)?; let mut act = FollowAct::default(); act.follow_props - .set_actor_link::(user.clone().into_id()) - .expect("Follow::to_activity: actor error"); + .set_actor_link::(user.clone().into_id())?; act.follow_props - .set_object_link::(target.clone().into_id()) - .expect("Follow::to_activity: object error"); + .set_object_link::(target.clone().into_id())?; act.object_props - .set_id_string(self.ap_url.clone()) - .expect("Follow::to_activity: id error"); + .set_id_string(self.ap_url.clone())?; act.object_props - .set_to_link(target.into_id()) - .expect("Follow::to_activity: target error"); + .set_to_link(target.into_id())?; act.object_props - .set_cc_link_vec::(vec![]) - .expect("Follow::to_activity: cc error"); - act + .set_cc_link_vec::(vec![])?; + Ok(act) } /// from -> The one sending the follow request @@ -81,78 +74,69 @@ impl Follow { follow: FollowAct, from_id: i32, target_id: i32, - ) -> Follow { + ) -> Result { let res = Follow::insert( conn, NewFollow { follower_id: from_id, following_id: target_id, - ap_url: follow.object_props.id_string().expect("Follow::accept_follow: get id error"), + ap_url: follow.object_props.id_string()?, }, - ); + )?; let mut accept = Accept::default(); let accept_id = ap_url(&format!("{}/follow/{}/accept", BASE_URL.as_str(), &res.id)); accept .object_props - .set_id_string(accept_id) - .expect("Follow::accept_follow: set id error"); + .set_id_string(accept_id)?; accept .object_props - .set_to_link(from.clone().into_id()) - .expect("Follow::accept_follow: to error"); + .set_to_link(from.clone().into_id())?; accept .object_props - .set_cc_link_vec::(vec![]) - .expect("Follow::accept_follow: cc error"); + .set_cc_link_vec::(vec![])?; accept .accept_props - .set_actor_link::(target.clone().into_id()) - .expect("Follow::accept_follow: actor error"); + .set_actor_link::(target.clone().into_id())?; accept .accept_props - .set_object_object(follow) - .expect("Follow::accept_follow: object error"); + .set_object_object(follow)?; broadcast(&*target, accept, vec![from.clone()]); - res + Ok(res) } } impl FromActivity for Follow { - fn from_activity(conn: &Connection, follow: FollowAct, _actor: Id) -> Follow { + type Error = Error; + + fn from_activity(conn: &Connection, follow: FollowAct, _actor: Id) -> Result { let from_id = follow .follow_props .actor_link::() .map(|l| l.into()) - .unwrap_or_else(|_| { - follow - .follow_props - .actor_object::() - .expect("Follow::from_activity: actor not found error") - .object_props - .id_string() - .expect("Follow::from_activity: actor not found error") - }); + .or_else(|_| Ok(follow + .follow_props + .actor_object::()? + .object_props + .id_string()?) as Result)?; let from = - User::from_url(conn, &from_id).expect("Follow::from_activity: actor not found error"); + User::from_url(conn, &from_id)?; match User::from_url( conn, follow .follow_props .object - .as_str() - .expect("Follow::from_activity: target url parsing error"), + .as_str()?, ) { - Some(user) => Follow::accept_follow(conn, &from, &user, follow, from.id, user.id), - None => { + Ok(user) => Follow::accept_follow(conn, &from, &user, follow, from.id, user.id), + Err(_) => { let blog = Blog::from_url( conn, follow .follow_props .object - .as_str() - .expect("Follow::from_activity: target url parsing error"), - ).expect("Follow::from_activity: target not found error"); + .as_str()?, + )?; Follow::accept_follow(conn, &from, &blog, follow, from.id, blog.id) } } @@ -160,7 +144,9 @@ impl FromActivity for Follow { } impl Notify for Follow { - fn notify(&self, conn: &Connection) { + type Error = Error; + + fn notify(&self, conn: &Connection) -> Result<()> { Notification::insert( conn, NewNotification { @@ -168,47 +154,43 @@ impl Notify for Follow { object_id: self.id, user_id: self.following_id, }, - ); + ).map(|_| ()) } } impl Deletable for Follow { - fn delete(&self, conn: &Connection) -> Undo { + type Error = Error; + + fn delete(&self, conn: &Connection) -> Result { diesel::delete(self) - .execute(conn) - .expect("Follow::delete: follow deletion error"); + .execute(conn)?; // delete associated notification if any - if let Some(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) { + if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) { diesel::delete(¬if) - .execute(conn) - .expect("Follow::delete: notification deletion error"); + .execute(conn)?; } let mut undo = Undo::default(); undo.undo_props .set_actor_link( - User::get(conn, self.follower_id) - .expect("Follow::delete: actor error") + User::get(conn, self.follower_id)? .into_id(), - ) - .expect("Follow::delete: actor error"); + )?; undo.object_props - .set_id_string(format!("{}/undo", self.ap_url)) - .expect("Follow::delete: id error"); + .set_id_string(format!("{}/undo", self.ap_url))?; undo.undo_props - .set_object_link::(self.clone().into_id()) - .expect("Follow::delete: object error"); - undo + .set_object_link::(self.clone().into_id())?; + Ok(undo) } - fn delete_id(id: &str, actor_id: &str, conn: &Connection) { - if let Some(follow) = Follow::find_by_ap_url(conn, id) { - if let Some(user) = User::find_by_ap_url(conn, actor_id) { - if user.id == follow.follower_id { - follow.delete(conn); - } - } + fn delete_id(id: &str, actor_id: &str, conn: &Connection) -> Result { + let follow = Follow::find_by_ap_url(conn, id)?; + let user = User::find_by_ap_url(conn, actor_id)?; + if user.id == follow.follower_id { + follow.delete(conn) + } else { + Err(Error::Unauthorized) } } } diff --git a/plume-models/src/instance.rs b/plume-models/src/instance.rs index 6d01ab19..5936d030 100644 --- a/plume-models/src/instance.rs +++ b/plume-models/src/instance.rs @@ -7,7 +7,7 @@ use plume_common::utils::md_to_html; use safe_string::SafeString; use schema::{instances, users}; use users::User; -use Connection; +use {Connection, Error, Result}; #[derive(Clone, Identifiable, Queryable, Serialize)] pub struct Instance { @@ -40,80 +40,73 @@ pub struct NewInstance { } impl Instance { - pub fn get_local(conn: &Connection) -> Option { + pub fn get_local(conn: &Connection) -> Result { instances::table .filter(instances::local.eq(true)) .limit(1) - .load::(conn) - .expect("Instance::get_local: loading error") + .load::(conn)? .into_iter() - .nth(0) + .nth(0).ok_or(Error::NotFound) } - pub fn get_remotes(conn: &Connection) -> Vec { + pub fn get_remotes(conn: &Connection) -> Result> { instances::table .filter(instances::local.eq(false)) .load::(conn) - .expect("Instance::get_remotes: loading error") + .map_err(Error::from) } - pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Vec { + pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Result> { instances::table .order(instances::public_domain.asc()) .offset(min.into()) .limit((max - min).into()) .load::(conn) - .expect("Instance::page: loading error") - } - - pub fn local_id(conn: &Connection) -> i32 { - Instance::get_local(conn) - .expect("Instance::local_id: local instance not found error") - .id + .map_err(Error::from) } insert!(instances, NewInstance); get!(instances); find_by!(instances, find_by_domain, public_domain as &str); - pub fn toggle_block(&self, conn: &Connection) { + pub fn toggle_block(&self, conn: &Connection) -> Result<()> { diesel::update(self) .set(instances::blocked.eq(!self.blocked)) .execute(conn) - .expect("Instance::toggle_block: update error"); + .map(|_| ()) + .map_err(Error::from) } /// id: AP object id - pub fn is_blocked(conn: &Connection, id: &str) -> bool { + pub fn is_blocked(conn: &Connection, id: &str) -> Result { for block in instances::table .filter(instances::blocked.eq(true)) - .get_results::(conn) - .expect("Instance::is_blocked: loading error") + .get_results::(conn)? { if id.starts_with(&format!("https://{}/", block.public_domain)) { - return true; + return Ok(true); } } - false + Ok(false) } - pub fn has_admin(&self, conn: &Connection) -> bool { - !users::table + pub fn has_admin(&self, conn: &Connection) -> Result { + users::table .filter(users::instance_id.eq(self.id)) .filter(users::is_admin.eq(true)) .load::(conn) - .expect("Instance::has_admin: loading error") - .is_empty() + .map_err(Error::from) + .map(|r| !r.is_empty()) } - pub fn main_admin(&self, conn: &Connection) -> User { + pub fn main_admin(&self, conn: &Connection) -> Result { users::table .filter(users::instance_id.eq(self.id)) .filter(users::is_admin.eq(true)) .limit(1) .get_result::(conn) - .expect("Instance::main_admin: loading error") + .map_err(Error::from) } pub fn compute_box( @@ -138,7 +131,7 @@ impl Instance { open_registrations: bool, short_description: SafeString, long_description: SafeString, - ) { + ) -> Result<()> { let (sd, _, _) = md_to_html(short_description.as_ref(), &self.public_domain); let (ld, _, _) = md_to_html(long_description.as_ref(), &self.public_domain); diesel::update(self) @@ -151,14 +144,15 @@ impl Instance { instances::long_description_html.eq(ld), )) .execute(conn) - .expect("Instance::update: update error"); + .map(|_| ()) + .map_err(Error::from) } - pub fn count(conn: &Connection) -> i64 { + pub fn count(conn: &Connection) -> Result { instances::table .count() .get_result(conn) - .expect("Instance::count: counting error") + .map_err(Error::from) } } @@ -220,7 +214,7 @@ pub(crate) mod tests { ( inst.clone(), Instance::find_by_domain(conn, &inst.public_domain) - .unwrap_or_else(|| Instance::insert(conn, inst)), + .unwrap_or_else(|_| Instance::insert(conn, inst).unwrap()), ) }) .collect() @@ -253,7 +247,6 @@ pub(crate) mod tests { assert_eq!(res.long_description_html.get(), &inserted.long_description_html); assert_eq!(res.short_description_html.get(), &inserted.short_description_html); - assert_eq!(Instance::local_id(conn), res.id); Ok(()) }); } @@ -263,9 +256,9 @@ pub(crate) mod tests { let conn = &db(); conn.test_transaction::<_, (), _>(|| { let inserted = fill_database(conn); - assert_eq!(Instance::count(conn), inserted.len() as i64); + assert_eq!(Instance::count(conn).unwrap(), inserted.len() as i64); - let res = Instance::get_remotes(conn); + let res = Instance::get_remotes(conn).unwrap(); assert_eq!( res.len(), inserted.iter().filter(|(inst, _)| !inst.local).count() @@ -293,15 +286,15 @@ pub(crate) mod tests { assert_eq!(&newinst.short_description_html, inst.short_description_html.get()); }); - let page = Instance::page(conn, (0, 2)); + let page = Instance::page(conn, (0, 2)).unwrap(); assert_eq!(page.len(), 2); let page1 = &page[0]; let page2 = &page[1]; assert!(page1.public_domain <= page2.public_domain); - let mut last_domaine: String = Instance::page(conn, (0, 1))[0].public_domain.clone(); + let mut last_domaine: String = Instance::page(conn, (0, 1)).unwrap()[0].public_domain.clone(); for i in 1..inserted.len() as i32 { - let page = Instance::page(conn, (i, i + 1)); + let page = Instance::page(conn, (i, i + 1)).unwrap(); assert_eq!(page.len(), 1); assert!(last_domaine <= page[0].public_domain); last_domaine = page[0].public_domain.clone(); @@ -320,7 +313,7 @@ pub(crate) mod tests { let inst_list = &inst_list[1..]; let blocked = inst.blocked; - inst.toggle_block(conn); + inst.toggle_block(conn).unwrap(); let inst = Instance::get(conn, inst.id).unwrap(); assert_eq!(inst.blocked, !blocked); assert_eq!( @@ -333,25 +326,25 @@ pub(crate) mod tests { 0 ); assert_eq!( - Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain)), + Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain)).unwrap(), inst.blocked ); assert_eq!( - Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain)), + Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain)).unwrap(), Instance::find_by_domain(conn, &format!("{}a", inst.public_domain)) .map(|inst| inst.blocked) .unwrap_or(false) ); - inst.toggle_block(conn); + inst.toggle_block(conn).unwrap(); let inst = Instance::get(conn, inst.id).unwrap(); assert_eq!(inst.blocked, blocked); assert_eq!( - Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain)), + Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain)).unwrap(), inst.blocked ); assert_eq!( - Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain)), + Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain)).unwrap(), Instance::find_by_domain(conn, &format!("{}a", inst.public_domain)) .map(|inst| inst.blocked) .unwrap_or(false) @@ -382,7 +375,7 @@ pub(crate) mod tests { false, SafeString::new("[short](#link)"), SafeString::new("[long_description](/with_link)"), - ); + ).unwrap(); let inst = Instance::get(conn, inst.id).unwrap(); assert_eq!(inst.name, "NewName".to_owned()); assert_eq!(inst.open_registrations, false); diff --git a/plume-models/src/lib.rs b/plume-models/src/lib.rs index 240ef38d..d3732d45 100644 --- a/plume-models/src/lib.rs +++ b/plume-models/src/lib.rs @@ -1,4 +1,5 @@ #![allow(proc_macro_derive_resolution_fallback)] // This can be removed after diesel-1.4 +#![feature(try_trait)] extern crate activitypub; extern crate ammonia; @@ -47,6 +48,102 @@ pub type Connection = diesel::SqliteConnection; #[cfg(all(not(feature = "sqlite"), feature = "postgres"))] pub type Connection = diesel::PgConnection; +/// All the possible errors that can be encoutered in this crate +#[derive(Debug)] +pub enum Error { + Db(diesel::result::Error), + InvalidValue, + Io(std::io::Error), + MissingApProperty, + NotFound, + Request, + SerDe, + Search(search::SearcherError), + Signature, + Unauthorized, + Url, + Webfinger, +} + +impl From for Error { + fn from(_: bcrypt::BcryptError) -> Self { + Error::Signature + } +} + +impl From for Error { + fn from(_: openssl::error::ErrorStack) -> Self { + Error::Signature + } +} + +impl From for Error { + fn from(err: diesel::result::Error) -> Self { + Error::Db(err) + } +} + +impl From for Error { + fn from(_: std::option::NoneError) -> Self { + Error::NotFound + } +} + +impl From for Error { + fn from(_: url::ParseError) -> Self { + Error::Url + } +} + +impl From for Error { + fn from(_: serde_json::Error) -> Self { + Error::SerDe + } +} + +impl From for Error { + fn from(_: reqwest::Error) -> Self { + Error::Request + } +} + +impl From for Error { + fn from(_: reqwest::header::InvalidHeaderValue) -> Self { + Error::Request + } +} + +impl From for Error { + fn from(err: activitypub::Error) -> Self { + match err { + activitypub::Error::NotFound => Error::MissingApProperty, + _ => Error::SerDe, + } + } +} + +impl From for Error { + fn from(_: webfinger::WebfingerError) -> Self { + Error::Webfinger + } +} + +impl From for Error { + fn from(err: search::SearcherError) -> Self { + Error::Search(err) + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } +} + +pub type Result = std::result::Result; + +pub type ApiResult = std::result::Result; + /// Adds a function to a model, that returns the first /// matching row for a given list of fields. /// @@ -63,13 +160,14 @@ pub type Connection = diesel::PgConnection; macro_rules! find_by { ($table:ident, $fn:ident, $($col:ident as $type:ty),+) => { /// Try to find a $table with a given $col - pub fn $fn(conn: &crate::Connection, $($col: $type),+) -> Option { + pub fn $fn(conn: &crate::Connection, $($col: $type),+) -> Result { $table::table $(.filter($table::$col.eq($col)))+ .limit(1) - .load::(conn) - .expect("macro::find_by: Error loading $table by $col") - .into_iter().nth(0) + .load::(conn)? + .into_iter() + .next() + .ok_or(Error::NotFound) } }; } @@ -89,11 +187,11 @@ macro_rules! find_by { macro_rules! list_by { ($table:ident, $fn:ident, $($col:ident as $type:ty),+) => { /// Try to find a $table with a given $col - pub fn $fn(conn: &crate::Connection, $($col: $type),+) -> Vec { + pub fn $fn(conn: &crate::Connection, $($col: $type),+) -> Result> { $table::table $(.filter($table::$col.eq($col)))+ .load::(conn) - .expect("macro::list_by: Error loading $table by $col") + .map_err(Error::from) } }; } @@ -112,14 +210,14 @@ macro_rules! list_by { /// ``` macro_rules! get { ($table:ident) => { - pub fn get(conn: &crate::Connection, id: i32) -> Option { + pub fn get(conn: &crate::Connection, id: i32) -> Result { $table::table .filter($table::id.eq(id)) .limit(1) - .load::(conn) - .expect("macro::get: Error loading $table by id") + .load::(conn)? .into_iter() - .nth(0) + .next() + .ok_or(Error::NotFound) } }; } @@ -140,11 +238,10 @@ macro_rules! insert { ($table:ident, $from:ident) => { last!($table); - pub fn insert(conn: &crate::Connection, new: $from) -> Self { + pub fn insert(conn: &crate::Connection, new: $from) -> Result { diesel::insert_into($table::table) .values(new) - .execute(conn) - .expect("macro::insert: Error saving new $table"); + .execute(conn)?; Self::last(conn) } }; @@ -164,19 +261,14 @@ macro_rules! insert { /// ``` macro_rules! last { ($table:ident) => { - pub fn last(conn: &crate::Connection) -> Self { + pub fn last(conn: &crate::Connection) -> Result { $table::table .order_by($table::id.desc()) .limit(1) - .load::(conn) - .expect(concat!( - "macro::last: Error getting last ", - stringify!($table) - )) - .iter() + .load::(conn)? + .into_iter() .next() - .expect(concat!("macro::last: No last ", stringify!($table))) - .clone() + .ok_or(Error::NotFound) } }; } diff --git a/plume-models/src/likes.rs b/plume-models/src/likes.rs index 0480e1e6..f501657a 100644 --- a/plume-models/src/likes.rs +++ b/plume-models/src/likes.rs @@ -10,7 +10,7 @@ use plume_common::activity_pub::{ use posts::Post; use schema::likes; use users::User; -use Connection; +use {Connection, Error, Result}; #[derive(Clone, Queryable, Identifiable)] pub struct Like { @@ -35,69 +35,64 @@ impl Like { find_by!(likes, find_by_ap_url, ap_url as &str); find_by!(likes, find_by_user_on_post, user_id as i32, post_id as i32); - pub fn to_activity(&self, conn: &Connection) -> activity::Like { + pub fn to_activity(&self, conn: &Connection) -> Result { let mut act = activity::Like::default(); act.like_props .set_actor_link( - User::get(conn, self.user_id) - .expect("Like::to_activity: user error") + User::get(conn, self.user_id)? .into_id(), - ) - .expect("Like::to_activity: actor error"); + )?; act.like_props .set_object_link( - Post::get(conn, self.post_id) - .expect("Like::to_activity: post error") + Post::get(conn, self.post_id)? .into_id(), - ) - .expect("Like::to_activity: object error"); + )?; act.object_props - .set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())) - .expect("Like::to_activity: to error"); + .set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?; act.object_props - .set_cc_link_vec::(vec![]) - .expect("Like::to_activity: cc error"); + .set_cc_link_vec::(vec![])?; act.object_props - .set_id_string(self.ap_url.clone()) - .expect("Like::to_activity: id error"); + .set_id_string(self.ap_url.clone())?; - act + Ok(act) } } impl FromActivity for Like { - fn from_activity(conn: &Connection, like: activity::Like, _actor: Id) -> Like { + type Error = Error; + + fn from_activity(conn: &Connection, like: activity::Like, _actor: Id) -> Result { let liker = User::from_url( conn, like.like_props .actor - .as_str() - .expect("Like::from_activity: actor error"), - ); + .as_str()?, + )?; let post = Post::find_by_ap_url( conn, like.like_props .object - .as_str() - .expect("Like::from_activity: object error"), - ); + .as_str()?, + )?; let res = Like::insert( conn, NewLike { - post_id: post.expect("Like::from_activity: post error").id, - user_id: liker.expect("Like::from_activity: user error").id, - ap_url: like.object_props.id_string().unwrap_or_default(), + post_id: post.id, + user_id: liker.id, + ap_url: like.object_props.id_string()?, }, - ); - res.notify(conn); - res + )?; + res.notify(conn)?; + Ok(res) } } impl Notify for Like { - fn notify(&self, conn: &Connection) { - let post = Post::get(conn, self.post_id).expect("Like::notify: post error"); - for author in post.get_authors(conn) { + type Error = Error; + + fn notify(&self, conn: &Connection) -> Result<()> { + let post = Post::get(conn, self.post_id)?; + for author in post.get_authors(conn)? { Notification::insert( conn, NewNotification { @@ -105,55 +100,47 @@ impl Notify for Like { object_id: self.id, user_id: author.id, }, - ); + )?; } + Ok(()) } } impl Deletable for Like { - fn delete(&self, conn: &Connection) -> activity::Undo { + type Error = Error; + + fn delete(&self, conn: &Connection) -> Result { diesel::delete(self) - .execute(conn) - .expect("Like::delete: delete error"); + .execute(conn)?; // delete associated notification if any - if let Some(notif) = Notification::find(conn, notification_kind::LIKE, self.id) { + if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) { diesel::delete(¬if) - .execute(conn) - .expect("Like::delete: notification error"); + .execute(conn)?; } let mut act = activity::Undo::default(); act.undo_props - .set_actor_link( - User::get(conn, self.user_id) - .expect("Like::delete: user error") - .into_id(), - ) - .expect("Like::delete: actor error"); + .set_actor_link(User::get(conn, self.user_id)?.into_id(),)?; act.undo_props - .set_object_object(self.to_activity(conn)) - .expect("Like::delete: object error"); + .set_object_object(self.to_activity(conn)?)?; act.object_props - .set_id_string(format!("{}#delete", self.ap_url)) - .expect("Like::delete: id error"); + .set_id_string(format!("{}#delete", self.ap_url))?; act.object_props - .set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())) - .expect("Like::delete: to error"); + .set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?; act.object_props - .set_cc_link_vec::(vec![]) - .expect("Like::delete: cc error"); + .set_cc_link_vec::(vec![])?; - act + Ok(act) } - fn delete_id(id: &str, actor_id: &str, conn: &Connection) { - if let Some(like) = Like::find_by_ap_url(conn, id) { - if let Some(user) = User::find_by_ap_url(conn, actor_id) { - if user.id == like.user_id { - like.delete(conn); - } - } + fn delete_id(id: &str, actor_id: &str, conn: &Connection) -> Result { + let like = Like::find_by_ap_url(conn, id)?; + let user = User::find_by_ap_url(conn, actor_id)?; + if user.id == like.user_id { + like.delete(conn) + } else { + Err(Error::Unauthorized) } } } diff --git a/plume-models/src/medias.rs b/plume-models/src/medias.rs index ccf6516c..9ec2e743 100644 --- a/plume-models/src/medias.rs +++ b/plume-models/src/medias.rs @@ -11,7 +11,7 @@ use instance::Instance; use safe_string::SafeString; use schema::medias; use users::User; -use {ap_url, Connection}; +use {ap_url, Connection, Error, Result}; #[derive(Clone, Identifiable, Queryable, Serialize)] pub struct Media { @@ -50,10 +50,10 @@ impl Media { get!(medias); list_by!(medias, for_user, owner_id as i32); - pub fn list_all_medias(conn: &Connection) -> Vec { + pub fn list_all_medias(conn: &Connection) -> Result> { medias::table .load::(conn) - .expect("Media::list_all_medias: loading error") + .map_err(Error::from) } pub fn category(&self) -> MediaCategory { @@ -70,9 +70,9 @@ impl Media { } } - pub fn preview_html(&self, conn: &Connection) -> SafeString { - let url = self.url(conn); - match self.category() { + pub fn preview_html(&self, conn: &Connection) -> Result { + let url = self.url(conn)?; + Ok(match self.category() { MediaCategory::Image => SafeString::new(&format!( r#"{}"#, url, escape(&self.alt_text), escape(&self.alt_text) @@ -86,12 +86,12 @@ impl Media { url, escape(&self.alt_text) )), MediaCategory::Unknown => SafeString::new(""), - } + }) } - pub fn html(&self, conn: &Connection) -> SafeString { - let url = self.url(conn); - match self.category() { + pub fn html(&self, conn: &Connection) -> Result { + let url = self.url(conn)?; + Ok(match self.category() { MediaCategory::Image => SafeString::new(&format!( r#"{}"#, url, escape(&self.alt_text), escape(&self.alt_text) @@ -105,46 +105,45 @@ impl Media { url, escape(&self.alt_text) )), MediaCategory::Unknown => SafeString::new(""), - } + }) } - pub fn markdown(&self, conn: &Connection) -> SafeString { - let url = self.url(conn); - match self.category() { + pub fn markdown(&self, conn: &Connection) -> Result { + let url = self.url(conn)?; + Ok(match self.category() { MediaCategory::Image => SafeString::new(&format!("![{}]({})", escape(&self.alt_text), url)), - MediaCategory::Audio | MediaCategory::Video => self.html(conn), + MediaCategory::Audio | MediaCategory::Video => self.html(conn)?, MediaCategory::Unknown => SafeString::new(""), - } + }) } - pub fn url(&self, conn: &Connection) -> String { + pub fn url(&self, conn: &Connection) -> Result { if self.is_remote { - self.remote_url.clone().unwrap_or_default() + Ok(self.remote_url.clone().unwrap_or_default()) } else { - ap_url(&format!( + Ok(ap_url(&format!( "{}/{}", - Instance::get_local(conn) - .expect("Media::url: local instance not found error") - .public_domain, + Instance::get_local(conn)?.public_domain, self.file_path - )) + ))) } } - pub fn delete(&self, conn: &Connection) { + pub fn delete(&self, conn: &Connection) -> Result<()> { if !self.is_remote { - fs::remove_file(self.file_path.as_str()).expect("Media::delete: file deletion error"); + fs::remove_file(self.file_path.as_str())?; } diesel::delete(self) .execute(conn) - .expect("Media::delete: database entry deletion error"); + .map(|_| ()) + .map_err(Error::from) } - pub fn save_remote(conn: &Connection, url: String, user: &User) -> Result { + pub fn save_remote(conn: &Connection, url: String, user: &User) -> Result { if url.contains(&['<', '>', '"'][..]) { - Err(()) + Err(Error::Url) } else { - Ok(Media::insert( + Media::insert( conn, NewMedia { file_path: String::new(), @@ -155,19 +154,20 @@ impl Media { content_warning: None, owner_id: user.id, }, - )) + ) } } - pub fn set_owner(&self, conn: &Connection, user: &User) { + pub fn set_owner(&self, conn: &Connection, user: &User) -> Result<()> { diesel::update(self) .set(medias::owner_id.eq(user.id)) .execute(conn) - .expect("Media::set_owner: owner update error"); + .map(|_| ()) + .map_err(Error::from) } // TODO: merge with save_remote? - pub fn from_activity(conn: &Connection, image: &Image) -> Option { + pub fn from_activity(conn: &Connection, image: &Image) -> Result { let remote_url = image.object_props.url_string().ok()?; let ext = remote_url .rsplit('.') @@ -185,7 +185,7 @@ impl Media { .copy_to(&mut dest) .ok()?; - Some(Media::insert( + Media::insert( conn, NewMedia { file_path: path.to_str()?.to_string(), @@ -205,7 +205,7 @@ impl Media { .as_ref(), )?.id, }, - )) + ) } } @@ -265,14 +265,14 @@ pub(crate) mod tests { owner_id: user_two, }, ].into_iter() - .map(|nm| Media::insert(conn, nm)) + .map(|nm| Media::insert(conn, nm).unwrap()) .collect()) } pub(crate) fn clean(conn: &Conn) { //used to remove files generated by tests - for media in Media::list_all_medias(conn) { - media.delete(conn); + for media in Media::list_all_medias(conn).unwrap() { + media.delete(conn).unwrap(); } } @@ -298,10 +298,10 @@ pub(crate) mod tests { content_warning: None, owner_id: user, }, - ); + ).unwrap(); assert!(Path::new(&path).exists()); - media.delete(conn); + media.delete(conn).unwrap(); assert!(!Path::new(&path).exists()); clean(conn); @@ -333,26 +333,26 @@ pub(crate) mod tests { content_warning: None, owner_id: u1.id, }, - ); + ).unwrap(); assert!( - Media::for_user(conn, u1.id) + Media::for_user(conn, u1.id).unwrap() .iter() .any(|m| m.id == media.id) ); assert!( - !Media::for_user(conn, u2.id) + !Media::for_user(conn, u2.id).unwrap() .iter() .any(|m| m.id == media.id) ); - media.set_owner(conn, u2); + media.set_owner(conn, u2).unwrap(); assert!( - !Media::for_user(conn, u1.id) + !Media::for_user(conn, u1.id).unwrap() .iter() .any(|m| m.id == media.id) ); assert!( - Media::for_user(conn, u2.id) + Media::for_user(conn, u2.id).unwrap() .iter() .any(|m| m.id == media.id) ); diff --git a/plume-models/src/mentions.rs b/plume-models/src/mentions.rs index c18ae6e5..e126d384 100644 --- a/plume-models/src/mentions.rs +++ b/plume-models/src/mentions.rs @@ -7,7 +7,7 @@ use plume_common::activity_pub::inbox::Notify; use posts::Post; use schema::mentions; use users::User; -use Connection; +use {Connection, Error, Result}; #[derive(Clone, Queryable, Identifiable, Serialize, Deserialize)] pub struct Mention { @@ -32,54 +32,47 @@ impl Mention { list_by!(mentions, list_for_post, post_id as i32); list_by!(mentions, list_for_comment, comment_id as i32); - pub fn get_mentioned(&self, conn: &Connection) -> Option { + pub fn get_mentioned(&self, conn: &Connection) -> Result { User::get(conn, self.mentioned_id) } - pub fn get_post(&self, conn: &Connection) -> Option { - self.post_id.and_then(|id| Post::get(conn, id)) + pub fn get_post(&self, conn: &Connection) -> Result { + self.post_id.ok_or(Error::NotFound).and_then(|id| Post::get(conn, id)) } - pub fn get_comment(&self, conn: &Connection) -> Option { - self.comment_id.and_then(|id| Comment::get(conn, id)) + pub fn get_comment(&self, conn: &Connection) -> Result { + self.comment_id.ok_or(Error::NotFound).and_then(|id| Comment::get(conn, id)) } - pub fn get_user(&self, conn: &Connection) -> Option { + pub fn get_user(&self, conn: &Connection) -> Result { match self.get_post(conn) { - Some(p) => p.get_authors(conn).into_iter().next(), - None => self.get_comment(conn).map(|c| c.get_author(conn)), + Ok(p) => Ok(p.get_authors(conn)?.into_iter().next()?), + Err(_) => self.get_comment(conn).and_then(|c| c.get_author(conn)), } } - pub fn build_activity(conn: &Connection, ment: &str) -> link::Mention { - let user = User::find_by_fqn(conn, ment); + pub fn build_activity(conn: &Connection, ment: &str) -> Result { + let user = User::find_by_fqn(conn, ment)?; let mut mention = link::Mention::default(); mention .link_props - .set_href_string(user.clone().map(|u| u.ap_url).unwrap_or_default()) - .expect("Mention::build_activity: href error"); + .set_href_string(user.ap_url)?; mention .link_props - .set_name_string(format!("@{}", ment)) - .expect("Mention::build_activity: name error:"); - mention + .set_name_string(format!("@{}", ment))?; + Ok(mention) } - pub fn to_activity(&self, conn: &Connection) -> link::Mention { - let user = self.get_mentioned(conn); + pub fn to_activity(&self, conn: &Connection) -> Result { + let user = self.get_mentioned(conn)?; let mut mention = link::Mention::default(); mention .link_props - .set_href_string(user.clone().map(|u| u.ap_url).unwrap_or_default()) - .expect("Mention::to_activity: href error"); + .set_href_string(user.ap_url.clone())?; mention .link_props - .set_name_string( - user.map(|u| format!("@{}", u.get_fqn(conn))) - .unwrap_or_default(), - ) - .expect("Mention::to_activity: mention error"); - mention + .set_name_string(format!("@{}", user.get_fqn(conn)))?; + Ok(mention) } pub fn from_activity( @@ -88,12 +81,12 @@ impl Mention { inside: i32, in_post: bool, notify: bool, - ) -> Option { + ) -> Result { let ap_url = ment.link_props.href_string().ok()?; let mentioned = User::find_by_ap_url(conn, &ap_url)?; if in_post { - Post::get(conn, inside).map(|post| { + Post::get(conn, inside).and_then(|post| { let res = Mention::insert( conn, NewMention { @@ -101,14 +94,14 @@ impl Mention { post_id: Some(post.id), comment_id: None, }, - ); + )?; if notify { - res.notify(conn); + res.notify(conn)?; } - res + Ok(res) }) } else { - Comment::get(conn, inside).map(|comment| { + Comment::get(conn, inside).and_then(|comment| { let res = Mention::insert( conn, NewMention { @@ -116,37 +109,38 @@ impl Mention { post_id: None, comment_id: Some(comment.id), }, - ); + )?; if notify { - res.notify(conn); + res.notify(conn)?; } - res + Ok(res) }) } } - pub fn delete(&self, conn: &Connection) { + pub fn delete(&self, conn: &Connection) -> Result<()> { //find related notifications and delete them - if let Some(n) = Notification::find(conn, notification_kind::MENTION, self.id) { - n.delete(conn) + if let Ok(n) = Notification::find(conn, notification_kind::MENTION, self.id) { + n.delete(conn)?; } diesel::delete(self) .execute(conn) - .expect("Mention::delete: mention deletion error"); + .map(|_| ()) + .map_err(Error::from) } } impl Notify for Mention { - fn notify(&self, conn: &Connection) { - if let Some(m) = self.get_mentioned(conn) { - Notification::insert( - conn, - NewNotification { - kind: notification_kind::MENTION.to_string(), - object_id: self.id, - user_id: m.id, - }, - ); - } + type Error = Error; + fn notify(&self, conn: &Connection) -> Result<()> { + let m = self.get_mentioned(conn)?; + Notification::insert( + conn, + NewNotification { + kind: notification_kind::MENTION.to_string(), + object_id: self.id, + user_id: m.id, + }, + ).map(|_| ()) } } diff --git a/plume-models/src/notifications.rs b/plume-models/src/notifications.rs index 146cc6de..e7c566e3 100644 --- a/plume-models/src/notifications.rs +++ b/plume-models/src/notifications.rs @@ -9,7 +9,7 @@ use posts::Post; use reshares::Reshare; use schema::notifications; use users::User; -use Connection; +use {Connection, Error, Result}; pub mod notification_kind { pub const COMMENT: &str = "COMMENT"; @@ -40,42 +40,42 @@ impl Notification { insert!(notifications, NewNotification); get!(notifications); - pub fn find_for_user(conn: &Connection, user: &User) -> Vec { + pub fn find_for_user(conn: &Connection, user: &User) -> Result> { notifications::table .filter(notifications::user_id.eq(user.id)) .order_by(notifications::creation_date.desc()) .load::(conn) - .expect("Notification::find_for_user: notification loading error") + .map_err(Error::from) } - pub fn count_for_user(conn: &Connection, user: &User) -> i64 { + pub fn count_for_user(conn: &Connection, user: &User) -> Result { notifications::table .filter(notifications::user_id.eq(user.id)) .count() .get_result(conn) - .expect("Notification::count_for_user: count loading error") + .map_err(Error::from) } pub fn page_for_user( conn: &Connection, user: &User, (min, max): (i32, i32), - ) -> Vec { + ) -> Result> { notifications::table .filter(notifications::user_id.eq(user.id)) .order_by(notifications::creation_date.desc()) .offset(min.into()) .limit((max - min).into()) .load::(conn) - .expect("Notification::page_for_user: notification loading error") + .map_err(Error::from) } - pub fn find>(conn: &Connection, kind: S, obj: i32) -> Option { + pub fn find>(conn: &Connection, kind: S, obj: i32) -> Result { notifications::table .filter(notifications::kind.eq(kind.into())) .filter(notifications::object_id.eq(obj)) .get_result::(conn) - .ok() + .map_err(Error::from) } pub fn get_message(&self) -> &'static str { @@ -91,41 +91,37 @@ impl Notification { pub fn get_url(&self, conn: &Connection) -> Option { match self.kind.as_ref() { - notification_kind::COMMENT => self.get_post(conn).map(|p| format!("{}#comment-{}", p.url(conn), self.object_id)), - notification_kind::FOLLOW => Some(format!("/@/{}/", self.get_actor(conn).get_fqn(conn))), - notification_kind::MENTION => Mention::get(conn, self.object_id).map(|mention| - mention.get_post(conn).map(|p| p.url(conn)) - .unwrap_or_else(|| { - let comment = mention.get_comment(conn).expect("Notification::get_url: comment not found error"); - format!("{}#comment-{}", comment.get_post(conn).url(conn), comment.id) + notification_kind::COMMENT => self.get_post(conn).and_then(|p| Some(format!("{}#comment-{}", p.url(conn).ok()?, self.object_id))), + notification_kind::FOLLOW => Some(format!("/@/{}/", self.get_actor(conn).ok()?.get_fqn(conn))), + notification_kind::MENTION => Mention::get(conn, self.object_id).and_then(|mention| + mention.get_post(conn).and_then(|p| p.url(conn)) + .or_else(|_| { + let comment = mention.get_comment(conn)?; + Ok(format!("{}#comment-{}", comment.get_post(conn)?.url(conn)?, comment.id)) }) - ), + ).ok(), _ => None, } } pub fn get_post(&self, conn: &Connection) -> Option { match self.kind.as_ref() { - notification_kind::COMMENT => Comment::get(conn, self.object_id).map(|comment| comment.get_post(conn)), - notification_kind::LIKE => Like::get(conn, self.object_id).and_then(|like| Post::get(conn, like.post_id)), - notification_kind::RESHARE => Reshare::get(conn, self.object_id).and_then(|reshare| reshare.get_post(conn)), + notification_kind::COMMENT => Comment::get(conn, self.object_id).and_then(|comment| comment.get_post(conn)).ok(), + notification_kind::LIKE => Like::get(conn, self.object_id).and_then(|like| Post::get(conn, like.post_id)).ok(), + notification_kind::RESHARE => Reshare::get(conn, self.object_id).and_then(|reshare| reshare.get_post(conn)).ok(), _ => None, } } - pub fn get_actor(&self, conn: &Connection) -> User { - match self.kind.as_ref() { - notification_kind::COMMENT => Comment::get(conn, self.object_id).expect("Notification::get_actor: comment error").get_author(conn), - notification_kind::FOLLOW => User::get(conn, Follow::get(conn, self.object_id).expect("Notification::get_actor: follow error").follower_id) - .expect("Notification::get_actor: follower error"), - notification_kind::LIKE => User::get(conn, Like::get(conn, self.object_id).expect("Notification::get_actor: like error").user_id) - .expect("Notification::get_actor: liker error"), - notification_kind::MENTION => Mention::get(conn, self.object_id).expect("Notification::get_actor: mention error").get_user(conn) - .expect("Notification::get_actor: mentioner error"), - notification_kind::RESHARE => Reshare::get(conn, self.object_id).expect("Notification::get_actor: reshare error").get_user(conn) - .expect("Notification::get_actor: resharer error"), + pub fn get_actor(&self, conn: &Connection) -> Result { + Ok(match self.kind.as_ref() { + notification_kind::COMMENT => Comment::get(conn, self.object_id)?.get_author(conn)?, + notification_kind::FOLLOW => User::get(conn, Follow::get(conn, self.object_id)?.follower_id)?, + notification_kind::LIKE => User::get(conn, Like::get(conn, self.object_id)?.user_id)?, + notification_kind::MENTION => Mention::get(conn, self.object_id)?.get_user(conn)?, + notification_kind::RESHARE => Reshare::get(conn, self.object_id)?.get_user(conn)?, _ => unreachable!("Notification::get_actor: Unknow type"), - } + }) } pub fn icon_class(&self) -> &'static str { @@ -139,9 +135,10 @@ impl Notification { } } - pub fn delete(&self, conn: &Connection) { + pub fn delete(&self, conn: &Connection) -> Result<()> { diesel::delete(self) .execute(conn) - .expect("Notification::delete: notification deletion error"); + .map(|_| ()) + .map_err(Error::from) } } diff --git a/plume-models/src/post_authors.rs b/plume-models/src/post_authors.rs index 47ff7da2..b8db5549 100644 --- a/plume-models/src/post_authors.rs +++ b/plume-models/src/post_authors.rs @@ -3,6 +3,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use posts::Post; use schema::post_authors; use users::User; +use {Error, Result}; #[derive(Clone, Queryable, Identifiable, Associations)] #[belongs_to(Post)] diff --git a/plume-models/src/posts.rs b/plume-models/src/posts.rs index 57cb98bd..8ef1089a 100644 --- a/plume-models/src/posts.rs +++ b/plume-models/src/posts.rs @@ -4,7 +4,7 @@ use activitypub::{ link, object::{Article, Image, Tombstone}, }; -use canapi::{Error, Provider}; +use canapi::{Error as ApiError, Provider}; use chrono::{NaiveDateTime, TimeZone, Utc}; use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl}; use heck::{CamelCase, KebabCase}; @@ -30,7 +30,7 @@ use search::Searcher; use schema::posts; use tags::*; use users::User; -use {ap_url, Connection, BASE_URL}; +use {ap_url, Connection, BASE_URL, Error, Result, ApiResult}; pub type LicensedArticle = CustomObject; @@ -73,10 +73,10 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option)> for P fn get( (conn, _worker, _search, user_id): &(&Connection, &Worker, &Searcher, Option), id: i32, - ) -> Result { - if let Some(post) = Post::get(conn, id) { - if !post.published && !user_id.map(|u| post.is_author(conn, u)).unwrap_or(false) { - return Err(Error::Authorization( + ) -> ApiResult { + if let Ok(post) = Post::get(conn, id) { + if !post.published && !user_id.map(|u| post.is_author(conn, u).unwrap_or(false)).unwrap_or(false) { + return Err(ApiError::Authorization( "You are not authorized to access this post yet.".to_string(), )); } @@ -86,16 +86,16 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option)> for P subtitle: Some(post.subtitle.clone()), content: Some(post.content.get().clone()), source: Some(post.source.clone()), - author: Some(post.get_authors(conn)[0].username.clone()), + author: Some(post.get_authors(conn).map_err(|_| ApiError::NotFound("Authors not found".into()))?[0].username.clone()), blog_id: Some(post.blog_id), published: Some(post.published), creation_date: Some(post.creation_date.format("%Y-%m-%d").to_string()), license: Some(post.license.clone()), - tags: Some(Tag::for_post(conn, post.id).into_iter().map(|t| t.tag).collect()), + tags: Some(Tag::for_post(conn, post.id).map_err(|_| ApiError::NotFound("Tags not found".into()))?.into_iter().map(|t| t.tag).collect()), cover_id: post.cover_id, }) } else { - Err(Error::NotFound("Request post was not found".to_string())) + Err(ApiError::NotFound("Request post was not found".to_string())) } } @@ -115,19 +115,19 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option)> for P } query.get_results::(*conn).map(|ps| ps.into_iter() - .filter(|p| p.published || user_id.map(|u| p.is_author(conn, u)).unwrap_or(false)) + .filter(|p| p.published || user_id.map(|u| p.is_author(conn, u).unwrap_or(false)).unwrap_or(false)) .map(|p| PostEndpoint { id: Some(p.id), title: Some(p.title.clone()), subtitle: Some(p.subtitle.clone()), content: Some(p.content.get().clone()), source: Some(p.source.clone()), - author: Some(p.get_authors(conn)[0].username.clone()), + author: Some(p.get_authors(conn).unwrap_or_default()[0].username.clone()), blog_id: Some(p.blog_id), published: Some(p.published), creation_date: Some(p.creation_date.format("%Y-%m-%d").to_string()), license: Some(p.license.clone()), - tags: Some(Tag::for_post(conn, p.id).into_iter().map(|t| t.tag).collect()), + tags: Some(Tag::for_post(conn, p.id).unwrap_or(vec![]).into_iter().map(|t| t.tag).collect()), cover_id: p.cover_id, }) .collect() @@ -138,15 +138,15 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option)> for P (_conn, _worker, _search, _user_id): &(&Connection, &Worker, &Searcher, Option), _id: i32, _new_data: PostEndpoint, - ) -> Result { + ) -> ApiResult { unimplemented!() } fn delete((conn, _worker, search, user_id): &(&Connection, &Worker, &Searcher, Option), id: i32) { let user_id = user_id.expect("Post as Provider::delete: not authenticated"); - if let Some(post) = Post::get(conn, id) { - if post.is_author(conn, user_id) { - post.delete(&(conn, search)); + if let Ok(post) = Post::get(conn, id) { + if post.is_author(conn, user_id).unwrap_or(false) { + post.delete(&(conn, search)).ok().expect("Post as Provider::delete: delete error"); } } } @@ -154,9 +154,9 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option)> for P fn create( (conn, worker, search, user_id): &(&Connection, &Worker, &Searcher, Option), query: PostEndpoint, - ) -> Result { + ) -> ApiResult { if user_id.is_none() { - return Err(Error::Authorization("You are not authorized to create new articles.".to_string())); + return Err(ApiError::Authorization("You are not authorized to create new articles.".to_string())); } let title = query.title.clone().expect("No title for new post in API"); @@ -165,16 +165,22 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option)> for P let date = query.creation_date.clone() .and_then(|d| NaiveDateTime::parse_from_str(format!("{} 00:00:00", d).as_ref(), "%Y-%m-%d %H:%M:%S").ok()); - let domain = &Instance::get_local(&conn).expect("posts::update: Error getting local instance").public_domain; + let domain = &Instance::get_local(&conn) + .map_err(|_| ApiError::NotFound("posts::update: Error getting local instance".into()))? + .public_domain; let (content, mentions, hashtags) = md_to_html(query.source.clone().unwrap_or(String::new()).clone().as_ref(), domain); - let author = User::get(conn, user_id.expect("::create: no user_id error"))?; - let blog = query.blog_id.unwrap_or_else(|| Blog::find_for_author(conn, &author)[0].id); + let author = User::get(conn, user_id.expect("::create: no user_id error")) + .map_err(|_| ApiError::NotFound("Author not found".into()))?; + let blog = match query.blog_id { + Some(x) => x, + None => Blog::find_for_author(conn, &author).map_err(|_| ApiError::NotFound("No default blog".into()))?[0].id + }; - if Post::find_by_slug(conn, &slug, blog).is_some() { + if Post::find_by_slug(conn, &slug, blog).is_ok() { // Not an actual authorization problem, but we have nothing better for now… // TODO: add another error variant to canapi and add it there - return Err(Error::Authorization("A post with the same slug already exists".to_string())); + return Err(ApiError::Authorization("A post with the same slug already exists".to_string())); } let post = Post::insert(conn, NewPost { @@ -191,13 +197,13 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option)> for P subtitle: query.subtitle.unwrap_or(String::new()), source: query.source.expect("Post API::create: no source error"), cover_id: query.cover_id, - }, search); - post.update_ap_url(conn); + }, search).map_err(|_| ApiError::NotFound("Creation error".into()))?; + post.update_ap_url(conn).map_err(|_| ApiError::NotFound("Error setting ActivityPub URLs".into()))?;; PostAuthor::insert(conn, NewPostAuthor { author_id: author.id, post_id: post.id - }); + }).map_err(|_| ApiError::NotFound("Error saving authors".into()))?; if let Some(tags) = query.tags { for tag in tags { @@ -205,7 +211,7 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option)> for P tag: tag, is_hashtag: false, post_id: post.id - }); + }).map_err(|_| ApiError::NotFound("Error saving tags".into()))?; } } for hashtag in hashtags { @@ -213,16 +219,22 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option)> for P tag: hashtag.to_camel_case(), is_hashtag: true, post_id: post.id - }); + }).map_err(|_| ApiError::NotFound("Error saving hashtags".into()))?; } if post.published { for m in mentions.into_iter() { - Mention::from_activity(&*conn, &Mention::build_activity(&*conn, &m), post.id, true, true); + Mention::from_activity( + &*conn, + &Mention::build_activity(&*conn, &m).map_err(|_| ApiError::NotFound("Couldn't build mentions".into()))?, + post.id, + true, + true + ).map_err(|_| ApiError::NotFound("Error saving mentions".into()))?; } - let act = post.create_activity(&*conn); - let dest = User::one_by_instance(&*conn); + let act = post.create_activity(&*conn).map_err(|_| ApiError::NotFound("Couldn't create activity".into()))?; + let dest = User::one_by_instance(&*conn).map_err(|_| ApiError::NotFound("Couldn't list remote instances".into()))?; worker.execute(move || broadcast(&author, act, dest)); } @@ -232,12 +244,12 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option)> for P subtitle: Some(post.subtitle.clone()), content: Some(post.content.get().clone()), source: Some(post.source.clone()), - author: Some(post.get_authors(conn)[0].username.clone()), + author: Some(post.get_authors(conn).map_err(|_| ApiError::NotFound("No authors".into()))?[0].username.clone()), blog_id: Some(post.blog_id), published: Some(post.published), creation_date: Some(post.creation_date.format("%Y-%m-%d").to_string()), license: Some(post.license.clone()), - tags: Some(Tag::for_post(conn, post.id).into_iter().map(|t| t.tag).collect()), + tags: Some(Tag::for_post(conn, post.id).map_err(|_| ApiError::NotFound("Tags not found".into()))?.into_iter().map(|t| t.tag).collect()), cover_id: post.cover_id, }) } @@ -249,28 +261,25 @@ impl Post { find_by!(posts, find_by_ap_url, ap_url as &str); last!(posts); - pub fn insert(conn: &Connection, new: NewPost, searcher: &Searcher) -> Self { + pub fn insert(conn: &Connection, new: NewPost, searcher: &Searcher) -> Result { diesel::insert_into(posts::table) .values(new) - .execute(conn) - .expect("Post::insert: Error saving in posts"); - let post = Self::last(conn); - searcher.add_document(conn, &post); - post + .execute(conn)?; + let post = Self::last(conn)?; + searcher.add_document(conn, &post)?; + Ok(post) } - pub fn update(&self, conn: &Connection, searcher: &Searcher) -> Self { + pub fn update(&self, conn: &Connection, searcher: &Searcher) -> Result { diesel::update(self) .set(self) - .execute(conn) - .expect("Post::update: Error updating posts"); - let post = Self::get(conn, self.id) - .expect("macro::update: posts we just updated doesn't exist anymore???"); - searcher.update_document(conn, &post); - post + .execute(conn)?; + let post = Self::get(conn, self.id)?; + searcher.update_document(conn, &post)?; + Ok(post) } - pub fn list_by_tag(conn: &Connection, tag: String, (min, max): (i32, i32)) -> Vec { + pub fn list_by_tag(conn: &Connection, tag: String, (min, max): (i32, i32)) -> Result> { use schema::tags; let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id); @@ -281,28 +290,28 @@ impl Post { .offset(min.into()) .limit((max - min).into()) .load(conn) - .expect("Post::list_by_tag: loading error") + .map_err(Error::from) } - pub fn count_for_tag(conn: &Connection, tag: String) -> i64 { + pub fn count_for_tag(conn: &Connection, tag: String) -> Result { use schema::tags; let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id); - *posts::table + posts::table .filter(posts::id.eq_any(ids)) .filter(posts::published.eq(true)) .count() - .load(conn) - .expect("Post::count_for_tag: counting error") + .load(conn)? .iter() .next() - .expect("Post::count_for_tag: no result error") + .map(|x| *x) + .ok_or(Error::NotFound) } - pub fn count_local(conn: &Connection) -> i64 { + pub fn count_local(conn: &Connection) -> Result { use schema::post_authors; use schema::users; let local_authors = users::table - .filter(users::instance_id.eq(Instance::local_id(conn))) + .filter(users::instance_id.eq(Instance::get_local(conn)?.id)) .select(users::id); let local_posts_id = post_authors::table .filter(post_authors::author_id.eq_any(local_authors)) @@ -312,27 +321,27 @@ impl Post { .filter(posts::published.eq(true)) .count() .get_result(conn) - .expect("Post::count_local: loading error") + .map_err(Error::from) } - pub fn count(conn: &Connection) -> i64 { + pub fn count(conn: &Connection) -> Result { posts::table .filter(posts::published.eq(true)) .count() .get_result(conn) - .expect("Post::count: counting error") + .map_err(Error::from) } - pub fn get_recents(conn: &Connection, limit: i64) -> Vec { + pub fn get_recents(conn: &Connection, limit: i64) -> Result> { posts::table .order(posts::creation_date.desc()) .filter(posts::published.eq(true)) .limit(limit) .load::(conn) - .expect("Post::get_recents: loading error") + .map_err(Error::from) } - pub fn get_recents_for_author(conn: &Connection, author: &User, limit: i64) -> Vec { + pub fn get_recents_for_author(conn: &Connection, author: &User, limit: i64) -> Result> { use schema::post_authors; let posts = PostAuthor::belonging_to(author).select(post_authors::post_id); @@ -342,37 +351,37 @@ impl Post { .order(posts::creation_date.desc()) .limit(limit) .load::(conn) - .expect("Post::get_recents_for_author: loading error") + .map_err(Error::from) } - pub fn get_recents_for_blog(conn: &Connection, blog: &Blog, limit: i64) -> Vec { + pub fn get_recents_for_blog(conn: &Connection, blog: &Blog, limit: i64) -> Result> { posts::table .filter(posts::blog_id.eq(blog.id)) .filter(posts::published.eq(true)) .order(posts::creation_date.desc()) .limit(limit) .load::(conn) - .expect("Post::get_recents_for_blog: loading error") + .map_err(Error::from) } - pub fn get_for_blog(conn: &Connection, blog: &Blog) -> Vec { + pub fn get_for_blog(conn: &Connection, blog: &Blog) -> Result> { posts::table .filter(posts::blog_id.eq(blog.id)) .filter(posts::published.eq(true)) .load::(conn) - .expect("Post::get_for_blog:: loading error") + .map_err(Error::from) } - pub fn count_for_blog(conn: &Connection, blog: &Blog) -> i64 { + pub fn count_for_blog(conn: &Connection, blog: &Blog) -> Result { posts::table .filter(posts::blog_id.eq(blog.id)) .filter(posts::published.eq(true)) .count() .get_result(conn) - .expect("Post::count_for_blog:: count error") + .map_err(Error::from) } - pub fn blog_page(conn: &Connection, blog: &Blog, (min, max): (i32, i32)) -> Vec { + pub fn blog_page(conn: &Connection, blog: &Blog, (min, max): (i32, i32)) -> Result> { posts::table .filter(posts::blog_id.eq(blog.id)) .filter(posts::published.eq(true)) @@ -380,18 +389,18 @@ impl Post { .offset(min.into()) .limit((max - min).into()) .load::(conn) - .expect("Post::blog_page: loading error") + .map_err(Error::from) } /// Give a page of all the recent posts known to this instance (= federated timeline) - pub fn get_recents_page(conn: &Connection, (min, max): (i32, i32)) -> Vec { + pub fn get_recents_page(conn: &Connection, (min, max): (i32, i32)) -> Result> { posts::table .order(posts::creation_date.desc()) .filter(posts::published.eq(true)) .offset(min.into()) .limit((max - min).into()) .load::(conn) - .expect("Post::get_recents_page: loading error") + .map_err(Error::from) } /// Give a page of posts from a specific instance @@ -399,7 +408,7 @@ impl Post { conn: &Connection, instance_id: i32, (min, max): (i32, i32), - ) -> Vec { + ) -> Result> { use schema::blogs; let blog_ids = blogs::table @@ -413,7 +422,7 @@ impl Post { .offset(min.into()) .limit((max - min).into()) .load::(conn) - .expect("Post::get_instance_page: loading error") + .map_err(Error::from) } /// Give a page of customized user feed, based on a list of followed users @@ -421,7 +430,7 @@ impl Post { conn: &Connection, followed: Vec, (min, max): (i32, i32), - ) -> Vec { + ) -> Result> { use schema::post_authors; let post_ids = post_authors::table .filter(post_authors::author_id.eq_any(followed)) @@ -434,10 +443,10 @@ impl Post { .offset(min.into()) .limit((max - min).into()) .load::(conn) - .expect("Post::user_feed_page: loading error") + .map_err(Error::from) } - pub fn drafts_by_author(conn: &Connection, author: &User) -> Vec { + pub fn drafts_by_author(conn: &Connection, author: &User) -> Result> { use schema::post_authors; let posts = PostAuthor::belonging_to(author).select(post_authors::post_id); @@ -446,255 +455,221 @@ impl Post { .filter(posts::published.eq(false)) .filter(posts::id.eq_any(posts)) .load::(conn) - .expect("Post::drafts_by_author: loading error") + .map_err(Error::from) } - pub fn get_authors(&self, conn: &Connection) -> Vec { + pub fn get_authors(&self, conn: &Connection) -> Result> { use schema::post_authors; use schema::users; let author_list = PostAuthor::belonging_to(self).select(post_authors::author_id); users::table .filter(users::id.eq_any(author_list)) .load::(conn) - .expect("Post::get_authors: loading error") + .map_err(Error::from) } - pub fn is_author(&self, conn: &Connection, author_id: i32) -> bool { + pub fn is_author(&self, conn: &Connection, author_id: i32) -> Result { use schema::post_authors; - PostAuthor::belonging_to(self) + Ok(PostAuthor::belonging_to(self) .filter(post_authors::author_id.eq(author_id)) .count() - .get_result::(conn) - .expect("Post::is_author: loading error") > 0 + .get_result::(conn)? > 0) } - pub fn get_blog(&self, conn: &Connection) -> Blog { + pub fn get_blog(&self, conn: &Connection) -> Result { use schema::blogs; blogs::table .filter(blogs::id.eq(self.blog_id)) .limit(1) - .load::(conn) - .expect("Post::get_blog: loading error") + .load::(conn)? .into_iter() .nth(0) - .expect("Post::get_blog: no result error") + .ok_or(Error::NotFound) } - pub fn count_likes(&self, conn: &Connection) -> i64 { + pub fn count_likes(&self, conn: &Connection) -> Result { use schema::likes; likes::table .filter(likes::post_id.eq(self.id)) .count() .get_result(conn) - .expect("Post::get_likes: loading error") + .map_err(Error::from) } - pub fn count_reshares(&self, conn: &Connection) -> i64 { + pub fn count_reshares(&self, conn: &Connection) -> Result { use schema::reshares; reshares::table .filter(reshares::post_id.eq(self.id)) .count() .get_result(conn) - .expect("Post::get_reshares: loading error") + .map_err(Error::from) } - pub fn update_ap_url(&self, conn: &Connection) -> Post { + pub fn update_ap_url(&self, conn: &Connection) -> Result { if self.ap_url.is_empty() { diesel::update(self) - .set(posts::ap_url.eq(self.compute_id(conn))) - .execute(conn) - .expect("Post::update_ap_url: update error"); - Post::get(conn, self.id).expect("Post::update_ap_url: get error") + .set(posts::ap_url.eq(self.compute_id(conn)?)) + .execute(conn)?; + Post::get(conn, self.id) } else { - self.clone() + Ok(self.clone()) } } - pub fn get_receivers_urls(&self, conn: &Connection) -> Vec { + pub fn get_receivers_urls(&self, conn: &Connection) -> Result> { let followers = self - .get_authors(conn) + .get_authors(conn)? .into_iter() - .map(|a| a.get_followers(conn)) + .filter_map(|a| a.get_followers(conn).ok()) .collect::>>(); - followers.into_iter().fold(vec![], |mut acc, f| { + Ok(followers.into_iter().fold(vec![], |mut acc, f| { for x in f { acc.push(x.ap_url); } acc - }) + })) } - pub fn to_activity(&self, conn: &Connection) -> LicensedArticle { - let cc = self.get_receivers_urls(conn); + pub fn to_activity(&self, conn: &Connection) -> Result { + let cc = self.get_receivers_urls(conn)?; let to = vec![PUBLIC_VISIBILTY.to_string()]; - let mut mentions_json = Mention::list_for_post(conn, self.id) + let mut mentions_json = Mention::list_for_post(conn, self.id)? .into_iter() - .map(|m| json!(m.to_activity(conn))) + .map(|m| json!(m.to_activity(conn).ok())) .collect::>(); - let mut tags_json = Tag::for_post(conn, self.id) + let mut tags_json = Tag::for_post(conn, self.id)? .into_iter() - .map(|t| json!(t.to_activity(conn))) + .map(|t| json!(t.to_activity(conn).ok())) .collect::>(); mentions_json.append(&mut tags_json); let mut article = Article::default(); article .object_props - .set_name_string(self.title.clone()) - .expect("Post::to_activity: name error"); + .set_name_string(self.title.clone())?; article .object_props - .set_id_string(self.ap_url.clone()) - .expect("Post::to_activity: id error"); + .set_id_string(self.ap_url.clone())?; let mut authors = self - .get_authors(conn) + .get_authors(conn)? .into_iter() .map(|x| Id::new(x.ap_url)) .collect::>(); - authors.push(self.get_blog(conn).into_id()); // add the blog URL here too + authors.push(self.get_blog(conn)?.into_id()); // add the blog URL here too article .object_props - .set_attributed_to_link_vec::(authors) - .expect("Post::to_activity: attributedTo error"); + .set_attributed_to_link_vec::(authors)?; article .object_props - .set_content_string(self.content.get().clone()) - .expect("Post::to_activity: content error"); + .set_content_string(self.content.get().clone())?; article .ap_object_props .set_source_object(Source { content: self.source.clone(), media_type: String::from("text/markdown"), - }) - .expect("Post::to_activity: source error"); + })?; article .object_props - .set_published_utctime(Utc.from_utc_datetime(&self.creation_date)) - .expect("Post::to_activity: published error"); + .set_published_utctime(Utc.from_utc_datetime(&self.creation_date))?; article .object_props - .set_summary_string(self.subtitle.clone()) - .expect("Post::to_activity: summary error"); + .set_summary_string(self.subtitle.clone())?; article.object_props.tag = Some(json!(mentions_json)); if let Some(media_id) = self.cover_id { - let media = Media::get(conn, media_id).expect("Post::to_activity: get cover error"); + let media = Media::get(conn, media_id)?; let mut cover = Image::default(); cover .object_props - .set_url_string(media.url(conn)) - .expect("Post::to_activity: icon.url error"); + .set_url_string(media.url(conn)?)?; if media.sensitive { cover .object_props - .set_summary_string(media.content_warning.unwrap_or_default()) - .expect("Post::to_activity: icon.summary error"); + .set_summary_string(media.content_warning.unwrap_or_default())?; } cover .object_props - .set_content_string(media.alt_text) - .expect("Post::to_activity: icon.content error"); + .set_content_string(media.alt_text)?; cover .object_props .set_attributed_to_link_vec(vec![ - User::get(conn, media.owner_id) - .expect("Post::to_activity: media owner not found") + User::get(conn, media.owner_id)? .into_id(), - ]) - .expect("Post::to_activity: icon.attributedTo error"); + ])?; article .object_props - .set_icon_object(cover) - .expect("Post::to_activity: icon error"); + .set_icon_object(cover)?; } article .object_props - .set_url_string(self.ap_url.clone()) - .expect("Post::to_activity: url error"); + .set_url_string(self.ap_url.clone())?; article .object_props - .set_to_link_vec::(to.into_iter().map(Id::new).collect()) - .expect("Post::to_activity: to error"); + .set_to_link_vec::(to.into_iter().map(Id::new).collect())?; article .object_props - .set_cc_link_vec::(cc.into_iter().map(Id::new).collect()) - .expect("Post::to_activity: cc error"); + .set_cc_link_vec::(cc.into_iter().map(Id::new).collect())?; let mut license = Licensed::default(); - license.set_license_string(self.license.clone()).expect("Post::to_activity: license error"); - LicensedArticle::new(article, license) + license.set_license_string(self.license.clone())?; + Ok(LicensedArticle::new(article, license)) } - pub fn create_activity(&self, conn: &Connection) -> Create { - let article = self.to_activity(conn); + pub fn create_activity(&self, conn: &Connection) -> Result { + let article = self.to_activity(conn)?; let mut act = Create::default(); act.object_props - .set_id_string(format!("{}activity", self.ap_url)) - .expect("Post::create_activity: id error"); + .set_id_string(format!("{}activity", self.ap_url))?; act.object_props .set_to_link_vec::( article.object .object_props - .to_link_vec() - .expect("Post::create_activity: Couldn't copy 'to'"), - ) - .expect("Post::create_activity: to error"); + .to_link_vec()?, + )?; act.object_props .set_cc_link_vec::( article.object .object_props - .cc_link_vec() - .expect("Post::create_activity: Couldn't copy 'cc'"), - ) - .expect("Post::create_activity: cc error"); + .cc_link_vec()?, + )?; act.create_props - .set_actor_link(Id::new(self.get_authors(conn)[0].clone().ap_url)) - .expect("Post::create_activity: actor error"); + .set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?; act.create_props - .set_object_object(article) - .expect("Post::create_activity: object error"); - act + .set_object_object(article)?; + Ok(act) } - pub fn update_activity(&self, conn: &Connection) -> Update { - let article = self.to_activity(conn); + pub fn update_activity(&self, conn: &Connection) -> Result { + let article = self.to_activity(conn)?; let mut act = Update::default(); act.object_props - .set_id_string(format!("{}/update-{}", self.ap_url, Utc::now().timestamp())) - .expect("Post::update_activity: id error"); + .set_id_string(format!("{}/update-{}", self.ap_url, Utc::now().timestamp()))?; act.object_props .set_to_link_vec::( article.object .object_props - .to_link_vec() - .expect("Post::update_activity: Couldn't copy 'to'"), - ) - .expect("Post::update_activity: to error"); + .to_link_vec()?, + )?; act.object_props .set_cc_link_vec::( article.object .object_props - .cc_link_vec() - .expect("Post::update_activity: Couldn't copy 'cc'"), - ) - .expect("Post::update_activity: cc error"); + .cc_link_vec()?, + )?; act.update_props - .set_actor_link(Id::new(self.get_authors(conn)[0].clone().ap_url)) - .expect("Post::update_activity: actor error"); + .set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?; act.update_props - .set_object_object(article) - .expect("Post::update_activity: object error"); - act + .set_object_object(article)?; + Ok(act) } - pub fn handle_update(conn: &Connection, updated: &LicensedArticle, searcher: &Searcher) { + pub fn handle_update(conn: &Connection, updated: &LicensedArticle, searcher: &Searcher) -> Result<()> { let id = updated.object .object_props - .id_string() - .expect("Post::handle_update: id error"); - let mut post = Post::find_by_ap_url(conn, &id).expect("Post::handle_update: finding error"); + .id_string()?; + let mut post = Post::find_by_ap_url(conn, &id)?; if let Ok(title) = updated.object.object_props.name_string() { post.slug = title.to_kebab_case(); @@ -736,27 +711,29 @@ impl Post { .ok(); serde_json::from_value::(tag.clone()) - .map(|t| { + .map_err(Error::from) + .and_then(|t| { let tag_name = t - .name_string() - .expect("Post::from_activity: tag name error"); + .name_string()?; if txt_hashtags.remove(&tag_name) { hashtags.push(t); } else { tags.push(t); } + Ok(()) }) .ok(); } - post.update_mentions(conn, mentions); - post.update_tags(conn, tags); - post.update_hashtags(conn, hashtags); + post.update_mentions(conn, mentions)?; + post.update_tags(conn, tags)?; + post.update_hashtags(conn, hashtags)?; } - post.update(conn, searcher); + post.update(conn, searcher)?; + Ok(()) } - pub fn update_mentions(&self, conn: &Connection, mentions: Vec) { + pub fn update_mentions(&self, conn: &Connection, mentions: Vec) -> Result<()> { let mentions = mentions .into_iter() .map(|m| { @@ -764,7 +741,7 @@ impl Post { m.link_props .href_string() .ok() - .and_then(|ap_url| User::find_by_ap_url(conn, &ap_url)) + .and_then(|ap_url| User::find_by_ap_url(conn, &ap_url).ok()) .map(|u| u.id), m, ) @@ -778,14 +755,14 @@ impl Post { }) .collect::>(); - let old_mentions = Mention::list_for_post(&conn, self.id); + let old_mentions = Mention::list_for_post(&conn, self.id)?; let old_user_mentioned = old_mentions .iter() .map(|m| m.mentioned_id) .collect::>(); for (m, id) in &mentions { if !old_user_mentioned.contains(&id) { - Mention::from_activity(&*conn, &m, self.id, true, true); + Mention::from_activity(&*conn, &m, self.id, true, true)?; } } @@ -797,19 +774,18 @@ impl Post { .iter() .filter(|m| !new_mentions.contains(&m.mentioned_id)) { - m.delete(&conn); + m.delete(&conn)?; } + Ok(()) } - pub fn update_tags(&self, conn: &Connection, tags: Vec) { + pub fn update_tags(&self, conn: &Connection, tags: Vec) -> Result<()> { let tags_name = tags .iter() .filter_map(|t| t.name_string().ok()) .collect::>(); - let old_tags = Tag::for_post(&*conn, self.id) - .into_iter() - .collect::>(); + let old_tags = Tag::for_post(&*conn, self.id)?; let old_tags_name = old_tags .iter() .filter_map(|tag| { @@ -827,26 +803,25 @@ impl Post { .map(|n| old_tags_name.contains(&n)) .unwrap_or(true) { - Tag::from_activity(conn, &t, self.id, false); + Tag::from_activity(conn, &t, self.id, false)?; } } for ot in old_tags.iter().filter(|t| !t.is_hashtag) { if !tags_name.contains(&ot.tag) { - ot.delete(conn); + ot.delete(conn)?; } } + Ok(()) } - pub fn update_hashtags(&self, conn: &Connection, tags: Vec) { + pub fn update_hashtags(&self, conn: &Connection, tags: Vec) -> Result<()> { let tags_name = tags .iter() .filter_map(|t| t.name_string().ok()) .collect::>(); - let old_tags = Tag::for_post(&*conn, self.id) - .into_iter() - .collect::>(); + let old_tags = Tag::for_post(&*conn, self.id)?; let old_tags_name = old_tags .iter() .filter_map(|tag| { @@ -864,59 +839,63 @@ impl Post { .map(|n| old_tags_name.contains(&n)) .unwrap_or(true) { - Tag::from_activity(conn, &t, self.id, true); + Tag::from_activity(conn, &t, self.id, true)?; } } for ot in old_tags.into_iter().filter(|t| t.is_hashtag) { if !tags_name.contains(&ot.tag) { - ot.delete(conn); + ot.delete(conn)?; } } + Ok(()) } - pub fn url(&self, conn: &Connection) -> String { - let blog = self.get_blog(conn); - format!("/~/{}/{}", blog.get_fqn(conn), self.slug) + pub fn url(&self, conn: &Connection) -> Result { + let blog = self.get_blog(conn)?; + Ok(format!("/~/{}/{}", blog.get_fqn(conn), self.slug)) } - pub fn compute_id(&self, conn: &Connection) -> String { - ap_url(&format!( + pub fn compute_id(&self, conn: &Connection) -> Result { + Ok(ap_url(&format!( "{}/~/{}/{}/", BASE_URL.as_str(), - self.get_blog(conn).get_fqn(conn), + self.get_blog(conn)?.get_fqn(conn), self.slug - )) + ))) } pub fn cover_url(&self, conn: &Connection) -> Option { - self.cover_id.and_then(|i| Media::get(conn, i)).map(|c| c.url(conn)) + self.cover_id.and_then(|i| Media::get(conn, i).ok()).and_then(|c| c.url(conn).ok()) } } impl<'a> FromActivity for Post { - fn from_activity((conn, searcher): &(&'a Connection, &'a Searcher), article: LicensedArticle, _actor: Id) -> Post { + type Error = Error; + + fn from_activity((conn, searcher): &(&'a Connection, &'a Searcher), article: LicensedArticle, _actor: Id) -> Result { let license = article.custom_props.license_string().unwrap_or_default(); let article = article.object; - if let Some(post) = Post::find_by_ap_url( + if let Ok(post) = Post::find_by_ap_url( conn, &article.object_props.id_string().unwrap_or_default(), ) { - post + Ok(post) } else { let (blog, authors) = article .object_props - .attributed_to_link_vec::() - .expect("Post::from_activity: attributedTo error") + .attributed_to_link_vec::()? .into_iter() .fold((None, vec![]), |(blog, mut authors), link| { let url: String = link.into(); match User::from_url(conn, &url) { - Some(user) => { - authors.push(user); + Ok(u) => { + authors.push(u); (blog, authors) - } - None => (blog.or_else(|| Blog::from_url(conn, &url)), authors), + }, + Err(_) => { + (blog.or_else(|| Blog::from_url(conn, &url).ok()), authors) + }, } }); @@ -924,53 +903,47 @@ impl<'a> FromActivity for Post .object_props .icon_object::() .ok() - .and_then(|img| Media::from_activity(conn, &img).map(|m| m.id)); + .and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id)); let title = article .object_props - .name_string() - .expect("Post::from_activity: title error"); + .name_string()?; let post = Post::insert( conn, NewPost { - blog_id: blog.expect("Post::from_activity: blog not found error").id, + blog_id: blog?.id, slug: title.to_kebab_case(), title, content: SafeString::new( &article .object_props - .content_string() - .expect("Post::from_activity: content error"), + .content_string()?, ), published: true, license: license, // FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields - ap_url: article.object_props.url_string().unwrap_or_else(|_| + ap_url: article.object_props.url_string().or_else(|_| article .object_props .id_string() - .expect("Post::from_activity: url + id error"), - ), + )?, creation_date: Some( article .object_props - .published_utctime() - .expect("Post::from_activity: published error") + .published_utctime()? .naive_utc(), ), subtitle: article .object_props - .summary_string() - .expect("Post::from_activity: summary error"), + .summary_string()?, source: article .ap_object_props - .source_object::() - .expect("Post::from_activity: source error") + .source_object::()? .content, cover_id: cover, }, searcher, - ); + )?; for author in authors { PostAuthor::insert( @@ -979,7 +952,7 @@ impl<'a> FromActivity for Post post_id: post.id, author_id: author.id, }, - ); + )?; } // save mentions and tags @@ -995,64 +968,56 @@ impl<'a> FromActivity for Post .ok(); serde_json::from_value::(tag.clone()) - .map(|t| { - let tag_name = t - .name_string() - .expect("Post::from_activity: tag name error"); - Tag::from_activity(conn, &t, post.id, hashtags.remove(&tag_name)); + .map_err(Error::from) + .and_then(|t| { + let tag_name = t.name_string()?; + Ok(Tag::from_activity(conn, &t, post.id, hashtags.remove(&tag_name))) }) .ok(); } } - post + Ok(post) } } } impl<'a> Deletable<(&'a Connection, &'a Searcher), Delete> for Post { - fn delete(&self, (conn, searcher): &(&Connection, &Searcher)) -> Delete { + type Error = Error; + + fn delete(&self, (conn, searcher): &(&Connection, &Searcher)) -> Result { let mut act = Delete::default(); act.delete_props - .set_actor_link(self.get_authors(conn)[0].clone().into_id()) - .expect("Post::delete: actor error"); + .set_actor_link(self.get_authors(conn)?[0].clone().into_id())?; let mut tombstone = Tombstone::default(); tombstone .object_props - .set_id_string(self.ap_url.clone()) - .expect("Post::delete: object.id error"); + .set_id_string(self.ap_url.clone())?; act.delete_props - .set_object_object(tombstone) - .expect("Post::delete: object error"); + .set_object_object(tombstone)?; act.object_props - .set_id_string(format!("{}#delete", self.ap_url)) - .expect("Post::delete: id error"); + .set_id_string(format!("{}#delete", self.ap_url))?; act.object_props - .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILTY)]) - .expect("Post::delete: to error"); + .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILTY)])?; - for m in Mention::list_for_post(&conn, self.id) { - m.delete(conn); + for m in Mention::list_for_post(&conn, self.id)? { + m.delete(conn)?; } diesel::delete(self) - .execute(*conn) - .expect("Post::delete: DB error"); + .execute(*conn)?; searcher.delete_document(self); - act + Ok(act) } - fn delete_id(id: &str, actor_id: &str, (conn, searcher): &(&Connection, &Searcher)) { - let actor = User::find_by_ap_url(conn, actor_id); - let post = Post::find_by_ap_url(conn, id); - let can_delete = actor - .and_then(|act| { - post.clone() - .map(|p| p.get_authors(conn).into_iter().any(|a| act.id == a.id)) - }) - .unwrap_or(false); + fn delete_id(id: &str, actor_id: &str, (conn, searcher): &(&Connection, &Searcher)) -> Result { + let actor = User::find_by_ap_url(conn, actor_id)?; + let post = Post::find_by_ap_url(conn, id)?; + let can_delete = post.get_authors(conn)?.into_iter().any(|a| actor.id == a.id); if can_delete { - post.map(|p| p.delete(&(conn, searcher))); + post.delete(&(conn, searcher)) + } else { + Err(Error::Unauthorized) } } } diff --git a/plume-models/src/reshares.rs b/plume-models/src/reshares.rs index 3ee771f8..5b656fd3 100644 --- a/plume-models/src/reshares.rs +++ b/plume-models/src/reshares.rs @@ -10,7 +10,7 @@ use plume_common::activity_pub::{ use posts::Post; use schema::reshares; use users::User; -use Connection; +use {Connection, Error, Result}; #[derive(Clone, Serialize, Deserialize, Queryable, Identifiable)] pub struct Reshare { @@ -40,91 +40,80 @@ impl Reshare { post_id as i32 ); - pub fn get_recents_for_author(conn: &Connection, user: &User, limit: i64) -> Vec { + pub fn get_recents_for_author(conn: &Connection, user: &User, limit: i64) -> Result> { reshares::table .filter(reshares::user_id.eq(user.id)) .order(reshares::creation_date.desc()) .limit(limit) .load::(conn) - .expect("Reshare::get_recents_for_author: loading error") + .map_err(Error::from) } - pub fn get_post(&self, conn: &Connection) -> Option { + pub fn get_post(&self, conn: &Connection) -> Result { Post::get(conn, self.post_id) } - pub fn get_user(&self, conn: &Connection) -> Option { + pub fn get_user(&self, conn: &Connection) -> Result { User::get(conn, self.user_id) } - pub fn to_activity(&self, conn: &Connection) -> Announce { + pub fn to_activity(&self, conn: &Connection) -> Result { let mut act = Announce::default(); act.announce_props - .set_actor_link( - User::get(conn, self.user_id) - .expect("Reshare::to_activity: user error") - .into_id(), - ) - .expect("Reshare::to_activity: actor error"); + .set_actor_link(User::get(conn, self.user_id)?.into_id())?; act.announce_props - .set_object_link( - Post::get(conn, self.post_id) - .expect("Reshare::to_activity: post error") - .into_id(), - ) - .expect("Reshare::to_activity: object error"); + .set_object_link(Post::get(conn, self.post_id)?.into_id())?; act.object_props - .set_id_string(self.ap_url.clone()) - .expect("Reshare::to_activity: id error"); + .set_id_string(self.ap_url.clone())?; act.object_props - .set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())) - .expect("Reshare::to_activity: to error"); + .set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?; act.object_props - .set_cc_link_vec::(vec![]) - .expect("Reshare::to_activity: cc error"); + .set_cc_link_vec::(vec![])?; - act + Ok(act) } } impl FromActivity for Reshare { - fn from_activity(conn: &Connection, announce: Announce, _actor: Id) -> Reshare { + type Error = Error; + + fn from_activity(conn: &Connection, announce: Announce, _actor: Id) -> Result { let user = User::from_url( conn, announce .announce_props - .actor_link::() - .expect("Reshare::from_activity: actor error") + .actor_link::()? .as_ref(), - ); + )?; let post = Post::find_by_ap_url( conn, announce .announce_props - .object_link::() - .expect("Reshare::from_activity: object error") + .object_link::()? .as_ref(), - ); + )?; let reshare = Reshare::insert( conn, NewReshare { - post_id: post.expect("Reshare::from_activity: post error").id, - user_id: user.expect("Reshare::from_activity: user error").id, + post_id: post.id, + user_id: user.id, ap_url: announce .object_props .id_string() .unwrap_or_default(), }, - ); - reshare.notify(conn); - reshare + )?; + reshare.notify(conn)?; + Ok(reshare) } } impl Notify for Reshare { - fn notify(&self, conn: &Connection) { - let post = self.get_post(conn).expect("Reshare::notify: post error"); - for author in post.get_authors(conn) { + type Error = Error; + + fn notify(&self, conn: &Connection) -> Result<()> { + let post = self.get_post(conn)?; + for author in post.get_authors(conn)? { Notification::insert( conn, NewNotification { @@ -132,55 +121,47 @@ impl Notify for Reshare { object_id: self.id, user_id: author.id, }, - ); + )?; } + Ok(()) } } impl Deletable for Reshare { - fn delete(&self, conn: &Connection) -> Undo { + type Error = Error; + + fn delete(&self, conn: &Connection) -> Result { diesel::delete(self) - .execute(conn) - .expect("Reshare::delete: delete error"); + .execute(conn)?; // delete associated notification if any - if let Some(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) { + if let Ok(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) { diesel::delete(¬if) - .execute(conn) - .expect("Reshare::delete: notification error"); + .execute(conn)?; } let mut act = Undo::default(); act.undo_props - .set_actor_link( - User::get(conn, self.user_id) - .expect("Reshare::delete: user error") - .into_id(), - ) - .expect("Reshare::delete: actor error"); + .set_actor_link(User::get(conn, self.user_id)?.into_id())?; act.undo_props - .set_object_object(self.to_activity(conn)) - .expect("Reshare::delete: object error"); + .set_object_object(self.to_activity(conn)?)?; act.object_props - .set_id_string(format!("{}#delete", self.ap_url)) - .expect("Reshare::delete: id error"); + .set_id_string(format!("{}#delete", self.ap_url))?; act.object_props - .set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())) - .expect("Reshare::delete: to error"); + .set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?; act.object_props - .set_cc_link_vec::(vec![]) - .expect("Reshare::delete: cc error"); + .set_cc_link_vec::(vec![])?; - act + Ok(act) } - fn delete_id(id: &str, actor_id: &str, conn: &Connection) { - if let Some(reshare) = Reshare::find_by_ap_url(conn, id) { - if let Some(actor) = User::find_by_ap_url(conn, actor_id) { - if actor.id == reshare.user_id { - reshare.delete(conn); - } - } + fn delete_id(id: &str, actor_id: &str, conn: &Connection) -> Result { + let reshare = Reshare::find_by_ap_url(conn, id)?; + let actor = User::find_by_ap_url(conn, actor_id)?; + if actor.id == reshare.user_id { + reshare.delete(conn) + } else { + Err(Error::Unauthorized) } } } diff --git a/plume-models/src/search/mod.rs b/plume-models/src/search/mod.rs index 30c08fc6..14ce88ae 100644 --- a/plume-models/src/search/mod.rs +++ b/plume-models/src/search/mod.rs @@ -118,7 +118,7 @@ pub(crate) mod tests { conn.test_transaction::<_, (), _>(|| { let searcher = get_searcher(); let blog = &fill_database(conn).1[0]; - let author = &blog.list_authors(conn)[0]; + let author = &blog.list_authors(conn).unwrap()[0]; let title = random_hex()[..8].to_owned(); @@ -134,23 +134,23 @@ pub(crate) mod tests { subtitle: "".to_owned(), source: "".to_owned(), cover_id: None, - }, &searcher); + }, &searcher).unwrap(); PostAuthor::insert(conn, NewPostAuthor { post_id: post.id, author_id: author.id, - }); + }).unwrap(); searcher.commit(); assert_eq!(searcher.search_document(conn, Query::from_str(&title), (0,1))[0].id, post.id); let newtitle = random_hex()[..8].to_owned(); post.title = newtitle.clone(); - post.update(conn, &searcher); + post.update(conn, &searcher).unwrap(); searcher.commit(); assert_eq!(searcher.search_document(conn, Query::from_str(&newtitle), (0,1))[0].id, post.id); assert!(searcher.search_document(conn, Query::from_str(&title), (0,1)).is_empty()); - post.delete(&(conn, &searcher)); + post.delete(&(conn, &searcher)).unwrap(); searcher.commit(); assert!(searcher.search_document(conn, Query::from_str(&newtitle), (0,1)).is_empty()); diff --git a/plume-models/src/search/searcher.rs b/plume-models/src/search/searcher.rs index 0ae53ad5..0e4c9110 100644 --- a/plume-models/src/search/searcher.rs +++ b/plume-models/src/search/searcher.rs @@ -14,9 +14,10 @@ use std::{cmp, fs::create_dir_all, path::Path, sync::Mutex}; use search::query::PlumeQuery; use super::tokenizer; +use Result; #[derive(Debug)] -pub enum SearcherError{ +pub enum SearcherError { IndexCreationError, WriteLockAcquisitionError, IndexOpeningError, @@ -66,7 +67,7 @@ impl Searcher { } - pub fn create(path: &AsRef) -> Result { + pub fn create(path: &AsRef) -> Result { let whitespace_tokenizer = tokenizer::WhitespaceTokenizer .filter(LowerCaser); @@ -94,7 +95,7 @@ impl Searcher { }) } - pub fn open(path: &AsRef) -> Result { + pub fn open(path: &AsRef) -> Result { let whitespace_tokenizer = tokenizer::WhitespaceTokenizer .filter(LowerCaser); @@ -121,7 +122,7 @@ impl Searcher { }) } - pub fn add_document(&self, conn: &Connection, post: &Post) { + pub fn add_document(&self, conn: &Connection, post: &Post) -> Result<()> { let schema = self.index.schema(); let post_id = schema.get_field("post_id").unwrap(); @@ -142,18 +143,19 @@ impl Searcher { let mut writer = self.writer.lock().unwrap(); let writer = writer.as_mut().unwrap(); writer.add_document(doc!( - post_id => i64::from(post.id), - author => post.get_authors(conn).into_iter().map(|u| u.get_fqn(conn)).join(" "), - creation_date => i64::from(post.creation_date.num_days_from_ce()), - instance => Instance::get(conn, post.get_blog(conn).instance_id).unwrap().public_domain.clone(), - tag => Tag::for_post(conn, post.id).into_iter().map(|t| t.tag).join(" "), - blog_name => post.get_blog(conn).title, - content => post.content.get().clone(), - subtitle => post.subtitle.clone(), - title => post.title.clone(), - lang => detect_lang(post.content.get()).and_then(|i| if i.is_reliable() { Some(i.lang()) } else {None} ).unwrap_or(Lang::Eng).name(), - license => post.license.clone(), - )); + post_id => i64::from(post.id), + author => post.get_authors(conn)?.into_iter().map(|u| u.get_fqn(conn)).join(" "), + creation_date => i64::from(post.creation_date.num_days_from_ce()), + instance => Instance::get(conn, post.get_blog(conn)?.instance_id)?.public_domain.clone(), + tag => Tag::for_post(conn, post.id)?.into_iter().map(|t| t.tag).join(" "), + blog_name => post.get_blog(conn)?.title, + content => post.content.get().clone(), + subtitle => post.subtitle.clone(), + title => post.title.clone(), + lang => detect_lang(post.content.get()).and_then(|i| if i.is_reliable() { Some(i.lang()) } else {None} ).unwrap_or(Lang::Eng).name(), + license => post.license.clone(), + )); + Ok(()) } pub fn delete_document(&self, post: &Post) { @@ -166,9 +168,9 @@ impl Searcher { writer.delete_term(doc_id); } - pub fn update_document(&self, conn: &Connection, post: &Post) { + pub fn update_document(&self, conn: &Connection, post: &Post) -> Result<()> { self.delete_document(post); - self.add_document(conn, post); + self.add_document(conn, post) } pub fn search_document(&self, conn: &Connection, query: PlumeQuery, (min, max): (i32, i32)) -> Vec{ @@ -185,9 +187,9 @@ impl Searcher { .filter_map(|doc_add| { let doc = searcher.doc(*doc_add).ok()?; let id = doc.get_first(post_id)?; - Post::get(conn, id.i64_value() as i32) - //borrow checker don't want me to use filter_map or and_then here - }) + Post::get(conn, id.i64_value() as i32).ok() + //borrow checker don't want me to use filter_map or and_then here + }) .collect() } diff --git a/plume-models/src/tags.rs b/plume-models/src/tags.rs index 986d9e3f..91c2b57a 100644 --- a/plume-models/src/tags.rs +++ b/plume-models/src/tags.rs @@ -3,7 +3,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use instance::Instance; use plume_common::activity_pub::Hashtag; use schema::tags; -use {ap_url, Connection}; +use {ap_url, Connection, Error, Result}; #[derive(Clone, Identifiable, Serialize, Queryable)] pub struct Tag { @@ -27,48 +27,43 @@ impl Tag { find_by!(tags, find_by_name, tag as &str); list_by!(tags, for_post, post_id as i32); - pub fn to_activity(&self, conn: &Connection) -> Hashtag { + pub fn to_activity(&self, conn: &Connection) -> Result { let mut ht = Hashtag::default(); ht.set_href_string(ap_url(&format!( "{}/tag/{}", - Instance::get_local(conn) - .expect("Tag::to_activity: local instance not found error") - .public_domain, + Instance::get_local(conn)?.public_domain, self.tag - ))).expect("Tag::to_activity: href error"); - ht.set_name_string(self.tag.clone()) - .expect("Tag::to_activity: name error"); - ht + )))?; + ht.set_name_string(self.tag.clone())?; + Ok(ht) } - pub fn from_activity(conn: &Connection, tag: &Hashtag, post: i32, is_hashtag: bool) -> Tag { + pub fn from_activity(conn: &Connection, tag: &Hashtag, post: i32, is_hashtag: bool) -> Result { Tag::insert( conn, NewTag { - tag: tag.name_string().expect("Tag::from_activity: name error"), + tag: tag.name_string()?, is_hashtag, post_id: post, }, ) } - pub fn build_activity(conn: &Connection, tag: String) -> Hashtag { + pub fn build_activity(conn: &Connection, tag: String) -> Result { let mut ht = Hashtag::default(); ht.set_href_string(ap_url(&format!( "{}/tag/{}", - Instance::get_local(conn) - .expect("Tag::to_activity: local instance not found error") - .public_domain, + Instance::get_local(conn)?.public_domain, tag - ))).expect("Tag::to_activity: href error"); - ht.set_name_string(tag) - .expect("Tag::to_activity: name error"); - ht + )))?; + ht.set_name_string(tag)?; + Ok(ht) } - pub fn delete(&self, conn: &Connection) { + pub fn delete(&self, conn: &Connection) -> Result<()> { diesel::delete(self) .execute(conn) - .expect("Tag::delete: database error"); + .map(|_| ()) + .map_err(Error::from) } } diff --git a/plume-models/src/users.rs b/plume-models/src/users.rs index ababc539..1a2f1d2d 100644 --- a/plume-models/src/users.rs +++ b/plume-models/src/users.rs @@ -40,7 +40,7 @@ use posts::Post; use safe_string::SafeString; use schema::users; use search::Searcher; -use {ap_url, Connection, BASE_URL, USE_HTTPS}; +use {ap_url, Connection, BASE_URL, USE_HTTPS, Error, Result}; pub type CustomPerson = CustomObject; @@ -96,139 +96,112 @@ impl User { find_by!(users, find_by_name, username as &str, instance_id as i32); find_by!(users, find_by_ap_url, ap_url as &str); - pub fn one_by_instance(conn: &Connection) -> Vec { + pub fn one_by_instance(conn: &Connection) -> Result> { users::table .filter(users::instance_id.eq_any(users::table.select(users::instance_id).distinct())) .load::(conn) - .expect("User::one_by_instance: loading error") + .map_err(Error::from) } - pub fn delete(&self, conn: &Connection, searcher: &Searcher) { + pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> { use schema::post_authors; - Blog::find_for_author(conn, self) + for blog in Blog::find_for_author(conn, self)? .iter() - .filter(|b| b.count_authors(conn) <= 1) - .for_each(|b| b.delete(conn, searcher)); + .filter(|b| b.count_authors(conn).map(|c| c <= 1).unwrap_or(false)) { + blog.delete(conn, searcher)?; + } // delete the posts if they is the only author let all_their_posts_ids: Vec = post_authors::table .filter(post_authors::author_id.eq(self.id)) .select(post_authors::post_id) - .load(conn) - .expect("User::delete: post loading error"); + .load(conn)?; for post_id in all_their_posts_ids { let has_other_authors = post_authors::table .filter(post_authors::post_id.eq(post_id)) .filter(post_authors::author_id.ne(self.id)) .count() - .load(conn) - .expect("User::delete: count author error") + .load(conn)? .first() .unwrap_or(&0) > &0; if !has_other_authors { - Post::get(conn, post_id) - .expect("User::delete: post not found error") - .delete(&(conn, searcher)); + Post::get(conn, post_id)? + .delete(&(conn, searcher))?; } } diesel::delete(self) .execute(conn) - .expect("User::delete: user deletion error"); + .map(|_| ()) + .map_err(Error::from) } - pub fn get_instance(&self, conn: &Connection) -> Instance { - Instance::get(conn, self.instance_id).expect("User::get_instance: instance not found error") + pub fn get_instance(&self, conn: &Connection) -> Result { + Instance::get(conn, self.instance_id) } - pub fn grant_admin_rights(&self, conn: &Connection) { + pub fn grant_admin_rights(&self, conn: &Connection) -> Result<()> { diesel::update(self) .set(users::is_admin.eq(true)) .execute(conn) - .expect("User::grand_admin_rights: update error"); + .map(|_| ()) + .map_err(Error::from) } - pub fn revoke_admin_rights(&self, conn: &Connection) { + pub fn revoke_admin_rights(&self, conn: &Connection) -> Result<()> { diesel::update(self) .set(users::is_admin.eq(false)) .execute(conn) - .expect("User::grand_admin_rights: update error"); + .map(|_| ()) + .map_err(Error::from) } - pub fn update(&self, conn: &Connection, name: String, email: String, summary: String) -> User { + pub fn update(&self, conn: &Connection, name: String, email: String, summary: String) -> Result { diesel::update(self) .set(( users::display_name.eq(name), users::email.eq(email), users::summary.eq(summary), )) - .execute(conn) - .expect("User::update: update error"); - User::get(conn, self.id).expect("User::update: get error") + .execute(conn)?; + User::get(conn, self.id) } - pub fn count_local(conn: &Connection) -> i64 { + pub fn count_local(conn: &Connection) -> Result { users::table - .filter(users::instance_id.eq(Instance::local_id(conn))) + .filter(users::instance_id.eq(Instance::get_local(conn)?.id)) .count() .get_result(conn) - .expect("User::count_local: loading error") + .map_err(Error::from) } - pub fn find_local(conn: &Connection, username: &str) -> Option { - User::find_by_name(conn, username, Instance::local_id(conn)) + pub fn find_local(conn: &Connection, username: &str) -> Result { + User::find_by_name(conn, username, Instance::get_local(conn)?.id) } - pub fn find_by_fqn(conn: &Connection, fqn: &str) -> Option { - if fqn.contains('@') { - // remote user - match Instance::find_by_domain( - conn, - fqn.split('@') - .last() - .expect("User::find_by_fqn: host error"), - ) { - Some(instance) => match User::find_by_name( - conn, - fqn.split('@') - .nth(0) - .expect("User::find_by_fqn: name error") - , - instance.id, - ) { - Some(u) => Some(u), - None => User::fetch_from_webfinger(conn, fqn), - }, - None => User::fetch_from_webfinger(conn, fqn), - } - } else { - // local user - User::find_local(conn, fqn) + pub fn find_by_fqn(conn: &Connection, fqn: &str) -> Result { + let mut split_fqn = fqn.split('@'); + let username = split_fqn.next().ok_or(Error::InvalidValue)?; + if let Some(domain) = split_fqn.next() { // remote user + Instance::find_by_domain(conn, domain) + .and_then(|instance| User::find_by_name(conn, username, instance.id)) + .or_else(|_| User::fetch_from_webfinger(conn, fqn)) + } else { // local user + User::find_local(conn, username) } } - fn fetch_from_webfinger(conn: &Connection, acct: &str) -> Option { - match resolve(acct.to_owned(), *USE_HTTPS) { - Ok(wf) => wf - .links - .into_iter() - .find(|l| l.mime_type == Some(String::from("application/activity+json"))) - .and_then(|l| { - User::fetch_from_url( - conn, - &l.href - .expect("User::fetch_from_webginfer: href not found error"), - ) - }), - Err(details) => { - println!("WF Error: {:?}", details); - None - } - } + fn fetch_from_webfinger(conn: &Connection, acct: &str) -> Result { + let link = resolve(acct.to_owned(), *USE_HTTPS)? + .links + .into_iter() + .find(|l| l.mime_type == Some(String::from("application/activity+json"))) + .ok_or(Error::Webfinger)?; + User::fetch_from_url(conn, link.href.as_ref()?) } - fn fetch(url: &str) -> Option { - let req = Client::new() + fn fetch(url: &str) -> Result { + let mut res = Client::new() .get(url) .header( ACCEPT, @@ -237,72 +210,45 @@ impl User { .into_iter() .collect::>() .join(", "), - ).expect("User::fetch: accept header error"), + )?, ) - .send(); - match req { - Ok(mut res) => { - if let Ok(text) = &res.text() { - if let Ok(ap_sign) = serde_json::from_str::(text) { - if let Ok(mut json) = serde_json::from_str::(text) { - json.custom_props = ap_sign; // without this workaround, publicKey is not correctly deserialized - Some(json) - } else { - None - } - } else { - None - } - } else { - None - } - } - Err(e) => { - println!("User fetch error: {:?}", e); - None - } - } + .send()?; + let text = &res.text()?; + // without this workaround, publicKey is not correctly deserialized + let ap_sign = serde_json::from_str::(text)?; + let mut json = serde_json::from_str::(text)?; + json.custom_props = ap_sign; + Ok(json) } - pub fn fetch_from_url(conn: &Connection, url: &str) -> Option { - User::fetch(url).and_then(|json| { - (User::from_activity( + pub fn fetch_from_url(conn: &Connection, url: &str) -> Result { + User::fetch(url).and_then(|json| User::from_activity( + conn, + &json, + Url::parse(url)?.host_str()?, + )) + } + + fn from_activity(conn: &Connection, acct: &CustomPerson, inst: &str) -> Result { + let instance = Instance::find_by_domain(conn, inst) + .or_else(|_| Instance::insert( conn, - &json, - Url::parse(url) - .expect("User::fetch_from_url: url error") - .host_str() - .expect("User::fetch_from_url: host error"), - ).ok()) - }) - } + NewInstance { + name: inst.to_owned(), + public_domain: inst.to_owned(), + local: false, + // We don't really care about all the following for remote instances + long_description: SafeString::new(""), + short_description: SafeString::new(""), + default_license: String::new(), + open_registrations: true, + short_description_html: String::new(), + long_description_html: String::new(), + }, + ))?; - fn from_activity(conn: &Connection, acct: &CustomPerson, inst: &str) -> Result { - let instance = match Instance::find_by_domain(conn, inst) { - Some(instance) => instance, - None => { - Instance::insert( - conn, - NewInstance { - name: inst.to_owned(), - public_domain: inst.to_owned(), - local: false, - // We don't really care about all the following for remote instances - long_description: SafeString::new(""), - short_description: SafeString::new(""), - default_license: String::new(), - open_registrations: true, - short_description_html: String::new(), - long_description_html: String::new(), - }, - ) - } - }; - - if acct.object.ap_actor_props.preferred_username_string() - .expect("User::from_activity: preferredUsername error") - .contains(&['<', '>', '&', '@', '\'', '"'][..]) { - return Err(()); + if acct.object.ap_actor_props.preferred_username_string()?.contains(&['<', '>', '&', '@', '\'', '"'][..]) { + return Err(Error::InvalidValue); } let user = User::insert( conn, @@ -315,18 +261,15 @@ impl User { display_name: acct .object .object_props - .name_string() - .expect("User::from_activity: name error"), + .name_string()?, outbox_url: acct .object .ap_actor_props - .outbox_string() - .expect("User::from_activity: outbox error"), + .outbox_string()?, inbox_url: acct .object .ap_actor_props - .inbox_string() - .expect("User::from_activity: inbox error"), + .inbox_string()?, is_admin: false, summary: SafeString::new( &acct @@ -341,14 +284,11 @@ impl User { ap_url: acct .object .object_props - .id_string() - .expect("User::from_activity: id error"), + .id_string()?, public_key: acct .custom_props - .public_key_publickey() - .expect("User::from_activity: publicKey error") - .public_key_pem_string() - .expect("User::from_activity: publicKey.publicKeyPem error"), + .public_key_publickey()? + .public_key_pem_string()?, private_key: None, shared_inbox_url: acct .object @@ -359,42 +299,37 @@ impl User { followers_endpoint: acct .object .ap_actor_props - .followers_string() - .expect("User::from_activity: followers error"), + .followers_string()?, avatar_id: None, }, - ); + )?; let avatar = Media::save_remote( conn, acct.object .object_props - .icon_image() - .expect("User::from_activity: icon error") + .icon_image()? .object_props - .url_string() - .expect("User::from_activity: icon.url error"), + .url_string()?, &user, ); if let Ok(avatar) = avatar { - user.set_avatar(conn, avatar.id); + user.set_avatar(conn, avatar.id)?; } Ok(user) } - pub fn refetch(&self, conn: &Connection) { - User::fetch(&self.ap_url.clone()).map(|json| { + pub fn refetch(&self, conn: &Connection) -> Result<()> { + User::fetch(&self.ap_url.clone()).and_then(|json| { let avatar = Media::save_remote( conn, json.object .object_props - .icon_image() - .expect("User::refetch: icon error") + .icon_image()? .object_props - .url_string() - .expect("User::refetch: icon.url error"), + .url_string()?, &self, ).ok(); @@ -403,23 +338,19 @@ impl User { users::username.eq(json .object .ap_actor_props - .preferred_username_string() - .expect("User::refetch: preferredUsername error")), + .preferred_username_string()?), users::display_name.eq(json .object .object_props - .name_string() - .expect("User::refetch: name error")), + .name_string()?), users::outbox_url.eq(json .object .ap_actor_props - .outbox_string() - .expect("User::refetch: outbox error")), + .outbox_string()?), users::inbox_url.eq(json .object .ap_actor_props - .inbox_string() - .expect("User::refetch: inbox error")), + .inbox_string()?), users::summary.eq(SafeString::new( &json .object @@ -430,36 +361,28 @@ impl User { users::followers_endpoint.eq(json .object .ap_actor_props - .followers_string() - .expect("User::refetch: followers error")), + .followers_string()?), users::avatar_id.eq(avatar.map(|a| a.id)), users::last_fetched_date.eq(Utc::now().naive_utc()), )) .execute(conn) - .expect("User::refetch: update error") - }); + .map(|_| ()) + .map_err(Error::from) + }) } - pub fn hash_pass(pass: &str) -> String { - bcrypt::hash(pass, 10).expect("User::hash_pass: hashing error") + pub fn hash_pass(pass: &str) -> Result { + bcrypt::hash(pass, 10).map_err(Error::from) } pub fn auth(&self, pass: &str) -> bool { - if let Ok(valid) = bcrypt::verify( - pass, - self.hashed_password - .clone() - .expect("User::auth: no password error") - .as_str(), - ) { - valid - } else { - false - } + self.hashed_password.clone() + .map(|hashed| bcrypt::verify(pass, hashed.as_ref()).unwrap_or(false)) + .unwrap_or(false) } - pub fn update_boxes(&self, conn: &Connection) { - let instance = self.get_instance(conn); + pub fn update_boxes(&self, conn: &Connection) -> Result<()> { + let instance = self.get_instance(conn)?; if self.outbox_url.is_empty() { diesel::update(self) .set(users::outbox_url.eq(instance.compute_box( @@ -467,8 +390,7 @@ impl User { &self.username, "outbox", ))) - .execute(conn) - .expect("User::update_boxes: outbox update error"); + .execute(conn)?; } if self.inbox_url.is_empty() { @@ -478,27 +400,23 @@ impl User { &self.username, "inbox", ))) - .execute(conn) - .expect("User::update_boxes: inbox update error"); + .execute(conn)?; } if self.ap_url.is_empty() { diesel::update(self) .set(users::ap_url.eq(instance.compute_box(USER_PREFIX, &self.username, ""))) - .execute(conn) - .expect("User::update_boxes: ap_url update error"); + .execute(conn)?; } if self.shared_inbox_url.is_none() { diesel::update(self) .set(users::shared_inbox_url.eq(ap_url(&format!( "{}/inbox", - Instance::get_local(conn) - .expect("User::update_boxes: local instance not found error") + Instance::get_local(conn)? .public_domain )))) - .execute(conn) - .expect("User::update_boxes: shared inbox update error"); + .execute(conn)?; } if self.followers_endpoint.is_empty() { @@ -508,35 +426,34 @@ impl User { &self.username, "followers", ))) - .execute(conn) - .expect("User::update_boxes: follower update error"); + .execute(conn)?; } + + Ok(()) } - pub fn get_local_page(conn: &Connection, (min, max): (i32, i32)) -> Vec { + pub fn get_local_page(conn: &Connection, (min, max): (i32, i32)) -> Result> { users::table - .filter(users::instance_id.eq(Instance::local_id(conn))) + .filter(users::instance_id.eq(Instance::get_local(conn)?.id)) .order(users::username.asc()) .offset(min.into()) .limit((max - min).into()) .load::(conn) - .expect("User::get_local_page: loading error") + .map_err(Error::from) } - pub fn outbox(&self, conn: &Connection) -> ActivityStream { - let acts = self.get_activities(conn); + pub fn outbox(&self, conn: &Connection) -> Result> { + let acts = self.get_activities(conn)?; let n_acts = acts.len(); let mut coll = OrderedCollection::default(); - coll.collection_props.items = - serde_json::to_value(acts).expect("User::outbox: activity error"); + coll.collection_props.items = serde_json::to_value(acts)?; coll.collection_props - .set_total_items_u64(n_acts as u64) - .expect("User::outbox: count error"); - ActivityStream::new(coll) + .set_total_items_u64(n_acts as u64)?; + Ok(ActivityStream::new(coll)) } - pub fn fetch_outbox(&self) -> Vec { - let req = Client::new() + pub fn fetch_outbox(&self) -> Result> { + let mut res = Client::new() .get(&self.outbox_url[..]) .header( ACCEPT, @@ -545,30 +462,22 @@ impl User { .into_iter() .collect::>() .join(", "), - ).expect("User::fetch_outbox: accept header error"), + )? ) - .send(); - match req { - Ok(mut res) => { - let text = &res.text().expect("User::fetch_outbox: body error"); - let json: serde_json::Value = - serde_json::from_str(text).expect("User::fetch_outbox: parsing error"); - json["items"] - .as_array() - .unwrap_or(&vec![]) - .into_iter() - .filter_map(|j| serde_json::from_value(j.clone()).ok()) - .collect::>() - } - Err(e) => { - println!("User outbox fetch error: {:?}", e); - vec![] - } - } + .send()?; + let text = &res.text()?; + let json: serde_json::Value = + serde_json::from_str(text)?; + Ok(json["items"] + .as_array() + .unwrap_or(&vec![]) + .into_iter() + .filter_map(|j| serde_json::from_value(j.clone()).ok()) + .collect::>()) } - pub fn fetch_followers_ids(&self) -> Vec { - let req = Client::new() + pub fn fetch_followers_ids(&self) -> Result> { + let mut res = Client::new() .get(&self.followers_endpoint[..]) .header( ACCEPT, @@ -577,78 +486,67 @@ impl User { .into_iter() .collect::>() .join(", "), - ).expect("User::fetch_followers_ids: accept header error"), + )? ) - .send(); - match req { - Ok(mut res) => { - let text = &res.text().expect("User::fetch_followers_ids: body error"); - let json: serde_json::Value = - serde_json::from_str(text).expect("User::fetch_followers_ids: parsing error"); - json["items"] - .as_array() - .unwrap_or(&vec![]) - .into_iter() - .filter_map(|j| serde_json::from_value(j.clone()).ok()) - .collect::>() - } - Err(e) => { - println!("User followers fetch error: {:?}", e); - vec![] - } - } + .send()?; + let text = &res.text()?; + let json: serde_json::Value = serde_json::from_str(text)?; + Ok(json["items"] + .as_array() + .unwrap_or(&vec![]) + .into_iter() + .filter_map(|j| serde_json::from_value(j.clone()).ok()) + .collect::>()) } - fn get_activities(&self, conn: &Connection) -> Vec { + fn get_activities(&self, conn: &Connection) -> Result> { use schema::post_authors; use schema::posts; let posts_by_self = PostAuthor::belonging_to(self).select(post_authors::post_id); let posts = posts::table .filter(posts::published.eq(true)) .filter(posts::id.eq_any(posts_by_self)) - .load::(conn) - .expect("User::get_activities: loading error"); - posts + .load::(conn)?; + Ok(posts .into_iter() - .map(|p| { - serde_json::to_value(p.create_activity(conn)) - .expect("User::get_activities: creation error") + .filter_map(|p| { + p.create_activity(conn).ok().and_then(|a| serde_json::to_value(a).ok()) }) - .collect::>() + .collect::>()) } pub fn get_fqn(&self, conn: &Connection) -> String { - if self.instance_id == Instance::local_id(conn) { + if self.instance_id == Instance::get_local(conn).ok().expect("User::get_fqn: instance error").id { self.username.clone() } else { format!( "{}@{}", self.username, - self.get_instance(conn).public_domain + self.get_instance(conn).ok().expect("User::get_fqn: instance error").public_domain ) } } - pub fn get_followers(&self, conn: &Connection) -> Vec { + pub fn get_followers(&self, conn: &Connection) -> Result> { use schema::follows; let follows = Follow::belonging_to(self).select(follows::follower_id); users::table .filter(users::id.eq_any(follows)) .load::(conn) - .expect("User::get_followers: loading error") + .map_err(Error::from) } - pub fn count_followers(&self, conn: &Connection) -> i64 { + pub fn count_followers(&self, conn: &Connection) -> Result { use schema::follows; let follows = Follow::belonging_to(self).select(follows::follower_id); users::table .filter(users::id.eq_any(follows)) .count() .get_result(conn) - .expect("User::count_followers: counting error") + .map_err(Error::from) } - pub fn get_followers_page(&self, conn: &Connection, (min, max): (i32, i32)) -> Vec { + pub fn get_followers_page(&self, conn: &Connection, (min, max): (i32, i32)) -> Result> { use schema::follows; let follows = Follow::belonging_to(self).select(follows::follower_id); users::table @@ -656,165 +554,155 @@ impl User { .offset(min.into()) .limit((max - min).into()) .load::(conn) - .expect("User::get_followers_page: loading error") + .map_err(Error::from) } - pub fn get_following(&self, conn: &Connection) -> Vec { + pub fn get_following(&self, conn: &Connection) -> Result> { use schema::follows::dsl::*; let f = follows.filter(follower_id.eq(self.id)).select(following_id); users::table .filter(users::id.eq_any(f)) .load::(conn) - .expect("User::get_following: loading error") + .map_err(Error::from) } - pub fn is_followed_by(&self, conn: &Connection, other_id: i32) -> bool { + pub fn is_followed_by(&self, conn: &Connection, other_id: i32) -> Result { use schema::follows; follows::table .filter(follows::follower_id.eq(other_id)) .filter(follows::following_id.eq(self.id)) .count() .get_result::(conn) - .expect("User::is_followed_by: loading error") > 0 + .map_err(Error::from) + .map(|r| r > 0) } - pub fn is_following(&self, conn: &Connection, other_id: i32) -> bool { + pub fn is_following(&self, conn: &Connection, other_id: i32) -> Result { use schema::follows; follows::table .filter(follows::follower_id.eq(self.id)) .filter(follows::following_id.eq(other_id)) .count() .get_result::(conn) - .expect("User::is_following: loading error") > 0 + .map_err(Error::from) + .map(|r| r > 0) } - pub fn has_liked(&self, conn: &Connection, post: &Post) -> bool { + pub fn has_liked(&self, conn: &Connection, post: &Post) -> Result { use schema::likes; likes::table .filter(likes::post_id.eq(post.id)) .filter(likes::user_id.eq(self.id)) .count() .get_result::(conn) - .expect("User::has_liked: loading error") > 0 + .map_err(Error::from) + .map(|r| r > 0) } - pub fn has_reshared(&self, conn: &Connection, post: &Post) -> bool { + pub fn has_reshared(&self, conn: &Connection, post: &Post) -> Result { use schema::reshares; reshares::table .filter(reshares::post_id.eq(post.id)) .filter(reshares::user_id.eq(self.id)) .count() .get_result::(conn) - .expect("User::has_reshared: loading error") > 0 + .map_err(Error::from) + .map(|r| r > 0) } - pub fn is_author_in(&self, conn: &Connection, blog: &Blog) -> bool { + pub fn is_author_in(&self, conn: &Connection, blog: &Blog) -> Result { use schema::blog_authors; blog_authors::table .filter(blog_authors::author_id.eq(self.id)) .filter(blog_authors::blog_id.eq(blog.id)) .count() .get_result::(conn) - .expect("User::is_author_in: loading error") > 0 + .map_err(Error::from) + .map(|r| r > 0) } - pub fn get_keypair(&self) -> PKey { + pub fn get_keypair(&self) -> Result> { PKey::from_rsa( Rsa::private_key_from_pem( self.private_key - .clone() - .expect("User::get_keypair: private key not found error") + .clone()? .as_ref(), - ).expect("User::get_keypair: pem parsing error"), - ).expect("User::get_keypair: private key deserialization error") + )?, + ).map_err(Error::from) } - pub fn to_activity(&self, conn: &Connection) -> CustomPerson { + pub fn to_activity(&self, conn: &Connection) -> Result { let mut actor = Person::default(); actor .object_props - .set_id_string(self.ap_url.clone()) - .expect("User::to_activity: id error"); + .set_id_string(self.ap_url.clone())?; actor .object_props - .set_name_string(self.display_name.clone()) - .expect("User::to_activity: name error"); + .set_name_string(self.display_name.clone())?; actor .object_props - .set_summary_string(self.summary.get().clone()) - .expect("User::to_activity: summary error"); + .set_summary_string(self.summary.get().clone())?; actor .object_props - .set_url_string(self.ap_url.clone()) - .expect("User::to_activity: url error"); + .set_url_string(self.ap_url.clone())?; actor .ap_actor_props - .set_inbox_string(self.inbox_url.clone()) - .expect("User::to_activity: inbox error"); + .set_inbox_string(self.inbox_url.clone())?; actor .ap_actor_props - .set_outbox_string(self.outbox_url.clone()) - .expect("User::to_activity: outbox error"); + .set_outbox_string(self.outbox_url.clone())?; actor .ap_actor_props - .set_preferred_username_string(self.username.clone()) - .expect("User::to_activity: preferredUsername error"); + .set_preferred_username_string(self.username.clone())?; actor .ap_actor_props - .set_followers_string(self.followers_endpoint.clone()) - .expect("User::to_activity: followers error"); + .set_followers_string(self.followers_endpoint.clone())?; let mut endpoints = Endpoint::default(); endpoints - .set_shared_inbox_string(ap_url(&format!("{}/inbox/", BASE_URL.as_str()))) - .expect("User::to_activity: endpoints.sharedInbox error"); + .set_shared_inbox_string(ap_url(&format!("{}/inbox/", BASE_URL.as_str())))?; actor .ap_actor_props - .set_endpoints_endpoint(endpoints) - .expect("User::to_activity: endpoints error"); + .set_endpoints_endpoint(endpoints)?; let mut public_key = PublicKey::default(); public_key - .set_id_string(format!("{}#main-key", self.ap_url)) - .expect("User::to_activity: publicKey.id error"); + .set_id_string(format!("{}#main-key", self.ap_url))?; public_key - .set_owner_string(self.ap_url.clone()) - .expect("User::to_activity: publicKey.owner error"); + .set_owner_string(self.ap_url.clone())?; public_key - .set_public_key_pem_string(self.public_key.clone()) - .expect("User::to_activity: publicKey.publicKeyPem error"); + .set_public_key_pem_string(self.public_key.clone())?; let mut ap_signature = ApSignature::default(); ap_signature - .set_public_key_publickey(public_key) - .expect("User::to_activity: publicKey error"); + .set_public_key_publickey(public_key)?; let mut avatar = Image::default(); avatar .object_props .set_url_string( self.avatar_id - .and_then(|id| Media::get(conn, id).map(|m| m.url(conn))) + .and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok()) .unwrap_or_default(), - ) - .expect("User::to_activity: icon.url error"); + )?; actor .object_props - .set_icon_object(avatar) - .expect("User::to_activity: icon error"); + .set_icon_object(avatar)?; - CustomPerson::new(actor, ap_signature) + Ok(CustomPerson::new(actor, ap_signature)) } pub fn avatar_url(&self, conn: &Connection) -> String { - self.avatar_id.and_then(|id| Media::get(conn, id).map(|m| m.url(conn))).unwrap_or("/static/default-avatar.png".to_string()) + self.avatar_id.and_then(|id| + Media::get(conn, id).and_then(|m| m.url(conn)).ok() + ).unwrap_or("/static/default-avatar.png".to_string()) } - pub fn webfinger(&self, conn: &Connection) -> Webfinger { - Webfinger { + pub fn webfinger(&self, conn: &Connection) -> Result { + Ok(Webfinger { subject: format!( "acct:{}@{}", self.username, - self.get_instance(conn).public_domain + self.get_instance(conn)?.public_domain ), aliases: vec![self.ap_url.clone()], links: vec![ @@ -827,7 +715,7 @@ impl User { Link { rel: String::from("http://schemas.google.com/g/2010#updates-from"), mime_type: Some(String::from("application/atom+xml")), - href: Some(self.get_instance(conn).compute_box( + href: Some(self.get_instance(conn)?.compute_box( USER_PREFIX, &self.username, "feed.atom", @@ -841,30 +729,27 @@ impl User { template: None, }, ], - } + }) } - pub fn from_url(conn: &Connection, url: &str) -> Option { - User::find_by_ap_url(conn, url).or_else(|| { + pub fn from_url(conn: &Connection, url: &str) -> Result { + User::find_by_ap_url(conn, url).or_else(|_| { // The requested user was not in the DB // We try to fetch it if it is remote - if Url::parse(&url) - .expect("User::from_url: url error") - .host_str() - .expect("User::from_url: host error") != BASE_URL.as_str() - { + if Url::parse(&url)?.host_str()? != BASE_URL.as_str() { User::fetch_from_url(conn, url) } else { - None + Err(Error::NotFound) } }) } - pub fn set_avatar(&self, conn: &Connection, id: i32) { + pub fn set_avatar(&self, conn: &Connection, id: i32) -> Result<()> { diesel::update(self) .set(users::avatar_id.eq(id)) .execute(conn) - .expect("User::set_avatar: update error"); + .map(|_| ()) + .map_err(Error::from) } pub fn needs_update(&self) -> bool { @@ -889,7 +774,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for User { .cookies() .get_private(AUTH_COOKIE) .and_then(|cookie| cookie.value().parse().ok()) - .map(|id| User::get(&*conn, id).expect("User::from_request: user not found error")) + .and_then(|id| User::get(&*conn, id).ok()) .or_forward(()) } } @@ -919,35 +804,30 @@ impl WithInbox for User { } impl Signer for User { + type Error = Error; + fn get_key_id(&self) -> String { format!("{}#main-key", self.ap_url) } - fn sign(&self, to_sign: &str) -> Vec { - let key = self.get_keypair(); - let mut signer = sign::Signer::new(MessageDigest::sha256(), &key) - .expect("User::sign: initialization error"); + fn sign(&self, to_sign: &str) -> Result> { + let key = self.get_keypair()?; + let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?; signer - .update(to_sign.as_bytes()) - .expect("User::sign: content insertion error"); + .update(to_sign.as_bytes())?; signer .sign_to_vec() - .expect("User::sign: finalization error") + .map_err(Error::from) } - fn verify(&self, data: &str, signature: &[u8]) -> bool { - let key = PKey::from_rsa( - Rsa::public_key_from_pem(self.public_key.as_ref()) - .expect("User::verify: pem parsing error"), - ).expect("User::verify: deserialization error"); - let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key) - .expect("User::verify: initialization error"); + fn verify(&self, data: &str, signature: &[u8]) -> Result { + let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?; + let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?; verifier - .update(data.as_bytes()) - .expect("User::verify: content insertion error"); + .update(data.as_bytes())?; verifier .verify(&signature) - .expect("User::verify: finalization error") + .map_err(Error::from) } } @@ -973,7 +853,7 @@ impl NewUser { summary: &str, email: String, password: String, - ) -> User { + ) -> Result { let (pub_key, priv_key) = gen_keypair(); User::insert( conn, @@ -986,7 +866,7 @@ impl NewUser { summary: SafeString::new(summary), email: Some(email), hashed_password: Some(password), - instance_id: Instance::local_id(conn), + instance_id: Instance::get_local(conn)?.id, ap_url: String::from(""), public_key: String::from_utf8(pub_key) .expect("NewUser::new_local: public key error"), @@ -1020,8 +900,8 @@ pub(crate) mod tests { "Hello there, I'm the admin", "admin@example.com".to_owned(), "invalid_admin_password".to_owned(), - ); - admin.update_boxes(conn); + ).unwrap(); + admin.update_boxes(conn).unwrap(); let user = NewUser::new_local( conn, "user".to_owned(), @@ -1030,8 +910,8 @@ pub(crate) mod tests { "Hello there, I'm no one", "user@example.com".to_owned(), "invalid_user_password".to_owned(), - ); - user.update_boxes(conn); + ).unwrap(); + user.update_boxes(conn).unwrap(); let other = NewUser::new_local( conn, "other".to_owned(), @@ -1040,8 +920,8 @@ pub(crate) mod tests { "Hello there, I'm someone else", "other@example.com".to_owned(), "invalid_other_password".to_owned(), - ); - other.update_boxes(conn); + ).unwrap(); + other.update_boxes(conn).unwrap(); vec![ admin, user, other ] } @@ -1057,13 +937,13 @@ pub(crate) mod tests { false, "Hello I'm a test", "test@example.com".to_owned(), - User::hash_pass("test_password"), - ); - test_user.update_boxes(conn); + User::hash_pass("test_password").unwrap(), + ).unwrap(); + test_user.update_boxes(conn).unwrap(); assert_eq!( test_user.id, - User::find_by_name(conn, "test", Instance::local_id(conn)) + User::find_by_name(conn, "test", Instance::get_local(conn).unwrap().id) .unwrap() .id ); @@ -1100,9 +980,9 @@ pub(crate) mod tests { conn.test_transaction::<_, (), _>(|| { let inserted = fill_database(conn); - assert!(User::get(conn, inserted[0].id).is_some()); - inserted[0].delete(conn, &get_searcher()); - assert!(User::get(conn, inserted[0].id).is_none()); + assert!(User::get(conn, inserted[0].id).is_ok()); + inserted[0].delete(conn, &get_searcher()).unwrap(); + assert!(User::get(conn, inserted[0].id).is_err()); Ok(()) }); @@ -1115,13 +995,13 @@ pub(crate) mod tests { let inserted = fill_database(conn); let local_inst = Instance::get_local(conn).unwrap(); let mut i = 0; - while local_inst.has_admin(conn) { + while local_inst.has_admin(conn).unwrap() { assert!(i < 100); //prevent from looping indefinitelly - local_inst.main_admin(conn).revoke_admin_rights(conn); + local_inst.main_admin(conn).unwrap().revoke_admin_rights(conn).unwrap(); i += 1; } - inserted[0].grant_admin_rights(conn); - assert_eq!(inserted[0].id, local_inst.main_admin(conn).id); + inserted[0].grant_admin_rights(conn).unwrap(); + assert_eq!(inserted[0].id, local_inst.main_admin(conn).unwrap().id); Ok(()) }); @@ -1137,7 +1017,7 @@ pub(crate) mod tests { "new name".to_owned(), "em@il".to_owned(), "

summary

".to_owned(), - ); + ).unwrap(); assert_eq!(updated.display_name, "new name"); assert_eq!(updated.email.unwrap(), "em@il"); assert_eq!(updated.summary.get(), "

summary

"); @@ -1158,9 +1038,9 @@ pub(crate) mod tests { false, "Hello I'm a test", "test@example.com".to_owned(), - User::hash_pass("test_password"), - ); - test_user.update_boxes(conn); + User::hash_pass("test_password").unwrap(), + ).unwrap(); + test_user.update_boxes(conn).unwrap(); assert!(test_user.auth("test_password")); assert!(!test_user.auth("other_password")); @@ -1175,20 +1055,20 @@ pub(crate) mod tests { conn.test_transaction::<_, (), _>(|| { fill_database(conn); - let page = User::get_local_page(conn, (0, 2)); + let page = User::get_local_page(conn, (0, 2)).unwrap(); assert_eq!(page.len(), 2); assert!(page[0].username <= page[1].username); - let mut last_username = User::get_local_page(conn, (0, 1))[0].username.clone(); - for i in 1..User::count_local(conn) as i32 { - let page = User::get_local_page(conn, (i, i + 1)); + let mut last_username = User::get_local_page(conn, (0, 1)).unwrap()[0].username.clone(); + for i in 1..User::count_local(conn).unwrap() as i32 { + let page = User::get_local_page(conn, (i, i + 1)).unwrap(); assert_eq!(page.len(), 1); assert!(last_username <= page[0].username); last_username = page[0].username.clone(); } assert_eq!( - User::get_local_page(conn, (0, User::count_local(conn) as i32 + 10)).len() as i64, - User::count_local(conn) + User::get_local_page(conn, (0, User::count_local(conn).unwrap() as i32 + 10)).unwrap().len() as i64, + User::count_local(conn).unwrap() ); Ok(()) diff --git a/src/api/mod.rs b/src/api/mod.rs index defdd8b8..f4417eaf 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,15 +1,41 @@ -use rocket::request::Form; +use rocket::{response::{self, Responder}, request::{Form, Request}}; use rocket_contrib::json::Json; use serde_json; use plume_common::utils::random_hex; use plume_models::{ + Error, apps::App, api_tokens::*, db_conn::DbConn, users::User, }; +#[derive(Debug)] +pub struct ApiError(Error); + +impl From for ApiError { + fn from(err: Error) -> ApiError { + ApiError(err) + } +} + +impl<'r> Responder<'r> for ApiError { + fn respond_to(self, req: &Request) -> response::Result<'r> { + match self.0 { + Error::NotFound => Json(json!({ + "error": "Not found" + })).respond_to(req), + Error::Unauthorized => Json(json!({ + "error": "You are not authorized to access this resource" + })).respond_to(req), + _ => Json(json!({ + "error": "Server error" + })).respond_to(req) + } + } +} + #[derive(FromForm)] pub struct OAuthRequest { client_id: String, @@ -20,38 +46,38 @@ pub struct OAuthRequest { } #[get("/oauth2?")] -pub fn oauth(query: Form, conn: DbConn) -> Json { - let app = App::find_by_client_id(&*conn, &query.client_id).expect("OAuth request from unknown client"); +pub fn oauth(query: Form, conn: DbConn) -> Result, ApiError> { + let app = App::find_by_client_id(&*conn, &query.client_id)?; if app.client_secret == query.client_secret { - if let Some(user) = User::find_local(&*conn, &query.username) { + if let Ok(user) = User::find_local(&*conn, &query.username) { if user.auth(&query.password) { let token = ApiToken::insert(&*conn, NewApiToken { app_id: app.id, user_id: user.id, value: random_hex(), scopes: query.scopes.clone(), - }); - Json(json!({ + })?; + Ok(Json(json!({ "token": token.value - })) + }))) } else { - Json(json!({ + Ok(Json(json!({ "error": "Invalid credentials" - })) + }))) } } else { // Making fake password verification to avoid different // response times that would make it possible to know // if a username is registered or not. - User::get(&*conn, 1).unwrap().auth(&query.password); - Json(json!({ + User::get(&*conn, 1)?.auth(&query.password); + Ok(Json(json!({ "error": "Invalid credentials" - })) + }))) } } else { - Json(json!({ + Ok(Json(json!({ "error": "Invalid client_secret" - })) + }))) } } diff --git a/src/inbox.rs b/src/inbox.rs index 670aba12..b5a9677d 100644 --- a/src/inbox.rs +++ b/src/inbox.rs @@ -42,13 +42,14 @@ pub trait Inbox { match act["type"].as_str() { Some(t) => match t { "Announce" => { - Reshare::from_activity(conn, serde_json::from_value(act.clone())?, actor_id); + Reshare::from_activity(conn, serde_json::from_value(act.clone())?, actor_id) + .expect("Inbox::received: Announce error");; Ok(()) } "Create" => { let act: Create = serde_json::from_value(act.clone())?; - if Post::try_from_activity(&(conn, searcher), act.clone()) - || Comment::try_from_activity(conn, act) + if Post::try_from_activity(&(conn, searcher), act.clone()).is_ok() + || Comment::try_from_activity(conn, act).is_ok() { Ok(()) } else { @@ -64,7 +65,7 @@ pub trait Inbox { .id_string()?, actor_id.as_ref(), &(conn, searcher), - ); + ).ok(); Comment::delete_id( &act.delete_props .object_object::()? @@ -72,11 +73,12 @@ pub trait Inbox { .id_string()?, actor_id.as_ref(), conn, - ); + ).ok(); Ok(()) } "Follow" => { - Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id).notify(conn); + Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id) + .and_then(|f| f.notify(conn)).expect("Inbox::received: follow from activity error");; Ok(()) } "Like" => { @@ -84,7 +86,7 @@ pub trait Inbox { conn, serde_json::from_value(act.clone())?, actor_id, - ); + ).expect("Inbox::received: like from activity error");; Ok(()) } "Undo" => { @@ -99,7 +101,7 @@ pub trait Inbox { .id_string()?, actor_id.as_ref(), conn, - ); + ).expect("Inbox::received: undo like fail");; Ok(()) } "Announce" => { @@ -110,7 +112,7 @@ pub trait Inbox { .id_string()?, actor_id.as_ref(), conn, - ); + ).expect("Inbox::received: undo reshare fail");; Ok(()) } "Follow" => { @@ -121,21 +123,21 @@ pub trait Inbox { .id_string()?, actor_id.as_ref(), conn, - ); + ).expect("Inbox::received: undo follow error");; Ok(()) } _ => Err(InboxError::CantUndo)?, } } else { let link = act.undo_props.object.as_str().expect("Inbox::received: undo don't contain type and isn't Link"); - if let Some(like) = likes::Like::find_by_ap_url(conn, link) { - likes::Like::delete_id(&like.ap_url, actor_id.as_ref(), conn); + if let Ok(like) = likes::Like::find_by_ap_url(conn, link) { + likes::Like::delete_id(&like.ap_url, actor_id.as_ref(), conn).expect("Inbox::received: delete Like error"); Ok(()) - } else if let Some(reshare) = Reshare::find_by_ap_url(conn, link) { - Reshare::delete_id(&reshare.ap_url, actor_id.as_ref(), conn); + } else if let Ok(reshare) = Reshare::find_by_ap_url(conn, link) { + Reshare::delete_id(&reshare.ap_url, actor_id.as_ref(), conn).expect("Inbox::received: delete Announce error"); Ok(()) - } else if let Some(follow) = Follow::find_by_ap_url(conn, link) { - Follow::delete_id(&follow.ap_url, actor_id.as_ref(), conn); + } else if let Ok(follow) = Follow::find_by_ap_url(conn, link) { + Follow::delete_id(&follow.ap_url, actor_id.as_ref(), conn).expect("Inbox::received: delete Follow error"); Ok(()) } else { Err(InboxError::NoType)? @@ -144,7 +146,7 @@ pub trait Inbox { } "Update" => { let act: Update = serde_json::from_value(act.clone())?; - Post::handle_update(conn, &act.update_props.object_object()?, searcher); + Post::handle_update(conn, &act.update_props.object_object()?, searcher).expect("Inbox::received: post update error");; Ok(()) } _ => Err(InboxError::InvalidType)?, diff --git a/src/main.rs b/src/main.rs index a160e887..8e98ebe0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,8 +38,11 @@ extern crate webfinger; use diesel::r2d2::ConnectionManager; use rocket::State; use rocket_csrf::CsrfFairingBuilder; -use plume_models::{DATABASE_URL, Connection, - db_conn::{DbPool, PragmaForeignKey}, search::Searcher as UnmanagedSearcher}; +use plume_models::{ + DATABASE_URL, Connection, Error, + db_conn::{DbPool, PragmaForeignKey}, + search::{Searcher as UnmanagedSearcher, SearcherError}, +}; use scheduled_thread_pool::ScheduledThreadPool; use std::process::exit; use std::sync::Arc; @@ -65,10 +68,23 @@ fn init_pool() -> Option { } fn main() { - let dbpool = init_pool().expect("main: database pool initialization error"); let workpool = ScheduledThreadPool::with_name("worker {}", num_cpus::get()); - let searcher = Arc::new(UnmanagedSearcher::open(&"search_index").unwrap()); + let searcher = match UnmanagedSearcher::open(&"search_index") { + Err(Error::Search(e)) => match e { + SearcherError::WriteLockAcquisitionError => panic!( +r#"Your search index is locked. Plume can't start. To fix this issue +make sure no other Plume instance is started, and run: + + plm search unlock + +Then try to restart Plume. +"#), + e => Err(e).unwrap() + }, + Err(_) => panic!("Unexpected error while opening search index"), + Ok(s) => Arc::new(s) + }; let commiter = searcher.clone(); workpool.execute_with_fixed_delay(Duration::from_secs(5), Duration::from_secs(60*30), move || commiter.commit()); diff --git a/src/routes/blogs.rs b/src/routes/blogs.rs index bb880110..f5fa8032 100644 --- a/src/routes/blogs.rs +++ b/src/routes/blogs.rs @@ -19,18 +19,17 @@ use plume_models::{ posts::Post, users::User }; -use routes::Page; +use routes::{Page, errors::ErrorPage}; use template_utils::Ructe; use Searcher; #[get("/~/?", rank = 2)] -pub fn details(intl: I18n, name: String, conn: DbConn, user: Option, page: Option) -> Result { +pub fn details(intl: I18n, name: String, conn: DbConn, user: Option, page: Option) -> Result { let page = page.unwrap_or_default(); - let blog = Blog::find_by_fqn(&*conn, &name) - .ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, user.clone()))))?; - let posts = Post::blog_page(&*conn, &blog, page.limits()); - let articles_count = Post::count_for_blog(&*conn, &blog); - let authors = &blog.list_authors(&*conn); + let blog = Blog::find_by_fqn(&*conn, &name)?; + let posts = Post::blog_page(&*conn, &blog, page.limits())?; + let articles_count = Post::count_for_blog(&*conn, &blog)?; + let authors = &blog.list_authors(&*conn)?; Ok(render!(blogs::details( &(&*conn, &intl.catalog, user.clone()), @@ -40,15 +39,15 @@ pub fn details(intl: I18n, name: String, conn: DbConn, user: Option, page: articles_count, page.0, Page::total(articles_count as i32), - user.map(|x| x.is_author_in(&*conn, &blog)).unwrap_or(false), + user.and_then(|x| x.is_author_in(&*conn, &blog).ok()).unwrap_or(false), posts ))) } #[get("/~/", rank = 1)] pub fn activity_details(name: String, conn: DbConn, _ap: ApRequest) -> Option> { - let blog = Blog::find_local(&*conn, &name)?; - Some(ActivityStream::new(blog.to_activity(&*conn))) + let blog = Blog::find_local(&*conn, &name).ok()?; + Some(ActivityStream::new(blog.to_activity(&*conn).ok()?)) } #[get("/blogs/new")] @@ -91,7 +90,7 @@ pub fn create(conn: DbConn, form: LenientForm, user: User, intl: I1 Ok(_) => ValidationErrors::new(), Err(e) => e }; - if Blog::find_local(&*conn, &slug).is_some() { + if Blog::find_local(&*conn, &slug).is_ok() { errors.add("title", ValidationError { code: Cow::from("existing_slug"), message: Some(Cow::from("A blog with the same name already exists.")), @@ -104,19 +103,19 @@ pub fn create(conn: DbConn, form: LenientForm, user: User, intl: I1 slug.clone(), form.title.to_string(), String::from(""), - Instance::local_id(&*conn) - )); - blog.update_boxes(&*conn); + Instance::get_local(&*conn).expect("blog::create: instance error").id + ).expect("blog::create: new local error")).expect("blog::create: error"); + blog.update_boxes(&*conn).expect("blog::create: insert error"); BlogAuthor::insert(&*conn, NewBlogAuthor { blog_id: blog.id, author_id: user.id, is_owner: true - }); + }).expect("blog::create: author error"); Ok(Redirect::to(uri!(details: name = slug.clone(), page = _))) } else { - Err(render!(blogs::new( + Err(render!(blogs::new( &(&*conn, &intl.catalog, Some(user)), &*form, errors @@ -125,38 +124,37 @@ pub fn create(conn: DbConn, form: LenientForm, user: User, intl: I1 } #[post("/~//delete")] -pub fn delete(conn: DbConn, name: String, user: Option, intl: I18n, searcher: Searcher) -> Result>{ - let blog = Blog::find_local(&*conn, &name).ok_or(None)?; - if user.clone().map(|u| u.is_author_in(&*conn, &blog)).unwrap_or(false) { - blog.delete(&conn, &searcher); +pub fn delete(conn: DbConn, name: String, user: Option, intl: I18n, searcher: Searcher) -> Result{ + let blog = Blog::find_local(&*conn, &name).expect("blog::delete: blog not found"); + if user.clone().and_then(|u| u.is_author_in(&*conn, &blog).ok()).unwrap_or(false) { + blog.delete(&conn, &searcher).expect("blog::expect: deletion error"); Ok(Redirect::to(uri!(super::instance::index))) } else { // TODO actually return 403 error code - Err(Some(render!(errors::not_authorized( + Err(render!(errors::not_authorized( &(&*conn, &intl.catalog, user), "You are not allowed to delete this blog." - )))) + ))) } } #[get("/~//outbox")] pub fn outbox(name: String, conn: DbConn) -> Option> { - let blog = Blog::find_local(&*conn, &name)?; - Some(blog.outbox(&*conn)) + let blog = Blog::find_local(&*conn, &name).ok()?; + Some(blog.outbox(&*conn).ok()?) } #[get("/~//atom.xml")] pub fn atom_feed(name: String, conn: DbConn) -> Option> { - let blog = Blog::find_by_fqn(&*conn, &name)?; + let blog = Blog::find_by_fqn(&*conn, &name).ok()?; let feed = FeedBuilder::default() .title(blog.title.clone()) - .id(Instance::get_local(&*conn).expect("blogs::atom_feed: local instance not found error") + .id(Instance::get_local(&*conn).ok()? .compute_box("~", &name, "atom.xml")) - .entries(Post::get_recents_for_blog(&*conn, &blog, 15) + .entries(Post::get_recents_for_blog(&*conn, &blog, 15).ok()? .into_iter() .map(|p| super::post_to_atom(p, &*conn)) .collect::>()) - .build() - .expect("blogs::atom_feed: feed creation error"); + .build().ok()?; Some(Content(ContentType::new("application", "atom+xml"), feed.to_string())) } diff --git a/src/routes/comments.rs b/src/routes/comments.rs index 356cf725..34bccd31 100644 --- a/src/routes/comments.rs +++ b/src/routes/comments.rs @@ -21,6 +21,7 @@ use plume_models::{ users::User }; use Worker; +use routes::errors::ErrorPage; #[derive(Default, FromForm, Debug, Validate, Serialize)] pub struct NewCommentForm { @@ -32,12 +33,15 @@ pub struct NewCommentForm { #[post("/~///comment", data = "
")] pub fn create(blog_name: String, slug: String, form: LenientForm, user: User, conn: DbConn, worker: Worker, intl: I18n) - -> Result> { - let blog = Blog::find_by_fqn(&*conn, &blog_name).ok_or(None)?; - let post = Post::find_by_slug(&*conn, &slug, blog.id).ok_or(None)?; + -> Result { + let blog = Blog::find_by_fqn(&*conn, &blog_name).expect("comments::create: blog error"); + let post = Post::find_by_slug(&*conn, &slug, blog.id).expect("comments::create: post error"); form.validate() .map(|_| { - let (html, mentions, _hashtags) = utils::md_to_html(form.content.as_ref(), &Instance::get_local(&conn).expect("comments::create: Error getting local instance").public_domain); + let (html, mentions, _hashtags) = utils::md_to_html( + form.content.as_ref(), + &Instance::get_local(&conn).expect("comments::create: local instance error").public_domain + ); let comm = Comment::insert(&*conn, NewComment { content: SafeString::new(html.as_ref()), in_response_to_id: form.responding_to, @@ -47,16 +51,22 @@ pub fn create(blog_name: String, slug: String, form: LenientForm sensitive: !form.warning.is_empty(), spoiler_text: form.warning.clone(), public_visibility: true - }).update_ap_url(&*conn); - let new_comment = comm.create_activity(&*conn); + }).expect("comments::create: insert error").update_ap_url(&*conn).expect("comments::create: update ap url error"); + let new_comment = comm.create_activity(&*conn).expect("comments::create: activity error"); // save mentions for ment in mentions { - Mention::from_activity(&*conn, &Mention::build_activity(&*conn, &ment), post.id, true, true); + Mention::from_activity( + &*conn, + &Mention::build_activity(&*conn, &ment).expect("comments::create: build mention error"), + post.id, + true, + true + ).expect("comments::create: mention save error"); } // federate - let dest = User::one_by_instance(&*conn); + let dest = User::one_by_instance(&*conn).expect("comments::create: dest error"); let user_clone = user.clone(); worker.execute(move || broadcast(&user_clone, new_comment, dest)); @@ -64,43 +74,46 @@ pub fn create(blog_name: String, slug: String, form: LenientForm }) .map_err(|errors| { // TODO: de-duplicate this code - let comments = CommentTree::from_post(&*conn, &post, Some(&user)); + let comments = CommentTree::from_post(&*conn, &post, Some(&user)).expect("comments::create: comments error"); - let previous = form.responding_to.map(|r| Comment::get(&*conn, r) - .expect("comments::create: Error retrieving previous comment")); + let previous = form.responding_to.and_then(|r| Comment::get(&*conn, r).ok()); - Some(render!(posts::details( + render!(posts::details( &(&*conn, &intl.catalog, Some(user.clone())), post.clone(), blog, &*form, errors, - Tag::for_post(&*conn, post.id), + Tag::for_post(&*conn, post.id).expect("comments::create: tags error"), comments, previous, - post.count_likes(&*conn), - post.count_reshares(&*conn), - user.has_liked(&*conn, &post), - user.has_reshared(&*conn, &post), - user.is_following(&*conn, post.get_authors(&*conn)[0].id), - post.get_authors(&*conn)[0].clone() - ))) + post.count_likes(&*conn).expect("comments::create: count likes error"), + post.count_reshares(&*conn).expect("comments::create: count reshares error"), + user.has_liked(&*conn, &post).expect("comments::create: liked error"), + user.has_reshared(&*conn, &post).expect("comments::create: reshared error"), + user.is_following(&*conn, post.get_authors(&*conn).expect("comments::create: authors error")[0].id) + .expect("comments::create: following error"), + post.get_authors(&*conn).expect("comments::create: authors error")[0].clone() + )) }) } #[post("/~///comment//delete")] -pub fn delete(blog: String, slug: String, id: i32, user: User, conn: DbConn, worker: Worker) -> Redirect { - if let Some(comment) = Comment::get(&*conn, id) { +pub fn delete(blog: String, slug: String, id: i32, user: User, conn: DbConn, worker: Worker) -> Result { + if let Ok(comment) = Comment::get(&*conn, id) { if comment.author_id == user.id { - let dest = User::one_by_instance(&*conn); - let delete_activity = comment.delete(&*conn); + let dest = User::one_by_instance(&*conn)?; + let delete_activity = comment.delete(&*conn)?; worker.execute(move || broadcast(&user, delete_activity, dest)); } } - Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _)) + Ok(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _))) } #[get("/~/<_blog>/<_slug>/comment/")] pub fn activity_pub(_blog: String, _slug: String, id: i32, _ap: ApRequest, conn: DbConn) -> Option> { - Comment::get(&*conn, id).map(|c| ActivityStream::new(c.to_activity(&*conn))) + Comment::get(&*conn, id) + .and_then(|c| c.to_activity(&*conn)) + .ok() + .map(ActivityStream::new) } diff --git a/src/routes/errors.rs b/src/routes/errors.rs index f450e939..d91bd9fb 100644 --- a/src/routes/errors.rs +++ b/src/routes/errors.rs @@ -1,10 +1,42 @@ -use rocket::Request; -use rocket::request::FromRequest; +use rocket::{ + Request, + request::FromRequest, + response::{self, Responder}, +}; use rocket_i18n::I18n; -use plume_models::db_conn::DbConn; +use plume_models::{Error, db_conn::DbConn}; use plume_models::users::User; use template_utils::Ructe; +#[derive(Debug)] +pub struct ErrorPage(Error); + +impl From for ErrorPage { + fn from(err: Error) -> ErrorPage { + ErrorPage(err) + } +} + +impl<'r> Responder<'r> for ErrorPage { + fn respond_to(self, req: &Request) -> response::Result<'r> { + let conn = req.guard::().succeeded(); + let intl = req.guard::().succeeded(); + let user = User::from_request(req).succeeded(); + + match self.0 { + Error::NotFound => render!(errors::not_found( + &(&*conn.unwrap(), &intl.unwrap().catalog, user) + )).respond_to(req), + Error::Unauthorized => render!(errors::not_found( + &(&*conn.unwrap(), &intl.unwrap().catalog, user) + )).respond_to(req), + _ => render!(errors::not_found( + &(&*conn.unwrap(), &intl.unwrap().catalog, user) + )).respond_to(req) + } + } +} + #[catch(404)] pub fn not_found(req: &Request) -> Ructe { let conn = req.guard::().succeeded(); diff --git a/src/routes/instance.rs b/src/routes/instance.rs index 68f05b90..42554cb6 100644 --- a/src/routes/instance.rs +++ b/src/routes/instance.rs @@ -17,86 +17,78 @@ use plume_models::{ instance::* }; use inbox::{Inbox, SignedJson}; -use routes::Page; +use routes::{Page, errors::ErrorPage}; use template_utils::Ructe; use Searcher; #[get("/")] -pub fn index(conn: DbConn, user: Option, intl: I18n) -> Ructe { - match Instance::get_local(&*conn) { - Some(inst) => { - let federated = Post::get_recents_page(&*conn, Page::default().limits()); - let local = Post::get_instance_page(&*conn, inst.id, Page::default().limits()); - let user_feed = user.clone().map(|user| { - let followed = user.get_following(&*conn); - let mut in_feed = followed.into_iter().map(|u| u.id).collect::>(); - in_feed.push(user.id); - Post::user_feed_page(&*conn, in_feed, Page::default().limits()) - }); +pub fn index(conn: DbConn, user: Option, intl: I18n) -> Result { + let inst = Instance::get_local(&*conn)?; + let federated = Post::get_recents_page(&*conn, Page::default().limits())?; + let local = Post::get_instance_page(&*conn, inst.id, Page::default().limits())?; + let user_feed = user.clone().and_then(|user| { + let followed = user.get_following(&*conn).ok()?; + let mut in_feed = followed.into_iter().map(|u| u.id).collect::>(); + in_feed.push(user.id); + Post::user_feed_page(&*conn, in_feed, Page::default().limits()).ok() + }); - render!(instance::index( - &(&*conn, &intl.catalog, user), - inst, - User::count_local(&*conn), - Post::count_local(&*conn), - local, - federated, - user_feed - )) - } - None => { - render!(errors::server_error( - &(&*conn, &intl.catalog, user) - )) - } - } + Ok(render!(instance::index( + &(&*conn, &intl.catalog, user), + inst, + User::count_local(&*conn)?, + Post::count_local(&*conn)?, + local, + federated, + user_feed + ))) } #[get("/local?")] -pub fn local(conn: DbConn, user: Option, page: Option, intl: I18n) -> Ructe { +pub fn local(conn: DbConn, user: Option, page: Option, intl: I18n) -> Result { let page = page.unwrap_or_default(); - let instance = Instance::get_local(&*conn).expect("instance::paginated_local: local instance not found error"); - let articles = Post::get_instance_page(&*conn, instance.id, page.limits()); - render!(instance::local( + let instance = Instance::get_local(&*conn)?; + let articles = Post::get_instance_page(&*conn, instance.id, page.limits())?; + Ok(render!(instance::local( &(&*conn, &intl.catalog, user), instance, articles, page.0, - Page::total(Post::count_local(&*conn) as i32) - )) + Page::total(Post::count_local(&*conn)? as i32) + ))) } #[get("/feed?")] -pub fn feed(conn: DbConn, user: User, page: Option, intl: I18n) -> Ructe { +pub fn feed(conn: DbConn, user: User, page: Option, intl: I18n) -> Result { let page = page.unwrap_or_default(); - let followed = user.get_following(&*conn); + let followed = user.get_following(&*conn)?; let mut in_feed = followed.into_iter().map(|u| u.id).collect::>(); in_feed.push(user.id); - let articles = Post::user_feed_page(&*conn, in_feed, page.limits()); - render!(instance::feed( + let articles = Post::user_feed_page(&*conn, in_feed, page.limits())?; + Ok(render!(instance::feed( &(&*conn, &intl.catalog, Some(user)), articles, page.0, - Page::total(Post::count_local(&*conn) as i32) - )) + Page::total(Post::count_local(&*conn)? as i32) + ))) } #[get("/federated?")] -pub fn federated(conn: DbConn, user: Option, page: Option, intl: I18n) -> Ructe { +pub fn federated(conn: DbConn, user: Option, page: Option, intl: I18n) -> Result { let page = page.unwrap_or_default(); - let articles = Post::get_recents_page(&*conn, page.limits()); - render!(instance::federated( + let articles = Post::get_recents_page(&*conn, page.limits())?; + Ok(render!(instance::federated( &(&*conn, &intl.catalog, user), articles, page.0, - Page::total(Post::count_local(&*conn) as i32) - )) + Page::total(Post::count_local(&*conn)? as i32) + ))) } #[get("/admin")] -pub fn admin(conn: DbConn, admin: Admin, intl: I18n) -> Ructe { - let local_inst = Instance::get_local(&*conn).expect("instance::admin: local instance not found"); - render!(instance::admin( +pub fn admin(conn: DbConn, admin: Admin, intl: I18n) -> Result { + let local_inst = Instance::get_local(&*conn)?; + Ok(render!(instance::admin( &(&*conn, &intl.catalog, Some(admin.0)), local_inst.clone(), InstanceSettingsForm { @@ -107,7 +99,7 @@ pub fn admin(conn: DbConn, admin: Admin, intl: I18n) -> Ructe { default_license: local_inst.default_license, }, ValidationErrors::default() - )) + ))) } #[derive(Clone, FromForm, Validate, Serialize)] @@ -124,65 +116,65 @@ pub struct InstanceSettingsForm { #[post("/admin", data = "")] pub fn update_settings(conn: DbConn, admin: Admin, form: LenientForm, intl: I18n) -> Result { form.validate() - .map(|_| { - let instance = Instance::get_local(&*conn).expect("instance::update_settings: local instance not found error"); + .and_then(|_| { + let instance = Instance::get_local(&*conn).expect("instance::update_settings: local instance error"); instance.update(&*conn, form.name.clone(), form.open_registrations, form.short_description.clone(), - form.long_description.clone()); - Redirect::to(uri!(admin)) + form.long_description.clone()).expect("instance::update_settings: save error"); + Ok(Redirect::to(uri!(admin))) }) - .map_err(|e| { - let local_inst = Instance::get_local(&*conn).expect("instance::update_settings: local instance not found"); - render!(instance::admin( + .or_else(|e| { + let local_inst = Instance::get_local(&*conn).expect("instance::update_settings: local instance error"); + Err(render!(instance::admin( &(&*conn, &intl.catalog, Some(admin.0)), local_inst, form.clone(), e - )) + ))) }) } #[get("/admin/instances?")] -pub fn admin_instances(admin: Admin, conn: DbConn, page: Option, intl: I18n) -> Ructe { +pub fn admin_instances(admin: Admin, conn: DbConn, page: Option, intl: I18n) -> Result { let page = page.unwrap_or_default(); - let instances = Instance::page(&*conn, page.limits()); - render!(instance::list( + let instances = Instance::page(&*conn, page.limits())?; + Ok(render!(instance::list( &(&*conn, &intl.catalog, Some(admin.0)), - Instance::get_local(&*conn).expect("admin_instances: local instance error"), + Instance::get_local(&*conn)?, instances, page.0, - Page::total(Instance::count(&*conn) as i32) - )) + Page::total(Instance::count(&*conn)? as i32) + ))) } #[post("/admin/instances//block")] -pub fn toggle_block(_admin: Admin, conn: DbConn, id: i32) -> Redirect { - if let Some(inst) = Instance::get(&*conn, id) { - inst.toggle_block(&*conn); +pub fn toggle_block(_admin: Admin, conn: DbConn, id: i32) -> Result { + if let Ok(inst) = Instance::get(&*conn, id) { + inst.toggle_block(&*conn)?; } - Redirect::to(uri!(admin_instances: page = _)) + Ok(Redirect::to(uri!(admin_instances: page = _))) } #[get("/admin/users?")] -pub fn admin_users(admin: Admin, conn: DbConn, page: Option, intl: I18n) -> Ructe { +pub fn admin_users(admin: Admin, conn: DbConn, page: Option, intl: I18n) -> Result { let page = page.unwrap_or_default(); - render!(instance::users( + Ok(render!(instance::users( &(&*conn, &intl.catalog, Some(admin.0)), - User::get_local_page(&*conn, page.limits()), + User::get_local_page(&*conn, page.limits())?, page.0, - Page::total(User::count_local(&*conn) as i32) - )) + Page::total(User::count_local(&*conn)? as i32) + ))) } #[post("/admin/users//ban")] -pub fn ban(_admin: Admin, conn: DbConn, id: i32, searcher: Searcher) -> Redirect { - if let Some(u) = User::get(&*conn, id) { - u.delete(&*conn, &searcher); +pub fn ban(_admin: Admin, conn: DbConn, id: i32, searcher: Searcher) -> Result { + if let Ok(u) = User::get(&*conn, id) { + u.delete(&*conn, &searcher)?; } - Redirect::to(uri!(admin_users: page = _)) + Ok(Redirect::to(uri!(admin_users: page = _))) } #[post("/inbox", data = "")] @@ -200,7 +192,7 @@ pub fn shared_inbox(conn: DbConn, data: SignedJson, headers: return Err(status::BadRequest(Some("Invalid signature"))); } - if Instance::is_blocked(&*conn, actor_id) { + if Instance::is_blocked(&*conn, actor_id).map_err(|_| status::BadRequest(Some("Can't tell if instance is blocked")))? { return Ok(String::new()); } let instance = Instance::get_local(&*conn).expect("instance::shared_inbox: local instance not found error"); @@ -214,8 +206,8 @@ pub fn shared_inbox(conn: DbConn, data: SignedJson, headers: } #[get("/nodeinfo")] -pub fn nodeinfo(conn: DbConn) -> Json { - Json(json!({ +pub fn nodeinfo(conn: DbConn) -> Result, ErrorPage> { + Ok(Json(json!({ "version": "2.0", "software": { "name": "Plume", @@ -229,31 +221,31 @@ pub fn nodeinfo(conn: DbConn) -> Json { "openRegistrations": true, "usage": { "users": { - "total": User::count_local(&*conn) + "total": User::count_local(&*conn)? }, - "localPosts": Post::count_local(&*conn), - "localComments": Comment::count_local(&*conn) + "localPosts": Post::count_local(&*conn)?, + "localComments": Comment::count_local(&*conn)? }, "metadata": {} - })) + }))) } #[get("/about")] -pub fn about(user: Option, conn: DbConn, intl: I18n) -> Ructe { - render!(instance::about( +pub fn about(user: Option, conn: DbConn, intl: I18n) -> Result { + Ok(render!(instance::about( &(&*conn, &intl.catalog, user), - Instance::get_local(&*conn).expect("Local instance not found"), - Instance::get_local(&*conn).expect("Local instance not found").main_admin(&*conn), - User::count_local(&*conn), - Post::count_local(&*conn), - Instance::count(&*conn) - 1 - )) + Instance::get_local(&*conn)?, + Instance::get_local(&*conn)?.main_admin(&*conn)?, + User::count_local(&*conn)?, + Post::count_local(&*conn)?, + Instance::count(&*conn)? - 1 + ))) } #[get("/manifest.json")] -pub fn web_manifest(conn: DbConn) -> Json { - let instance = Instance::get_local(&*conn).expect("instance::web_manifest: local instance not found error"); - Json(json!({ +pub fn web_manifest(conn: DbConn) -> Result, ErrorPage> { + let instance = Instance::get_local(&*conn)?; + Ok(Json(json!({ "name": &instance.name, "description": &instance.short_description, "start_url": String::from("/"), @@ -306,5 +298,5 @@ pub fn web_manifest(conn: DbConn) -> Json { "src": "/static/icons/trwnh/feather/plumeFeather.svg" } ] - })) + }))) } diff --git a/src/routes/likes.rs b/src/routes/likes.rs index 2c49afc9..3f24952f 100644 --- a/src/routes/likes.rs +++ b/src/routes/likes.rs @@ -11,27 +11,28 @@ use plume_models::{ users::User }; use Worker; +use routes::errors::ErrorPage; #[post("/~///like")] -pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Option { +pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Result { let b = Blog::find_by_fqn(&*conn, &blog)?; let post = Post::find_by_slug(&*conn, &slug, b.id)?; - if !user.has_liked(&*conn, &post) { - let like = likes::Like::insert(&*conn, likes::NewLike::new(&post ,&user)); - like.notify(&*conn); + if !user.has_liked(&*conn, &post)? { + let like = likes::Like::insert(&*conn, likes::NewLike::new(&post ,&user))?; + like.notify(&*conn)?; - let dest = User::one_by_instance(&*conn); - let act = like.to_activity(&*conn); + let dest = User::one_by_instance(&*conn)?; + let act = like.to_activity(&*conn)?; worker.execute(move || broadcast(&user, act, dest)); } else { - let like = likes::Like::find_by_user_on_post(&*conn, user.id, post.id).expect("likes::create: like exist but not found error"); - let delete_act = like.delete(&*conn); - let dest = User::one_by_instance(&*conn); + let like = likes::Like::find_by_user_on_post(&*conn, user.id, post.id)?; + let delete_act = like.delete(&*conn)?; + let dest = User::one_by_instance(&*conn)?; worker.execute(move || broadcast(&user, delete_act, dest)); } - Some(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _))) + Ok(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _))) } #[post("/~///like", rank = 2)] diff --git a/src/routes/medias.rs b/src/routes/medias.rs index 79703fd2..58441437 100644 --- a/src/routes/medias.rs +++ b/src/routes/medias.rs @@ -5,14 +5,15 @@ use rocket_i18n::I18n; use std::fs; use plume_models::{db_conn::DbConn, medias::*, users::User}; use template_utils::Ructe; +use routes::errors::ErrorPage; #[get("/medias")] -pub fn list(user: User, conn: DbConn, intl: I18n) -> Ructe { - let medias = Media::for_user(&*conn, user.id); - render!(medias::index( +pub fn list(user: User, conn: DbConn, intl: I18n) -> Result { + let medias = Media::for_user(&*conn, user.id)?; + Ok(render!(medias::index( &(&*conn, &intl.catalog, Some(user)), medias - )) + ))) } #[get("/medias/new")] @@ -39,69 +40,65 @@ pub fn upload(user: User, data: Data, ct: &ContentType, conn: DbConn) -> Result< let dest = format!("static/media/{}.{}", GUID::rand().to_string(), ext); match fields[&"file".to_string()][0].data { - SavedData::Bytes(ref bytes) => fs::write(&dest, bytes).expect("media::upload: Couldn't save upload"), - SavedData::File(ref path, _) => {fs::copy(path, &dest).expect("media::upload: Couldn't copy upload");}, + SavedData::Bytes(ref bytes) => fs::write(&dest, bytes).map_err(|_| status::BadRequest(Some("Couldn't save upload")))?, + SavedData::File(ref path, _) => {fs::copy(path, &dest).map_err(|_| status::BadRequest(Some("Couldn't copy upload")))?;}, _ => { - println!("not a file"); return Ok(Redirect::to(uri!(new))); } } - let has_cw = !read(&fields[&"cw".to_string()][0].data).is_empty(); + let has_cw = !read(&fields[&"cw".to_string()][0].data).map(|cw| cw.is_empty()).unwrap_or(false); let media = Media::insert(&*conn, NewMedia { file_path: dest, - alt_text: read(&fields[&"alt".to_string()][0].data), + alt_text: read(&fields[&"alt".to_string()][0].data)?, is_remote: false, remote_url: None, sensitive: has_cw, content_warning: if has_cw { - Some(read(&fields[&"cw".to_string()][0].data)) + Some(read(&fields[&"cw".to_string()][0].data)?) } else { None }, owner_id: user.id - }); - println!("ok"); + }).map_err(|_| status::BadRequest(Some("Error while saving media")))?; Ok(Redirect::to(uri!(details: id = media.id))) }, SaveResult::Partial(_, _) | SaveResult::Error(_) => { - println!("partial err"); Ok(Redirect::to(uri!(new))) } } } else { - println!("not form data"); Ok(Redirect::to(uri!(new))) } } -fn read(data: &SavedData) -> String { +fn read(data: &SavedData) -> Result> { if let SavedData::Text(s) = data { - s.clone() + Ok(s.clone()) } else { - panic!("Field is not a string") + Err(status::BadRequest(Some("Error while reading data"))) } } #[get("/medias/")] -pub fn details(id: i32, user: User, conn: DbConn, intl: I18n) -> Ructe { - let media = Media::get(&*conn, id).expect("Media::details: media not found"); - render!(medias::details( +pub fn details(id: i32, user: User, conn: DbConn, intl: I18n) -> Result { + let media = Media::get(&*conn, id)?; + Ok(render!(medias::details( &(&*conn, &intl.catalog, Some(user)), media - )) + ))) } #[post("/medias//delete")] -pub fn delete(id: i32, _user: User, conn: DbConn) -> Option { +pub fn delete(id: i32, _user: User, conn: DbConn) -> Result { let media = Media::get(&*conn, id)?; - media.delete(&*conn); - Some(Redirect::to(uri!(list))) + media.delete(&*conn)?; + Ok(Redirect::to(uri!(list))) } #[post("/medias//avatar")] -pub fn set_avatar(id: i32, user: User, conn: DbConn) -> Option { +pub fn set_avatar(id: i32, user: User, conn: DbConn) -> Result { let media = Media::get(&*conn, id)?; - user.set_avatar(&*conn, media.id); - Some(Redirect::to(uri!(details: id = id))) + user.set_avatar(&*conn, media.id)?; + Ok(Redirect::to(uri!(details: id = id))) } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 5f5bdd90..a6ff1917 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -60,7 +60,7 @@ pub fn post_to_atom(post: Post, conn: &Connection) -> Entry { .src(post.ap_url.clone()) .content_type("html".to_string()) .build().expect("Atom feed: content error")) - .authors(post.get_authors(&*conn) + .authors(post.get_authors(&*conn).expect("Atom feed: author error") .into_iter() .map(|a| PersonBuilder::default() .name(a.display_name) diff --git a/src/routes/notifications.rs b/src/routes/notifications.rs index cad67d8f..4d9927a3 100644 --- a/src/routes/notifications.rs +++ b/src/routes/notifications.rs @@ -3,18 +3,18 @@ use rocket_i18n::I18n; use plume_common::utils; use plume_models::{db_conn::DbConn, notifications::Notification, users::User}; -use routes::Page; +use routes::{Page, errors::ErrorPage}; use template_utils::Ructe; #[get("/notifications?")] -pub fn notifications(conn: DbConn, user: User, page: Option, intl: I18n) -> Ructe { +pub fn notifications(conn: DbConn, user: User, page: Option, intl: I18n) -> Result { let page = page.unwrap_or_default(); - render!(notifications::index( + Ok(render!(notifications::index( &(&*conn, &intl.catalog, Some(user.clone())), - Notification::page_for_user(&*conn, &user, page.limits()), + Notification::page_for_user(&*conn, &user, page.limits())?, page.0, - Page::total(Notification::count_for_user(&*conn, &user) as i32) - )) + Page::total(Notification::count_for_user(&*conn, &user)? as i32) + ))) } #[get("/notifications?", rank = 2)] diff --git a/src/routes/posts.rs b/src/routes/posts.rs index 0bbb071b..f8538830 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -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("/~//?", rank = 4)] -pub fn details(blog: String, slug: String, conn: DbConn, user: Option, responding_to: Option, intl: I18n) -> Result { - 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, responding_to: Option, intl: I18n) -> Result { + 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, 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, res None } }).collect::>().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, res #[get("/~//", rank = 3)] pub fn activity_details(blog: String, slug: String, conn: DbConn, _ap: ApRequest) -> Result, Option> { - 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 { } #[get("/~//new", rank = 1)] -pub fn new(blog: String, user: User, conn: DbConn, intl: I18n) -> Option { +pub fn new(blog: String, user: User, conn: DbConn, intl: I18n) -> Result { 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 } #[get("/~///edit")] -pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) -> Option { +pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) -> Result { 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::>() @@ -173,9 +172,9 @@ pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) -> #[post("/~///edit", data = "")] pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: LenientForm, worker: Worker, intl: I18n, searcher: Searcher) - -> Result> { - 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 { + 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::>().into_iter().map(|t| Tag::build_activity(&conn, t)).collect::>(); - post.update_tags(&conn, tags); + .collect::>().into_iter().filter_map(|t| Tag::build_activity(&conn, t).ok()).collect::>(); + post.update_tags(&conn, tags).expect("post::update: tags error"); let hashtags = hashtags.into_iter().map(|h| h.to_camel_case()).collect::>() - .into_iter().map(|t| Tag::build_activity(&conn, t)).collect::>(); - post.update_hashtags(&conn, hashtags); + .into_iter().filter_map(|t| Tag::build_activity(&conn, t).ok()).collect::>(); + 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("/~//new", data = "")] -pub fn create(blog_name: String, form: LenientForm, user: User, conn: DbConn, worker: Worker, intl: I18n, searcher: Searcher) -> Result> { - let blog = Blog::find_by_fqn(&*conn, &blog_name).ok_or(None)?; +pub fn create(blog_name: String, form: LenientForm, user: User, conn: DbConn, worker: Worker, intl: I18n, searcher: Searcher) -> Result> { + 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, 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, 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, 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, user: User, con } #[post("/~///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 { 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 = _))) } } diff --git a/src/routes/reshares.rs b/src/routes/reshares.rs index 9c3dda73..a0b1df74 100644 --- a/src/routes/reshares.rs +++ b/src/routes/reshares.rs @@ -10,29 +10,29 @@ use plume_models::{ reshares::*, users::User }; +use routes::errors::ErrorPage; use Worker; #[post("/~///reshare")] -pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Option { +pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Result { let b = Blog::find_by_fqn(&*conn, &blog)?; let post = Post::find_by_slug(&*conn, &slug, b.id)?; - if !user.has_reshared(&*conn, &post) { - let reshare = Reshare::insert(&*conn, NewReshare::new(&post, &user)); - reshare.notify(&*conn); + if !user.has_reshared(&*conn, &post)? { + let reshare = Reshare::insert(&*conn, NewReshare::new(&post, &user))?; + reshare.notify(&*conn)?; - let dest = User::one_by_instance(&*conn); - let act = reshare.to_activity(&*conn); + let dest = User::one_by_instance(&*conn)?; + let act = reshare.to_activity(&*conn)?; worker.execute(move || broadcast(&user, act, dest)); } else { - let reshare = Reshare::find_by_user_on_post(&*conn, user.id, post.id) - .expect("reshares::create: reshare exist but not found error"); - let delete_act = reshare.delete(&*conn); - let dest = User::one_by_instance(&*conn); + let reshare = Reshare::find_by_user_on_post(&*conn, user.id, post.id)?; + let delete_act = reshare.delete(&*conn)?; + let dest = User::one_by_instance(&*conn)?; worker.execute(move || broadcast(&user, delete_act, dest)); } - Some(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _))) + Ok(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _))) } #[post("/~///reshare", rank=1)] diff --git a/src/routes/session.rs b/src/routes/session.rs index 86a7346f..6352767b 100644 --- a/src/routes/session.rs +++ b/src/routes/session.rs @@ -14,6 +14,7 @@ use plume_models::{ users::{User, AUTH_COOKIE} }; + #[get("/login?")] pub fn new(user: Option, conn: DbConn, m: Option, intl: I18n) -> Ructe { render!(session::login( @@ -35,30 +36,34 @@ pub struct LoginForm { #[post("/login", data = "")] pub fn create(conn: DbConn, form: LenientForm, flash: Option, mut cookies: Cookies, intl: I18n) -> Result { let user = User::find_by_email(&*conn, &form.email_or_name) - .or_else(|| User::find_local(&*conn, &form.email_or_name)); + .or_else(|_| User::find_local(&*conn, &form.email_or_name)); let mut errors = match form.validate() { Ok(_) => ValidationErrors::new(), Err(e) => e }; - if let Some(user) = user.clone() { + let user_id = if let Ok(user) = user { if !user.auth(&form.password) { let mut err = ValidationError::new("invalid_login"); err.message = Some(Cow::from("Invalid username or password")); - errors.add("email_or_name", err) + errors.add("email_or_name", err); + user.id.to_string() + } else { + String::new() } } else { // Fake password verification, only to avoid different login times // that could be used to see if an email adress is registered or not - User::get(&*conn, 1).map(|u| u.auth(&form.password)); + User::get(&*conn, 1).map(|u| u.auth(&form.password)).expect("No user is registered"); let mut err = ValidationError::new("invalid_login"); err.message = Some(Cow::from("Invalid username or password")); - errors.add("email_or_name", err) - } + errors.add("email_or_name", err); + String::new() + }; if errors.is_empty() { - cookies.add_private(Cookie::build(AUTH_COOKIE, user.unwrap().id.to_string()) + cookies.add_private(Cookie::build(AUTH_COOKIE, user_id) .same_site(SameSite::Lax) .finish()); diff --git a/src/routes/tags.rs b/src/routes/tags.rs index c3d8b31b..477a0b6e 100644 --- a/src/routes/tags.rs +++ b/src/routes/tags.rs @@ -5,18 +5,18 @@ use plume_models::{ posts::Post, users::User, }; -use routes::Page; +use routes::{Page, errors::ErrorPage}; use template_utils::Ructe; #[get("/tag/?")] -pub fn tag(user: Option, conn: DbConn, name: String, page: Option, intl: I18n) -> Ructe { +pub fn tag(user: Option, conn: DbConn, name: String, page: Option, intl: I18n) -> Result { let page = page.unwrap_or_default(); - let posts = Post::list_by_tag(&*conn, name.clone(), page.limits()); - render!(tags::index( + let posts = Post::list_by_tag(&*conn, name.clone(), page.limits())?; + Ok(render!(tags::index( &(&*conn, &intl.catalog, user), name.clone(), posts, page.0, - Page::total(Post::count_for_tag(&*conn, name) as i32) - )) + Page::total(Post::count_for_tag(&*conn, name)? as i32) + ))) } diff --git a/src/routes/user.rs b/src/routes/user.rs index 2abdaa60..a6a91de0 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -7,6 +7,7 @@ use rocket::{ }; use rocket_i18n::I18n; use serde_json; +use std::{borrow::Cow, collections::HashMap}; use validator::{Validate, ValidationError, ValidationErrors}; use inbox::{Inbox, SignedJson}; @@ -18,10 +19,11 @@ use plume_common::activity_pub::{ }; use plume_common::utils; use plume_models::{ + Error, blogs::Blog, db_conn::DbConn, follows, headers::Headers, instance::Instance, posts::{LicensedArticle, Post}, reshares::Reshare, users::*, }; -use routes::Page; +use routes::{Page, errors::ErrorPage}; use template_utils::Ructe; use Worker; use Searcher; @@ -45,24 +47,24 @@ pub fn details( update_conn: DbConn, intl: I18n, searcher: Searcher, -) -> Result { - let user = User::find_by_fqn(&*conn, &name).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, account.clone()))))?; - let recents = Post::get_recents_for_author(&*conn, &user, 6); - let reshares = Reshare::get_recents_for_author(&*conn, &user, 6); +) -> Result { + let user = User::find_by_fqn(&*conn, &name)?; + let recents = Post::get_recents_for_author(&*conn, &user, 6)?; + let reshares = Reshare::get_recents_for_author(&*conn, &user, 6)?; - if !user.get_instance(&*conn).local { + if !user.get_instance(&*conn)?.local { // Fetch new articles let user_clone = user.clone(); let searcher = searcher.clone(); worker.execute(move || { - for create_act in user_clone.fetch_outbox::() { + for create_act in user_clone.fetch_outbox::().expect("Remote user: outbox couldn't be fetched") { match create_act.create_props.object_object::() { Ok(article) => { Post::from_activity( &(&*fetch_articles_conn, &searcher), article, user_clone.clone().into_id(), - ); + ).expect("Article from remote user couldn't be saved"); println!("Fetched article from remote user"); } Err(e) => { @@ -75,10 +77,10 @@ pub fn details( // Fetch followers let user_clone = user.clone(); worker.execute(move || { - for user_id in user_clone.fetch_followers_ids() { + for user_id in user_clone.fetch_followers_ids().expect("Remote user: fetching followers error") { let follower = User::find_by_ap_url(&*fetch_followers_conn, &user_id) - .unwrap_or_else(|| { + .unwrap_or_else(|_| { User::fetch_from_url(&*fetch_followers_conn, &user_id) .expect("user::details: Couldn't fetch follower") }); @@ -89,7 +91,7 @@ pub fn details( following_id: user_clone.id, ap_url: format!("{}/follow/{}", follower.ap_url, user_clone.ap_url), }, - ); + ).expect("Couldn't save follower for remote user"); } }); @@ -97,7 +99,7 @@ pub fn details( let user_clone = user.clone(); if user.needs_update() { worker.execute(move || { - user_clone.refetch(&*update_conn); + user_clone.refetch(&*update_conn).expect("Couldn't update user info"); }); } } @@ -105,22 +107,22 @@ pub fn details( Ok(render!(users::details( &(&*conn, &intl.catalog, account.clone()), user.clone(), - account.map(|x| x.is_following(&*conn, user.id)).unwrap_or(false), - user.instance_id != Instance::local_id(&*conn), - user.get_instance(&*conn).public_domain, + account.and_then(|x| x.is_following(&*conn, user.id).ok()).unwrap_or(false), + user.instance_id != Instance::get_local(&*conn)?.id, + user.get_instance(&*conn)?.public_domain, recents, - reshares.into_iter().map(|r| r.get_post(&*conn).expect("user::details: Reshared post error")).collect() + reshares.into_iter().filter_map(|r| r.get_post(&*conn).ok()).collect() ))) } #[get("/dashboard")] -pub fn dashboard(user: User, conn: DbConn, intl: I18n) -> Ructe { - let blogs = Blog::find_for_author(&*conn, &user); - render!(users::dashboard( +pub fn dashboard(user: User, conn: DbConn, intl: I18n) -> Result { + let blogs = Blog::find_for_author(&*conn, &user)?; + Ok(render!(users::dashboard( &(&*conn, &intl.catalog, Some(user.clone())), blogs, - Post::drafts_by_author(&*conn, &user) - )) + Post::drafts_by_author(&*conn, &user)? + ))) } #[get("/dashboard", rank = 2)] @@ -132,10 +134,10 @@ pub fn dashboard_auth(i18n: I18n) -> Flash { } #[post("/@//follow")] -pub fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Option { +pub fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Result { let target = User::find_by_fqn(&*conn, &name)?; - if let Some(follow) = follows::Follow::find(&*conn, user.id, target.id) { - let delete_act = follow.delete(&*conn); + if let Ok(follow) = follows::Follow::find(&*conn, user.id, target.id) { + let delete_act = follow.delete(&*conn)?; worker.execute(move || { broadcast(&user, delete_act, vec![target]) }); @@ -147,13 +149,13 @@ pub fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Option< following_id: target.id, ap_url: format!("{}/follow/{}", user.ap_url, target.ap_url), }, - ); - f.notify(&*conn); + )?; + f.notify(&*conn)?; - let act = f.to_activity(&*conn); + let act = f.to_activity(&*conn)?; worker.execute(move || broadcast(&user, act, vec![target])); } - Some(Redirect::to(uri!(details: name = name))) + Ok(Redirect::to(uri!(details: name = name))) } #[post("/@//follow", rank = 2)] @@ -165,18 +167,18 @@ pub fn follow_auth(name: String, i18n: I18n) -> Flash { } #[get("/@//followers?", rank = 2)] -pub fn followers(name: String, conn: DbConn, account: Option, page: Option, intl: I18n) -> Result { +pub fn followers(name: String, conn: DbConn, account: Option, page: Option, intl: I18n) -> Result { let page = page.unwrap_or_default(); - let user = User::find_by_fqn(&*conn, &name).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, account.clone()))))?; - let followers_count = user.count_followers(&*conn); + let user = User::find_by_fqn(&*conn, &name)?; + let followers_count = user.count_followers(&*conn)?; Ok(render!(users::followers( &(&*conn, &intl.catalog, account.clone()), user.clone(), - account.map(|x| x.is_following(&*conn, user.id)).unwrap_or(false), - user.instance_id != Instance::local_id(&*conn), - user.get_instance(&*conn).public_domain, - user.get_followers_page(&*conn, page.limits()), + account.and_then(|x| x.is_following(&*conn, user.id).ok()).unwrap_or(false), + user.instance_id != Instance::get_local(&*conn)?.id, + user.get_instance(&*conn)?.public_domain, + user.get_followers_page(&*conn, page.limits())?, page.0, Page::total(followers_count as i32) ))) @@ -188,24 +190,24 @@ pub fn activity_details( conn: DbConn, _ap: ApRequest, ) -> Option> { - let user = User::find_local(&*conn, &name)?; - Some(ActivityStream::new(user.to_activity(&*conn))) + let user = User::find_local(&*conn, &name).ok()?; + Some(ActivityStream::new(user.to_activity(&*conn).ok()?)) } #[get("/users/new")] -pub fn new(user: Option, conn: DbConn, intl: I18n) -> Ructe { - render!(users::new( +pub fn new(user: Option, conn: DbConn, intl: I18n) -> Result { + Ok(render!(users::new( &(&*conn, &intl.catalog, user), - Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true), + Instance::get_local(&*conn)?.open_registrations, &NewUserForm::default(), ValidationErrors::default() - )) + ))) } #[get("/@//edit")] -pub fn edit(name: String, user: User, conn: DbConn, intl: I18n) -> Option { +pub fn edit(name: String, user: User, conn: DbConn, intl: I18n) -> Result { if user.username == name && !name.contains('@') { - Some(render!(users::edit( + Ok(render!(users::edit( &(&*conn, &intl.catalog, Some(user.clone())), UpdateUserForm { display_name: user.display_name.clone(), @@ -215,7 +217,7 @@ pub fn edit(name: String, user: User, conn: DbConn, intl: I18n) -> Option ValidationErrors::default() ))) } else { - None + Err(Error::Unauthorized)? } } @@ -235,29 +237,29 @@ pub struct UpdateUserForm { } #[put("/@/<_name>/edit", data = "")] -pub fn update(_name: String, conn: DbConn, user: User, form: LenientForm) -> Redirect { +pub fn update(_name: String, conn: DbConn, user: User, form: LenientForm) -> Result { user.update( &*conn, if !form.display_name.is_empty() { form.display_name.clone() } else { user.display_name.clone() }, if !form.email.is_empty() { form.email.clone() } else { user.email.clone().unwrap_or_default() }, if !form.summary.is_empty() { form.summary.clone() } else { user.summary.to_string() }, - ); - Redirect::to(uri!(me)) + )?; + Ok(Redirect::to(uri!(me))) } #[post("/@//delete")] -pub fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, searcher: Searcher) -> Option { +pub fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, searcher: Searcher) -> Result { let account = User::find_by_fqn(&*conn, &name)?; if user.id == account.id { - account.delete(&*conn, &searcher); + account.delete(&*conn, &searcher)?; - if let Some(cookie) = cookies.get_private(AUTH_COOKIE) { - cookies.remove_private(cookie); - } + if let Some(cookie) = cookies.get_private(AUTH_COOKIE) { + cookies.remove_private(cookie); + } - Some(Redirect::to(uri!(super::instance::index))) + Ok(Redirect::to(uri!(super::instance::index))) } else { - Some(Redirect::to(uri!(edit: name = name))) + Ok(Redirect::to(uri!(edit: name = name))) } } @@ -307,6 +309,16 @@ pub fn validate_username(username: &str) -> Result<(), ValidationError> { } } +fn to_validation(_: Error) -> ValidationErrors { + let mut errors = ValidationErrors::new(); + errors.add("", ValidationError { + code: Cow::from("server_error"), + message: Some(Cow::from("An unknown error occured")), + params: HashMap::new() + }); + errors +} + #[post("/users/new", data = "")] pub fn create(conn: DbConn, form: LenientForm, intl: I18n) -> Result { if !Instance::get_local(&*conn) @@ -320,7 +332,7 @@ pub fn create(conn: DbConn, form: LenientForm, intl: I18n) -> Resul form.username = form.username.trim().to_owned(); form.email = form.email.trim().to_owned(); form.validate() - .map(|_| { + .and_then(|_| { NewUser::new_local( &*conn, form.username.to_string(), @@ -328,9 +340,9 @@ pub fn create(conn: DbConn, form: LenientForm, intl: I18n) -> Resul false, "", form.email.to_string(), - User::hash_pass(&form.password), - ).update_boxes(&*conn); - Redirect::to(uri!(super::session::new: m = _)) + User::hash_pass(&form.password).map_err(to_validation)?, + ).and_then(|u| u.update_boxes(&*conn)).map_err(to_validation)?; + Ok(Redirect::to(uri!(super::session::new: m = _))) }) .map_err(|err| { render!(users::new( @@ -344,8 +356,8 @@ pub fn create(conn: DbConn, form: LenientForm, intl: I18n) -> Resul #[get("/@//outbox")] pub fn outbox(name: String, conn: DbConn) -> Option> { - let user = User::find_local(&*conn, &name)?; - Some(user.outbox(&*conn)) + let user = User::find_local(&*conn, &name).ok()?; + user.outbox(&*conn).ok() } #[post("/@//inbox", data = "")] @@ -356,7 +368,7 @@ pub fn inbox( headers: Headers, searcher: Searcher, ) -> Result>> { - let user = User::find_local(&*conn, &name).ok_or(None)?; + let user = User::find_local(&*conn, &name).map_err(|_| None)?; let act = data.1.into_inner(); let activity = act.clone(); @@ -378,7 +390,7 @@ pub fn inbox( return Err(Some(status::BadRequest(Some("Invalid signature")))); } - if Instance::is_blocked(&*conn, actor_id) { + if Instance::is_blocked(&*conn, actor_id).map_err(|_| None)? { return Ok(String::new()); } Ok(match user.received(&*conn, &searcher, act) { @@ -396,36 +408,33 @@ pub fn ap_followers( conn: DbConn, _ap: ApRequest, ) -> Option> { - let user = User::find_local(&*conn, &name)?; + let user = User::find_local(&*conn, &name).ok()?; let followers = user - .get_followers(&*conn) + .get_followers(&*conn).ok()? .into_iter() .map(|f| Id::new(f.ap_url)) .collect::>(); let mut coll = OrderedCollection::default(); coll.object_props - .set_id_string(user.followers_endpoint) - .expect("user::ap_followers: id error"); + .set_id_string(user.followers_endpoint).ok()?; coll.collection_props - .set_total_items_u64(followers.len() as u64) - .expect("user::ap_followers: totalItems error"); + .set_total_items_u64(followers.len() as u64).ok()?; coll.collection_props - .set_items_link_vec(followers) - .expect("user::ap_followers items error"); + .set_items_link_vec(followers).ok()?; Some(ActivityStream::new(coll)) } #[get("/@//atom.xml")] pub fn atom_feed(name: String, conn: DbConn) -> Option> { - let author = User::find_by_fqn(&*conn, &name)?; + let author = User::find_by_fqn(&*conn, &name).ok()?; let feed = FeedBuilder::default() .title(author.display_name.clone()) .id(Instance::get_local(&*conn) .unwrap() .compute_box("~", &name, "atom.xml")) .entries( - Post::get_recents_for_author(&*conn, &author, 15) + Post::get_recents_for_author(&*conn, &author, 15).ok()? .into_iter() .map(|p| super::post_to_atom(p, &*conn)) .collect::>(), diff --git a/src/routes/well_known.rs b/src/routes/well_known.rs index 88ec9ce4..3f41c909 100644 --- a/src/routes/well_known.rs +++ b/src/routes/well_known.rs @@ -35,13 +35,11 @@ impl Resolver for WebfingerResolver { } fn find(acct: String, conn: DbConn) -> Result { - match User::find_local(&*conn, &acct) { - Some(usr) => Ok(usr.webfinger(&*conn)), - None => match Blog::find_local(&*conn, &acct) { - Some(blog) => Ok(blog.webfinger(&*conn)), - None => Err(ResolverError::NotFound) - } - } + User::find_local(&*conn, &acct) + .and_then(|usr| usr.webfinger(&*conn)) + .or_else(|_| Blog::find_local(&*conn, &acct) + .and_then(|blog| blog.webfinger(&*conn)) + .or(Err(ResolverError::NotFound))) } } diff --git a/templates/medias/details.rs.html b/templates/medias/details.rs.html index 2c084555..033d3b86 100644 --- a/templates/medias/details.rs.html +++ b/templates/medias/details.rs.html @@ -1,4 +1,5 @@ @use plume_models::medias::{Media, MediaCategory}; +@use plume_models::safe_string::SafeString; @use templates::base; @use template_utils::*; @use routes::*; @@ -13,7 +14,7 @@
- @Html(media.html(ctx.0)) + @Html(media.html(ctx.0).unwrap_or(SafeString::new("")))
@media.alt_text
@@ -21,7 +22,7 @@ @i18n!(ctx.1, "Markdown syntax") @i18n!(ctx.1, "Copy it into your articles, to insert this media:")

- @media.markdown(ctx.0) + @media.markdown(ctx.0).unwrap_or(SafeString::new(""))
@if media.category() == MediaCategory::Image { diff --git a/templates/medias/index.rs.html b/templates/medias/index.rs.html index fb7d9c30..b1e5a14a 100644 --- a/templates/medias/index.rs.html +++ b/templates/medias/index.rs.html @@ -1,4 +1,5 @@ @use plume_models::medias::Media; +@use plume_models::safe_string::SafeString; @use templates::base; @use template_utils::*; @use routes::*; @@ -18,7 +19,7 @@
@for media in medias {
- @Html(media.preview_html(ctx.0)) + @Html(media.preview_html(ctx.0).unwrap_or(SafeString::new("")))

@media.alt_text

diff --git a/templates/notifications/index.rs.html b/templates/notifications/index.rs.html index 87e13f06..e1ed75f6 100644 --- a/templates/notifications/index.rs.html +++ b/templates/notifications/index.rs.html @@ -15,14 +15,14 @@

@if let Some(url) = notification.get_url(ctx.0) { - @i18n!(ctx.1, notification.get_message(); notification.get_actor(ctx.0).name(ctx.0)) + @i18n!(ctx.1, notification.get_message(); notification.get_actor(ctx.0).unwrap().name(ctx.0)) } else { - @i18n!(ctx.1, notification.get_message(); notification.get_actor(ctx.0).name(ctx.0)) + @i18n!(ctx.1, notification.get_message(); notification.get_actor(ctx.0).unwrap().name(ctx.0)) }

@if let Some(post) = notification.get_post(ctx.0) { -

@post.title

+

@post.title

}

@notification.creation_date.format("%B %e, %H:%M")

diff --git a/templates/partials/comment.rs.html b/templates/partials/comment.rs.html index 57d205da..646e9506 100644 --- a/templates/partials/comment.rs.html +++ b/templates/partials/comment.rs.html @@ -5,7 +5,7 @@ @(ctx: BaseContext, comment_tree: &CommentTree, in_reply_to: Option<&str>, blog: &str, slug: &str) @if let Some(ref comm) = Some(&comment_tree.comment) { -@if let Some(author) = Some(comm.get_author(ctx.0)) { +@if let Some(author) = comm.get_author(ctx.0).ok() {
@avatar(ctx.0, &author, Size::Small, true, ctx.1) diff --git a/templates/partials/post_card.rs.html b/templates/partials/post_card.rs.html index 94dc7193..3301cb30 100644 --- a/templates/partials/post_card.rs.html +++ b/templates/partials/post_card.rs.html @@ -9,7 +9,7 @@
}

- + @article.title

@@ -19,13 +19,13 @@

@Html(i18n!(ctx.1, "By {0}"; format!( "{}", - uri!(user::details: name = article.get_authors(ctx.0)[0].get_fqn(ctx.0)), - escape(&article.get_authors(ctx.0)[0].name(ctx.0)) + uri!(user::details: name = article.get_authors(ctx.0).unwrap_or_default()[0].get_fqn(ctx.0)), + escape(&article.get_authors(ctx.0).unwrap_or_default()[0].name(ctx.0)) ))) @if article.published { ⋅ @article.creation_date.format("%B %e, %Y") } - ⋅ @article.get_blog(ctx.0).title + ⋅ @article.get_blog(ctx.0).unwrap().title @if !article.published { ⋅ @i18n!(ctx.1, "Draft") }