diff --git a/plume-cli/src/instance.rs b/plume-cli/src/instance.rs index 0bd45db..38b1af4 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 67cbd58..7083e55 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 2820b14..9b3155e 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 01836af..0f5b3d3 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 e569d2b..78059dd 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 94368ce..9d00a2e 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 b3f4577..b19bc12 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 d388393..aae3af9 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 64e82c9..20b99b1 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 dc6a9c6..0a8e19a 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 9083eec..89c5d28 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 abcf961..9020482 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 9bdd4c9..05378a6 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 c7b14a5..9c194a9 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 6d01ab1..5936d03 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 240ef38..d3732d4 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 0480e1e..f501657 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 ccf6516..9ec2e74 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 c18ae6e..e126d38 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 146cc6d..e7c566e 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 47ff7da..b8db554 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 57cb98b..8ef1089 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 3ee771f..5b656fd 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 30c08fc..14ce88a 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 0ae53ad..0e4c911 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 986d9e3..91c2b57 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 ababc53..1a2f1d2 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 defdd8b..f4417ea 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 670aba1..b5a9677 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 a160e88..8e98ebe 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 bb88011..f5fa803 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 356cf72..34bccd3 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 f450e93..d91bd9f 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 68f05b9..42554cb 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 2c49afc..3f24952 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 79703fd..5844143 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 5f5bdd9..a6ff191 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 cad67d8..4d9927a 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 0bbb071..f853883 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 9c3dda7..a0b1df7 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 86a7346..6352767 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 c3d8b31..477a0b6 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 2abdaa6..a6a91de 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 88ec9ce..3f41c90 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 2c08455..033d3b8 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 fb7d9c3..b1e5a14 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 87e13f0..e1ed75f 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 57d205d..646e950 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 94dc719..3301cb3 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") }