Avoid panics (#392)

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

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

(sorry, this isn't going to be fun to review, the diff is huge, but it is always the same changes)
This commit is contained in:
Baptiste Gelez 2018-12-29 09:36:07 +01:00 committed by GitHub
parent 4059a840be
commit 80a4dae8bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 1879 additions and 1990 deletions

View File

@ -59,5 +59,5 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
open_registrations: open_reg, open_registrations: open_reg,
short_description_html: String::new(), short_description_html: String::new(),
long_description_html: String::new() long_description_html: String::new()
}); }).expect("Couldn't save instance");
} }

View File

@ -94,7 +94,7 @@ fn refill<'a>(args: &ArgMatches<'a>, conn: &Connection, searcher: Option<Searche
let len = posts.len(); let len = posts.len();
for (i,post) in posts.iter().enumerate() { for (i,post) in posts.iter().enumerate() {
println!("Importing {}/{} : {}", i+1, len, post.title); println!("Importing {}/{} : {}", i+1, len, post.title);
searcher.update_document(conn, &post); searcher.update_document(conn, &post).expect("Couldn't import post");
} }
println!("Commiting result"); println!("Commiting result");
searcher.commit(); searcher.commit();

View File

@ -72,6 +72,7 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
admin, admin,
&bio, &bio,
email, email,
User::hash_pass(&password), User::hash_pass(&password).expect("Couldn't hash password"),
).update_boxes(conn); ).expect("Couldn't save new user")
.update_boxes(conn).expect("Couldn't update ActivityPub informations for new user");
} }

View File

@ -1,4 +1,4 @@
use activitypub::{activity::Create, Object}; use activitypub::{activity::Create, Error as ApError, Object};
use activity_pub::Id; use activity_pub::Id;
@ -13,31 +13,30 @@ pub enum InboxError {
} }
pub trait FromActivity<T: Object, C>: Sized { pub trait FromActivity<T: Object, C>: Sized {
fn from_activity(conn: &C, obj: T, actor: Id) -> Self; type Error: From<ApError>;
fn try_from_activity(conn: &C, act: Create) -> bool { fn from_activity(conn: &C, obj: T, actor: Id) -> Result<Self, Self::Error>;
if let Ok(obj) = act.create_props.object_object() {
Self::from_activity( fn try_from_activity(conn: &C, act: Create) -> Result<Self, Self::Error> {
conn, Self::from_activity(
obj, conn,
act.create_props act.create_props.object_object()?,
.actor_link::<Id>() act.create_props.actor_link::<Id>()?,
.expect("FromActivity::try_from_activity: id not found error"), )
);
true
} else {
false
}
} }
} }
pub trait Notify<C> { pub trait Notify<C> {
fn notify(&self, conn: &C); type Error;
fn notify(&self, conn: &C) -> Result<(), Self::Error>;
} }
pub trait Deletable<C, A> { pub trait Deletable<C, A> {
fn delete(&self, conn: &C) -> A; type Error;
fn delete_id(id: &str, actor_id: &str, conn: &C);
fn delete(&self, conn: &C) -> Result<A, Self::Error>;
fn delete_id(id: &str, actor_id: &str, conn: &C) -> Result<A, Self::Error>;
} }
pub trait WithInbox { pub trait WithInbox {

View File

@ -120,7 +120,7 @@ pub fn broadcast<S: sign::Signer, A: Activity, T: inbox::WithInbox + Actor>(
let mut act = serde_json::to_value(act).expect("activity_pub::broadcast: serialization error"); let mut act = serde_json::to_value(act).expect("activity_pub::broadcast: serialization error");
act["@context"] = context(); act["@context"] = context();
let signed = act.sign(sender); let signed = act.sign(sender).expect("activity_pub::broadcast: signature error");
for inbox in boxes { for inbox in boxes {
// TODO: run it in Sidekiq or something like that // TODO: run it in Sidekiq or something like that
@ -130,7 +130,7 @@ pub fn broadcast<S: sign::Signer, A: Activity, T: inbox::WithInbox + Actor>(
let res = Client::new() let res = Client::new()
.post(&inbox) .post(&inbox)
.headers(headers.clone()) .headers(headers.clone())
.header("Signature", request::signature(sender, &headers)) .header("Signature", request::signature(sender, &headers).expect("activity_pub::broadcast: request signature error"))
.body(body) .body(body)
.send(); .send();
match res { match res {

View File

@ -105,7 +105,7 @@ pub fn headers() -> HeaderMap {
headers headers
} }
pub fn signature<S: Signer>(signer: &S, headers: &HeaderMap) -> HeaderValue { pub fn signature<S: Signer>(signer: &S, headers: &HeaderMap) -> Result<HeaderValue, ()> {
let signed_string = headers let signed_string = headers
.iter() .iter()
.map(|(h, v)| { .map(|(h, v)| {
@ -125,7 +125,7 @@ pub fn signature<S: Signer>(signer: &S, headers: &HeaderMap) -> HeaderValue {
.join(" ") .join(" ")
.to_lowercase(); .to_lowercase();
let data = signer.sign(&signed_string); let data = signer.sign(&signed_string).map_err(|_| ())?;
let sign = base64::encode(&data); let sign = base64::encode(&data);
HeaderValue::from_str(&format!( HeaderValue::from_str(&format!(
@ -133,5 +133,5 @@ pub fn signature<S: Signer>(signer: &S, headers: &HeaderMap) -> HeaderValue {
key_id = signer.get_key_id(), key_id = signer.get_key_id(),
signed_headers = signed_headers, signed_headers = signed_headers,
signature = sign signature = sign
)).expect("request::signature: signature header error") )).map_err(|_| ())
} }

View File

@ -22,16 +22,18 @@ pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
} }
pub trait Signer { pub trait Signer {
type Error;
fn get_key_id(&self) -> String; fn get_key_id(&self) -> String;
/// Sign some data with the signer keypair /// Sign some data with the signer keypair
fn sign(&self, to_sign: &str) -> Vec<u8>; fn sign(&self, to_sign: &str) -> Result<Vec<u8>, Self::Error>;
/// Verify if the signature is valid /// Verify if the signature is valid
fn verify(&self, data: &str, signature: &[u8]) -> bool; fn verify(&self, data: &str, signature: &[u8]) -> Result<bool, Self::Error>;
} }
pub trait Signable { pub trait Signable {
fn sign<T>(&mut self, creator: &T) -> &mut Self fn sign<T>(&mut self, creator: &T) -> Result<&mut Self, ()>
where where
T: Signer; T: Signer;
fn verify<T>(self, creator: &T) -> bool fn verify<T>(self, creator: &T) -> bool
@ -45,7 +47,7 @@ pub trait Signable {
} }
impl Signable for serde_json::Value { impl Signable for serde_json::Value {
fn sign<T: Signer>(&mut self, creator: &T) -> &mut serde_json::Value { fn sign<T: Signer>(&mut self, creator: &T) -> Result<&mut serde_json::Value, ()> {
let creation_date = Utc::now().to_rfc3339(); let creation_date = Utc::now().to_rfc3339();
let mut options = json!({ let mut options = json!({
"type": "RsaSignature2017", "type": "RsaSignature2017",
@ -62,11 +64,11 @@ impl Signable for serde_json::Value {
let document_hash = Self::hash(&self.to_string()); let document_hash = Self::hash(&self.to_string());
let to_be_signed = options_hash + &document_hash; let to_be_signed = options_hash + &document_hash;
let signature = base64::encode(&creator.sign(&to_be_signed)); let signature = base64::encode(&creator.sign(&to_be_signed).map_err(|_| ())?);
options["signatureValue"] = serde_json::Value::String(signature); options["signatureValue"] = serde_json::Value::String(signature);
self["signature"] = options; self["signature"] = options;
self Ok(self)
} }
fn verify<T: Signer>(mut self, creator: &T) -> bool { fn verify<T: Signer>(mut self, creator: &T) -> bool {
@ -107,7 +109,7 @@ impl Signable for serde_json::Value {
} }
let document_hash = Self::hash(&self.to_string()); let document_hash = Self::hash(&self.to_string());
let to_be_signed = options_hash + &document_hash; let to_be_signed = options_hash + &document_hash;
creator.verify(&to_be_signed, &signature) creator.verify(&to_be_signed, &signature).unwrap_or(false)
} }
} }
@ -167,7 +169,7 @@ pub fn verify_http_headers<S: Signer + ::std::fmt::Debug>(
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n"); .join("\n");
if !sender.verify(&h, &base64::decode(signature).unwrap_or_default()) { if !sender.verify(&h, &base64::decode(signature).unwrap_or_default()).unwrap_or(false) {
return SignatureValidity::Invalid; return SignatureValidity::Invalid;
} }
if !headers.contains(&"digest") { if !headers.contains(&"digest") {

View File

@ -8,6 +8,7 @@ use rocket::{
use db_conn::DbConn; use db_conn::DbConn;
use schema::api_tokens; use schema::api_tokens;
use {Error, Result};
#[derive(Clone, Queryable)] #[derive(Clone, Queryable)]
pub struct ApiToken { pub struct ApiToken {
@ -63,22 +64,39 @@ impl ApiToken {
} }
} }
impl<'a, 'r> FromRequest<'a, 'r> for ApiToken { #[derive(Debug)]
type Error = (); pub enum TokenError {
/// The Authorization header was not present
NoHeader,
fn from_request(request: &'a Request<'r>) -> request::Outcome<ApiToken, ()> { /// The type of the token was not specified ("Basic" or "Bearer" for instance)
NoType,
/// No value was provided
NoValue,
/// Error while connecting to the database to retrieve all the token metadata
DbError,
}
impl<'a, 'r> FromRequest<'a, 'r> for ApiToken {
type Error = TokenError;
fn from_request(request: &'a Request<'r>) -> request::Outcome<ApiToken, TokenError> {
let headers: Vec<_> = request.headers().get("Authorization").collect(); let headers: Vec<_> = request.headers().get("Authorization").collect();
if headers.len() != 1 { if headers.len() != 1 {
return Outcome::Failure((Status::BadRequest, ())); return Outcome::Failure((Status::BadRequest, TokenError::NoHeader));
} }
let mut parsed_header = headers[0].split(' '); let mut parsed_header = headers[0].split(' ');
let auth_type = parsed_header.next().expect("Expect a token type"); let auth_type = parsed_header.next()
let val = parsed_header.next().expect("Expect a token value"); .map_or_else(|| Outcome::Failure((Status::BadRequest, TokenError::NoType)), |t| Outcome::Success(t))?;
let val = parsed_header.next()
.map_or_else(|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)), |t| Outcome::Success(t))?;
if auth_type == "Bearer" { if auth_type == "Bearer" {
let conn = request.guard::<DbConn>().expect("Couldn't connect to DB"); let conn = request.guard::<DbConn>().map_failure(|_| (Status::InternalServerError, TokenError::DbError))?;
if let Some(token) = ApiToken::find_by_value(&*conn, val) { if let Ok(token) = ApiToken::find_by_value(&*conn, val) {
return Outcome::Success(token); return Outcome::Success(token);
} }
} }

View File

@ -1,11 +1,11 @@
use canapi::{Error, Provider}; use canapi::{Error as ApiError, Provider};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use plume_api::apps::AppEndpoint; use plume_api::apps::AppEndpoint;
use plume_common::utils::random_hex; use plume_common::utils::random_hex;
use schema::apps; use schema::apps;
use Connection; use {Connection, Error, Result, ApiResult};
#[derive(Clone, Queryable)] #[derive(Clone, Queryable)]
pub struct App { pub struct App {
@ -31,7 +31,7 @@ pub struct NewApp {
impl Provider<Connection> for App { impl Provider<Connection> for App {
type Data = AppEndpoint; type Data = AppEndpoint;
fn get(_conn: &Connection, _id: i32) -> Result<AppEndpoint, Error> { fn get(_conn: &Connection, _id: i32) -> ApiResult<AppEndpoint> {
unimplemented!() unimplemented!()
} }
@ -39,7 +39,7 @@ impl Provider<Connection> for App {
unimplemented!() unimplemented!()
} }
fn create(conn: &Connection, data: AppEndpoint) -> Result<AppEndpoint, Error> { fn create(conn: &Connection, data: AppEndpoint) -> ApiResult<AppEndpoint> {
let client_id = random_hex(); let client_id = random_hex();
let client_secret = random_hex(); let client_secret = random_hex();
@ -52,7 +52,7 @@ impl Provider<Connection> for App {
redirect_uri: data.redirect_uri, redirect_uri: data.redirect_uri,
website: data.website, website: data.website,
}, },
); ).map_err(|_| ApiError::NotFound("Couldn't register app".into()))?;
Ok(AppEndpoint { Ok(AppEndpoint {
id: Some(app.id), id: Some(app.id),
@ -64,7 +64,7 @@ impl Provider<Connection> for App {
}) })
} }
fn update(_conn: &Connection, _id: i32, _new_data: AppEndpoint) -> Result<AppEndpoint, Error> { fn update(_conn: &Connection, _id: i32, _new_data: AppEndpoint) -> ApiResult<AppEndpoint> {
unimplemented!() unimplemented!()
} }

View File

@ -1,6 +1,7 @@
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use schema::blog_authors; use schema::blog_authors;
use {Error, Result};
#[derive(Clone, Queryable, Identifiable)] #[derive(Clone, Queryable, Identifiable)]
pub struct BlogAuthor { pub struct BlogAuthor {

View File

@ -26,7 +26,7 @@ use safe_string::SafeString;
use schema::blogs; use schema::blogs;
use search::Searcher; use search::Searcher;
use users::User; use users::User;
use {Connection, BASE_URL, USE_HTTPS}; use {Connection, BASE_URL, USE_HTTPS, Error, Result};
pub type CustomGroup = CustomObject<ApSignature, Group>; pub type CustomGroup = CustomObject<ApSignature, Group>;
@ -67,11 +67,11 @@ impl Blog {
find_by!(blogs, find_by_ap_url, ap_url as &str); find_by!(blogs, find_by_ap_url, ap_url as &str);
find_by!(blogs, find_by_name, actor_id as &str, instance_id as i32); find_by!(blogs, find_by_name, actor_id as &str, instance_id as i32);
pub fn get_instance(&self, conn: &Connection) -> Instance { pub fn get_instance(&self, conn: &Connection) -> Result<Instance> {
Instance::get(conn, self.instance_id).expect("Blog::get_instance: instance not found error") Instance::get(conn, self.instance_id)
} }
pub fn list_authors(&self, conn: &Connection) -> Vec<User> { pub fn list_authors(&self, conn: &Connection) -> Result<Vec<User>> {
use schema::blog_authors; use schema::blog_authors;
use schema::users; use schema::users;
let authors_ids = blog_authors::table let authors_ids = blog_authors::table
@ -80,19 +80,19 @@ impl Blog {
users::table users::table
.filter(users::id.eq_any(authors_ids)) .filter(users::id.eq_any(authors_ids))
.load::<User>(conn) .load::<User>(conn)
.expect("Blog::list_authors: author loading error") .map_err(Error::from)
} }
pub fn count_authors(&self, conn: &Connection) -> i64 { pub fn count_authors(&self, conn: &Connection) -> Result<i64> {
use schema::blog_authors; use schema::blog_authors;
blog_authors::table blog_authors::table
.filter(blog_authors::blog_id.eq(self.id)) .filter(blog_authors::blog_id.eq(self.id))
.count() .count()
.get_result(conn) .get_result(conn)
.expect("Blog::count_authors: count loading error") .map_err(Error::from)
} }
pub fn find_for_author(conn: &Connection, author: &User) -> Vec<Blog> { pub fn find_for_author(conn: &Connection, author: &User) -> Result<Vec<Blog>> {
use schema::blog_authors; use schema::blog_authors;
let author_ids = blog_authors::table let author_ids = blog_authors::table
.filter(blog_authors::author_id.eq(author.id)) .filter(blog_authors::author_id.eq(author.id))
@ -100,62 +100,40 @@ impl Blog {
blogs::table blogs::table
.filter(blogs::id.eq_any(author_ids)) .filter(blogs::id.eq_any(author_ids))
.load::<Blog>(conn) .load::<Blog>(conn)
.expect("Blog::find_for_author: blog loading error") .map_err(Error::from)
} }
pub fn find_local(conn: &Connection, name: &str) -> Option<Blog> { pub fn find_local(conn: &Connection, name: &str) -> Result<Blog> {
Blog::find_by_name(conn, name, Instance::local_id(conn)) Blog::find_by_name(conn, name, Instance::get_local(conn)?.id)
} }
pub fn find_by_fqn(conn: &Connection, fqn: &str) -> Option<Blog> { pub fn find_by_fqn(conn: &Connection, fqn: &str) -> Result<Blog> {
if fqn.contains('@') { let mut split_fqn = fqn.split('@');
// remote blog let actor = split_fqn.next().ok_or(Error::InvalidValue)?;
match Instance::find_by_domain( if let Some(domain) = split_fqn.next() { // remote blog
conn, Instance::find_by_domain(conn, domain)
fqn.split('@') .and_then(|instance| Blog::find_by_name(conn, actor, instance.id))
.last() .or_else(|_| Blog::fetch_from_webfinger(conn, fqn))
.expect("Blog::find_by_fqn: unreachable"), } else { // local blog
) { Blog::find_local(conn, actor)
Some(instance) => match Blog::find_by_name( }
}
fn fetch_from_webfinger(conn: &Connection, acct: &str) -> Result<Blog> {
resolve(acct.to_owned(), *USE_HTTPS)?.links
.into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
.ok_or(Error::Webfinger)
.and_then(|l| {
Blog::fetch_from_url(
conn, conn,
fqn.split('@') &l.href?
.nth(0) )
.expect("Blog::find_by_fqn: unreachable"), })
instance.id,
) {
Some(u) => Some(u),
None => Blog::fetch_from_webfinger(conn, fqn),
},
None => Blog::fetch_from_webfinger(conn, fqn),
}
} else {
// local blog
Blog::find_local(conn, fqn)
}
} }
fn fetch_from_webfinger(conn: &Connection, acct: &str) -> Option<Blog> { fn fetch_from_url(conn: &Connection, url: &str) -> Result<Blog> {
match resolve(acct.to_owned(), *USE_HTTPS) { let mut res = Client::new()
Ok(wf) => wf
.links
.into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
.and_then(|l| {
Blog::fetch_from_url(
conn,
&l.href
.expect("Blog::fetch_from_webfinger: href not found error"),
)
}),
Err(details) => {
println!("{:?}", details);
None
}
}
}
fn fetch_from_url(conn: &Connection, url: &str) -> Option<Blog> {
let req = Client::new()
.get(url) .get(url)
.header( .header(
ACCEPT, ACCEPT,
@ -164,139 +142,109 @@ impl Blog {
.into_iter() .into_iter()
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", "), .join(", "),
).expect("Blog::fetch_from_url: accept_header generation error"), )?,
) )
.send(); .send()?;
match req {
Ok(mut res) => { let text = &res.text()?;
let text = &res let ap_sign: ApSignature =
.text() serde_json::from_str(text)?;
.expect("Blog::fetch_from_url: body reading error"); let mut json: CustomGroup =
let ap_sign: ApSignature = serde_json::from_str(text)?;
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
let mut json: CustomGroup = Blog::from_activity(
serde_json::from_str(text).expect("Blog::fetch_from_url: body parsing error"); conn,
json.custom_props = ap_sign; // without this workaround, publicKey is not correctly deserialized &json,
Some(Blog::from_activity( Url::parse(url)?.host_str()?,
conn, )
&json,
Url::parse(url)
.expect("Blog::fetch_from_url: url parsing error")
.host_str()
.expect("Blog::fetch_from_url: host extraction error"),
))
}
Err(_) => None,
}
} }
fn from_activity(conn: &Connection, acct: &CustomGroup, inst: &str) -> Blog { fn from_activity(conn: &Connection, acct: &CustomGroup, inst: &str) -> Result<Blog> {
let instance = match Instance::find_by_domain(conn, inst) { let instance = Instance::find_by_domain(conn, inst).or_else(|_|
Some(instance) => instance, Instance::insert(
None => { conn,
Instance::insert( NewInstance {
conn, public_domain: inst.to_owned(),
NewInstance { name: inst.to_owned(),
public_domain: inst.to_owned(), local: false,
name: inst.to_owned(), // We don't really care about all the following for remote instances
local: false, long_description: SafeString::new(""),
// We don't really care about all the following for remote instances short_description: SafeString::new(""),
long_description: SafeString::new(""), default_license: String::new(),
short_description: SafeString::new(""), open_registrations: true,
default_license: String::new(), short_description_html: String::new(),
open_registrations: true, long_description_html: String::new(),
short_description_html: String::new(), },
long_description_html: String::new(), )
}, )?;
)
}
};
Blog::insert( Blog::insert(
conn, conn,
NewBlog { NewBlog {
actor_id: acct actor_id: acct
.object .object
.ap_actor_props .ap_actor_props
.preferred_username_string() .preferred_username_string()?,
.expect("Blog::from_activity: preferredUsername error"),
title: acct title: acct
.object .object
.object_props .object_props
.name_string() .name_string()?,
.expect("Blog::from_activity: name error"),
outbox_url: acct outbox_url: acct
.object .object
.ap_actor_props .ap_actor_props
.outbox_string() .outbox_string()?,
.expect("Blog::from_activity: outbox error"),
inbox_url: acct inbox_url: acct
.object .object
.ap_actor_props .ap_actor_props
.inbox_string() .inbox_string()?,
.expect("Blog::from_activity: inbox error"),
summary: acct summary: acct
.object .object
.object_props .object_props
.summary_string() .summary_string()?,
.expect("Blog::from_activity: summary error"),
instance_id: instance.id, instance_id: instance.id,
ap_url: acct ap_url: acct
.object .object
.object_props .object_props
.id_string() .id_string()?,
.expect("Blog::from_activity: id error"),
public_key: acct public_key: acct
.custom_props .custom_props
.public_key_publickey() .public_key_publickey()?
.expect("Blog::from_activity: publicKey error") .public_key_pem_string()?,
.public_key_pem_string()
.expect("Blog::from_activity: publicKey.publicKeyPem error"),
private_key: None, private_key: None,
}, },
) )
} }
pub fn to_activity(&self, _conn: &Connection) -> CustomGroup { pub fn to_activity(&self, _conn: &Connection) -> Result<CustomGroup> {
let mut blog = Group::default(); let mut blog = Group::default();
blog.ap_actor_props blog.ap_actor_props
.set_preferred_username_string(self.actor_id.clone()) .set_preferred_username_string(self.actor_id.clone())?;
.expect("Blog::to_activity: preferredUsername error");
blog.object_props blog.object_props
.set_name_string(self.title.clone()) .set_name_string(self.title.clone())?;
.expect("Blog::to_activity: name error");
blog.ap_actor_props blog.ap_actor_props
.set_outbox_string(self.outbox_url.clone()) .set_outbox_string(self.outbox_url.clone())?;
.expect("Blog::to_activity: outbox error");
blog.ap_actor_props blog.ap_actor_props
.set_inbox_string(self.inbox_url.clone()) .set_inbox_string(self.inbox_url.clone())?;
.expect("Blog::to_activity: inbox error");
blog.object_props blog.object_props
.set_summary_string(self.summary.clone()) .set_summary_string(self.summary.clone())?;
.expect("Blog::to_activity: summary error");
blog.object_props blog.object_props
.set_id_string(self.ap_url.clone()) .set_id_string(self.ap_url.clone())?;
.expect("Blog::to_activity: id error");
let mut public_key = PublicKey::default(); let mut public_key = PublicKey::default();
public_key public_key
.set_id_string(format!("{}#main-key", self.ap_url)) .set_id_string(format!("{}#main-key", self.ap_url))?;
.expect("Blog::to_activity: publicKey.id error");
public_key public_key
.set_owner_string(self.ap_url.clone()) .set_owner_string(self.ap_url.clone())?;
.expect("Blog::to_activity: publicKey.owner error");
public_key public_key
.set_public_key_pem_string(self.public_key.clone()) .set_public_key_pem_string(self.public_key.clone())?;
.expect("Blog::to_activity: publicKey.publicKeyPem error");
let mut ap_signature = ApSignature::default(); let mut ap_signature = ApSignature::default();
ap_signature ap_signature
.set_public_key_publickey(public_key) .set_public_key_publickey(public_key)?;
.expect("Blog::to_activity: publicKey error");
CustomGroup::new(blog, ap_signature) Ok(CustomGroup::new(blog, ap_signature))
} }
pub fn update_boxes(&self, conn: &Connection) { pub fn update_boxes(&self, conn: &Connection) -> Result<()> {
let instance = self.get_instance(conn); let instance = self.get_instance(conn)?;
if self.outbox_url.is_empty() { if self.outbox_url.is_empty() {
diesel::update(self) diesel::update(self)
.set(blogs::outbox_url.eq(instance.compute_box( .set(blogs::outbox_url.eq(instance.compute_box(
@ -304,8 +252,7 @@ impl Blog {
&self.actor_id, &self.actor_id,
"outbox", "outbox",
))) )))
.execute(conn) .execute(conn)?;
.expect("Blog::update_boxes: outbox update error");
} }
if self.inbox_url.is_empty() { if self.inbox_url.is_empty() {
@ -315,49 +262,45 @@ impl Blog {
&self.actor_id, &self.actor_id,
"inbox", "inbox",
))) )))
.execute(conn) .execute(conn)?;
.expect("Blog::update_boxes: inbox update error");
} }
if self.ap_url.is_empty() { if self.ap_url.is_empty() {
diesel::update(self) diesel::update(self)
.set(blogs::ap_url.eq(instance.compute_box(BLOG_PREFIX, &self.actor_id, ""))) .set(blogs::ap_url.eq(instance.compute_box(BLOG_PREFIX, &self.actor_id, "")))
.execute(conn) .execute(conn)?;
.expect("Blog::update_boxes: ap_url update error");
} }
Ok(())
} }
pub fn outbox(&self, conn: &Connection) -> ActivityStream<OrderedCollection> { pub fn outbox(&self, conn: &Connection) -> Result<ActivityStream<OrderedCollection>> {
let mut coll = OrderedCollection::default(); let mut coll = OrderedCollection::default();
coll.collection_props.items = serde_json::to_value(self.get_activities(conn)) coll.collection_props.items = serde_json::to_value(self.get_activities(conn)?)?;
.expect("Blog::outbox: activity serialization error");
coll.collection_props coll.collection_props
.set_total_items_u64(self.get_activities(conn).len() as u64) .set_total_items_u64(self.get_activities(conn)?.len() as u64)?;
.expect("Blog::outbox: count serialization error"); Ok(ActivityStream::new(coll))
ActivityStream::new(coll)
} }
fn get_activities(&self, _conn: &Connection) -> Vec<serde_json::Value> { fn get_activities(&self, _conn: &Connection) -> Result<Vec<serde_json::Value>> {
vec![] Ok(vec![])
} }
pub fn get_keypair(&self) -> PKey<Private> { pub fn get_keypair(&self) -> Result<PKey<Private>> {
PKey::from_rsa( PKey::from_rsa(
Rsa::private_key_from_pem( Rsa::private_key_from_pem(
self.private_key self.private_key
.clone() .clone()?
.expect("Blog::get_keypair: private key not found error")
.as_ref(), .as_ref(),
).expect("Blog::get_keypair: pem parsing error"), )?,
).expect("Blog::get_keypair: private key deserialization error") ).map_err(Error::from)
} }
pub fn webfinger(&self, conn: &Connection) -> Webfinger { pub fn webfinger(&self, conn: &Connection) -> Result<Webfinger> {
Webfinger { Ok(Webfinger {
subject: format!( subject: format!(
"acct:{}@{}", "acct:{}@{}",
self.actor_id, self.actor_id,
self.get_instance(conn).public_domain self.get_instance(conn)?.public_domain
), ),
aliases: vec![self.ap_url.clone()], aliases: vec![self.ap_url.clone()],
links: vec![ links: vec![
@ -370,7 +313,7 @@ impl Blog {
Link { Link {
rel: String::from("http://schemas.google.com/g/2010#updates-from"), rel: String::from("http://schemas.google.com/g/2010#updates-from"),
mime_type: Some(String::from("application/atom+xml")), mime_type: Some(String::from("application/atom+xml")),
href: Some(self.get_instance(conn).compute_box( href: Some(self.get_instance(conn)?.compute_box(
BLOG_PREFIX, BLOG_PREFIX,
&self.actor_id, &self.actor_id,
"feed.atom", "feed.atom",
@ -384,50 +327,41 @@ impl Blog {
template: None, template: None,
}, },
], ],
} })
} }
pub fn from_url(conn: &Connection, url: &str) -> Option<Blog> { pub fn from_url(conn: &Connection, url: &str) -> Result<Blog> {
Blog::find_by_ap_url(conn, url).or_else(|| { Blog::find_by_ap_url(conn, url).or_else(|_| {
// The requested blog was not in the DB // The requested blog was not in the DB
// We try to fetch it if it is remote // We try to fetch it if it is remote
if Url::parse(url) if Url::parse(url)?.host_str()? != BASE_URL.as_str() {
.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) Blog::fetch_from_url(conn, url)
} else { } else {
None Err(Error::NotFound)
} }
}) })
} }
pub fn get_fqn(&self, conn: &Connection) -> String { pub fn get_fqn(&self, conn: &Connection) -> String {
if self.instance_id == Instance::local_id(conn) { if self.instance_id == Instance::get_local(conn).ok().expect("Blog::get_fqn: local instance error").id {
self.actor_id.clone() self.actor_id.clone()
} else { } else {
format!( format!(
"{}@{}", "{}@{}",
self.actor_id, self.actor_id,
self.get_instance(conn).public_domain self.get_instance(conn).ok().expect("Blog::get_fqn: instance error").public_domain
) )
} }
} }
pub fn to_json(&self, conn: &Connection) -> serde_json::Value { pub fn delete(&self, conn: &Connection, searcher: &Searcher) -> Result<()> {
let mut json = serde_json::to_value(self).expect("Blog::to_json: serialization error"); for post in Post::get_for_blog(conn, &self)? {
json["fqn"] = json!(self.get_fqn(conn)); post.delete(&(conn, searcher))?;
json
}
pub fn delete(&self, conn: &Connection, searcher: &Searcher) {
for post in Post::get_for_blog(conn, &self) {
post.delete(&(conn, searcher));
} }
diesel::delete(self) diesel::delete(self)
.execute(conn) .execute(conn)
.expect("Blog::delete: blog deletion error"); .map(|_| ())
.map_err(Error::from)
} }
} }
@ -455,35 +389,33 @@ impl WithInbox for Blog {
} }
impl sign::Signer for Blog { impl sign::Signer for Blog {
type Error = Error;
fn get_key_id(&self) -> String { fn get_key_id(&self) -> String {
format!("{}#main-key", self.ap_url) format!("{}#main-key", self.ap_url)
} }
fn sign(&self, to_sign: &str) -> Vec<u8> { fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
let key = self.get_keypair(); let key = self.get_keypair()?;
let mut signer = let mut signer =
Signer::new(MessageDigest::sha256(), &key).expect("Blog::sign: initialization error"); Signer::new(MessageDigest::sha256(), &key)?;
signer signer
.update(to_sign.as_bytes()) .update(to_sign.as_bytes())?;
.expect("Blog::sign: content insertion error");
signer signer
.sign_to_vec() .sign_to_vec()
.expect("Blog::sign: finalization error") .map_err(Error::from)
} }
fn verify(&self, data: &str, signature: &[u8]) -> bool { fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
let key = PKey::from_rsa( let key = PKey::from_rsa(
Rsa::public_key_from_pem(self.public_key.as_ref()) 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)?;
let mut verifier = Verifier::new(MessageDigest::sha256(), &key)
.expect("Blog::verify: initialization error");
verifier verifier
.update(data.as_bytes()) .update(data.as_bytes())?;
.expect("Blog::verify: content insertion error");
verifier verifier
.verify(&signature) .verify(&signature)
.expect("Blog::verify: finalization error") .map_err(Error::from)
} }
} }
@ -493,9 +425,9 @@ impl NewBlog {
title: String, title: String,
summary: String, summary: String,
instance_id: i32, instance_id: i32,
) -> NewBlog { ) -> Result<NewBlog> {
let (pub_key, priv_key) = sign::gen_keypair(); let (pub_key, priv_key) = sign::gen_keypair();
NewBlog { Ok(NewBlog {
actor_id, actor_id,
title, title,
summary, summary,
@ -503,11 +435,9 @@ impl NewBlog {
inbox_url: String::from(""), inbox_url: String::from(""),
instance_id, instance_id,
ap_url: String::from(""), ap_url: String::from(""),
public_key: String::from_utf8(pub_key).expect("NewBlog::new_local: public key error"), public_key: String::from_utf8(pub_key).or(Err(Error::Signature))?,
private_key: Some( private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
String::from_utf8(priv_key).expect("NewBlog::new_local: private key error"), })
),
}
} }
} }
@ -529,23 +459,23 @@ pub(crate) mod tests {
"BlogName".to_owned(), "BlogName".to_owned(),
"Blog name".to_owned(), "Blog name".to_owned(),
"This is a small blog".to_owned(), "This is a small blog".to_owned(),
Instance::local_id(conn), Instance::get_local(conn).unwrap().id
)); ).unwrap()).unwrap();
blog1.update_boxes(conn); blog1.update_boxes(conn).unwrap();
let blog2 = Blog::insert(conn, NewBlog::new_local( let blog2 = Blog::insert(conn, NewBlog::new_local(
"MyBlog".to_owned(), "MyBlog".to_owned(),
"My blog".to_owned(), "My blog".to_owned(),
"Welcome to my blog".to_owned(), "Welcome to my blog".to_owned(),
Instance::local_id(conn), Instance::get_local(conn).unwrap().id
)); ).unwrap()).unwrap();
blog2.update_boxes(conn); blog2.update_boxes(conn).unwrap();
let blog3 = Blog::insert(conn, NewBlog::new_local( let blog3 = Blog::insert(conn, NewBlog::new_local(
"WhyILikePlume".to_owned(), "WhyILikePlume".to_owned(),
"Why I like Plume".to_owned(), "Why I like Plume".to_owned(),
"In this blog I will explay you why I like Plume so much".to_owned(), "In this blog I will explay you why I like Plume so much".to_owned(),
Instance::local_id(conn), Instance::get_local(conn).unwrap().id
)); ).unwrap()).unwrap();
blog3.update_boxes(conn); blog3.update_boxes(conn).unwrap();
BlogAuthor::insert( BlogAuthor::insert(
conn, conn,
@ -554,7 +484,7 @@ pub(crate) mod tests {
author_id: users[0].id, author_id: users[0].id,
is_owner: true, is_owner: true,
}, },
); ).unwrap();
BlogAuthor::insert( BlogAuthor::insert(
conn, conn,
@ -563,7 +493,7 @@ pub(crate) mod tests {
author_id: users[1].id, author_id: users[1].id,
is_owner: false, is_owner: false,
}, },
); ).unwrap();
BlogAuthor::insert( BlogAuthor::insert(
conn, conn,
@ -572,7 +502,7 @@ pub(crate) mod tests {
author_id: users[1].id, author_id: users[1].id,
is_owner: true, is_owner: true,
}, },
); ).unwrap();
BlogAuthor::insert( BlogAuthor::insert(
conn, conn,
@ -581,7 +511,7 @@ pub(crate) mod tests {
author_id: users[2].id, author_id: users[2].id,
is_owner: true, is_owner: true,
}, },
); ).unwrap();
(users, vec![ blog1, blog2, blog3 ]) (users, vec![ blog1, blog2, blog3 ])
} }
@ -597,11 +527,11 @@ pub(crate) mod tests {
"SomeName".to_owned(), "SomeName".to_owned(),
"Some name".to_owned(), "Some name".to_owned(),
"This is some blog".to_owned(), "This is some blog".to_owned(),
Instance::local_id(conn), Instance::get_local(conn).unwrap().id
), ).unwrap(),
); ).unwrap();
assert_eq!(blog.get_instance(conn).id, Instance::local_id(conn)); assert_eq!(blog.get_instance(conn).unwrap().id, Instance::get_local(conn).unwrap().id);
// TODO add tests for remote instance // TODO add tests for remote instance
Ok(()) Ok(())
@ -620,20 +550,20 @@ pub(crate) mod tests {
"SomeName".to_owned(), "SomeName".to_owned(),
"Some name".to_owned(), "Some name".to_owned(),
"This is some blog".to_owned(), "This is some blog".to_owned(),
Instance::local_id(conn), Instance::get_local(conn).unwrap().id,
), ).unwrap(),
); ).unwrap();
b1.update_boxes(conn); b1.update_boxes(conn).unwrap();
let b2 = Blog::insert( let b2 = Blog::insert(
conn, conn,
NewBlog::new_local( NewBlog::new_local(
"Blog".to_owned(), "Blog".to_owned(),
"Blog".to_owned(), "Blog".to_owned(),
"I've named my blog Blog".to_owned(), "I've named my blog Blog".to_owned(),
Instance::local_id(conn), Instance::get_local(conn).unwrap().id
), ).unwrap(),
); ).unwrap();
b2.update_boxes(conn); b2.update_boxes(conn).unwrap();
let blog = vec![ b1, b2 ]; let blog = vec![ b1, b2 ];
BlogAuthor::insert( BlogAuthor::insert(
@ -643,7 +573,7 @@ pub(crate) mod tests {
author_id: user[0].id, author_id: user[0].id,
is_owner: true, is_owner: true,
}, },
); ).unwrap();
BlogAuthor::insert( BlogAuthor::insert(
conn, conn,
@ -652,7 +582,7 @@ pub(crate) mod tests {
author_id: user[1].id, author_id: user[1].id,
is_owner: false, is_owner: false,
}, },
); ).unwrap();
BlogAuthor::insert( BlogAuthor::insert(
conn, conn,
@ -661,50 +591,50 @@ pub(crate) mod tests {
author_id: user[0].id, author_id: user[0].id,
is_owner: true, is_owner: true,
}, },
); ).unwrap();
assert!( assert!(
blog[0] blog[0]
.list_authors(conn) .list_authors(conn).unwrap()
.iter() .iter()
.any(|a| a.id == user[0].id) .any(|a| a.id == user[0].id)
); );
assert!( assert!(
blog[0] blog[0]
.list_authors(conn) .list_authors(conn).unwrap()
.iter() .iter()
.any(|a| a.id == user[1].id) .any(|a| a.id == user[1].id)
); );
assert!( assert!(
blog[1] blog[1]
.list_authors(conn) .list_authors(conn).unwrap()
.iter() .iter()
.any(|a| a.id == user[0].id) .any(|a| a.id == user[0].id)
); );
assert!( assert!(
!blog[1] !blog[1]
.list_authors(conn) .list_authors(conn).unwrap()
.iter() .iter()
.any(|a| a.id == user[1].id) .any(|a| a.id == user[1].id)
); );
assert!( assert!(
Blog::find_for_author(conn, &user[0]) Blog::find_for_author(conn, &user[0]).unwrap()
.iter() .iter()
.any(|b| b.id == blog[0].id) .any(|b| b.id == blog[0].id)
); );
assert!( assert!(
Blog::find_for_author(conn, &user[1]) Blog::find_for_author(conn, &user[1]).unwrap()
.iter() .iter()
.any(|b| b.id == blog[0].id) .any(|b| b.id == blog[0].id)
); );
assert!( assert!(
Blog::find_for_author(conn, &user[0]) Blog::find_for_author(conn, &user[0]).unwrap()
.iter() .iter()
.any(|b| b.id == blog[1].id) .any(|b| b.id == blog[1].id)
); );
assert!( assert!(
!Blog::find_for_author(conn, &user[1]) !Blog::find_for_author(conn, &user[1]).unwrap()
.iter() .iter()
.any(|b| b.id == blog[1].id) .any(|b| b.id == blog[1].id)
); );
@ -725,9 +655,9 @@ pub(crate) mod tests {
"SomeName".to_owned(), "SomeName".to_owned(),
"Some name".to_owned(), "Some name".to_owned(),
"This is some blog".to_owned(), "This is some blog".to_owned(),
Instance::local_id(conn), Instance::get_local(conn).unwrap().id,
), ).unwrap(),
); ).unwrap();
assert_eq!( assert_eq!(
Blog::find_local(conn, "SomeName").unwrap().id, Blog::find_local(conn, "SomeName").unwrap().id,
@ -750,9 +680,9 @@ pub(crate) mod tests {
"SomeName".to_owned(), "SomeName".to_owned(),
"Some name".to_owned(), "Some name".to_owned(),
"This is some blog".to_owned(), "This is some blog".to_owned(),
Instance::local_id(conn), Instance::get_local(conn).unwrap().id,
), ).unwrap(),
); ).unwrap();
assert_eq!(blog.get_fqn(conn), "SomeName"); assert_eq!(blog.get_fqn(conn), "SomeName");
@ -766,8 +696,8 @@ pub(crate) mod tests {
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (_, blogs) = fill_database(conn); let (_, blogs) = fill_database(conn);
blogs[0].delete(conn, &get_searcher()); blogs[0].delete(conn, &get_searcher()).unwrap();
assert!(Blog::get(conn, blogs[0].id).is_none()); assert!(Blog::get(conn, blogs[0].id).is_err());
Ok(()) Ok(())
}); });
@ -786,20 +716,20 @@ pub(crate) mod tests {
"SomeName".to_owned(), "SomeName".to_owned(),
"Some name".to_owned(), "Some name".to_owned(),
"This is some blog".to_owned(), "This is some blog".to_owned(),
Instance::local_id(conn), Instance::get_local(conn).unwrap().id,
), ).unwrap(),
); ).unwrap();
b1.update_boxes(conn); b1.update_boxes(conn).unwrap();
let b2 = Blog::insert( let b2 = Blog::insert(
conn, conn,
NewBlog::new_local( NewBlog::new_local(
"Blog".to_owned(), "Blog".to_owned(),
"Blog".to_owned(), "Blog".to_owned(),
"I've named my blog Blog".to_owned(), "I've named my blog Blog".to_owned(),
Instance::local_id(conn), Instance::get_local(conn).unwrap().id,
), ).unwrap(),
); ).unwrap();
b2.update_boxes(conn); b2.update_boxes(conn).unwrap();
let blog = vec![ b1, b2 ]; let blog = vec![ b1, b2 ];
BlogAuthor::insert( BlogAuthor::insert(
@ -809,7 +739,7 @@ pub(crate) mod tests {
author_id: user[0].id, author_id: user[0].id,
is_owner: true, is_owner: true,
}, },
); ).unwrap();
BlogAuthor::insert( BlogAuthor::insert(
conn, conn,
@ -818,7 +748,7 @@ pub(crate) mod tests {
author_id: user[1].id, author_id: user[1].id,
is_owner: false, is_owner: false,
}, },
); ).unwrap();
BlogAuthor::insert( BlogAuthor::insert(
conn, conn,
@ -827,13 +757,13 @@ pub(crate) mod tests {
author_id: user[0].id, author_id: user[0].id,
is_owner: true, is_owner: true,
}, },
); ).unwrap();
user[0].delete(conn, &searcher); user[0].delete(conn, &searcher).unwrap();
assert!(Blog::get(conn, blog[0].id).is_some()); assert!(Blog::get(conn, blog[0].id).is_ok());
assert!(Blog::get(conn, blog[1].id).is_none()); assert!(Blog::get(conn, blog[1].id).is_err());
user[1].delete(conn, &searcher); user[1].delete(conn, &searcher).unwrap();
assert!(Blog::get(conn, blog[0].id).is_none()); assert!(Blog::get(conn, blog[0].id).is_err());
Ok(()) Ok(())
}); });

View File

@ -3,7 +3,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use comments::Comment; use comments::Comment;
use schema::comment_seers; use schema::comment_seers;
use users::User; use users::User;
use Connection; use {Connection, Error, Result};
#[derive(Queryable, Serialize, Clone)] #[derive(Queryable, Serialize, Clone)]
pub struct CommentSeers { pub struct CommentSeers {
@ -22,11 +22,11 @@ pub struct NewCommentSeers {
impl CommentSeers { impl CommentSeers {
insert!(comment_seers, NewCommentSeers); insert!(comment_seers, NewCommentSeers);
pub fn can_see(conn: &Connection, c: &Comment, u: &User) -> bool { pub fn can_see(conn: &Connection, c: &Comment, u: &User) -> Result<bool> {
!comment_seers::table.filter(comment_seers::comment_id.eq(c.id)) comment_seers::table.filter(comment_seers::comment_id.eq(c.id))
.filter(comment_seers::user_id.eq(u.id)) .filter(comment_seers::user_id.eq(u.id))
.load::<CommentSeers>(conn) .load::<CommentSeers>(conn)
.expect("Comment::get_responses: loading error") .map_err(Error::from)
.is_empty() .map(|r| !r.is_empty())
} }
} }

View File

@ -18,7 +18,7 @@ use posts::Post;
use safe_string::SafeString; use safe_string::SafeString;
use schema::comments; use schema::comments;
use users::User; use users::User;
use Connection; use {Connection, Error, Result};
#[derive(Queryable, Identifiable, Serialize, Clone)] #[derive(Queryable, Identifiable, Serialize, Clone)]
pub struct Comment { pub struct Comment {
@ -53,150 +53,125 @@ impl Comment {
list_by!(comments, list_by_post, post_id as i32); list_by!(comments, list_by_post, post_id as i32);
find_by!(comments, find_by_ap_url, ap_url as &str); find_by!(comments, find_by_ap_url, ap_url as &str);
pub fn get_author(&self, conn: &Connection) -> User { pub fn get_author(&self, conn: &Connection) -> Result<User> {
User::get(conn, self.author_id).expect("Comment::get_author: author error") User::get(conn, self.author_id)
} }
pub fn get_post(&self, conn: &Connection) -> Post { pub fn get_post(&self, conn: &Connection) -> Result<Post> {
Post::get(conn, self.post_id).expect("Comment::get_post: post error") Post::get(conn, self.post_id)
} }
pub fn count_local(conn: &Connection) -> i64 { pub fn count_local(conn: &Connection) -> Result<i64> {
use schema::users; use schema::users;
let local_authors = users::table let local_authors = users::table
.filter(users::instance_id.eq(Instance::local_id(conn))) .filter(users::instance_id.eq(Instance::get_local(conn)?.id))
.select(users::id); .select(users::id);
comments::table comments::table
.filter(comments::author_id.eq_any(local_authors)) .filter(comments::author_id.eq_any(local_authors))
.count() .count()
.get_result(conn) .get_result(conn)
.expect("Comment::count_local: loading error") .map_err(Error::from)
} }
pub fn get_responses(&self, conn: &Connection) -> Vec<Comment> { pub fn get_responses(&self, conn: &Connection) -> Result<Vec<Comment>> {
comments::table.filter(comments::in_response_to_id.eq(self.id)) comments::table.filter(comments::in_response_to_id.eq(self.id))
.load::<Comment>(conn) .load::<Comment>(conn)
.expect("Comment::get_responses: loading error") .map_err(Error::from)
} }
pub fn update_ap_url(&self, conn: &Connection) -> Comment { pub fn update_ap_url(&self, conn: &Connection) -> Result<Comment> {
if self.ap_url.is_none() { if self.ap_url.is_none() {
diesel::update(self) diesel::update(self)
.set(comments::ap_url.eq(self.compute_id(conn))) .set(comments::ap_url.eq(self.compute_id(conn)?))
.execute(conn) .execute(conn)?;
.expect("Comment::update_ap_url: update error"); Comment::get(conn, self.id)
Comment::get(conn, self.id).expect("Comment::update_ap_url: get error")
} else { } else {
self.clone() Ok(self.clone())
} }
} }
pub fn compute_id(&self, conn: &Connection) -> String { pub fn compute_id(&self, conn: &Connection) -> Result<String> {
format!("{}comment/{}", self.get_post(conn).ap_url, self.id) Ok(format!("{}comment/{}", self.get_post(conn)?.ap_url, self.id))
} }
pub fn can_see(&self, conn: &Connection, user: Option<&User>) -> bool { pub fn can_see(&self, conn: &Connection, user: Option<&User>) -> bool {
self.public_visibility || self.public_visibility ||
user.as_ref().map(|u| CommentSeers::can_see(conn, self, u)).unwrap_or(false) user.as_ref().map(|u| CommentSeers::can_see(conn, self, u).unwrap_or(false))
.unwrap_or(false)
} }
pub fn to_activity(&self, conn: &Connection) -> Note { pub fn to_activity(&self, conn: &Connection) -> Result<Note> {
let (html, mentions, _hashtags) = utils::md_to_html(self.content.get().as_ref(), let (html, mentions, _hashtags) = utils::md_to_html(self.content.get().as_ref(),
&Instance::get_local(conn) &Instance::get_local(conn)?.public_domain);
.expect("Comment::to_activity: instance error")
.public_domain);
let author = User::get(conn, self.author_id).expect("Comment::to_activity: author error"); let author = User::get(conn, self.author_id)?;
let mut note = Note::default(); let mut note = Note::default();
let to = vec![Id::new(PUBLIC_VISIBILTY.to_string())]; let to = vec![Id::new(PUBLIC_VISIBILTY.to_string())];
note.object_props note.object_props
.set_id_string(self.ap_url.clone().unwrap_or_default()) .set_id_string(self.ap_url.clone().unwrap_or_default())?;
.expect("Comment::to_activity: id error");
note.object_props note.object_props
.set_summary_string(self.spoiler_text.clone()) .set_summary_string(self.spoiler_text.clone())?;
.expect("Comment::to_activity: summary error");
note.object_props note.object_props
.set_content_string(html) .set_content_string(html)?;
.expect("Comment::to_activity: content error");
note.object_props note.object_props
.set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else( .set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else(
|| { || Ok(Post::get(conn, self.post_id)?.ap_url),
Post::get(conn, self.post_id) |id| Ok(Comment::get(conn, id)?.compute_id(conn)?) as Result<String>,
.expect("Comment::to_activity: post error") )?))?;
.ap_url
},
|id| {
let comm =
Comment::get(conn, id).expect("Comment::to_activity: comment error");
comm.ap_url.clone().unwrap_or_else(|| comm.compute_id(conn))
},
)))
.expect("Comment::to_activity: in_reply_to error");
note.object_props note.object_props
.set_published_string(chrono::Utc::now().to_rfc3339()) .set_published_string(chrono::Utc::now().to_rfc3339())?;
.expect("Comment::to_activity: published error");
note.object_props note.object_props
.set_attributed_to_link(author.clone().into_id()) .set_attributed_to_link(author.clone().into_id())?;
.expect("Comment::to_activity: attributed_to error");
note.object_props note.object_props
.set_to_link_vec(to.clone()) .set_to_link_vec(to.clone())?;
.expect("Comment::to_activity: to error");
note.object_props note.object_props
.set_tag_link_vec( .set_tag_link_vec(
mentions mentions
.into_iter() .into_iter()
.map(|m| Mention::build_activity(conn, &m)) .filter_map(|m| Mention::build_activity(conn, &m).ok())
.collect::<Vec<link::Mention>>(), .collect::<Vec<link::Mention>>(),
) )?;
.expect("Comment::to_activity: tag error"); Ok(note)
note
} }
pub fn create_activity(&self, conn: &Connection) -> Create { pub fn create_activity(&self, conn: &Connection) -> Result<Create> {
let author = let author =
User::get(conn, self.author_id).expect("Comment::create_activity: author error"); User::get(conn, self.author_id)?;
let note = self.to_activity(conn); let note = self.to_activity(conn)?;
let mut act = Create::default(); let mut act = Create::default();
act.create_props act.create_props
.set_actor_link(author.into_id()) .set_actor_link(author.into_id())?;
.expect("Comment::create_activity: actor error");
act.create_props act.create_props
.set_object_object(note.clone()) .set_object_object(note.clone())?;
.expect("Comment::create_activity: object error");
act.object_props act.object_props
.set_id_string(format!( .set_id_string(format!(
"{}/activity", "{}/activity",
self.ap_url self.ap_url
.clone() .clone()?,
.expect("Comment::create_activity: ap_url error") ))?;
))
.expect("Comment::create_activity: id error");
act.object_props act.object_props
.set_to_link_vec( .set_to_link_vec(
note.object_props note.object_props
.to_link_vec::<Id>() .to_link_vec::<Id>()?,
.expect("Comment::create_activity: id error"), )?;
)
.expect("Comment::create_activity: to error");
act.object_props act.object_props
.set_cc_link_vec::<Id>(vec![]) .set_cc_link_vec::<Id>(vec![])?;
.expect("Comment::create_activity: cc error"); Ok(act)
act
} }
} }
impl FromActivity<Note, Connection> for Comment { impl FromActivity<Note, Connection> for Comment {
fn from_activity(conn: &Connection, note: Note, actor: Id) -> Comment { type Error = Error;
fn from_activity(conn: &Connection, note: Note, actor: Id) -> Result<Comment> {
let comm = { let comm = {
let previous_url = note let previous_url = note
.object_props .object_props
.in_reply_to .in_reply_to
.as_ref() .as_ref()?
.expect("Comment::from_activity: not an answer error") .as_str()?;
.as_str()
.expect("Comment::from_activity: in_reply_to parsing error");
let previous_comment = Comment::find_by_ap_url(conn, previous_url); let previous_comment = Comment::find_by_ap_url(conn, previous_url);
let is_public = |v: &Option<serde_json::Value>| match v.as_ref().unwrap_or(&serde_json::Value::Null) { let is_public = |v: &Option<serde_json::Value>| match v.as_ref().unwrap_or(&serde_json::Value::Null) {
@ -216,42 +191,35 @@ impl FromActivity<Note, Connection> for Comment {
content: SafeString::new( content: SafeString::new(
&note &note
.object_props .object_props
.content_string() .content_string()?
.expect("Comment::from_activity: content deserialization error"),
), ),
spoiler_text: note spoiler_text: note
.object_props .object_props
.summary_string() .summary_string()
.unwrap_or_default(), .unwrap_or_default(),
ap_url: note.object_props.id_string().ok(), ap_url: note.object_props.id_string().ok(),
in_response_to_id: previous_comment.clone().map(|c| c.id), in_response_to_id: previous_comment.iter().map(|c| c.id).next(),
post_id: previous_comment.map(|c| c.post_id).unwrap_or_else(|| { post_id: previous_comment.map(|c| c.post_id)
Post::find_by_ap_url(conn, previous_url) .or_else(|_| Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result<i32>)?,
.expect("Comment::from_activity: post error") author_id: User::from_url(conn, actor.as_ref())?.id,
.id
}),
author_id: User::from_url(conn, actor.as_ref())
.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 sensitive: false, // "sensitive" is not a standard property, we need to think about how to support it with the activitypub crate
public_visibility public_visibility
}, },
); )?;
// save mentions // save mentions
if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() { if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() {
for tag in tags { for tag in tags {
serde_json::from_value::<link::Mention>(tag) serde_json::from_value::<link::Mention>(tag)
.map(|m| { .map_err(Error::from)
let author = &Post::get(conn, comm.post_id) .and_then(|m| {
.expect("Comment::from_activity: error") let author = &Post::get(conn, comm.post_id)?
.get_authors(conn)[0]; .get_authors(conn)?[0];
let not_author = m let not_author = m
.link_props .link_props
.href_string() .href_string()?
.expect("Comment::from_activity: no href error")
!= author.ap_url.clone(); != author.ap_url.clone();
Mention::from_activity(conn, &m, comm.id, false, not_author) Ok(Mention::from_activity(conn, &m, comm.id, false, not_author)?)
}) })
.ok(); .ok();
} }
@ -279,13 +247,13 @@ impl FromActivity<Note, Connection> for Comment {
let receivers_ap_url = to.chain(cc).chain(bto).chain(bcc) let receivers_ap_url = to.chain(cc).chain(bto).chain(bcc)
.collect::<HashSet<_>>()//remove duplicates (don't do a query more than once) .collect::<HashSet<_>>()//remove duplicates (don't do a query more than once)
.into_iter() .into_iter()
.map(|v| if let Some(user) = User::from_url(conn,&v) { .map(|v| if let Ok(user) = User::from_url(conn,&v) {
vec![user] vec![user]
} else { } else {
vec![]// TODO try to fetch collection vec![]// TODO try to fetch collection
}) })
.flatten() .flatten()
.filter(|u| u.get_instance(conn).local) .filter(|u| u.get_instance(conn).map(|i| i.local).unwrap_or(false))
.collect::<HashSet<User>>();//remove duplicates (prevent db error) .collect::<HashSet<User>>();//remove duplicates (prevent db error)
for user in &receivers_ap_url { for user in &receivers_ap_url {
@ -295,18 +263,20 @@ impl FromActivity<Note, Connection> for Comment {
comment_id: comm.id, comment_id: comm.id,
user_id: user.id user_id: user.id
} }
); )?;
} }
} }
comm.notify(conn); comm.notify(conn)?;
comm Ok(comm)
} }
} }
impl Notify<Connection> for Comment { impl Notify<Connection> for Comment {
fn notify(&self, conn: &Connection) { type Error = Error;
for author in self.get_post(conn).get_authors(conn) {
fn notify(&self, conn: &Connection) -> Result<()> {
for author in self.get_post(conn)?.get_authors(conn)? {
Notification::insert( Notification::insert(
conn, conn,
NewNotification { NewNotification {
@ -314,8 +284,9 @@ impl Notify<Connection> for Comment {
object_id: self.id, object_id: self.id,
user_id: author.id, user_id: author.id,
}, },
); )?;
} }
Ok(())
} }
} }
@ -325,67 +296,64 @@ pub struct CommentTree {
} }
impl CommentTree { impl CommentTree {
pub fn from_post(conn: &Connection, p: &Post, user: Option<&User>) -> Vec<Self> { pub fn from_post(conn: &Connection, p: &Post, user: Option<&User>) -> Result<Vec<Self>> {
Comment::list_by_post(conn, p.id).into_iter() Ok(Comment::list_by_post(conn, p.id)?.into_iter()
.filter(|c| c.in_response_to_id.is_none()) .filter(|c| c.in_response_to_id.is_none())
.filter(|c| c.can_see(conn, user)) .filter(|c| c.can_see(conn, user))
.map(|c| Self::from_comment(conn, c, user)) .filter_map(|c| Self::from_comment(conn, c, user).ok())
.collect() .collect())
} }
pub fn from_comment(conn: &Connection, comment: Comment, user: Option<&User>) -> Self { pub fn from_comment(conn: &Connection, comment: Comment, user: Option<&User>) -> Result<Self> {
let responses = comment.get_responses(conn).into_iter() let responses = comment.get_responses(conn)?.into_iter()
.filter(|c| c.can_see(conn, user)) .filter(|c| c.can_see(conn, user))
.map(|c| Self::from_comment(conn, c, user)) .filter_map(|c| Self::from_comment(conn, c, user).ok())
.collect(); .collect();
CommentTree { Ok(CommentTree {
comment, comment,
responses, responses,
} })
} }
} }
impl<'a> Deletable<Connection, Delete> for Comment { impl<'a> Deletable<Connection, Delete> for Comment {
fn delete(&self, conn: &Connection) -> Delete { type Error = Error;
fn delete(&self, conn: &Connection) -> Result<Delete> {
let mut act = Delete::default(); let mut act = Delete::default();
act.delete_props act.delete_props
.set_actor_link(self.get_author(conn).into_id()) .set_actor_link(self.get_author(conn)?.into_id())?;
.expect("Comment::delete: actor error");
let mut tombstone = Tombstone::default(); let mut tombstone = Tombstone::default();
tombstone tombstone
.object_props .object_props
.set_id_string(self.ap_url.clone().expect("Comment::delete: no ap_url")) .set_id_string(self.ap_url.clone()?)?;
.expect("Comment::delete: object.id error");
act.delete_props act.delete_props
.set_object_object(tombstone) .set_object_object(tombstone)?;
.expect("Comment::delete: object error");
act.object_props act.object_props
.set_id_string(format!("{}#delete", self.ap_url.clone().unwrap())) .set_id_string(format!("{}#delete", self.ap_url.clone().unwrap()))?;
.expect("Comment::delete: id error");
act.object_props act.object_props
.set_to_link_vec(vec![Id::new(PUBLIC_VISIBILTY)]) .set_to_link_vec(vec![Id::new(PUBLIC_VISIBILTY)])?;
.expect("Comment::delete: to error");
for m in Mention::list_for_comment(&conn, self.id) { for m in Mention::list_for_comment(&conn, self.id)? {
m.delete(conn); m.delete(conn)?;
} }
diesel::update(comments::table).filter(comments::in_response_to_id.eq(self.id)) diesel::update(comments::table).filter(comments::in_response_to_id.eq(self.id))
.set(comments::in_response_to_id.eq(self.in_response_to_id)) .set(comments::in_response_to_id.eq(self.in_response_to_id))
.execute(conn) .execute(conn)?;
.expect("Comment::delete: DB error could not update other comments");
diesel::delete(self) diesel::delete(self)
.execute(conn) .execute(conn)?;
.expect("Comment::delete: DB error"); Ok(act)
act
} }
fn delete_id(id: &str, actor_id: &str, conn: &Connection) { fn delete_id(id: &str, actor_id: &str, conn: &Connection) -> Result<Delete> {
let actor = User::find_by_ap_url(conn, actor_id); let actor = User::find_by_ap_url(conn, actor_id)?;
let comment = Comment::find_by_ap_url(conn, id); let comment = Comment::find_by_ap_url(conn, id)?;
if let Some(comment) = comment.filter(|c| c.author_id == actor.unwrap().id) { if comment.author_id == actor.id {
comment.delete(conn); comment.delete(conn)
} else {
Err(Error::Unauthorized)
} }
} }
} }

View File

@ -1,4 +1,6 @@
use diesel::{dsl::sql_query, r2d2::{ConnectionManager, CustomizeConnection, Error as ConnError, Pool, PooledConnection}, ConnectionError, RunQueryDsl}; use diesel::{r2d2::{ConnectionManager, CustomizeConnection, Error as ConnError, Pool, PooledConnection}};
#[cfg(feature = "sqlite")]
use diesel::{dsl::sql_query, ConnectionError, RunQueryDsl};
use rocket::{ use rocket::{
http::Status, http::Status,
request::{self, FromRequest}, request::{self, FromRequest},

View File

@ -15,7 +15,7 @@ use plume_common::activity_pub::{
}; };
use schema::follows; use schema::follows;
use users::User; use users::User;
use {ap_url, Connection, BASE_URL}; use {ap_url, Connection, BASE_URL, Error, Result};
#[derive(Clone, Queryable, Identifiable, Associations)] #[derive(Clone, Queryable, Identifiable, Associations)]
#[belongs_to(User, foreign_key = "following_id")] #[belongs_to(User, foreign_key = "following_id")]
@ -39,37 +39,30 @@ impl Follow {
get!(follows); get!(follows);
find_by!(follows, find_by_ap_url, ap_url as &str); find_by!(follows, find_by_ap_url, ap_url as &str);
pub fn find(conn: &Connection, from: i32, to: i32) -> Option<Follow> { pub fn find(conn: &Connection, from: i32, to: i32) -> Result<Follow> {
follows::table follows::table
.filter(follows::follower_id.eq(from)) .filter(follows::follower_id.eq(from))
.filter(follows::following_id.eq(to)) .filter(follows::following_id.eq(to))
.get_result(conn) .get_result(conn)
.ok() .map_err(Error::from)
} }
pub fn to_activity(&self, conn: &Connection) -> FollowAct { pub fn to_activity(&self, conn: &Connection) -> Result<FollowAct> {
let user = User::get(conn, self.follower_id) let user = User::get(conn, self.follower_id)?;
.expect("Follow::to_activity: actor not found error"); let target = User::get(conn, self.following_id)?;
let target = User::get(conn, self.following_id)
.expect("Follow::to_activity: target not found error");
let mut act = FollowAct::default(); let mut act = FollowAct::default();
act.follow_props act.follow_props
.set_actor_link::<Id>(user.clone().into_id()) .set_actor_link::<Id>(user.clone().into_id())?;
.expect("Follow::to_activity: actor error");
act.follow_props act.follow_props
.set_object_link::<Id>(target.clone().into_id()) .set_object_link::<Id>(target.clone().into_id())?;
.expect("Follow::to_activity: object error");
act.object_props act.object_props
.set_id_string(self.ap_url.clone()) .set_id_string(self.ap_url.clone())?;
.expect("Follow::to_activity: id error");
act.object_props act.object_props
.set_to_link(target.into_id()) .set_to_link(target.into_id())?;
.expect("Follow::to_activity: target error");
act.object_props act.object_props
.set_cc_link_vec::<Id>(vec![]) .set_cc_link_vec::<Id>(vec![])?;
.expect("Follow::to_activity: cc error"); Ok(act)
act
} }
/// from -> The one sending the follow request /// from -> The one sending the follow request
@ -81,78 +74,69 @@ impl Follow {
follow: FollowAct, follow: FollowAct,
from_id: i32, from_id: i32,
target_id: i32, target_id: i32,
) -> Follow { ) -> Result<Follow> {
let res = Follow::insert( let res = Follow::insert(
conn, conn,
NewFollow { NewFollow {
follower_id: from_id, follower_id: from_id,
following_id: target_id, following_id: target_id,
ap_url: follow.object_props.id_string().expect("Follow::accept_follow: get id error"), ap_url: follow.object_props.id_string()?,
}, },
); )?;
let mut accept = Accept::default(); let mut accept = Accept::default();
let accept_id = ap_url(&format!("{}/follow/{}/accept", BASE_URL.as_str(), &res.id)); let accept_id = ap_url(&format!("{}/follow/{}/accept", BASE_URL.as_str(), &res.id));
accept accept
.object_props .object_props
.set_id_string(accept_id) .set_id_string(accept_id)?;
.expect("Follow::accept_follow: set id error");
accept accept
.object_props .object_props
.set_to_link(from.clone().into_id()) .set_to_link(from.clone().into_id())?;
.expect("Follow::accept_follow: to error");
accept accept
.object_props .object_props
.set_cc_link_vec::<Id>(vec![]) .set_cc_link_vec::<Id>(vec![])?;
.expect("Follow::accept_follow: cc error");
accept accept
.accept_props .accept_props
.set_actor_link::<Id>(target.clone().into_id()) .set_actor_link::<Id>(target.clone().into_id())?;
.expect("Follow::accept_follow: actor error");
accept accept
.accept_props .accept_props
.set_object_object(follow) .set_object_object(follow)?;
.expect("Follow::accept_follow: object error");
broadcast(&*target, accept, vec![from.clone()]); broadcast(&*target, accept, vec![from.clone()]);
res Ok(res)
} }
} }
impl FromActivity<FollowAct, Connection> for Follow { impl FromActivity<FollowAct, Connection> for Follow {
fn from_activity(conn: &Connection, follow: FollowAct, _actor: Id) -> Follow { type Error = Error;
fn from_activity(conn: &Connection, follow: FollowAct, _actor: Id) -> Result<Follow> {
let from_id = follow let from_id = follow
.follow_props .follow_props
.actor_link::<Id>() .actor_link::<Id>()
.map(|l| l.into()) .map(|l| l.into())
.unwrap_or_else(|_| { .or_else(|_| Ok(follow
follow .follow_props
.follow_props .actor_object::<Person>()?
.actor_object::<Person>() .object_props
.expect("Follow::from_activity: actor not found error") .id_string()?) as Result<String>)?;
.object_props
.id_string()
.expect("Follow::from_activity: actor not found error")
});
let from = let from =
User::from_url(conn, &from_id).expect("Follow::from_activity: actor not found error"); User::from_url(conn, &from_id)?;
match User::from_url( match User::from_url(
conn, conn,
follow follow
.follow_props .follow_props
.object .object
.as_str() .as_str()?,
.expect("Follow::from_activity: target url parsing error"),
) { ) {
Some(user) => Follow::accept_follow(conn, &from, &user, follow, from.id, user.id), Ok(user) => Follow::accept_follow(conn, &from, &user, follow, from.id, user.id),
None => { Err(_) => {
let blog = Blog::from_url( let blog = Blog::from_url(
conn, conn,
follow follow
.follow_props .follow_props
.object .object
.as_str() .as_str()?,
.expect("Follow::from_activity: target url parsing error"), )?;
).expect("Follow::from_activity: target not found error");
Follow::accept_follow(conn, &from, &blog, follow, from.id, blog.id) Follow::accept_follow(conn, &from, &blog, follow, from.id, blog.id)
} }
} }
@ -160,7 +144,9 @@ impl FromActivity<FollowAct, Connection> for Follow {
} }
impl Notify<Connection> for Follow { impl Notify<Connection> for Follow {
fn notify(&self, conn: &Connection) { type Error = Error;
fn notify(&self, conn: &Connection) -> Result<()> {
Notification::insert( Notification::insert(
conn, conn,
NewNotification { NewNotification {
@ -168,47 +154,43 @@ impl Notify<Connection> for Follow {
object_id: self.id, object_id: self.id,
user_id: self.following_id, user_id: self.following_id,
}, },
); ).map(|_| ())
} }
} }
impl Deletable<Connection, Undo> for Follow { impl Deletable<Connection, Undo> for Follow {
fn delete(&self, conn: &Connection) -> Undo { type Error = Error;
fn delete(&self, conn: &Connection) -> Result<Undo> {
diesel::delete(self) diesel::delete(self)
.execute(conn) .execute(conn)?;
.expect("Follow::delete: follow deletion error");
// delete associated notification if any // delete associated notification if any
if let Some(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) { if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
diesel::delete(&notif) diesel::delete(&notif)
.execute(conn) .execute(conn)?;
.expect("Follow::delete: notification deletion error");
} }
let mut undo = Undo::default(); let mut undo = Undo::default();
undo.undo_props undo.undo_props
.set_actor_link( .set_actor_link(
User::get(conn, self.follower_id) User::get(conn, self.follower_id)?
.expect("Follow::delete: actor error")
.into_id(), .into_id(),
) )?;
.expect("Follow::delete: actor error");
undo.object_props undo.object_props
.set_id_string(format!("{}/undo", self.ap_url)) .set_id_string(format!("{}/undo", self.ap_url))?;
.expect("Follow::delete: id error");
undo.undo_props undo.undo_props
.set_object_link::<Id>(self.clone().into_id()) .set_object_link::<Id>(self.clone().into_id())?;
.expect("Follow::delete: object error"); Ok(undo)
undo
} }
fn delete_id(id: &str, actor_id: &str, conn: &Connection) { fn delete_id(id: &str, actor_id: &str, conn: &Connection) -> Result<Undo> {
if let Some(follow) = Follow::find_by_ap_url(conn, id) { let follow = Follow::find_by_ap_url(conn, id)?;
if let Some(user) = User::find_by_ap_url(conn, actor_id) { let user = User::find_by_ap_url(conn, actor_id)?;
if user.id == follow.follower_id { if user.id == follow.follower_id {
follow.delete(conn); follow.delete(conn)
} } else {
} Err(Error::Unauthorized)
} }
} }
} }

View File

@ -7,7 +7,7 @@ use plume_common::utils::md_to_html;
use safe_string::SafeString; use safe_string::SafeString;
use schema::{instances, users}; use schema::{instances, users};
use users::User; use users::User;
use Connection; use {Connection, Error, Result};
#[derive(Clone, Identifiable, Queryable, Serialize)] #[derive(Clone, Identifiable, Queryable, Serialize)]
pub struct Instance { pub struct Instance {
@ -40,80 +40,73 @@ pub struct NewInstance {
} }
impl Instance { impl Instance {
pub fn get_local(conn: &Connection) -> Option<Instance> { pub fn get_local(conn: &Connection) -> Result<Instance> {
instances::table instances::table
.filter(instances::local.eq(true)) .filter(instances::local.eq(true))
.limit(1) .limit(1)
.load::<Instance>(conn) .load::<Instance>(conn)?
.expect("Instance::get_local: loading error")
.into_iter() .into_iter()
.nth(0) .nth(0).ok_or(Error::NotFound)
} }
pub fn get_remotes(conn: &Connection) -> Vec<Instance> { pub fn get_remotes(conn: &Connection) -> Result<Vec<Instance>> {
instances::table instances::table
.filter(instances::local.eq(false)) .filter(instances::local.eq(false))
.load::<Instance>(conn) .load::<Instance>(conn)
.expect("Instance::get_remotes: loading error") .map_err(Error::from)
} }
pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Vec<Instance> { pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<Instance>> {
instances::table instances::table
.order(instances::public_domain.asc()) .order(instances::public_domain.asc())
.offset(min.into()) .offset(min.into())
.limit((max - min).into()) .limit((max - min).into())
.load::<Instance>(conn) .load::<Instance>(conn)
.expect("Instance::page: loading error") .map_err(Error::from)
}
pub fn local_id(conn: &Connection) -> i32 {
Instance::get_local(conn)
.expect("Instance::local_id: local instance not found error")
.id
} }
insert!(instances, NewInstance); insert!(instances, NewInstance);
get!(instances); get!(instances);
find_by!(instances, find_by_domain, public_domain as &str); find_by!(instances, find_by_domain, public_domain as &str);
pub fn toggle_block(&self, conn: &Connection) { pub fn toggle_block(&self, conn: &Connection) -> Result<()> {
diesel::update(self) diesel::update(self)
.set(instances::blocked.eq(!self.blocked)) .set(instances::blocked.eq(!self.blocked))
.execute(conn) .execute(conn)
.expect("Instance::toggle_block: update error"); .map(|_| ())
.map_err(Error::from)
} }
/// id: AP object id /// id: AP object id
pub fn is_blocked(conn: &Connection, id: &str) -> bool { pub fn is_blocked(conn: &Connection, id: &str) -> Result<bool> {
for block in instances::table for block in instances::table
.filter(instances::blocked.eq(true)) .filter(instances::blocked.eq(true))
.get_results::<Instance>(conn) .get_results::<Instance>(conn)?
.expect("Instance::is_blocked: loading error")
{ {
if id.starts_with(&format!("https://{}/", block.public_domain)) { if id.starts_with(&format!("https://{}/", block.public_domain)) {
return true; return Ok(true);
} }
} }
false Ok(false)
} }
pub fn has_admin(&self, conn: &Connection) -> bool { pub fn has_admin(&self, conn: &Connection) -> Result<bool> {
!users::table users::table
.filter(users::instance_id.eq(self.id)) .filter(users::instance_id.eq(self.id))
.filter(users::is_admin.eq(true)) .filter(users::is_admin.eq(true))
.load::<User>(conn) .load::<User>(conn)
.expect("Instance::has_admin: loading error") .map_err(Error::from)
.is_empty() .map(|r| !r.is_empty())
} }
pub fn main_admin(&self, conn: &Connection) -> User { pub fn main_admin(&self, conn: &Connection) -> Result<User> {
users::table users::table
.filter(users::instance_id.eq(self.id)) .filter(users::instance_id.eq(self.id))
.filter(users::is_admin.eq(true)) .filter(users::is_admin.eq(true))
.limit(1) .limit(1)
.get_result::<User>(conn) .get_result::<User>(conn)
.expect("Instance::main_admin: loading error") .map_err(Error::from)
} }
pub fn compute_box( pub fn compute_box(
@ -138,7 +131,7 @@ impl Instance {
open_registrations: bool, open_registrations: bool,
short_description: SafeString, short_description: SafeString,
long_description: SafeString, long_description: SafeString,
) { ) -> Result<()> {
let (sd, _, _) = md_to_html(short_description.as_ref(), &self.public_domain); let (sd, _, _) = md_to_html(short_description.as_ref(), &self.public_domain);
let (ld, _, _) = md_to_html(long_description.as_ref(), &self.public_domain); let (ld, _, _) = md_to_html(long_description.as_ref(), &self.public_domain);
diesel::update(self) diesel::update(self)
@ -151,14 +144,15 @@ impl Instance {
instances::long_description_html.eq(ld), instances::long_description_html.eq(ld),
)) ))
.execute(conn) .execute(conn)
.expect("Instance::update: update error"); .map(|_| ())
.map_err(Error::from)
} }
pub fn count(conn: &Connection) -> i64 { pub fn count(conn: &Connection) -> Result<i64> {
instances::table instances::table
.count() .count()
.get_result(conn) .get_result(conn)
.expect("Instance::count: counting error") .map_err(Error::from)
} }
} }
@ -220,7 +214,7 @@ pub(crate) mod tests {
( (
inst.clone(), inst.clone(),
Instance::find_by_domain(conn, &inst.public_domain) Instance::find_by_domain(conn, &inst.public_domain)
.unwrap_or_else(|| Instance::insert(conn, inst)), .unwrap_or_else(|_| Instance::insert(conn, inst).unwrap()),
) )
}) })
.collect() .collect()
@ -253,7 +247,6 @@ pub(crate) mod tests {
assert_eq!(res.long_description_html.get(), &inserted.long_description_html); assert_eq!(res.long_description_html.get(), &inserted.long_description_html);
assert_eq!(res.short_description_html.get(), &inserted.short_description_html); assert_eq!(res.short_description_html.get(), &inserted.short_description_html);
assert_eq!(Instance::local_id(conn), res.id);
Ok(()) Ok(())
}); });
} }
@ -263,9 +256,9 @@ pub(crate) mod tests {
let conn = &db(); let conn = &db();
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let inserted = fill_database(conn); let inserted = fill_database(conn);
assert_eq!(Instance::count(conn), inserted.len() as i64); assert_eq!(Instance::count(conn).unwrap(), inserted.len() as i64);
let res = Instance::get_remotes(conn); let res = Instance::get_remotes(conn).unwrap();
assert_eq!( assert_eq!(
res.len(), res.len(),
inserted.iter().filter(|(inst, _)| !inst.local).count() inserted.iter().filter(|(inst, _)| !inst.local).count()
@ -293,15 +286,15 @@ pub(crate) mod tests {
assert_eq!(&newinst.short_description_html, inst.short_description_html.get()); assert_eq!(&newinst.short_description_html, inst.short_description_html.get());
}); });
let page = Instance::page(conn, (0, 2)); let page = Instance::page(conn, (0, 2)).unwrap();
assert_eq!(page.len(), 2); assert_eq!(page.len(), 2);
let page1 = &page[0]; let page1 = &page[0];
let page2 = &page[1]; let page2 = &page[1];
assert!(page1.public_domain <= page2.public_domain); assert!(page1.public_domain <= page2.public_domain);
let mut last_domaine: String = Instance::page(conn, (0, 1))[0].public_domain.clone(); let mut last_domaine: String = Instance::page(conn, (0, 1)).unwrap()[0].public_domain.clone();
for i in 1..inserted.len() as i32 { for i in 1..inserted.len() as i32 {
let page = Instance::page(conn, (i, i + 1)); let page = Instance::page(conn, (i, i + 1)).unwrap();
assert_eq!(page.len(), 1); assert_eq!(page.len(), 1);
assert!(last_domaine <= page[0].public_domain); assert!(last_domaine <= page[0].public_domain);
last_domaine = page[0].public_domain.clone(); last_domaine = page[0].public_domain.clone();
@ -320,7 +313,7 @@ pub(crate) mod tests {
let inst_list = &inst_list[1..]; let inst_list = &inst_list[1..];
let blocked = inst.blocked; let blocked = inst.blocked;
inst.toggle_block(conn); inst.toggle_block(conn).unwrap();
let inst = Instance::get(conn, inst.id).unwrap(); let inst = Instance::get(conn, inst.id).unwrap();
assert_eq!(inst.blocked, !blocked); assert_eq!(inst.blocked, !blocked);
assert_eq!( assert_eq!(
@ -333,25 +326,25 @@ pub(crate) mod tests {
0 0
); );
assert_eq!( assert_eq!(
Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain)), Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain)).unwrap(),
inst.blocked inst.blocked
); );
assert_eq!( assert_eq!(
Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain)), Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain)).unwrap(),
Instance::find_by_domain(conn, &format!("{}a", inst.public_domain)) Instance::find_by_domain(conn, &format!("{}a", inst.public_domain))
.map(|inst| inst.blocked) .map(|inst| inst.blocked)
.unwrap_or(false) .unwrap_or(false)
); );
inst.toggle_block(conn); inst.toggle_block(conn).unwrap();
let inst = Instance::get(conn, inst.id).unwrap(); let inst = Instance::get(conn, inst.id).unwrap();
assert_eq!(inst.blocked, blocked); assert_eq!(inst.blocked, blocked);
assert_eq!( assert_eq!(
Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain)), Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain)).unwrap(),
inst.blocked inst.blocked
); );
assert_eq!( assert_eq!(
Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain)), Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain)).unwrap(),
Instance::find_by_domain(conn, &format!("{}a", inst.public_domain)) Instance::find_by_domain(conn, &format!("{}a", inst.public_domain))
.map(|inst| inst.blocked) .map(|inst| inst.blocked)
.unwrap_or(false) .unwrap_or(false)
@ -382,7 +375,7 @@ pub(crate) mod tests {
false, false,
SafeString::new("[short](#link)"), SafeString::new("[short](#link)"),
SafeString::new("[long_description](/with_link)"), SafeString::new("[long_description](/with_link)"),
); ).unwrap();
let inst = Instance::get(conn, inst.id).unwrap(); let inst = Instance::get(conn, inst.id).unwrap();
assert_eq!(inst.name, "NewName".to_owned()); assert_eq!(inst.name, "NewName".to_owned());
assert_eq!(inst.open_registrations, false); assert_eq!(inst.open_registrations, false);

View File

@ -1,4 +1,5 @@
#![allow(proc_macro_derive_resolution_fallback)] // This can be removed after diesel-1.4 #![allow(proc_macro_derive_resolution_fallback)] // This can be removed after diesel-1.4
#![feature(try_trait)]
extern crate activitypub; extern crate activitypub;
extern crate ammonia; extern crate ammonia;
@ -47,6 +48,102 @@ pub type Connection = diesel::SqliteConnection;
#[cfg(all(not(feature = "sqlite"), feature = "postgres"))] #[cfg(all(not(feature = "sqlite"), feature = "postgres"))]
pub type Connection = diesel::PgConnection; pub type Connection = diesel::PgConnection;
/// All the possible errors that can be encoutered in this crate
#[derive(Debug)]
pub enum Error {
Db(diesel::result::Error),
InvalidValue,
Io(std::io::Error),
MissingApProperty,
NotFound,
Request,
SerDe,
Search(search::SearcherError),
Signature,
Unauthorized,
Url,
Webfinger,
}
impl From<bcrypt::BcryptError> for Error {
fn from(_: bcrypt::BcryptError) -> Self {
Error::Signature
}
}
impl From<openssl::error::ErrorStack> for Error {
fn from(_: openssl::error::ErrorStack) -> Self {
Error::Signature
}
}
impl From<diesel::result::Error> for Error {
fn from(err: diesel::result::Error) -> Self {
Error::Db(err)
}
}
impl From<std::option::NoneError> for Error {
fn from(_: std::option::NoneError) -> Self {
Error::NotFound
}
}
impl From<url::ParseError> for Error {
fn from(_: url::ParseError) -> Self {
Error::Url
}
}
impl From<serde_json::Error> for Error {
fn from(_: serde_json::Error) -> Self {
Error::SerDe
}
}
impl From<reqwest::Error> for Error {
fn from(_: reqwest::Error) -> Self {
Error::Request
}
}
impl From<reqwest::header::InvalidHeaderValue> for Error {
fn from(_: reqwest::header::InvalidHeaderValue) -> Self {
Error::Request
}
}
impl From<activitypub::Error> for Error {
fn from(err: activitypub::Error) -> Self {
match err {
activitypub::Error::NotFound => Error::MissingApProperty,
_ => Error::SerDe,
}
}
}
impl From<webfinger::WebfingerError> for Error {
fn from(_: webfinger::WebfingerError) -> Self {
Error::Webfinger
}
}
impl From<search::SearcherError> for Error {
fn from(err: search::SearcherError) -> Self {
Error::Search(err)
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Error::Io(err)
}
}
pub type Result<T> = std::result::Result<T, Error>;
pub type ApiResult<T> = std::result::Result<T, canapi::Error>;
/// Adds a function to a model, that returns the first /// Adds a function to a model, that returns the first
/// matching row for a given list of fields. /// matching row for a given list of fields.
/// ///
@ -63,13 +160,14 @@ pub type Connection = diesel::PgConnection;
macro_rules! find_by { macro_rules! find_by {
($table:ident, $fn:ident, $($col:ident as $type:ty),+) => { ($table:ident, $fn:ident, $($col:ident as $type:ty),+) => {
/// Try to find a $table with a given $col /// Try to find a $table with a given $col
pub fn $fn(conn: &crate::Connection, $($col: $type),+) -> Option<Self> { pub fn $fn(conn: &crate::Connection, $($col: $type),+) -> Result<Self> {
$table::table $table::table
$(.filter($table::$col.eq($col)))+ $(.filter($table::$col.eq($col)))+
.limit(1) .limit(1)
.load::<Self>(conn) .load::<Self>(conn)?
.expect("macro::find_by: Error loading $table by $col") .into_iter()
.into_iter().nth(0) .next()
.ok_or(Error::NotFound)
} }
}; };
} }
@ -89,11 +187,11 @@ macro_rules! find_by {
macro_rules! list_by { macro_rules! list_by {
($table:ident, $fn:ident, $($col:ident as $type:ty),+) => { ($table:ident, $fn:ident, $($col:ident as $type:ty),+) => {
/// Try to find a $table with a given $col /// Try to find a $table with a given $col
pub fn $fn(conn: &crate::Connection, $($col: $type),+) -> Vec<Self> { pub fn $fn(conn: &crate::Connection, $($col: $type),+) -> Result<Vec<Self>> {
$table::table $table::table
$(.filter($table::$col.eq($col)))+ $(.filter($table::$col.eq($col)))+
.load::<Self>(conn) .load::<Self>(conn)
.expect("macro::list_by: Error loading $table by $col") .map_err(Error::from)
} }
}; };
} }
@ -112,14 +210,14 @@ macro_rules! list_by {
/// ``` /// ```
macro_rules! get { macro_rules! get {
($table:ident) => { ($table:ident) => {
pub fn get(conn: &crate::Connection, id: i32) -> Option<Self> { pub fn get(conn: &crate::Connection, id: i32) -> Result<Self> {
$table::table $table::table
.filter($table::id.eq(id)) .filter($table::id.eq(id))
.limit(1) .limit(1)
.load::<Self>(conn) .load::<Self>(conn)?
.expect("macro::get: Error loading $table by id")
.into_iter() .into_iter()
.nth(0) .next()
.ok_or(Error::NotFound)
} }
}; };
} }
@ -140,11 +238,10 @@ macro_rules! insert {
($table:ident, $from:ident) => { ($table:ident, $from:ident) => {
last!($table); last!($table);
pub fn insert(conn: &crate::Connection, new: $from) -> Self { pub fn insert(conn: &crate::Connection, new: $from) -> Result<Self> {
diesel::insert_into($table::table) diesel::insert_into($table::table)
.values(new) .values(new)
.execute(conn) .execute(conn)?;
.expect("macro::insert: Error saving new $table");
Self::last(conn) Self::last(conn)
} }
}; };
@ -164,19 +261,14 @@ macro_rules! insert {
/// ``` /// ```
macro_rules! last { macro_rules! last {
($table:ident) => { ($table:ident) => {
pub fn last(conn: &crate::Connection) -> Self { pub fn last(conn: &crate::Connection) -> Result<Self> {
$table::table $table::table
.order_by($table::id.desc()) .order_by($table::id.desc())
.limit(1) .limit(1)
.load::<Self>(conn) .load::<Self>(conn)?
.expect(concat!( .into_iter()
"macro::last: Error getting last ",
stringify!($table)
))
.iter()
.next() .next()
.expect(concat!("macro::last: No last ", stringify!($table))) .ok_or(Error::NotFound)
.clone()
} }
}; };
} }

View File

@ -10,7 +10,7 @@ use plume_common::activity_pub::{
use posts::Post; use posts::Post;
use schema::likes; use schema::likes;
use users::User; use users::User;
use Connection; use {Connection, Error, Result};
#[derive(Clone, Queryable, Identifiable)] #[derive(Clone, Queryable, Identifiable)]
pub struct Like { pub struct Like {
@ -35,69 +35,64 @@ impl Like {
find_by!(likes, find_by_ap_url, ap_url as &str); find_by!(likes, find_by_ap_url, ap_url as &str);
find_by!(likes, find_by_user_on_post, user_id as i32, post_id as i32); find_by!(likes, find_by_user_on_post, user_id as i32, post_id as i32);
pub fn to_activity(&self, conn: &Connection) -> activity::Like { pub fn to_activity(&self, conn: &Connection) -> Result<activity::Like> {
let mut act = activity::Like::default(); let mut act = activity::Like::default();
act.like_props act.like_props
.set_actor_link( .set_actor_link(
User::get(conn, self.user_id) User::get(conn, self.user_id)?
.expect("Like::to_activity: user error")
.into_id(), .into_id(),
) )?;
.expect("Like::to_activity: actor error");
act.like_props act.like_props
.set_object_link( .set_object_link(
Post::get(conn, self.post_id) Post::get(conn, self.post_id)?
.expect("Like::to_activity: post error")
.into_id(), .into_id(),
) )?;
.expect("Like::to_activity: object error");
act.object_props act.object_props
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())) .set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
.expect("Like::to_activity: to error");
act.object_props act.object_props
.set_cc_link_vec::<Id>(vec![]) .set_cc_link_vec::<Id>(vec![])?;
.expect("Like::to_activity: cc error");
act.object_props act.object_props
.set_id_string(self.ap_url.clone()) .set_id_string(self.ap_url.clone())?;
.expect("Like::to_activity: id error");
act Ok(act)
} }
} }
impl FromActivity<activity::Like, Connection> for Like { impl FromActivity<activity::Like, Connection> for Like {
fn from_activity(conn: &Connection, like: activity::Like, _actor: Id) -> Like { type Error = Error;
fn from_activity(conn: &Connection, like: activity::Like, _actor: Id) -> Result<Like> {
let liker = User::from_url( let liker = User::from_url(
conn, conn,
like.like_props like.like_props
.actor .actor
.as_str() .as_str()?,
.expect("Like::from_activity: actor error"), )?;
);
let post = Post::find_by_ap_url( let post = Post::find_by_ap_url(
conn, conn,
like.like_props like.like_props
.object .object
.as_str() .as_str()?,
.expect("Like::from_activity: object error"), )?;
);
let res = Like::insert( let res = Like::insert(
conn, conn,
NewLike { NewLike {
post_id: post.expect("Like::from_activity: post error").id, post_id: post.id,
user_id: liker.expect("Like::from_activity: user error").id, user_id: liker.id,
ap_url: like.object_props.id_string().unwrap_or_default(), ap_url: like.object_props.id_string()?,
}, },
); )?;
res.notify(conn); res.notify(conn)?;
res Ok(res)
} }
} }
impl Notify<Connection> for Like { impl Notify<Connection> for Like {
fn notify(&self, conn: &Connection) { type Error = Error;
let post = Post::get(conn, self.post_id).expect("Like::notify: post error");
for author in post.get_authors(conn) { fn notify(&self, conn: &Connection) -> Result<()> {
let post = Post::get(conn, self.post_id)?;
for author in post.get_authors(conn)? {
Notification::insert( Notification::insert(
conn, conn,
NewNotification { NewNotification {
@ -105,55 +100,47 @@ impl Notify<Connection> for Like {
object_id: self.id, object_id: self.id,
user_id: author.id, user_id: author.id,
}, },
); )?;
} }
Ok(())
} }
} }
impl Deletable<Connection, activity::Undo> for Like { impl Deletable<Connection, activity::Undo> for Like {
fn delete(&self, conn: &Connection) -> activity::Undo { type Error = Error;
fn delete(&self, conn: &Connection) -> Result<activity::Undo> {
diesel::delete(self) diesel::delete(self)
.execute(conn) .execute(conn)?;
.expect("Like::delete: delete error");
// delete associated notification if any // delete associated notification if any
if let Some(notif) = Notification::find(conn, notification_kind::LIKE, self.id) { if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) {
diesel::delete(&notif) diesel::delete(&notif)
.execute(conn) .execute(conn)?;
.expect("Like::delete: notification error");
} }
let mut act = activity::Undo::default(); let mut act = activity::Undo::default();
act.undo_props act.undo_props
.set_actor_link( .set_actor_link(User::get(conn, self.user_id)?.into_id(),)?;
User::get(conn, self.user_id)
.expect("Like::delete: user error")
.into_id(),
)
.expect("Like::delete: actor error");
act.undo_props act.undo_props
.set_object_object(self.to_activity(conn)) .set_object_object(self.to_activity(conn)?)?;
.expect("Like::delete: object error");
act.object_props act.object_props
.set_id_string(format!("{}#delete", self.ap_url)) .set_id_string(format!("{}#delete", self.ap_url))?;
.expect("Like::delete: id error");
act.object_props act.object_props
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())) .set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
.expect("Like::delete: to error");
act.object_props act.object_props
.set_cc_link_vec::<Id>(vec![]) .set_cc_link_vec::<Id>(vec![])?;
.expect("Like::delete: cc error");
act Ok(act)
} }
fn delete_id(id: &str, actor_id: &str, conn: &Connection) { fn delete_id(id: &str, actor_id: &str, conn: &Connection) -> Result<activity::Undo> {
if let Some(like) = Like::find_by_ap_url(conn, id) { let like = Like::find_by_ap_url(conn, id)?;
if let Some(user) = User::find_by_ap_url(conn, actor_id) { let user = User::find_by_ap_url(conn, actor_id)?;
if user.id == like.user_id { if user.id == like.user_id {
like.delete(conn); like.delete(conn)
} } else {
} Err(Error::Unauthorized)
} }
} }
} }

View File

@ -11,7 +11,7 @@ use instance::Instance;
use safe_string::SafeString; use safe_string::SafeString;
use schema::medias; use schema::medias;
use users::User; use users::User;
use {ap_url, Connection}; use {ap_url, Connection, Error, Result};
#[derive(Clone, Identifiable, Queryable, Serialize)] #[derive(Clone, Identifiable, Queryable, Serialize)]
pub struct Media { pub struct Media {
@ -50,10 +50,10 @@ impl Media {
get!(medias); get!(medias);
list_by!(medias, for_user, owner_id as i32); list_by!(medias, for_user, owner_id as i32);
pub fn list_all_medias(conn: &Connection) -> Vec<Media> { pub fn list_all_medias(conn: &Connection) -> Result<Vec<Media>> {
medias::table medias::table
.load::<Media>(conn) .load::<Media>(conn)
.expect("Media::list_all_medias: loading error") .map_err(Error::from)
} }
pub fn category(&self) -> MediaCategory { pub fn category(&self) -> MediaCategory {
@ -70,9 +70,9 @@ impl Media {
} }
} }
pub fn preview_html(&self, conn: &Connection) -> SafeString { pub fn preview_html(&self, conn: &Connection) -> Result<SafeString> {
let url = self.url(conn); let url = self.url(conn)?;
match self.category() { Ok(match self.category() {
MediaCategory::Image => SafeString::new(&format!( MediaCategory::Image => SafeString::new(&format!(
r#"<img src="{}" alt="{}" title="{}" class=\"preview\">"#, r#"<img src="{}" alt="{}" title="{}" class=\"preview\">"#,
url, escape(&self.alt_text), escape(&self.alt_text) url, escape(&self.alt_text), escape(&self.alt_text)
@ -86,12 +86,12 @@ impl Media {
url, escape(&self.alt_text) url, escape(&self.alt_text)
)), )),
MediaCategory::Unknown => SafeString::new(""), MediaCategory::Unknown => SafeString::new(""),
} })
} }
pub fn html(&self, conn: &Connection) -> SafeString { pub fn html(&self, conn: &Connection) -> Result<SafeString> {
let url = self.url(conn); let url = self.url(conn)?;
match self.category() { Ok(match self.category() {
MediaCategory::Image => SafeString::new(&format!( MediaCategory::Image => SafeString::new(&format!(
r#"<img src="{}" alt="{}" title="{}">"#, r#"<img src="{}" alt="{}" title="{}">"#,
url, escape(&self.alt_text), escape(&self.alt_text) url, escape(&self.alt_text), escape(&self.alt_text)
@ -105,46 +105,45 @@ impl Media {
url, escape(&self.alt_text) url, escape(&self.alt_text)
)), )),
MediaCategory::Unknown => SafeString::new(""), MediaCategory::Unknown => SafeString::new(""),
} })
} }
pub fn markdown(&self, conn: &Connection) -> SafeString { pub fn markdown(&self, conn: &Connection) -> Result<SafeString> {
let url = self.url(conn); let url = self.url(conn)?;
match self.category() { Ok(match self.category() {
MediaCategory::Image => SafeString::new(&format!("![{}]({})", escape(&self.alt_text), url)), MediaCategory::Image => SafeString::new(&format!("![{}]({})", escape(&self.alt_text), url)),
MediaCategory::Audio | MediaCategory::Video => self.html(conn), MediaCategory::Audio | MediaCategory::Video => self.html(conn)?,
MediaCategory::Unknown => SafeString::new(""), MediaCategory::Unknown => SafeString::new(""),
} })
} }
pub fn url(&self, conn: &Connection) -> String { pub fn url(&self, conn: &Connection) -> Result<String> {
if self.is_remote { if self.is_remote {
self.remote_url.clone().unwrap_or_default() Ok(self.remote_url.clone().unwrap_or_default())
} else { } else {
ap_url(&format!( Ok(ap_url(&format!(
"{}/{}", "{}/{}",
Instance::get_local(conn) Instance::get_local(conn)?.public_domain,
.expect("Media::url: local instance not found error")
.public_domain,
self.file_path self.file_path
)) )))
} }
} }
pub fn delete(&self, conn: &Connection) { pub fn delete(&self, conn: &Connection) -> Result<()> {
if !self.is_remote { if !self.is_remote {
fs::remove_file(self.file_path.as_str()).expect("Media::delete: file deletion error"); fs::remove_file(self.file_path.as_str())?;
} }
diesel::delete(self) diesel::delete(self)
.execute(conn) .execute(conn)
.expect("Media::delete: database entry deletion error"); .map(|_| ())
.map_err(Error::from)
} }
pub fn save_remote(conn: &Connection, url: String, user: &User) -> Result<Media, ()> { pub fn save_remote(conn: &Connection, url: String, user: &User) -> Result<Media> {
if url.contains(&['<', '>', '"'][..]) { if url.contains(&['<', '>', '"'][..]) {
Err(()) Err(Error::Url)
} else { } else {
Ok(Media::insert( Media::insert(
conn, conn,
NewMedia { NewMedia {
file_path: String::new(), file_path: String::new(),
@ -155,19 +154,20 @@ impl Media {
content_warning: None, content_warning: None,
owner_id: user.id, owner_id: user.id,
}, },
)) )
} }
} }
pub fn set_owner(&self, conn: &Connection, user: &User) { pub fn set_owner(&self, conn: &Connection, user: &User) -> Result<()> {
diesel::update(self) diesel::update(self)
.set(medias::owner_id.eq(user.id)) .set(medias::owner_id.eq(user.id))
.execute(conn) .execute(conn)
.expect("Media::set_owner: owner update error"); .map(|_| ())
.map_err(Error::from)
} }
// TODO: merge with save_remote? // TODO: merge with save_remote?
pub fn from_activity(conn: &Connection, image: &Image) -> Option<Media> { pub fn from_activity(conn: &Connection, image: &Image) -> Result<Media> {
let remote_url = image.object_props.url_string().ok()?; let remote_url = image.object_props.url_string().ok()?;
let ext = remote_url let ext = remote_url
.rsplit('.') .rsplit('.')
@ -185,7 +185,7 @@ impl Media {
.copy_to(&mut dest) .copy_to(&mut dest)
.ok()?; .ok()?;
Some(Media::insert( Media::insert(
conn, conn,
NewMedia { NewMedia {
file_path: path.to_str()?.to_string(), file_path: path.to_str()?.to_string(),
@ -205,7 +205,7 @@ impl Media {
.as_ref(), .as_ref(),
)?.id, )?.id,
}, },
)) )
} }
} }
@ -265,14 +265,14 @@ pub(crate) mod tests {
owner_id: user_two, owner_id: user_two,
}, },
].into_iter() ].into_iter()
.map(|nm| Media::insert(conn, nm)) .map(|nm| Media::insert(conn, nm).unwrap())
.collect()) .collect())
} }
pub(crate) fn clean(conn: &Conn) { pub(crate) fn clean(conn: &Conn) {
//used to remove files generated by tests //used to remove files generated by tests
for media in Media::list_all_medias(conn) { for media in Media::list_all_medias(conn).unwrap() {
media.delete(conn); media.delete(conn).unwrap();
} }
} }
@ -298,10 +298,10 @@ pub(crate) mod tests {
content_warning: None, content_warning: None,
owner_id: user, owner_id: user,
}, },
); ).unwrap();
assert!(Path::new(&path).exists()); assert!(Path::new(&path).exists());
media.delete(conn); media.delete(conn).unwrap();
assert!(!Path::new(&path).exists()); assert!(!Path::new(&path).exists());
clean(conn); clean(conn);
@ -333,26 +333,26 @@ pub(crate) mod tests {
content_warning: None, content_warning: None,
owner_id: u1.id, owner_id: u1.id,
}, },
); ).unwrap();
assert!( assert!(
Media::for_user(conn, u1.id) Media::for_user(conn, u1.id).unwrap()
.iter() .iter()
.any(|m| m.id == media.id) .any(|m| m.id == media.id)
); );
assert!( assert!(
!Media::for_user(conn, u2.id) !Media::for_user(conn, u2.id).unwrap()
.iter() .iter()
.any(|m| m.id == media.id) .any(|m| m.id == media.id)
); );
media.set_owner(conn, u2); media.set_owner(conn, u2).unwrap();
assert!( assert!(
!Media::for_user(conn, u1.id) !Media::for_user(conn, u1.id).unwrap()
.iter() .iter()
.any(|m| m.id == media.id) .any(|m| m.id == media.id)
); );
assert!( assert!(
Media::for_user(conn, u2.id) Media::for_user(conn, u2.id).unwrap()
.iter() .iter()
.any(|m| m.id == media.id) .any(|m| m.id == media.id)
); );

View File

@ -7,7 +7,7 @@ use plume_common::activity_pub::inbox::Notify;
use posts::Post; use posts::Post;
use schema::mentions; use schema::mentions;
use users::User; use users::User;
use Connection; use {Connection, Error, Result};
#[derive(Clone, Queryable, Identifiable, Serialize, Deserialize)] #[derive(Clone, Queryable, Identifiable, Serialize, Deserialize)]
pub struct Mention { pub struct Mention {
@ -32,54 +32,47 @@ impl Mention {
list_by!(mentions, list_for_post, post_id as i32); list_by!(mentions, list_for_post, post_id as i32);
list_by!(mentions, list_for_comment, comment_id as i32); list_by!(mentions, list_for_comment, comment_id as i32);
pub fn get_mentioned(&self, conn: &Connection) -> Option<User> { pub fn get_mentioned(&self, conn: &Connection) -> Result<User> {
User::get(conn, self.mentioned_id) User::get(conn, self.mentioned_id)
} }
pub fn get_post(&self, conn: &Connection) -> Option<Post> { pub fn get_post(&self, conn: &Connection) -> Result<Post> {
self.post_id.and_then(|id| Post::get(conn, id)) self.post_id.ok_or(Error::NotFound).and_then(|id| Post::get(conn, id))
} }
pub fn get_comment(&self, conn: &Connection) -> Option<Comment> { pub fn get_comment(&self, conn: &Connection) -> Result<Comment> {
self.comment_id.and_then(|id| Comment::get(conn, id)) self.comment_id.ok_or(Error::NotFound).and_then(|id| Comment::get(conn, id))
} }
pub fn get_user(&self, conn: &Connection) -> Option<User> { pub fn get_user(&self, conn: &Connection) -> Result<User> {
match self.get_post(conn) { match self.get_post(conn) {
Some(p) => p.get_authors(conn).into_iter().next(), Ok(p) => Ok(p.get_authors(conn)?.into_iter().next()?),
None => self.get_comment(conn).map(|c| c.get_author(conn)), Err(_) => self.get_comment(conn).and_then(|c| c.get_author(conn)),
} }
} }
pub fn build_activity(conn: &Connection, ment: &str) -> link::Mention { pub fn build_activity(conn: &Connection, ment: &str) -> Result<link::Mention> {
let user = User::find_by_fqn(conn, ment); let user = User::find_by_fqn(conn, ment)?;
let mut mention = link::Mention::default(); let mut mention = link::Mention::default();
mention mention
.link_props .link_props
.set_href_string(user.clone().map(|u| u.ap_url).unwrap_or_default()) .set_href_string(user.ap_url)?;
.expect("Mention::build_activity: href error");
mention mention
.link_props .link_props
.set_name_string(format!("@{}", ment)) .set_name_string(format!("@{}", ment))?;
.expect("Mention::build_activity: name error:"); Ok(mention)
mention
} }
pub fn to_activity(&self, conn: &Connection) -> link::Mention { pub fn to_activity(&self, conn: &Connection) -> Result<link::Mention> {
let user = self.get_mentioned(conn); let user = self.get_mentioned(conn)?;
let mut mention = link::Mention::default(); let mut mention = link::Mention::default();
mention mention
.link_props .link_props
.set_href_string(user.clone().map(|u| u.ap_url).unwrap_or_default()) .set_href_string(user.ap_url.clone())?;
.expect("Mention::to_activity: href error");
mention mention
.link_props .link_props
.set_name_string( .set_name_string(format!("@{}", user.get_fqn(conn)))?;
user.map(|u| format!("@{}", u.get_fqn(conn))) Ok(mention)
.unwrap_or_default(),
)
.expect("Mention::to_activity: mention error");
mention
} }
pub fn from_activity( pub fn from_activity(
@ -88,12 +81,12 @@ impl Mention {
inside: i32, inside: i32,
in_post: bool, in_post: bool,
notify: bool, notify: bool,
) -> Option<Self> { ) -> Result<Self> {
let ap_url = ment.link_props.href_string().ok()?; let ap_url = ment.link_props.href_string().ok()?;
let mentioned = User::find_by_ap_url(conn, &ap_url)?; let mentioned = User::find_by_ap_url(conn, &ap_url)?;
if in_post { if in_post {
Post::get(conn, inside).map(|post| { Post::get(conn, inside).and_then(|post| {
let res = Mention::insert( let res = Mention::insert(
conn, conn,
NewMention { NewMention {
@ -101,14 +94,14 @@ impl Mention {
post_id: Some(post.id), post_id: Some(post.id),
comment_id: None, comment_id: None,
}, },
); )?;
if notify { if notify {
res.notify(conn); res.notify(conn)?;
} }
res Ok(res)
}) })
} else { } else {
Comment::get(conn, inside).map(|comment| { Comment::get(conn, inside).and_then(|comment| {
let res = Mention::insert( let res = Mention::insert(
conn, conn,
NewMention { NewMention {
@ -116,37 +109,38 @@ impl Mention {
post_id: None, post_id: None,
comment_id: Some(comment.id), comment_id: Some(comment.id),
}, },
); )?;
if notify { if notify {
res.notify(conn); res.notify(conn)?;
} }
res Ok(res)
}) })
} }
} }
pub fn delete(&self, conn: &Connection) { pub fn delete(&self, conn: &Connection) -> Result<()> {
//find related notifications and delete them //find related notifications and delete them
if let Some(n) = Notification::find(conn, notification_kind::MENTION, self.id) { if let Ok(n) = Notification::find(conn, notification_kind::MENTION, self.id) {
n.delete(conn) n.delete(conn)?;
} }
diesel::delete(self) diesel::delete(self)
.execute(conn) .execute(conn)
.expect("Mention::delete: mention deletion error"); .map(|_| ())
.map_err(Error::from)
} }
} }
impl Notify<Connection> for Mention { impl Notify<Connection> for Mention {
fn notify(&self, conn: &Connection) { type Error = Error;
if let Some(m) = self.get_mentioned(conn) { fn notify(&self, conn: &Connection) -> Result<()> {
Notification::insert( let m = self.get_mentioned(conn)?;
conn, Notification::insert(
NewNotification { conn,
kind: notification_kind::MENTION.to_string(), NewNotification {
object_id: self.id, kind: notification_kind::MENTION.to_string(),
user_id: m.id, object_id: self.id,
}, user_id: m.id,
); },
} ).map(|_| ())
} }
} }

View File

@ -9,7 +9,7 @@ use posts::Post;
use reshares::Reshare; use reshares::Reshare;
use schema::notifications; use schema::notifications;
use users::User; use users::User;
use Connection; use {Connection, Error, Result};
pub mod notification_kind { pub mod notification_kind {
pub const COMMENT: &str = "COMMENT"; pub const COMMENT: &str = "COMMENT";
@ -40,42 +40,42 @@ impl Notification {
insert!(notifications, NewNotification); insert!(notifications, NewNotification);
get!(notifications); get!(notifications);
pub fn find_for_user(conn: &Connection, user: &User) -> Vec<Notification> { pub fn find_for_user(conn: &Connection, user: &User) -> Result<Vec<Notification>> {
notifications::table notifications::table
.filter(notifications::user_id.eq(user.id)) .filter(notifications::user_id.eq(user.id))
.order_by(notifications::creation_date.desc()) .order_by(notifications::creation_date.desc())
.load::<Notification>(conn) .load::<Notification>(conn)
.expect("Notification::find_for_user: notification loading error") .map_err(Error::from)
} }
pub fn count_for_user(conn: &Connection, user: &User) -> i64 { pub fn count_for_user(conn: &Connection, user: &User) -> Result<i64> {
notifications::table notifications::table
.filter(notifications::user_id.eq(user.id)) .filter(notifications::user_id.eq(user.id))
.count() .count()
.get_result(conn) .get_result(conn)
.expect("Notification::count_for_user: count loading error") .map_err(Error::from)
} }
pub fn page_for_user( pub fn page_for_user(
conn: &Connection, conn: &Connection,
user: &User, user: &User,
(min, max): (i32, i32), (min, max): (i32, i32),
) -> Vec<Notification> { ) -> Result<Vec<Notification>> {
notifications::table notifications::table
.filter(notifications::user_id.eq(user.id)) .filter(notifications::user_id.eq(user.id))
.order_by(notifications::creation_date.desc()) .order_by(notifications::creation_date.desc())
.offset(min.into()) .offset(min.into())
.limit((max - min).into()) .limit((max - min).into())
.load::<Notification>(conn) .load::<Notification>(conn)
.expect("Notification::page_for_user: notification loading error") .map_err(Error::from)
} }
pub fn find<S: Into<String>>(conn: &Connection, kind: S, obj: i32) -> Option<Notification> { pub fn find<S: Into<String>>(conn: &Connection, kind: S, obj: i32) -> Result<Notification> {
notifications::table notifications::table
.filter(notifications::kind.eq(kind.into())) .filter(notifications::kind.eq(kind.into()))
.filter(notifications::object_id.eq(obj)) .filter(notifications::object_id.eq(obj))
.get_result::<Notification>(conn) .get_result::<Notification>(conn)
.ok() .map_err(Error::from)
} }
pub fn get_message(&self) -> &'static str { pub fn get_message(&self) -> &'static str {
@ -91,41 +91,37 @@ impl Notification {
pub fn get_url(&self, conn: &Connection) -> Option<String> { pub fn get_url(&self, conn: &Connection) -> Option<String> {
match self.kind.as_ref() { match self.kind.as_ref() {
notification_kind::COMMENT => self.get_post(conn).map(|p| format!("{}#comment-{}", p.url(conn), self.object_id)), notification_kind::COMMENT => self.get_post(conn).and_then(|p| Some(format!("{}#comment-{}", p.url(conn).ok()?, self.object_id))),
notification_kind::FOLLOW => Some(format!("/@/{}/", self.get_actor(conn).get_fqn(conn))), notification_kind::FOLLOW => Some(format!("/@/{}/", self.get_actor(conn).ok()?.get_fqn(conn))),
notification_kind::MENTION => Mention::get(conn, self.object_id).map(|mention| notification_kind::MENTION => Mention::get(conn, self.object_id).and_then(|mention|
mention.get_post(conn).map(|p| p.url(conn)) mention.get_post(conn).and_then(|p| p.url(conn))
.unwrap_or_else(|| { .or_else(|_| {
let comment = mention.get_comment(conn).expect("Notification::get_url: comment not found error"); let comment = mention.get_comment(conn)?;
format!("{}#comment-{}", comment.get_post(conn).url(conn), comment.id) Ok(format!("{}#comment-{}", comment.get_post(conn)?.url(conn)?, comment.id))
}) })
), ).ok(),
_ => None, _ => None,
} }
} }
pub fn get_post(&self, conn: &Connection) -> Option<Post> { pub fn get_post(&self, conn: &Connection) -> Option<Post> {
match self.kind.as_ref() { match self.kind.as_ref() {
notification_kind::COMMENT => Comment::get(conn, self.object_id).map(|comment| comment.get_post(conn)), notification_kind::COMMENT => Comment::get(conn, self.object_id).and_then(|comment| comment.get_post(conn)).ok(),
notification_kind::LIKE => Like::get(conn, self.object_id).and_then(|like| Post::get(conn, like.post_id)), notification_kind::LIKE => Like::get(conn, self.object_id).and_then(|like| Post::get(conn, like.post_id)).ok(),
notification_kind::RESHARE => Reshare::get(conn, self.object_id).and_then(|reshare| reshare.get_post(conn)), notification_kind::RESHARE => Reshare::get(conn, self.object_id).and_then(|reshare| reshare.get_post(conn)).ok(),
_ => None, _ => None,
} }
} }
pub fn get_actor(&self, conn: &Connection) -> User { pub fn get_actor(&self, conn: &Connection) -> Result<User> {
match self.kind.as_ref() { Ok(match self.kind.as_ref() {
notification_kind::COMMENT => Comment::get(conn, self.object_id).expect("Notification::get_actor: comment error").get_author(conn), notification_kind::COMMENT => Comment::get(conn, self.object_id)?.get_author(conn)?,
notification_kind::FOLLOW => User::get(conn, Follow::get(conn, self.object_id).expect("Notification::get_actor: follow error").follower_id) notification_kind::FOLLOW => User::get(conn, Follow::get(conn, self.object_id)?.follower_id)?,
.expect("Notification::get_actor: follower error"), notification_kind::LIKE => User::get(conn, Like::get(conn, self.object_id)?.user_id)?,
notification_kind::LIKE => User::get(conn, Like::get(conn, self.object_id).expect("Notification::get_actor: like error").user_id) notification_kind::MENTION => Mention::get(conn, self.object_id)?.get_user(conn)?,
.expect("Notification::get_actor: liker error"), notification_kind::RESHARE => Reshare::get(conn, self.object_id)?.get_user(conn)?,
notification_kind::MENTION => Mention::get(conn, self.object_id).expect("Notification::get_actor: mention error").get_user(conn)
.expect("Notification::get_actor: mentioner error"),
notification_kind::RESHARE => Reshare::get(conn, self.object_id).expect("Notification::get_actor: reshare error").get_user(conn)
.expect("Notification::get_actor: resharer error"),
_ => unreachable!("Notification::get_actor: Unknow type"), _ => unreachable!("Notification::get_actor: Unknow type"),
} })
} }
pub fn icon_class(&self) -> &'static str { pub fn icon_class(&self) -> &'static str {
@ -139,9 +135,10 @@ impl Notification {
} }
} }
pub fn delete(&self, conn: &Connection) { pub fn delete(&self, conn: &Connection) -> Result<()> {
diesel::delete(self) diesel::delete(self)
.execute(conn) .execute(conn)
.expect("Notification::delete: notification deletion error"); .map(|_| ())
.map_err(Error::from)
} }
} }

View File

@ -3,6 +3,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use posts::Post; use posts::Post;
use schema::post_authors; use schema::post_authors;
use users::User; use users::User;
use {Error, Result};
#[derive(Clone, Queryable, Identifiable, Associations)] #[derive(Clone, Queryable, Identifiable, Associations)]
#[belongs_to(Post)] #[belongs_to(Post)]

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ use plume_common::activity_pub::{
use posts::Post; use posts::Post;
use schema::reshares; use schema::reshares;
use users::User; use users::User;
use Connection; use {Connection, Error, Result};
#[derive(Clone, Serialize, Deserialize, Queryable, Identifiable)] #[derive(Clone, Serialize, Deserialize, Queryable, Identifiable)]
pub struct Reshare { pub struct Reshare {
@ -40,91 +40,80 @@ impl Reshare {
post_id as i32 post_id as i32
); );
pub fn get_recents_for_author(conn: &Connection, user: &User, limit: i64) -> Vec<Reshare> { pub fn get_recents_for_author(conn: &Connection, user: &User, limit: i64) -> Result<Vec<Reshare>> {
reshares::table reshares::table
.filter(reshares::user_id.eq(user.id)) .filter(reshares::user_id.eq(user.id))
.order(reshares::creation_date.desc()) .order(reshares::creation_date.desc())
.limit(limit) .limit(limit)
.load::<Reshare>(conn) .load::<Reshare>(conn)
.expect("Reshare::get_recents_for_author: loading error") .map_err(Error::from)
} }
pub fn get_post(&self, conn: &Connection) -> Option<Post> { pub fn get_post(&self, conn: &Connection) -> Result<Post> {
Post::get(conn, self.post_id) Post::get(conn, self.post_id)
} }
pub fn get_user(&self, conn: &Connection) -> Option<User> { pub fn get_user(&self, conn: &Connection) -> Result<User> {
User::get(conn, self.user_id) User::get(conn, self.user_id)
} }
pub fn to_activity(&self, conn: &Connection) -> Announce { pub fn to_activity(&self, conn: &Connection) -> Result<Announce> {
let mut act = Announce::default(); let mut act = Announce::default();
act.announce_props act.announce_props
.set_actor_link( .set_actor_link(User::get(conn, self.user_id)?.into_id())?;
User::get(conn, self.user_id)
.expect("Reshare::to_activity: user error")
.into_id(),
)
.expect("Reshare::to_activity: actor error");
act.announce_props act.announce_props
.set_object_link( .set_object_link(Post::get(conn, self.post_id)?.into_id())?;
Post::get(conn, self.post_id)
.expect("Reshare::to_activity: post error")
.into_id(),
)
.expect("Reshare::to_activity: object error");
act.object_props act.object_props
.set_id_string(self.ap_url.clone()) .set_id_string(self.ap_url.clone())?;
.expect("Reshare::to_activity: id error");
act.object_props act.object_props
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())) .set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
.expect("Reshare::to_activity: to error");
act.object_props act.object_props
.set_cc_link_vec::<Id>(vec![]) .set_cc_link_vec::<Id>(vec![])?;
.expect("Reshare::to_activity: cc error");
act Ok(act)
} }
} }
impl FromActivity<Announce, Connection> for Reshare { impl FromActivity<Announce, Connection> for Reshare {
fn from_activity(conn: &Connection, announce: Announce, _actor: Id) -> Reshare { type Error = Error;
fn from_activity(conn: &Connection, announce: Announce, _actor: Id) -> Result<Reshare> {
let user = User::from_url( let user = User::from_url(
conn, conn,
announce announce
.announce_props .announce_props
.actor_link::<Id>() .actor_link::<Id>()?
.expect("Reshare::from_activity: actor error")
.as_ref(), .as_ref(),
); )?;
let post = Post::find_by_ap_url( let post = Post::find_by_ap_url(
conn, conn,
announce announce
.announce_props .announce_props
.object_link::<Id>() .object_link::<Id>()?
.expect("Reshare::from_activity: object error")
.as_ref(), .as_ref(),
); )?;
let reshare = Reshare::insert( let reshare = Reshare::insert(
conn, conn,
NewReshare { NewReshare {
post_id: post.expect("Reshare::from_activity: post error").id, post_id: post.id,
user_id: user.expect("Reshare::from_activity: user error").id, user_id: user.id,
ap_url: announce ap_url: announce
.object_props .object_props
.id_string() .id_string()
.unwrap_or_default(), .unwrap_or_default(),
}, },
); )?;
reshare.notify(conn); reshare.notify(conn)?;
reshare Ok(reshare)
} }
} }
impl Notify<Connection> for Reshare { impl Notify<Connection> for Reshare {
fn notify(&self, conn: &Connection) { type Error = Error;
let post = self.get_post(conn).expect("Reshare::notify: post error");
for author in post.get_authors(conn) { fn notify(&self, conn: &Connection) -> Result<()> {
let post = self.get_post(conn)?;
for author in post.get_authors(conn)? {
Notification::insert( Notification::insert(
conn, conn,
NewNotification { NewNotification {
@ -132,55 +121,47 @@ impl Notify<Connection> for Reshare {
object_id: self.id, object_id: self.id,
user_id: author.id, user_id: author.id,
}, },
); )?;
} }
Ok(())
} }
} }
impl Deletable<Connection, Undo> for Reshare { impl Deletable<Connection, Undo> for Reshare {
fn delete(&self, conn: &Connection) -> Undo { type Error = Error;
fn delete(&self, conn: &Connection) -> Result<Undo> {
diesel::delete(self) diesel::delete(self)
.execute(conn) .execute(conn)?;
.expect("Reshare::delete: delete error");
// delete associated notification if any // delete associated notification if any
if let Some(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) { if let Ok(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) {
diesel::delete(&notif) diesel::delete(&notif)
.execute(conn) .execute(conn)?;
.expect("Reshare::delete: notification error");
} }
let mut act = Undo::default(); let mut act = Undo::default();
act.undo_props act.undo_props
.set_actor_link( .set_actor_link(User::get(conn, self.user_id)?.into_id())?;
User::get(conn, self.user_id)
.expect("Reshare::delete: user error")
.into_id(),
)
.expect("Reshare::delete: actor error");
act.undo_props act.undo_props
.set_object_object(self.to_activity(conn)) .set_object_object(self.to_activity(conn)?)?;
.expect("Reshare::delete: object error");
act.object_props act.object_props
.set_id_string(format!("{}#delete", self.ap_url)) .set_id_string(format!("{}#delete", self.ap_url))?;
.expect("Reshare::delete: id error");
act.object_props act.object_props
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string())) .set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
.expect("Reshare::delete: to error");
act.object_props act.object_props
.set_cc_link_vec::<Id>(vec![]) .set_cc_link_vec::<Id>(vec![])?;
.expect("Reshare::delete: cc error");
act Ok(act)
} }
fn delete_id(id: &str, actor_id: &str, conn: &Connection) { fn delete_id(id: &str, actor_id: &str, conn: &Connection) -> Result<Undo> {
if let Some(reshare) = Reshare::find_by_ap_url(conn, id) { let reshare = Reshare::find_by_ap_url(conn, id)?;
if let Some(actor) = User::find_by_ap_url(conn, actor_id) { let actor = User::find_by_ap_url(conn, actor_id)?;
if actor.id == reshare.user_id { if actor.id == reshare.user_id {
reshare.delete(conn); reshare.delete(conn)
} } else {
} Err(Error::Unauthorized)
} }
} }
} }

View File

@ -118,7 +118,7 @@ pub(crate) mod tests {
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let searcher = get_searcher(); let searcher = get_searcher();
let blog = &fill_database(conn).1[0]; let blog = &fill_database(conn).1[0];
let author = &blog.list_authors(conn)[0]; let author = &blog.list_authors(conn).unwrap()[0];
let title = random_hex()[..8].to_owned(); let title = random_hex()[..8].to_owned();
@ -134,23 +134,23 @@ pub(crate) mod tests {
subtitle: "".to_owned(), subtitle: "".to_owned(),
source: "".to_owned(), source: "".to_owned(),
cover_id: None, cover_id: None,
}, &searcher); }, &searcher).unwrap();
PostAuthor::insert(conn, NewPostAuthor { PostAuthor::insert(conn, NewPostAuthor {
post_id: post.id, post_id: post.id,
author_id: author.id, author_id: author.id,
}); }).unwrap();
searcher.commit(); searcher.commit();
assert_eq!(searcher.search_document(conn, Query::from_str(&title), (0,1))[0].id, post.id); assert_eq!(searcher.search_document(conn, Query::from_str(&title), (0,1))[0].id, post.id);
let newtitle = random_hex()[..8].to_owned(); let newtitle = random_hex()[..8].to_owned();
post.title = newtitle.clone(); post.title = newtitle.clone();
post.update(conn, &searcher); post.update(conn, &searcher).unwrap();
searcher.commit(); searcher.commit();
assert_eq!(searcher.search_document(conn, Query::from_str(&newtitle), (0,1))[0].id, post.id); assert_eq!(searcher.search_document(conn, Query::from_str(&newtitle), (0,1))[0].id, post.id);
assert!(searcher.search_document(conn, Query::from_str(&title), (0,1)).is_empty()); assert!(searcher.search_document(conn, Query::from_str(&title), (0,1)).is_empty());
post.delete(&(conn, &searcher)); post.delete(&(conn, &searcher)).unwrap();
searcher.commit(); searcher.commit();
assert!(searcher.search_document(conn, Query::from_str(&newtitle), (0,1)).is_empty()); assert!(searcher.search_document(conn, Query::from_str(&newtitle), (0,1)).is_empty());

View File

@ -14,9 +14,10 @@ use std::{cmp, fs::create_dir_all, path::Path, sync::Mutex};
use search::query::PlumeQuery; use search::query::PlumeQuery;
use super::tokenizer; use super::tokenizer;
use Result;
#[derive(Debug)] #[derive(Debug)]
pub enum SearcherError{ pub enum SearcherError {
IndexCreationError, IndexCreationError,
WriteLockAcquisitionError, WriteLockAcquisitionError,
IndexOpeningError, IndexOpeningError,
@ -66,7 +67,7 @@ impl Searcher {
} }
pub fn create(path: &AsRef<Path>) -> Result<Self,SearcherError> { pub fn create(path: &AsRef<Path>) -> Result<Self> {
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer let whitespace_tokenizer = tokenizer::WhitespaceTokenizer
.filter(LowerCaser); .filter(LowerCaser);
@ -94,7 +95,7 @@ impl Searcher {
}) })
} }
pub fn open(path: &AsRef<Path>) -> Result<Self, SearcherError> { pub fn open(path: &AsRef<Path>) -> Result<Self> {
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer let whitespace_tokenizer = tokenizer::WhitespaceTokenizer
.filter(LowerCaser); .filter(LowerCaser);
@ -121,7 +122,7 @@ impl Searcher {
}) })
} }
pub fn add_document(&self, conn: &Connection, post: &Post) { pub fn add_document(&self, conn: &Connection, post: &Post) -> Result<()> {
let schema = self.index.schema(); let schema = self.index.schema();
let post_id = schema.get_field("post_id").unwrap(); let post_id = schema.get_field("post_id").unwrap();
@ -142,18 +143,19 @@ impl Searcher {
let mut writer = self.writer.lock().unwrap(); let mut writer = self.writer.lock().unwrap();
let writer = writer.as_mut().unwrap(); let writer = writer.as_mut().unwrap();
writer.add_document(doc!( writer.add_document(doc!(
post_id => i64::from(post.id), post_id => i64::from(post.id),
author => post.get_authors(conn).into_iter().map(|u| u.get_fqn(conn)).join(" "), author => post.get_authors(conn)?.into_iter().map(|u| u.get_fqn(conn)).join(" "),
creation_date => i64::from(post.creation_date.num_days_from_ce()), creation_date => i64::from(post.creation_date.num_days_from_ce()),
instance => Instance::get(conn, post.get_blog(conn).instance_id).unwrap().public_domain.clone(), instance => Instance::get(conn, post.get_blog(conn)?.instance_id)?.public_domain.clone(),
tag => Tag::for_post(conn, post.id).into_iter().map(|t| t.tag).join(" "), tag => Tag::for_post(conn, post.id)?.into_iter().map(|t| t.tag).join(" "),
blog_name => post.get_blog(conn).title, blog_name => post.get_blog(conn)?.title,
content => post.content.get().clone(), content => post.content.get().clone(),
subtitle => post.subtitle.clone(), subtitle => post.subtitle.clone(),
title => post.title.clone(), title => post.title.clone(),
lang => detect_lang(post.content.get()).and_then(|i| if i.is_reliable() { Some(i.lang()) } else {None} ).unwrap_or(Lang::Eng).name(), lang => detect_lang(post.content.get()).and_then(|i| if i.is_reliable() { Some(i.lang()) } else {None} ).unwrap_or(Lang::Eng).name(),
license => post.license.clone(), license => post.license.clone(),
)); ));
Ok(())
} }
pub fn delete_document(&self, post: &Post) { pub fn delete_document(&self, post: &Post) {
@ -166,9 +168,9 @@ impl Searcher {
writer.delete_term(doc_id); writer.delete_term(doc_id);
} }
pub fn update_document(&self, conn: &Connection, post: &Post) { pub fn update_document(&self, conn: &Connection, post: &Post) -> Result<()> {
self.delete_document(post); self.delete_document(post);
self.add_document(conn, post); self.add_document(conn, post)
} }
pub fn search_document(&self, conn: &Connection, query: PlumeQuery, (min, max): (i32, i32)) -> Vec<Post>{ pub fn search_document(&self, conn: &Connection, query: PlumeQuery, (min, max): (i32, i32)) -> Vec<Post>{
@ -185,9 +187,9 @@ impl Searcher {
.filter_map(|doc_add| { .filter_map(|doc_add| {
let doc = searcher.doc(*doc_add).ok()?; let doc = searcher.doc(*doc_add).ok()?;
let id = doc.get_first(post_id)?; let id = doc.get_first(post_id)?;
Post::get(conn, id.i64_value() as i32) Post::get(conn, id.i64_value() as i32).ok()
//borrow checker don't want me to use filter_map or and_then here //borrow checker don't want me to use filter_map or and_then here
}) })
.collect() .collect()
} }

View File

@ -3,7 +3,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use instance::Instance; use instance::Instance;
use plume_common::activity_pub::Hashtag; use plume_common::activity_pub::Hashtag;
use schema::tags; use schema::tags;
use {ap_url, Connection}; use {ap_url, Connection, Error, Result};
#[derive(Clone, Identifiable, Serialize, Queryable)] #[derive(Clone, Identifiable, Serialize, Queryable)]
pub struct Tag { pub struct Tag {
@ -27,48 +27,43 @@ impl Tag {
find_by!(tags, find_by_name, tag as &str); find_by!(tags, find_by_name, tag as &str);
list_by!(tags, for_post, post_id as i32); list_by!(tags, for_post, post_id as i32);
pub fn to_activity(&self, conn: &Connection) -> Hashtag { pub fn to_activity(&self, conn: &Connection) -> Result<Hashtag> {
let mut ht = Hashtag::default(); let mut ht = Hashtag::default();
ht.set_href_string(ap_url(&format!( ht.set_href_string(ap_url(&format!(
"{}/tag/{}", "{}/tag/{}",
Instance::get_local(conn) Instance::get_local(conn)?.public_domain,
.expect("Tag::to_activity: local instance not found error")
.public_domain,
self.tag self.tag
))).expect("Tag::to_activity: href error"); )))?;
ht.set_name_string(self.tag.clone()) ht.set_name_string(self.tag.clone())?;
.expect("Tag::to_activity: name error"); Ok(ht)
ht
} }
pub fn from_activity(conn: &Connection, tag: &Hashtag, post: i32, is_hashtag: bool) -> Tag { pub fn from_activity(conn: &Connection, tag: &Hashtag, post: i32, is_hashtag: bool) -> Result<Tag> {
Tag::insert( Tag::insert(
conn, conn,
NewTag { NewTag {
tag: tag.name_string().expect("Tag::from_activity: name error"), tag: tag.name_string()?,
is_hashtag, is_hashtag,
post_id: post, post_id: post,
}, },
) )
} }
pub fn build_activity(conn: &Connection, tag: String) -> Hashtag { pub fn build_activity(conn: &Connection, tag: String) -> Result<Hashtag> {
let mut ht = Hashtag::default(); let mut ht = Hashtag::default();
ht.set_href_string(ap_url(&format!( ht.set_href_string(ap_url(&format!(
"{}/tag/{}", "{}/tag/{}",
Instance::get_local(conn) Instance::get_local(conn)?.public_domain,
.expect("Tag::to_activity: local instance not found error")
.public_domain,
tag tag
))).expect("Tag::to_activity: href error"); )))?;
ht.set_name_string(tag) ht.set_name_string(tag)?;
.expect("Tag::to_activity: name error"); Ok(ht)
ht
} }
pub fn delete(&self, conn: &Connection) { pub fn delete(&self, conn: &Connection) -> Result<()> {
diesel::delete(self) diesel::delete(self)
.execute(conn) .execute(conn)
.expect("Tag::delete: database error"); .map(|_| ())
.map_err(Error::from)
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,41 @@
use rocket::request::Form; use rocket::{response::{self, Responder}, request::{Form, Request}};
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use serde_json; use serde_json;
use plume_common::utils::random_hex; use plume_common::utils::random_hex;
use plume_models::{ use plume_models::{
Error,
apps::App, apps::App,
api_tokens::*, api_tokens::*,
db_conn::DbConn, db_conn::DbConn,
users::User, users::User,
}; };
#[derive(Debug)]
pub struct ApiError(Error);
impl From<Error> for ApiError {
fn from(err: Error) -> ApiError {
ApiError(err)
}
}
impl<'r> Responder<'r> for ApiError {
fn respond_to(self, req: &Request) -> response::Result<'r> {
match self.0 {
Error::NotFound => Json(json!({
"error": "Not found"
})).respond_to(req),
Error::Unauthorized => Json(json!({
"error": "You are not authorized to access this resource"
})).respond_to(req),
_ => Json(json!({
"error": "Server error"
})).respond_to(req)
}
}
}
#[derive(FromForm)] #[derive(FromForm)]
pub struct OAuthRequest { pub struct OAuthRequest {
client_id: String, client_id: String,
@ -20,38 +46,38 @@ pub struct OAuthRequest {
} }
#[get("/oauth2?<query..>")] #[get("/oauth2?<query..>")]
pub fn oauth(query: Form<OAuthRequest>, conn: DbConn) -> Json<serde_json::Value> { pub fn oauth(query: Form<OAuthRequest>, conn: DbConn) -> Result<Json<serde_json::Value>, ApiError> {
let app = App::find_by_client_id(&*conn, &query.client_id).expect("OAuth request from unknown client"); let app = App::find_by_client_id(&*conn, &query.client_id)?;
if app.client_secret == query.client_secret { if app.client_secret == query.client_secret {
if let Some(user) = User::find_local(&*conn, &query.username) { if let Ok(user) = User::find_local(&*conn, &query.username) {
if user.auth(&query.password) { if user.auth(&query.password) {
let token = ApiToken::insert(&*conn, NewApiToken { let token = ApiToken::insert(&*conn, NewApiToken {
app_id: app.id, app_id: app.id,
user_id: user.id, user_id: user.id,
value: random_hex(), value: random_hex(),
scopes: query.scopes.clone(), scopes: query.scopes.clone(),
}); })?;
Json(json!({ Ok(Json(json!({
"token": token.value "token": token.value
})) })))
} else { } else {
Json(json!({ Ok(Json(json!({
"error": "Invalid credentials" "error": "Invalid credentials"
})) })))
} }
} else { } else {
// Making fake password verification to avoid different // Making fake password verification to avoid different
// response times that would make it possible to know // response times that would make it possible to know
// if a username is registered or not. // if a username is registered or not.
User::get(&*conn, 1).unwrap().auth(&query.password); User::get(&*conn, 1)?.auth(&query.password);
Json(json!({ Ok(Json(json!({
"error": "Invalid credentials" "error": "Invalid credentials"
})) })))
} }
} else { } else {
Json(json!({ Ok(Json(json!({
"error": "Invalid client_secret" "error": "Invalid client_secret"
})) })))
} }
} }

View File

@ -42,13 +42,14 @@ pub trait Inbox {
match act["type"].as_str() { match act["type"].as_str() {
Some(t) => match t { Some(t) => match t {
"Announce" => { "Announce" => {
Reshare::from_activity(conn, serde_json::from_value(act.clone())?, actor_id); Reshare::from_activity(conn, serde_json::from_value(act.clone())?, actor_id)
.expect("Inbox::received: Announce error");;
Ok(()) Ok(())
} }
"Create" => { "Create" => {
let act: Create = serde_json::from_value(act.clone())?; let act: Create = serde_json::from_value(act.clone())?;
if Post::try_from_activity(&(conn, searcher), act.clone()) if Post::try_from_activity(&(conn, searcher), act.clone()).is_ok()
|| Comment::try_from_activity(conn, act) || Comment::try_from_activity(conn, act).is_ok()
{ {
Ok(()) Ok(())
} else { } else {
@ -64,7 +65,7 @@ pub trait Inbox {
.id_string()?, .id_string()?,
actor_id.as_ref(), actor_id.as_ref(),
&(conn, searcher), &(conn, searcher),
); ).ok();
Comment::delete_id( Comment::delete_id(
&act.delete_props &act.delete_props
.object_object::<Tombstone>()? .object_object::<Tombstone>()?
@ -72,11 +73,12 @@ pub trait Inbox {
.id_string()?, .id_string()?,
actor_id.as_ref(), actor_id.as_ref(),
conn, conn,
); ).ok();
Ok(()) Ok(())
} }
"Follow" => { "Follow" => {
Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id).notify(conn); Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id)
.and_then(|f| f.notify(conn)).expect("Inbox::received: follow from activity error");;
Ok(()) Ok(())
} }
"Like" => { "Like" => {
@ -84,7 +86,7 @@ pub trait Inbox {
conn, conn,
serde_json::from_value(act.clone())?, serde_json::from_value(act.clone())?,
actor_id, actor_id,
); ).expect("Inbox::received: like from activity error");;
Ok(()) Ok(())
} }
"Undo" => { "Undo" => {
@ -99,7 +101,7 @@ pub trait Inbox {
.id_string()?, .id_string()?,
actor_id.as_ref(), actor_id.as_ref(),
conn, conn,
); ).expect("Inbox::received: undo like fail");;
Ok(()) Ok(())
} }
"Announce" => { "Announce" => {
@ -110,7 +112,7 @@ pub trait Inbox {
.id_string()?, .id_string()?,
actor_id.as_ref(), actor_id.as_ref(),
conn, conn,
); ).expect("Inbox::received: undo reshare fail");;
Ok(()) Ok(())
} }
"Follow" => { "Follow" => {
@ -121,21 +123,21 @@ pub trait Inbox {
.id_string()?, .id_string()?,
actor_id.as_ref(), actor_id.as_ref(),
conn, conn,
); ).expect("Inbox::received: undo follow error");;
Ok(()) Ok(())
} }
_ => Err(InboxError::CantUndo)?, _ => Err(InboxError::CantUndo)?,
} }
} else { } else {
let link = act.undo_props.object.as_str().expect("Inbox::received: undo don't contain type and isn't Link"); let link = act.undo_props.object.as_str().expect("Inbox::received: undo don't contain type and isn't Link");
if let Some(like) = likes::Like::find_by_ap_url(conn, link) { if let Ok(like) = likes::Like::find_by_ap_url(conn, link) {
likes::Like::delete_id(&like.ap_url, actor_id.as_ref(), conn); likes::Like::delete_id(&like.ap_url, actor_id.as_ref(), conn).expect("Inbox::received: delete Like error");
Ok(()) Ok(())
} else if let Some(reshare) = Reshare::find_by_ap_url(conn, link) { } else if let Ok(reshare) = Reshare::find_by_ap_url(conn, link) {
Reshare::delete_id(&reshare.ap_url, actor_id.as_ref(), conn); Reshare::delete_id(&reshare.ap_url, actor_id.as_ref(), conn).expect("Inbox::received: delete Announce error");
Ok(()) Ok(())
} else if let Some(follow) = Follow::find_by_ap_url(conn, link) { } else if let Ok(follow) = Follow::find_by_ap_url(conn, link) {
Follow::delete_id(&follow.ap_url, actor_id.as_ref(), conn); Follow::delete_id(&follow.ap_url, actor_id.as_ref(), conn).expect("Inbox::received: delete Follow error");
Ok(()) Ok(())
} else { } else {
Err(InboxError::NoType)? Err(InboxError::NoType)?
@ -144,7 +146,7 @@ pub trait Inbox {
} }
"Update" => { "Update" => {
let act: Update = serde_json::from_value(act.clone())?; let act: Update = serde_json::from_value(act.clone())?;
Post::handle_update(conn, &act.update_props.object_object()?, searcher); Post::handle_update(conn, &act.update_props.object_object()?, searcher).expect("Inbox::received: post update error");;
Ok(()) Ok(())
} }
_ => Err(InboxError::InvalidType)?, _ => Err(InboxError::InvalidType)?,

View File

@ -38,8 +38,11 @@ extern crate webfinger;
use diesel::r2d2::ConnectionManager; use diesel::r2d2::ConnectionManager;
use rocket::State; use rocket::State;
use rocket_csrf::CsrfFairingBuilder; use rocket_csrf::CsrfFairingBuilder;
use plume_models::{DATABASE_URL, Connection, use plume_models::{
db_conn::{DbPool, PragmaForeignKey}, search::Searcher as UnmanagedSearcher}; DATABASE_URL, Connection, Error,
db_conn::{DbPool, PragmaForeignKey},
search::{Searcher as UnmanagedSearcher, SearcherError},
};
use scheduled_thread_pool::ScheduledThreadPool; use scheduled_thread_pool::ScheduledThreadPool;
use std::process::exit; use std::process::exit;
use std::sync::Arc; use std::sync::Arc;
@ -65,10 +68,23 @@ fn init_pool() -> Option<DbPool> {
} }
fn main() { fn main() {
let dbpool = init_pool().expect("main: database pool initialization error"); let dbpool = init_pool().expect("main: database pool initialization error");
let workpool = ScheduledThreadPool::with_name("worker {}", num_cpus::get()); let workpool = ScheduledThreadPool::with_name("worker {}", num_cpus::get());
let searcher = Arc::new(UnmanagedSearcher::open(&"search_index").unwrap()); let searcher = match UnmanagedSearcher::open(&"search_index") {
Err(Error::Search(e)) => match e {
SearcherError::WriteLockAcquisitionError => panic!(
r#"Your search index is locked. Plume can't start. To fix this issue
make sure no other Plume instance is started, and run:
plm search unlock
Then try to restart Plume.
"#),
e => Err(e).unwrap()
},
Err(_) => panic!("Unexpected error while opening search index"),
Ok(s) => Arc::new(s)
};
let commiter = searcher.clone(); let commiter = searcher.clone();
workpool.execute_with_fixed_delay(Duration::from_secs(5), Duration::from_secs(60*30), move || commiter.commit()); workpool.execute_with_fixed_delay(Duration::from_secs(5), Duration::from_secs(60*30), move || commiter.commit());

View File

@ -19,18 +19,17 @@ use plume_models::{
posts::Post, posts::Post,
users::User users::User
}; };
use routes::Page; use routes::{Page, errors::ErrorPage};
use template_utils::Ructe; use template_utils::Ructe;
use Searcher; use Searcher;
#[get("/~/<name>?<page>", rank = 2)] #[get("/~/<name>?<page>", rank = 2)]
pub fn details(intl: I18n, name: String, conn: DbConn, user: Option<User>, page: Option<Page>) -> Result<Ructe, Ructe> { pub fn details(intl: I18n, name: String, conn: DbConn, user: Option<User>, page: Option<Page>) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
let blog = Blog::find_by_fqn(&*conn, &name) let blog = Blog::find_by_fqn(&*conn, &name)?;
.ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, user.clone()))))?; let posts = Post::blog_page(&*conn, &blog, page.limits())?;
let posts = Post::blog_page(&*conn, &blog, page.limits()); let articles_count = Post::count_for_blog(&*conn, &blog)?;
let articles_count = Post::count_for_blog(&*conn, &blog); let authors = &blog.list_authors(&*conn)?;
let authors = &blog.list_authors(&*conn);
Ok(render!(blogs::details( Ok(render!(blogs::details(
&(&*conn, &intl.catalog, user.clone()), &(&*conn, &intl.catalog, user.clone()),
@ -40,15 +39,15 @@ pub fn details(intl: I18n, name: String, conn: DbConn, user: Option<User>, page:
articles_count, articles_count,
page.0, page.0,
Page::total(articles_count as i32), Page::total(articles_count as i32),
user.map(|x| x.is_author_in(&*conn, &blog)).unwrap_or(false), user.and_then(|x| x.is_author_in(&*conn, &blog).ok()).unwrap_or(false),
posts posts
))) )))
} }
#[get("/~/<name>", rank = 1)] #[get("/~/<name>", rank = 1)]
pub fn activity_details(name: String, conn: DbConn, _ap: ApRequest) -> Option<ActivityStream<CustomGroup>> { pub fn activity_details(name: String, conn: DbConn, _ap: ApRequest) -> Option<ActivityStream<CustomGroup>> {
let blog = Blog::find_local(&*conn, &name)?; let blog = Blog::find_local(&*conn, &name).ok()?;
Some(ActivityStream::new(blog.to_activity(&*conn))) Some(ActivityStream::new(blog.to_activity(&*conn).ok()?))
} }
#[get("/blogs/new")] #[get("/blogs/new")]
@ -91,7 +90,7 @@ pub fn create(conn: DbConn, form: LenientForm<NewBlogForm>, user: User, intl: I1
Ok(_) => ValidationErrors::new(), Ok(_) => ValidationErrors::new(),
Err(e) => e Err(e) => e
}; };
if Blog::find_local(&*conn, &slug).is_some() { if Blog::find_local(&*conn, &slug).is_ok() {
errors.add("title", ValidationError { errors.add("title", ValidationError {
code: Cow::from("existing_slug"), code: Cow::from("existing_slug"),
message: Some(Cow::from("A blog with the same name already exists.")), message: Some(Cow::from("A blog with the same name already exists.")),
@ -104,19 +103,19 @@ pub fn create(conn: DbConn, form: LenientForm<NewBlogForm>, user: User, intl: I1
slug.clone(), slug.clone(),
form.title.to_string(), form.title.to_string(),
String::from(""), String::from(""),
Instance::local_id(&*conn) Instance::get_local(&*conn).expect("blog::create: instance error").id
)); ).expect("blog::create: new local error")).expect("blog::create: error");
blog.update_boxes(&*conn); blog.update_boxes(&*conn).expect("blog::create: insert error");
BlogAuthor::insert(&*conn, NewBlogAuthor { BlogAuthor::insert(&*conn, NewBlogAuthor {
blog_id: blog.id, blog_id: blog.id,
author_id: user.id, author_id: user.id,
is_owner: true is_owner: true
}); }).expect("blog::create: author error");
Ok(Redirect::to(uri!(details: name = slug.clone(), page = _))) Ok(Redirect::to(uri!(details: name = slug.clone(), page = _)))
} else { } else {
Err(render!(blogs::new( Err(render!(blogs::new(
&(&*conn, &intl.catalog, Some(user)), &(&*conn, &intl.catalog, Some(user)),
&*form, &*form,
errors errors
@ -125,38 +124,37 @@ pub fn create(conn: DbConn, form: LenientForm<NewBlogForm>, user: User, intl: I1
} }
#[post("/~/<name>/delete")] #[post("/~/<name>/delete")]
pub fn delete(conn: DbConn, name: String, user: Option<User>, intl: I18n, searcher: Searcher) -> Result<Redirect, Option<Ructe>>{ pub fn delete(conn: DbConn, name: String, user: Option<User>, intl: I18n, searcher: Searcher) -> Result<Redirect, Ructe>{
let blog = Blog::find_local(&*conn, &name).ok_or(None)?; let blog = Blog::find_local(&*conn, &name).expect("blog::delete: blog not found");
if user.clone().map(|u| u.is_author_in(&*conn, &blog)).unwrap_or(false) { if user.clone().and_then(|u| u.is_author_in(&*conn, &blog).ok()).unwrap_or(false) {
blog.delete(&conn, &searcher); blog.delete(&conn, &searcher).expect("blog::expect: deletion error");
Ok(Redirect::to(uri!(super::instance::index))) Ok(Redirect::to(uri!(super::instance::index)))
} else { } else {
// TODO actually return 403 error code // TODO actually return 403 error code
Err(Some(render!(errors::not_authorized( Err(render!(errors::not_authorized(
&(&*conn, &intl.catalog, user), &(&*conn, &intl.catalog, user),
"You are not allowed to delete this blog." "You are not allowed to delete this blog."
)))) )))
} }
} }
#[get("/~/<name>/outbox")] #[get("/~/<name>/outbox")]
pub fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> { pub fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> {
let blog = Blog::find_local(&*conn, &name)?; let blog = Blog::find_local(&*conn, &name).ok()?;
Some(blog.outbox(&*conn)) Some(blog.outbox(&*conn).ok()?)
} }
#[get("/~/<name>/atom.xml")] #[get("/~/<name>/atom.xml")]
pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> { pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
let blog = Blog::find_by_fqn(&*conn, &name)?; let blog = Blog::find_by_fqn(&*conn, &name).ok()?;
let feed = FeedBuilder::default() let feed = FeedBuilder::default()
.title(blog.title.clone()) .title(blog.title.clone())
.id(Instance::get_local(&*conn).expect("blogs::atom_feed: local instance not found error") .id(Instance::get_local(&*conn).ok()?
.compute_box("~", &name, "atom.xml")) .compute_box("~", &name, "atom.xml"))
.entries(Post::get_recents_for_blog(&*conn, &blog, 15) .entries(Post::get_recents_for_blog(&*conn, &blog, 15).ok()?
.into_iter() .into_iter()
.map(|p| super::post_to_atom(p, &*conn)) .map(|p| super::post_to_atom(p, &*conn))
.collect::<Vec<Entry>>()) .collect::<Vec<Entry>>())
.build() .build().ok()?;
.expect("blogs::atom_feed: feed creation error");
Some(Content(ContentType::new("application", "atom+xml"), feed.to_string())) Some(Content(ContentType::new("application", "atom+xml"), feed.to_string()))
} }

View File

@ -21,6 +21,7 @@ use plume_models::{
users::User users::User
}; };
use Worker; use Worker;
use routes::errors::ErrorPage;
#[derive(Default, FromForm, Debug, Validate, Serialize)] #[derive(Default, FromForm, Debug, Validate, Serialize)]
pub struct NewCommentForm { pub struct NewCommentForm {
@ -32,12 +33,15 @@ pub struct NewCommentForm {
#[post("/~/<blog_name>/<slug>/comment", data = "<form>")] #[post("/~/<blog_name>/<slug>/comment", data = "<form>")]
pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>, user: User, conn: DbConn, worker: Worker, intl: I18n) pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>, user: User, conn: DbConn, worker: Worker, intl: I18n)
-> Result<Redirect, Option<Ructe>> { -> Result<Redirect, Ructe> {
let blog = Blog::find_by_fqn(&*conn, &blog_name).ok_or(None)?; let blog = Blog::find_by_fqn(&*conn, &blog_name).expect("comments::create: blog error");
let post = Post::find_by_slug(&*conn, &slug, blog.id).ok_or(None)?; let post = Post::find_by_slug(&*conn, &slug, blog.id).expect("comments::create: post error");
form.validate() form.validate()
.map(|_| { .map(|_| {
let (html, mentions, _hashtags) = utils::md_to_html(form.content.as_ref(), &Instance::get_local(&conn).expect("comments::create: Error getting local instance").public_domain); let (html, mentions, _hashtags) = utils::md_to_html(
form.content.as_ref(),
&Instance::get_local(&conn).expect("comments::create: local instance error").public_domain
);
let comm = Comment::insert(&*conn, NewComment { let comm = Comment::insert(&*conn, NewComment {
content: SafeString::new(html.as_ref()), content: SafeString::new(html.as_ref()),
in_response_to_id: form.responding_to, in_response_to_id: form.responding_to,
@ -47,16 +51,22 @@ pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>
sensitive: !form.warning.is_empty(), sensitive: !form.warning.is_empty(),
spoiler_text: form.warning.clone(), spoiler_text: form.warning.clone(),
public_visibility: true public_visibility: true
}).update_ap_url(&*conn); }).expect("comments::create: insert error").update_ap_url(&*conn).expect("comments::create: update ap url error");
let new_comment = comm.create_activity(&*conn); let new_comment = comm.create_activity(&*conn).expect("comments::create: activity error");
// save mentions // save mentions
for ment in mentions { for ment in mentions {
Mention::from_activity(&*conn, &Mention::build_activity(&*conn, &ment), post.id, true, true); Mention::from_activity(
&*conn,
&Mention::build_activity(&*conn, &ment).expect("comments::create: build mention error"),
post.id,
true,
true
).expect("comments::create: mention save error");
} }
// federate // federate
let dest = User::one_by_instance(&*conn); let dest = User::one_by_instance(&*conn).expect("comments::create: dest error");
let user_clone = user.clone(); let user_clone = user.clone();
worker.execute(move || broadcast(&user_clone, new_comment, dest)); worker.execute(move || broadcast(&user_clone, new_comment, dest));
@ -64,43 +74,46 @@ pub fn create(blog_name: String, slug: String, form: LenientForm<NewCommentForm>
}) })
.map_err(|errors| { .map_err(|errors| {
// TODO: de-duplicate this code // TODO: de-duplicate this code
let comments = CommentTree::from_post(&*conn, &post, Some(&user)); let comments = CommentTree::from_post(&*conn, &post, Some(&user)).expect("comments::create: comments error");
let previous = form.responding_to.map(|r| Comment::get(&*conn, r) let previous = form.responding_to.and_then(|r| Comment::get(&*conn, r).ok());
.expect("comments::create: Error retrieving previous comment"));
Some(render!(posts::details( render!(posts::details(
&(&*conn, &intl.catalog, Some(user.clone())), &(&*conn, &intl.catalog, Some(user.clone())),
post.clone(), post.clone(),
blog, blog,
&*form, &*form,
errors, errors,
Tag::for_post(&*conn, post.id), Tag::for_post(&*conn, post.id).expect("comments::create: tags error"),
comments, comments,
previous, previous,
post.count_likes(&*conn), post.count_likes(&*conn).expect("comments::create: count likes error"),
post.count_reshares(&*conn), post.count_reshares(&*conn).expect("comments::create: count reshares error"),
user.has_liked(&*conn, &post), user.has_liked(&*conn, &post).expect("comments::create: liked error"),
user.has_reshared(&*conn, &post), user.has_reshared(&*conn, &post).expect("comments::create: reshared error"),
user.is_following(&*conn, post.get_authors(&*conn)[0].id), user.is_following(&*conn, post.get_authors(&*conn).expect("comments::create: authors error")[0].id)
post.get_authors(&*conn)[0].clone() .expect("comments::create: following error"),
))) post.get_authors(&*conn).expect("comments::create: authors error")[0].clone()
))
}) })
} }
#[post("/~/<blog>/<slug>/comment/<id>/delete")] #[post("/~/<blog>/<slug>/comment/<id>/delete")]
pub fn delete(blog: String, slug: String, id: i32, user: User, conn: DbConn, worker: Worker) -> Redirect { pub fn delete(blog: String, slug: String, id: i32, user: User, conn: DbConn, worker: Worker) -> Result<Redirect, ErrorPage> {
if let Some(comment) = Comment::get(&*conn, id) { if let Ok(comment) = Comment::get(&*conn, id) {
if comment.author_id == user.id { if comment.author_id == user.id {
let dest = User::one_by_instance(&*conn); let dest = User::one_by_instance(&*conn)?;
let delete_activity = comment.delete(&*conn); let delete_activity = comment.delete(&*conn)?;
worker.execute(move || broadcast(&user, delete_activity, dest)); worker.execute(move || broadcast(&user, delete_activity, dest));
} }
} }
Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _)) Ok(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _)))
} }
#[get("/~/<_blog>/<_slug>/comment/<id>")] #[get("/~/<_blog>/<_slug>/comment/<id>")]
pub fn activity_pub(_blog: String, _slug: String, id: i32, _ap: ApRequest, conn: DbConn) -> Option<ActivityStream<Note>> { pub fn activity_pub(_blog: String, _slug: String, id: i32, _ap: ApRequest, conn: DbConn) -> Option<ActivityStream<Note>> {
Comment::get(&*conn, id).map(|c| ActivityStream::new(c.to_activity(&*conn))) Comment::get(&*conn, id)
.and_then(|c| c.to_activity(&*conn))
.ok()
.map(ActivityStream::new)
} }

View File

@ -1,10 +1,42 @@
use rocket::Request; use rocket::{
use rocket::request::FromRequest; Request,
request::FromRequest,
response::{self, Responder},
};
use rocket_i18n::I18n; use rocket_i18n::I18n;
use plume_models::db_conn::DbConn; use plume_models::{Error, db_conn::DbConn};
use plume_models::users::User; use plume_models::users::User;
use template_utils::Ructe; use template_utils::Ructe;
#[derive(Debug)]
pub struct ErrorPage(Error);
impl From<Error> for ErrorPage {
fn from(err: Error) -> ErrorPage {
ErrorPage(err)
}
}
impl<'r> Responder<'r> for ErrorPage {
fn respond_to(self, req: &Request) -> response::Result<'r> {
let conn = req.guard::<DbConn>().succeeded();
let intl = req.guard::<I18n>().succeeded();
let user = User::from_request(req).succeeded();
match self.0 {
Error::NotFound => render!(errors::not_found(
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
)).respond_to(req),
Error::Unauthorized => render!(errors::not_found(
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
)).respond_to(req),
_ => render!(errors::not_found(
&(&*conn.unwrap(), &intl.unwrap().catalog, user)
)).respond_to(req)
}
}
}
#[catch(404)] #[catch(404)]
pub fn not_found(req: &Request) -> Ructe { pub fn not_found(req: &Request) -> Ructe {
let conn = req.guard::<DbConn>().succeeded(); let conn = req.guard::<DbConn>().succeeded();

View File

@ -17,86 +17,78 @@ use plume_models::{
instance::* instance::*
}; };
use inbox::{Inbox, SignedJson}; use inbox::{Inbox, SignedJson};
use routes::Page; use routes::{Page, errors::ErrorPage};
use template_utils::Ructe; use template_utils::Ructe;
use Searcher; use Searcher;
#[get("/")] #[get("/")]
pub fn index(conn: DbConn, user: Option<User>, intl: I18n) -> Ructe { pub fn index(conn: DbConn, user: Option<User>, intl: I18n) -> Result<Ructe, ErrorPage> {
match Instance::get_local(&*conn) { let inst = Instance::get_local(&*conn)?;
Some(inst) => { let federated = Post::get_recents_page(&*conn, Page::default().limits())?;
let federated = Post::get_recents_page(&*conn, Page::default().limits()); let local = Post::get_instance_page(&*conn, inst.id, Page::default().limits())?;
let local = Post::get_instance_page(&*conn, inst.id, Page::default().limits()); let user_feed = user.clone().and_then(|user| {
let user_feed = user.clone().map(|user| { let followed = user.get_following(&*conn).ok()?;
let followed = user.get_following(&*conn); let mut in_feed = followed.into_iter().map(|u| u.id).collect::<Vec<i32>>();
let mut in_feed = followed.into_iter().map(|u| u.id).collect::<Vec<i32>>(); in_feed.push(user.id);
in_feed.push(user.id); Post::user_feed_page(&*conn, in_feed, Page::default().limits()).ok()
Post::user_feed_page(&*conn, in_feed, Page::default().limits()) });
});
render!(instance::index( Ok(render!(instance::index(
&(&*conn, &intl.catalog, user), &(&*conn, &intl.catalog, user),
inst, inst,
User::count_local(&*conn), User::count_local(&*conn)?,
Post::count_local(&*conn), Post::count_local(&*conn)?,
local, local,
federated, federated,
user_feed user_feed
)) )))
}
None => {
render!(errors::server_error(
&(&*conn, &intl.catalog, user)
))
}
}
} }
#[get("/local?<page>")] #[get("/local?<page>")]
pub fn local(conn: DbConn, user: Option<User>, page: Option<Page>, intl: I18n) -> Ructe { pub fn local(conn: DbConn, user: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
let instance = Instance::get_local(&*conn).expect("instance::paginated_local: local instance not found error"); let instance = Instance::get_local(&*conn)?;
let articles = Post::get_instance_page(&*conn, instance.id, page.limits()); let articles = Post::get_instance_page(&*conn, instance.id, page.limits())?;
render!(instance::local( Ok(render!(instance::local(
&(&*conn, &intl.catalog, user), &(&*conn, &intl.catalog, user),
instance, instance,
articles, articles,
page.0, page.0,
Page::total(Post::count_local(&*conn) as i32) Page::total(Post::count_local(&*conn)? as i32)
)) )))
} }
#[get("/feed?<page>")] #[get("/feed?<page>")]
pub fn feed(conn: DbConn, user: User, page: Option<Page>, intl: I18n) -> Ructe { pub fn feed(conn: DbConn, user: User, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
let followed = user.get_following(&*conn); let followed = user.get_following(&*conn)?;
let mut in_feed = followed.into_iter().map(|u| u.id).collect::<Vec<i32>>(); let mut in_feed = followed.into_iter().map(|u| u.id).collect::<Vec<i32>>();
in_feed.push(user.id); in_feed.push(user.id);
let articles = Post::user_feed_page(&*conn, in_feed, page.limits()); let articles = Post::user_feed_page(&*conn, in_feed, page.limits())?;
render!(instance::feed( Ok(render!(instance::feed(
&(&*conn, &intl.catalog, Some(user)), &(&*conn, &intl.catalog, Some(user)),
articles, articles,
page.0, page.0,
Page::total(Post::count_local(&*conn) as i32) Page::total(Post::count_local(&*conn)? as i32)
)) )))
} }
#[get("/federated?<page>")] #[get("/federated?<page>")]
pub fn federated(conn: DbConn, user: Option<User>, page: Option<Page>, intl: I18n) -> Ructe { pub fn federated(conn: DbConn, user: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
let articles = Post::get_recents_page(&*conn, page.limits()); let articles = Post::get_recents_page(&*conn, page.limits())?;
render!(instance::federated( Ok(render!(instance::federated(
&(&*conn, &intl.catalog, user), &(&*conn, &intl.catalog, user),
articles, articles,
page.0, page.0,
Page::total(Post::count_local(&*conn) as i32) Page::total(Post::count_local(&*conn)? as i32)
)) )))
} }
#[get("/admin")] #[get("/admin")]
pub fn admin(conn: DbConn, admin: Admin, intl: I18n) -> Ructe { pub fn admin(conn: DbConn, admin: Admin, intl: I18n) -> Result<Ructe, ErrorPage> {
let local_inst = Instance::get_local(&*conn).expect("instance::admin: local instance not found"); let local_inst = Instance::get_local(&*conn)?;
render!(instance::admin( Ok(render!(instance::admin(
&(&*conn, &intl.catalog, Some(admin.0)), &(&*conn, &intl.catalog, Some(admin.0)),
local_inst.clone(), local_inst.clone(),
InstanceSettingsForm { InstanceSettingsForm {
@ -107,7 +99,7 @@ pub fn admin(conn: DbConn, admin: Admin, intl: I18n) -> Ructe {
default_license: local_inst.default_license, default_license: local_inst.default_license,
}, },
ValidationErrors::default() ValidationErrors::default()
)) )))
} }
#[derive(Clone, FromForm, Validate, Serialize)] #[derive(Clone, FromForm, Validate, Serialize)]
@ -124,65 +116,65 @@ pub struct InstanceSettingsForm {
#[post("/admin", data = "<form>")] #[post("/admin", data = "<form>")]
pub fn update_settings(conn: DbConn, admin: Admin, form: LenientForm<InstanceSettingsForm>, intl: I18n) -> Result<Redirect, Ructe> { pub fn update_settings(conn: DbConn, admin: Admin, form: LenientForm<InstanceSettingsForm>, intl: I18n) -> Result<Redirect, Ructe> {
form.validate() form.validate()
.map(|_| { .and_then(|_| {
let instance = Instance::get_local(&*conn).expect("instance::update_settings: local instance not found error"); let instance = Instance::get_local(&*conn).expect("instance::update_settings: local instance error");
instance.update(&*conn, instance.update(&*conn,
form.name.clone(), form.name.clone(),
form.open_registrations, form.open_registrations,
form.short_description.clone(), form.short_description.clone(),
form.long_description.clone()); form.long_description.clone()).expect("instance::update_settings: save error");
Redirect::to(uri!(admin)) Ok(Redirect::to(uri!(admin)))
}) })
.map_err(|e| { .or_else(|e| {
let local_inst = Instance::get_local(&*conn).expect("instance::update_settings: local instance not found"); let local_inst = Instance::get_local(&*conn).expect("instance::update_settings: local instance error");
render!(instance::admin( Err(render!(instance::admin(
&(&*conn, &intl.catalog, Some(admin.0)), &(&*conn, &intl.catalog, Some(admin.0)),
local_inst, local_inst,
form.clone(), form.clone(),
e e
)) )))
}) })
} }
#[get("/admin/instances?<page>")] #[get("/admin/instances?<page>")]
pub fn admin_instances(admin: Admin, conn: DbConn, page: Option<Page>, intl: I18n) -> Ructe { pub fn admin_instances(admin: Admin, conn: DbConn, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
let instances = Instance::page(&*conn, page.limits()); let instances = Instance::page(&*conn, page.limits())?;
render!(instance::list( Ok(render!(instance::list(
&(&*conn, &intl.catalog, Some(admin.0)), &(&*conn, &intl.catalog, Some(admin.0)),
Instance::get_local(&*conn).expect("admin_instances: local instance error"), Instance::get_local(&*conn)?,
instances, instances,
page.0, page.0,
Page::total(Instance::count(&*conn) as i32) Page::total(Instance::count(&*conn)? as i32)
)) )))
} }
#[post("/admin/instances/<id>/block")] #[post("/admin/instances/<id>/block")]
pub fn toggle_block(_admin: Admin, conn: DbConn, id: i32) -> Redirect { pub fn toggle_block(_admin: Admin, conn: DbConn, id: i32) -> Result<Redirect, ErrorPage> {
if let Some(inst) = Instance::get(&*conn, id) { if let Ok(inst) = Instance::get(&*conn, id) {
inst.toggle_block(&*conn); inst.toggle_block(&*conn)?;
} }
Redirect::to(uri!(admin_instances: page = _)) Ok(Redirect::to(uri!(admin_instances: page = _)))
} }
#[get("/admin/users?<page>")] #[get("/admin/users?<page>")]
pub fn admin_users(admin: Admin, conn: DbConn, page: Option<Page>, intl: I18n) -> Ructe { pub fn admin_users(admin: Admin, conn: DbConn, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
render!(instance::users( Ok(render!(instance::users(
&(&*conn, &intl.catalog, Some(admin.0)), &(&*conn, &intl.catalog, Some(admin.0)),
User::get_local_page(&*conn, page.limits()), User::get_local_page(&*conn, page.limits())?,
page.0, page.0,
Page::total(User::count_local(&*conn) as i32) Page::total(User::count_local(&*conn)? as i32)
)) )))
} }
#[post("/admin/users/<id>/ban")] #[post("/admin/users/<id>/ban")]
pub fn ban(_admin: Admin, conn: DbConn, id: i32, searcher: Searcher) -> Redirect { pub fn ban(_admin: Admin, conn: DbConn, id: i32, searcher: Searcher) -> Result<Redirect, ErrorPage> {
if let Some(u) = User::get(&*conn, id) { if let Ok(u) = User::get(&*conn, id) {
u.delete(&*conn, &searcher); u.delete(&*conn, &searcher)?;
} }
Redirect::to(uri!(admin_users: page = _)) Ok(Redirect::to(uri!(admin_users: page = _)))
} }
#[post("/inbox", data = "<data>")] #[post("/inbox", data = "<data>")]
@ -200,7 +192,7 @@ pub fn shared_inbox(conn: DbConn, data: SignedJson<serde_json::Value>, headers:
return Err(status::BadRequest(Some("Invalid signature"))); return Err(status::BadRequest(Some("Invalid signature")));
} }
if Instance::is_blocked(&*conn, actor_id) { if Instance::is_blocked(&*conn, actor_id).map_err(|_| status::BadRequest(Some("Can't tell if instance is blocked")))? {
return Ok(String::new()); return Ok(String::new());
} }
let instance = Instance::get_local(&*conn).expect("instance::shared_inbox: local instance not found error"); let instance = Instance::get_local(&*conn).expect("instance::shared_inbox: local instance not found error");
@ -214,8 +206,8 @@ pub fn shared_inbox(conn: DbConn, data: SignedJson<serde_json::Value>, headers:
} }
#[get("/nodeinfo")] #[get("/nodeinfo")]
pub fn nodeinfo(conn: DbConn) -> Json<serde_json::Value> { pub fn nodeinfo(conn: DbConn) -> Result<Json<serde_json::Value>, ErrorPage> {
Json(json!({ Ok(Json(json!({
"version": "2.0", "version": "2.0",
"software": { "software": {
"name": "Plume", "name": "Plume",
@ -229,31 +221,31 @@ pub fn nodeinfo(conn: DbConn) -> Json<serde_json::Value> {
"openRegistrations": true, "openRegistrations": true,
"usage": { "usage": {
"users": { "users": {
"total": User::count_local(&*conn) "total": User::count_local(&*conn)?
}, },
"localPosts": Post::count_local(&*conn), "localPosts": Post::count_local(&*conn)?,
"localComments": Comment::count_local(&*conn) "localComments": Comment::count_local(&*conn)?
}, },
"metadata": {} "metadata": {}
})) })))
} }
#[get("/about")] #[get("/about")]
pub fn about(user: Option<User>, conn: DbConn, intl: I18n) -> Ructe { pub fn about(user: Option<User>, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
render!(instance::about( Ok(render!(instance::about(
&(&*conn, &intl.catalog, user), &(&*conn, &intl.catalog, user),
Instance::get_local(&*conn).expect("Local instance not found"), Instance::get_local(&*conn)?,
Instance::get_local(&*conn).expect("Local instance not found").main_admin(&*conn), Instance::get_local(&*conn)?.main_admin(&*conn)?,
User::count_local(&*conn), User::count_local(&*conn)?,
Post::count_local(&*conn), Post::count_local(&*conn)?,
Instance::count(&*conn) - 1 Instance::count(&*conn)? - 1
)) )))
} }
#[get("/manifest.json")] #[get("/manifest.json")]
pub fn web_manifest(conn: DbConn) -> Json<serde_json::Value> { pub fn web_manifest(conn: DbConn) -> Result<Json<serde_json::Value>, ErrorPage> {
let instance = Instance::get_local(&*conn).expect("instance::web_manifest: local instance not found error"); let instance = Instance::get_local(&*conn)?;
Json(json!({ Ok(Json(json!({
"name": &instance.name, "name": &instance.name,
"description": &instance.short_description, "description": &instance.short_description,
"start_url": String::from("/"), "start_url": String::from("/"),
@ -306,5 +298,5 @@ pub fn web_manifest(conn: DbConn) -> Json<serde_json::Value> {
"src": "/static/icons/trwnh/feather/plumeFeather.svg" "src": "/static/icons/trwnh/feather/plumeFeather.svg"
} }
] ]
})) })))
} }

View File

@ -11,27 +11,28 @@ use plume_models::{
users::User users::User
}; };
use Worker; use Worker;
use routes::errors::ErrorPage;
#[post("/~/<blog>/<slug>/like")] #[post("/~/<blog>/<slug>/like")]
pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Option<Redirect> { pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Result<Redirect, ErrorPage> {
let b = Blog::find_by_fqn(&*conn, &blog)?; let b = Blog::find_by_fqn(&*conn, &blog)?;
let post = Post::find_by_slug(&*conn, &slug, b.id)?; let post = Post::find_by_slug(&*conn, &slug, b.id)?;
if !user.has_liked(&*conn, &post) { if !user.has_liked(&*conn, &post)? {
let like = likes::Like::insert(&*conn, likes::NewLike::new(&post ,&user)); let like = likes::Like::insert(&*conn, likes::NewLike::new(&post ,&user))?;
like.notify(&*conn); like.notify(&*conn)?;
let dest = User::one_by_instance(&*conn); let dest = User::one_by_instance(&*conn)?;
let act = like.to_activity(&*conn); let act = like.to_activity(&*conn)?;
worker.execute(move || broadcast(&user, act, dest)); worker.execute(move || broadcast(&user, act, dest));
} else { } else {
let like = likes::Like::find_by_user_on_post(&*conn, user.id, post.id).expect("likes::create: like exist but not found error"); let like = likes::Like::find_by_user_on_post(&*conn, user.id, post.id)?;
let delete_act = like.delete(&*conn); let delete_act = like.delete(&*conn)?;
let dest = User::one_by_instance(&*conn); let dest = User::one_by_instance(&*conn)?;
worker.execute(move || broadcast(&user, delete_act, dest)); worker.execute(move || broadcast(&user, delete_act, dest));
} }
Some(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _))) Ok(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _)))
} }
#[post("/~/<blog>/<slug>/like", rank = 2)] #[post("/~/<blog>/<slug>/like", rank = 2)]

View File

@ -5,14 +5,15 @@ use rocket_i18n::I18n;
use std::fs; use std::fs;
use plume_models::{db_conn::DbConn, medias::*, users::User}; use plume_models::{db_conn::DbConn, medias::*, users::User};
use template_utils::Ructe; use template_utils::Ructe;
use routes::errors::ErrorPage;
#[get("/medias")] #[get("/medias")]
pub fn list(user: User, conn: DbConn, intl: I18n) -> Ructe { pub fn list(user: User, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
let medias = Media::for_user(&*conn, user.id); let medias = Media::for_user(&*conn, user.id)?;
render!(medias::index( Ok(render!(medias::index(
&(&*conn, &intl.catalog, Some(user)), &(&*conn, &intl.catalog, Some(user)),
medias medias
)) )))
} }
#[get("/medias/new")] #[get("/medias/new")]
@ -39,69 +40,65 @@ pub fn upload(user: User, data: Data, ct: &ContentType, conn: DbConn) -> Result<
let dest = format!("static/media/{}.{}", GUID::rand().to_string(), ext); let dest = format!("static/media/{}.{}", GUID::rand().to_string(), ext);
match fields[&"file".to_string()][0].data { match fields[&"file".to_string()][0].data {
SavedData::Bytes(ref bytes) => fs::write(&dest, bytes).expect("media::upload: Couldn't save upload"), SavedData::Bytes(ref bytes) => fs::write(&dest, bytes).map_err(|_| status::BadRequest(Some("Couldn't save upload")))?,
SavedData::File(ref path, _) => {fs::copy(path, &dest).expect("media::upload: Couldn't copy upload");}, SavedData::File(ref path, _) => {fs::copy(path, &dest).map_err(|_| status::BadRequest(Some("Couldn't copy upload")))?;},
_ => { _ => {
println!("not a file");
return Ok(Redirect::to(uri!(new))); return Ok(Redirect::to(uri!(new)));
} }
} }
let has_cw = !read(&fields[&"cw".to_string()][0].data).is_empty(); let has_cw = !read(&fields[&"cw".to_string()][0].data).map(|cw| cw.is_empty()).unwrap_or(false);
let media = Media::insert(&*conn, NewMedia { let media = Media::insert(&*conn, NewMedia {
file_path: dest, file_path: dest,
alt_text: read(&fields[&"alt".to_string()][0].data), alt_text: read(&fields[&"alt".to_string()][0].data)?,
is_remote: false, is_remote: false,
remote_url: None, remote_url: None,
sensitive: has_cw, sensitive: has_cw,
content_warning: if has_cw { content_warning: if has_cw {
Some(read(&fields[&"cw".to_string()][0].data)) Some(read(&fields[&"cw".to_string()][0].data)?)
} else { } else {
None None
}, },
owner_id: user.id owner_id: user.id
}); }).map_err(|_| status::BadRequest(Some("Error while saving media")))?;
println!("ok");
Ok(Redirect::to(uri!(details: id = media.id))) Ok(Redirect::to(uri!(details: id = media.id)))
}, },
SaveResult::Partial(_, _) | SaveResult::Error(_) => { SaveResult::Partial(_, _) | SaveResult::Error(_) => {
println!("partial err");
Ok(Redirect::to(uri!(new))) Ok(Redirect::to(uri!(new)))
} }
} }
} else { } else {
println!("not form data");
Ok(Redirect::to(uri!(new))) Ok(Redirect::to(uri!(new)))
} }
} }
fn read(data: &SavedData) -> String { fn read(data: &SavedData) -> Result<String, status::BadRequest<&'static str>> {
if let SavedData::Text(s) = data { if let SavedData::Text(s) = data {
s.clone() Ok(s.clone())
} else { } else {
panic!("Field is not a string") Err(status::BadRequest(Some("Error while reading data")))
} }
} }
#[get("/medias/<id>")] #[get("/medias/<id>")]
pub fn details(id: i32, user: User, conn: DbConn, intl: I18n) -> Ructe { pub fn details(id: i32, user: User, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
let media = Media::get(&*conn, id).expect("Media::details: media not found"); let media = Media::get(&*conn, id)?;
render!(medias::details( Ok(render!(medias::details(
&(&*conn, &intl.catalog, Some(user)), &(&*conn, &intl.catalog, Some(user)),
media media
)) )))
} }
#[post("/medias/<id>/delete")] #[post("/medias/<id>/delete")]
pub fn delete(id: i32, _user: User, conn: DbConn) -> Option<Redirect> { pub fn delete(id: i32, _user: User, conn: DbConn) -> Result<Redirect, ErrorPage> {
let media = Media::get(&*conn, id)?; let media = Media::get(&*conn, id)?;
media.delete(&*conn); media.delete(&*conn)?;
Some(Redirect::to(uri!(list))) Ok(Redirect::to(uri!(list)))
} }
#[post("/medias/<id>/avatar")] #[post("/medias/<id>/avatar")]
pub fn set_avatar(id: i32, user: User, conn: DbConn) -> Option<Redirect> { pub fn set_avatar(id: i32, user: User, conn: DbConn) -> Result<Redirect, ErrorPage> {
let media = Media::get(&*conn, id)?; let media = Media::get(&*conn, id)?;
user.set_avatar(&*conn, media.id); user.set_avatar(&*conn, media.id)?;
Some(Redirect::to(uri!(details: id = id))) Ok(Redirect::to(uri!(details: id = id)))
} }

View File

@ -60,7 +60,7 @@ pub fn post_to_atom(post: Post, conn: &Connection) -> Entry {
.src(post.ap_url.clone()) .src(post.ap_url.clone())
.content_type("html".to_string()) .content_type("html".to_string())
.build().expect("Atom feed: content error")) .build().expect("Atom feed: content error"))
.authors(post.get_authors(&*conn) .authors(post.get_authors(&*conn).expect("Atom feed: author error")
.into_iter() .into_iter()
.map(|a| PersonBuilder::default() .map(|a| PersonBuilder::default()
.name(a.display_name) .name(a.display_name)

View File

@ -3,18 +3,18 @@ use rocket_i18n::I18n;
use plume_common::utils; use plume_common::utils;
use plume_models::{db_conn::DbConn, notifications::Notification, users::User}; use plume_models::{db_conn::DbConn, notifications::Notification, users::User};
use routes::Page; use routes::{Page, errors::ErrorPage};
use template_utils::Ructe; use template_utils::Ructe;
#[get("/notifications?<page>")] #[get("/notifications?<page>")]
pub fn notifications(conn: DbConn, user: User, page: Option<Page>, intl: I18n) -> Ructe { pub fn notifications(conn: DbConn, user: User, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
render!(notifications::index( Ok(render!(notifications::index(
&(&*conn, &intl.catalog, Some(user.clone())), &(&*conn, &intl.catalog, Some(user.clone())),
Notification::page_for_user(&*conn, &user, page.limits()), Notification::page_for_user(&*conn, &user, page.limits())?,
page.0, page.0,
Page::total(Notification::count_for_user(&*conn, &user) as i32) Page::total(Notification::count_for_user(&*conn, &user)? as i32)
)) )))
} }
#[get("/notifications?<page>", rank = 2)] #[get("/notifications?<page>", rank = 2)]

View File

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

View File

@ -10,29 +10,29 @@ use plume_models::{
reshares::*, reshares::*,
users::User users::User
}; };
use routes::errors::ErrorPage;
use Worker; use Worker;
#[post("/~/<blog>/<slug>/reshare")] #[post("/~/<blog>/<slug>/reshare")]
pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Option<Redirect> { pub fn create(blog: String, slug: String, user: User, conn: DbConn, worker: Worker) -> Result<Redirect, ErrorPage> {
let b = Blog::find_by_fqn(&*conn, &blog)?; let b = Blog::find_by_fqn(&*conn, &blog)?;
let post = Post::find_by_slug(&*conn, &slug, b.id)?; let post = Post::find_by_slug(&*conn, &slug, b.id)?;
if !user.has_reshared(&*conn, &post) { if !user.has_reshared(&*conn, &post)? {
let reshare = Reshare::insert(&*conn, NewReshare::new(&post, &user)); let reshare = Reshare::insert(&*conn, NewReshare::new(&post, &user))?;
reshare.notify(&*conn); reshare.notify(&*conn)?;
let dest = User::one_by_instance(&*conn); let dest = User::one_by_instance(&*conn)?;
let act = reshare.to_activity(&*conn); let act = reshare.to_activity(&*conn)?;
worker.execute(move || broadcast(&user, act, dest)); worker.execute(move || broadcast(&user, act, dest));
} else { } else {
let reshare = Reshare::find_by_user_on_post(&*conn, user.id, post.id) let reshare = Reshare::find_by_user_on_post(&*conn, user.id, post.id)?;
.expect("reshares::create: reshare exist but not found error"); let delete_act = reshare.delete(&*conn)?;
let delete_act = reshare.delete(&*conn); let dest = User::one_by_instance(&*conn)?;
let dest = User::one_by_instance(&*conn);
worker.execute(move || broadcast(&user, delete_act, dest)); worker.execute(move || broadcast(&user, delete_act, dest));
} }
Some(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _))) Ok(Redirect::to(uri!(super::posts::details: blog = blog, slug = slug, responding_to = _)))
} }
#[post("/~/<blog>/<slug>/reshare", rank=1)] #[post("/~/<blog>/<slug>/reshare", rank=1)]

View File

@ -14,6 +14,7 @@ use plume_models::{
users::{User, AUTH_COOKIE} users::{User, AUTH_COOKIE}
}; };
#[get("/login?<m>")] #[get("/login?<m>")]
pub fn new(user: Option<User>, conn: DbConn, m: Option<String>, intl: I18n) -> Ructe { pub fn new(user: Option<User>, conn: DbConn, m: Option<String>, intl: I18n) -> Ructe {
render!(session::login( render!(session::login(
@ -35,30 +36,34 @@ pub struct LoginForm {
#[post("/login", data = "<form>")] #[post("/login", data = "<form>")]
pub fn create(conn: DbConn, form: LenientForm<LoginForm>, flash: Option<FlashMessage>, mut cookies: Cookies, intl: I18n) -> Result<Redirect, Ructe> { pub fn create(conn: DbConn, form: LenientForm<LoginForm>, flash: Option<FlashMessage>, mut cookies: Cookies, intl: I18n) -> Result<Redirect, Ructe> {
let user = User::find_by_email(&*conn, &form.email_or_name) let user = User::find_by_email(&*conn, &form.email_or_name)
.or_else(|| User::find_local(&*conn, &form.email_or_name)); .or_else(|_| User::find_local(&*conn, &form.email_or_name));
let mut errors = match form.validate() { let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(), Ok(_) => ValidationErrors::new(),
Err(e) => e Err(e) => e
}; };
if let Some(user) = user.clone() { let user_id = if let Ok(user) = user {
if !user.auth(&form.password) { if !user.auth(&form.password) {
let mut err = ValidationError::new("invalid_login"); let mut err = ValidationError::new("invalid_login");
err.message = Some(Cow::from("Invalid username or password")); err.message = Some(Cow::from("Invalid username or password"));
errors.add("email_or_name", err) errors.add("email_or_name", err);
user.id.to_string()
} else {
String::new()
} }
} else { } else {
// Fake password verification, only to avoid different login times // Fake password verification, only to avoid different login times
// that could be used to see if an email adress is registered or not // that could be used to see if an email adress is registered or not
User::get(&*conn, 1).map(|u| u.auth(&form.password)); User::get(&*conn, 1).map(|u| u.auth(&form.password)).expect("No user is registered");
let mut err = ValidationError::new("invalid_login"); let mut err = ValidationError::new("invalid_login");
err.message = Some(Cow::from("Invalid username or password")); err.message = Some(Cow::from("Invalid username or password"));
errors.add("email_or_name", err) errors.add("email_or_name", err);
} String::new()
};
if errors.is_empty() { if errors.is_empty() {
cookies.add_private(Cookie::build(AUTH_COOKIE, user.unwrap().id.to_string()) cookies.add_private(Cookie::build(AUTH_COOKIE, user_id)
.same_site(SameSite::Lax) .same_site(SameSite::Lax)
.finish()); .finish());

View File

@ -5,18 +5,18 @@ use plume_models::{
posts::Post, posts::Post,
users::User, users::User,
}; };
use routes::Page; use routes::{Page, errors::ErrorPage};
use template_utils::Ructe; use template_utils::Ructe;
#[get("/tag/<name>?<page>")] #[get("/tag/<name>?<page>")]
pub fn tag(user: Option<User>, conn: DbConn, name: String, page: Option<Page>, intl: I18n) -> Ructe { pub fn tag(user: Option<User>, conn: DbConn, name: String, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
let posts = Post::list_by_tag(&*conn, name.clone(), page.limits()); let posts = Post::list_by_tag(&*conn, name.clone(), page.limits())?;
render!(tags::index( Ok(render!(tags::index(
&(&*conn, &intl.catalog, user), &(&*conn, &intl.catalog, user),
name.clone(), name.clone(),
posts, posts,
page.0, page.0,
Page::total(Post::count_for_tag(&*conn, name) as i32) Page::total(Post::count_for_tag(&*conn, name)? as i32)
)) )))
} }

View File

@ -7,6 +7,7 @@ use rocket::{
}; };
use rocket_i18n::I18n; use rocket_i18n::I18n;
use serde_json; use serde_json;
use std::{borrow::Cow, collections::HashMap};
use validator::{Validate, ValidationError, ValidationErrors}; use validator::{Validate, ValidationError, ValidationErrors};
use inbox::{Inbox, SignedJson}; use inbox::{Inbox, SignedJson};
@ -18,10 +19,11 @@ use plume_common::activity_pub::{
}; };
use plume_common::utils; use plume_common::utils;
use plume_models::{ use plume_models::{
Error,
blogs::Blog, db_conn::DbConn, follows, headers::Headers, instance::Instance, posts::{LicensedArticle, Post}, blogs::Blog, db_conn::DbConn, follows, headers::Headers, instance::Instance, posts::{LicensedArticle, Post},
reshares::Reshare, users::*, reshares::Reshare, users::*,
}; };
use routes::Page; use routes::{Page, errors::ErrorPage};
use template_utils::Ructe; use template_utils::Ructe;
use Worker; use Worker;
use Searcher; use Searcher;
@ -45,24 +47,24 @@ pub fn details(
update_conn: DbConn, update_conn: DbConn,
intl: I18n, intl: I18n,
searcher: Searcher, searcher: Searcher,
) -> Result<Ructe, Ructe> { ) -> Result<Ructe, ErrorPage> {
let user = User::find_by_fqn(&*conn, &name).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, account.clone()))))?; let user = User::find_by_fqn(&*conn, &name)?;
let recents = Post::get_recents_for_author(&*conn, &user, 6); let recents = Post::get_recents_for_author(&*conn, &user, 6)?;
let reshares = Reshare::get_recents_for_author(&*conn, &user, 6); let reshares = Reshare::get_recents_for_author(&*conn, &user, 6)?;
if !user.get_instance(&*conn).local { if !user.get_instance(&*conn)?.local {
// Fetch new articles // Fetch new articles
let user_clone = user.clone(); let user_clone = user.clone();
let searcher = searcher.clone(); let searcher = searcher.clone();
worker.execute(move || { worker.execute(move || {
for create_act in user_clone.fetch_outbox::<Create>() { for create_act in user_clone.fetch_outbox::<Create>().expect("Remote user: outbox couldn't be fetched") {
match create_act.create_props.object_object::<LicensedArticle>() { match create_act.create_props.object_object::<LicensedArticle>() {
Ok(article) => { Ok(article) => {
Post::from_activity( Post::from_activity(
&(&*fetch_articles_conn, &searcher), &(&*fetch_articles_conn, &searcher),
article, article,
user_clone.clone().into_id(), user_clone.clone().into_id(),
); ).expect("Article from remote user couldn't be saved");
println!("Fetched article from remote user"); println!("Fetched article from remote user");
} }
Err(e) => { Err(e) => {
@ -75,10 +77,10 @@ pub fn details(
// Fetch followers // Fetch followers
let user_clone = user.clone(); let user_clone = user.clone();
worker.execute(move || { worker.execute(move || {
for user_id in user_clone.fetch_followers_ids() { for user_id in user_clone.fetch_followers_ids().expect("Remote user: fetching followers error") {
let follower = let follower =
User::find_by_ap_url(&*fetch_followers_conn, &user_id) User::find_by_ap_url(&*fetch_followers_conn, &user_id)
.unwrap_or_else(|| { .unwrap_or_else(|_| {
User::fetch_from_url(&*fetch_followers_conn, &user_id) User::fetch_from_url(&*fetch_followers_conn, &user_id)
.expect("user::details: Couldn't fetch follower") .expect("user::details: Couldn't fetch follower")
}); });
@ -89,7 +91,7 @@ pub fn details(
following_id: user_clone.id, following_id: user_clone.id,
ap_url: format!("{}/follow/{}", follower.ap_url, user_clone.ap_url), ap_url: format!("{}/follow/{}", follower.ap_url, user_clone.ap_url),
}, },
); ).expect("Couldn't save follower for remote user");
} }
}); });
@ -97,7 +99,7 @@ pub fn details(
let user_clone = user.clone(); let user_clone = user.clone();
if user.needs_update() { if user.needs_update() {
worker.execute(move || { worker.execute(move || {
user_clone.refetch(&*update_conn); user_clone.refetch(&*update_conn).expect("Couldn't update user info");
}); });
} }
} }
@ -105,22 +107,22 @@ pub fn details(
Ok(render!(users::details( Ok(render!(users::details(
&(&*conn, &intl.catalog, account.clone()), &(&*conn, &intl.catalog, account.clone()),
user.clone(), user.clone(),
account.map(|x| x.is_following(&*conn, user.id)).unwrap_or(false), account.and_then(|x| x.is_following(&*conn, user.id).ok()).unwrap_or(false),
user.instance_id != Instance::local_id(&*conn), user.instance_id != Instance::get_local(&*conn)?.id,
user.get_instance(&*conn).public_domain, user.get_instance(&*conn)?.public_domain,
recents, recents,
reshares.into_iter().map(|r| r.get_post(&*conn).expect("user::details: Reshared post error")).collect() reshares.into_iter().filter_map(|r| r.get_post(&*conn).ok()).collect()
))) )))
} }
#[get("/dashboard")] #[get("/dashboard")]
pub fn dashboard(user: User, conn: DbConn, intl: I18n) -> Ructe { pub fn dashboard(user: User, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
let blogs = Blog::find_for_author(&*conn, &user); let blogs = Blog::find_for_author(&*conn, &user)?;
render!(users::dashboard( Ok(render!(users::dashboard(
&(&*conn, &intl.catalog, Some(user.clone())), &(&*conn, &intl.catalog, Some(user.clone())),
blogs, blogs,
Post::drafts_by_author(&*conn, &user) Post::drafts_by_author(&*conn, &user)?
)) )))
} }
#[get("/dashboard", rank = 2)] #[get("/dashboard", rank = 2)]
@ -132,10 +134,10 @@ pub fn dashboard_auth(i18n: I18n) -> Flash<Redirect> {
} }
#[post("/@/<name>/follow")] #[post("/@/<name>/follow")]
pub fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Option<Redirect> { pub fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Result<Redirect, ErrorPage> {
let target = User::find_by_fqn(&*conn, &name)?; let target = User::find_by_fqn(&*conn, &name)?;
if let Some(follow) = follows::Follow::find(&*conn, user.id, target.id) { if let Ok(follow) = follows::Follow::find(&*conn, user.id, target.id) {
let delete_act = follow.delete(&*conn); let delete_act = follow.delete(&*conn)?;
worker.execute(move || { worker.execute(move || {
broadcast(&user, delete_act, vec![target]) broadcast(&user, delete_act, vec![target])
}); });
@ -147,13 +149,13 @@ pub fn follow(name: String, conn: DbConn, user: User, worker: Worker) -> Option<
following_id: target.id, following_id: target.id,
ap_url: format!("{}/follow/{}", user.ap_url, target.ap_url), ap_url: format!("{}/follow/{}", user.ap_url, target.ap_url),
}, },
); )?;
f.notify(&*conn); f.notify(&*conn)?;
let act = f.to_activity(&*conn); let act = f.to_activity(&*conn)?;
worker.execute(move || broadcast(&user, act, vec![target])); worker.execute(move || broadcast(&user, act, vec![target]));
} }
Some(Redirect::to(uri!(details: name = name))) Ok(Redirect::to(uri!(details: name = name)))
} }
#[post("/@/<name>/follow", rank = 2)] #[post("/@/<name>/follow", rank = 2)]
@ -165,18 +167,18 @@ pub fn follow_auth(name: String, i18n: I18n) -> Flash<Redirect> {
} }
#[get("/@/<name>/followers?<page>", rank = 2)] #[get("/@/<name>/followers?<page>", rank = 2)]
pub fn followers(name: String, conn: DbConn, account: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, Ructe> { pub fn followers(name: String, conn: DbConn, account: Option<User>, page: Option<Page>, intl: I18n) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
let user = User::find_by_fqn(&*conn, &name).ok_or_else(|| render!(errors::not_found(&(&*conn, &intl.catalog, account.clone()))))?; let user = User::find_by_fqn(&*conn, &name)?;
let followers_count = user.count_followers(&*conn); let followers_count = user.count_followers(&*conn)?;
Ok(render!(users::followers( Ok(render!(users::followers(
&(&*conn, &intl.catalog, account.clone()), &(&*conn, &intl.catalog, account.clone()),
user.clone(), user.clone(),
account.map(|x| x.is_following(&*conn, user.id)).unwrap_or(false), account.and_then(|x| x.is_following(&*conn, user.id).ok()).unwrap_or(false),
user.instance_id != Instance::local_id(&*conn), user.instance_id != Instance::get_local(&*conn)?.id,
user.get_instance(&*conn).public_domain, user.get_instance(&*conn)?.public_domain,
user.get_followers_page(&*conn, page.limits()), user.get_followers_page(&*conn, page.limits())?,
page.0, page.0,
Page::total(followers_count as i32) Page::total(followers_count as i32)
))) )))
@ -188,24 +190,24 @@ pub fn activity_details(
conn: DbConn, conn: DbConn,
_ap: ApRequest, _ap: ApRequest,
) -> Option<ActivityStream<CustomPerson>> { ) -> Option<ActivityStream<CustomPerson>> {
let user = User::find_local(&*conn, &name)?; let user = User::find_local(&*conn, &name).ok()?;
Some(ActivityStream::new(user.to_activity(&*conn))) Some(ActivityStream::new(user.to_activity(&*conn).ok()?))
} }
#[get("/users/new")] #[get("/users/new")]
pub fn new(user: Option<User>, conn: DbConn, intl: I18n) -> Ructe { pub fn new(user: Option<User>, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
render!(users::new( Ok(render!(users::new(
&(&*conn, &intl.catalog, user), &(&*conn, &intl.catalog, user),
Instance::get_local(&*conn).map(|i| i.open_registrations).unwrap_or(true), Instance::get_local(&*conn)?.open_registrations,
&NewUserForm::default(), &NewUserForm::default(),
ValidationErrors::default() ValidationErrors::default()
)) )))
} }
#[get("/@/<name>/edit")] #[get("/@/<name>/edit")]
pub fn edit(name: String, user: User, conn: DbConn, intl: I18n) -> Option<Ructe> { pub fn edit(name: String, user: User, conn: DbConn, intl: I18n) -> Result<Ructe, ErrorPage> {
if user.username == name && !name.contains('@') { if user.username == name && !name.contains('@') {
Some(render!(users::edit( Ok(render!(users::edit(
&(&*conn, &intl.catalog, Some(user.clone())), &(&*conn, &intl.catalog, Some(user.clone())),
UpdateUserForm { UpdateUserForm {
display_name: user.display_name.clone(), display_name: user.display_name.clone(),
@ -215,7 +217,7 @@ pub fn edit(name: String, user: User, conn: DbConn, intl: I18n) -> Option<Ructe>
ValidationErrors::default() ValidationErrors::default()
))) )))
} else { } else {
None Err(Error::Unauthorized)?
} }
} }
@ -235,29 +237,29 @@ pub struct UpdateUserForm {
} }
#[put("/@/<_name>/edit", data = "<form>")] #[put("/@/<_name>/edit", data = "<form>")]
pub fn update(_name: String, conn: DbConn, user: User, form: LenientForm<UpdateUserForm>) -> Redirect { pub fn update(_name: String, conn: DbConn, user: User, form: LenientForm<UpdateUserForm>) -> Result<Redirect, ErrorPage> {
user.update( user.update(
&*conn, &*conn,
if !form.display_name.is_empty() { form.display_name.clone() } else { user.display_name.clone() }, if !form.display_name.is_empty() { form.display_name.clone() } else { user.display_name.clone() },
if !form.email.is_empty() { form.email.clone() } else { user.email.clone().unwrap_or_default() }, if !form.email.is_empty() { form.email.clone() } else { user.email.clone().unwrap_or_default() },
if !form.summary.is_empty() { form.summary.clone() } else { user.summary.to_string() }, if !form.summary.is_empty() { form.summary.clone() } else { user.summary.to_string() },
); )?;
Redirect::to(uri!(me)) Ok(Redirect::to(uri!(me)))
} }
#[post("/@/<name>/delete")] #[post("/@/<name>/delete")]
pub fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, searcher: Searcher) -> Option<Redirect> { pub fn delete(name: String, conn: DbConn, user: User, mut cookies: Cookies, searcher: Searcher) -> Result<Redirect, ErrorPage> {
let account = User::find_by_fqn(&*conn, &name)?; let account = User::find_by_fqn(&*conn, &name)?;
if user.id == account.id { if user.id == account.id {
account.delete(&*conn, &searcher); account.delete(&*conn, &searcher)?;
if let Some(cookie) = cookies.get_private(AUTH_COOKIE) { if let Some(cookie) = cookies.get_private(AUTH_COOKIE) {
cookies.remove_private(cookie); cookies.remove_private(cookie);
} }
Some(Redirect::to(uri!(super::instance::index))) Ok(Redirect::to(uri!(super::instance::index)))
} else { } else {
Some(Redirect::to(uri!(edit: name = name))) Ok(Redirect::to(uri!(edit: name = name)))
} }
} }
@ -307,6 +309,16 @@ pub fn validate_username(username: &str) -> Result<(), ValidationError> {
} }
} }
fn to_validation(_: Error) -> ValidationErrors {
let mut errors = ValidationErrors::new();
errors.add("", ValidationError {
code: Cow::from("server_error"),
message: Some(Cow::from("An unknown error occured")),
params: HashMap::new()
});
errors
}
#[post("/users/new", data = "<form>")] #[post("/users/new", data = "<form>")]
pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Result<Redirect, Ructe> { pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Result<Redirect, Ructe> {
if !Instance::get_local(&*conn) if !Instance::get_local(&*conn)
@ -320,7 +332,7 @@ pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Resul
form.username = form.username.trim().to_owned(); form.username = form.username.trim().to_owned();
form.email = form.email.trim().to_owned(); form.email = form.email.trim().to_owned();
form.validate() form.validate()
.map(|_| { .and_then(|_| {
NewUser::new_local( NewUser::new_local(
&*conn, &*conn,
form.username.to_string(), form.username.to_string(),
@ -328,9 +340,9 @@ pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Resul
false, false,
"", "",
form.email.to_string(), form.email.to_string(),
User::hash_pass(&form.password), User::hash_pass(&form.password).map_err(to_validation)?,
).update_boxes(&*conn); ).and_then(|u| u.update_boxes(&*conn)).map_err(to_validation)?;
Redirect::to(uri!(super::session::new: m = _)) Ok(Redirect::to(uri!(super::session::new: m = _)))
}) })
.map_err(|err| { .map_err(|err| {
render!(users::new( render!(users::new(
@ -344,8 +356,8 @@ pub fn create(conn: DbConn, form: LenientForm<NewUserForm>, intl: I18n) -> Resul
#[get("/@/<name>/outbox")] #[get("/@/<name>/outbox")]
pub fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> { pub fn outbox(name: String, conn: DbConn) -> Option<ActivityStream<OrderedCollection>> {
let user = User::find_local(&*conn, &name)?; let user = User::find_local(&*conn, &name).ok()?;
Some(user.outbox(&*conn)) user.outbox(&*conn).ok()
} }
#[post("/@/<name>/inbox", data = "<data>")] #[post("/@/<name>/inbox", data = "<data>")]
@ -356,7 +368,7 @@ pub fn inbox(
headers: Headers, headers: Headers,
searcher: Searcher, searcher: Searcher,
) -> Result<String, Option<status::BadRequest<&'static str>>> { ) -> Result<String, Option<status::BadRequest<&'static str>>> {
let user = User::find_local(&*conn, &name).ok_or(None)?; let user = User::find_local(&*conn, &name).map_err(|_| None)?;
let act = data.1.into_inner(); let act = data.1.into_inner();
let activity = act.clone(); let activity = act.clone();
@ -378,7 +390,7 @@ pub fn inbox(
return Err(Some(status::BadRequest(Some("Invalid signature")))); return Err(Some(status::BadRequest(Some("Invalid signature"))));
} }
if Instance::is_blocked(&*conn, actor_id) { if Instance::is_blocked(&*conn, actor_id).map_err(|_| None)? {
return Ok(String::new()); return Ok(String::new());
} }
Ok(match user.received(&*conn, &searcher, act) { Ok(match user.received(&*conn, &searcher, act) {
@ -396,36 +408,33 @@ pub fn ap_followers(
conn: DbConn, conn: DbConn,
_ap: ApRequest, _ap: ApRequest,
) -> Option<ActivityStream<OrderedCollection>> { ) -> Option<ActivityStream<OrderedCollection>> {
let user = User::find_local(&*conn, &name)?; let user = User::find_local(&*conn, &name).ok()?;
let followers = user let followers = user
.get_followers(&*conn) .get_followers(&*conn).ok()?
.into_iter() .into_iter()
.map(|f| Id::new(f.ap_url)) .map(|f| Id::new(f.ap_url))
.collect::<Vec<Id>>(); .collect::<Vec<Id>>();
let mut coll = OrderedCollection::default(); let mut coll = OrderedCollection::default();
coll.object_props coll.object_props
.set_id_string(user.followers_endpoint) .set_id_string(user.followers_endpoint).ok()?;
.expect("user::ap_followers: id error");
coll.collection_props coll.collection_props
.set_total_items_u64(followers.len() as u64) .set_total_items_u64(followers.len() as u64).ok()?;
.expect("user::ap_followers: totalItems error");
coll.collection_props coll.collection_props
.set_items_link_vec(followers) .set_items_link_vec(followers).ok()?;
.expect("user::ap_followers items error");
Some(ActivityStream::new(coll)) Some(ActivityStream::new(coll))
} }
#[get("/@/<name>/atom.xml")] #[get("/@/<name>/atom.xml")]
pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> { pub fn atom_feed(name: String, conn: DbConn) -> Option<Content<String>> {
let author = User::find_by_fqn(&*conn, &name)?; let author = User::find_by_fqn(&*conn, &name).ok()?;
let feed = FeedBuilder::default() let feed = FeedBuilder::default()
.title(author.display_name.clone()) .title(author.display_name.clone())
.id(Instance::get_local(&*conn) .id(Instance::get_local(&*conn)
.unwrap() .unwrap()
.compute_box("~", &name, "atom.xml")) .compute_box("~", &name, "atom.xml"))
.entries( .entries(
Post::get_recents_for_author(&*conn, &author, 15) Post::get_recents_for_author(&*conn, &author, 15).ok()?
.into_iter() .into_iter()
.map(|p| super::post_to_atom(p, &*conn)) .map(|p| super::post_to_atom(p, &*conn))
.collect::<Vec<Entry>>(), .collect::<Vec<Entry>>(),

View File

@ -35,13 +35,11 @@ impl Resolver<DbConn> for WebfingerResolver {
} }
fn find(acct: String, conn: DbConn) -> Result<Webfinger, ResolverError> { fn find(acct: String, conn: DbConn) -> Result<Webfinger, ResolverError> {
match User::find_local(&*conn, &acct) { User::find_local(&*conn, &acct)
Some(usr) => Ok(usr.webfinger(&*conn)), .and_then(|usr| usr.webfinger(&*conn))
None => match Blog::find_local(&*conn, &acct) { .or_else(|_| Blog::find_local(&*conn, &acct)
Some(blog) => Ok(blog.webfinger(&*conn)), .and_then(|blog| blog.webfinger(&*conn))
None => Err(ResolverError::NotFound) .or(Err(ResolverError::NotFound)))
}
}
} }
} }

View File

@ -1,4 +1,5 @@
@use plume_models::medias::{Media, MediaCategory}; @use plume_models::medias::{Media, MediaCategory};
@use plume_models::safe_string::SafeString;
@use templates::base; @use templates::base;
@use template_utils::*; @use template_utils::*;
@use routes::*; @use routes::*;
@ -13,7 +14,7 @@
<section> <section>
<figure class="media"> <figure class="media">
@Html(media.html(ctx.0)) @Html(media.html(ctx.0).unwrap_or(SafeString::new("")))
<figcaption>@media.alt_text</figcaption> <figcaption>@media.alt_text</figcaption>
</figure> </figure>
<div> <div>
@ -21,7 +22,7 @@
@i18n!(ctx.1, "Markdown syntax") @i18n!(ctx.1, "Markdown syntax")
<small>@i18n!(ctx.1, "Copy it into your articles, to insert this media:")</small> <small>@i18n!(ctx.1, "Copy it into your articles, to insert this media:")</small>
</p> </p>
<code>@media.markdown(ctx.0)</code> <code>@media.markdown(ctx.0).unwrap_or(SafeString::new(""))</code>
</div> </div>
<div> <div>
@if media.category() == MediaCategory::Image { @if media.category() == MediaCategory::Image {

View File

@ -1,4 +1,5 @@
@use plume_models::medias::Media; @use plume_models::medias::Media;
@use plume_models::safe_string::SafeString;
@use templates::base; @use templates::base;
@use template_utils::*; @use template_utils::*;
@use routes::*; @use routes::*;
@ -18,7 +19,7 @@
<div class="list"> <div class="list">
@for media in medias { @for media in medias {
<div class="card flex"> <div class="card flex">
@Html(media.preview_html(ctx.0)) @Html(media.preview_html(ctx.0).unwrap_or(SafeString::new("")))
<main class="grow"> <main class="grow">
<p><a href="@uri!(medias::details: id = media.id)">@media.alt_text</a></p> <p><a href="@uri!(medias::details: id = media.id)">@media.alt_text</a></p>
</main> </main>

View File

@ -15,14 +15,14 @@
<h3> <h3>
@if let Some(url) = notification.get_url(ctx.0) { @if let Some(url) = notification.get_url(ctx.0) {
<a href="@url"> <a href="@url">
@i18n!(ctx.1, notification.get_message(); notification.get_actor(ctx.0).name(ctx.0)) @i18n!(ctx.1, notification.get_message(); notification.get_actor(ctx.0).unwrap().name(ctx.0))
</a> </a>
} else { } else {
@i18n!(ctx.1, notification.get_message(); notification.get_actor(ctx.0).name(ctx.0)) @i18n!(ctx.1, notification.get_message(); notification.get_actor(ctx.0).unwrap().name(ctx.0))
} }
</h3> </h3>
@if let Some(post) = notification.get_post(ctx.0) { @if let Some(post) = notification.get_post(ctx.0) {
<p><a href="@post.url(ctx.0)">@post.title</a></p> <p><a href="@post.url(ctx.0).unwrap_or_default()">@post.title</a></p>
} }
</main> </main>
<p><small>@notification.creation_date.format("%B %e, %H:%M")</small></p> <p><small>@notification.creation_date.format("%B %e, %H:%M")</small></p>

View File

@ -5,7 +5,7 @@
@(ctx: BaseContext, comment_tree: &CommentTree, in_reply_to: Option<&str>, blog: &str, slug: &str) @(ctx: BaseContext, comment_tree: &CommentTree, in_reply_to: Option<&str>, blog: &str, slug: &str)
@if let Some(ref comm) = Some(&comment_tree.comment) { @if let Some(ref comm) = Some(&comment_tree.comment) {
@if let Some(author) = Some(comm.get_author(ctx.0)) { @if let Some(author) = comm.get_author(ctx.0).ok() {
<div class="comment u-comment h-cite" id="comment-@comm.id"> <div class="comment u-comment h-cite" id="comment-@comm.id">
<a class="author u-author h-card" href="@uri!(user::details: name = author.get_fqn(ctx.0))"> <a class="author u-author h-card" href="@uri!(user::details: name = author.get_fqn(ctx.0))">
@avatar(ctx.0, &author, Size::Small, true, ctx.1) @avatar(ctx.0, &author, Size::Small, true, ctx.1)

View File

@ -9,7 +9,7 @@
<div class="cover" style="background-image: url('@Html(article.cover_url(ctx.0).unwrap_or_default())')"></div> <div class="cover" style="background-image: url('@Html(article.cover_url(ctx.0).unwrap_or_default())')"></div>
} }
<h3 class="p-name"> <h3 class="p-name">
<a class="u-url" href="@uri!(posts::details: blog = article.get_blog(ctx.0).get_fqn(ctx.0), slug = &article.slug, responding_to = _)"> <a class="u-url" href="@uri!(posts::details: blog = article.get_blog(ctx.0).unwrap().get_fqn(ctx.0), slug = &article.slug, responding_to = _)">
@article.title @article.title
</a> </a>
</h3> </h3>
@ -19,13 +19,13 @@
<p class="author"> <p class="author">
@Html(i18n!(ctx.1, "By {0}"; format!( @Html(i18n!(ctx.1, "By {0}"; format!(
"<a class=\"p-author h-card\" href=\"{}\">{}</a>", "<a class=\"p-author h-card\" href=\"{}\">{}</a>",
uri!(user::details: name = article.get_authors(ctx.0)[0].get_fqn(ctx.0)), uri!(user::details: name = article.get_authors(ctx.0).unwrap_or_default()[0].get_fqn(ctx.0)),
escape(&article.get_authors(ctx.0)[0].name(ctx.0)) escape(&article.get_authors(ctx.0).unwrap_or_default()[0].name(ctx.0))
))) )))
@if article.published { @if article.published {
<span class="dt-published" datetime="@article.creation_date.format("%F %T")">@article.creation_date.format("%B %e, %Y")</span> <span class="dt-published" datetime="@article.creation_date.format("%F %T")">@article.creation_date.format("%B %e, %Y")</span>
} }
<a href="@uri!(blogs::details: name = article.get_blog(ctx.0).get_fqn(ctx.0), page = _)">@article.get_blog(ctx.0).title</a> <a href="@uri!(blogs::details: name = article.get_blog(ctx.0).unwrap().get_fqn(ctx.0), page = _)">@article.get_blog(ctx.0).unwrap().title</a>
@if !article.published { @if !article.published {
⋅ @i18n!(ctx.1, "Draft") ⋅ @i18n!(ctx.1, "Draft")
} }