Avoid panics (#392)
- Use `Result` as much as possible - Display errors instead of panicking TODO (maybe in another PR? this one is already quite big): - Find a way to merge Ructe/ErrorPage types, so that we can have routes returning `Result<X, ErrorPage>` instead of panicking when we have an `Error` - Display more details about the error, to make it easier to debug (sorry, this isn't going to be fun to review, the diff is huge, but it is always the same changes)
This commit is contained in:
		
							parent
							
								
									4059a840be
								
							
						
					
					
						commit
						80a4dae8bd
					
				| @ -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"); | ||||
| } | ||||
|  | ||||
| @ -94,7 +94,7 @@ fn refill<'a>(args: &ArgMatches<'a>, conn: &Connection, searcher: Option<Searche | ||||
|     let len = posts.len(); | ||||
|     for (i,post) in posts.iter().enumerate() { | ||||
|         println!("Importing {}/{} : {}", i+1, len, post.title); | ||||
|         searcher.update_document(conn, &post); | ||||
|         searcher.update_document(conn, &post).expect("Couldn't import post"); | ||||
|     } | ||||
|     println!("Commiting result"); | ||||
|     searcher.commit(); | ||||
|  | ||||
| @ -72,6 +72,7 @@ fn new<'a>(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"); | ||||
| } | ||||
|  | ||||
| @ -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<T: Object, C>: Sized { | ||||
|     fn from_activity(conn: &C, obj: T, actor: Id) -> Self; | ||||
|     type Error: From<ApError>; | ||||
| 
 | ||||
|     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::<Id>() | ||||
|                     .expect("FromActivity::try_from_activity: id not found error"), | ||||
|             ); | ||||
|             true | ||||
|         } else { | ||||
|             false | ||||
|         } | ||||
|     fn from_activity(conn: &C, obj: T, actor: Id) -> Result<Self, Self::Error>; | ||||
| 
 | ||||
|     fn try_from_activity(conn: &C, act: Create) -> Result<Self, Self::Error> { | ||||
|         Self::from_activity( | ||||
|             conn, | ||||
|             act.create_props.object_object()?, | ||||
|             act.create_props.actor_link::<Id>()?, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub trait Notify<C> { | ||||
|     fn notify(&self, conn: &C); | ||||
|     type Error; | ||||
| 
 | ||||
|     fn notify(&self, conn: &C) -> Result<(), Self::Error>; | ||||
| } | ||||
| 
 | ||||
| pub trait Deletable<C, A> { | ||||
|     fn delete(&self, conn: &C) -> A; | ||||
|     fn delete_id(id: &str, actor_id: &str, conn: &C); | ||||
|     type Error; | ||||
| 
 | ||||
|     fn delete(&self, conn: &C) -> Result<A, Self::Error>; | ||||
|     fn delete_id(id: &str, actor_id: &str, conn: &C) -> Result<A, Self::Error>; | ||||
| } | ||||
| 
 | ||||
| pub trait WithInbox { | ||||
|  | ||||
| @ -120,7 +120,7 @@ pub fn broadcast<S: sign::Signer, A: Activity, T: inbox::WithInbox + Actor>( | ||||
| 
 | ||||
|     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<S: sign::Signer, A: Activity, T: inbox::WithInbox + Actor>( | ||||
|         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 { | ||||
|  | ||||
| @ -105,7 +105,7 @@ pub fn headers() -> HeaderMap { | ||||
|     headers | ||||
| } | ||||
| 
 | ||||
| pub fn signature<S: Signer>(signer: &S, headers: &HeaderMap) -> HeaderValue { | ||||
| pub fn signature<S: Signer>(signer: &S, headers: &HeaderMap) -> Result<HeaderValue, ()> { | ||||
|     let signed_string = headers | ||||
|         .iter() | ||||
|         .map(|(h, v)| { | ||||
| @ -125,7 +125,7 @@ pub fn signature<S: Signer>(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<S: Signer>(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(|_| ()) | ||||
| } | ||||
|  | ||||
| @ -22,16 +22,18 @@ pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) { | ||||
| } | ||||
| 
 | ||||
| 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<u8>; | ||||
|     fn sign(&self, to_sign: &str) -> Result<Vec<u8>, Self::Error>; | ||||
|     /// Verify if the signature is valid
 | ||||
|     fn verify(&self, data: &str, signature: &[u8]) -> bool; | ||||
|     fn verify(&self, data: &str, signature: &[u8]) -> Result<bool, Self::Error>; | ||||
| } | ||||
| 
 | ||||
| pub trait Signable { | ||||
|     fn sign<T>(&mut self, creator: &T) -> &mut Self | ||||
|     fn sign<T>(&mut self, creator: &T) -> Result<&mut Self, ()> | ||||
|     where | ||||
|         T: Signer; | ||||
|     fn verify<T>(self, creator: &T) -> bool | ||||
| @ -45,7 +47,7 @@ pub trait Signable { | ||||
| } | ||||
| 
 | ||||
| impl Signable for serde_json::Value { | ||||
|     fn sign<T: Signer>(&mut self, creator: &T) -> &mut serde_json::Value { | ||||
|     fn sign<T: Signer>(&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<T: Signer>(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<S: Signer + ::std::fmt::Debug>( | ||||
|         .collect::<Vec<_>>() | ||||
|         .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") { | ||||
|  | ||||
| @ -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<ApiToken, ()> { | ||||
|     /// 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<ApiToken, TokenError> { | ||||
|         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::<DbConn>().expect("Couldn't connect to DB"); | ||||
|             if let Some(token) = ApiToken::find_by_value(&*conn, val) { | ||||
|             let conn = request.guard::<DbConn>().map_failure(|_| (Status::InternalServerError, TokenError::DbError))?; | ||||
|             if let Ok(token) = ApiToken::find_by_value(&*conn, val) { | ||||
|                 return Outcome::Success(token); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @ -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<Connection> for App { | ||||
|     type Data = AppEndpoint; | ||||
| 
 | ||||
|     fn get(_conn: &Connection, _id: i32) -> Result<AppEndpoint, Error> { | ||||
|     fn get(_conn: &Connection, _id: i32) -> ApiResult<AppEndpoint> { | ||||
|         unimplemented!() | ||||
|     } | ||||
| 
 | ||||
| @ -39,7 +39,7 @@ impl Provider<Connection> for App { | ||||
|         unimplemented!() | ||||
|     } | ||||
| 
 | ||||
|     fn create(conn: &Connection, data: AppEndpoint) -> Result<AppEndpoint, Error> { | ||||
|     fn create(conn: &Connection, data: AppEndpoint) -> ApiResult<AppEndpoint> { | ||||
|         let client_id = random_hex(); | ||||
| 
 | ||||
|         let client_secret = random_hex(); | ||||
| @ -52,7 +52,7 @@ impl Provider<Connection> 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<Connection> for App { | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     fn update(_conn: &Connection, _id: i32, _new_data: AppEndpoint) -> Result<AppEndpoint, Error> { | ||||
|     fn update(_conn: &Connection, _id: i32, _new_data: AppEndpoint) -> ApiResult<AppEndpoint> { | ||||
|         unimplemented!() | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; | ||||
| 
 | ||||
| use schema::blog_authors; | ||||
| use {Error, Result}; | ||||
| 
 | ||||
| #[derive(Clone, Queryable, Identifiable)] | ||||
| pub struct BlogAuthor { | ||||
|  | ||||
| @ -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<ApSignature, Group>; | ||||
| 
 | ||||
| @ -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> { | ||||
|         Instance::get(conn, self.instance_id) | ||||
|     } | ||||
| 
 | ||||
|     pub fn list_authors(&self, conn: &Connection) -> Vec<User> { | ||||
|     pub fn list_authors(&self, conn: &Connection) -> Result<Vec<User>> { | ||||
|         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::<User>(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<i64> { | ||||
|         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<Blog> { | ||||
|     pub fn find_for_author(conn: &Connection, author: &User) -> Result<Vec<Blog>> { | ||||
|         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::<Blog>(conn) | ||||
|             .expect("Blog::find_for_author: blog loading error") | ||||
|             .map_err(Error::from) | ||||
|     } | ||||
| 
 | ||||
|     pub fn find_local(conn: &Connection, name: &str) -> Option<Blog> { | ||||
|         Blog::find_by_name(conn, name, Instance::local_id(conn)) | ||||
|     pub fn find_local(conn: &Connection, name: &str) -> Result<Blog> { | ||||
|         Blog::find_by_name(conn, name, Instance::get_local(conn)?.id) | ||||
|     } | ||||
| 
 | ||||
|     pub fn find_by_fqn(conn: &Connection, fqn: &str) -> Option<Blog> { | ||||
|         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<Blog> { | ||||
|         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<Blog> { | ||||
|         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<Blog> { | ||||
|         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<Blog> { | ||||
|         let req = Client::new() | ||||
|     fn fetch_from_url(conn: &Connection, url: &str) -> Result<Blog> { | ||||
|         let mut res = Client::new() | ||||
|             .get(url) | ||||
|             .header( | ||||
|                 ACCEPT, | ||||
| @ -164,139 +142,109 @@ impl Blog { | ||||
|                         .into_iter() | ||||
|                         .collect::<Vec<_>>() | ||||
|                         .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<Blog> { | ||||
|         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<CustomGroup> { | ||||
|         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<OrderedCollection> { | ||||
|     pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> { | ||||
|         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<serde_json::Value> { | ||||
|         vec![] | ||||
|     fn get_activities(&self, _conn: &Connection) -> Result<Vec<serde_json::Value>> { | ||||
|         Ok(vec![]) | ||||
|     } | ||||
| 
 | ||||
|     pub fn get_keypair(&self) -> PKey<Private> { | ||||
|     pub fn get_keypair(&self) -> Result<PKey<Private>> { | ||||
|         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<Webfinger> { | ||||
|         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> { | ||||
|         Blog::find_by_ap_url(conn, url).or_else(|| { | ||||
|     pub fn from_url(conn: &Connection, url: &str) -> Result<Blog> { | ||||
|         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<u8> { | ||||
|         let key = self.get_keypair(); | ||||
|     fn sign(&self, to_sign: &str) -> Result<Vec<u8>> { | ||||
|         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<bool> { | ||||
|         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<NewBlog> { | ||||
|         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(()) | ||||
|         }); | ||||
|  | ||||
| @ -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<bool> { | ||||
|         comment_seers::table.filter(comment_seers::comment_id.eq(c.id)) | ||||
|             .filter(comment_seers::user_id.eq(u.id)) | ||||
|             .load::<CommentSeers>(conn) | ||||
|             .expect("Comment::get_responses: loading error") | ||||
|             .is_empty() | ||||
|             .map_err(Error::from) | ||||
|             .map(|r| !r.is_empty()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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> { | ||||
|         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> { | ||||
|         Post::get(conn, self.post_id) | ||||
|     } | ||||
| 
 | ||||
|     pub fn count_local(conn: &Connection) -> i64 { | ||||
|     pub fn count_local(conn: &Connection) -> Result<i64> { | ||||
|         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<Comment> { | ||||
|     pub fn get_responses(&self, conn: &Connection) -> Result<Vec<Comment>> { | ||||
|         comments::table.filter(comments::in_response_to_id.eq(self.id)) | ||||
|             .load::<Comment>(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<Comment> { | ||||
|         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<String> { | ||||
|         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<Note> { | ||||
|         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<String>, | ||||
|             )?))?; | ||||
|         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::<Vec<link::Mention>>(), | ||||
|             ) | ||||
|             .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<Create> { | ||||
|         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::<Id>() | ||||
|                     .expect("Comment::create_activity: id error"), | ||||
|             ) | ||||
|             .expect("Comment::create_activity: to error"); | ||||
|                     .to_link_vec::<Id>()?, | ||||
|             )?; | ||||
|         act.object_props | ||||
|             .set_cc_link_vec::<Id>(vec![]) | ||||
|             .expect("Comment::create_activity: cc error"); | ||||
|         act | ||||
|             .set_cc_link_vec::<Id>(vec![])?; | ||||
|         Ok(act) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl FromActivity<Note, Connection> 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<Comment> { | ||||
|         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<serde_json::Value>| match v.as_ref().unwrap_or(&serde_json::Value::Null) { | ||||
| @ -216,42 +191,35 @@ impl FromActivity<Note, Connection> 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<i32>)?, | ||||
|                     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::<link::Mention>(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<Note, Connection> for Comment { | ||||
|             let receivers_ap_url = to.chain(cc).chain(bto).chain(bcc) | ||||
|                 .collect::<HashSet<_>>()//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::<HashSet<User>>();//remove duplicates (prevent db error)
 | ||||
| 
 | ||||
|             for user in &receivers_ap_url { | ||||
| @ -295,18 +263,20 @@ impl FromActivity<Note, Connection> for Comment { | ||||
|                         comment_id: comm.id, | ||||
|                         user_id: user.id | ||||
|                     } | ||||
|                 ); | ||||
|                 )?; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         comm.notify(conn); | ||||
|         comm | ||||
|         comm.notify(conn)?; | ||||
|         Ok(comm) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Notify<Connection> 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<Connection> 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<Self> { | ||||
|         Comment::list_by_post(conn, p.id).into_iter() | ||||
|     pub fn from_post(conn: &Connection, p: &Post, user: Option<&User>) -> Result<Vec<Self>> { | ||||
|         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<Self> { | ||||
|         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<Connection, Delete> for Comment { | ||||
|     fn delete(&self, conn: &Connection) -> Delete { | ||||
|     type Error = Error; | ||||
| 
 | ||||
|     fn delete(&self, conn: &Connection) -> Result<Delete> { | ||||
|         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<Delete> { | ||||
|         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) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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}, | ||||
|  | ||||
| @ -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<Follow> { | ||||
|     pub fn find(conn: &Connection, from: i32, to: i32) -> Result<Follow> { | ||||
|         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<FollowAct> { | ||||
|         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::<Id>(user.clone().into_id()) | ||||
|             .expect("Follow::to_activity: actor error"); | ||||
|             .set_actor_link::<Id>(user.clone().into_id())?; | ||||
|         act.follow_props | ||||
|             .set_object_link::<Id>(target.clone().into_id()) | ||||
|             .expect("Follow::to_activity: object error"); | ||||
|             .set_object_link::<Id>(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::<Id>(vec![]) | ||||
|             .expect("Follow::to_activity: cc error"); | ||||
|         act | ||||
|             .set_cc_link_vec::<Id>(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<Follow> { | ||||
|         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::<Id>(vec![]) | ||||
|             .expect("Follow::accept_follow: cc error"); | ||||
|             .set_cc_link_vec::<Id>(vec![])?; | ||||
|         accept | ||||
|             .accept_props | ||||
|             .set_actor_link::<Id>(target.clone().into_id()) | ||||
|             .expect("Follow::accept_follow: actor error"); | ||||
|             .set_actor_link::<Id>(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<FollowAct, Connection> 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<Follow> { | ||||
|         let from_id = follow | ||||
|             .follow_props | ||||
|             .actor_link::<Id>() | ||||
|             .map(|l| l.into()) | ||||
|             .unwrap_or_else(|_| { | ||||
|                 follow | ||||
|                     .follow_props | ||||
|                     .actor_object::<Person>() | ||||
|                     .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::<Person>()? | ||||
|                 .object_props | ||||
|                 .id_string()?) as Result<String>)?; | ||||
|         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<FollowAct, Connection> for Follow { | ||||
| } | ||||
| 
 | ||||
| impl Notify<Connection> 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<Connection> for Follow { | ||||
|                 object_id: self.id, | ||||
|                 user_id: self.following_id, | ||||
|             }, | ||||
|         ); | ||||
|         ).map(|_| ()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Deletable<Connection, Undo> for Follow { | ||||
|     fn delete(&self, conn: &Connection) -> Undo { | ||||
|     type Error = Error; | ||||
| 
 | ||||
|     fn delete(&self, conn: &Connection) -> Result<Undo> { | ||||
|         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::<Id>(self.clone().into_id()) | ||||
|             .expect("Follow::delete: object error"); | ||||
|         undo | ||||
|             .set_object_link::<Id>(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<Undo> { | ||||
|         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) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<Instance> { | ||||
|     pub fn get_local(conn: &Connection) -> Result<Instance> { | ||||
|         instances::table | ||||
|             .filter(instances::local.eq(true)) | ||||
|             .limit(1) | ||||
|             .load::<Instance>(conn) | ||||
|             .expect("Instance::get_local: loading error") | ||||
|             .load::<Instance>(conn)? | ||||
|             .into_iter() | ||||
|             .nth(0) | ||||
|             .nth(0).ok_or(Error::NotFound) | ||||
|     } | ||||
| 
 | ||||
|     pub fn get_remotes(conn: &Connection) -> Vec<Instance> { | ||||
|     pub fn get_remotes(conn: &Connection) -> Result<Vec<Instance>> { | ||||
|         instances::table | ||||
|             .filter(instances::local.eq(false)) | ||||
|             .load::<Instance>(conn) | ||||
|             .expect("Instance::get_remotes: loading error") | ||||
|             .map_err(Error::from) | ||||
|     } | ||||
| 
 | ||||
|     pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Vec<Instance> { | ||||
|     pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<Instance>> { | ||||
|         instances::table | ||||
|             .order(instances::public_domain.asc()) | ||||
|             .offset(min.into()) | ||||
|             .limit((max - min).into()) | ||||
|             .load::<Instance>(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<bool> { | ||||
|         for block in instances::table | ||||
|             .filter(instances::blocked.eq(true)) | ||||
|             .get_results::<Instance>(conn) | ||||
|             .expect("Instance::is_blocked: loading error") | ||||
|             .get_results::<Instance>(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<bool> { | ||||
|         users::table | ||||
|             .filter(users::instance_id.eq(self.id)) | ||||
|             .filter(users::is_admin.eq(true)) | ||||
|             .load::<User>(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<User> { | ||||
|         users::table | ||||
|             .filter(users::instance_id.eq(self.id)) | ||||
|             .filter(users::is_admin.eq(true)) | ||||
|             .limit(1) | ||||
|             .get_result::<User>(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<i64> { | ||||
|         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); | ||||
|  | ||||
| @ -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<bcrypt::BcryptError> for Error { | ||||
|     fn from(_: bcrypt::BcryptError) -> Self { | ||||
|         Error::Signature | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<openssl::error::ErrorStack> for Error { | ||||
|     fn from(_: openssl::error::ErrorStack) -> Self { | ||||
|         Error::Signature | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<diesel::result::Error> for Error { | ||||
|     fn from(err: diesel::result::Error) -> Self { | ||||
|         Error::Db(err) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<std::option::NoneError> for Error { | ||||
|     fn from(_: std::option::NoneError) -> Self { | ||||
|         Error::NotFound | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<url::ParseError> for Error { | ||||
|     fn from(_: url::ParseError) -> Self { | ||||
|         Error::Url | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<serde_json::Error> for Error { | ||||
|     fn from(_: serde_json::Error) -> Self { | ||||
|         Error::SerDe | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<reqwest::Error> for Error { | ||||
|     fn from(_: reqwest::Error) -> Self { | ||||
|         Error::Request | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<reqwest::header::InvalidHeaderValue> for Error { | ||||
|     fn from(_: reqwest::header::InvalidHeaderValue) -> Self { | ||||
|         Error::Request | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<activitypub::Error> for Error { | ||||
|     fn from(err: activitypub::Error) -> Self { | ||||
|         match err { | ||||
|             activitypub::Error::NotFound => Error::MissingApProperty, | ||||
|             _ => Error::SerDe, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<webfinger::WebfingerError> for Error { | ||||
|     fn from(_: webfinger::WebfingerError) -> Self { | ||||
|         Error::Webfinger | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<search::SearcherError> for Error { | ||||
|     fn from(err: search::SearcherError) -> Self { | ||||
|         Error::Search(err) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl From<std::io::Error> for Error { | ||||
|     fn from(err: std::io::Error) -> Self { | ||||
|         Error::Io(err) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub type Result<T> = std::result::Result<T, Error>; | ||||
| 
 | ||||
| pub type ApiResult<T> = std::result::Result<T, canapi::Error>; | ||||
| 
 | ||||
| /// 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<Self> { | ||||
|         pub fn $fn(conn: &crate::Connection, $($col: $type),+) -> Result<Self> { | ||||
|             $table::table | ||||
|                 $(.filter($table::$col.eq($col)))+ | ||||
|                 .limit(1) | ||||
|                 .load::<Self>(conn) | ||||
|                 .expect("macro::find_by: Error loading $table by $col") | ||||
|                 .into_iter().nth(0) | ||||
|                 .load::<Self>(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<Self> { | ||||
|         pub fn $fn(conn: &crate::Connection, $($col: $type),+) -> Result<Vec<Self>> { | ||||
|             $table::table | ||||
|                 $(.filter($table::$col.eq($col)))+ | ||||
|                 .load::<Self>(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<Self> { | ||||
|         pub fn get(conn: &crate::Connection, id: i32) -> Result<Self> { | ||||
|             $table::table | ||||
|                 .filter($table::id.eq(id)) | ||||
|                 .limit(1) | ||||
|                 .load::<Self>(conn) | ||||
|                 .expect("macro::get: Error loading $table by id") | ||||
|                 .load::<Self>(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<Self> { | ||||
|             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<Self> { | ||||
|             $table::table | ||||
|                 .order_by($table::id.desc()) | ||||
|                 .limit(1) | ||||
|                 .load::<Self>(conn) | ||||
|                 .expect(concat!( | ||||
|                     "macro::last: Error getting last ", | ||||
|                     stringify!($table) | ||||
|                 )) | ||||
|                 .iter() | ||||
|                 .load::<Self>(conn)? | ||||
|                 .into_iter() | ||||
|                 .next() | ||||
|                 .expect(concat!("macro::last: No last ", stringify!($table))) | ||||
|                 .clone() | ||||
|                 .ok_or(Error::NotFound) | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|  | ||||
| @ -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<activity::Like> { | ||||
|         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::<Id>(vec![]) | ||||
|             .expect("Like::to_activity: cc error"); | ||||
|             .set_cc_link_vec::<Id>(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<activity::Like, Connection> 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<Like> { | ||||
|         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<Connection> 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<Connection> for Like { | ||||
|                     object_id: self.id, | ||||
|                     user_id: author.id, | ||||
|                 }, | ||||
|             ); | ||||
|             )?; | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Deletable<Connection, activity::Undo> for Like { | ||||
|     fn delete(&self, conn: &Connection) -> activity::Undo { | ||||
|     type Error = Error; | ||||
| 
 | ||||
|     fn delete(&self, conn: &Connection) -> Result<activity::Undo> { | ||||
|         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::<Id>(vec![]) | ||||
|             .expect("Like::delete: cc error"); | ||||
|             .set_cc_link_vec::<Id>(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<activity::Undo> { | ||||
|         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) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<Media> { | ||||
|     pub fn list_all_medias(conn: &Connection) -> Result<Vec<Media>> { | ||||
|         medias::table | ||||
|             .load::<Media>(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<SafeString> { | ||||
|         let url = self.url(conn)?; | ||||
|         Ok(match self.category() { | ||||
|             MediaCategory::Image => SafeString::new(&format!( | ||||
|                 r#"<img src="{}" alt="{}" title="{}" class=\"preview\">"#, | ||||
|                 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<SafeString> { | ||||
|         let url = self.url(conn)?; | ||||
|         Ok(match self.category() { | ||||
|             MediaCategory::Image => SafeString::new(&format!( | ||||
|                 r#"<img src="{}" alt="{}" title="{}">"#, | ||||
|                 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<SafeString> { | ||||
|         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<String> { | ||||
|         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<Media, ()> { | ||||
|     pub fn save_remote(conn: &Connection, url: String, user: &User) -> Result<Media> { | ||||
|         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<Media> { | ||||
|     pub fn from_activity(conn: &Connection, image: &Image) -> Result<Media> { | ||||
|         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) | ||||
|             ); | ||||
|  | ||||
| @ -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<User> { | ||||
|     pub fn get_mentioned(&self, conn: &Connection) -> Result<User> { | ||||
|         User::get(conn, self.mentioned_id) | ||||
|     } | ||||
| 
 | ||||
|     pub fn get_post(&self, conn: &Connection) -> Option<Post> { | ||||
|         self.post_id.and_then(|id| Post::get(conn, id)) | ||||
|     pub fn get_post(&self, conn: &Connection) -> Result<Post> { | ||||
|         self.post_id.ok_or(Error::NotFound).and_then(|id| Post::get(conn, id)) | ||||
|     } | ||||
| 
 | ||||
|     pub fn get_comment(&self, conn: &Connection) -> Option<Comment> { | ||||
|         self.comment_id.and_then(|id| Comment::get(conn, id)) | ||||
|     pub fn get_comment(&self, conn: &Connection) -> Result<Comment> { | ||||
|         self.comment_id.ok_or(Error::NotFound).and_then(|id| Comment::get(conn, id)) | ||||
|     } | ||||
| 
 | ||||
|     pub fn get_user(&self, conn: &Connection) -> Option<User> { | ||||
|     pub fn get_user(&self, conn: &Connection) -> Result<User> { | ||||
|         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<link::Mention> { | ||||
|         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<link::Mention> { | ||||
|         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<Self> { | ||||
|     ) -> Result<Self> { | ||||
|         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<Connection> 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(|_| ()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<Notification> { | ||||
|     pub fn find_for_user(conn: &Connection, user: &User) -> Result<Vec<Notification>> { | ||||
|         notifications::table | ||||
|             .filter(notifications::user_id.eq(user.id)) | ||||
|             .order_by(notifications::creation_date.desc()) | ||||
|             .load::<Notification>(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<i64> { | ||||
|         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<Notification> { | ||||
|     ) -> Result<Vec<Notification>> { | ||||
|         notifications::table | ||||
|             .filter(notifications::user_id.eq(user.id)) | ||||
|             .order_by(notifications::creation_date.desc()) | ||||
|             .offset(min.into()) | ||||
|             .limit((max - min).into()) | ||||
|             .load::<Notification>(conn) | ||||
|             .expect("Notification::page_for_user: notification loading error") | ||||
|             .map_err(Error::from) | ||||
|     } | ||||
| 
 | ||||
|     pub fn find<S: Into<String>>(conn: &Connection, kind: S, obj: i32) -> Option<Notification> { | ||||
|     pub fn find<S: Into<String>>(conn: &Connection, kind: S, obj: i32) -> Result<Notification> { | ||||
|         notifications::table | ||||
|             .filter(notifications::kind.eq(kind.into())) | ||||
|             .filter(notifications::object_id.eq(obj)) | ||||
|             .get_result::<Notification>(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<String> { | ||||
|         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<Post> { | ||||
|         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<User> { | ||||
|         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) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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)] | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -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<Reshare> { | ||||
|     pub fn get_recents_for_author(conn: &Connection, user: &User, limit: i64) -> Result<Vec<Reshare>> { | ||||
|         reshares::table | ||||
|             .filter(reshares::user_id.eq(user.id)) | ||||
|             .order(reshares::creation_date.desc()) | ||||
|             .limit(limit) | ||||
|             .load::<Reshare>(conn) | ||||
|             .expect("Reshare::get_recents_for_author: loading error") | ||||
|             .map_err(Error::from) | ||||
|     } | ||||
| 
 | ||||
|     pub fn get_post(&self, conn: &Connection) -> Option<Post> { | ||||
|     pub fn get_post(&self, conn: &Connection) -> Result<Post> { | ||||
|         Post::get(conn, self.post_id) | ||||
|     } | ||||
| 
 | ||||
|     pub fn get_user(&self, conn: &Connection) -> Option<User> { | ||||
|     pub fn get_user(&self, conn: &Connection) -> Result<User> { | ||||
|         User::get(conn, self.user_id) | ||||
|     } | ||||
| 
 | ||||
|     pub fn to_activity(&self, conn: &Connection) -> Announce { | ||||
|     pub fn to_activity(&self, conn: &Connection) -> Result<Announce> { | ||||
|         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::<Id>(vec![]) | ||||
|             .expect("Reshare::to_activity: cc error"); | ||||
|             .set_cc_link_vec::<Id>(vec![])?; | ||||
| 
 | ||||
|         act | ||||
|         Ok(act) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl FromActivity<Announce, Connection> 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<Reshare> { | ||||
|         let user = User::from_url( | ||||
|             conn, | ||||
|             announce | ||||
|                 .announce_props | ||||
|                 .actor_link::<Id>() | ||||
|                 .expect("Reshare::from_activity: actor error") | ||||
|                 .actor_link::<Id>()? | ||||
|                 .as_ref(), | ||||
|         ); | ||||
|         )?; | ||||
|         let post = Post::find_by_ap_url( | ||||
|             conn, | ||||
|             announce | ||||
|                 .announce_props | ||||
|                 .object_link::<Id>() | ||||
|                 .expect("Reshare::from_activity: object error") | ||||
|                 .object_link::<Id>()? | ||||
|                 .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<Connection> 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<Connection> for Reshare { | ||||
|                     object_id: self.id, | ||||
|                     user_id: author.id, | ||||
|                 }, | ||||
|             ); | ||||
|             )?; | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Deletable<Connection, Undo> for Reshare { | ||||
|     fn delete(&self, conn: &Connection) -> Undo { | ||||
|     type Error = Error; | ||||
| 
 | ||||
|     fn delete(&self, conn: &Connection) -> Result<Undo> { | ||||
|         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::<Id>(vec![]) | ||||
|             .expect("Reshare::delete: cc error"); | ||||
|             .set_cc_link_vec::<Id>(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<Undo> { | ||||
|         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) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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()); | ||||
| 
 | ||||
|  | ||||
| @ -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<Path>) -> Result<Self,SearcherError> { | ||||
|     pub fn create(path: &AsRef<Path>) -> Result<Self> { | ||||
|         let whitespace_tokenizer = tokenizer::WhitespaceTokenizer | ||||
|             .filter(LowerCaser); | ||||
| 
 | ||||
| @ -94,7 +95,7 @@ impl Searcher { | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     pub fn open(path: &AsRef<Path>) -> Result<Self, SearcherError> { | ||||
|     pub fn open(path: &AsRef<Path>) -> Result<Self> { | ||||
|         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<Post>{ | ||||
| @ -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() | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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<Hashtag> { | ||||
|         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> { | ||||
|         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<Hashtag> { | ||||
|         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) | ||||
|     } | ||||
| } | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -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<Error> 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?<query..>")] | ||||
| pub fn oauth(query: Form<OAuthRequest>, conn: DbConn) -> Json<serde_json::Value> { | ||||
|     let app = App::find_by_client_id(&*conn, &query.client_id).expect("OAuth request from unknown client"); | ||||
| pub fn oauth(query: Form<OAuthRequest>, conn: DbConn) -> Result<Json<serde_json::Value>, 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" | ||||
|         })) | ||||
|         }))) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										36
									
								
								src/inbox.rs
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								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::<Tombstone>()? | ||||
| @ -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)?, | ||||
|  | ||||
							
								
								
									
										24
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								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<DbPool> { | ||||
| } | ||||
| 
 | ||||
| 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()); | ||||
|  | ||||
| @ -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("/~/<name>?<page>", rank = 2)] | ||||
| pub fn details(intl: I18n, name: String, conn: DbConn, user: Option<User>, page: Option<Page>) -> Result<Ructe, Ructe> { | ||||
| pub fn details(intl: I18n, name: String, conn: DbConn, user: Option<User>, page: Option<Page>) -> Result<Ructe, ErrorPage> { | ||||
|     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<User>, 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("/~/<name>", rank = 1)] | ||||
| pub fn activity_details(name: String, conn: DbConn, _ap: ApRequest) -> Option<ActivityStream<CustomGroup>> { | ||||
|     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<NewBlogForm>, 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<NewBlogForm>, 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<NewBlogForm>, user: User, intl: I1 | ||||
| } | ||||
| 
 | ||||
| #[post("/~/<name>/delete")] | ||||
| pub fn delete(conn: DbConn, name: String, user: Option<User>, intl: I18n, searcher: Searcher) -> Result<Redirect, Option<Ructe>>{ | ||||
|     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<User>, intl: I18n, searcher: Searcher) -> Result<Redirect, Ructe>{ | ||||
|     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("/~/<name>/outbox")] | ||||
| pub fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> { | ||||
|     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("/~/<name>/atom.xml")] | ||||
| pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> { | ||||
|     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::<Vec<Entry>>()) | ||||
|         .build() | ||||
|         .expect("blogs::atom_feed: feed creation error"); | ||||
|         .build().ok()?; | ||||
|     Some(Content(ContentType::new("application", "atom+xml"), feed.to_string())) | ||||
| } | ||||
|  | ||||
| @ -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("/~/<blog_name>/<slug>/comment", data = "<form>")] | ||||
| pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>, user: User, conn: DbConn, worker: Worker, intl: I18n) | ||||
|     -> Result<Redirect, Option<Ructe>> { | ||||
|     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<Redirect, Ructe> { | ||||
|     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<NewCommentForm> | ||||
|                 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<NewCommentForm> | ||||
|         }) | ||||
|         .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("/~/<blog>/<slug>/comment/<id>/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<Redirect, ErrorPage> { | ||||
|     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/<id>")] | ||||
| pub fn activity_pub(_blog: String, _slug: String, id: i32, _ap: ApRequest, conn: DbConn) -> Option<ActivityStream<Note>> { | ||||
|     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) | ||||
| } | ||||
|  | ||||
| @ -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<Error> 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::<DbConn>().succeeded(); | ||||
|         let intl = req.guard::<I18n>().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::<DbConn>().succeeded(); | ||||
|  | ||||
| @ -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<User>, 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::<Vec<i32>>(); | ||||
|                 in_feed.push(user.id); | ||||
|                 Post::user_feed_page(&*conn, in_feed, Page::default().limits()) | ||||
|             }); | ||||
| pub fn index(conn: DbConn, user: Option<User>, intl: I18n) -> Result<Ructe, ErrorPage> { | ||||
|     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::<Vec<i32>>(); | ||||
|         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?<page>")] | ||||
| pub fn local(conn: DbConn, user: Option<User>, page: Option<Page>, intl: I18n) -> Ructe { | ||||
| pub fn local(conn: DbConn, user: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> { | ||||
|     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?<page>")] | ||||
| pub fn feed(conn: DbConn, user: User, page: Option<Page>, intl: I18n) -> Ructe { | ||||
| pub fn feed(conn: DbConn, user: User, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> { | ||||
|     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::<Vec<i32>>(); | ||||
|     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?<page>")] | ||||
| pub fn federated(conn: DbConn, user: Option<User>, page: Option<Page>, intl: I18n) -> Ructe { | ||||
| pub fn federated(conn: DbConn, user: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> { | ||||
|     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<Ructe, ErrorPage> { | ||||
|     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 = "<form>")] | ||||
| pub fn update_settings(conn: DbConn, admin: Admin, form: LenientForm<InstanceSettingsForm>, intl: I18n) -> Result<Redirect, Ructe> { | ||||
|     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?<page>")] | ||||
| pub fn admin_instances(admin: Admin, conn: DbConn, page: Option<Page>, intl: I18n) -> Ructe { | ||||
| pub fn admin_instances(admin: Admin, conn: DbConn, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> { | ||||
|     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/<id>/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<Redirect, ErrorPage> { | ||||
|     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?<page>")] | ||||
| pub fn admin_users(admin: Admin, conn: DbConn, page: Option<Page>, intl: I18n) -> Ructe { | ||||
| pub fn admin_users(admin: Admin, conn: DbConn, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> { | ||||
|     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/<id>/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<Redirect, ErrorPage> { | ||||
|     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 = "<data>")] | ||||
| @ -200,7 +192,7 @@ pub fn shared_inbox(conn: DbConn, data: SignedJson<serde_json::Value>, 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<serde_json::Value>, headers: | ||||
| } | ||||
| 
 | ||||
| #[get("/nodeinfo")] | ||||
| pub fn nodeinfo(conn: DbConn) -> Json<serde_json::Value> { | ||||
|     Json(json!({ | ||||
| pub fn nodeinfo(conn: DbConn) -> Result<Json<serde_json::Value>, ErrorPage> { | ||||
|     Ok(Json(json!({ | ||||
|         "version": "2.0", | ||||
|         "software": { | ||||
|             "name": "Plume", | ||||
| @ -229,31 +221,31 @@ pub fn nodeinfo(conn: DbConn) -> Json<serde_json::Value> { | ||||
|         "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<User>, conn: DbConn, intl: I18n) -> Ructe { | ||||
|     render!(instance::about( | ||||
| pub fn about(user: Option<User>, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> { | ||||
|     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<serde_json::Value> { | ||||
|     let instance = Instance::get_local(&*conn).expect("instance::web_manifest: local instance not found error"); | ||||
|     Json(json!({ | ||||
| pub fn web_manifest(conn: DbConn) -> Result<Json<serde_json::Value>, 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<serde_json::Value> { | ||||
|                 "src": "/static/icons/trwnh/feather/plumeFeather.svg" | ||||
|             } | ||||
|         ] | ||||
|     })) | ||||
|     }))) | ||||
| } | ||||
|  | ||||
| @ -11,27 +11,28 @@ use plume_models::{ | ||||
|     users::User | ||||
| }; | ||||
| use Worker; | ||||
| use routes::errors::ErrorPage; | ||||
| 
 | ||||
| #[post("/~/<blog>/<slug>/like")] | ||||
| pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Option<Redirect> { | ||||
| pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Result<Redirect, ErrorPage> { | ||||
|     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("/~/<blog>/<slug>/like", rank = 2)] | ||||
|  | ||||
| @ -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<Ructe, ErrorPage> { | ||||
|     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<String, status::BadRequest<&'static str>> { | ||||
|     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/<id>")] | ||||
| 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<Ructe, ErrorPage> { | ||||
|     let media = Media::get(&*conn, id)?; | ||||
|     Ok(render!(medias::details( | ||||
|         &(&*conn, &intl.catalog, Some(user)), | ||||
|         media | ||||
|     )) | ||||
|     ))) | ||||
| } | ||||
| 
 | ||||
| #[post("/medias/<id>/delete")] | ||||
| pub fn delete(id: i32, _user: User, conn: DbConn) -> Option<Redirect> { | ||||
| pub fn delete(id: i32, _user: User, conn: DbConn) -> Result<Redirect, ErrorPage> { | ||||
|     let media = Media::get(&*conn, id)?; | ||||
|     media.delete(&*conn); | ||||
|     Some(Redirect::to(uri!(list))) | ||||
|     media.delete(&*conn)?; | ||||
|     Ok(Redirect::to(uri!(list))) | ||||
| } | ||||
| 
 | ||||
| #[post("/medias/<id>/avatar")] | ||||
| pub fn set_avatar(id: i32, user: User, conn: DbConn) -> Option<Redirect> { | ||||
| pub fn set_avatar(id: i32, user: User, conn: DbConn) -> Result<Redirect, ErrorPage> { | ||||
|     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))) | ||||
| } | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
| @ -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?<page>")] | ||||
| pub fn notifications(conn: DbConn, user: User, page: Option<Page>, intl: I18n) -> Ructe { | ||||
| pub fn notifications(conn: DbConn, user: User, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> { | ||||
|     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?<page>", rank = 2)] | ||||
|  | ||||
| @ -21,20 +21,19 @@ use plume_models::{ | ||||
|     tags::*, | ||||
|     users::User | ||||
| }; | ||||
| use routes::comments::NewCommentForm; | ||||
| use routes::{errors::ErrorPage, comments::NewCommentForm}; | ||||
| use template_utils::Ructe; | ||||
| use Worker; | ||||
| use Searcher; | ||||
| 
 | ||||
| #[get("/~/<blog>/<slug>?<responding_to>", rank = 4)] | ||||
| pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, responding_to: Option<i32>, intl: I18n) -> Result<Ructe, Ructe> { | ||||
|     let blog = Blog::find_by_fqn(&*conn, &blog).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, user.clone()))))?; | ||||
|     let post = Post::find_by_slug(&*conn, &slug, blog.id).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, user.clone()))))?; | ||||
|     if post.published || post.get_authors(&*conn).into_iter().any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)) { | ||||
|         let comments = CommentTree::from_post(&*conn, &post, user.as_ref()); | ||||
| pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, responding_to: Option<i32>, intl: I18n) -> Result<Ructe, ErrorPage> { | ||||
|     let blog = Blog::find_by_fqn(&*conn, &blog)?; | ||||
|     let post = Post::find_by_slug(&*conn, &slug, blog.id)?; | ||||
|     if post.published || post.get_authors(&*conn)?.into_iter().any(|a| a.id == user.clone().map(|u| u.id).unwrap_or(0)) { | ||||
|         let comments = CommentTree::from_post(&*conn, &post, user.as_ref())?; | ||||
| 
 | ||||
|         let previous = responding_to.map(|r| Comment::get(&*conn, r) | ||||
|             .expect("posts::details_reponse: Error retrieving previous comment")); | ||||
|         let previous = responding_to.and_then(|r| Comment::get(&*conn, r).ok()); | ||||
| 
 | ||||
|         Ok(render!(posts::details( | ||||
|             &(&*conn, &intl.catalog, user.clone()), | ||||
| @ -42,14 +41,14 @@ pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, res | ||||
|             blog, | ||||
|             &NewCommentForm { | ||||
|                 warning: previous.clone().map(|p| p.spoiler_text).unwrap_or_default(), | ||||
|                 content: previous.clone().map(|p| format!( | ||||
|                 content: previous.clone().and_then(|p| Some(format!( | ||||
|                     "@{} {}", | ||||
|                     p.get_author(&*conn).get_fqn(&*conn), | ||||
|                     Mention::list_for_comment(&*conn, p.id) | ||||
|                     p.get_author(&*conn).ok()?.get_fqn(&*conn), | ||||
|                     Mention::list_for_comment(&*conn, p.id).ok()? | ||||
|                         .into_iter() | ||||
|                         .filter_map(|m| { | ||||
|                             let user = user.clone(); | ||||
|                             if let Some(mentioned) = m.get_mentioned(&*conn) { | ||||
|                             if let Ok(mentioned) = m.get_mentioned(&*conn) { | ||||
|                                 if user.is_none() || mentioned.id != user.expect("posts::details_response: user error while listing mentions").id { | ||||
|                                     Some(format!("@{}", mentioned.get_fqn(&*conn))) | ||||
|                                 } else { | ||||
| @ -59,22 +58,22 @@ pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, res | ||||
|                                 None | ||||
|                             } | ||||
|                         }).collect::<Vec<String>>().join(" ")) | ||||
|                     ).unwrap_or_default(), | ||||
|                     )).unwrap_or_default(), | ||||
|                 ..NewCommentForm::default() | ||||
|             }, | ||||
|             ValidationErrors::default(), | ||||
|             Tag::for_post(&*conn, post.id), | ||||
|             Tag::for_post(&*conn, post.id)?, | ||||
|             comments, | ||||
|             previous, | ||||
|             post.count_likes(&*conn), | ||||
|             post.count_reshares(&*conn), | ||||
|             user.clone().map(|u| u.has_liked(&*conn, &post)).unwrap_or(false), | ||||
|             user.clone().map(|u| u.has_reshared(&*conn, &post)).unwrap_or(false), | ||||
|             user.map(|u| u.is_following(&*conn, post.get_authors(&*conn)[0].id)).unwrap_or(false), | ||||
|             post.get_authors(&*conn)[0].clone() | ||||
|             post.count_likes(&*conn)?, | ||||
|             post.count_reshares(&*conn)?, | ||||
|             user.clone().and_then(|u| u.has_liked(&*conn, &post).ok()).unwrap_or(false), | ||||
|             user.clone().and_then(|u| u.has_reshared(&*conn, &post).ok()).unwrap_or(false), | ||||
|             user.and_then(|u| u.is_following(&*conn, post.get_authors(&*conn).ok()?[0].id).ok()).unwrap_or(false), | ||||
|             post.get_authors(&*conn)?[0].clone() | ||||
|         ))) | ||||
|     } else { | ||||
|         Err(render!(errors::not_authorized( | ||||
|         Ok(render!(errors::not_authorized( | ||||
|             &(&*conn, &intl.catalog, user.clone()), | ||||
|             "This post isn't published yet." | ||||
|         ))) | ||||
| @ -83,10 +82,10 @@ pub fn details(blog: String, slug: String, conn: DbConn, user: Option<User>, res | ||||
| 
 | ||||
| #[get("/~/<blog>/<slug>", rank = 3)] | ||||
| pub fn activity_details(blog: String, slug: String, conn: DbConn, _ap: ApRequest) -> Result<ActivityStream<LicensedArticle>, Option<String>> { | ||||
|     let blog = Blog::find_by_fqn(&*conn, &blog).ok_or(None)?; | ||||
|     let post = Post::find_by_slug(&*conn, &slug, blog.id).ok_or(None)?; | ||||
|     let blog = Blog::find_by_fqn(&*conn, &blog).map_err(|_| None)?; | ||||
|     let post = Post::find_by_slug(&*conn, &slug, blog.id).map_err(|_| None)?; | ||||
|     if post.published { | ||||
|         Ok(ActivityStream::new(post.to_activity(&*conn))) | ||||
|         Ok(ActivityStream::new(post.to_activity(&*conn).map_err(|_| String::from("Post serialization error"))?)) | ||||
|     } else { | ||||
|         Err(Some(String::from("Not published yet."))) | ||||
|     } | ||||
| @ -101,23 +100,23 @@ pub fn new_auth(blog: String, i18n: I18n) -> Flash<Redirect> { | ||||
| } | ||||
| 
 | ||||
| #[get("/~/<blog>/new", rank = 1)] | ||||
| pub fn new(blog: String, user: User, conn: DbConn, intl: I18n) -> Option<Ructe> { | ||||
| pub fn new(blog: String, user: User, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> { | ||||
|     let b = Blog::find_by_fqn(&*conn, &blog)?; | ||||
| 
 | ||||
|     if !user.is_author_in(&*conn, &b) { | ||||
|     if !user.is_author_in(&*conn, &b)? { | ||||
|         // TODO actually return 403 error code
 | ||||
|         Some(render!(errors::not_authorized( | ||||
|         Ok(render!(errors::not_authorized( | ||||
|             &(&*conn, &intl.catalog, Some(user)), | ||||
|             "You are not author in this blog." | ||||
|         ))) | ||||
|     } else { | ||||
|         let medias = Media::for_user(&*conn, user.id); | ||||
|         Some(render!(posts::new( | ||||
|         let medias = Media::for_user(&*conn, user.id)?; | ||||
|         Ok(render!(posts::new( | ||||
|             &(&*conn, &intl.catalog, Some(user)), | ||||
|             b, | ||||
|             false, | ||||
|             &NewPostForm { | ||||
|                 license: Instance::get_local(&*conn).map(|i| i.default_license).unwrap_or_else(||String::from("CC-BY-SA")), | ||||
|                 license: Instance::get_local(&*conn)?.default_license, | ||||
|                 ..NewPostForm::default() | ||||
|             }, | ||||
|             true, | ||||
| @ -129,12 +128,12 @@ pub fn new(blog: String, user: User, conn: DbConn, intl: I18n) -> Option<Ructe> | ||||
| } | ||||
| 
 | ||||
| #[get("/~/<blog>/<slug>/edit")] | ||||
| pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) -> Option<Ructe> { | ||||
| pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> { | ||||
|     let b = Blog::find_by_fqn(&*conn, &blog)?; | ||||
|     let post = Post::find_by_slug(&*conn, &slug, b.id)?; | ||||
| 
 | ||||
|     if !user.is_author_in(&*conn, &b) { | ||||
|         Some(render!(errors::not_authorized( | ||||
|     if !user.is_author_in(&*conn, &b)? { | ||||
|         Ok(render!(errors::not_authorized( | ||||
|             &(&*conn, &intl.catalog, Some(user)), | ||||
|             "You are not author in this blog." | ||||
|         ))) | ||||
| @ -145,8 +144,8 @@ pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) -> | ||||
|             post.content.get().clone() // fallback to HTML if the markdown was not stored
 | ||||
|         }; | ||||
| 
 | ||||
|         let medias = Media::for_user(&*conn, user.id); | ||||
|         Some(render!(posts::new( | ||||
|         let medias = Media::for_user(&*conn, user.id)?; | ||||
|         Ok(render!(posts::new( | ||||
|             &(&*conn, &intl.catalog, Some(user)), | ||||
|             b, | ||||
|             true, | ||||
| @ -154,7 +153,7 @@ pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) -> | ||||
|                 title: post.title.clone(), | ||||
|                 subtitle: post.subtitle.clone(), | ||||
|                 content: source, | ||||
|                 tags: Tag::for_post(&*conn, post.id) | ||||
|                 tags: Tag::for_post(&*conn, post.id)? | ||||
|                     .into_iter() | ||||
|                     .filter_map(|t| if !t.is_hashtag {Some(t.tag)} else {None}) | ||||
|                     .collect::<Vec<String>>() | ||||
| @ -173,9 +172,9 @@ pub fn edit(blog: String, slug: String, user: User, conn: DbConn, intl: I18n) -> | ||||
| 
 | ||||
| #[post("/~/<blog>/<slug>/edit", data = "<form>")] | ||||
| pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: LenientForm<NewPostForm>, worker: Worker, intl: I18n, searcher: Searcher) | ||||
|     -> Result<Redirect, Option<Ructe>> { | ||||
|     let b = Blog::find_by_fqn(&*conn, &blog).ok_or(None)?; | ||||
|     let mut post = Post::find_by_slug(&*conn, &slug, b.id).ok_or(None)?; | ||||
|     -> Result<Redirect, Ructe> { | ||||
|     let b = Blog::find_by_fqn(&*conn, &blog).expect("post::update: blog error"); | ||||
|     let mut post = Post::find_by_slug(&*conn, &slug, b.id).expect("post::update: find by slug error"); | ||||
| 
 | ||||
|     let new_slug = if !post.published { | ||||
|         form.title.to_string().to_kebab_case() | ||||
| @ -188,7 +187,7 @@ pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: Lenien | ||||
|         Err(e) => e | ||||
|     }; | ||||
| 
 | ||||
|     if new_slug != slug && Post::find_by_slug(&*conn, &new_slug, b.id).is_some() { | ||||
|     if new_slug != slug && Post::find_by_slug(&*conn, &new_slug, b.id).is_ok() { | ||||
|         errors.add("title", ValidationError { | ||||
|             code: Cow::from("existing_slug"), | ||||
|             message: Some(Cow::from("A post with the same title already exists.")), | ||||
| @ -197,7 +196,7 @@ pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: Lenien | ||||
|     } | ||||
| 
 | ||||
|     if errors.is_empty() { | ||||
|         if !user.is_author_in(&*conn, &b) { | ||||
|         if !user.is_author_in(&*conn, &b).expect("posts::update: is author in error") { | ||||
|             // actually it's not "Ok"…
 | ||||
|             Ok(Redirect::to(uri!(super::blogs::details: name = blog, page = _))) | ||||
|         } else { | ||||
| @ -219,29 +218,30 @@ pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: Lenien | ||||
|             post.source = form.content.clone(); | ||||
|             post.license = form.license.clone(); | ||||
|             post.cover_id = form.cover; | ||||
|             post.update(&*conn, &searcher); | ||||
|             let post = post.update_ap_url(&*conn); | ||||
|             post.update(&*conn, &searcher).expect("post::update: update error");; | ||||
|             let post = post.update_ap_url(&*conn).expect("post::update: update ap url error"); | ||||
| 
 | ||||
|             if post.published { | ||||
|                 post.update_mentions(&conn, mentions.into_iter().map(|m| Mention::build_activity(&conn, &m)).collect()); | ||||
|                 post.update_mentions(&conn, mentions.into_iter().filter_map(|m| Mention::build_activity(&conn, &m).ok()).collect()) | ||||
|                     .expect("post::update: mentions error");; | ||||
|             } | ||||
| 
 | ||||
|             let tags = form.tags.split(',').map(|t| t.trim().to_camel_case()).filter(|t| !t.is_empty()) | ||||
|                 .collect::<HashSet<_>>().into_iter().map(|t| Tag::build_activity(&conn, t)).collect::<Vec<_>>(); | ||||
|             post.update_tags(&conn, tags); | ||||
|                 .collect::<HashSet<_>>().into_iter().filter_map(|t| Tag::build_activity(&conn, t).ok()).collect::<Vec<_>>(); | ||||
|             post.update_tags(&conn, tags).expect("post::update: tags error"); | ||||
| 
 | ||||
|             let hashtags = hashtags.into_iter().map(|h| h.to_camel_case()).collect::<HashSet<_>>() | ||||
|                 .into_iter().map(|t| Tag::build_activity(&conn, t)).collect::<Vec<_>>(); | ||||
|             post.update_hashtags(&conn, hashtags); | ||||
|                 .into_iter().filter_map(|t| Tag::build_activity(&conn, t).ok()).collect::<Vec<_>>(); | ||||
|             post.update_hashtags(&conn, hashtags).expect("post::update: hashtags error"); | ||||
| 
 | ||||
|             if post.published { | ||||
|                 if newly_published { | ||||
|                     let act = post.create_activity(&conn); | ||||
|                     let dest = User::one_by_instance(&*conn); | ||||
|                     let act = post.create_activity(&conn).expect("post::update: act error"); | ||||
|                     let dest = User::one_by_instance(&*conn).expect("post::update: dest error"); | ||||
|                     worker.execute(move || broadcast(&user, act, dest)); | ||||
|                 } else { | ||||
|                     let act = post.update_activity(&*conn); | ||||
|                     let dest = User::one_by_instance(&*conn); | ||||
|                     let act = post.update_activity(&*conn).expect("post::update: act error"); | ||||
|                     let dest = User::one_by_instance(&*conn).expect("posts::update: dest error"); | ||||
|                     worker.execute(move || broadcast(&user, act, dest)); | ||||
|                 } | ||||
|             } | ||||
| @ -249,8 +249,8 @@ pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: Lenien | ||||
|             Ok(Redirect::to(uri!(details: blog = blog, slug = new_slug, responding_to = _))) | ||||
|         } | ||||
|     } else { | ||||
|         let medias = Media::for_user(&*conn, user.id); | ||||
|         let temp = render!(posts::new( | ||||
|         let medias = Media::for_user(&*conn, user.id).expect("posts:update: medias error"); | ||||
|         Err(render!(posts::new( | ||||
|             &(&*conn, &intl.catalog, Some(user)), | ||||
|             b, | ||||
|             true, | ||||
| @ -259,8 +259,7 @@ pub fn update(blog: String, slug: String, user: User, conn: DbConn, form: Lenien | ||||
|             Some(post), | ||||
|             errors.clone(), | ||||
|             medias.clone() | ||||
|         )); | ||||
|         Err(Some(temp)) | ||||
|         ))) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -288,15 +287,15 @@ pub fn valid_slug(title: &str) -> Result<(), ValidationError> { | ||||
| } | ||||
| 
 | ||||
| #[post("/~/<blog_name>/new", data = "<form>")] | ||||
| pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, conn: DbConn, worker: Worker, intl: I18n, searcher: Searcher) -> Result<Redirect, Option<Ructe>> { | ||||
|     let blog = Blog::find_by_fqn(&*conn, &blog_name).ok_or(None)?; | ||||
| pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, conn: DbConn, worker: Worker, intl: I18n, searcher: Searcher) -> Result<Redirect, Result<Ructe, ErrorPage>> { | ||||
|     let blog = Blog::find_by_fqn(&*conn, &blog_name).expect("post::create: blog error");; | ||||
|     let slug = form.title.to_string().to_kebab_case(); | ||||
| 
 | ||||
|     let mut errors = match form.validate() { | ||||
|         Ok(_) => ValidationErrors::new(), | ||||
|         Err(e) => e | ||||
|     }; | ||||
|     if Post::find_by_slug(&*conn, &slug, blog.id).is_some() { | ||||
|     if Post::find_by_slug(&*conn, &slug, blog.id).is_ok() { | ||||
|         errors.add("title", ValidationError { | ||||
|             code: Cow::from("existing_slug"), | ||||
|             message: Some(Cow::from("A post with the same title already exists.")), | ||||
| @ -305,11 +304,14 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, con | ||||
|     } | ||||
| 
 | ||||
|     if errors.is_empty() { | ||||
|         if !user.is_author_in(&*conn, &blog) { | ||||
|         if !user.is_author_in(&*conn, &blog).expect("post::create: is author in error") { | ||||
|             // actually it's not "Ok"…
 | ||||
|             Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _))) | ||||
|         } else { | ||||
|             let (content, mentions, hashtags) = utils::md_to_html(form.content.to_string().as_ref(), &Instance::get_local(&conn).expect("posts::create: Error getting l  ocal instance").public_domain); | ||||
|             let (content, mentions, hashtags) = utils::md_to_html( | ||||
|                 form.content.to_string().as_ref(), | ||||
|                 &Instance::get_local(&conn).expect("post::create: local instance error").public_domain | ||||
|             ); | ||||
| 
 | ||||
|             let post = Post::insert(&*conn, NewPost { | ||||
|                 blog_id: blog.id, | ||||
| @ -325,12 +327,12 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, con | ||||
|                 cover_id: form.cover, | ||||
|                 }, | ||||
|                 &searcher, | ||||
|             ); | ||||
|             let post = post.update_ap_url(&*conn); | ||||
|             ).expect("post::create: post save error"); | ||||
|             let post = post.update_ap_url(&*conn).expect("post::create: update ap url error"); | ||||
|             PostAuthor::insert(&*conn, NewPostAuthor { | ||||
|                 post_id: post.id, | ||||
|                 author_id: user.id | ||||
|             }); | ||||
|             }).expect("post::create: author save error"); | ||||
| 
 | ||||
|             let tags = form.tags.split(',') | ||||
|                 .map(|t| t.trim().to_camel_case()) | ||||
| @ -341,31 +343,37 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, con | ||||
|                     tag, | ||||
|                     is_hashtag: false, | ||||
|                     post_id: post.id | ||||
|                 }); | ||||
|                 }).expect("post::create: tags save error"); | ||||
|             } | ||||
|             for hashtag in hashtags { | ||||
|                 Tag::insert(&*conn, NewTag { | ||||
|                     tag: hashtag.to_camel_case(), | ||||
|                     is_hashtag: true, | ||||
|                     post_id: post.id | ||||
|                 }); | ||||
|                 }).expect("post::create: hashtags save error"); | ||||
|             } | ||||
| 
 | ||||
|             if post.published { | ||||
|                 for m in mentions { | ||||
|                     Mention::from_activity(&*conn, &Mention::build_activity(&*conn, &m), post.id, true, true); | ||||
|                     Mention::from_activity( | ||||
|                         &*conn, | ||||
|                         &Mention::build_activity(&*conn, &m).expect("post::create: mention build error"), | ||||
|                         post.id, | ||||
|                         true, | ||||
|                         true | ||||
|                     ).expect("post::create: mention save error"); | ||||
|                 } | ||||
| 
 | ||||
|                 let act = post.create_activity(&*conn); | ||||
|                 let dest = User::one_by_instance(&*conn); | ||||
|                 let act = post.create_activity(&*conn).expect("posts::create: activity error"); | ||||
|                 let dest = User::one_by_instance(&*conn).expect("posts::create: dest error"); | ||||
|                 worker.execute(move || broadcast(&user, act, dest)); | ||||
|             } | ||||
| 
 | ||||
|             Ok(Redirect::to(uri!(details: blog = blog_name, slug = slug, responding_to = _))) | ||||
|         } | ||||
|     } else { | ||||
|         let medias = Media::for_user(&*conn, user.id); | ||||
|        Err(Some(render!(posts::new( | ||||
|         let medias = Media::for_user(&*conn, user.id).expect("posts::create: medias error"); | ||||
|         Err(Ok(render!(posts::new( | ||||
|             &(&*conn, &intl.catalog, Some(user)), | ||||
|             blog, | ||||
|             false, | ||||
| @ -379,21 +387,21 @@ pub fn create(blog_name: String, form: LenientForm<NewPostForm>, user: User, con | ||||
| } | ||||
| 
 | ||||
| #[post("/~/<blog_name>/<slug>/delete")] | ||||
| pub fn delete(blog_name: String, slug: String, conn: DbConn, user: User, worker: Worker, searcher: Searcher) -> Redirect { | ||||
| pub fn delete(blog_name: String, slug: String, conn: DbConn, user: User, worker: Worker, searcher: Searcher) -> Result<Redirect, ErrorPage> { | ||||
|     let post = Blog::find_by_fqn(&*conn, &blog_name) | ||||
|         .and_then(|blog| Post::find_by_slug(&*conn, &slug, blog.id)); | ||||
| 
 | ||||
|     if let Some(post) = post { | ||||
|         if !post.get_authors(&*conn).into_iter().any(|a| a.id == user.id) { | ||||
|             Redirect::to(uri!(details: blog = blog_name.clone(), slug = slug.clone(), responding_to = _)) | ||||
|     if let Ok(post) = post { | ||||
|         if !post.get_authors(&*conn)?.into_iter().any(|a| a.id == user.id) { | ||||
|             Ok(Redirect::to(uri!(details: blog = blog_name.clone(), slug = slug.clone(), responding_to = _))) | ||||
|         } else { | ||||
|             let dest = User::one_by_instance(&*conn); | ||||
|             let delete_activity = post.delete(&(&conn, &searcher)); | ||||
|             let dest = User::one_by_instance(&*conn)?; | ||||
|             let delete_activity = post.delete(&(&conn, &searcher))?; | ||||
|             worker.execute(move || broadcast(&user, delete_activity, dest)); | ||||
| 
 | ||||
|             Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)) | ||||
|             Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _))) | ||||
|         } | ||||
|     } else { | ||||
|         Redirect::to(uri!(super::blogs::details: name = blog_name, page = _)) | ||||
|         Ok(Redirect::to(uri!(super::blogs::details: name = blog_name, page = _))) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -10,29 +10,29 @@ use plume_models::{ | ||||
|     reshares::*, | ||||
|     users::User | ||||
| }; | ||||
| use routes::errors::ErrorPage; | ||||
| use Worker; | ||||
| 
 | ||||
| #[post("/~/<blog>/<slug>/reshare")] | ||||
| pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Option<Redirect> { | ||||
| pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Result<Redirect, ErrorPage> { | ||||
|     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("/~/<blog>/<slug>/reshare", rank=1)] | ||||
|  | ||||
| @ -14,6 +14,7 @@ use plume_models::{ | ||||
|     users::{User, AUTH_COOKIE} | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| #[get("/login?<m>")] | ||||
| pub fn new(user: Option<User>, conn: DbConn, m: Option<String>, intl: I18n) -> Ructe { | ||||
|     render!(session::login( | ||||
| @ -35,30 +36,34 @@ pub struct LoginForm { | ||||
| #[post("/login", data = "<form>")] | ||||
| pub fn create(conn: DbConn, form: LenientForm<LoginForm>, flash: Option<FlashMessage>, mut cookies: Cookies, intl: I18n) -> Result<Redirect, Ructe> { | ||||
|     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()); | ||||
| 
 | ||||
|  | ||||
| @ -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/<name>?<page>")] | ||||
| pub fn tag(user: Option<User>, conn: DbConn, name: String, page: Option<Page>, intl: I18n) -> Ructe { | ||||
| pub fn tag(user: Option<User>, conn: DbConn, name: String, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> { | ||||
|     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) | ||||
|     ))) | ||||
| } | ||||
|  | ||||
| @ -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<Ructe, Ructe> { | ||||
|     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<Ructe, ErrorPage> { | ||||
|     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::<Create>() { | ||||
|             for create_act in user_clone.fetch_outbox::<Create>().expect("Remote user: outbox couldn't be fetched") { | ||||
|                 match create_act.create_props.object_object::<LicensedArticle>() { | ||||
|                     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<Ructe, ErrorPage> { | ||||
|     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<Redirect> { | ||||
| } | ||||
| 
 | ||||
| #[post("/@/<name>/follow")] | ||||
| pub fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Option<Redirect> { | ||||
| pub fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Result<Redirect, ErrorPage> { | ||||
|     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("/@/<name>/follow", rank = 2)] | ||||
| @ -165,18 +167,18 @@ pub fn follow_auth(name: String, i18n: I18n) -> Flash<Redirect> { | ||||
| } | ||||
| 
 | ||||
| #[get("/@/<name>/followers?<page>", rank = 2)] | ||||
| pub fn followers(name: String, conn: DbConn, account: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, Ructe> { | ||||
| pub fn followers(name: String, conn: DbConn, account: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> { | ||||
|     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<ActivityStream<CustomPerson>> { | ||||
|     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<User>, conn: DbConn, intl: I18n) -> Ructe { | ||||
|     render!(users::new( | ||||
| pub fn new(user: Option<User>, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> { | ||||
|     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("/@/<name>/edit")] | ||||
| pub fn edit(name: String, user: User, conn: DbConn, intl: I18n) -> Option<Ructe> { | ||||
| pub fn edit(name: String, user: User, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> { | ||||
|     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<Ructe> | ||||
|             ValidationErrors::default() | ||||
|         ))) | ||||
|     } else { | ||||
|         None | ||||
|         Err(Error::Unauthorized)? | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -235,29 +237,29 @@ pub struct UpdateUserForm { | ||||
| } | ||||
| 
 | ||||
| #[put("/@/<_name>/edit", data = "<form>")] | ||||
| pub fn update(_name: String, conn: DbConn, user: User, form: LenientForm<UpdateUserForm>) -> Redirect { | ||||
| pub fn update(_name: String, conn: DbConn, user: User, form: LenientForm<UpdateUserForm>) -> Result<Redirect, ErrorPage> { | ||||
|     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("/@/<name>/delete")] | ||||
| pub fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, searcher: Searcher) -> Option<Redirect> { | ||||
| pub fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, searcher: Searcher) -> Result<Redirect, ErrorPage> { | ||||
|     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 = "<form>")] | ||||
| pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Result<Redirect, Ructe> { | ||||
|   if !Instance::get_local(&*conn) | ||||
| @ -320,7 +332,7 @@ pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, 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<NewUserForm>, 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<NewUserForm>, intl: I18n) -> Resul | ||||
| 
 | ||||
| #[get("/@/<name>/outbox")] | ||||
| pub fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> { | ||||
|     let user = User::find_local(&*conn, &name)?; | ||||
|     Some(user.outbox(&*conn)) | ||||
|     let user = User::find_local(&*conn, &name).ok()?; | ||||
|     user.outbox(&*conn).ok() | ||||
| } | ||||
| 
 | ||||
| #[post("/@/<name>/inbox", data = "<data>")] | ||||
| @ -356,7 +368,7 @@ pub fn inbox( | ||||
|     headers: Headers, | ||||
|     searcher: Searcher, | ||||
| ) -> Result<String, Option<status::BadRequest<&'static str>>> { | ||||
|     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<ActivityStream<OrderedCollection>> { | ||||
|     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::<Vec<Id>>(); | ||||
| 
 | ||||
|     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("/@/<name>/atom.xml")] | ||||
| pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> { | ||||
|     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::<Vec<Entry>>(), | ||||
|  | ||||
| @ -35,13 +35,11 @@ impl Resolver<DbConn> for WebfingerResolver { | ||||
|     } | ||||
| 
 | ||||
|     fn find(acct: String, conn: DbConn) -> Result<Webfinger, ResolverError> { | ||||
|         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))) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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 @@ | ||||
| 
 | ||||
|     <section> | ||||
|         <figure class="media"> | ||||
|             @Html(media.html(ctx.0)) | ||||
|             @Html(media.html(ctx.0).unwrap_or(SafeString::new(""))) | ||||
|             <figcaption>@media.alt_text</figcaption> | ||||
|         </figure> | ||||
|         <div> | ||||
| @ -21,7 +22,7 @@ | ||||
|                 @i18n!(ctx.1, "Markdown syntax") | ||||
|                 <small>@i18n!(ctx.1, "Copy it into your articles, to insert this media:")</small> | ||||
|             </p> | ||||
|             <code>@media.markdown(ctx.0)</code> | ||||
|             <code>@media.markdown(ctx.0).unwrap_or(SafeString::new(""))</code> | ||||
|         </div> | ||||
|         <div> | ||||
| 	    @if media.category() == MediaCategory::Image { | ||||
|  | ||||
| @ -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 @@ | ||||
|         <div class="list"> | ||||
|             @for media in medias { | ||||
|                 <div class="card flex"> | ||||
|                     @Html(media.preview_html(ctx.0)) | ||||
|                     @Html(media.preview_html(ctx.0).unwrap_or(SafeString::new(""))) | ||||
|                     <main class="grow"> | ||||
|                         <p><a href="@uri!(medias::details: id = media.id)">@media.alt_text</a></p> | ||||
|                     </main> | ||||
|  | ||||
| @ -15,14 +15,14 @@ | ||||
|                     <h3> | ||||
|                         @if let Some(url) = notification.get_url(ctx.0) { | ||||
|                             <a href="@url"> | ||||
|                                 @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)) | ||||
|                             </a> | ||||
|                         } 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)) | ||||
|                         } | ||||
|                     </h3> | ||||
|                     @if let Some(post) = notification.get_post(ctx.0) { | ||||
|                         <p><a href="@post.url(ctx.0)">@post.title</a></p> | ||||
|                         <p><a href="@post.url(ctx.0).unwrap_or_default()">@post.title</a></p> | ||||
|                     } | ||||
|                 </main> | ||||
|                 <p><small>@notification.creation_date.format("%B %e, %H:%M")</small></p> | ||||
|  | ||||
| @ -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() { | ||||
| <div class="comment u-comment h-cite" id="comment-@comm.id"> | ||||
|     <a class="author u-author h-card" href="@uri!(user::details: name = author.get_fqn(ctx.0))"> | ||||
|         @avatar(ctx.0, &author, Size::Small, true, ctx.1) | ||||
|  | ||||
| @ -9,7 +9,7 @@ | ||||
|         <div class="cover" style="background-image: url('@Html(article.cover_url(ctx.0).unwrap_or_default())')"></div> | ||||
|     } | ||||
|     <h3 class="p-name"> | ||||
|         <a class="u-url" href="@uri!(posts::details: blog = article.get_blog(ctx.0).get_fqn(ctx.0), slug = &article.slug, responding_to = _)"> | ||||
|         <a class="u-url" href="@uri!(posts::details: blog = article.get_blog(ctx.0).unwrap().get_fqn(ctx.0), slug = &article.slug, responding_to = _)"> | ||||
|             @article.title | ||||
|         </a> | ||||
|     </h3> | ||||
| @ -19,13 +19,13 @@ | ||||
|     <p class="author"> | ||||
|         @Html(i18n!(ctx.1, "By {0}"; format!( | ||||
|             "<a class=\"p-author h-card\" href=\"{}\">{}</a>", | ||||
|             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 { | ||||
| 	⋅ <span class="dt-published" datetime="@article.creation_date.format("%F %T")">@article.creation_date.format("%B %e, %Y")</span> | ||||
|         } | ||||
|         ⋅ <a href="@uri!(blogs::details: name = article.get_blog(ctx.0).get_fqn(ctx.0), page = _)">@article.get_blog(ctx.0).title</a> | ||||
|         ⋅ <a href="@uri!(blogs::details: name = article.get_blog(ctx.0).unwrap().get_fqn(ctx.0), page = _)">@article.get_blog(ctx.0).unwrap().title</a> | ||||
|         @if !article.published { | ||||
|         ⋅ @i18n!(ctx.1, "Draft") | ||||
|         } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user