diff --git a/.travis.yml b/.travis.yml index ac83b7cc..12f3f84c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,7 +39,7 @@ jobs: name: "Test with potgresql backend" env: - MIGRATION_DIR=migrations/postgres FEATURES=postgres DATABASE_URL=postgres://postgres@localhost/plume_tests - - RUSTFLAGS='-C link-dead-code' + - RUSTFLAGS='-C link-dead-code' RUST_TEST_THREADS=1 before_script: psql -c 'create database plume_tests;' -U postgres script: - | @@ -49,7 +49,7 @@ jobs: name: "Test with Sqlite backend" env: - MIGRATION_DIR=migrations/sqlite FEATURES=sqlite DATABASE_URL=plume.sqlite3 - - RUSTFLAGS='-C link-dead-code' + - RUSTFLAGS='-C link-dead-code' RUST_TEST_THREADS=1 script: - | cargo test --features "${FEATURES}" --no-default-features --all && diff --git a/plume-common/src/activity_pub/inbox.rs b/plume-common/src/activity_pub/inbox.rs index ca64c8ba..940411ef 100644 --- a/plume-common/src/activity_pub/inbox.rs +++ b/plume-common/src/activity_pub/inbox.rs @@ -1,4 +1,4 @@ -use activitypub::{Object, activity::Create}; +use activitypub::{activity::Create, Object}; use activity_pub::Id; @@ -9,7 +9,7 @@ pub enum InboxError { #[fail(display = "Invalid activity type")] InvalidType, #[fail(display = "Couldn't undo activity")] - CantUndo + CantUndo, } pub trait FromActivity: Sized { @@ -17,7 +17,13 @@ pub trait FromActivity: Sized { fn try_from_activity(conn: &C, act: Create) -> bool { if let Ok(obj) = act.create_props.object_object() { - Self::from_activity(conn, obj, act.create_props.actor_link::().expect("FromActivity::try_from_activity: id not found error")); + Self::from_activity( + conn, + obj, + act.create_props + .actor_link::() + .expect("FromActivity::try_from_activity: id not found error"), + ); true } else { false @@ -32,7 +38,6 @@ pub trait Notify { pub trait Deletable { fn delete(&self, conn: &C) -> A; fn delete_id(id: String, actor_id: String, conn: &C); - } pub trait WithInbox { diff --git a/plume-common/src/activity_pub/mod.rs b/plume-common/src/activity_pub/mod.rs index c6e2fca6..9238fdda 100644 --- a/plume-common/src/activity_pub/mod.rs +++ b/plume-common/src/activity_pub/mod.rs @@ -1,10 +1,11 @@ -use activitypub::{Activity, Actor, Object, Link}; +use activitypub::{Activity, Actor, Link, Object}; use array_tool::vec::Uniq; use reqwest::Client; use rocket::{ - Outcome, http::Status, - response::{Response, Responder}, - request::{FromRequest, Request} + http::Status, + request::{FromRequest, Request}, + response::{Responder, Response}, + Outcome, }; use serde_json; @@ -24,7 +25,7 @@ pub fn ap_accept_header() -> Vec<&'static str> { "application/ld+json; profile=\"https://w3.org/ns/activitystreams\"", "application/ld+json;profile=\"https://w3.org/ns/activitystreams\"", "application/activity+json", - "application/ld+json" + "application/ld+json", ] } @@ -52,7 +53,7 @@ pub fn context() -> serde_json::Value { ]) } -pub struct ActivityStream (T); +pub struct ActivityStream(T); impl ActivityStream { pub fn new(t: T) -> ActivityStream { @@ -64,9 +65,11 @@ impl<'r, O: Object> Responder<'r> for ActivityStream { fn respond_to(self, request: &Request) -> Result, Status> { let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?; json["@context"] = context(); - serde_json::to_string(&json).respond_to(request).map(|r| Response::build_from(r) - .raw_header("Content-Type", "application/activity+json") - .finalize()) + serde_json::to_string(&json).respond_to(request).map(|r| { + Response::build_from(r) + .raw_header("Content-Type", "application/activity+json") + .finalize() + }) } } @@ -76,29 +79,45 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApRequest { type Error = (); fn from_request(request: &'a Request<'r>) -> Outcome { - request.headers().get_one("Accept").map(|header| header.split(",").map(|ct| match ct.trim() { - // bool for Forward: true if found a valid Content-Type for Plume first (HTML), false otherwise - "application/ld+json; profile=\"https://w3.org/ns/activitystreams\"" | - "application/ld+json;profile=\"https://w3.org/ns/activitystreams\"" | - "application/activity+json" | - "application/ld+json" => Outcome::Success(ApRequest), - "text/html" => Outcome::Forward(true), - _ => Outcome::Forward(false) - }).fold(Outcome::Forward(false), |out, ct| if out.clone().forwarded().unwrap_or(out.is_success()) { - out - } else { - ct - }).map_forward(|_| ())).unwrap_or(Outcome::Forward(())) + request + .headers() + .get_one("Accept") + .map(|header| { + header + .split(",") + .map(|ct| match ct.trim() { + // bool for Forward: true if found a valid Content-Type for Plume first (HTML), false otherwise + "application/ld+json; profile=\"https://w3.org/ns/activitystreams\"" + | "application/ld+json;profile=\"https://w3.org/ns/activitystreams\"" + | "application/activity+json" + | "application/ld+json" => Outcome::Success(ApRequest), + "text/html" => Outcome::Forward(true), + _ => Outcome::Forward(false), + }) + .fold(Outcome::Forward(false), |out, ct| { + if out.clone().forwarded().unwrap_or(out.is_success()) { + out + } else { + ct + } + }) + .map_forward(|_| ()) + }) + .unwrap_or(Outcome::Forward(())) } } -pub fn broadcast(sender: &S, act: A, to: Vec) { - let boxes = to.into_iter() +pub fn broadcast( + sender: &S, + act: A, + to: Vec, +) { + let boxes = to + .into_iter() .filter(|u| !u.is_local()) .map(|u| u.get_shared_inbox_url().unwrap_or(u.get_inbox_url())) .collect::>() .unique(); - let mut act = serde_json::to_value(act).expect("activity_pub::broadcast: serialization error"); act["@context"] = context(); let signed = act.sign(sender); @@ -121,8 +140,8 @@ pub fn broadcast(send } else { println!("Error while reading response") } - }, - Err(e) => println!("Error while sending to inbox ({:?})", e) + } + Err(e) => println!("Error while sending to inbox ({:?})", e), } } } @@ -152,7 +171,7 @@ impl Link for Id {} #[serde(rename_all = "camelCase")] pub struct ApSignature { #[activitystreams(concrete(PublicKey), functional)] - pub public_key: Option + pub public_key: Option, } #[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)] @@ -165,7 +184,7 @@ pub struct PublicKey { pub owner: Option, #[activitystreams(concrete(String), functional)] - pub public_key_pem: Option + pub public_key_pem: Option, } #[derive(Clone, Debug, Default, UnitString)] diff --git a/plume-common/src/activity_pub/request.rs b/plume-common/src/activity_pub/request.rs index 298541fc..3f13dc9d 100644 --- a/plume-common/src/activity_pub/request.rs +++ b/plume-common/src/activity_pub/request.rs @@ -1,5 +1,5 @@ use base64; -use chrono::{DateTime, offset::Utc}; +use chrono::{offset::Utc, DateTime}; use openssl::hash::{Hasher, MessageDigest}; use reqwest::header::{ACCEPT, CONTENT_TYPE, DATE, HeaderMap, HeaderValue, USER_AGENT}; use std::ops::Deref; @@ -14,35 +14,52 @@ pub struct Digest(String); impl Digest { pub fn digest(body: String) -> HeaderValue { - let mut hasher = Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error"); - hasher.update(&body.into_bytes()[..]).expect("Digest::digest: content insertion error"); + let mut hasher = + Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error"); + hasher + .update(&body.into_bytes()[..]) + .expect("Digest::digest: content insertion error"); let res = base64::encode(&hasher.finish().expect("Digest::digest: finalizing error")); - HeaderValue::from_str(&format!("SHA-256={}", res)).expect("Digest::digest: header creation error") + HeaderValue::from_str(&format!("SHA-256={}", res)) + .expect("Digest::digest: header creation error") } pub fn verify(&self, body: String) -> bool { - if self.algorithm()=="SHA-256" { - let mut hasher = Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error"); - hasher.update(&body.into_bytes()).expect("Digest::digest: content insertion error"); - self.value().deref()==hasher.finish().expect("Digest::digest: finalizing error").deref() + if self.algorithm() == "SHA-256" { + let mut hasher = + Hasher::new(MessageDigest::sha256()).expect("Digest::digest: initialization error"); + hasher + .update(&body.into_bytes()) + .expect("Digest::digest: content insertion error"); + self.value().deref() + == hasher + .finish() + .expect("Digest::digest: finalizing error") + .deref() } else { false //algorithm not supported } } pub fn algorithm(&self) -> &str { - let pos = self.0.find('=').expect("Digest::algorithm: invalid header error"); + let pos = self + .0 + .find('=') + .expect("Digest::algorithm: invalid header error"); &self.0[..pos] } pub fn value(&self) -> Vec { - let pos = self.0.find('=').expect("Digest::value: invalid header error")+1; + let pos = self + .0 + .find('=') + .expect("Digest::value: invalid header error") + 1; base64::decode(&self.0[pos..]).expect("Digest::value: invalid encoding error") } pub fn from_header(dig: &str) -> Result { if let Some(pos) = dig.find('=') { - let pos = pos+1; + let pos = pos + 1; if let Ok(_) = base64::decode(&dig[pos..]) { Ok(Digest(dig.to_owned())) } else { @@ -60,15 +77,42 @@ pub fn headers() -> HeaderMap { let mut headers = HeaderMap::new(); headers.insert(USER_AGENT, HeaderValue::from_static(PLUME_USER_AGENT)); - headers.insert(DATE, HeaderValue::from_str(&date).expect("request::headers: date error")); - headers.insert(ACCEPT, HeaderValue::from_str(&ap_accept_header().into_iter().collect::>().join(", ")).expect("request::headers: accept error")); + headers.insert( + DATE, + HeaderValue::from_str(&date).expect("request::headers: date error"), + ); + headers.insert( + ACCEPT, + HeaderValue::from_str( + &ap_accept_header() + .into_iter() + .collect::>() + .join(", "), + ).expect("request::headers: accept error"), + ); headers.insert(CONTENT_TYPE, HeaderValue::from_static(AP_CONTENT_TYPE)); headers } pub fn signature(signer: &S, headers: HeaderMap) -> HeaderValue { - let signed_string = headers.iter().map(|(h,v)| format!("{}: {}", h.as_str().to_lowercase(), v.to_str().expect("request::signature: invalid header error"))).collect::>().join("\n"); - let signed_headers = headers.iter().map(|(h,_)| h.as_str()).collect::>().join(" ").to_lowercase(); + let signed_string = headers + .iter() + .map(|(h, v)| { + format!( + "{}: {}", + h.as_str().to_lowercase(), + v.to_str() + .expect("request::signature: invalid header error") + ) + }) + .collect::>() + .join("\n"); + let signed_headers = headers + .iter() + .map(|(h, _)| h.as_str()) + .collect::>() + .join(" ") + .to_lowercase(); let data = signer.sign(signed_string); let sign = base64::encode(&data[..]); diff --git a/plume-common/src/activity_pub/sign.rs b/plume-common/src/activity_pub/sign.rs index 3abfc7d8..83d1b3b7 100644 --- a/plume-common/src/activity_pub/sign.rs +++ b/plume-common/src/activity_pub/sign.rs @@ -1,12 +1,8 @@ +use super::request; use base64; use chrono::Utc; use hex; -use openssl::{ - pkey::PKey, - rsa::Rsa, - sha::sha256 -}; -use super::request; +use openssl::{pkey::PKey, rsa::Rsa, sha::sha256}; use rocket::http::HeaderMap; use serde_json; @@ -15,14 +11,18 @@ pub fn gen_keypair() -> (Vec, Vec) { let keypair = Rsa::generate(2048).expect("sign::gen_keypair: key generation error"); let keypair = PKey::from_rsa(keypair).expect("sign::gen_keypair: parsing error"); ( - keypair.public_key_to_pem().expect("sign::gen_keypair: public key encoding error"), - keypair.private_key_to_pem_pkcs8().expect("sign::gen_keypair: private key encoding error") + keypair + .public_key_to_pem() + .expect("sign::gen_keypair: public key encoding error"), + keypair + .private_key_to_pem_pkcs8() + .expect("sign::gen_keypair: private key encoding error"), ) } pub trait Signer { fn get_key_id(&self) -> String; - + /// Sign some data with the signer keypair fn sign(&self, to_sign: String) -> Vec; /// Verify if the signature is valid @@ -30,8 +30,12 @@ pub trait Signer { } pub trait Signable { - fn sign(&mut self, creator: &T) -> &mut Self where T: Signer; - fn verify(self, creator: &T) -> bool where T: Signer; + fn sign(&mut self, creator: &T) -> &mut Self + where + T: Signer; + fn verify(self, creator: &T) -> bool + where + T: Signer; fn hash(data: String) -> String { let bytes = data.into_bytes(); @@ -48,10 +52,12 @@ impl Signable for serde_json::Value { "created": creation_date }); - let options_hash = Self::hash(json!({ + let options_hash = Self::hash( + json!({ "@context": "https://w3id.org/identity/v1", "created": creation_date - }).to_string()); + }).to_string(), + ); let document_hash = Self::hash(self.to_string()); let to_be_signed = options_hash + &document_hash; @@ -63,29 +69,34 @@ impl Signable for serde_json::Value { } fn verify(mut self, creator: &T) -> bool { - let signature_obj = if let Some(sig) = self.as_object_mut().and_then(|o| o.remove("signature")) { + let signature_obj = + if let Some(sig) = self.as_object_mut().and_then(|o| o.remove("signature")) { + sig + } else { + //signature not present + return false; + }; + let signature = if let Ok(sig) = + base64::decode(&signature_obj["signatureValue"].as_str().unwrap_or("")) + { sig } else { - //signature not present - return false - }; - let signature = if let Ok(sig) = base64::decode(&signature_obj["signatureValue"].as_str().unwrap_or("")) { - sig - } else { - return false + return false; }; let creation_date = &signature_obj["created"]; - let options_hash = Self::hash(json!({ + let options_hash = Self::hash( + json!({ "@context": "https://w3id.org/identity/v1", "created": creation_date - }).to_string()); + }).to_string(), + ); let document_hash = Self::hash(self.to_string()); let to_be_signed = options_hash + &document_hash; creator.verify(to_be_signed, signature) } } -#[derive(Debug,Copy,Clone,PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq)] pub enum SignatureValidity { Invalid, ValidNoDigest, @@ -95,14 +106,18 @@ pub enum SignatureValidity { impl SignatureValidity { pub fn is_secure(&self) -> bool { - self==&SignatureValidity::Valid + self == &SignatureValidity::Valid } } -pub fn verify_http_headers(sender: &S, all_headers: HeaderMap, data: String) -> SignatureValidity{ +pub fn verify_http_headers( + sender: &S, + all_headers: HeaderMap, + data: String, +) -> SignatureValidity { let sig_header = all_headers.get_one("Signature"); if sig_header.is_none() { - return SignatureValidity::Absent + return SignatureValidity::Absent; } let sig_header = sig_header.expect("sign::verify_http_headers: unreachable"); @@ -112,35 +127,43 @@ pub fn verify_http_headers(sender: &S, all_headers: let mut signature = None; for part in sig_header.split(',') { match part { - part if part.starts_with("keyId=") => _key_id = Some(&part[7..part.len()-1]), - part if part.starts_with("algorithm=") => _algorithm = Some(&part[11..part.len()-1]), - part if part.starts_with("headers=") => headers = Some(&part[9..part.len()-1]), - part if part.starts_with("signature=") => signature = Some(&part[11..part.len()-1]), - _ => {}, + part if part.starts_with("keyId=") => _key_id = Some(&part[7..part.len() - 1]), + part if part.starts_with("algorithm=") => _algorithm = Some(&part[11..part.len() - 1]), + part if part.starts_with("headers=") => headers = Some(&part[9..part.len() - 1]), + part if part.starts_with("signature=") => signature = Some(&part[11..part.len() - 1]), + _ => {} } } - if signature.is_none() || headers.is_none() {//missing part of the header - return SignatureValidity::Invalid + if signature.is_none() || headers.is_none() { + //missing part of the header + return SignatureValidity::Invalid; } - let headers = headers.expect("sign::verify_http_headers: unreachable").split_whitespace().collect::>(); + let headers = headers + .expect("sign::verify_http_headers: unreachable") + .split_whitespace() + .collect::>(); let signature = signature.expect("sign::verify_http_headers: unreachable"); - let h = headers.iter() - .map(|header| (header,all_headers.get_one(header))) + let h = headers + .iter() + .map(|header| (header, all_headers.get_one(header))) .map(|(header, value)| format!("{}: {}", header.to_lowercase(), value.unwrap_or(""))) - .collect::>().join("\n"); + .collect::>() + .join("\n"); if !sender.verify(h, base64::decode(signature).unwrap_or(Vec::new())) { - return SignatureValidity::Invalid + return SignatureValidity::Invalid; } - if !headers.contains(&"digest") {// signature is valid, but body content is not verified - return SignatureValidity::ValidNoDigest + if !headers.contains(&"digest") { + // signature is valid, but body content is not verified + return SignatureValidity::ValidNoDigest; } let digest = all_headers.get_one("digest").unwrap_or(""); let digest = request::Digest::from_header(digest); - if !digest.map(|d| d.verify(data)).unwrap_or(false) {// signature was valid, but body content does not match its digest + if !digest.map(|d| d.verify(data)).unwrap_or(false) { + // signature was valid, but body content does not match its digest SignatureValidity::Invalid } else { - SignatureValidity::Valid// all check passed + SignatureValidity::Valid // all check passed } } diff --git a/plume-models/src/admin.rs b/plume-models/src/admin.rs index 8d85c3bd..5acdea86 100644 --- a/plume-models/src/admin.rs +++ b/plume-models/src/admin.rs @@ -1,4 +1,8 @@ -use rocket::{Outcome, http::Status, request::{self, FromRequest, Request}}; +use rocket::{ + http::Status, + request::{self, FromRequest, Request}, + Outcome, +}; use users::User; diff --git a/plume-models/src/api_tokens.rs b/plume-models/src/api_tokens.rs index faac6ecd..d215cca0 100644 --- a/plume-models/src/api_tokens.rs +++ b/plume-models/src/api_tokens.rs @@ -1,9 +1,9 @@ use chrono::NaiveDateTime; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use rocket::{ - Outcome, http::Status, - request::{self, FromRequest, Request} + request::{self, FromRequest, Request}, + Outcome, }; use db_conn::DbConn; @@ -48,7 +48,7 @@ impl ApiToken { let full_scope = what.to_owned() + ":" + scope; for s in self.scopes.split('+') { if s == what || s == full_scope { - return true + return true; } } false diff --git a/plume-models/src/apps.rs b/plume-models/src/apps.rs old mode 100755 new mode 100644 index 5aebc6bf..ddae4814 --- a/plume-models/src/apps.rs +++ b/plume-models/src/apps.rs @@ -1,11 +1,11 @@ use canapi::{Error, Provider}; use chrono::NaiveDateTime; -use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use plume_api::apps::AppEndpoint; use plume_common::utils::random_hex; -use Connection; use schema::apps; +use Connection; #[derive(Clone, Queryable)] pub struct App { @@ -19,7 +19,7 @@ pub struct App { } #[derive(Insertable)] -#[table_name= "apps"] +#[table_name = "apps"] pub struct NewApp { pub name: String, pub client_id: String, @@ -43,13 +43,16 @@ impl Provider for App { let client_id = random_hex(); let client_secret = random_hex(); - let app = App::insert(conn, NewApp { - name: data.name, - client_id: client_id, - client_secret: client_secret, - redirect_uri: data.redirect_uri, - website: data.website, - }); + let app = App::insert( + conn, + NewApp { + name: data.name, + client_id: client_id, + client_secret: client_secret, + redirect_uri: data.redirect_uri, + website: data.website, + }, + ); Ok(AppEndpoint { id: Some(app.id), diff --git a/plume-models/src/blog_authors.rs b/plume-models/src/blog_authors.rs index 65e04969..64e82c9f 100644 --- a/plume-models/src/blog_authors.rs +++ b/plume-models/src/blog_authors.rs @@ -1,4 +1,4 @@ -use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use schema::blog_authors; diff --git a/plume-models/src/blogs.rs b/plume-models/src/blogs.rs index 0965f216..69583cd7 100644 --- a/plume-models/src/blogs.rs +++ b/plume-models/src/blogs.rs @@ -1,30 +1,31 @@ -use activitypub::{Actor, Object, CustomObject, actor::Group, collection::OrderedCollection}; +use activitypub::{actor::Group, collection::OrderedCollection, Actor, CustomObject, Object}; use chrono::NaiveDateTime; -use reqwest::{Client, - header::{ACCEPT, HeaderValue} -}; -use serde_json; -use url::Url; -use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use openssl::{ hash::MessageDigest, pkey::{PKey, Private}, rsa::Rsa, - sign::{Signer,Verifier} + sign::{Signer, Verifier}, }; +use reqwest::{ + header::{HeaderValue, ACCEPT}, + Client, +}; +use serde_json; +use url::Url; use webfinger::*; -use {BASE_URL, USE_HTTPS, Connection}; -use plume_common::activity_pub::{ - ap_accept_header, ApSignature, ActivityStream, Id, IntoId, PublicKey, - inbox::{Deletable, WithInbox}, - sign -}; -use safe_string::SafeString; use instance::*; +use plume_common::activity_pub::{ + ap_accept_header, + inbox::{Deletable, WithInbox}, + sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, +}; use posts::Post; +use safe_string::SafeString; use schema::blogs; use users::User; +use {Connection, BASE_URL, USE_HTTPS}; pub type CustomGroup = CustomObject; @@ -40,7 +41,7 @@ pub struct Blog { pub creation_date: NaiveDateTime, pub ap_url: String, pub private_key: Option, - pub public_key: String + pub public_key: String, } #[derive(Insertable)] @@ -54,7 +55,7 @@ pub struct NewBlog { pub instance_id: i32, pub ap_url: String, pub private_key: Option, - pub public_key: String + pub public_key: String, } const BLOG_PREFIX: &'static str = "~"; @@ -72,16 +73,22 @@ impl Blog { pub fn list_authors(&self, conn: &Connection) -> Vec { use schema::blog_authors; use schema::users; - let authors_ids = blog_authors::table.filter(blog_authors::blog_id.eq(self.id)).select(blog_authors::author_id); - users::table.filter(users::id.eq_any(authors_ids)) + let authors_ids = blog_authors::table + .filter(blog_authors::blog_id.eq(self.id)) + .select(blog_authors::author_id); + users::table + .filter(users::id.eq_any(authors_ids)) .load::(conn) .expect("Blog::list_authors: author loading error") } - pub fn find_for_author(conn: &Connection, author_id: i32) -> Vec { + pub fn find_for_author(conn: &Connection, author: &User) -> Vec { use schema::blog_authors; - let author_ids = blog_authors::table.filter(blog_authors::author_id.eq(author_id)).select(blog_authors::blog_id); - blogs::table.filter(blogs::id.eq_any(author_ids)) + let author_ids = blog_authors::table + .filter(blog_authors::author_id.eq(author.id)) + .select(blog_authors::blog_id); + blogs::table + .filter(blogs::id.eq_any(author_ids)) .load::(conn) .expect("Blog::find_for_author: blog loading error") } @@ -91,24 +98,49 @@ impl Blog { } pub fn find_by_fqn(conn: &Connection, fqn: String) -> Option { - if fqn.contains("@") { // remote blog - match Instance::find_by_domain(conn, String::from(fqn.split("@").last().expect("Blog::find_by_fqn: unreachable"))) { - Some(instance) => { - match Blog::find_by_name(conn, String::from(fqn.split("@").nth(0).expect("Blog::find_by_fqn: unreachable")), instance.id) { - Some(u) => Some(u), - None => Blog::fetch_from_webfinger(conn, fqn) - } + if fqn.contains("@") { + // remote blog + match Instance::find_by_domain( + conn, + String::from( + fqn.split("@") + .last() + .expect("Blog::find_by_fqn: unreachable"), + ), + ) { + Some(instance) => match Blog::find_by_name( + conn, + String::from( + 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) + None => Blog::fetch_from_webfinger(conn, fqn), } - } else { // local blog + } else { + // local blog Blog::find_local(conn, fqn) } } fn fetch_from_webfinger(conn: &Connection, acct: String) -> Option { match resolve(acct.clone(), *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"))), + 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 @@ -119,17 +151,37 @@ impl Blog { fn fetch_from_url(conn: &Connection, url: String) -> Option { let req = Client::new() .get(&url[..]) - .header(ACCEPT, HeaderValue::from_str(&ap_accept_header().into_iter().collect::>().join(", ")).expect("Blog::fetch_from_url: accept_header generation error")) + .header( + ACCEPT, + HeaderValue::from_str( + &ap_accept_header() + .into_iter() + .collect::>() + .join(", "), + ).expect("Blog::fetch_from_url: accept_header generation error"), + ) .send(); match req { Ok(mut res) => { - let text = &res.text().expect("Blog::fetch_from_url: body reading error"); - let ap_sign: ApSignature = serde_json::from_str(text).expect("Blog::fetch_from_url: body parsing error"); - let mut json: CustomGroup = serde_json::from_str(text).expect("Blog::fetch_from_url: body parsing error"); + 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.as_ref()).expect("Blog::fetch_from_url: url parsing error").host_str().expect("Blog::fetch_from_url: host extraction error").to_string())) - }, - Err(_) => None + Some(Blog::from_activity( + conn, + json, + Url::parse(url.as_ref()) + .expect("Blog::fetch_from_url: url parsing error") + .host_str() + .expect("Blog::fetch_from_url: host extraction error") + .to_string(), + )) + } + Err(_) => None, } } @@ -137,49 +189,103 @@ impl Blog { let instance = match Instance::find_by_domain(conn, inst.clone()) { Some(instance) => instance, None => { - Instance::insert(conn, NewInstance { - public_domain: inst.clone(), - name: inst.clone(), - 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() - }) + Instance::insert( + conn, + NewInstance { + public_domain: inst.clone(), + name: inst.clone(), + 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"), - title: acct.object.object_props.name_string().expect("Blog::from_activity: name error"), - outbox_url: acct.object.ap_actor_props.outbox_string().expect("Blog::from_activity: outbox error"), - inbox_url: acct.object.ap_actor_props.inbox_string().expect("Blog::from_activity: inbox error"), - summary: acct.object.object_props.summary_string().expect("Blog::from_activity: summary error"), - instance_id: instance.id, - ap_url: acct.object.object_props.id_string().expect("Blog::from_activity: id error"), - 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"), - private_key: None - }) + Blog::insert( + conn, + NewBlog { + actor_id: acct + .object + .ap_actor_props + .preferred_username_string() + .expect("Blog::from_activity: preferredUsername error"), + title: acct + .object + .object_props + .name_string() + .expect("Blog::from_activity: name error"), + outbox_url: acct + .object + .ap_actor_props + .outbox_string() + .expect("Blog::from_activity: outbox error"), + inbox_url: acct + .object + .ap_actor_props + .inbox_string() + .expect("Blog::from_activity: inbox error"), + summary: acct + .object + .object_props + .summary_string() + .expect("Blog::from_activity: summary error"), + instance_id: instance.id, + ap_url: acct + .object + .object_props + .id_string() + .expect("Blog::from_activity: id error"), + 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"), + private_key: None, + }, + ) } pub fn into_activity(&self, _conn: &Connection) -> CustomGroup { let mut blog = Group::default(); - blog.ap_actor_props.set_preferred_username_string(self.actor_id.clone()).expect("Blog::into_activity: preferredUsername error"); - blog.object_props.set_name_string(self.title.clone()).expect("Blog::into_activity: name error"); - blog.ap_actor_props.set_outbox_string(self.outbox_url.clone()).expect("Blog::into_activity: outbox error"); - blog.ap_actor_props.set_inbox_string(self.inbox_url.clone()).expect("Blog::into_activity: inbox error"); - blog.object_props.set_summary_string(self.summary.clone()).expect("Blog::into_activity: summary error"); - blog.object_props.set_id_string(self.ap_url.clone()).expect("Blog::into_activity: id error"); + blog.ap_actor_props + .set_preferred_username_string(self.actor_id.clone()) + .expect("Blog::into_activity: preferredUsername error"); + blog.object_props + .set_name_string(self.title.clone()) + .expect("Blog::into_activity: name error"); + blog.ap_actor_props + .set_outbox_string(self.outbox_url.clone()) + .expect("Blog::into_activity: outbox error"); + blog.ap_actor_props + .set_inbox_string(self.inbox_url.clone()) + .expect("Blog::into_activity: inbox error"); + blog.object_props + .set_summary_string(self.summary.clone()) + .expect("Blog::into_activity: summary error"); + blog.object_props + .set_id_string(self.ap_url.clone()) + .expect("Blog::into_activity: id error"); let mut public_key = PublicKey::default(); - public_key.set_id_string(format!("{}#main-key", self.ap_url)).expect("Blog::into_activity: publicKey.id error"); - public_key.set_owner_string(self.ap_url.clone()).expect("Blog::into_activity: publicKey.owner error"); - public_key.set_public_key_pem_string(self.public_key.clone()).expect("Blog::into_activity: publicKey.publicKeyPem error"); + public_key + .set_id_string(format!("{}#main-key", self.ap_url)) + .expect("Blog::into_activity: publicKey.id error"); + public_key + .set_owner_string(self.ap_url.clone()) + .expect("Blog::into_activity: publicKey.owner error"); + public_key + .set_public_key_pem_string(self.public_key.clone()) + .expect("Blog::into_activity: publicKey.publicKeyPem error"); let mut ap_signature = ApSignature::default(); - ap_signature.set_public_key_publickey(public_key).expect("Blog::into_activity: publicKey error"); + ap_signature + .set_public_key_publickey(public_key) + .expect("Blog::into_activity: publicKey error"); CustomGroup::new(blog, ap_signature) } @@ -188,27 +294,41 @@ impl Blog { let instance = self.get_instance(conn); if self.outbox_url.len() == 0 { diesel::update(self) - .set(blogs::outbox_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), "outbox"))) - .execute(conn).expect("Blog::update_boxes: outbox update error"); + .set(blogs::outbox_url.eq(instance.compute_box( + BLOG_PREFIX, + self.actor_id.clone(), + "outbox", + ))) + .execute(conn) + .expect("Blog::update_boxes: outbox update error"); } if self.inbox_url.len() == 0 { diesel::update(self) - .set(blogs::inbox_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), "inbox"))) - .execute(conn).expect("Blog::update_boxes: inbox update error"); + .set(blogs::inbox_url.eq(instance.compute_box( + BLOG_PREFIX, + self.actor_id.clone(), + "inbox", + ))) + .execute(conn) + .expect("Blog::update_boxes: inbox update error"); } if self.ap_url.len() == 0 { diesel::update(self) .set(blogs::ap_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), ""))) - .execute(conn).expect("Blog::update_boxes: ap_url update error"); + .execute(conn) + .expect("Blog::update_boxes: ap_url update error"); } } pub fn outbox(&self, conn: &Connection) -> ActivityStream { 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.set_total_items_u64(self.get_activities(conn).len() as u64).expect("Blog::outbox: count serialization error"); + coll.collection_props.items = serde_json::to_value(self.get_activities(conn)) + .expect("Blog::outbox: activity serialization error"); + coll.collection_props + .set_total_items_u64(self.get_activities(conn).len() as u64) + .expect("Blog::outbox: count serialization error"); ActivityStream::new(coll) } @@ -217,35 +337,48 @@ impl Blog { } pub fn get_keypair(&self) -> PKey { - PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.clone().expect("Blog::get_keypair: private key not found error").as_ref()) - .expect("Blog::get_keypair: pem parsing error")) - .expect("Blog::get_keypair: private key deserialization error") + PKey::from_rsa( + Rsa::private_key_from_pem( + self.private_key + .clone() + .expect("Blog::get_keypair: private key not found error") + .as_ref(), + ).expect("Blog::get_keypair: pem parsing error"), + ).expect("Blog::get_keypair: private key deserialization error") } pub fn webfinger(&self, conn: &Connection) -> Webfinger { Webfinger { - subject: format!("acct:{}@{}", self.actor_id, self.get_instance(conn).public_domain), + subject: format!( + "acct:{}@{}", + self.actor_id, + self.get_instance(conn).public_domain + ), aliases: vec![self.ap_url.clone()], links: vec![ Link { rel: String::from("http://webfinger.net/rel/profile-page"), mime_type: None, href: Some(self.ap_url.clone()), - template: None + template: None, }, 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(BLOG_PREFIX, self.actor_id.clone(), "feed.atom")), - template: None + href: Some(self.get_instance(conn).compute_box( + BLOG_PREFIX, + self.actor_id.clone(), + "feed.atom", + )), + template: None, }, Link { rel: String::from("self"), mime_type: Some(String::from("application/activity+json")), href: Some(self.ap_url.clone()), - template: None - } - ] + template: None, + }, + ], } } @@ -253,7 +386,11 @@ impl Blog { Blog::find_by_ap_url(conn, url.clone()).or_else(|| { // The requested blog was not in the DB // We try to fetch it if it is remote - if Url::parse(url.as_ref()).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.as_ref()) + .expect("Blog::from_url: ap_url parsing error") + .host_str() + .expect("Blog::from_url: host extraction error") != BASE_URL.as_str() + { Blog::fetch_from_url(conn, url) } else { None @@ -265,7 +402,11 @@ impl Blog { if self.instance_id == Instance::local_id(conn) { self.actor_id.clone() } else { - format!("{}@{}", self.actor_id, self.get_instance(conn).public_domain) + format!( + "{}@{}", + self.actor_id, + self.get_instance(conn).public_domain + ) } } @@ -277,9 +418,11 @@ impl Blog { pub fn delete(&self, conn: &Connection) { for post in Post::get_for_blog(conn, &self) { - post.delete(conn); + post.delete(conn); } - diesel::delete(self).execute(conn).expect("Blog::delete: blog deletion error"); + diesel::delete(self) + .execute(conn) + .expect("Blog::delete: blog deletion error"); } } @@ -313,17 +456,29 @@ impl sign::Signer for Blog { fn sign(&self, to_sign: String) -> Vec { let key = self.get_keypair(); - let mut signer = Signer::new(MessageDigest::sha256(), &key).expect("Blog::sign: initialization error"); - signer.update(to_sign.as_bytes()).expect("Blog::sign: content insertion error"); - signer.sign_to_vec().expect("Blog::sign: finalization error") + let mut signer = + Signer::new(MessageDigest::sha256(), &key).expect("Blog::sign: initialization error"); + signer + .update(to_sign.as_bytes()) + .expect("Blog::sign: content insertion error"); + signer + .sign_to_vec() + .expect("Blog::sign: finalization error") } fn verify(&self, data: String, signature: Vec) -> 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"); - verifier.update(data.as_bytes()).expect("Blog::verify: content insertion error"); - verifier.verify(&signature).expect("Blog::verify: finalization error") + 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"); + verifier + .update(data.as_bytes()) + .expect("Blog::verify: content insertion error"); + verifier + .verify(&signature) + .expect("Blog::verify: finalization error") } } @@ -332,7 +487,7 @@ impl NewBlog { actor_id: String, title: String, summary: String, - instance_id: i32 + instance_id: i32, ) -> NewBlog { let (pub_key, priv_key) = sign::gen_keypair(); NewBlog { @@ -344,7 +499,337 @@ impl NewBlog { instance_id: 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")) + private_key: Some( + String::from_utf8(priv_key).expect("NewBlog::new_local: private key error"), + ), } } } + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use blog_authors::*; + use diesel::Connection; + use instance::tests as instance_tests; + use tests::db; + use users::tests as usersTests; + use Connection as Conn; + + pub(crate) fn fill_database(conn: &Conn) -> Vec { + instance_tests::fill_database(conn); + let users = usersTests::fill_database(conn); + let blogs = vec![ + NewBlog::new_local( + "BlogName".to_owned(), + "Blog name".to_owned(), + "This is a small blog".to_owned(), + Instance::local_id(conn), + ), + NewBlog::new_local( + "MyBlog".to_owned(), + "My blog".to_owned(), + "Welcome to my blog".to_owned(), + Instance::local_id(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), + ), + ].into_iter() + .map(|nb| Blog::insert(conn, nb)) + .collect::>(); + + BlogAuthor::insert( + conn, + NewBlogAuthor { + blog_id: blogs[0].id, + author_id: users[0].id, + is_owner: true, + }, + ); + + BlogAuthor::insert( + conn, + NewBlogAuthor { + blog_id: blogs[0].id, + author_id: users[1].id, + is_owner: false, + }, + ); + + BlogAuthor::insert( + conn, + NewBlogAuthor { + blog_id: blogs[1].id, + author_id: users[1].id, + is_owner: true, + }, + ); + + BlogAuthor::insert( + conn, + NewBlogAuthor { + blog_id: blogs[2].id, + author_id: users[2].id, + is_owner: true, + }, + ); + blogs + } + + #[test] + fn get_instance() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + fill_database(conn); + + let blog = Blog::insert( + conn, + NewBlog::new_local( + "SomeName".to_owned(), + "Some name".to_owned(), + "This is some blog".to_owned(), + Instance::local_id(conn), + ), + ); + + assert_eq!(blog.get_instance(conn).id, Instance::local_id(conn)); + // TODO add tests for remote instance + + Ok(()) + }); + } + + #[test] + fn authors() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let user = usersTests::fill_database(conn); + fill_database(conn); + + let blog = vec![ + Blog::insert( + conn, + NewBlog::new_local( + "SomeName".to_owned(), + "Some name".to_owned(), + "This is some blog".to_owned(), + Instance::local_id(conn), + ), + ), + Blog::insert( + conn, + NewBlog::new_local( + "Blog".to_owned(), + "Blog".to_owned(), + "I've named my blog Blog".to_owned(), + Instance::local_id(conn), + ), + ), + ]; + + BlogAuthor::insert( + conn, + NewBlogAuthor { + blog_id: blog[0].id, + author_id: user[0].id, + is_owner: true, + }, + ); + + BlogAuthor::insert( + conn, + NewBlogAuthor { + blog_id: blog[0].id, + author_id: user[1].id, + is_owner: false, + }, + ); + + BlogAuthor::insert( + conn, + NewBlogAuthor { + blog_id: blog[1].id, + author_id: user[0].id, + is_owner: true, + }, + ); + + assert!( + blog[0] + .list_authors(conn) + .iter() + .any(|a| a.id == user[0].id) + ); + assert!( + blog[0] + .list_authors(conn) + .iter() + .any(|a| a.id == user[1].id) + ); + assert!( + blog[1] + .list_authors(conn) + .iter() + .any(|a| a.id == user[0].id) + ); + assert!( + !blog[1] + .list_authors(conn) + .iter() + .any(|a| a.id == user[1].id) + ); + + assert!( + Blog::find_for_author(conn, &user[0]) + .iter() + .any(|b| b.id == blog[0].id) + ); + assert!( + Blog::find_for_author(conn, &user[1]) + .iter() + .any(|b| b.id == blog[0].id) + ); + assert!( + Blog::find_for_author(conn, &user[0]) + .iter() + .any(|b| b.id == blog[1].id) + ); + assert!( + !Blog::find_for_author(conn, &user[1]) + .iter() + .any(|b| b.id == blog[1].id) + ); + + Ok(()) + }); + } + + #[test] + fn find_local() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + fill_database(conn); + + let blog = Blog::insert( + conn, + NewBlog::new_local( + "SomeName".to_owned(), + "Some name".to_owned(), + "This is some blog".to_owned(), + Instance::local_id(conn), + ), + ); + + assert_eq!( + Blog::find_local(conn, "SomeName".to_owned()).unwrap().id, + blog.id + ); + + Ok(()) + }); + } + + #[test] + fn get_fqn() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + fill_database(conn); + + let blog = Blog::insert( + conn, + NewBlog::new_local( + "SomeName".to_owned(), + "Some name".to_owned(), + "This is some blog".to_owned(), + Instance::local_id(conn), + ), + ); + + assert_eq!(blog.get_fqn(conn), "SomeName"); + + Ok(()) + }); + } + + #[test] + fn delete() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let blogs = fill_database(conn); + + blogs[0].delete(conn); + assert!(Blog::get(conn, blogs[0].id).is_none()); + + Ok(()) + }); + } + + #[test] + fn delete_via_user() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let user = usersTests::fill_database(conn); + fill_database(conn); + + let blog = vec![ + Blog::insert( + conn, + NewBlog::new_local( + "SomeName".to_owned(), + "Some name".to_owned(), + "This is some blog".to_owned(), + Instance::local_id(conn), + ), + ), + Blog::insert( + conn, + NewBlog::new_local( + "Blog".to_owned(), + "Blog".to_owned(), + "I've named my blog Blog".to_owned(), + Instance::local_id(conn), + ), + ), + ]; + + BlogAuthor::insert( + conn, + NewBlogAuthor { + blog_id: blog[0].id, + author_id: user[0].id, + is_owner: true, + }, + ); + + BlogAuthor::insert( + conn, + NewBlogAuthor { + blog_id: blog[0].id, + author_id: user[1].id, + is_owner: false, + }, + ); + + BlogAuthor::insert( + conn, + NewBlogAuthor { + blog_id: blog[1].id, + author_id: user[0].id, + is_owner: true, + }, + ); + + user[0].delete(conn); + assert!(Blog::get(conn, blog[0].id).is_some()); + assert!(Blog::get(conn, blog[1].id).is_none()); + user[1].delete(conn); + assert!(Blog::get(conn, blog[0].id).is_none()); + + Ok(()) + }); + } +} diff --git a/plume-models/src/comments.rs b/plume-models/src/comments.rs index d70908f7..a9d3488c 100644 --- a/plume-models/src/comments.rs +++ b/plume-models/src/comments.rs @@ -1,25 +1,21 @@ -use activitypub::{ - activity::Create, - link, - object::{Note} -}; +use activitypub::{activity::Create, link, object::Note}; use chrono::{self, NaiveDateTime}; -use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use serde_json; -use plume_common::activity_pub::{ - Id, IntoId, PUBLIC_VISIBILTY, - inbox::{FromActivity, Notify} -}; -use plume_common::utils; -use Connection; use instance::Instance; use mentions::Mention; use notifications::*; +use plume_common::activity_pub::{ + inbox::{FromActivity, Notify}, + Id, IntoId, PUBLIC_VISIBILTY, +}; +use plume_common::utils; use posts::Post; -use users::User; -use schema::comments; use safe_string::SafeString; +use schema::comments; +use users::User; +use Connection; #[derive(Queryable, Identifiable, Serialize, Clone)] pub struct Comment { @@ -31,7 +27,7 @@ pub struct Comment { pub creation_date: NaiveDateTime, pub ap_url: Option, pub sensitive: bool, - pub spoiler_text: String + pub spoiler_text: String, } #[derive(Insertable, Default)] @@ -43,7 +39,7 @@ pub struct NewComment { pub author_id: i32, pub ap_url: Option, pub sensitive: bool, - pub spoiler_text: String + pub spoiler_text: String, } impl Comment { @@ -62,24 +58,35 @@ impl Comment { pub fn count_local(conn: &Connection) -> usize { use schema::users; - let local_authors = users::table.filter(users::instance_id.eq(Instance::local_id(conn))).select(users::id); - comments::table.filter(comments::author_id.eq_any(local_authors)) + let local_authors = users::table + .filter(users::instance_id.eq(Instance::local_id(conn))) + .select(users::id); + comments::table + .filter(comments::author_id.eq_any(local_authors)) .load::(conn) .expect("Comment::count_local: loading error") - .len()// TODO count in database? + .len() // TODO count in database? } pub fn to_json(&self, conn: &Connection, others: &Vec) -> serde_json::Value { let mut json = serde_json::to_value(self).expect("Comment::to_json: serialization error"); json["author"] = self.get_author(conn).to_json(conn); - let mentions = Mention::list_for_comment(conn, self.id).into_iter() - .map(|m| m.get_mentioned(conn).map(|u| u.get_fqn(conn)).unwrap_or(String::new())) + let mentions = Mention::list_for_comment(conn, self.id) + .into_iter() + .map(|m| { + m.get_mentioned(conn) + .map(|u| u.get_fqn(conn)) + .unwrap_or(String::new()) + }) .collect::>(); json["mentions"] = serde_json::to_value(mentions).expect("Comment::to_json: mention error"); - json["responses"] = json!(others.into_iter() - .filter(|c| c.in_response_to_id.map(|id| id == self.id).unwrap_or(false)) - .map(|c| c.to_json(conn, others)) - .collect::>()); + json["responses"] = json!( + others + .into_iter() + .filter(|c| c.in_response_to_id.map(|id| id == self.id).unwrap_or(false)) + .map(|c| c.to_json(conn, others)) + .collect::>() + ); json } @@ -106,61 +113,138 @@ impl Comment { 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(String::new())).expect("Comment::into_activity: id error"); - note.object_props.set_summary_string(self.spoiler_text.clone()).expect("Comment::into_activity: summary error"); - note.object_props.set_content_string(html).expect("Comment::into_activity: content error"); - 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::into_activity: post error").ap_url, |id| { - let comm = Comment::get(conn, id).expect("Comment::into_activity: comment error"); - comm.ap_url.clone().unwrap_or(comm.compute_id(conn)) - }))).expect("Comment::into_activity: in_reply_to error"); - note.object_props.set_published_string(chrono::Utc::now().to_rfc3339()).expect("Comment::into_activity: published error"); - note.object_props.set_attributed_to_link(author.clone().into_id()).expect("Comment::into_activity: attributed_to error"); - note.object_props.set_to_link_vec(to.clone()).expect("Comment::into_activity: to error"); - note.object_props.set_tag_link_vec(mentions.into_iter().map(|m| Mention::build_activity(conn, m)).collect::>()) + note.object_props + .set_id_string(self.ap_url.clone().unwrap_or(String::new())) + .expect("Comment::into_activity: id error"); + note.object_props + .set_summary_string(self.spoiler_text.clone()) + .expect("Comment::into_activity: summary error"); + note.object_props + .set_content_string(html) + .expect("Comment::into_activity: content error"); + 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::into_activity: post error") + .ap_url + }, + |id| { + let comm = + Comment::get(conn, id).expect("Comment::into_activity: comment error"); + comm.ap_url.clone().unwrap_or(comm.compute_id(conn)) + }, + ))) + .expect("Comment::into_activity: in_reply_to error"); + note.object_props + .set_published_string(chrono::Utc::now().to_rfc3339()) + .expect("Comment::into_activity: published error"); + note.object_props + .set_attributed_to_link(author.clone().into_id()) + .expect("Comment::into_activity: attributed_to error"); + note.object_props + .set_to_link_vec(to.clone()) + .expect("Comment::into_activity: to error"); + note.object_props + .set_tag_link_vec( + mentions + .into_iter() + .map(|m| Mention::build_activity(conn, m)) + .collect::>(), + ) .expect("Comment::into_activity: tag error"); note } pub fn create_activity(&self, conn: &Connection) -> Create { - let author = User::get(conn, self.author_id).expect("Comment::create_activity: author error"); + let author = + User::get(conn, self.author_id).expect("Comment::create_activity: author error"); let note = self.into_activity(conn); let mut act = Create::default(); - act.create_props.set_actor_link(author.into_id()).expect("Comment::create_activity: actor error"); - act.create_props.set_object_object(note.clone()).expect("Comment::create_activity: object error"); - 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"); - act.object_props.set_to_link_vec(note.object_props.to_link_vec::().expect("Comment::create_activity: id error")).expect("Comment::create_activity: to error"); - act.object_props.set_cc_link_vec::(vec![]).expect("Comment::create_activity: cc error"); + act.create_props + .set_actor_link(author.into_id()) + .expect("Comment::create_activity: actor error"); + act.create_props + .set_object_object(note.clone()) + .expect("Comment::create_activity: object error"); + 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"); + act.object_props + .set_to_link_vec( + note.object_props + .to_link_vec::() + .expect("Comment::create_activity: id error"), + ) + .expect("Comment::create_activity: to error"); + act.object_props + .set_cc_link_vec::(vec![]) + .expect("Comment::create_activity: cc error"); act } } impl FromActivity for Comment { fn from_activity(conn: &Connection, note: Note, actor: Id) -> Comment { - let previous_url = note.object_props.in_reply_to.clone().expect("Comment::from_activity: not an answer error").as_str().expect("Comment::from_activity: in_reply_to parsing error").to_string(); + let previous_url = note + .object_props + .in_reply_to + .clone() + .expect("Comment::from_activity: not an answer error") + .as_str() + .expect("Comment::from_activity: in_reply_to parsing error") + .to_string(); let previous_comment = Comment::find_by_ap_url(conn, previous_url.clone()); - let comm = Comment::insert(conn, NewComment { - content: SafeString::new(¬e.object_props.content_string().expect("Comment::from_activity: content deserialization error")), - spoiler_text: note.object_props.summary_string().unwrap_or(String::from("")), - 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.clone().into()).expect("Comment::from_activity: author error").id, - sensitive: false // "sensitive" is not a standard property, we need to think about how to support it with the activitypub crate - }); + let comm = Comment::insert( + conn, + NewComment { + content: SafeString::new( + ¬e + .object_props + .content_string() + .expect("Comment::from_activity: content deserialization error"), + ), + spoiler_text: note + .object_props + .summary_string() + .unwrap_or(String::from("")), + 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.clone().into()) + .expect("Comment::from_activity: author error") + .id, + sensitive: false, // "sensitive" is not a standard property, we need to think about how to support it with the activitypub crate + }, + ); // save mentions if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() { for tag in tags.into_iter() { serde_json::from_value::(tag) .map(|m| { - let author = &Post::get(conn, comm.post_id).expect("Comment::from_activity: error").get_authors(conn)[0]; - let not_author = m.link_props.href_string().expect("Comment::from_activity: no href error") != author.ap_url.clone(); + let author = &Post::get(conn, comm.post_id) + .expect("Comment::from_activity: error") + .get_authors(conn)[0]; + let not_author = m + .link_props + .href_string() + .expect("Comment::from_activity: no href error") + != author.ap_url.clone(); Mention::from_activity(conn, m, comm.id, false, not_author) - }).ok(); + }) + .ok(); } } @@ -172,11 +256,14 @@ impl FromActivity for Comment { impl Notify for Comment { fn notify(&self, conn: &Connection) { for author in self.get_post(conn).get_authors(conn) { - Notification::insert(conn, NewNotification { - kind: notification_kind::COMMENT.to_string(), - object_id: self.id, - user_id: author.id - }); + Notification::insert( + conn, + NewNotification { + kind: notification_kind::COMMENT.to_string(), + object_id: self.id, + user_id: author.id, + }, + ); } } } diff --git a/plume-models/src/db_conn.rs b/plume-models/src/db_conn.rs index eef62825..a17ac33f 100644 --- a/plume-models/src/db_conn.rs +++ b/plume-models/src/db_conn.rs @@ -1,7 +1,9 @@ -use diesel::{ - r2d2::{ConnectionManager, Pool, PooledConnection} +use diesel::r2d2::{ConnectionManager, Pool, PooledConnection}; +use rocket::{ + http::Status, + request::{self, FromRequest}, + Outcome, Request, State, }; -use rocket::{Request, State, Outcome, http::Status, request::{self, FromRequest}}; use std::ops::Deref; use Connection; @@ -23,7 +25,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for DbConn { let pool = request.guard::>()?; match pool.get() { Ok(conn) => Outcome::Success(DbConn(conn)), - Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())) + Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())), } } } diff --git a/plume-models/src/follows.rs b/plume-models/src/follows.rs index f2f22ef5..25c6e6a1 100644 --- a/plume-models/src/follows.rs +++ b/plume-models/src/follows.rs @@ -1,12 +1,21 @@ -use activitypub::{Actor, activity::{Accept, Follow as FollowAct, Undo}, actor::Person}; +use activitypub::{ + activity::{Accept, Follow as FollowAct, Undo}, + actor::Person, + Actor, +}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; -use plume_common::activity_pub::{broadcast, Id, IntoId, inbox::{FromActivity, Notify, WithInbox, Deletable}, sign::Signer}; -use {BASE_URL, ap_url, Connection}; use blogs::Blog; use notifications::*; -use users::User; +use plume_common::activity_pub::{ + broadcast, + inbox::{Deletable, FromActivity, Notify, WithInbox}, + sign::Signer, + Id, IntoId, +}; use schema::follows; +use users::User; +use {ap_url, Connection, BASE_URL}; #[derive(Clone, Queryable, Identifiable, Associations)] #[belongs_to(User, foreign_key = "following_id")] @@ -31,22 +40,35 @@ impl Follow { find_by!(follows, find_by_ap_url, ap_url as String); pub fn find(conn: &Connection, from: i32, to: i32) -> Option { - follows::table.filter(follows::follower_id.eq(from)) + follows::table + .filter(follows::follower_id.eq(from)) .filter(follows::following_id.eq(to)) .get_result(conn) .ok() } pub fn into_activity(&self, conn: &Connection) -> FollowAct { - let user = User::get(conn, self.follower_id).expect("Follow::into_activity: actor not found error"); - let target = User::get(conn, self.following_id).expect("Follow::into_activity: target not found error"); + let user = User::get(conn, self.follower_id) + .expect("Follow::into_activity: actor not found error"); + let target = User::get(conn, self.following_id) + .expect("Follow::into_activity: target not found error"); let mut act = FollowAct::default(); - act.follow_props.set_actor_link::(user.clone().into_id()).expect("Follow::into_activity: actor error"); - act.follow_props.set_object_object(user.into_activity(&*conn)).expect("Follow::into_activity: object error"); - act.object_props.set_id_string(self.ap_url.clone()).expect("Follow::into_activity: id error"); - act.object_props.set_to_link(target.clone().into_id()).expect("Follow::into_activity: target error"); - act.object_props.set_cc_link_vec::(vec![]).expect("Follow::into_activity: cc error"); + act.follow_props + .set_actor_link::(user.clone().into_id()) + .expect("Follow::into_activity: actor error"); + act.follow_props + .set_object_object(user.into_activity(&*conn)) + .expect("Follow::into_activity: object error"); + act.object_props + .set_id_string(self.ap_url.clone()) + .expect("Follow::into_activity: id error"); + act.object_props + .set_to_link(target.clone().into_id()) + .expect("Follow::into_activity: target error"); + act.object_props + .set_cc_link_vec::(vec![]) + .expect("Follow::into_activity: cc error"); act } @@ -58,23 +80,41 @@ impl Follow { target: &A, follow: FollowAct, from_id: i32, - target_id: i32 + target_id: i32, ) -> Follow { let from_url: String = from.clone().into_id().into(); let target_url: String = target.clone().into_id().into(); - let res = Follow::insert(conn, NewFollow { - follower_id: from_id, - following_id: target_id, - ap_url: format!("{}/follow/{}", from_url, target_url), - }); + let res = Follow::insert( + conn, + NewFollow { + follower_id: from_id, + following_id: target_id, + ap_url: format!("{}/follow/{}", from_url, target_url), + }, + ); 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: id error"); - accept.object_props.set_to_link(from.clone().into_id()).expect("Follow::accept_follow: to error"); - accept.object_props.set_cc_link_vec::(vec![]).expect("Follow::accept_follow: cc error"); - accept.accept_props.set_actor_link::(target.clone().into_id()).expect("Follow::accept_follow: actor error"); - accept.accept_props.set_object_object(follow).expect("Follow::accept_follow: object error"); + accept + .object_props + .set_id_string(accept_id) + .expect("Follow::accept_follow: id error"); + accept + .object_props + .set_to_link(from.clone().into_id()) + .expect("Follow::accept_follow: to error"); + accept + .object_props + .set_cc_link_vec::(vec![]) + .expect("Follow::accept_follow: cc error"); + accept + .accept_props + .set_actor_link::(target.clone().into_id()) + .expect("Follow::accept_follow: actor error"); + accept + .accept_props + .set_object_object(follow) + .expect("Follow::accept_follow: object error"); broadcast(&*target, accept, vec![from.clone()]); res } @@ -82,14 +122,41 @@ impl Follow { impl FromActivity for Follow { fn from_activity(conn: &Connection, follow: FollowAct, _actor: Id) -> Follow { - let from_id = follow.follow_props.actor_link::().map(|l| l.into()) - .unwrap_or_else(|_| follow.follow_props.actor_object::().expect("Follow::from_activity: actor not found error").object_props.id_string().expect("Follow::from_activity: actor not found error")); - let from = User::from_url(conn, from_id).expect("Follow::from_activity: actor not found error"); - match User::from_url(conn, follow.follow_props.object.as_str().expect("Follow::from_activity: target url parsing error").to_string()) { + let from_id = follow + .follow_props + .actor_link::() + .map(|l| l.into()) + .unwrap_or_else(|_| { + follow + .follow_props + .actor_object::() + .expect("Follow::from_activity: actor not found error") + .object_props + .id_string() + .expect("Follow::from_activity: actor not found error") + }); + let from = + User::from_url(conn, from_id).expect("Follow::from_activity: actor not found error"); + match User::from_url( + conn, + follow + .follow_props + .object + .as_str() + .expect("Follow::from_activity: target url parsing error") + .to_string(), + ) { Some(user) => Follow::accept_follow(conn, &from, &user, follow, from.id, user.id), None => { - let blog = Blog::from_url(conn, follow.follow_props.object.as_str().expect("Follow::from_activity: target url parsing error").to_string()) - .expect("Follow::from_activity: target not found error"); + let blog = Blog::from_url( + conn, + follow + .follow_props + .object + .as_str() + .expect("Follow::from_activity: target url parsing error") + .to_string(), + ).expect("Follow::from_activity: target not found error"); Follow::accept_follow(conn, &from, &blog, follow, from.id, blog.id) } } @@ -98,27 +165,44 @@ impl FromActivity for Follow { impl Notify for Follow { fn notify(&self, conn: &Connection) { - Notification::insert(conn, NewNotification { - kind: notification_kind::FOLLOW.to_string(), - object_id: self.id, - user_id: self.following_id - }); + Notification::insert( + conn, + NewNotification { + kind: notification_kind::FOLLOW.to_string(), + object_id: self.id, + user_id: self.following_id, + }, + ); } } impl Deletable for Follow { fn delete(&self, conn: &Connection) -> Undo { - diesel::delete(self).execute(conn).expect("Follow::delete: follow deletion error"); + diesel::delete(self) + .execute(conn) + .expect("Follow::delete: follow deletion error"); // delete associated notification if any if let Some(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) { - diesel::delete(¬if).execute(conn).expect("Follow::delete: notification deletion error"); + diesel::delete(¬if) + .execute(conn) + .expect("Follow::delete: notification deletion error"); } let mut undo = Undo::default(); - undo.undo_props.set_actor_link(User::get(conn, self.follower_id).expect("Follow::delete: actor error").into_id()).expect("Follow::delete: actor error"); - undo.object_props.set_id_string(format!("{}/undo", self.ap_url)).expect("Follow::delete: id error"); - undo.undo_props.set_object_object(self.into_activity(conn)).expect("Follow::delete: object error"); + undo.undo_props + .set_actor_link( + User::get(conn, self.follower_id) + .expect("Follow::delete: actor error") + .into_id(), + ) + .expect("Follow::delete: actor error"); + undo.object_props + .set_id_string(format!("{}/undo", self.ap_url)) + .expect("Follow::delete: id error"); + undo.undo_props + .set_object_object(self.into_activity(conn)) + .expect("Follow::delete: object error"); undo } diff --git a/plume-models/src/follows.rs.orig b/plume-models/src/follows.rs.orig new file mode 100644 index 00000000..c2c962cb --- /dev/null +++ b/plume-models/src/follows.rs.orig @@ -0,0 +1,235 @@ +use activitypub::{ + activity::{Accept, Follow as FollowAct, Undo}, + actor::Person, + Actor, +}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; + +<<<<<<< HEAD +use plume_common::activity_pub::{broadcast, Id, IntoId, inbox::{FromActivity, Notify, WithInbox, Deletable}, sign::Signer}; +use {BASE_URL, ap_url, Connection}; +======= +>>>>>>> Run rustfmt and rename instanceTests to instance_tests +use blogs::Blog; +use notifications::*; +use plume_common::activity_pub::{ + broadcast, + inbox::{Deletable, FromActivity, Notify, WithInbox}, + sign::Signer, + Id, IntoId, +}; +use schema::follows; +use users::User; +use Connection; + +#[derive(Clone, Queryable, Identifiable, Associations)] +#[belongs_to(User, foreign_key = "following_id")] +pub struct Follow { + pub id: i32, + pub follower_id: i32, + pub following_id: i32, + pub ap_url: String, +} + +#[derive(Insertable)] +#[table_name = "follows"] +pub struct NewFollow { + pub follower_id: i32, + pub following_id: i32, + pub ap_url: String, +} + +impl Follow { + insert!(follows, NewFollow); + get!(follows); + find_by!(follows, find_by_ap_url, ap_url as String); + + pub fn find(conn: &Connection, from: i32, to: i32) -> Option { + follows::table + .filter(follows::follower_id.eq(from)) + .filter(follows::following_id.eq(to)) + .get_result(conn) + .ok() + } + + pub fn into_activity(&self, conn: &Connection) -> FollowAct { + let user = User::get(conn, self.follower_id) + .expect("Follow::into_activity: actor not found error"); + let target = User::get(conn, self.following_id) + .expect("Follow::into_activity: target not found error"); + + let mut act = FollowAct::default(); + act.follow_props + .set_actor_link::(user.clone().into_id()) + .expect("Follow::into_activity: actor error"); + act.follow_props + .set_object_object(user.into_activity(&*conn)) + .expect("Follow::into_activity: object error"); + act.object_props + .set_id_string(self.ap_url.clone()) + .expect("Follow::into_activity: id error"); + act.object_props + .set_to_link(target.clone().into_id()) + .expect("Follow::into_activity: target error"); + act.object_props + .set_cc_link_vec::(vec![]) + .expect("Follow::into_activity: cc error"); + act + } + + /// from -> The one sending the follow request + /// target -> The target of the request, responding with Accept + pub fn accept_follow( + conn: &Connection, + from: &B, + target: &A, + follow: FollowAct, + from_id: i32, + target_id: i32, + ) -> Follow { + let from_url: String = from.clone().into_id().into(); + let target_url: String = target.clone().into_id().into(); + let res = Follow::insert( + conn, + NewFollow { + follower_id: from_id, + following_id: target_id, + ap_url: format!("{}/follow/{}", from_url, target_url), + }, + ); + + let mut accept = Accept::default(); +<<<<<<< HEAD + 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: id error"); + accept.object_props.set_to_link(from.clone().into_id()).expect("Follow::accept_follow: to error"); + accept.object_props.set_cc_link_vec::(vec![]).expect("Follow::accept_follow: cc error"); + accept.accept_props.set_actor_link::(target.clone().into_id()).expect("Follow::accept_follow: actor error"); + accept.accept_props.set_object_object(follow).expect("Follow::accept_follow: object error"); +======= + let accept_id = format!( + "{}#accept", + follow.object_props.id_string().unwrap_or(String::new()) + ); + accept + .object_props + .set_id_string(accept_id) + .expect("Follow::accept_follow: id error"); + accept + .object_props + .set_to_link(from.clone().into_id()) + .expect("Follow::accept_follow: to error"); + accept + .object_props + .set_cc_link_vec::(vec![]) + .expect("Follow::accept_follow: cc error"); + accept + .accept_props + .set_actor_link::(target.clone().into_id()) + .expect("Follow::accept_follow: actor error"); + accept + .accept_props + .set_object_object(follow) + .expect("Follow::accept_follow: object error"); +>>>>>>> Run rustfmt and rename instanceTests to instance_tests + broadcast(&*target, accept, vec![from.clone()]); + res + } +} + +impl FromActivity for Follow { + fn from_activity(conn: &Connection, follow: FollowAct, _actor: Id) -> Follow { + let from_id = follow + .follow_props + .actor_link::() + .map(|l| l.into()) + .unwrap_or_else(|_| { + follow + .follow_props + .actor_object::() + .expect("Follow::from_activity: actor not found error") + .object_props + .id_string() + .expect("Follow::from_activity: actor not found error") + }); + let from = + User::from_url(conn, from_id).expect("Follow::from_activity: actor not found error"); + match User::from_url( + conn, + follow + .follow_props + .object + .as_str() + .expect("Follow::from_activity: target url parsing error") + .to_string(), + ) { + Some(user) => Follow::accept_follow(conn, &from, &user, follow, from.id, user.id), + None => { + let blog = Blog::from_url( + conn, + follow + .follow_props + .object + .as_str() + .expect("Follow::from_activity: target url parsing error") + .to_string(), + ).expect("Follow::from_activity: target not found error"); + Follow::accept_follow(conn, &from, &blog, follow, from.id, blog.id) + } + } + } +} + +impl Notify for Follow { + fn notify(&self, conn: &Connection) { + Notification::insert( + conn, + NewNotification { + kind: notification_kind::FOLLOW.to_string(), + object_id: self.id, + user_id: self.following_id, + }, + ); + } +} + +impl Deletable for Follow { + fn delete(&self, conn: &Connection) -> Undo { + diesel::delete(self) + .execute(conn) + .expect("Follow::delete: follow deletion error"); + + // delete associated notification if any + if let Some(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) { + diesel::delete(¬if) + .execute(conn) + .expect("Follow::delete: notification deletion error"); + } + + let mut undo = Undo::default(); + undo.undo_props + .set_actor_link( + User::get(conn, self.follower_id) + .expect("Follow::delete: actor error") + .into_id(), + ) + .expect("Follow::delete: actor error"); + undo.object_props + .set_id_string(format!("{}/undo", self.ap_url)) + .expect("Follow::delete: id error"); + undo.undo_props + .set_object_object(self.into_activity(conn)) + .expect("Follow::delete: object error"); + undo + } + + fn delete_id(id: String, actor_id: String, 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); + } + } + } + } +} diff --git a/plume-models/src/headers.rs b/plume-models/src/headers.rs index 57030deb..0d5d2923 100644 --- a/plume-models/src/headers.rs +++ b/plume-models/src/headers.rs @@ -1,6 +1,8 @@ use rocket::request::{self, FromRequest, Request}; -use rocket::{http::{Header, HeaderMap}, Outcome}; - +use rocket::{ + http::{Header, HeaderMap}, + Outcome, +}; pub struct Headers<'r>(pub HeaderMap<'r>); @@ -18,10 +20,10 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers<'r> { } else { ori.path().to_owned() }; - headers.add(Header::new("(request-target)", - format!("{} {}", - request.method().as_str().to_lowercase(), - uri))); + headers.add(Header::new( + "(request-target)", + format!("{} {}", request.method().as_str().to_lowercase(), uri), + )); Outcome::Success(Headers(headers)) } } diff --git a/plume-models/src/instance.rs b/plume-models/src/instance.rs index 2e4bfac8..538320fe 100644 --- a/plume-models/src/instance.rs +++ b/plume-models/src/instance.rs @@ -1,13 +1,13 @@ use chrono::NaiveDateTime; -use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use std::iter::Iterator; -use plume_common::utils::md_to_html; -use Connection; -use safe_string::SafeString; use ap_url; -use users::User; +use plume_common::utils::md_to_html; +use safe_string::SafeString; use schema::{instances, users}; +use users::User; +use Connection; #[derive(Clone, Identifiable, Queryable, Serialize)] pub struct Instance { @@ -20,12 +20,12 @@ pub struct Instance { pub open_registrations: bool, pub short_description: SafeString, pub long_description: SafeString, - pub default_license : String, + pub default_license: String, pub long_description_html: String, - pub short_description_html: String + pub short_description_html: String, } -#[derive(Insertable)] +#[derive(Clone, Insertable)] #[table_name = "instances"] pub struct NewInstance { pub public_domain: String, @@ -34,28 +34,32 @@ pub struct NewInstance { pub open_registrations: bool, pub short_description: SafeString, pub long_description: SafeString, - pub default_license : String, + pub default_license: String, pub long_description_html: String, - pub short_description_html: String + pub short_description_html: String, } impl Instance { pub fn get_local(conn: &Connection) -> Option { - instances::table.filter(instances::local.eq(true)) + instances::table + .filter(instances::local.eq(true)) .limit(1) .load::(conn) .expect("Instance::get_local: loading error") - .into_iter().nth(0) + .into_iter() + .nth(0) } pub fn get_remotes(conn: &Connection) -> Vec { - instances::table.filter(instances::local.eq(false)) + instances::table + .filter(instances::local.eq(false)) .load::(conn) .expect("Instance::get_remotes: loading error") } pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Vec { - instances::table.order(instances::public_domain.asc()) + instances::table + .order(instances::public_domain.asc()) .offset(min.into()) .limit((max - min).into()) .load::(conn) @@ -63,7 +67,9 @@ impl Instance { } pub fn local_id(conn: &Connection) -> i32 { - Instance::get_local(conn).expect("Instance::local_id: local instance not found error").id + Instance::get_local(conn) + .expect("Instance::local_id: local instance not found error") + .id } insert!(instances, NewInstance); @@ -79,10 +85,12 @@ impl Instance { /// id: AP object id pub fn is_blocked(conn: &Connection, id: String) -> bool { - for block in instances::table.filter(instances::blocked.eq(true)) + for block in instances::table + .filter(instances::blocked.eq(true)) .get_results::(conn) - .expect("Instance::is_blocked: loading error") { - if id.starts_with(format!("https://{}", block.public_domain).as_str()) { + .expect("Instance::is_blocked: loading error") + { + if id.starts_with(format!("https://{}/", block.public_domain).as_str()) { return true; } } @@ -91,7 +99,8 @@ impl Instance { } pub fn has_admin(&self, conn: &Connection) -> bool { - users::table.filter(users::instance_id.eq(self.id)) + users::table + .filter(users::instance_id.eq(self.id)) .filter(users::is_admin.eq(true)) .load::(conn) .expect("Instance::has_admin: loading error") @@ -99,14 +108,20 @@ impl Instance { } pub fn main_admin(&self, conn: &Connection) -> User { - users::table.filter(users::instance_id.eq(self.id)) + users::table + .filter(users::instance_id.eq(self.id)) .filter(users::is_admin.eq(true)) .limit(1) .get_result::(conn) .expect("Instance::main_admin: loading error") } - pub fn compute_box(&self, prefix: &'static str, name: String, box_name: &'static str) -> String { + pub fn compute_box( + &self, + prefix: &'static str, + name: String, + box_name: &'static str, + ) -> String { ap_url(format!( "{instance}/{prefix}/{name}/{box_name}", instance = self.public_domain, @@ -116,7 +131,14 @@ impl Instance { )) } - pub fn update(&self, conn: &Connection, name: String, open_registrations: bool, short_description: SafeString, long_description: SafeString) { + pub fn update( + &self, + conn: &Connection, + name: String, + open_registrations: bool, + short_description: SafeString, + long_description: SafeString, + ) { let (sd, _, _) = md_to_html(short_description.as_ref()); let (ld, _, _) = md_to_html(long_description.as_ref()); diesel::update(self) @@ -126,12 +148,258 @@ impl Instance { instances::short_description.eq(short_description), instances::long_description.eq(long_description), instances::short_description_html.eq(sd), - instances::long_description_html.eq(ld) - )).execute(conn) + instances::long_description_html.eq(ld), + )) + .execute(conn) .expect("Instance::update: update error"); } pub fn count(conn: &Connection) -> i64 { - instances::table.count().get_result(conn).expect("Instance::count: counting error") + instances::table + .count() + .get_result(conn) + .expect("Instance::count: counting error") + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use diesel::Connection; + use tests::db; + use Connection as Conn; + + pub(crate) fn fill_database(conn: &Conn) -> Vec<(NewInstance, Instance)> { + vec![ + NewInstance { + default_license: "WTFPL".to_string(), + local: true, + long_description: SafeString::new("This is my instance."), + long_description_html: "

This is my instance

".to_string(), + short_description: SafeString::new("My instance."), + short_description_html: "

My instance

".to_string(), + name: "My instance".to_string(), + open_registrations: true, + public_domain: "plu.me".to_string(), + }, + NewInstance { + default_license: "WTFPL".to_string(), + local: false, + long_description: SafeString::new("This is an instance."), + long_description_html: "

This is an instance

".to_string(), + short_description: SafeString::new("An instance."), + short_description_html: "

An instance

".to_string(), + name: "An instance".to_string(), + open_registrations: true, + public_domain: "1plu.me".to_string(), + }, + NewInstance { + default_license: "CC-0".to_string(), + local: false, + long_description: SafeString::new("This is the instance of someone."), + long_description_html: "

This is the instance of someone

".to_string(), + short_description: SafeString::new("Someone instance."), + short_description_html: "

Someone instance

".to_string(), + name: "Someone instance".to_string(), + open_registrations: false, + public_domain: "2plu.me".to_string(), + }, + NewInstance { + default_license: "CC-0-BY-SA".to_string(), + local: false, + long_description: SafeString::new("Good morning"), + long_description_html: "

Good morning

".to_string(), + short_description: SafeString::new("Hello"), + short_description_html: "

Hello

".to_string(), + name: "Nice day".to_string(), + open_registrations: true, + public_domain: "3plu.me".to_string(), + }, + ].into_iter() + .map(|inst| { + ( + inst.clone(), + Instance::find_by_domain(conn, inst.public_domain.clone()) + .unwrap_or_else(|| Instance::insert(conn, inst)), + ) + }) + .collect() + } + + #[test] + fn local_instance() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let inserted = fill_database(conn) + .into_iter() + .map(|(inserted, _)| inserted) + .find(|inst| inst.local) + .unwrap(); + let res = Instance::get_local(conn).unwrap(); + + part_eq!( + res, + inserted, + [ + default_license, + local, + long_description, + long_description_html, + short_description, + short_description_html, + name, + open_registrations, + public_domain + ] + ); + assert_eq!(Instance::local_id(conn), res.id); + Ok(()) + }); + } + + #[test] + fn remote_instance() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let inserted = fill_database(conn); + assert_eq!(Instance::count(conn), inserted.len() as i64); + + let res = Instance::get_remotes(conn); + assert_eq!( + res.len(), + inserted.iter().filter(|(inst, _)| !inst.local).count() + ); + + inserted + .iter() + .filter(|(newinst, _)| !newinst.local) + .map(|(newinst, inst)| (newinst, res.iter().find(|res| res.id == inst.id).unwrap())) + .for_each(|(newinst, inst)| { + part_eq!( + newinst, + inst, + [ + default_license, + local, + long_description, + long_description_html, + short_description, + short_description_html, + name, + open_registrations, + public_domain + ] + ) + }); + + let page = Instance::page(conn, (0, 2)); + 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(); + for i in 1..inserted.len() as i32 { + let page = Instance::page(conn, (i, i + 1)); + assert_eq!(page.len(), 1); + assert!(last_domaine <= page[0].public_domain); + last_domaine = page[0].public_domain.clone(); + } + + Ok(()) + }); + } + + #[test] + fn blocked() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let inst_list = fill_database(conn); + let inst = &inst_list[0].1; + let inst_list = &inst_list[1..]; + + let blocked = inst.blocked; + inst.toggle_block(conn); + let inst = Instance::get(conn, inst.id).unwrap(); + assert_eq!(inst.blocked, !blocked); + assert_eq!( + inst_list + .iter() + .filter( + |(_, inst)| inst.blocked != Instance::get(conn, inst.id).unwrap().blocked + ) + .count(), + 0 + ); + assert_eq!( + Instance::is_blocked(conn, format!("https://{}/something", inst.public_domain)), + inst.blocked + ); + assert_eq!( + Instance::is_blocked(conn, format!("https://{}a/something", inst.public_domain)), + Instance::find_by_domain(conn, format!("{}a", inst.public_domain)) + .map(|inst| inst.blocked) + .unwrap_or(false) + ); + + inst.toggle_block(conn); + 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)), + inst.blocked + ); + assert_eq!( + Instance::is_blocked(conn, format!("https://{}a/something", inst.public_domain)), + Instance::find_by_domain(conn, format!("{}a", inst.public_domain)) + .map(|inst| inst.blocked) + .unwrap_or(false) + ); + assert_eq!( + inst_list + .iter() + .filter( + |(_, inst)| inst.blocked != Instance::get(conn, inst.id).unwrap().blocked + ) + .count(), + 0 + ); + + Ok(()) + }); + } + + #[test] + fn update() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let inst = &fill_database(conn)[0].1; + + inst.update( + conn, + "NewName".to_owned(), + false, + SafeString::new("[short](#link)"), + SafeString::new("[long_description](/with_link)"), + ); + let inst = Instance::get(conn, inst.id).unwrap(); + assert_eq!(inst.name, "NewName".to_owned()); + assert_eq!(inst.open_registrations, false); + assert_eq!( + inst.long_description.get(), + "[long_description](/with_link)" + ); + assert_eq!( + inst.long_description_html, + "

long_description

\n" + ); + assert_eq!(inst.short_description.get(), "[short](#link)"); + assert_eq!( + inst.short_description_html, + "

short

\n" + ); + + Ok(()) + }); } } diff --git a/plume-models/src/lib.rs b/plume-models/src/lib.rs index 0283fda1..0979ebe3 100644 --- a/plume-models/src/lib.rs +++ b/plume-models/src/lib.rs @@ -25,6 +25,10 @@ extern crate serde_json; extern crate url; extern crate webfinger; +#[cfg(test)] +#[macro_use] +extern crate diesel_migrations; + use std::env; #[cfg(all(feature = "sqlite", not(feature = "postgres")))] @@ -99,11 +103,13 @@ macro_rules! list_by { macro_rules! get { ($table:ident) => { pub fn get(conn: &crate::Connection, id: i32) -> Option { - $table::table.filter($table::id.eq(id)) + $table::table + .filter($table::id.eq(id)) .limit(1) .load::(conn) .expect("macro::get: Error loading $table by id") - .into_iter().nth(0) + .into_iter() + .nth(0) } }; } @@ -177,11 +183,16 @@ macro_rules! update { macro_rules! last { ($table:ident) => { pub fn last(conn: &crate::Connection) -> Self { - $table::table.order_by($table::id.desc()) + $table::table + .order_by($table::id.desc()) .limit(1) .load::(conn) - .expect(concat!("macro::last: Error getting last ", stringify!($table))) - .iter().next() + .expect(concat!( + "macro::last: Error getting last ", + stringify!($table) + )) + .iter() + .next() .expect(concat!("macro::last: No last ", stringify!($table))) .clone() } @@ -189,31 +200,67 @@ macro_rules! last { } lazy_static! { - pub static ref BASE_URL: String = env::var("BASE_URL") - .unwrap_or(format!("127.0.0.1:{}", env::var("ROCKET_PORT").unwrap_or(String::from("8000")))); - + pub static ref BASE_URL: String = env::var("BASE_URL").unwrap_or(format!( + "127.0.0.1:{}", + env::var("ROCKET_PORT").unwrap_or(String::from("8000")) + )); pub static ref USE_HTTPS: bool = env::var("USE_HTTPS").map(|val| val == "1").unwrap_or(true); } +#[cfg(not(test))] +static DB_NAME: &str = "plume"; +#[cfg(test)] +static DB_NAME: &str = "plume_tests"; + #[cfg(all(feature = "postgres", not(feature = "sqlite")))] lazy_static! { - pub static ref DATABASE_URL: String = env::var("DATABASE_URL").unwrap_or(String::from("postgres://plume:plume@localhost/plume")); + pub static ref DATABASE_URL: String = + env::var("DATABASE_URL").unwrap_or(format!("postgres://plume:plume@localhost/{}", DB_NAME)); } #[cfg(all(feature = "sqlite", not(feature = "postgres")))] lazy_static! { - pub static ref DATABASE_URL: String = env::var("DATABASE_URL").unwrap_or(String::from("plume.sqlite")); + pub static ref DATABASE_URL: String = + env::var("DATABASE_URL").unwrap_or(format!("{}.sqlite", DB_NAME)); } pub fn ap_url(url: String) -> String { - let scheme = if *USE_HTTPS { - "https" - } else { - "http" - }; + let scheme = if *USE_HTTPS { "https" } else { "http" }; format!("{}://{}", scheme, url) } +#[cfg(test)] +#[macro_use] +mod tests { + use diesel::Connection; + use Connection as Conn; + use DATABASE_URL; + + #[cfg(feature = "sqlite")] + embed_migrations!("../migrations/sqlite"); + + #[cfg(feature = "postgres")] + embed_migrations!("../migrations/postgres"); + + #[macro_export] + macro_rules! part_eq { + ( $x:expr, $y:expr, [$( $var:ident ),*] ) => { + { + $( + assert_eq!($x.$var, $y.$var); + )* + } + }; + } + + pub fn db() -> Conn { + let conn = + Conn::establish(&*DATABASE_URL.as_str()).expect("Couldn't connect to the database"); + embedded_migrations::run(&conn).expect("Couldn't run migrations"); + conn + } +} + pub mod admin; pub mod api_tokens; pub mod apps; diff --git a/plume-models/src/likes.rs b/plume-models/src/likes.rs index 8c281a0c..a8c6bc2b 100644 --- a/plume-models/src/likes.rs +++ b/plume-models/src/likes.rs @@ -1,18 +1,16 @@ use activitypub::activity; use chrono::NaiveDateTime; -use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; -use plume_common::activity_pub::{ - PUBLIC_VISIBILTY, - Id, - IntoId, - inbox::{FromActivity, Deletable, Notify} -}; -use Connection; use notifications::*; +use plume_common::activity_pub::{ + inbox::{Deletable, FromActivity, Notify}, + Id, IntoId, PUBLIC_VISIBILTY, +}; use posts::Post; -use users::User; use schema::likes; +use users::User; +use Connection; #[derive(Clone, Queryable, Identifiable)] pub struct Like { @@ -20,7 +18,7 @@ pub struct Like { pub user_id: i32, pub post_id: i32, pub creation_date: NaiveDateTime, - pub ap_url: String + pub ap_url: String, } #[derive(Default, Insertable)] @@ -28,7 +26,7 @@ pub struct Like { pub struct NewLike { pub user_id: i32, pub post_id: i32, - pub ap_url: String + pub ap_url: String, } impl Like { @@ -45,17 +43,36 @@ impl Like { User::get(conn, self.user_id).expect("Like::update_ap_url: user error").ap_url, Post::get(conn, self.post_id).expect("Like::update_ap_url: post error").ap_url ))) - .execute(conn).expect("Like::update_ap_url: update error"); + .execute(conn) + .expect("Like::update_ap_url: update error"); } } pub fn into_activity(&self, conn: &Connection) -> activity::Like { let mut act = activity::Like::default(); - act.like_props.set_actor_link(User::get(conn, self.user_id).expect("Like::into_activity: user error").into_id()).expect("Like::into_activity: actor error"); - act.like_props.set_object_link(Post::get(conn, self.post_id).expect("Like::into_activity: post error").into_id()).expect("Like::into_activity: object error"); - act.object_props.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())).expect("Like::into_activity: to error"); - act.object_props.set_cc_link_vec::(vec![]).expect("Like::into_activity: cc error"); - act.object_props.set_id_string(self.ap_url.clone()).expect("Like::into_activity: id error"); + act.like_props + .set_actor_link( + User::get(conn, self.user_id) + .expect("Like::into_activity: user error") + .into_id(), + ) + .expect("Like::into_activity: actor error"); + act.like_props + .set_object_link( + Post::get(conn, self.post_id) + .expect("Like::into_activity: post error") + .into_id(), + ) + .expect("Like::into_activity: object error"); + act.object_props + .set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())) + .expect("Like::into_activity: to error"); + act.object_props + .set_cc_link_vec::(vec![]) + .expect("Like::into_activity: cc error"); + act.object_props + .set_id_string(self.ap_url.clone()) + .expect("Like::into_activity: id error"); act } @@ -63,13 +80,30 @@ impl Like { impl FromActivity for Like { fn from_activity(conn: &Connection, like: activity::Like, _actor: Id) -> Like { - let liker = User::from_url(conn, like.like_props.actor.as_str().expect("Like::from_activity: actor error").to_string()); - let post = Post::find_by_ap_url(conn, like.like_props.object.as_str().expect("Like::from_activity: object error").to_string()); - 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(String::from("")) - }); + let liker = User::from_url( + conn, + like.like_props + .actor + .as_str() + .expect("Like::from_activity: actor error") + .to_string(), + ); + let post = Post::find_by_ap_url( + conn, + like.like_props + .object + .as_str() + .expect("Like::from_activity: object error") + .to_string(), + ); + 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(String::from("")), + }, + ); res.notify(conn); res } @@ -79,30 +113,51 @@ impl Notify for Like { fn notify(&self, conn: &Connection) { let post = Post::get(conn, self.post_id).expect("Like::notify: post error"); for author in post.get_authors(conn) { - Notification::insert(conn, NewNotification { - kind: notification_kind::LIKE.to_string(), - object_id: self.id, - user_id: author.id - }); + Notification::insert( + conn, + NewNotification { + kind: notification_kind::LIKE.to_string(), + object_id: self.id, + user_id: author.id, + }, + ); } } } impl Deletable for Like { fn delete(&self, conn: &Connection) -> activity::Undo { - diesel::delete(self).execute(conn).expect("Like::delete: delete error"); + diesel::delete(self) + .execute(conn) + .expect("Like::delete: delete error"); // delete associated notification if any if let Some(notif) = Notification::find(conn, notification_kind::LIKE, self.id) { - diesel::delete(¬if).execute(conn).expect("Like::delete: notification error"); + diesel::delete(¬if) + .execute(conn) + .expect("Like::delete: notification error"); } 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"); - act.undo_props.set_object_object(self.into_activity(conn)).expect("Like::delete: object error"); - act.object_props.set_id_string(format!("{}#delete", self.ap_url)).expect("Like::delete: id error"); - act.object_props.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())).expect("Like::delete: to error"); - act.object_props.set_cc_link_vec::(vec![]).expect("Like::delete: cc error"); + act.undo_props + .set_actor_link( + User::get(conn, self.user_id) + .expect("Like::delete: user error") + .into_id(), + ) + .expect("Like::delete: actor error"); + act.undo_props + .set_object_object(self.into_activity(conn)) + .expect("Like::delete: object error"); + act.object_props + .set_id_string(format!("{}#delete", self.ap_url)) + .expect("Like::delete: id error"); + act.object_props + .set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())) + .expect("Like::delete: to error"); + act.object_props + .set_cc_link_vec::(vec![]) + .expect("Like::delete: cc error"); act } diff --git a/plume-models/src/medias.rs b/plume-models/src/medias.rs index 6f834867..f2caa18c 100644 --- a/plume-models/src/medias.rs +++ b/plume-models/src/medias.rs @@ -1,5 +1,5 @@ use activitypub::object::Image; -use diesel::{self, QueryDsl, ExpressionMethods, RunQueryDsl}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use guid_create::GUID; use reqwest; use serde_json; @@ -7,10 +7,10 @@ use std::{fs, path::Path}; use plume_common::activity_pub::Id; -use {ap_url, Connection}; use instance::Instance; -use users::User; use schema::medias; +use users::User; +use {ap_url, Connection}; #[derive(Clone, Identifiable, Queryable, Serialize)] pub struct Media { @@ -21,7 +21,7 @@ pub struct Media { pub remote_url: Option, pub sensitive: bool, pub content_warning: Option, - pub owner_id: i32 + pub owner_id: i32, } #[derive(Insertable)] @@ -33,7 +33,7 @@ pub struct NewMedia { pub remote_url: Option, pub sensitive: bool, pub content_warning: Option, - pub owner_id: i32 + pub owner_id: i32, } impl Media { @@ -41,29 +41,64 @@ impl Media { get!(medias); list_by!(medias, for_user, owner_id as i32); + pub fn list_all_medias(conn: &Connection) -> Vec { + medias::table + .load::(conn) + .expect("Media::list_all_medias: loading error") + } + pub fn to_json(&self, conn: &Connection) -> serde_json::Value { let mut json = serde_json::to_value(self).expect("Media::to_json: serialization error"); let url = self.url(conn); - let (cat, preview, html, md) = match self.file_path.rsplitn(2, '.').next().expect("Media::to_json: extension error") { + let (cat, preview, html, md) = match self + .file_path + .rsplitn(2, '.') + .next() + .expect("Media::to_json: extension error") + { "png" | "jpg" | "jpeg" | "gif" | "svg" => ( "image", - format!("\"{}\"", url, self.alt_text, self.alt_text), - format!("\"{}\"", url, self.alt_text, self.alt_text), + format!( + "\"{}\"", + url, self.alt_text, self.alt_text + ), + format!( + "\"{}\"", + url, self.alt_text, self.alt_text + ), format!("![{}]({})", self.alt_text, url), ), "mp3" | "wav" | "flac" => ( "audio", - format!("", url, self.alt_text), - format!("", url, self.alt_text), - format!("", url, self.alt_text), + format!( + "", + url, self.alt_text + ), + format!( + "", + url, self.alt_text + ), + format!( + "", + url, self.alt_text + ), ), "mp4" | "avi" | "webm" | "mov" => ( "video", - format!("", url, self.alt_text), - format!("", url, self.alt_text), - format!("", url, self.alt_text), + format!( + "", + url, self.alt_text + ), + format!( + "", + url, self.alt_text + ), + format!( + "", + url, self.alt_text + ), ), - _ => ("unknown", String::new(), String::new(), String::new()) + _ => ("unknown", String::new(), String::new(), String::new()), }; json["html_preview"] = json!(preview); json["html"] = json!(html); @@ -77,30 +112,43 @@ impl Media { if self.is_remote { self.remote_url.clone().unwrap_or(String::new()) } else { - ap_url(format!("{}/{}", Instance::get_local(conn).expect("Media::url: local instance not found error").public_domain, self.file_path)) + ap_url(format!( + "{}/{}", + Instance::get_local(conn) + .expect("Media::url: local instance not found error") + .public_domain, + self.file_path + )) } } pub fn delete(&self, conn: &Connection) { - fs::remove_file(self.file_path.as_str()).expect("Media::delete: file deletion error"); - diesel::delete(self).execute(conn).expect("Media::delete: database entry deletion error"); + if !self.is_remote { + fs::remove_file(self.file_path.as_str()).expect("Media::delete: file deletion error"); + } + diesel::delete(self) + .execute(conn) + .expect("Media::delete: database entry deletion error"); } - pub fn save_remote(conn: &Connection, url: String) -> Media { - Media::insert(conn, NewMedia { - file_path: String::new(), - alt_text: String::new(), - is_remote: true, - remote_url: Some(url), - sensitive: false, - content_warning: None, - owner_id: 1 // It will be owned by the admin during an instant, but set_owner will be called just after - }) + pub fn save_remote(conn: &Connection, url: String, user: &User) -> Media { + Media::insert( + conn, + NewMedia { + file_path: String::new(), + alt_text: String::new(), + is_remote: true, + remote_url: Some(url), + sensitive: false, + content_warning: None, + owner_id: user.id, + }, + ) } - pub fn set_owner(&self, conn: &Connection, id: i32) { + pub fn set_owner(&self, conn: &Connection, user: &User) { diesel::update(self) - .set(medias::owner_id.eq(id)) + .set(medias::owner_id.eq(user.id)) .execute(conn) .expect("Media::set_owner: owner update error"); } @@ -108,21 +156,199 @@ impl Media { // TODO: merge with save_remote? pub fn from_activity(conn: &Connection, image: Image) -> Option { let remote_url = image.object_props.url_string().ok()?; - let ext = remote_url.rsplit('.').next().map(|ext| ext.to_owned()).unwrap_or("png".to_owned()); - let path = Path::new("static").join("media").join(format!("{}.{}", GUID::rand().to_string(), ext)); + let ext = remote_url + .rsplit('.') + .next() + .map(|ext| ext.to_owned()) + .unwrap_or("png".to_owned()); + let path = + Path::new("static") + .join("media") + .join(format!("{}.{}", GUID::rand().to_string(), ext)); let mut dest = fs::File::create(path.clone()).ok()?; - reqwest::get(remote_url.as_str()).ok()? - .copy_to(&mut dest).ok()?; + reqwest::get(remote_url.as_str()) + .ok()? + .copy_to(&mut dest) + .ok()?; - Some(Media::insert(conn, NewMedia { - file_path: path.to_str()?.to_string(), - alt_text: image.object_props.content_string().ok()?, - is_remote: true, - remote_url: None, - sensitive: image.object_props.summary_string().is_ok(), - content_warning: image.object_props.summary_string().ok(), - owner_id: User::from_url(conn, image.object_props.attributed_to_link_vec::().ok()?.into_iter().next()?.into())?.id - })) + Some(Media::insert( + conn, + NewMedia { + file_path: path.to_str()?.to_string(), + alt_text: image.object_props.content_string().ok()?, + is_remote: true, + remote_url: None, + sensitive: image.object_props.summary_string().is_ok(), + content_warning: image.object_props.summary_string().ok(), + owner_id: User::from_url( + conn, + image + .object_props + .attributed_to_link_vec::() + .ok()? + .into_iter() + .next()? + .into(), + )?.id, + }, + )) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use diesel::Connection; + use std::env::{current_dir, set_current_dir}; + use std::fs; + use std::path::Path; + use tests::db; + use users::tests as usersTests; + use Connection as Conn; + + pub(crate) fn fill_database(conn: &Conn) -> Vec { + let mut wd = current_dir().unwrap().to_path_buf(); + while wd.pop() { + if wd.join(".git").exists() { + set_current_dir(wd).unwrap(); + break; + } + } + + let users = usersTests::fill_database(conn); + let user_one = users[0].id; + let user_two = users[1].id; + let f1 = "static/media/1.png".to_owned(); + let f2 = "static/media/2.mp3".to_owned(); + fs::write(f1.clone(), []).unwrap(); + fs::write(f2.clone(), []).unwrap(); + vec![ + NewMedia { + file_path: f1, + alt_text: "some alt".to_owned(), + is_remote: false, + remote_url: None, + sensitive: false, + content_warning: None, + owner_id: user_one, + }, + NewMedia { + file_path: f2, + alt_text: "alt message".to_owned(), + is_remote: false, + remote_url: None, + sensitive: true, + content_warning: Some("Content warning".to_owned()), + owner_id: user_one, + }, + NewMedia { + file_path: "".to_owned(), + alt_text: "another alt".to_owned(), + is_remote: true, + remote_url: Some("https://example.com/".to_owned()), + sensitive: false, + content_warning: None, + owner_id: user_two, + }, + ].into_iter() + .map(|nm| Media::insert(conn, nm)) + .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); + } + } + + //set_owner + + #[test] + fn delete() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let user = usersTests::fill_database(conn)[0].id; + fill_database(conn); + + let path = "static/media/test_deletion".to_owned(); + fs::write(path.clone(), []).unwrap(); + + let media = Media::insert( + conn, + NewMedia { + file_path: path.clone(), + alt_text: "alt message".to_owned(), + is_remote: false, + remote_url: None, + sensitive: false, + content_warning: None, + owner_id: user, + }, + ); + + assert!(Path::new(&path).exists()); + media.delete(conn); + assert!(!Path::new(&path).exists()); + + clean(conn); + + Ok(()) + }); + } + + #[test] + + fn set_owner() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let users = usersTests::fill_database(conn); + let u1 = &users[0]; + let u2 = &users[1]; + fill_database(conn); + + let path = "static/media/test_set_owner".to_owned(); + fs::write(path.clone(), []).unwrap(); + + let media = Media::insert( + conn, + NewMedia { + file_path: path.clone(), + alt_text: "alt message".to_owned(), + is_remote: false, + remote_url: None, + sensitive: false, + content_warning: None, + owner_id: u1.id, + }, + ); + + assert!( + Media::for_user(conn, u1.id) + .iter() + .any(|m| m.id == media.id) + ); + assert!( + !Media::for_user(conn, u2.id) + .iter() + .any(|m| m.id == media.id) + ); + media.set_owner(conn, u2); + assert!( + !Media::for_user(conn, u1.id) + .iter() + .any(|m| m.id == media.id) + ); + assert!( + Media::for_user(conn, u2.id) + .iter() + .any(|m| m.id == media.id) + ); + + clean(conn); + + Ok(()) + }); } } diff --git a/plume-models/src/mentions.rs b/plume-models/src/mentions.rs index b6c97aa3..9620fb69 100644 --- a/plume-models/src/mentions.rs +++ b/plume-models/src/mentions.rs @@ -1,13 +1,13 @@ use activitypub::link; -use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; -use plume_common::activity_pub::inbox::Notify; -use Connection; use comments::Comment; use notifications::*; +use plume_common::activity_pub::inbox::Notify; use posts::Post; -use users::User; use schema::mentions; +use users::User; +use Connection; #[derive(Clone, Queryable, Identifiable, Serialize, Deserialize)] pub struct Mention { @@ -15,7 +15,7 @@ pub struct Mention { pub mentioned_id: i32, pub post_id: Option, pub comment_id: Option, - pub ap_url: String // TODO: remove, since mentions don't have an AP URL actually, this field was added by mistake + pub ap_url: String, // TODO: remove, since mentions don't have an AP URL actually, this field was added by mistake } #[derive(Insertable)] @@ -24,7 +24,7 @@ pub struct NewMention { pub mentioned_id: i32, pub post_id: Option, pub comment_id: Option, - pub ap_url: String + pub ap_url: String, } impl Mention { @@ -50,38 +50,62 @@ impl Mention { pub fn get_user(&self, conn: &Connection) -> Option { 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)) + None => self.get_comment(conn).map(|c| c.get_author(conn)), } } pub fn build_activity(conn: &Connection, ment: String) -> link::Mention { let user = User::find_by_fqn(conn, ment.clone()); let mut mention = link::Mention::default(); - mention.link_props.set_href_string(user.clone().map(|u| u.ap_url).unwrap_or(String::new())).expect("Mention::build_activity: href error"); - mention.link_props.set_name_string(format!("@{}", ment)).expect("Mention::build_activity: name error:"); + mention + .link_props + .set_href_string(user.clone().map(|u| u.ap_url).unwrap_or(String::new())) + .expect("Mention::build_activity: href error"); + mention + .link_props + .set_name_string(format!("@{}", ment)) + .expect("Mention::build_activity: name error:"); mention } pub fn to_activity(&self, conn: &Connection) -> 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(String::new())).expect("Mention::to_activity: href error"); - mention.link_props.set_name_string(user.map(|u| format!("@{}", u.get_fqn(conn))).unwrap_or(String::new())).expect("Mention::to_activity: mention error"); + mention + .link_props + .set_href_string(user.clone().map(|u| u.ap_url).unwrap_or(String::new())) + .expect("Mention::to_activity: href error"); + mention + .link_props + .set_name_string( + user.map(|u| format!("@{}", u.get_fqn(conn))) + .unwrap_or(String::new()), + ) + .expect("Mention::to_activity: mention error"); mention } - pub fn from_activity(conn: &Connection, ment: link::Mention, inside: i32, in_post: bool, notify: bool) -> Option { + pub fn from_activity( + conn: &Connection, + ment: link::Mention, + inside: i32, + in_post: bool, + notify: bool, + ) -> Option { 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.clone().into()).map(|post| { - let res = Mention::insert(conn, NewMention { - mentioned_id: mentioned.id, - post_id: Some(post.id), - comment_id: None, - ap_url: ment.link_props.href_string().unwrap_or(String::new()) - }); + let res = Mention::insert( + conn, + NewMention { + mentioned_id: mentioned.id, + post_id: Some(post.id), + comment_id: None, + ap_url: ment.link_props.href_string().unwrap_or(String::new()), + }, + ); if notify { res.notify(conn); } @@ -89,12 +113,15 @@ impl Mention { }) } else { Comment::get(conn, inside.into()).map(|comment| { - let res = Mention::insert(conn, NewMention { - mentioned_id: mentioned.id, - post_id: None, - comment_id: Some(comment.id), - ap_url: ment.link_props.href_string().unwrap_or(String::new()) - }); + let res = Mention::insert( + conn, + NewMention { + mentioned_id: mentioned.id, + post_id: None, + comment_id: Some(comment.id), + ap_url: ment.link_props.href_string().unwrap_or(String::new()), + }, + ); if notify { res.notify(conn); } @@ -106,18 +133,23 @@ impl Mention { pub fn delete(&self, conn: &Connection) { //find related notifications and delete them Notification::find(conn, notification_kind::MENTION, self.id).map(|n| n.delete(conn)); - diesel::delete(self).execute(conn).expect("Mention::delete: mention deletion error"); + diesel::delete(self) + .execute(conn) + .expect("Mention::delete: mention deletion error"); } } impl Notify for Mention { fn notify(&self, conn: &Connection) { self.get_mentioned(conn).map(|m| { - Notification::insert(conn, NewNotification { - kind: notification_kind::MENTION.to_string(), - object_id: self.id, - user_id: m.id - }); + Notification::insert( + conn, + NewNotification { + kind: notification_kind::MENTION.to_string(), + object_id: self.id, + user_id: m.id, + }, + ); }); } } diff --git a/plume-models/src/notifications.rs b/plume-models/src/notifications.rs index 499d4ceb..8142b68e 100644 --- a/plume-models/src/notifications.rs +++ b/plume-models/src/notifications.rs @@ -1,16 +1,16 @@ use chrono::NaiveDateTime; -use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use serde_json; -use Connection; use comments::Comment; use follows::Follow; use likes::Like; use mentions::Mention; use posts::Post; use reshares::Reshare; -use users::User; use schema::notifications; +use users::User; +use Connection; pub mod notification_kind { pub const COMMENT: &'static str = "COMMENT"; @@ -26,7 +26,7 @@ pub struct Notification { pub user_id: i32, pub creation_date: NaiveDateTime, pub kind: String, - pub object_id: i32 + pub object_id: i32, } #[derive(Insertable)] @@ -34,7 +34,7 @@ pub struct Notification { pub struct NewNotification { pub user_id: i32, pub kind: String, - pub object_id: i32 + pub object_id: i32, } impl Notification { @@ -42,14 +42,20 @@ impl Notification { get!(notifications); pub fn find_for_user(conn: &Connection, user: &User) -> Vec { - notifications::table.filter(notifications::user_id.eq(user.id)) + notifications::table + .filter(notifications::user_id.eq(user.id)) .order_by(notifications::creation_date.desc()) .load::(conn) .expect("Notification::find_for_user: notification loading error") } - pub fn page_for_user(conn: &Connection, user: &User, (min, max): (i32, i32)) -> Vec { - notifications::table.filter(notifications::user_id.eq(user.id)) + pub fn page_for_user( + conn: &Connection, + user: &User, + (min, max): (i32, i32), + ) -> Vec { + notifications::table + .filter(notifications::user_id.eq(user.id)) .order_by(notifications::creation_date.desc()) .offset(min.into()) .limit((max - min).into()) @@ -58,7 +64,8 @@ impl Notification { } pub fn find>(conn: &Connection, kind: S, obj: i32) -> Option { - notifications::table.filter(notifications::kind.eq(kind.into())) + notifications::table + .filter(notifications::kind.eq(kind.into())) .filter(notifications::object_id.eq(obj)) .get_result::(conn) .ok() @@ -67,25 +74,23 @@ impl Notification { pub fn to_json(&self, conn: &Connection) -> serde_json::Value { let mut json = json!(self); json["object"] = json!(match self.kind.as_ref() { - notification_kind::COMMENT => Comment::get(conn, self.object_id).map(|comment| - json!({ + notification_kind::COMMENT => Comment::get(conn, self.object_id).map(|comment| json!({ "post": comment.get_post(conn).to_json(conn), "user": comment.get_author(conn).to_json(conn), "id": comment.id - }) - ), - notification_kind::FOLLOW => Follow::get(conn, self.object_id).map(|follow| + })), + notification_kind::FOLLOW => Follow::get(conn, self.object_id).map(|follow| { json!({ "follower": User::get(conn, follow.follower_id).map(|u| u.to_json(conn)) }) - ), - notification_kind::LIKE => Like::get(conn, self.object_id).map(|like| + }), + notification_kind::LIKE => Like::get(conn, self.object_id).map(|like| { json!({ "post": Post::get(conn, like.post_id).map(|p| p.to_json(conn)), "user": User::get(conn, like.user_id).map(|u| u.to_json(conn)) }) - ), - notification_kind::MENTION => Mention::get(conn, self.object_id).map(|mention| + }), + notification_kind::MENTION => Mention::get(conn, self.object_id).map(|mention| { json!({ "user": mention.get_user(conn).map(|u| u.to_json(conn)), "url": mention.get_post(conn).map(|p| p.to_json(conn)["url"].clone()) @@ -95,19 +100,21 @@ impl Notification { json!(format!("{}#comment-{}", post["url"].as_str().expect("Notification::to_json: post url error"), comment.id)) }) }) - ), - notification_kind::RESHARE => Reshare::get(conn, self.object_id).map(|reshare| + }), + notification_kind::RESHARE => Reshare::get(conn, self.object_id).map(|reshare| { json!({ "post": reshare.get_post(conn).map(|p| p.to_json(conn)), "user": reshare.get_user(conn).map(|u| u.to_json(conn)) }) - ), - _ => Some(json!({})) + }), + _ => Some(json!({})), }); json } pub fn delete(&self, conn: &Connection) { - diesel::delete(self).execute(conn).expect("Notification::delete: notification deletion error"); + diesel::delete(self) + .execute(conn) + .expect("Notification::delete: notification deletion error"); } } diff --git a/plume-models/src/post_authors.rs b/plume-models/src/post_authors.rs index 56b11c65..47ff7da2 100644 --- a/plume-models/src/post_authors.rs +++ b/plume-models/src/post_authors.rs @@ -1,8 +1,8 @@ -use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use posts::Post; -use users::User; use schema::post_authors; +use users::User; #[derive(Clone, Queryable, Identifiable, Associations)] #[belongs_to(Post)] @@ -10,14 +10,14 @@ use schema::post_authors; pub struct PostAuthor { pub id: i32, pub post_id: i32, - pub author_id: i32 + pub author_id: i32, } #[derive(Insertable)] #[table_name = "post_authors"] pub struct NewPostAuthor { pub post_id: i32, - pub author_id: i32 + pub author_id: i32, } impl PostAuthor { diff --git a/plume-models/src/posts.rs b/plume-models/src/posts.rs index e40563ff..823281c7 100644 --- a/plume-models/src/posts.rs +++ b/plume-models/src/posts.rs @@ -1,36 +1,35 @@ use activitypub::{ activity::{Create, Delete, Update}, link, - object::{Article, Image, Tombstone} + object::{Article, Image, Tombstone}, }; use canapi::{Error, Provider}; use chrono::{NaiveDateTime, TimeZone, Utc}; -use diesel::{self, RunQueryDsl, QueryDsl, ExpressionMethods, BelongingToDsl}; +use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl}; use heck::{CamelCase, KebabCase}; use serde_json; -use plume_api::posts::PostEndpoint; -use plume_common::{ - activity_pub::{ - Hashtag, Source, - PUBLIC_VISIBILTY, Id, IntoId, - inbox::{Deletable, FromActivity} - }, - utils::md_to_html -}; -use {BASE_URL, ap_url, Connection}; use blogs::Blog; use instance::Instance; use likes::Like; use medias::Media; use mentions::Mention; +use plume_api::posts::PostEndpoint; +use plume_common::{ + activity_pub::{ + inbox::{Deletable, FromActivity}, + Hashtag, Id, IntoId, Source, PUBLIC_VISIBILTY, + }, + utils::md_to_html, +}; use post_authors::*; use reshares::Reshare; +use safe_string::SafeString; +use schema::posts; +use std::collections::HashSet; use tags::Tag; use users::User; -use schema::posts; -use safe_string::SafeString; -use std::collections::HashSet; +use {ap_url, Connection, BASE_URL}; #[derive(Queryable, Identifiable, Serialize, Clone, AsChangeset)] #[changeset_options(treat_none_as_null = "true")] @@ -68,24 +67,32 @@ pub struct NewPost { impl<'a> Provider<(&'a Connection, Option)> for Post { type Data = PostEndpoint; - fn get((conn, user_id): &(&'a Connection, Option), id: i32) -> Result { + fn get( + (conn, user_id): &(&'a Connection, Option), + id: i32, + ) -> Result { if let Some(post) = Post::get(conn, id) { if !post.published && !user_id.map(|u| post.is_author(conn, u)).unwrap_or(false) { - return Err(Error::Authorization("You are not authorized to access this post yet.".to_string())) + return Err(Error::Authorization( + "You are not authorized to access this post yet.".to_string(), + )); } Ok(PostEndpoint { id: Some(post.id), title: Some(post.title.clone()), subtitle: Some(post.subtitle.clone()), - content: Some(post.content.get().clone()) + content: Some(post.content.get().clone()), }) } else { Err(Error::NotFound("Request post was not found".to_string())) } } - fn list((conn, user_id): &(&'a Connection, Option), filter: PostEndpoint) -> Vec { + fn list( + (conn, user_id): &(&'a Connection, Option), + filter: PostEndpoint, + ) -> Vec { let mut query = posts::table.into_boxed(); if let Some(title) = filter.title { query = query.filter(posts::title.eq(title)); @@ -97,23 +104,36 @@ impl<'a> Provider<(&'a Connection, Option)> for Post { query = query.filter(posts::content.eq(content)); } - query.get_results::(*conn).map(|ps| ps.into_iter() - .filter(|p| p.published || user_id.map(|u| p.is_author(conn, u)).unwrap_or(false)) - .map(|p| PostEndpoint { - id: Some(p.id), - title: Some(p.title.clone()), - subtitle: Some(p.subtitle.clone()), - content: Some(p.content.get().clone()) + query + .get_results::(*conn) + .map(|ps| { + ps.into_iter() + .filter(|p| { + p.published || user_id.map(|u| p.is_author(conn, u)).unwrap_or(false) + }) + .map(|p| PostEndpoint { + id: Some(p.id), + title: Some(p.title.clone()), + subtitle: Some(p.subtitle.clone()), + content: Some(p.content.get().clone()), + }) + .collect() }) - .collect() - ).unwrap_or(vec![]) + .unwrap_or(vec![]) } - fn create((_conn, _user_id): &(&'a Connection, Option), _query: PostEndpoint) -> Result { + fn create( + (_conn, _user_id): &(&'a Connection, Option), + _query: PostEndpoint, + ) -> Result { unimplemented!() } - fn update((_conn, _user_id): &(&'a Connection, Option), _id: i32, _new_data: PostEndpoint) -> Result { + fn update( + (_conn, _user_id): &(&'a Connection, Option), + _id: i32, + _new_data: PostEndpoint, + ) -> Result { unimplemented!() } @@ -138,7 +158,8 @@ impl Post { use schema::tags; let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id); - posts::table.filter(posts::id.eq_any(ids)) + posts::table + .filter(posts::id.eq_any(ids)) .filter(posts::published.eq(true)) .order(posts::creation_date.desc()) .offset(min.into()) @@ -150,35 +171,45 @@ impl Post { pub fn count_for_tag(conn: &Connection, tag: String) -> i64 { use schema::tags; let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id); - *posts::table.filter(posts::id.eq_any(ids)) + *posts::table + .filter(posts::id.eq_any(ids)) .filter(posts::published.eq(true)) .count() .load(conn) .expect("Post::count_for_tag: counting error") - .iter().next().expect("Post::count_for_tag: no result error") + .iter() + .next() + .expect("Post::count_for_tag: no result error") } pub fn count_local(conn: &Connection) -> usize { use schema::post_authors; use schema::users; - let local_authors = users::table.filter(users::instance_id.eq(Instance::local_id(conn))).select(users::id); - let local_posts_id = post_authors::table.filter(post_authors::author_id.eq_any(local_authors)).select(post_authors::post_id); - posts::table.filter(posts::id.eq_any(local_posts_id)) + let local_authors = users::table + .filter(users::instance_id.eq(Instance::local_id(conn))) + .select(users::id); + let local_posts_id = post_authors::table + .filter(post_authors::author_id.eq_any(local_authors)) + .select(post_authors::post_id); + posts::table + .filter(posts::id.eq_any(local_posts_id)) .filter(posts::published.eq(true)) .load::(conn) .expect("Post::count_local: loading error") - .len()// TODO count in database? + .len() // TODO count in database? } pub fn count(conn: &Connection) -> i64 { - posts::table.filter(posts::published.eq(true)) + posts::table + .filter(posts::published.eq(true)) .count() .get_result(conn) .expect("Post::count: counting error") } pub fn get_recents(conn: &Connection, limit: i64) -> Vec { - posts::table.order(posts::creation_date.desc()) + posts::table + .order(posts::creation_date.desc()) .filter(posts::published.eq(true)) .limit(limit) .load::(conn) @@ -189,7 +220,8 @@ impl Post { use schema::post_authors; let posts = PostAuthor::belonging_to(author).select(post_authors::post_id); - posts::table.filter(posts::id.eq_any(posts)) + posts::table + .filter(posts::id.eq_any(posts)) .filter(posts::published.eq(true)) .order(posts::creation_date.desc()) .limit(limit) @@ -198,7 +230,8 @@ impl Post { } pub fn get_recents_for_blog(conn: &Connection, blog: &Blog, limit: i64) -> Vec { - posts::table.filter(posts::blog_id.eq(blog.id)) + posts::table + .filter(posts::blog_id.eq(blog.id)) .filter(posts::published.eq(true)) .order(posts::creation_date.desc()) .limit(limit) @@ -206,15 +239,17 @@ impl Post { .expect("Post::get_recents_for_blog: loading error") } - pub fn get_for_blog(conn: &Connection, blog:&Blog) -> Vec { - posts::table.filter(posts::blog_id.eq(blog.id)) + pub fn get_for_blog(conn: &Connection, blog: &Blog) -> Vec { + posts::table + .filter(posts::blog_id.eq(blog.id)) .filter(posts::published.eq(true)) .load::(conn) .expect("Post::get_for_blog:: loading error") } pub fn blog_page(conn: &Connection, blog: &Blog, (min, max): (i32, i32)) -> Vec { - posts::table.filter(posts::blog_id.eq(blog.id)) + posts::table + .filter(posts::blog_id.eq(blog.id)) .filter(posts::published.eq(true)) .order(posts::creation_date.desc()) .offset(min.into()) @@ -225,7 +260,8 @@ impl Post { /// Give a page of all the recent posts known to this instance (= federated timeline) pub fn get_recents_page(conn: &Connection, (min, max): (i32, i32)) -> Vec { - posts::table.order(posts::creation_date.desc()) + posts::table + .order(posts::creation_date.desc()) .filter(posts::published.eq(true)) .offset(min.into()) .limit((max - min).into()) @@ -234,12 +270,19 @@ impl Post { } /// Give a page of posts from a specific instance - pub fn get_instance_page(conn: &Connection, instance_id: i32, (min, max): (i32, i32)) -> Vec { + pub fn get_instance_page( + conn: &Connection, + instance_id: i32, + (min, max): (i32, i32), + ) -> Vec { use schema::blogs; - let blog_ids = blogs::table.filter(blogs::instance_id.eq(instance_id)).select(blogs::id); + let blog_ids = blogs::table + .filter(blogs::instance_id.eq(instance_id)) + .select(blogs::id); - posts::table.order(posts::creation_date.desc()) + posts::table + .order(posts::creation_date.desc()) .filter(posts::published.eq(true)) .filter(posts::blog_id.eq_any(blog_ids)) .offset(min.into()) @@ -249,13 +292,18 @@ impl Post { } /// Give a page of customized user feed, based on a list of followed users - pub fn user_feed_page(conn: &Connection, followed: Vec, (min, max): (i32, i32)) -> Vec { + pub fn user_feed_page( + conn: &Connection, + followed: Vec, + (min, max): (i32, i32), + ) -> Vec { use schema::post_authors; let post_ids = post_authors::table .filter(post_authors::author_id.eq_any(followed)) .select(post_authors::post_id); - posts::table.order(posts::creation_date.desc()) + posts::table + .order(posts::creation_date.desc()) .filter(posts::published.eq(true)) .filter(posts::id.eq_any(post_ids)) .offset(min.into()) @@ -268,7 +316,8 @@ impl Post { use schema::post_authors; let posts = PostAuthor::belonging_to(author).select(post_authors::post_id); - posts::table.order(posts::creation_date.desc()) + posts::table + .order(posts::creation_date.desc()) .filter(posts::published.eq(false)) .filter(posts::id.eq_any(posts)) .load::(conn) @@ -276,10 +325,13 @@ impl Post { } pub fn get_authors(&self, conn: &Connection) -> Vec { - use schema::users; use schema::post_authors; + use schema::users; let author_list = PostAuthor::belonging_to(self).select(post_authors::author_id); - users::table.filter(users::id.eq_any(author_list)).load::(conn).expect("Post::get_authors: loading error") + users::table + .filter(users::id.eq_any(author_list)) + .load::(conn) + .expect("Post::get_authors: loading error") } pub fn is_author(&self, conn: &Connection, author_id: i32) -> bool { @@ -293,23 +345,28 @@ impl Post { pub fn get_blog(&self, conn: &Connection) -> Blog { use schema::blogs; - blogs::table.filter(blogs::id.eq(self.blog_id)) + blogs::table + .filter(blogs::id.eq(self.blog_id)) .limit(1) .load::(conn) .expect("Post::get_blog: loading error") - .into_iter().nth(0).expect("Post::get_blog: no result error") + .into_iter() + .nth(0) + .expect("Post::get_blog: no result error") } pub fn get_likes(&self, conn: &Connection) -> Vec { use schema::likes; - likes::table.filter(likes::post_id.eq(self.id)) + likes::table + .filter(likes::post_id.eq(self.id)) .load::(conn) .expect("Post::get_likes: loading error") } pub fn get_reshares(&self, conn: &Connection) -> Vec { use schema::reshares; - reshares::table.filter(reshares::post_id.eq(self.id)) + reshares::table + .filter(reshares::post_id.eq(self.id)) .load::(conn) .expect("Post::get_reshares: loading error") } @@ -318,7 +375,8 @@ impl Post { if self.ap_url.len() == 0 { diesel::update(self) .set(posts::ap_url.eq(self.compute_id(conn))) - .execute(conn).expect("Post::update_ap_url: update error"); + .execute(conn) + .expect("Post::update_ap_url: update error"); Post::get(conn, self.id).expect("Post::update_ap_url: get error") } else { self.clone() @@ -326,7 +384,11 @@ impl Post { } pub fn get_receivers_urls(&self, conn: &Connection) -> Vec { - let followers = self.get_authors(conn).into_iter().map(|a| a.get_followers(conn)).collect::>>(); + let followers = self + .get_authors(conn) + .into_iter() + .map(|a| a.get_followers(conn)) + .collect::>>(); let to = followers.into_iter().fold(vec![], |mut acc, f| { for x in f { acc.push(x.ap_url); @@ -340,74 +402,170 @@ impl Post { let mut to = self.get_receivers_urls(conn); to.push(PUBLIC_VISIBILTY.to_string()); - let mut mentions_json = Mention::list_for_post(conn, self.id).into_iter().map(|m| json!(m.to_activity(conn))).collect::>(); - let mut tags_json = Tag::for_post(conn, self.id).into_iter().map(|t| json!(t.into_activity(conn))).collect::>(); + let mut mentions_json = Mention::list_for_post(conn, self.id) + .into_iter() + .map(|m| json!(m.to_activity(conn))) + .collect::>(); + let mut tags_json = Tag::for_post(conn, self.id) + .into_iter() + .map(|t| json!(t.into_activity(conn))) + .collect::>(); mentions_json.append(&mut tags_json); let mut article = Article::default(); - article.object_props.set_name_string(self.title.clone()).expect("Post::into_activity: name error"); - article.object_props.set_id_string(self.ap_url.clone()).expect("Post::into_activity: id error"); + article + .object_props + .set_name_string(self.title.clone()) + .expect("Post::into_activity: name error"); + article + .object_props + .set_id_string(self.ap_url.clone()) + .expect("Post::into_activity: id error"); - let mut authors = self.get_authors(conn).into_iter().map(|x| Id::new(x.ap_url)).collect::>(); + let mut authors = self + .get_authors(conn) + .into_iter() + .map(|x| Id::new(x.ap_url)) + .collect::>(); authors.push(self.get_blog(conn).into_id()); // add the blog URL here too - article.object_props.set_attributed_to_link_vec::(authors).expect("Post::into_activity: attributedTo error"); - article.object_props.set_content_string(self.content.get().clone()).expect("Post::into_activity: content error"); - article.ap_object_props.set_source_object(Source { - content: self.source.clone(), - media_type: String::from("text/markdown"), - }).expect("Post::into_activity: source error"); - article.object_props.set_published_utctime(Utc.from_utc_datetime(&self.creation_date)).expect("Post::into_activity: published error"); - article.object_props.set_summary_string(self.subtitle.clone()).expect("Post::into_activity: summary error"); + article + .object_props + .set_attributed_to_link_vec::(authors) + .expect("Post::into_activity: attributedTo error"); + article + .object_props + .set_content_string(self.content.get().clone()) + .expect("Post::into_activity: content error"); + article + .ap_object_props + .set_source_object(Source { + content: self.source.clone(), + media_type: String::from("text/markdown"), + }) + .expect("Post::into_activity: source error"); + article + .object_props + .set_published_utctime(Utc.from_utc_datetime(&self.creation_date)) + .expect("Post::into_activity: published error"); + article + .object_props + .set_summary_string(self.subtitle.clone()) + .expect("Post::into_activity: summary error"); article.object_props.tag = Some(json!(mentions_json)); if let Some(media_id) = self.cover_id { let media = Media::get(conn, media_id).expect("Post::into_activity: get cover error"); let mut cover = Image::default(); - cover.object_props.set_url_string(media.url(conn)).expect("Post::into_activity: icon.url error"); + cover + .object_props + .set_url_string(media.url(conn)) + .expect("Post::into_activity: icon.url error"); if media.sensitive { - cover.object_props.set_summary_string(media.content_warning.unwrap_or(String::new())).expect("Post::into_activity: icon.summary error"); + cover + .object_props + .set_summary_string(media.content_warning.unwrap_or(String::new())) + .expect("Post::into_activity: icon.summary error"); } - cover.object_props.set_content_string(media.alt_text).expect("Post::into_activity: icon.content error"); - cover.object_props.set_attributed_to_link_vec(vec![ - User::get(conn, media.owner_id).expect("Post::into_activity: media owner not found").into_id() - ]).expect("Post::into_activity: icon.attributedTo error"); - article.object_props.set_icon_object(cover).expect("Post::into_activity: icon error"); + cover + .object_props + .set_content_string(media.alt_text) + .expect("Post::into_activity: icon.content error"); + cover + .object_props + .set_attributed_to_link_vec(vec![ + User::get(conn, media.owner_id) + .expect("Post::into_activity: media owner not found") + .into_id(), + ]) + .expect("Post::into_activity: icon.attributedTo error"); + article + .object_props + .set_icon_object(cover) + .expect("Post::into_activity: icon error"); } - article.object_props.set_url_string(self.ap_url.clone()).expect("Post::into_activity: url error"); - article.object_props.set_to_link_vec::(to.into_iter().map(Id::new).collect()).expect("Post::into_activity: to error"); - article.object_props.set_cc_link_vec::(vec![]).expect("Post::into_activity: cc error"); + article + .object_props + .set_url_string(self.ap_url.clone()) + .expect("Post::into_activity: url error"); + article + .object_props + .set_to_link_vec::(to.into_iter().map(Id::new).collect()) + .expect("Post::into_activity: to error"); + article + .object_props + .set_cc_link_vec::(vec![]) + .expect("Post::into_activity: cc error"); article } pub fn create_activity(&self, conn: &Connection) -> Create { let article = self.into_activity(conn); let mut act = Create::default(); - act.object_props.set_id_string(format!("{}activity", self.ap_url)).expect("Post::create_activity: id error"); - act.object_props.set_to_link_vec::(article.object_props.to_link_vec().expect("Post::create_activity: Couldn't copy 'to'")) + act.object_props + .set_id_string(format!("{}activity", self.ap_url)) + .expect("Post::create_activity: id error"); + act.object_props + .set_to_link_vec::( + article + .object_props + .to_link_vec() + .expect("Post::create_activity: Couldn't copy 'to'"), + ) .expect("Post::create_activity: to error"); - act.object_props.set_cc_link_vec::(article.object_props.cc_link_vec().expect("Post::create_activity: Couldn't copy 'cc'")) + act.object_props + .set_cc_link_vec::( + article + .object_props + .cc_link_vec() + .expect("Post::create_activity: Couldn't copy 'cc'"), + ) .expect("Post::create_activity: cc error"); - act.create_props.set_actor_link(Id::new(self.get_authors(conn)[0].clone().ap_url)).expect("Post::create_activity: actor error"); - act.create_props.set_object_object(article).expect("Post::create_activity: object error"); + act.create_props + .set_actor_link(Id::new(self.get_authors(conn)[0].clone().ap_url)) + .expect("Post::create_activity: actor error"); + act.create_props + .set_object_object(article) + .expect("Post::create_activity: object error"); act } pub fn update_activity(&self, conn: &Connection) -> Update { let article = self.into_activity(conn); let mut act = Update::default(); - act.object_props.set_id_string(format!("{}/update-{}", self.ap_url, Utc::now().timestamp())).expect("Post::update_activity: id error"); - act.object_props.set_to_link_vec::(article.object_props.to_link_vec().expect("Post::update_activity: Couldn't copy 'to'")) + act.object_props + .set_id_string(format!("{}/update-{}", self.ap_url, Utc::now().timestamp())) + .expect("Post::update_activity: id error"); + act.object_props + .set_to_link_vec::( + article + .object_props + .to_link_vec() + .expect("Post::update_activity: Couldn't copy 'to'"), + ) .expect("Post::update_activity: to error"); - act.object_props.set_cc_link_vec::(article.object_props.cc_link_vec().expect("Post::update_activity: Couldn't copy 'cc'")) + act.object_props + .set_cc_link_vec::( + article + .object_props + .cc_link_vec() + .expect("Post::update_activity: Couldn't copy 'cc'"), + ) .expect("Post::update_activity: cc error"); - act.update_props.set_actor_link(Id::new(self.get_authors(conn)[0].clone().ap_url)).expect("Post::update_activity: actor error"); - act.update_props.set_object_object(article).expect("Article::update_activity: object error"); + act.update_props + .set_actor_link(Id::new(self.get_authors(conn)[0].clone().ap_url)) + .expect("Post::update_activity: actor error"); + act.update_props + .set_object_object(article) + .expect("Article::update_activity: object error"); act } pub fn handle_update(conn: &Connection, updated: Article) { - let id = updated.object_props.id_string().expect("Post::handle_update: id error"); + let id = updated + .object_props + .id_string() + .expect("Post::handle_update: id error"); let mut post = Post::find_by_ap_url(conn, id).expect("Post::handle_update: finding error"); if let Ok(title) = updated.object_props.name_string() { @@ -431,7 +589,11 @@ impl Post { post.source = source.content; } - let mut txt_hashtags = md_to_html(&post.source).2.into_iter().map(|s| s.to_camel_case()).collect::>(); + let mut txt_hashtags = md_to_html(&post.source) + .2 + .into_iter() + .map(|s| s.to_camel_case()) + .collect::>(); if let Some(serde_json::Value::Array(mention_tags)) = updated.object_props.tag.clone() { let mut mentions = vec![]; let mut tags = vec![]; @@ -443,13 +605,16 @@ impl Post { serde_json::from_value::(tag.clone()) .map(|t| { - let tag_name = t.name_string().expect("Post::from_activity: tag name error"); + let tag_name = t + .name_string() + .expect("Post::from_activity: tag name error"); if txt_hashtags.remove(&tag_name) { hashtags.push(t); } else { tags.push(t); } - }).ok(); + }) + .ok(); } post.update_mentions(conn, mentions); post.update_tags(conn, tags); @@ -460,34 +625,76 @@ impl Post { } pub fn update_mentions(&self, conn: &Connection, mentions: Vec) { - let mentions = mentions.into_iter().map(|m| (m.link_props.href_string().ok() - .and_then(|ap_url| User::find_by_ap_url(conn, ap_url)) - .map(|u| u.id),m)) - .filter_map(|(id, m)| if let Some(id)=id {Some((m,id))} else {None}).collect::>(); + let mentions = mentions + .into_iter() + .map(|m| { + ( + m.link_props + .href_string() + .ok() + .and_then(|ap_url| User::find_by_ap_url(conn, ap_url)) + .map(|u| u.id), + m, + ) + }) + .filter_map(|(id, m)| { + if let Some(id) = id { + Some((m, id)) + } else { + None + } + }) + .collect::>(); let old_mentions = Mention::list_for_post(&conn, self.id); - let old_user_mentioned = old_mentions.iter() - .map(|m| m.mentioned_id).collect::>(); - for (m,id) in mentions.iter() { - if !old_user_mentioned.contains(&id) { + let old_user_mentioned = old_mentions + .iter() + .map(|m| m.mentioned_id) + .collect::>(); + for (m, id) in mentions.iter() { + if !old_user_mentioned.contains(&id) { Mention::from_activity(&*conn, m.clone(), self.id, true, true); } } - let new_mentions = mentions.into_iter().map(|(_m,id)| id).collect::>(); - for m in old_mentions.iter().filter(|m| !new_mentions.contains(&m.mentioned_id)) { + let new_mentions = mentions + .into_iter() + .map(|(_m, id)| id) + .collect::>(); + for m in old_mentions + .iter() + .filter(|m| !new_mentions.contains(&m.mentioned_id)) + { m.delete(&conn); } } pub fn update_tags(&self, conn: &Connection, tags: Vec) { - let tags_name = tags.iter().filter_map(|t| t.name_string().ok()).collect::>(); + let tags_name = tags + .iter() + .filter_map(|t| t.name_string().ok()) + .collect::>(); - let old_tags = Tag::for_post(&*conn, self.id).into_iter().collect::>(); - let old_tags_name = old_tags.iter().filter_map(|tag| if !tag.is_hashtag {Some(tag.tag.clone())} else {None}).collect::>(); + let old_tags = Tag::for_post(&*conn, self.id) + .into_iter() + .collect::>(); + let old_tags_name = old_tags + .iter() + .filter_map(|tag| { + if !tag.is_hashtag { + Some(tag.tag.clone()) + } else { + None + } + }) + .collect::>(); for t in tags.into_iter() { - if !t.name_string().map(|n| old_tags_name.contains(&n)).unwrap_or(true) { + if !t + .name_string() + .map(|n| old_tags_name.contains(&n)) + .unwrap_or(true) + { Tag::from_activity(conn, t, self.id, false); } } @@ -500,13 +707,31 @@ impl Post { } pub fn update_hashtags(&self, conn: &Connection, tags: Vec) { - let tags_name = tags.iter().filter_map(|t| t.name_string().ok()).collect::>(); + let tags_name = tags + .iter() + .filter_map(|t| t.name_string().ok()) + .collect::>(); - let old_tags = Tag::for_post(&*conn, self.id).into_iter().collect::>(); - let old_tags_name = old_tags.iter().filter_map(|tag| if tag.is_hashtag {Some(tag.tag.clone())} else {None}).collect::>(); + let old_tags = Tag::for_post(&*conn, self.id) + .into_iter() + .collect::>(); + let old_tags_name = old_tags + .iter() + .filter_map(|tag| { + if tag.is_hashtag { + Some(tag.tag.clone()) + } else { + None + } + }) + .collect::>(); for t in tags.into_iter() { - if !t.name_string().map(|n| old_tags_name.contains(&n)).unwrap_or(true) { + if !t + .name_string() + .map(|n| old_tags_name.contains(&n)) + .unwrap_or(true) + { Tag::from_activity(conn, t, self.id, true); } } @@ -532,16 +757,26 @@ impl Post { } pub fn compute_id(&self, conn: &Connection) -> String { - ap_url(format!("{}/~/{}/{}/", BASE_URL.as_str(), self.get_blog(conn).get_fqn(conn), self.slug)) + ap_url(format!( + "{}/~/{}/{}/", + BASE_URL.as_str(), + self.get_blog(conn).get_fqn(conn), + self.slug + )) } } impl FromActivity for Post { fn from_activity(conn: &Connection, article: Article, _actor: Id) -> Post { - if let Some(post) = Post::find_by_ap_url(conn, article.object_props.id_string().unwrap_or(String::new())) { + if let Some(post) = Post::find_by_ap_url( + conn, + article.object_props.id_string().unwrap_or(String::new()), + ) { post } else { - let (blog, authors) = article.object_props.attributed_to_link_vec::() + let (blog, authors) = article + .object_props + .attributed_to_link_vec::() .expect("Post::from_activity: attributedTo error") .into_iter() .fold((None, vec![]), |(blog, mut authors), link| { @@ -550,39 +785,78 @@ impl FromActivity for Post { Some(user) => { authors.push(user); (blog, authors) - }, - None => (blog.or_else(|| Blog::from_url(conn, url)), authors) + } + None => (blog.or_else(|| Blog::from_url(conn, url)), authors), } }); - let cover = article.object_props.icon_object::().ok() + let cover = article + .object_props + .icon_object::() + .ok() .and_then(|img| Media::from_activity(conn, img).map(|m| m.id)); - let title = article.object_props.name_string().expect("Post::from_activity: title error"); - let post = Post::insert(conn, NewPost { - blog_id: blog.expect("Post::from_activity: blog not found error").id, - slug: title.to_kebab_case(), - title: title, - content: SafeString::new(&article.object_props.content_string().expect("Post::from_activity: content error")), - published: true, - license: String::from("CC-BY-SA"), // TODO - // FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields - ap_url: article.object_props.url_string().unwrap_or(article.object_props.id_string().expect("Post::from_activity: url + id error")), - creation_date: Some(article.object_props.published_utctime().expect("Post::from_activity: published error").naive_utc()), - subtitle: article.object_props.summary_string().expect("Post::from_activity: summary error"), - source: article.ap_object_props.source_object::().expect("Post::from_activity: source error").content, - cover_id: cover, - }); + let title = article + .object_props + .name_string() + .expect("Post::from_activity: title error"); + let post = Post::insert( + conn, + NewPost { + blog_id: blog.expect("Post::from_activity: blog not found error").id, + slug: title.to_kebab_case(), + title: title, + content: SafeString::new( + &article + .object_props + .content_string() + .expect("Post::from_activity: content error"), + ), + published: true, + license: String::from("CC-BY-SA"), // TODO + // FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields + ap_url: article.object_props.url_string().unwrap_or( + article + .object_props + .id_string() + .expect("Post::from_activity: url + id error"), + ), + creation_date: Some( + article + .object_props + .published_utctime() + .expect("Post::from_activity: published error") + .naive_utc(), + ), + subtitle: article + .object_props + .summary_string() + .expect("Post::from_activity: summary error"), + source: article + .ap_object_props + .source_object::() + .expect("Post::from_activity: source error") + .content, + cover_id: cover, + }, + ); for author in authors.into_iter() { - PostAuthor::insert(conn, NewPostAuthor { - post_id: post.id, - author_id: author.id - }); + PostAuthor::insert( + conn, + NewPostAuthor { + post_id: post.id, + author_id: author.id, + }, + ); } // save mentions and tags - let mut hashtags = md_to_html(&post.source).2.into_iter().map(|s| s.to_camel_case()).collect::>(); + let mut hashtags = md_to_html(&post.source) + .2 + .into_iter() + .map(|s| s.to_camel_case()) + .collect::>(); if let Some(serde_json::Value::Array(tags)) = article.object_props.tag.clone() { for tag in tags.into_iter() { serde_json::from_value::(tag.clone()) @@ -591,7 +865,9 @@ impl FromActivity for Post { serde_json::from_value::(tag.clone()) .map(|t| { - let tag_name = t.name_string().expect("Post::from_activity: tag name error"); + let tag_name = t + .name_string() + .expect("Post::from_activity: tag name error"); Tag::from_activity(conn, t, post.id, hashtags.remove(&tag_name)); }) .ok(); @@ -605,28 +881,44 @@ impl FromActivity for Post { impl Deletable for Post { fn delete(&self, conn: &Connection) -> Delete { let mut act = Delete::default(); - act.delete_props.set_actor_link(self.get_authors(conn)[0].clone().into_id()).expect("Post::delete: actor error"); + act.delete_props + .set_actor_link(self.get_authors(conn)[0].clone().into_id()) + .expect("Post::delete: actor error"); let mut tombstone = Tombstone::default(); - tombstone.object_props.set_id_string(self.ap_url.clone()).expect("Post::delete: object.id error"); - act.delete_props.set_object_object(tombstone).expect("Post::delete: object error"); + tombstone + .object_props + .set_id_string(self.ap_url.clone()) + .expect("Post::delete: object.id error"); + act.delete_props + .set_object_object(tombstone) + .expect("Post::delete: object error"); - act.object_props.set_id_string(format!("{}#delete", self.ap_url)).expect("Post::delete: id error"); - act.object_props.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILTY)]).expect("Post::delete: to error"); + act.object_props + .set_id_string(format!("{}#delete", self.ap_url)) + .expect("Post::delete: id error"); + act.object_props + .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILTY)]) + .expect("Post::delete: to error"); for m in Mention::list_for_post(&conn, self.id) { m.delete(conn); } - diesel::delete(self).execute(conn).expect("Post::delete: DB error"); + diesel::delete(self) + .execute(conn) + .expect("Post::delete: DB error"); act } fn delete_id(id: String, actor_id: String, conn: &Connection) { let actor = User::find_by_ap_url(conn, actor_id); let post = Post::find_by_ap_url(conn, id); - let can_delete = actor.and_then(|act| - post.clone().map(|p| p.get_authors(conn).into_iter().any(|a| act.id == a.id)) - ).unwrap_or(false); + let can_delete = actor + .and_then(|act| { + post.clone() + .map(|p| p.get_authors(conn).into_iter().any(|a| act.id == a.id)) + }) + .unwrap_or(false); if can_delete { post.map(|p| p.delete(conn)); } diff --git a/plume-models/src/reshares.rs b/plume-models/src/reshares.rs index 575ee493..d3006f24 100644 --- a/plume-models/src/reshares.rs +++ b/plume-models/src/reshares.rs @@ -1,13 +1,16 @@ use activitypub::activity::{Announce, Undo}; use chrono::NaiveDateTime; -use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; -use plume_common::activity_pub::{Id, IntoId, inbox::{FromActivity, Notify, Deletable}, PUBLIC_VISIBILTY}; -use Connection; use notifications::*; +use plume_common::activity_pub::{ + inbox::{Deletable, FromActivity, Notify}, + Id, IntoId, PUBLIC_VISIBILTY, +}; use posts::Post; -use users::User; use schema::reshares; +use users::User; +use Connection; #[derive(Clone, Serialize, Deserialize, Queryable, Identifiable)] pub struct Reshare { @@ -15,7 +18,7 @@ pub struct Reshare { pub user_id: i32, pub post_id: i32, pub ap_url: String, - pub creation_date: NaiveDateTime + pub creation_date: NaiveDateTime, } #[derive(Insertable)] @@ -23,29 +26,40 @@ pub struct Reshare { pub struct NewReshare { pub user_id: i32, pub post_id: i32, - pub ap_url: String + pub ap_url: String, } impl Reshare { insert!(reshares, NewReshare); get!(reshares); find_by!(reshares, find_by_ap_url, ap_url as String); - find_by!(reshares, find_by_user_on_post, user_id as i32, post_id as i32); + find_by!( + reshares, + find_by_user_on_post, + user_id as i32, + post_id as i32 + ); pub fn update_ap_url(&self, conn: &Connection) { if self.ap_url.len() == 0 { diesel::update(self) .set(reshares::ap_url.eq(format!( - "{}/reshare/{}", - User::get(conn, self.user_id).expect("Reshare::update_ap_url: user error").ap_url, - Post::get(conn, self.post_id).expect("Reshare::update_ap_url: post error").ap_url - ))) - .execute(conn).expect("Reshare::update_ap_url: update error"); + "{}/reshare/{}", + User::get(conn, self.user_id) + .expect("Reshare::update_ap_url: user error") + .ap_url, + Post::get(conn, self.post_id) + .expect("Reshare::update_ap_url: post error") + .ap_url + ))) + .execute(conn) + .expect("Reshare::update_ap_url: update error"); } } pub fn get_recents_for_author(conn: &Connection, user: &User, limit: i64) -> Vec { - reshares::table.filter(reshares::user_id.eq(user.id)) + reshares::table + .filter(reshares::user_id.eq(user.id)) .order(reshares::creation_date.desc()) .limit(limit) .load::(conn) @@ -62,13 +76,29 @@ impl Reshare { pub fn into_activity(&self, conn: &Connection) -> Announce { let mut act = Announce::default(); - act.announce_props.set_actor_link(User::get(conn, self.user_id).expect("Reshare::into_activity: user error").into_id()) + act.announce_props + .set_actor_link( + User::get(conn, self.user_id) + .expect("Reshare::into_activity: user error") + .into_id(), + ) .expect("Reshare::into_activity: actor error"); - act.announce_props.set_object_link(Post::get(conn, self.post_id).expect("Reshare::into_activity: post error").into_id()) + act.announce_props + .set_object_link( + Post::get(conn, self.post_id) + .expect("Reshare::into_activity: post error") + .into_id(), + ) .expect("Reshare::into_activity: object error"); - act.object_props.set_id_string(self.ap_url.clone()).expect("Reshare::into_activity: id error"); - act.object_props.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())).expect("Reshare::into_activity: to error"); - act.object_props.set_cc_link_vec::(vec![]).expect("Reshare::into_activity: cc error"); + act.object_props + .set_id_string(self.ap_url.clone()) + .expect("Reshare::into_activity: id error"); + act.object_props + .set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())) + .expect("Reshare::into_activity: to error"); + act.object_props + .set_cc_link_vec::(vec![]) + .expect("Reshare::into_activity: cc error"); act } @@ -76,13 +106,33 @@ impl Reshare { impl FromActivity for Reshare { fn from_activity(conn: &Connection, announce: Announce, _actor: Id) -> Reshare { - let user = User::from_url(conn, announce.announce_props.actor_link::().expect("Reshare::from_activity: actor error").into()); - let post = Post::find_by_ap_url(conn, announce.announce_props.object_link::().expect("Reshare::from_activity: object error").into()); - 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, - ap_url: announce.object_props.id_string().unwrap_or(String::from("")) - }); + let user = User::from_url( + conn, + announce + .announce_props + .actor_link::() + .expect("Reshare::from_activity: actor error") + .into(), + ); + let post = Post::find_by_ap_url( + conn, + announce + .announce_props + .object_link::() + .expect("Reshare::from_activity: object error") + .into(), + ); + 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, + ap_url: announce + .object_props + .id_string() + .unwrap_or(String::from("")), + }, + ); reshare.notify(conn); reshare } @@ -92,30 +142,51 @@ impl Notify for Reshare { fn notify(&self, conn: &Connection) { let post = self.get_post(conn).expect("Reshare::notify: post error"); for author in post.get_authors(conn) { - Notification::insert(conn, NewNotification { - kind: notification_kind::RESHARE.to_string(), - object_id: self.id, - user_id: author.id - }); + Notification::insert( + conn, + NewNotification { + kind: notification_kind::RESHARE.to_string(), + object_id: self.id, + user_id: author.id, + }, + ); } } } impl Deletable for Reshare { fn delete(&self, conn: &Connection) -> Undo { - diesel::delete(self).execute(conn).expect("Reshare::delete: delete error"); + diesel::delete(self) + .execute(conn) + .expect("Reshare::delete: delete error"); // delete associated notification if any if let Some(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) { - diesel::delete(¬if).execute(conn).expect("Reshare::delete: notification error"); + diesel::delete(¬if) + .execute(conn) + .expect("Reshare::delete: notification error"); } 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"); - act.undo_props.set_object_object(self.into_activity(conn)).expect("Reshare::delete: object error"); - act.object_props.set_id_string(format!("{}#delete", self.ap_url)).expect("Reshare::delete: id error"); - act.object_props.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())).expect("Reshare::delete: to error"); - act.object_props.set_cc_link_vec::(vec![]).expect("Reshare::delete: cc error"); + act.undo_props + .set_actor_link( + User::get(conn, self.user_id) + .expect("Reshare::delete: user error") + .into_id(), + ) + .expect("Reshare::delete: actor error"); + act.undo_props + .set_object_object(self.into_activity(conn)) + .expect("Reshare::delete: object error"); + act.object_props + .set_id_string(format!("{}#delete", self.ap_url)) + .expect("Reshare::delete: id error"); + act.object_props + .set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())) + .expect("Reshare::delete: to error"); + act.object_props + .set_cc_link_vec::(vec![]) + .expect("Reshare::delete: cc error"); act } diff --git a/plume-models/src/safe_string.rs b/plume-models/src/safe_string.rs index 26c7a210..bef1aa73 100644 --- a/plume-models/src/safe_string.rs +++ b/plume-models/src/safe_string.rs @@ -1,13 +1,19 @@ use ammonia::{Builder, UrlRelative}; -use serde::{self, Serialize, Deserialize, - Serializer, Deserializer, de::Visitor}; -use std::{fmt::{self, Display}, - borrow::{Borrow, Cow}, io::Write, - iter, ops::Deref}; -use diesel::{self, deserialize::Queryable, - types::ToSql, +use diesel::{ + self, + deserialize::Queryable, + serialize::{self, Output}, sql_types::Text, - serialize::{self, Output}}; + types::ToSql, +}; +use serde::{self, de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; +use std::{ + borrow::{Borrow, Cow}, + fmt::{self, Display}, + io::Write, + iter, + ops::Deref, +}; lazy_static! { static ref CLEAN: Builder<'static> = { @@ -16,17 +22,18 @@ lazy_static! { .add_tags(iter::once("iframe")) .id_prefix(Some("postcontent-")) .url_relative(UrlRelative::Custom(Box::new(url_add_prefix))) - .add_tag_attributes("iframe", - ["width", "height", "src", "frameborder"] - .iter() - .map(|&v| v)); + .add_tag_attributes( + "iframe", + ["width", "height", "src", "frameborder"].iter().map(|&v| v), + ); b }; } fn url_add_prefix(url: &str) -> Option> { - if url.starts_with('#') && ! url.starts_with("#postcontent-") {//if start with an # - let mut new_url = "#postcontent-".to_owned();//change to valid id + if url.starts_with('#') && !url.starts_with("#postcontent-") { + //if start with an # + let mut new_url = "#postcontent-".to_owned(); //change to valid id new_url.push_str(&url[1..]); Some(Cow::Owned(new_url)) } else { @@ -34,15 +41,15 @@ fn url_add_prefix(url: &str) -> Option> { } } -#[derive(Debug, Clone, AsExpression, FromSqlRow, Default)] +#[derive(Debug, Clone, PartialEq, AsExpression, FromSqlRow, Default)] #[sql_type = "Text"] -pub struct SafeString{ +pub struct SafeString { value: String, } -impl SafeString{ -pub fn new(value: &str) -> Self { - SafeString{ +impl SafeString { + pub fn new(value: &str) -> Self { + SafeString { value: CLEAN.clean(&value).to_string(), } } @@ -56,7 +63,9 @@ pub fn new(value: &str) -> Self { impl Serialize for SafeString { fn serialize(&self, serializer: S) -> Result - where S: Serializer, { + where + S: Serializer, + { serializer.serialize_str(&self.value) } } @@ -66,22 +75,24 @@ struct SafeStringVisitor; impl<'de> Visitor<'de> for SafeStringVisitor { type Value = SafeString; - fn expecting(&self, formatter:&mut fmt::Formatter) -> fmt::Result { + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a string") } fn visit_str(self, value: &str) -> Result - where E: serde::de::Error{ + where + E: serde::de::Error, + { Ok(SafeString::new(value)) } } impl<'de> Deserialize<'de> for SafeString { fn deserialize(deserializer: D) -> Result - where D: Deserializer<'de>, { - Ok( - deserializer.deserialize_string(SafeStringVisitor)? - ) + where + D: Deserializer<'de>, + { + Ok(deserializer.deserialize_string(SafeStringVisitor)?) } } @@ -101,17 +112,16 @@ impl Queryable for SafeString { } } - impl ToSql for SafeString where DB: diesel::backend::Backend, - str: ToSql, { + str: ToSql, +{ fn to_sql(&self, out: &mut Output) -> serialize::Result { str::to_sql(&self.value, out) } } - impl Borrow for SafeString { fn borrow(&self) -> &str { &self.value @@ -137,8 +147,8 @@ impl AsRef for SafeString { } } -use rocket::request::FromFormValue; use rocket::http::RawStr; +use rocket::request::FromFormValue; impl<'v> FromFormValue<'v> for SafeString { type Error = &'v RawStr; diff --git a/plume-models/src/tags.rs b/plume-models/src/tags.rs index ed704bd1..5494dd43 100644 --- a/plume-models/src/tags.rs +++ b/plume-models/src/tags.rs @@ -1,16 +1,16 @@ -use diesel::{self, ExpressionMethods, RunQueryDsl, QueryDsl}; +use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; -use plume_common::activity_pub::Hashtag; -use {ap_url, Connection}; use instance::Instance; +use plume_common::activity_pub::Hashtag; use schema::tags; +use {ap_url, Connection}; #[derive(Clone, Identifiable, Serialize, Queryable)] pub struct Tag { pub id: i32, pub tag: String, pub is_hashtag: bool, - pub post_id: i32 + pub post_id: i32, } #[derive(Insertable)] @@ -18,7 +18,7 @@ pub struct Tag { pub struct NewTag { pub tag: String, pub is_hashtag: bool, - pub post_id: i32 + pub post_id: i32, } impl Tag { @@ -29,33 +29,46 @@ impl Tag { pub fn into_activity(&self, conn: &Connection) -> Hashtag { let mut ht = Hashtag::default(); - ht.set_href_string(ap_url(format!("{}/tag/{}", - Instance::get_local(conn).expect("Tag::into_activity: local instance not found error").public_domain, - self.tag) - )).expect("Tag::into_activity: href error"); - ht.set_name_string(self.tag.clone()).expect("Tag::into_activity: name error"); + ht.set_href_string(ap_url(format!( + "{}/tag/{}", + Instance::get_local(conn) + .expect("Tag::into_activity: local instance not found error") + .public_domain, + self.tag + ))).expect("Tag::into_activity: href error"); + ht.set_name_string(self.tag.clone()) + .expect("Tag::into_activity: name error"); ht } pub fn from_activity(conn: &Connection, tag: Hashtag, post: i32, is_hashtag: bool) -> Tag { - Tag::insert(conn, NewTag { - tag: tag.name_string().expect("Tag::from_activity: name error"), - is_hashtag, - post_id: post - }) + Tag::insert( + conn, + NewTag { + tag: tag.name_string().expect("Tag::from_activity: name error"), + is_hashtag, + post_id: post, + }, + ) } pub fn build_activity(conn: &Connection, tag: String) -> Hashtag { let mut ht = Hashtag::default(); - ht.set_href_string(ap_url(format!("{}/tag/{}", - Instance::get_local(conn).expect("Tag::into_activity: local instance not found error").public_domain, - tag) - )).expect("Tag::into_activity: href error"); - ht.set_name_string(tag).expect("Tag::into_activity: name error"); + ht.set_href_string(ap_url(format!( + "{}/tag/{}", + Instance::get_local(conn) + .expect("Tag::into_activity: local instance not found error") + .public_domain, + tag + ))).expect("Tag::into_activity: href error"); + ht.set_name_string(tag) + .expect("Tag::into_activity: name error"); ht } pub fn delete(&self, conn: &Connection) { - diesel::delete(self).execute(conn).expect("Tag::delete: database error"); + diesel::delete(self) + .execute(conn) + .expect("Tag::delete: database error"); } } diff --git a/plume-models/src/users.rs b/plume-models/src/users.rs index 35afb73f..fc46612f 100644 --- a/plume-models/src/users.rs +++ b/plume-models/src/users.rs @@ -1,39 +1,37 @@ use activitypub::{ - Activity, Actor, Object, Endpoint, CustomObject, - actor::Person, - collection::OrderedCollection, - object::Image, + actor::Person, collection::OrderedCollection, object::Image, Activity, Actor, CustomObject, + Endpoint, Object, }; use bcrypt; -use chrono::{Utc, NaiveDateTime}; -use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods, BelongingToDsl}; +use chrono::{NaiveDateTime, Utc}; +use diesel::{self, BelongingToDsl, ExpressionMethods, QueryDsl, RunQueryDsl}; use openssl::{ hash::MessageDigest, pkey::{PKey, Private}, rsa::Rsa, - sign + sign, }; use plume_common::activity_pub::{ - ap_accept_header, ActivityStream, Id, IntoId, ApSignature, PublicKey, + ap_accept_header, inbox::{Deletable, WithInbox}, - sign::{Signer, gen_keypair} + sign::{gen_keypair, Signer}, + ActivityStream, ApSignature, Id, IntoId, PublicKey, }; use reqwest::{ + header::{HeaderValue, ACCEPT}, Client, - header::{ACCEPT, HeaderValue} }; use rocket::{ + outcome::IntoOutcome, request::{self, FromRequest, Request}, - outcome::IntoOutcome }; use serde_json; use url::Url; use webfinger::*; -use {BASE_URL, USE_HTTPS, ap_url, Connection}; -use db_conn::DbConn; -use blogs::Blog; use blog_authors::BlogAuthor; +use blogs::Blog; +use db_conn::DbConn; use follows::Follow; use instance::*; use likes::Like; @@ -43,6 +41,7 @@ use posts::Post; use reshares::Reshare; use safe_string::SafeString; use schema::users; +use {ap_url, Connection, BASE_URL, USE_HTTPS}; pub const AUTH_COOKIE: &'static str = "user_id"; @@ -67,7 +66,7 @@ pub struct User { pub shared_inbox_url: Option, pub followers_endpoint: String, pub avatar_id: Option, - pub last_fetched_date: NaiveDateTime + pub last_fetched_date: NaiveDateTime, } #[derive(Insertable)] @@ -100,7 +99,8 @@ impl User { find_by!(users, find_by_ap_url, ap_url as String); pub fn one_by_instance(conn: &Connection) -> Vec { - users::table.filter(users::instance_id.eq_any(users::table.select(users::instance_id).distinct())) + users::table + .filter(users::instance_id.eq_any(users::table.select(users::instance_id).distinct())) .load::(conn) .expect("User::one_by_instance: loading error") } @@ -108,6 +108,10 @@ impl User { pub fn delete(&self, conn: &Connection) { use schema::post_authors; + Blog::find_for_author(conn, self) + .iter() + .filter(|b| b.list_authors(conn).len() <= 1) + .for_each(|b| b.delete(conn)); // delete the posts if they is the only author let all_their_posts_ids: Vec = post_authors::table .filter(post_authors::author_id.eq(self.id)) @@ -120,13 +124,20 @@ impl User { .filter(post_authors::author_id.ne(self.id)) .count() .load(conn) - .expect("User::delete: count author error").iter().next().unwrap_or(&0) > &0; + .expect("User::delete: count author error") + .iter() + .next() + .unwrap_or(&0) > &0; if !has_other_authors { - Post::get(conn, post_id).expect("User::delete: post not found error").delete(conn); + Post::get(conn, post_id) + .expect("User::delete: post not found error") + .delete(conn); } } - diesel::delete(self).execute(conn).expect("User::delete: user deletion error"); + diesel::delete(self) + .execute(conn) + .expect("User::delete: user deletion error"); } pub fn get_instance(&self, conn: &Connection) -> Instance { @@ -140,22 +151,31 @@ impl User { .expect("User::grand_admin_rights: update error"); } + pub fn revoke_admin_rights(&self, conn: &Connection) { + diesel::update(self) + .set(users::is_admin.eq(false)) + .execute(conn) + .expect("User::grand_admin_rights: update error"); + } + pub fn update(&self, conn: &Connection, name: String, email: String, summary: String) -> User { diesel::update(self) .set(( users::display_name.eq(name), users::email.eq(email), users::summary.eq(summary), - )).execute(conn) + )) + .execute(conn) .expect("User::update: update error"); User::get(conn, self.id).expect("User::update: get error") } pub fn count_local(conn: &Connection) -> usize { - users::table.filter(users::instance_id.eq(Instance::local_id(conn))) + users::table + .filter(users::instance_id.eq(Instance::local_id(conn))) .load::(conn) .expect("User::count_local: loading error") - .len()// TODO count in database? + .len() // TODO count in database? } pub fn find_local(conn: &Connection, username: String) -> Option { @@ -163,24 +183,49 @@ impl User { } pub fn find_by_fqn(conn: &Connection, fqn: String) -> Option { - if fqn.contains("@") { // remote user - match Instance::find_by_domain(conn, String::from(fqn.split("@").last().expect("User::find_by_fqn: host error"))) { - Some(instance) => { - match User::find_by_name(conn, String::from(fqn.split("@").nth(0).expect("User::find_by_fqn: name error")), instance.id) { - Some(u) => Some(u), - None => User::fetch_from_webfinger(conn, fqn) - } + if fqn.contains("@") { + // remote user + match Instance::find_by_domain( + conn, + String::from( + fqn.split("@") + .last() + .expect("User::find_by_fqn: host error"), + ), + ) { + Some(instance) => match User::find_by_name( + conn, + String::from( + fqn.split("@") + .nth(0) + .expect("User::find_by_fqn: name error"), + ), + instance.id, + ) { + Some(u) => Some(u), + None => User::fetch_from_webfinger(conn, fqn), }, - None => User::fetch_from_webfinger(conn, fqn) + None => User::fetch_from_webfinger(conn, fqn), } - } else { // local user + } else { + // local user User::find_local(conn, fqn) } } fn fetch_from_webfinger(conn: &Connection, acct: String) -> Option { match resolve(acct.clone(), *USE_HTTPS) { - Ok(wf) => wf.links.into_iter().find(|l| l.mime_type == Some(String::from("application/activity+json"))).and_then(|l| User::fetch_from_url(conn, l.href.expect("User::fetch_from_webginfer: href not found error"))), + Ok(wf) => wf + .links + .into_iter() + .find(|l| l.mime_type == Some(String::from("application/activity+json"))) + .and_then(|l| { + User::fetch_from_url( + conn, + l.href + .expect("User::fetch_from_webginfer: href not found error"), + ) + }), Err(details) => { println!("WF Error: {:?}", details); None @@ -191,7 +236,15 @@ impl User { fn fetch(url: String) -> Option { let req = Client::new() .get(&url[..]) - .header(ACCEPT, HeaderValue::from_str(&ap_accept_header().into_iter().collect::>().join(", ")).expect("User::fetch: accept header error")) + .header( + ACCEPT, + HeaderValue::from_str( + &ap_accept_header() + .into_iter() + .collect::>() + .join(", "), + ).expect("User::fetch: accept header error"), + ) .send(); match req { Ok(mut res) => { @@ -200,10 +253,16 @@ impl User { if let Ok(mut json) = serde_json::from_str::(text) { json.custom_props = ap_sign; // without this workaround, publicKey is not correctly deserialized Some(json) - } else { None } - } else { None } - } else { None } - }, + } else { + None + } + } else { + None + } + } else { + None + } + } Err(e) => { println!("User fetch error: {:?}", e); None @@ -212,73 +271,171 @@ impl User { } pub fn fetch_from_url(conn: &Connection, url: String) -> Option { - User::fetch(url.clone()).map(|json| (User::from_activity(conn, json, Url::parse(url.as_ref()) - .expect("User::fetch_from_url: url error").host_str() - .expect("User::fetch_from_url: host error").to_string()))) + User::fetch(url.clone()).map(|json| { + (User::from_activity( + conn, + json, + Url::parse(url.as_ref()) + .expect("User::fetch_from_url: url error") + .host_str() + .expect("User::fetch_from_url: host error") + .to_string(), + )) + }) } fn from_activity(conn: &Connection, acct: CustomPerson, inst: String) -> User { let instance = match Instance::find_by_domain(conn, inst.clone()) { Some(instance) => instance, None => { - Instance::insert(conn, NewInstance { - name: inst.clone(), - public_domain: inst.clone(), - 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() - }) + Instance::insert( + conn, + NewInstance { + name: inst.clone(), + public_domain: inst.clone(), + 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(), + }, + ) } }; - let avatar = Media::save_remote(conn, acct.object.object_props.icon_image().expect("User::from_activity: icon error") - .object_props.url_string().expect("User::from_activity: icon.url error")); + let user = User::insert( + conn, + NewUser { + username: acct + .object + .ap_actor_props + .preferred_username_string() + .expect("User::from_activity: preferredUsername error"), + display_name: acct + .object + .object_props + .name_string() + .expect("User::from_activity: name error"), + outbox_url: acct + .object + .ap_actor_props + .outbox_string() + .expect("User::from_activity: outbox error"), + inbox_url: acct + .object + .ap_actor_props + .inbox_string() + .expect("User::from_activity: inbox error"), + is_admin: false, + summary: SafeString::new( + &acct + .object + .object_props + .summary_string() + .unwrap_or(String::new()), + ), + email: None, + hashed_password: None, + instance_id: instance.id, + ap_url: acct + .object + .object_props + .id_string() + .expect("User::from_activity: id error"), + public_key: acct + .custom_props + .public_key_publickey() + .expect("User::from_activity: publicKey error") + .public_key_pem_string() + .expect("User::from_activity: publicKey.publicKeyPem error"), + private_key: None, + shared_inbox_url: acct + .object + .ap_actor_props + .endpoints_endpoint() + .and_then(|e| e.shared_inbox_string()) + .ok(), + followers_endpoint: acct + .object + .ap_actor_props + .followers_string() + .expect("User::from_activity: followers error"), + avatar_id: None, + }, + ); - let user = User::insert(conn, NewUser { - username: acct.object.ap_actor_props.preferred_username_string().expect("User::from_activity: preferredUsername error"), - display_name: acct.object.object_props.name_string().expect("User::from_activity: name error"), - outbox_url: acct.object.ap_actor_props.outbox_string().expect("User::from_activity: outbox error"), - inbox_url: acct.object.ap_actor_props.inbox_string().expect("User::from_activity: inbox error"), - is_admin: false, - summary: SafeString::new(&acct.object.object_props.summary_string().unwrap_or(String::new())), - email: None, - hashed_password: None, - instance_id: instance.id, - ap_url: acct.object.object_props.id_string().expect("User::from_activity: id error"), - public_key: acct.custom_props.public_key_publickey().expect("User::from_activity: publicKey error") - .public_key_pem_string().expect("User::from_activity: publicKey.publicKeyPem error"), - private_key: None, - shared_inbox_url: acct.object.ap_actor_props.endpoints_endpoint() - .and_then(|e| e.shared_inbox_string()).ok(), - followers_endpoint: acct.object.ap_actor_props.followers_string().expect("User::from_activity: followers error"), - avatar_id: Some(avatar.id) - }); - avatar.set_owner(conn, user.id); + let avatar = Media::save_remote( + conn, + acct.object + .object_props + .icon_image() + .expect("User::from_activity: icon error") + .object_props + .url_string() + .expect("User::from_activity: icon.url error"), + &user, + ); + + user.set_avatar(conn, avatar.id); user } pub fn refetch(&self, conn: &Connection) { User::fetch(self.ap_url.clone()).map(|json| { - let avatar = Media::save_remote(conn, json.object.object_props.icon_image().expect("User::refetch: icon error") - .object_props.url_string().expect("User::refetch: icon.url error")); + let avatar = Media::save_remote( + conn, + json.object + .object_props + .icon_image() + .expect("User::refetch: icon error") + .object_props + .url_string() + .expect("User::refetch: icon.url error"), + &self, + ); diesel::update(self) .set(( - users::username.eq(json.object.ap_actor_props.preferred_username_string().expect("User::refetch: preferredUsername error")), - users::display_name.eq(json.object.object_props.name_string().expect("User::refetch: name error")), - users::outbox_url.eq(json.object.ap_actor_props.outbox_string().expect("User::refetch: outbox error")), - users::inbox_url.eq(json.object.ap_actor_props.inbox_string().expect("User::refetch: inbox error")), - users::summary.eq(SafeString::new(&json.object.object_props.summary_string().unwrap_or(String::new()))), - users::followers_endpoint.eq(json.object.ap_actor_props.followers_string().expect("User::refetch: followers error")), + users::username.eq(json + .object + .ap_actor_props + .preferred_username_string() + .expect("User::refetch: preferredUsername error")), + users::display_name.eq(json + .object + .object_props + .name_string() + .expect("User::refetch: name error")), + users::outbox_url.eq(json + .object + .ap_actor_props + .outbox_string() + .expect("User::refetch: outbox error")), + users::inbox_url.eq(json + .object + .ap_actor_props + .inbox_string() + .expect("User::refetch: inbox error")), + users::summary.eq(SafeString::new( + &json + .object + .object_props + .summary_string() + .unwrap_or(String::new()), + )), + users::followers_endpoint.eq(json + .object + .ap_actor_props + .followers_string() + .expect("User::refetch: followers error")), users::avatar_id.eq(Some(avatar.id)), - users::last_fetched_date.eq(Utc::now().naive_utc()) - )).execute(conn) + users::last_fetched_date.eq(Utc::now().naive_utc()), + )) + .execute(conn) .expect("User::refetch: update error") }); } @@ -288,7 +445,13 @@ impl User { } pub fn auth(&self, pass: String) -> bool { - if let Ok(valid) = bcrypt::verify(pass.as_str(), self.hashed_password.clone().expect("User::auth: no password error").as_str()) { + if let Ok(valid) = bcrypt::verify( + pass.as_str(), + self.hashed_password + .clone() + .expect("User::auth: no password error") + .as_str(), + ) { valid } else { false @@ -299,37 +462,60 @@ impl User { let instance = self.get_instance(conn); if self.outbox_url.len() == 0 { diesel::update(self) - .set(users::outbox_url.eq(instance.compute_box(USER_PREFIX, self.username.clone(), "outbox"))) - .execute(conn).expect("User::update_boxes: outbox update error"); + .set(users::outbox_url.eq(instance.compute_box( + USER_PREFIX, + self.username.clone(), + "outbox", + ))) + .execute(conn) + .expect("User::update_boxes: outbox update error"); } if self.inbox_url.len() == 0 { diesel::update(self) - .set(users::inbox_url.eq(instance.compute_box(USER_PREFIX, self.username.clone(), "inbox"))) - .execute(conn).expect("User::update_boxes: inbox update error"); + .set(users::inbox_url.eq(instance.compute_box( + USER_PREFIX, + self.username.clone(), + "inbox", + ))) + .execute(conn) + .expect("User::update_boxes: inbox update error"); } if self.ap_url.len() == 0 { diesel::update(self) .set(users::ap_url.eq(instance.compute_box(USER_PREFIX, self.username.clone(), ""))) - .execute(conn).expect("User::update_boxes: ap_url update error"); + .execute(conn) + .expect("User::update_boxes: ap_url update error"); } if self.shared_inbox_url.is_none() { diesel::update(self) - .set(users::shared_inbox_url.eq(ap_url(format!("{}/inbox", Instance::get_local(conn).expect("User::update_boxes: local instance not found error").public_domain)))) - .execute(conn).expect("User::update_boxes: shared inbox update error"); + .set(users::shared_inbox_url.eq(ap_url(format!( + "{}/inbox", + Instance::get_local(conn) + .expect("User::update_boxes: local instance not found error") + .public_domain + )))) + .execute(conn) + .expect("User::update_boxes: shared inbox update error"); } if self.followers_endpoint.len() == 0 { diesel::update(self) - .set(users::followers_endpoint.eq(instance.compute_box(USER_PREFIX, self.username.clone(), "followers"))) - .execute(conn).expect("User::update_boxes: follower update error"); + .set(users::followers_endpoint.eq(instance.compute_box( + USER_PREFIX, + self.username.clone(), + "followers", + ))) + .execute(conn) + .expect("User::update_boxes: follower update error"); } } pub fn get_local_page(conn: &Connection, (min, max): (i32, i32)) -> Vec { - users::table.filter(users::instance_id.eq(1)) + users::table + .filter(users::instance_id.eq(Instance::local_id(conn))) .order(users::username.asc()) .offset(min.into()) .limit((max - min).into()) @@ -341,26 +527,39 @@ impl User { let acts = self.get_activities(conn); let n_acts = acts.len(); let mut coll = OrderedCollection::default(); - coll.collection_props.items = serde_json::to_value(acts).expect("User::outbox: activity error"); - coll.collection_props.set_total_items_u64(n_acts as u64).expect("User::outbox: count error"); + coll.collection_props.items = + serde_json::to_value(acts).expect("User::outbox: activity error"); + coll.collection_props + .set_total_items_u64(n_acts as u64) + .expect("User::outbox: count error"); ActivityStream::new(coll) } pub fn fetch_outbox(&self) -> Vec { let req = Client::new() .get(&self.outbox_url[..]) - .header(ACCEPT, HeaderValue::from_str(&ap_accept_header().into_iter().collect::>().join(", ")).expect("User::fetch_outbox: accept header error")) + .header( + ACCEPT, + HeaderValue::from_str( + &ap_accept_header() + .into_iter() + .collect::>() + .join(", "), + ).expect("User::fetch_outbox: accept header error"), + ) .send(); match req { Ok(mut res) => { let text = &res.text().expect("User::fetch_outbox: body error"); - let json: serde_json::Value = serde_json::from_str(text).expect("User::fetch_outbox: parsing error"); - json["items"].as_array() + let json: serde_json::Value = + serde_json::from_str(text).expect("User::fetch_outbox: parsing error"); + json["items"] + .as_array() .expect("Outbox.items is not an array") .into_iter() .filter_map(|j| serde_json::from_value(j.clone()).ok()) .collect::>() - }, + } Err(e) => { println!("User outbox fetch error: {:?}", e); vec![] @@ -371,18 +570,28 @@ impl User { pub fn fetch_followers_ids(&self) -> Vec { let req = Client::new() .get(&self.followers_endpoint[..]) - .header(ACCEPT, HeaderValue::from_str(&ap_accept_header().into_iter().collect::>().join(", ")).expect("User::fetch_followers_ids: accept header error")) + .header( + ACCEPT, + HeaderValue::from_str( + &ap_accept_header() + .into_iter() + .collect::>() + .join(", "), + ).expect("User::fetch_followers_ids: accept header error"), + ) .send(); match req { Ok(mut res) => { let text = &res.text().expect("User::fetch_followers_ids: body error"); - let json: serde_json::Value = serde_json::from_str(text).expect("User::fetch_followers_ids: parsing error"); - json["items"].as_array() + let json: serde_json::Value = + serde_json::from_str(text).expect("User::fetch_followers_ids: parsing error"); + json["items"] + .as_array() .expect("User::fetch_followers_ids: not an array error") .into_iter() .filter_map(|j| serde_json::from_value(j.clone()).ok()) .collect::>() - }, + } Err(e) => { println!("User followers fetch error: {:?}", e); vec![] @@ -391,45 +600,62 @@ impl User { } fn get_activities(&self, conn: &Connection) -> Vec { - use schema::posts; use schema::post_authors; + use schema::posts; let posts_by_self = PostAuthor::belonging_to(self).select(post_authors::post_id); let posts = posts::table .filter(posts::published.eq(true)) .filter(posts::id.eq_any(posts_by_self)) - .load::(conn).expect("User::get_activities: loading error"); - posts.into_iter().map(|p| { - serde_json::to_value(p.create_activity(conn)).expect("User::get_activities: creation error") - }).collect::>() + .load::(conn) + .expect("User::get_activities: loading error"); + posts + .into_iter() + .map(|p| { + serde_json::to_value(p.create_activity(conn)) + .expect("User::get_activities: creation error") + }) + .collect::>() } pub fn get_fqn(&self, conn: &Connection) -> String { if self.instance_id == Instance::local_id(conn) { self.username.clone() } else { - format!("{}@{}", self.username, self.get_instance(conn).public_domain) + format!( + "{}@{}", + self.username, + self.get_instance(conn).public_domain + ) } } pub fn get_followers(&self, conn: &Connection) -> Vec { use schema::follows; let follows = Follow::belonging_to(self).select(follows::follower_id); - users::table.filter(users::id.eq_any(follows)).load::(conn).expect("User::get_followers: loading error") + users::table + .filter(users::id.eq_any(follows)) + .load::(conn) + .expect("User::get_followers: loading error") } pub fn get_followers_page(&self, conn: &Connection, (min, max): (i32, i32)) -> Vec { use schema::follows; let follows = Follow::belonging_to(self).select(follows::follower_id); - users::table.filter(users::id.eq_any(follows)) + users::table + .filter(users::id.eq_any(follows)) .offset(min.into()) .limit((max - min).into()) - .load::(conn).expect("User::get_followers_page: loading error") + .load::(conn) + .expect("User::get_followers_page: loading error") } pub fn get_following(&self, conn: &Connection) -> Vec { use schema::follows::dsl::*; let f = follows.filter(follower_id.eq(self.id)).select(following_id); - users::table.filter(users::id.eq_any(f)).load::(conn).expect("User::get_following: loading error") + users::table + .filter(users::id.eq_any(f)) + .load::(conn) + .expect("User::get_following: loading error") } pub fn is_followed_by(&self, conn: &Connection, other_id: i32) -> bool { @@ -439,7 +665,7 @@ impl User { .filter(follows::following_id.eq(self.id)) .load::(conn) .expect("User::is_followed_by: loading error") - .len() > 0// TODO count in database? + .len() > 0 // TODO count in database? } pub fn is_following(&self, conn: &Connection, other_id: i32) -> bool { @@ -449,7 +675,7 @@ impl User { .filter(follows::following_id.eq(other_id)) .load::(conn) .expect("User::is_following: loading error") - .len() > 0// TODO count in database? + .len() > 0 // TODO count in database? } pub fn has_liked(&self, conn: &Connection, post: &Post) -> bool { @@ -459,7 +685,7 @@ impl User { .filter(likes::user_id.eq(self.id)) .load::(conn) .expect("User::has_liked: loading error") - .len() > 0// TODO count in database? + .len() > 0 // TODO count in database? } pub fn has_reshared(&self, conn: &Connection, post: &Post) -> bool { @@ -469,50 +695,102 @@ impl User { .filter(reshares::user_id.eq(self.id)) .load::(conn) .expect("User::has_reshared: loading error") - .len() > 0// TODO count in database? + .len() > 0 // TODO count in database? } pub fn is_author_in(&self, conn: &Connection, blog: Blog) -> bool { use schema::blog_authors; - blog_authors::table.filter(blog_authors::author_id.eq(self.id)) + blog_authors::table + .filter(blog_authors::author_id.eq(self.id)) .filter(blog_authors::blog_id.eq(blog.id)) .load::(conn) .expect("User::is_author_in: loading error") - .len() > 0// TODO count in database? + .len() > 0 // TODO count in database? } pub fn get_keypair(&self) -> PKey { - PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.clone().expect("User::get_keypair: private key not found error").as_ref()) - .expect("User::get_keypair: pem parsing error")) - .expect("User::get_keypair: private key deserialization error") + PKey::from_rsa( + Rsa::private_key_from_pem( + self.private_key + .clone() + .expect("User::get_keypair: private key not found error") + .as_ref(), + ).expect("User::get_keypair: pem parsing error"), + ).expect("User::get_keypair: private key deserialization error") } pub fn into_activity(&self, conn: &Connection) -> CustomPerson { let mut actor = Person::default(); - actor.object_props.set_id_string(self.ap_url.clone()).expect("User::into_activity: id error"); - actor.object_props.set_name_string(self.display_name.clone()).expect("User::into_activity: name error"); - actor.object_props.set_summary_string(self.summary.get().clone()).expect("User::into_activity: summary error"); - actor.object_props.set_url_string(self.ap_url.clone()).expect("User::into_activity: url error"); - actor.ap_actor_props.set_inbox_string(self.inbox_url.clone()).expect("User::into_activity: inbox error"); - actor.ap_actor_props.set_outbox_string(self.outbox_url.clone()).expect("User::into_activity: outbox error"); - actor.ap_actor_props.set_preferred_username_string(self.username.clone()).expect("User::into_activity: preferredUsername error"); - actor.ap_actor_props.set_followers_string(self.followers_endpoint.clone()).expect("User::into_activity: followers error"); + actor + .object_props + .set_id_string(self.ap_url.clone()) + .expect("User::into_activity: id error"); + actor + .object_props + .set_name_string(self.display_name.clone()) + .expect("User::into_activity: name error"); + actor + .object_props + .set_summary_string(self.summary.get().clone()) + .expect("User::into_activity: summary error"); + actor + .object_props + .set_url_string(self.ap_url.clone()) + .expect("User::into_activity: url error"); + actor + .ap_actor_props + .set_inbox_string(self.inbox_url.clone()) + .expect("User::into_activity: inbox error"); + actor + .ap_actor_props + .set_outbox_string(self.outbox_url.clone()) + .expect("User::into_activity: outbox error"); + actor + .ap_actor_props + .set_preferred_username_string(self.username.clone()) + .expect("User::into_activity: preferredUsername error"); + actor + .ap_actor_props + .set_followers_string(self.followers_endpoint.clone()) + .expect("User::into_activity: followers error"); let mut endpoints = Endpoint::default(); - endpoints.set_shared_inbox_string(ap_url(format!("{}/inbox/", BASE_URL.as_str()))).expect("User::into_activity: endpoints.sharedInbox error"); - actor.ap_actor_props.set_endpoints_endpoint(endpoints).expect("User::into_activity: endpoints error"); + endpoints + .set_shared_inbox_string(ap_url(format!("{}/inbox/", BASE_URL.as_str()))) + .expect("User::into_activity: endpoints.sharedInbox error"); + actor + .ap_actor_props + .set_endpoints_endpoint(endpoints) + .expect("User::into_activity: endpoints error"); let mut public_key = PublicKey::default(); - public_key.set_id_string(format!("{}#main-key", self.ap_url)).expect("User::into_activity: publicKey.id error"); - public_key.set_owner_string(self.ap_url.clone()).expect("User::into_activity: publicKey.owner error"); - public_key.set_public_key_pem_string(self.public_key.clone()).expect("User::into_activity: publicKey.publicKeyPem error"); + public_key + .set_id_string(format!("{}#main-key", self.ap_url)) + .expect("User::into_activity: publicKey.id error"); + public_key + .set_owner_string(self.ap_url.clone()) + .expect("User::into_activity: publicKey.owner error"); + public_key + .set_public_key_pem_string(self.public_key.clone()) + .expect("User::into_activity: publicKey.publicKeyPem error"); let mut ap_signature = ApSignature::default(); - ap_signature.set_public_key_publickey(public_key).expect("User::into_activity: publicKey error"); + ap_signature + .set_public_key_publickey(public_key) + .expect("User::into_activity: publicKey error"); let mut avatar = Image::default(); - avatar.object_props.set_url_string(self.avatar_id.and_then(|id| Media::get(conn, id).map(|m| m.url(conn))).unwrap_or(String::new())) + avatar + .object_props + .set_url_string( + self.avatar_id + .and_then(|id| Media::get(conn, id).map(|m| m.url(conn))) + .unwrap_or(String::new()), + ) .expect("User::into_activity: icon.url error"); - actor.object_props.set_icon_object(avatar).expect("User::into_activity: icon error"); + actor + .object_props + .set_icon_object(avatar) + .expect("User::into_activity: icon error"); CustomPerson::new(actor, ap_signature) } @@ -525,34 +803,46 @@ impl User { } else { json!(self.get_fqn(conn)) }; - json["avatar"] = json!(self.avatar_id.and_then(|id| Media::get(conn, id).map(|m| m.url(conn))).unwrap_or("/static/default-avatar.png".to_string())); + json["avatar"] = json!( + self.avatar_id + .and_then(|id| Media::get(conn, id).map(|m| m.url(conn))) + .unwrap_or("/static/default-avatar.png".to_string()) + ); json } pub fn webfinger(&self, conn: &Connection) -> Webfinger { Webfinger { - subject: format!("acct:{}@{}", self.username, self.get_instance(conn).public_domain), + subject: format!( + "acct:{}@{}", + self.username, + self.get_instance(conn).public_domain + ), aliases: vec![self.ap_url.clone()], links: vec![ Link { rel: String::from("http://webfinger.net/rel/profile-page"), mime_type: None, href: Some(self.ap_url.clone()), - template: None + template: None, }, 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(USER_PREFIX, self.username.clone(), "feed.atom")), - template: None + href: Some(self.get_instance(conn).compute_box( + USER_PREFIX, + self.username.clone(), + "feed.atom", + )), + template: None, }, Link { rel: String::from("self"), mime_type: Some(String::from("application/activity+json")), href: Some(self.ap_url.clone()), - template: None - } - ] + template: None, + }, + ], } } @@ -560,7 +850,11 @@ impl User { User::find_by_ap_url(conn, url.clone()).or_else(|| { // The requested user was not in the DB // We try to fetch it if it is remote - if Url::parse(url.as_ref()).expect("User::from_url: url error").host_str().expect("User::from_url: host error") != BASE_URL.as_str() { + if Url::parse(url.as_ref()) + .expect("User::from_url: url error") + .host_str() + .expect("User::from_url: host error") != BASE_URL.as_str() + { User::fetch_from_url(conn, url) } else { None @@ -585,7 +879,8 @@ impl<'a, 'r> FromRequest<'a, 'r> for User { fn from_request(request: &'a Request<'r>) -> request::Outcome { let conn = request.guard::()?; - request.cookies() + request + .cookies() .get_private(AUTH_COOKIE) .and_then(|cookie| cookie.value().parse().ok()) .map(|id| User::get(&*conn, id).expect("User::from_request: user not found error")) @@ -608,7 +903,7 @@ impl WithInbox for User { } fn get_shared_inbox_url(&self) -> Option { - self.shared_inbox_url.clone() + self.shared_inbox_url.clone() } fn is_local(&self) -> bool { @@ -623,17 +918,29 @@ impl Signer for User { fn sign(&self, to_sign: String) -> Vec { let key = self.get_keypair(); - let mut signer = sign::Signer::new(MessageDigest::sha256(), &key).expect("User::sign: initialization error"); - signer.update(to_sign.as_bytes()).expect("User::sign: content insertion error"); - signer.sign_to_vec().expect("User::sign: finalization error") + let mut signer = sign::Signer::new(MessageDigest::sha256(), &key) + .expect("User::sign: initialization error"); + signer + .update(to_sign.as_bytes()) + .expect("User::sign: content insertion error"); + signer + .sign_to_vec() + .expect("User::sign: finalization error") } fn verify(&self, data: String, signature: Vec) -> bool { - let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).expect("User::verify: pem parsing error")) - .expect("User::verify: deserialization error"); - let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key).expect("User::verify: initialization error"); - verifier.update(data.as_bytes()).expect("User::verify: content insertion error"); - verifier.verify(&signature).expect("User::verify: finalization error") + let key = PKey::from_rsa( + Rsa::public_key_from_pem(self.public_key.as_ref()) + .expect("User::verify: pem parsing error"), + ).expect("User::verify: deserialization error"); + let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key) + .expect("User::verify: initialization error"); + verifier + .update(data.as_bytes()) + .expect("User::verify: content insertion error"); + verifier + .verify(&signature) + .expect("User::verify: finalization error") } } @@ -646,25 +953,227 @@ impl NewUser { is_admin: bool, summary: String, email: String, - password: String + password: String, ) -> User { let (pub_key, priv_key) = gen_keypair(); - User::insert(conn, NewUser { - username: username, - display_name: display_name, - outbox_url: String::from(""), - inbox_url: String::from(""), - is_admin: is_admin, - summary: SafeString::new(&summary), - email: Some(email), - hashed_password: Some(password), - instance_id: Instance::local_id(conn), - ap_url: String::from(""), - public_key: String::from_utf8(pub_key).expect("NewUser::new_local: public key error"), - private_key: Some(String::from_utf8(priv_key).expect("NewUser::new_local: private key error")), - shared_inbox_url: None, - followers_endpoint: String::from(""), - avatar_id: None - }) + User::insert( + conn, + NewUser { + username: username, + display_name: display_name, + outbox_url: String::from(""), + inbox_url: String::from(""), + is_admin: is_admin, + summary: SafeString::new(&summary), + email: Some(email), + hashed_password: Some(password), + instance_id: Instance::local_id(conn), + ap_url: String::from(""), + public_key: String::from_utf8(pub_key) + .expect("NewUser::new_local: public key error"), + private_key: Some( + String::from_utf8(priv_key).expect("NewUser::new_local: private key error"), + ), + shared_inbox_url: None, + followers_endpoint: String::from(""), + avatar_id: None, + }, + ) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use diesel::Connection; + use instance::{tests as instance_tests, Instance}; + use tests::db; + use Connection as Conn; + + pub(crate) fn fill_database(conn: &Conn) -> Vec { + instance_tests::fill_database(conn); + let local_user = vec![ + NewUser::new_local( + conn, + "admin".to_owned(), + "The admin".to_owned(), + true, + "Hello there, I'm the admin".to_owned(), + "admin@example.com".to_owned(), + "invalid_admin_password".to_owned(), + ), + NewUser::new_local( + conn, + "user".to_owned(), + "Some user".to_owned(), + false, + "Hello there, I'm no one".to_owned(), + "user@example.com".to_owned(), + "invalid_user_password".to_owned(), + ), + NewUser::new_local( + conn, + "other".to_owned(), + "Another user".to_owned(), + false, + "Hello there, I'm someone else".to_owned(), + "other@example.com".to_owned(), + "invalid_other_password".to_owned(), + ), + ]; + for u in local_user.iter() { + u.update_boxes(conn); + } + local_user + } + + #[test] + fn find_by() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + fill_database(conn); + let test_user = NewUser::new_local( + conn, + "test".to_owned(), + "test user".to_owned(), + false, + "Hello I'm a test".to_owned(), + "test@example.com".to_owned(), + User::hash_pass("test_password".to_owned()), + ); + test_user.update_boxes(conn); + + assert_eq!( + test_user.id, + User::find_by_name(conn, "test".to_owned(), Instance::local_id(conn)) + .unwrap() + .id + ); + assert_eq!( + test_user.id, + User::find_by_fqn(conn, test_user.get_fqn(conn)).unwrap().id + ); + assert_eq!( + test_user.id, + User::find_by_email(conn, "test@example.com".to_owned()) + .unwrap() + .id + ); + assert_eq!( + test_user.id, + User::find_by_ap_url( + conn, + format!( + "https://{}/@/{}/", + Instance::get_local(conn).unwrap().public_domain, + "test" + ) + ).unwrap() + .id + ); + + Ok(()) + }); + } + + #[test] + fn delete() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let inserted = fill_database(conn); + + assert!(User::get(conn, inserted[0].id).is_some()); + inserted[0].delete(conn); + assert!(User::get(conn, inserted[0].id).is_none()); + + Ok(()) + }); + } + + #[test] + fn admin() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let inserted = fill_database(conn); + let local_inst = Instance::get_local(conn).unwrap(); + let mut i = 0; + while local_inst.has_admin(conn) { + assert!(i < 100); //prevent from looping indefinitelly + local_inst.main_admin(conn).revoke_admin_rights(conn); + i += 1; + } + inserted[0].grant_admin_rights(conn); + assert_eq!(inserted[0].id, local_inst.main_admin(conn).id); + + Ok(()) + }); + } + + #[test] + fn update() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + let inserted = fill_database(conn); + let updated = inserted[0].update( + conn, + "new name".to_owned(), + "em@il".to_owned(), + "

summary

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

summary

"); + + Ok(()) + }); + } + + #[test] + fn auth() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + fill_database(conn); + let test_user = NewUser::new_local( + conn, + "test".to_owned(), + "test user".to_owned(), + false, + "Hello I'm a test".to_owned(), + "test@example.com".to_owned(), + User::hash_pass("test_password".to_owned()), + ); + test_user.update_boxes(conn); + + assert!(test_user.auth("test_password".to_owned())); + assert!(!test_user.auth("other_password".to_owned())); + + Ok(()) + }); + } + + #[test] + fn get_local_page() { + let conn = &db(); + conn.test_transaction::<_, (), _>(|| { + fill_database(conn); + + let page = User::get_local_page(conn, (0, 2)); + assert_eq!(page.len(), 2); + assert!(page[0].username <= page[1].username); + + let mut last_username = User::get_local_page(conn, (0, 1))[0].username.clone(); + for i in 1..User::count_local(conn) as i32 { + let page = User::get_local_page(conn, (i, i + 1)); + assert_eq!(page.len(), 1); + assert!(last_username <= page[0].username); + last_username = page[0].username.clone(); + } + assert_eq!( + User::get_local_page(conn, (0, User::count_local(conn) as i32 + 10)).len(), + User::count_local(conn) + ); + + Ok(()) + }); } } diff --git a/plume-models/tests/lib.rs b/plume-models/tests/lib.rs index 210a8f81..96137e0d 100644 --- a/plume-models/tests/lib.rs +++ b/plume-models/tests/lib.rs @@ -1,15 +1,11 @@ extern crate diesel; -#[macro_use] extern crate diesel_migrations; +#[macro_use] +extern crate diesel_migrations; extern crate plume_models; use diesel::Connection; -use plume_models::{ - DATABASE_URL, - Connection as Conn, - instance::*, - safe_string::SafeString, -}; +use plume_models::{Connection as Conn, DATABASE_URL}; #[cfg(feature = "sqlite")] embed_migrations!("../migrations/sqlite"); @@ -24,24 +20,7 @@ fn db() -> Conn { } #[test] -fn instance_insert() { +fn empty_test() { let conn = &db(); - conn.test_transaction::<_, (), _>(|| { - Instance::insert(conn, NewInstance { - default_license: "WTFPL".to_string(), - local: true, - long_description: SafeString::new("This is my instance."), - long_description_html: "

This is my instance

".to_string(), - short_description: SafeString::new("My instance."), - short_description_html: "

My instance

".to_string(), - name: "My instance".to_string(), - open_registrations: true, - public_domain: "plu.me".to_string(), - }); - let inst = Instance::get_local(conn); - assert!(inst.is_some()); - let inst = inst.unwrap(); - assert_eq!(inst.name, "My instance".to_string()); - Ok(()) - }); + conn.test_transaction::<_, (), _>(|| Ok(())); } diff --git a/src/inbox.rs b/src/inbox.rs index 9cb1a47a..d120b64d 100644 --- a/src/inbox.rs +++ b/src/inbox.rs @@ -13,76 +13,112 @@ use activitypub::{ use failure::Error; use serde_json; -use plume_common::activity_pub::{Id, inbox::{Deletable, FromActivity, InboxError}}; +use plume_common::activity_pub::{ + inbox::{Deletable, FromActivity, InboxError}, + Id, +}; use plume_models::{ - Connection, - comments::Comment, - follows::Follow, - instance::Instance, - likes, - reshares::Reshare, - posts::Post, - users::User + comments::Comment, follows::Follow, instance::Instance, likes, posts::Post, reshares::Reshare, + users::User, Connection, }; pub trait Inbox { fn received(&self, conn: &Connection, act: serde_json::Value) -> Result<(), Error> { - let actor_id = Id::new(act["actor"].as_str().unwrap_or_else(|| act["actor"]["id"].as_str().expect("Inbox::received: actor_id missing error"))); + let actor_id = Id::new(act["actor"].as_str().unwrap_or_else(|| { + act["actor"]["id"] + .as_str() + .expect("Inbox::received: actor_id missing error") + })); match act["type"].as_str() { - Some(t) => { - match t { - "Announce" => { - Reshare::from_activity(conn, serde_json::from_value(act.clone())?, actor_id); - Ok(()) - }, - "Create" => { - let act: Create = serde_json::from_value(act.clone())?; - if Post::try_from_activity(conn, act.clone()) || Comment::try_from_activity(conn, act) { - Ok(()) - } else { - Err(InboxError::InvalidType)? - } - }, - "Delete" => { - let act: Delete = serde_json::from_value(act.clone())?; - Post::delete_id(act.delete_props.object_object::()?.object_props.id_string()?, actor_id.into(), conn); - Ok(()) - }, - "Follow" => { - Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id); - Ok(()) - }, - "Like" => { - likes::Like::from_activity(conn, serde_json::from_value(act.clone())?, actor_id); - Ok(()) - }, - "Undo" => { - let act: Undo = serde_json::from_value(act.clone())?; - match act.undo_props.object["type"].as_str().expect("Inbox::received: undo without original type error") { - "Like" => { - likes::Like::delete_id(act.undo_props.object_object::()?.object_props.id_string()?, actor_id.into(), conn); - Ok(()) - }, - "Announce" => { - Reshare::delete_id(act.undo_props.object_object::()?.object_props.id_string()?, actor_id.into(), conn); - Ok(()) - }, - "Follow" => { - Follow::delete_id(act.undo_props.object_object::()?.object_props.id_string()?, actor_id.into(), conn); - Ok(()) - } - _ => Err(InboxError::CantUndo)? - } - } - "Update" => { - let act: Update = serde_json::from_value(act.clone())?; - Post::handle_update(conn, act.update_props.object_object()?); - Ok(()) - } - _ => Err(InboxError::InvalidType)? + Some(t) => match t { + "Announce" => { + Reshare::from_activity(conn, serde_json::from_value(act.clone())?, actor_id); + Ok(()) } + "Create" => { + let act: Create = serde_json::from_value(act.clone())?; + if Post::try_from_activity(conn, act.clone()) + || Comment::try_from_activity(conn, act) + { + Ok(()) + } else { + Err(InboxError::InvalidType)? + } + } + "Delete" => { + let act: Delete = serde_json::from_value(act.clone())?; + Post::delete_id( + act.delete_props + .object_object::()? + .object_props + .id_string()?, + actor_id.into(), + conn, + ); + Ok(()) + } + "Follow" => { + Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id); + Ok(()) + } + "Like" => { + likes::Like::from_activity( + conn, + serde_json::from_value(act.clone())?, + actor_id, + ); + Ok(()) + } + "Undo" => { + let act: Undo = serde_json::from_value(act.clone())?; + match act.undo_props.object["type"] + .as_str() + .expect("Inbox::received: undo without original type error") + { + "Like" => { + likes::Like::delete_id( + act.undo_props + .object_object::()? + .object_props + .id_string()?, + actor_id.into(), + conn, + ); + Ok(()) + } + "Announce" => { + Reshare::delete_id( + act.undo_props + .object_object::()? + .object_props + .id_string()?, + actor_id.into(), + conn, + ); + Ok(()) + } + "Follow" => { + Follow::delete_id( + act.undo_props + .object_object::()? + .object_props + .id_string()?, + actor_id.into(), + conn, + ); + Ok(()) + } + _ => Err(InboxError::CantUndo)?, + } + } + "Update" => { + let act: Update = serde_json::from_value(act.clone())?; + Post::handle_update(conn, act.update_props.object_object()?); + Ok(()) + } + _ => Err(InboxError::InvalidType)?, }, - None => Err(InboxError::NoType)? + None => Err(InboxError::NoType)?, } } } diff --git a/src/routes/user.rs b/src/routes/user.rs index 64ea5b62..0ee4cd28 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -1,36 +1,27 @@ -use activitypub::{ - activity::Create, - collection::OrderedCollection, - object::Article -}; +use activitypub::{activity::Create, collection::OrderedCollection, object::Article}; use atom_syndication::{Entry, FeedBuilder}; use rocket::{ + http::{ContentType, Cookies}, request::LenientForm, - response::{Content, Flash, Redirect, status}, - http::{ContentType, Cookies} + response::{status, Content, Flash, Redirect}, }; use rocket_contrib::Template; use serde_json; use validator::{Validate, ValidationError}; use workerpool::thunk::*; +use inbox::Inbox; use plume_common::activity_pub::{ - ActivityStream, broadcast, Id, IntoId, ApRequest, - inbox::{FromActivity, Notify, Deletable}, - sign::{Signable, verify_http_headers} + broadcast, + inbox::{Deletable, FromActivity, Notify}, + sign::{verify_http_headers, Signable}, + ActivityStream, ApRequest, Id, IntoId, }; use plume_common::utils; use plume_models::{ - blogs::Blog, - db_conn::DbConn, - follows, - headers::Headers, - instance::Instance, - posts::Post, - reshares::Reshare, - users::* + blogs::Blog, db_conn::DbConn, follows, headers::Headers, instance::Instance, posts::Post, + reshares::Reshare, users::*, }; -use inbox::Inbox; use routes::Page; use Worker; @@ -38,57 +29,84 @@ use Worker; fn me(user: Option) -> Result> { match user { Some(user) => Ok(Redirect::to(uri!(details: name = user.username))), - None => Err(utils::requires_login("", uri!(me).into())) + None => Err(utils::requires_login("", uri!(me).into())), } } #[get("/@/", rank = 2)] -fn details(name: String, conn: DbConn, account: Option, worker: Worker, fecth_articles_conn: DbConn, fecth_followers_conn: DbConn, update_conn: DbConn) -> Template { - may_fail!(account.map(|a| a.to_json(&*conn)), User::find_by_fqn(&*conn, name), "Couldn't find requested user", |user| { - let recents = Post::get_recents_for_author(&*conn, &user, 6); - let reshares = Reshare::get_recents_for_author(&*conn, &user, 6); - let user_id = user.id.clone(); - let n_followers = user.get_followers(&*conn).len(); +fn details( + name: String, + conn: DbConn, + account: Option, + worker: Worker, + fecth_articles_conn: DbConn, + fecth_followers_conn: DbConn, + update_conn: DbConn, +) -> Template { + may_fail!( + account.map(|a| a.to_json(&*conn)), + User::find_by_fqn(&*conn, name), + "Couldn't find requested user", + |user| { + let recents = Post::get_recents_for_author(&*conn, &user, 6); + let reshares = Reshare::get_recents_for_author(&*conn, &user, 6); + let user_id = user.id.clone(); + let n_followers = user.get_followers(&*conn).len(); - if !user.get_instance(&*conn).local { - // Fetch new articles - let user_clone = user.clone(); - worker.execute(Thunk::of(move || { - for create_act in user_clone.fetch_outbox::() { - match create_act.create_props.object_object::
() { - Ok(article) => { - Post::from_activity(&*fecth_articles_conn, article, user_clone.clone().into_id()); - println!("Fetched article from remote user"); - } - Err(e) => println!("Error while fetching articles in background: {:?}", e) - } - } - })); - - // Fetch followers - let user_clone = user.clone(); - worker.execute(Thunk::of(move || { - for user_id in user_clone.fetch_followers_ids() { - let follower = User::find_by_ap_url(&*fecth_followers_conn, user_id.clone()) - .unwrap_or_else(|| User::fetch_from_url(&*fecth_followers_conn, user_id).expect("user::details: Couldn't fetch follower")); - follows::Follow::insert(&*fecth_followers_conn, follows::NewFollow { - follower_id: follower.id, - following_id: user_clone.id, - ap_url: format!("{}/follow/{}", follower.ap_url, user_clone.ap_url), - }); - } - })); - - // Update profile information if needed - let user_clone = user.clone(); - if user.needs_update() { + if !user.get_instance(&*conn).local { + // Fetch new articles + let user_clone = user.clone(); worker.execute(Thunk::of(move || { - user_clone.refetch(&*update_conn); - })) - } - } + for create_act in user_clone.fetch_outbox::() { + match create_act.create_props.object_object::
() { + Ok(article) => { + Post::from_activity( + &*fecth_articles_conn, + article, + user_clone.clone().into_id(), + ); + println!("Fetched article from remote user"); + } + Err(e) => { + println!("Error while fetching articles in background: {:?}", e) + } + } + } + })); - Template::render("users/details", json!({ + // Fetch followers + let user_clone = user.clone(); + worker.execute(Thunk::of(move || { + for user_id in user_clone.fetch_followers_ids() { + let follower = + User::find_by_ap_url(&*fecth_followers_conn, user_id.clone()) + .unwrap_or_else(|| { + User::fetch_from_url(&*fecth_followers_conn, user_id) + .expect("user::details: Couldn't fetch follower") + }); + follows::Follow::insert( + &*fecth_followers_conn, + follows::NewFollow { + follower_id: follower.id, + following_id: user_clone.id, + ap_url: format!("{}/follow/{}", follower.ap_url, user_clone.ap_url), + }, + ); + } + })); + + // Update profile information if needed + let user_clone = user.clone(); + if user.needs_update() { + worker.execute(Thunk::of(move || { + user_clone.refetch(&*update_conn); + })) + } + } + + Template::render( + "users/details", + json!({ "user": user.to_json(&*conn), "instance_url": user.get_instance(&*conn).public_domain, "is_remote": user.instance_id != Instance::local_id(&*conn), @@ -98,25 +116,30 @@ fn details(name: String, conn: DbConn, account: Option, worker: Worker, fe "reshares": reshares.into_iter().map(|r| r.get_post(&*conn).unwrap().to_json(&*conn)).collect::>(), "is_self": account.map(|a| a.id == user_id).unwrap_or(false), "n_followers": n_followers - })) - }) + }), + ) + } + ) } #[get("/dashboard")] fn dashboard(user: User, conn: DbConn) -> Template { - let blogs = Blog::find_for_author(&*conn, user.id); - Template::render("users/dashboard", json!({ + let blogs = Blog::find_for_author(&*conn, &user); + Template::render( + "users/dashboard", + json!({ "account": user.to_json(&*conn), "blogs": blogs, "drafts": Post::drafts_by_author(&*conn, &user).into_iter().map(|a| a.to_json(&*conn)).collect::>(), - })) + }), + ) } #[get("/dashboard", rank = 2)] fn dashboard_auth() -> Flash { utils::requires_login( "You need to be logged in order to access your dashboard", - uri!(dashboard).into() + uri!(dashboard).into(), ) } @@ -125,13 +148,18 @@ fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Option Option Flash { utils::requires_login( "You need to be logged in order to follow someone", - uri!(follow: name = name).into() + uri!(follow: name = name).into(), ) } #[get("/@//followers?")] fn followers_paginated(name: String, conn: DbConn, account: Option, page: Page) -> Template { - may_fail!(account.map(|a| a.to_json(&*conn)), User::find_by_fqn(&*conn, name.clone()), "Couldn't find requested user", |user| { - let user_id = user.id.clone(); - let followers_count = user.get_followers(&*conn).len(); + may_fail!( + account.map(|a| a.to_json(&*conn)), + User::find_by_fqn(&*conn, name.clone()), + "Couldn't find requested user", + |user| { + let user_id = user.id.clone(); + let followers_count = user.get_followers(&*conn).len(); - Template::render("users/followers", json!({ + Template::render( + "users/followers", + json!({ "user": user.to_json(&*conn), "instance_url": user.get_instance(&*conn).public_domain, "is_remote": user.instance_id != Instance::local_id(&*conn), @@ -165,8 +199,10 @@ fn followers_paginated(name: String, conn: DbConn, account: Option, page: "n_followers": followers_count, "page": page.page, "n_pages": Page::total(followers_count as i32) - })) - }) + }), + ) + } + ) } #[get("/@//followers", rank = 2)] @@ -174,29 +210,38 @@ fn followers(name: String, conn: DbConn, account: Option) -> Template { followers_paginated(name, conn, account, Page::first()) } - #[get("/@/", rank = 1)] -fn activity_details(name: String, conn: DbConn, _ap: ApRequest) -> Option> { +fn activity_details( + name: String, + conn: DbConn, + _ap: ApRequest, +) -> Option> { let user = User::find_local(&*conn, name)?; Some(ActivityStream::new(user.into_activity(&*conn))) } #[get("/users/new")] fn new(user: Option, conn: DbConn) -> Template { - Template::render("users/new", json!({ + Template::render( + "users/new", + json!({ "enabled": Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true), "account": user.map(|u| u.to_json(&*conn)), "errors": null, "form": null - })) + }), + ) } #[get("/@//edit")] fn edit(name: String, user: User, conn: DbConn) -> Option