Run 'cargo fmt' to format code (#489)
This commit is contained in:
committed by
Baptiste Gelez
parent
732f514da7
commit
b945d1f602
@@ -89,13 +89,19 @@ impl<'a, 'r> FromRequest<'a, 'r> for ApiToken {
|
||||
}
|
||||
|
||||
let mut parsed_header = headers[0].split(' ');
|
||||
let auth_type = parsed_header.next()
|
||||
.map_or_else(|| Outcome::Failure((Status::BadRequest, TokenError::NoType)), Outcome::Success)?;
|
||||
let val = parsed_header.next()
|
||||
.map_or_else(|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)), Outcome::Success)?;
|
||||
let auth_type = parsed_header.next().map_or_else(
|
||||
|| Outcome::Failure((Status::BadRequest, TokenError::NoType)),
|
||||
Outcome::Success,
|
||||
)?;
|
||||
let val = parsed_header.next().map_or_else(
|
||||
|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)),
|
||||
Outcome::Success,
|
||||
)?;
|
||||
|
||||
if auth_type == "Bearer" {
|
||||
let conn = request.guard::<DbConn>().map_failure(|_| (Status::InternalServerError, TokenError::DbError))?;
|
||||
let conn = request
|
||||
.guard::<DbConn>()
|
||||
.map_failure(|_| (Status::InternalServerError, TokenError::DbError))?;
|
||||
if let Ok(token) = ApiToken::find_by_value(&*conn, val) {
|
||||
return Outcome::Success(token);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use plume_api::apps::AppEndpoint;
|
||||
use plume_common::utils::random_hex;
|
||||
use schema::apps;
|
||||
use {Connection, Error, Result, ApiResult};
|
||||
use {ApiResult, Connection, Error, Result};
|
||||
|
||||
#[derive(Clone, Queryable)]
|
||||
pub struct App {
|
||||
@@ -52,7 +52,8 @@ impl Provider<Connection> for App {
|
||||
redirect_uri: data.redirect_uri,
|
||||
website: data.website,
|
||||
},
|
||||
).map_err(|_| ApiError::NotFound("Couldn't register app".into()))?;
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Couldn't register app".into()))?;
|
||||
|
||||
Ok(AppEndpoint {
|
||||
id: Some(app.id),
|
||||
|
||||
+157
-185
@@ -26,7 +26,7 @@ use safe_string::SafeString;
|
||||
use schema::blogs;
|
||||
use search::Searcher;
|
||||
use users::User;
|
||||
use {Connection, BASE_URL, USE_HTTPS, Error, Result};
|
||||
use {Connection, Error, Result, BASE_URL, USE_HTTPS};
|
||||
|
||||
pub type CustomGroup = CustomObject<ApSignature, Group>;
|
||||
|
||||
@@ -66,27 +66,15 @@ impl Blog {
|
||||
insert!(blogs, NewBlog, |inserted, conn| {
|
||||
let instance = inserted.get_instance(conn)?;
|
||||
if inserted.outbox_url.is_empty() {
|
||||
inserted.outbox_url = instance.compute_box(
|
||||
BLOG_PREFIX,
|
||||
&inserted.actor_id,
|
||||
"outbox",
|
||||
);
|
||||
inserted.outbox_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "outbox");
|
||||
}
|
||||
|
||||
if inserted.inbox_url.is_empty() {
|
||||
inserted.inbox_url = instance.compute_box(
|
||||
BLOG_PREFIX,
|
||||
&inserted.actor_id,
|
||||
"inbox",
|
||||
);
|
||||
inserted.inbox_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "inbox");
|
||||
}
|
||||
|
||||
if inserted.ap_url.is_empty() {
|
||||
inserted.ap_url = instance.compute_box(
|
||||
BLOG_PREFIX,
|
||||
&inserted.actor_id,
|
||||
"",
|
||||
);
|
||||
inserted.ap_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "");
|
||||
}
|
||||
|
||||
if inserted.fqn.is_empty() {
|
||||
@@ -154,16 +142,12 @@ impl Blog {
|
||||
}
|
||||
|
||||
fn fetch_from_webfinger(conn: &Connection, acct: &str) -> Result<Blog> {
|
||||
resolve(acct.to_owned(), *USE_HTTPS)?.links
|
||||
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,
|
||||
&l.href?
|
||||
)
|
||||
})
|
||||
.and_then(|l| Blog::fetch_from_url(conn, &l.href?))
|
||||
}
|
||||
|
||||
fn fetch_from_url(conn: &Connection, url: &str) -> Result<Blog> {
|
||||
@@ -181,20 +165,14 @@ impl Blog {
|
||||
.send()?;
|
||||
|
||||
let text = &res.text()?;
|
||||
let ap_sign: ApSignature =
|
||||
serde_json::from_str(text)?;
|
||||
let mut json: CustomGroup =
|
||||
serde_json::from_str(text)?;
|
||||
let ap_sign: ApSignature = serde_json::from_str(text)?;
|
||||
let mut json: CustomGroup = serde_json::from_str(text)?;
|
||||
json.custom_props = ap_sign; // without this workaround, publicKey is not correctly deserialized
|
||||
Blog::from_activity(
|
||||
conn,
|
||||
&json,
|
||||
Url::parse(url)?.host_str()?,
|
||||
)
|
||||
Blog::from_activity(conn, &json, Url::parse(url)?.host_str()?)
|
||||
}
|
||||
|
||||
fn from_activity(conn: &Connection, acct: &CustomGroup, inst: &str) -> Result<Blog> {
|
||||
let instance = Instance::find_by_domain(conn, inst).or_else(|_|
|
||||
let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
|
||||
Instance::insert(
|
||||
conn,
|
||||
NewInstance {
|
||||
@@ -210,35 +188,17 @@ impl Blog {
|
||||
long_description_html: String::new(),
|
||||
},
|
||||
)
|
||||
)?;
|
||||
})?;
|
||||
Blog::insert(
|
||||
conn,
|
||||
NewBlog {
|
||||
actor_id: acct
|
||||
.object
|
||||
.ap_actor_props
|
||||
.preferred_username_string()?,
|
||||
title: acct
|
||||
.object
|
||||
.object_props
|
||||
.name_string()?,
|
||||
outbox_url: acct
|
||||
.object
|
||||
.ap_actor_props
|
||||
.outbox_string()?,
|
||||
inbox_url: acct
|
||||
.object
|
||||
.ap_actor_props
|
||||
.inbox_string()?,
|
||||
summary: acct
|
||||
.object
|
||||
.object_props
|
||||
.summary_string()?,
|
||||
actor_id: acct.object.ap_actor_props.preferred_username_string()?,
|
||||
title: acct.object.object_props.name_string()?,
|
||||
outbox_url: acct.object.ap_actor_props.outbox_string()?,
|
||||
inbox_url: acct.object.ap_actor_props.inbox_string()?,
|
||||
summary: acct.object.object_props.summary_string()?,
|
||||
instance_id: instance.id,
|
||||
ap_url: acct
|
||||
.object
|
||||
.object_props
|
||||
.id_string()?,
|
||||
ap_url: acct.object.object_props.id_string()?,
|
||||
public_key: acct
|
||||
.custom_props
|
||||
.public_key_publickey()?
|
||||
@@ -252,27 +212,20 @@ impl Blog {
|
||||
let mut blog = Group::default();
|
||||
blog.ap_actor_props
|
||||
.set_preferred_username_string(self.actor_id.clone())?;
|
||||
blog.object_props
|
||||
.set_name_string(self.title.clone())?;
|
||||
blog.object_props.set_name_string(self.title.clone())?;
|
||||
blog.ap_actor_props
|
||||
.set_outbox_string(self.outbox_url.clone())?;
|
||||
blog.ap_actor_props
|
||||
.set_inbox_string(self.inbox_url.clone())?;
|
||||
blog.object_props
|
||||
.set_summary_string(self.summary.clone())?;
|
||||
blog.object_props
|
||||
.set_id_string(self.ap_url.clone())?;
|
||||
blog.object_props.set_summary_string(self.summary.clone())?;
|
||||
blog.object_props.set_id_string(self.ap_url.clone())?;
|
||||
|
||||
let mut public_key = PublicKey::default();
|
||||
public_key
|
||||
.set_id_string(format!("{}#main-key", self.ap_url))?;
|
||||
public_key
|
||||
.set_owner_string(self.ap_url.clone())?;
|
||||
public_key
|
||||
.set_public_key_pem_string(self.public_key.clone())?;
|
||||
public_key.set_id_string(format!("{}#main-key", self.ap_url))?;
|
||||
public_key.set_owner_string(self.ap_url.clone())?;
|
||||
public_key.set_public_key_pem_string(self.public_key.clone())?;
|
||||
let mut ap_signature = ApSignature::default();
|
||||
ap_signature
|
||||
.set_public_key_publickey(public_key)?;
|
||||
ap_signature.set_public_key_publickey(public_key)?;
|
||||
|
||||
Ok(CustomGroup::new(blog, ap_signature))
|
||||
}
|
||||
@@ -290,13 +243,10 @@ impl Blog {
|
||||
}
|
||||
|
||||
pub fn get_keypair(&self) -> Result<PKey<Private>> {
|
||||
PKey::from_rsa(
|
||||
Rsa::private_key_from_pem(
|
||||
self.private_key
|
||||
.clone()?
|
||||
.as_ref(),
|
||||
)?,
|
||||
).map_err(Error::from)
|
||||
PKey::from_rsa(Rsa::private_key_from_pem(
|
||||
self.private_key.clone()?.as_ref(),
|
||||
)?)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn webfinger(&self, conn: &Connection) -> Result<Webfinger> {
|
||||
@@ -386,25 +336,16 @@ impl sign::Signer for Blog {
|
||||
|
||||
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
|
||||
let key = self.get_keypair()?;
|
||||
let mut signer =
|
||||
Signer::new(MessageDigest::sha256(), &key)?;
|
||||
signer
|
||||
.update(to_sign.as_bytes())?;
|
||||
signer
|
||||
.sign_to_vec()
|
||||
.map_err(Error::from)
|
||||
let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
|
||||
signer.update(to_sign.as_bytes())?;
|
||||
signer.sign_to_vec().map_err(Error::from)
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
|
||||
let key = PKey::from_rsa(
|
||||
Rsa::public_key_from_pem(self.public_key.as_ref())?
|
||||
)?;
|
||||
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
|
||||
let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?;
|
||||
verifier
|
||||
.update(data.as_bytes())?;
|
||||
verifier
|
||||
.verify(&signature)
|
||||
.map_err(Error::from)
|
||||
verifier.update(data.as_bytes())?;
|
||||
verifier.verify(&signature).map_err(Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,32 +375,47 @@ pub(crate) mod tests {
|
||||
use blog_authors::*;
|
||||
use diesel::Connection;
|
||||
use instance::tests as instance_tests;
|
||||
use search::tests::get_searcher;
|
||||
use tests::db;
|
||||
use users::tests as usersTests;
|
||||
use search::tests::get_searcher;
|
||||
use Connection as Conn;
|
||||
|
||||
pub(crate) fn fill_database(conn: &Conn) -> (Vec<User>, Vec<Blog>) {
|
||||
instance_tests::fill_database(conn);
|
||||
let users = usersTests::fill_database(conn);
|
||||
let blog1 = Blog::insert(conn, NewBlog::new_local(
|
||||
"BlogName".to_owned(),
|
||||
"Blog name".to_owned(),
|
||||
"This is a small blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id
|
||||
).unwrap()).unwrap();
|
||||
let blog2 = Blog::insert(conn, NewBlog::new_local(
|
||||
let blog1 = Blog::insert(
|
||||
conn,
|
||||
NewBlog::new_local(
|
||||
"BlogName".to_owned(),
|
||||
"Blog name".to_owned(),
|
||||
"This is a small blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let blog2 = Blog::insert(
|
||||
conn,
|
||||
NewBlog::new_local(
|
||||
"MyBlog".to_owned(),
|
||||
"My blog".to_owned(),
|
||||
"Welcome to my blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id
|
||||
).unwrap()).unwrap();
|
||||
let blog3 = Blog::insert(conn, NewBlog::new_local(
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let blog3 = Blog::insert(
|
||||
conn,
|
||||
NewBlog::new_local(
|
||||
"WhyILikePlume".to_owned(),
|
||||
"Why I like Plume".to_owned(),
|
||||
"In this blog I will explay you why I like Plume so much".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id
|
||||
).unwrap()).unwrap();
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
@@ -468,7 +424,8 @@ pub(crate) mod tests {
|
||||
author_id: users[0].id,
|
||||
is_owner: true,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
@@ -477,7 +434,8 @@ pub(crate) mod tests {
|
||||
author_id: users[1].id,
|
||||
is_owner: false,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
@@ -486,7 +444,8 @@ pub(crate) mod tests {
|
||||
author_id: users[1].id,
|
||||
is_owner: true,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
@@ -495,8 +454,9 @@ pub(crate) mod tests {
|
||||
author_id: users[2].id,
|
||||
is_owner: true,
|
||||
},
|
||||
).unwrap();
|
||||
(users, vec![ blog1, blog2, blog3 ])
|
||||
)
|
||||
.unwrap();
|
||||
(users, vec![blog1, blog2, blog3])
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -511,11 +471,16 @@ pub(crate) mod tests {
|
||||
"SomeName".to_owned(),
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id
|
||||
).unwrap(),
|
||||
).unwrap();
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(blog.get_instance(conn).unwrap().id, Instance::get_local(conn).unwrap().id);
|
||||
assert_eq!(
|
||||
blog.get_instance(conn).unwrap().id,
|
||||
Instance::get_local(conn).unwrap().id
|
||||
);
|
||||
// TODO add tests for remote instance
|
||||
|
||||
Ok(())
|
||||
@@ -535,18 +500,22 @@ pub(crate) mod tests {
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
).unwrap(),
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let b2 = Blog::insert(
|
||||
conn,
|
||||
NewBlog::new_local(
|
||||
"Blog".to_owned(),
|
||||
"Blog".to_owned(),
|
||||
"I've named my blog Blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id
|
||||
).unwrap(),
|
||||
).unwrap();
|
||||
let blog = vec![ b1, b2 ];
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let blog = vec![b1, b2];
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
@@ -555,7 +524,8 @@ pub(crate) mod tests {
|
||||
author_id: user[0].id,
|
||||
is_owner: true,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
@@ -564,7 +534,8 @@ pub(crate) mod tests {
|
||||
author_id: user[1].id,
|
||||
is_owner: false,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
@@ -573,53 +544,46 @@ pub(crate) mod tests {
|
||||
author_id: user[0].id,
|
||||
is_owner: true,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
blog[0]
|
||||
.list_authors(conn).unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[0].id)
|
||||
);
|
||||
assert!(
|
||||
blog[0]
|
||||
.list_authors(conn).unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[1].id)
|
||||
);
|
||||
assert!(
|
||||
blog[1]
|
||||
.list_authors(conn).unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[0].id)
|
||||
);
|
||||
assert!(
|
||||
!blog[1]
|
||||
.list_authors(conn).unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[1].id)
|
||||
);
|
||||
assert!(blog[0]
|
||||
.list_authors(conn)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[0].id));
|
||||
assert!(blog[0]
|
||||
.list_authors(conn)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[1].id));
|
||||
assert!(blog[1]
|
||||
.list_authors(conn)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[0].id));
|
||||
assert!(!blog[1]
|
||||
.list_authors(conn)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|a| a.id == user[1].id));
|
||||
|
||||
assert!(
|
||||
Blog::find_for_author(conn, &user[0]).unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[0].id)
|
||||
);
|
||||
assert!(
|
||||
Blog::find_for_author(conn, &user[1]).unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[0].id)
|
||||
);
|
||||
assert!(
|
||||
Blog::find_for_author(conn, &user[0]).unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[1].id)
|
||||
);
|
||||
assert!(
|
||||
!Blog::find_for_author(conn, &user[1]).unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[1].id)
|
||||
);
|
||||
assert!(Blog::find_for_author(conn, &user[0])
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[0].id));
|
||||
assert!(Blog::find_for_author(conn, &user[1])
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[0].id));
|
||||
assert!(Blog::find_for_author(conn, &user[0])
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[1].id));
|
||||
assert!(!Blog::find_for_author(conn, &user[1])
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|b| b.id == blog[1].id));
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@@ -638,13 +602,12 @@ pub(crate) mod tests {
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
).unwrap(),
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Blog::find_by_fqn(conn, "SomeName").unwrap().id,
|
||||
blog.id
|
||||
);
|
||||
assert_eq!(Blog::find_by_fqn(conn, "SomeName").unwrap().id, blog.id);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@@ -663,8 +626,10 @@ pub(crate) mod tests {
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
).unwrap(),
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(blog.fqn, "SomeName");
|
||||
|
||||
@@ -699,8 +664,10 @@ pub(crate) mod tests {
|
||||
"Some name".to_owned(),
|
||||
"This is some blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
).unwrap(),
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let b2 = Blog::insert(
|
||||
conn,
|
||||
NewBlog::new_local(
|
||||
@@ -708,9 +675,11 @@ pub(crate) mod tests {
|
||||
"Blog".to_owned(),
|
||||
"I've named my blog Blog".to_owned(),
|
||||
Instance::get_local(conn).unwrap().id,
|
||||
).unwrap(),
|
||||
).unwrap();
|
||||
let blog = vec![ b1, b2 ];
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let blog = vec![b1, b2];
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
@@ -719,7 +688,8 @@ pub(crate) mod tests {
|
||||
author_id: user[0].id,
|
||||
is_owner: true,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
@@ -728,7 +698,8 @@ pub(crate) mod tests {
|
||||
author_id: user[1].id,
|
||||
is_owner: false,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
BlogAuthor::insert(
|
||||
conn,
|
||||
@@ -737,7 +708,8 @@ pub(crate) mod tests {
|
||||
author_id: user[0].id,
|
||||
is_owner: true,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
user[0].delete(conn, &searcher).unwrap();
|
||||
assert!(Blog::get(conn, blog[0].id).is_ok());
|
||||
|
||||
@@ -23,7 +23,8 @@ impl CommentSeers {
|
||||
insert!(comment_seers, NewCommentSeers);
|
||||
|
||||
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))
|
||||
.load::<CommentSeers>(conn)
|
||||
.map_err(Error::from)
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
use activitypub::{activity::{Create, Delete}, link, object::{Note, Tombstone}};
|
||||
use activitypub::{
|
||||
activity::{Create, Delete},
|
||||
link,
|
||||
object::{Note, Tombstone},
|
||||
};
|
||||
use chrono::{self, NaiveDateTime};
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
|
||||
use serde_json;
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use comment_seers::{CommentSeers, NewCommentSeers};
|
||||
use instance::Instance;
|
||||
use mentions::Mention;
|
||||
use notifications::*;
|
||||
use plume_common::activity_pub::{
|
||||
inbox::{FromActivity, Notify, Deletable},
|
||||
inbox::{Deletable, FromActivity, Notify},
|
||||
Id, IntoId, PUBLIC_VISIBILTY,
|
||||
};
|
||||
use plume_common::utils;
|
||||
use comment_seers::{CommentSeers, NewCommentSeers};
|
||||
use posts::Post;
|
||||
use safe_string::SafeString;
|
||||
use schema::comments;
|
||||
@@ -50,7 +54,11 @@ pub struct NewComment {
|
||||
impl Comment {
|
||||
insert!(comments, NewComment, |inserted, conn| {
|
||||
if inserted.ap_url.is_none() {
|
||||
inserted.ap_url = Some(format!("{}comment/{}", inserted.get_post(conn)?.ap_url, inserted.id));
|
||||
inserted.ap_url = Some(format!(
|
||||
"{}comment/{}",
|
||||
inserted.get_post(conn)?.ap_url,
|
||||
inserted.id
|
||||
));
|
||||
let _: Comment = inserted.save_changes(conn)?;
|
||||
}
|
||||
Ok(inserted)
|
||||
@@ -80,20 +88,25 @@ impl 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)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn can_see(&self, conn: &Connection, user: Option<&User>) -> bool {
|
||||
self.public_visibility ||
|
||||
user.as_ref().map(|u| CommentSeers::can_see(conn, self, u).unwrap_or(false))
|
||||
self.public_visibility
|
||||
|| user
|
||||
.as_ref()
|
||||
.map(|u| CommentSeers::can_see(conn, self, u).unwrap_or(false))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<Note> {
|
||||
let (html, mentions, _hashtags) = utils::md_to_html(self.content.get().as_ref(),
|
||||
&Instance::get_local(conn)?.public_domain);
|
||||
let (html, mentions, _hashtags) = utils::md_to_html(
|
||||
self.content.get().as_ref(),
|
||||
&Instance::get_local(conn)?.public_domain,
|
||||
);
|
||||
|
||||
let author = User::get(conn, self.author_id)?;
|
||||
let mut note = Note::default();
|
||||
@@ -103,8 +116,7 @@ impl Comment {
|
||||
.set_id_string(self.ap_url.clone().unwrap_or_default())?;
|
||||
note.object_props
|
||||
.set_summary_string(self.spoiler_text.clone())?;
|
||||
note.object_props
|
||||
.set_content_string(html)?;
|
||||
note.object_props.set_content_string(html)?;
|
||||
note.object_props
|
||||
.set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else(
|
||||
|| Ok(Post::get(conn, self.post_id)?.ap_url),
|
||||
@@ -114,41 +126,28 @@ impl Comment {
|
||||
.set_published_string(chrono::Utc::now().to_rfc3339())?;
|
||||
note.object_props
|
||||
.set_attributed_to_link(author.clone().into_id())?;
|
||||
note.object_props
|
||||
.set_to_link_vec(to.clone())?;
|
||||
note.object_props
|
||||
.set_tag_link_vec(
|
||||
mentions
|
||||
.into_iter()
|
||||
.filter_map(|m| Mention::build_activity(conn, &m).ok())
|
||||
.collect::<Vec<link::Mention>>(),
|
||||
)?;
|
||||
note.object_props.set_to_link_vec(to.clone())?;
|
||||
note.object_props.set_tag_link_vec(
|
||||
mentions
|
||||
.into_iter()
|
||||
.filter_map(|m| Mention::build_activity(conn, &m).ok())
|
||||
.collect::<Vec<link::Mention>>(),
|
||||
)?;
|
||||
Ok(note)
|
||||
}
|
||||
|
||||
pub fn create_activity(&self, conn: &Connection) -> Result<Create> {
|
||||
let author =
|
||||
User::get(conn, self.author_id)?;
|
||||
let author = User::get(conn, self.author_id)?;
|
||||
|
||||
let note = self.to_activity(conn)?;
|
||||
let mut act = Create::default();
|
||||
act.create_props
|
||||
.set_actor_link(author.into_id())?;
|
||||
act.create_props
|
||||
.set_object_object(note.clone())?;
|
||||
act.create_props.set_actor_link(author.into_id())?;
|
||||
act.create_props.set_object_object(note.clone())?;
|
||||
act.object_props
|
||||
.set_id_string(format!(
|
||||
"{}/activity",
|
||||
self.ap_url
|
||||
.clone()?,
|
||||
))?;
|
||||
.set_id_string(format!("{}/activity", self.ap_url.clone()?,))?;
|
||||
act.object_props
|
||||
.set_to_link_vec(
|
||||
note.object_props
|
||||
.to_link_vec::<Id>()?,
|
||||
)?;
|
||||
act.object_props
|
||||
.set_cc_link_vec::<Id>(vec![])?;
|
||||
.set_to_link_vec(note.object_props.to_link_vec::<Id>()?)?;
|
||||
act.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||
Ok(act)
|
||||
}
|
||||
}
|
||||
@@ -158,43 +157,39 @@ impl FromActivity<Note, Connection> for Comment {
|
||||
|
||||
fn from_activity(conn: &Connection, note: Note, actor: Id) -> Result<Comment> {
|
||||
let comm = {
|
||||
let previous_url = note
|
||||
.object_props
|
||||
.in_reply_to
|
||||
.as_ref()?
|
||||
.as_str()?;
|
||||
let previous_url = note.object_props.in_reply_to.as_ref()?.as_str()?;
|
||||
let previous_comment = Comment::find_by_ap_url(conn, previous_url);
|
||||
|
||||
let is_public = |v: &Option<serde_json::Value>| match v.as_ref().unwrap_or(&serde_json::Value::Null) {
|
||||
serde_json::Value::Array(v) => v.iter().filter_map(serde_json::Value::as_str).any(|s| s==PUBLIC_VISIBILTY),
|
||||
let is_public = |v: &Option<serde_json::Value>| match v
|
||||
.as_ref()
|
||||
.unwrap_or(&serde_json::Value::Null)
|
||||
{
|
||||
serde_json::Value::Array(v) => v
|
||||
.iter()
|
||||
.filter_map(serde_json::Value::as_str)
|
||||
.any(|s| s == PUBLIC_VISIBILTY),
|
||||
serde_json::Value::String(s) => s == PUBLIC_VISIBILTY,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let public_visibility = is_public(¬e.object_props.to) ||
|
||||
is_public(¬e.object_props.bto) ||
|
||||
is_public(¬e.object_props.cc) ||
|
||||
is_public(¬e.object_props.bcc);
|
||||
let public_visibility = is_public(¬e.object_props.to)
|
||||
|| is_public(¬e.object_props.bto)
|
||||
|| is_public(¬e.object_props.cc)
|
||||
|| is_public(¬e.object_props.bcc);
|
||||
|
||||
let comm = Comment::insert(
|
||||
conn,
|
||||
NewComment {
|
||||
content: SafeString::new(
|
||||
¬e
|
||||
.object_props
|
||||
.content_string()?
|
||||
),
|
||||
spoiler_text: note
|
||||
.object_props
|
||||
.summary_string()
|
||||
.unwrap_or_default(),
|
||||
content: SafeString::new(¬e.object_props.content_string()?),
|
||||
spoiler_text: note.object_props.summary_string().unwrap_or_default(),
|
||||
ap_url: note.object_props.id_string().ok(),
|
||||
in_response_to_id: previous_comment.iter().map(|c| c.id).next(),
|
||||
post_id: previous_comment.map(|c| c.post_id)
|
||||
.or_else(|_| Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result<i32>)?,
|
||||
post_id: previous_comment.map(|c| c.post_id).or_else(|_| {
|
||||
Ok(Post::find_by_ap_url(conn, previous_url)?.id) as Result<i32>
|
||||
})?,
|
||||
author_id: User::from_url(conn, actor.as_ref())?.id,
|
||||
sensitive: false, // "sensitive" is not a standard property, we need to think about how to support it with the activitypub crate
|
||||
public_visibility
|
||||
public_visibility,
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -204,13 +199,11 @@ impl FromActivity<Note, Connection> for Comment {
|
||||
serde_json::from_value::<link::Mention>(tag)
|
||||
.map_err(Error::from)
|
||||
.and_then(|m| {
|
||||
let author = &Post::get(conn, comm.post_id)?
|
||||
.get_authors(conn)?[0];
|
||||
let not_author = m
|
||||
.link_props
|
||||
.href_string()?
|
||||
!= author.ap_url.clone();
|
||||
Ok(Mention::from_activity(conn, &m, comm.id, false, not_author)?)
|
||||
let author = &Post::get(conn, comm.post_id)?.get_authors(conn)?[0];
|
||||
let not_author = m.link_props.href_string()? != author.ap_url.clone();
|
||||
Ok(Mention::from_activity(
|
||||
conn, &m, comm.id, false, not_author,
|
||||
)?)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -218,14 +211,21 @@ impl FromActivity<Note, Connection> for Comment {
|
||||
comm
|
||||
};
|
||||
|
||||
|
||||
if !comm.public_visibility {
|
||||
let receivers_ap_url = |v: Option<serde_json::Value>| {
|
||||
let filter = |e: serde_json::Value| if let serde_json::Value::String(s) = e { Some(s) } else { None };
|
||||
let filter = |e: serde_json::Value| {
|
||||
if let serde_json::Value::String(s) = e {
|
||||
Some(s)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
match v.unwrap_or(serde_json::Value::Null) {
|
||||
serde_json::Value::Array(v) => v,
|
||||
v => vec![v],
|
||||
}.into_iter().filter_map(filter)
|
||||
}
|
||||
.into_iter()
|
||||
.filter_map(filter)
|
||||
};
|
||||
|
||||
let mut note = note;
|
||||
@@ -235,25 +235,30 @@ impl FromActivity<Note, Connection> for Comment {
|
||||
let bto = receivers_ap_url(note.object_props.bto.take());
|
||||
let bcc = receivers_ap_url(note.object_props.bcc.take());
|
||||
|
||||
let receivers_ap_url = to.chain(cc).chain(bto).chain(bcc)
|
||||
.collect::<HashSet<_>>()//remove duplicates (don't do a query more than once)
|
||||
let receivers_ap_url = to
|
||||
.chain(cc)
|
||||
.chain(bto)
|
||||
.chain(bcc)
|
||||
.collect::<HashSet<_>>() //remove duplicates (don't do a query more than once)
|
||||
.into_iter()
|
||||
.map(|v| if let Ok(user) = User::from_url(conn,&v) {
|
||||
vec![user]
|
||||
} else {
|
||||
vec![]// TODO try to fetch collection
|
||||
.map(|v| {
|
||||
if let Ok(user) = User::from_url(conn, &v) {
|
||||
vec![user]
|
||||
} else {
|
||||
vec![] // TODO try to fetch collection
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.filter(|u| u.get_instance(conn).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 {
|
||||
CommentSeers::insert(
|
||||
conn,
|
||||
NewCommentSeers {
|
||||
comment_id: comm.id,
|
||||
user_id: user.id
|
||||
}
|
||||
user_id: user.id,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
@@ -288,7 +293,8 @@ pub struct CommentTree {
|
||||
|
||||
impl CommentTree {
|
||||
pub fn from_post(conn: &Connection, p: &Post, user: Option<&User>) -> Result<Vec<Self>> {
|
||||
Ok(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.can_see(conn, user))
|
||||
.filter_map(|c| Self::from_comment(conn, c, user).ok())
|
||||
@@ -296,14 +302,13 @@ impl CommentTree {
|
||||
}
|
||||
|
||||
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_map(|c| Self::from_comment(conn, c, user).ok())
|
||||
.collect();
|
||||
Ok(CommentTree {
|
||||
comment,
|
||||
responses,
|
||||
})
|
||||
Ok(CommentTree { comment, responses })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,11 +321,8 @@ impl<'a> Deletable<Connection, Delete> for Comment {
|
||||
.set_actor_link(self.get_author(conn)?.into_id())?;
|
||||
|
||||
let mut tombstone = Tombstone::default();
|
||||
tombstone
|
||||
.object_props
|
||||
.set_id_string(self.ap_url.clone()?)?;
|
||||
act.delete_props
|
||||
.set_object_object(tombstone)?;
|
||||
tombstone.object_props.set_id_string(self.ap_url.clone()?)?;
|
||||
act.delete_props.set_object_object(tombstone)?;
|
||||
|
||||
act.object_props
|
||||
.set_id_string(format!("{}#delete", self.ap_url.clone().unwrap()))?;
|
||||
@@ -330,11 +332,11 @@ impl<'a> Deletable<Connection, Delete> for Comment {
|
||||
for m in Mention::list_for_comment(&conn, self.id)? {
|
||||
m.delete(conn)?;
|
||||
}
|
||||
diesel::update(comments::table).filter(comments::in_response_to_id.eq(self.id))
|
||||
diesel::update(comments::table)
|
||||
.filter(comments::in_response_to_id.eq(self.id))
|
||||
.set(comments::in_response_to_id.eq(self.in_response_to_id))
|
||||
.execute(conn)?;
|
||||
diesel::delete(self)
|
||||
.execute(conn)?;
|
||||
diesel::delete(self).execute(conn)?;
|
||||
Ok(act)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use diesel::{r2d2::{ConnectionManager, CustomizeConnection, Error as ConnError, Pool, PooledConnection}};
|
||||
use diesel::r2d2::{
|
||||
ConnectionManager, CustomizeConnection, Error as ConnError, Pool, PooledConnection,
|
||||
};
|
||||
#[cfg(feature = "sqlite")]
|
||||
use diesel::{dsl::sql_query, ConnectionError, RunQueryDsl};
|
||||
use rocket::{
|
||||
@@ -47,8 +49,13 @@ pub struct PragmaForeignKey;
|
||||
impl CustomizeConnection<Connection, ConnError> for PragmaForeignKey {
|
||||
#[cfg(feature = "sqlite")] // will default to an empty function for postgres
|
||||
fn on_acquire(&self, conn: &mut Connection) -> Result<(), ConnError> {
|
||||
sql_query("PRAGMA foreign_keys = on;").execute(conn)
|
||||
sql_query("PRAGMA foreign_keys = on;")
|
||||
.execute(conn)
|
||||
.map(|_| ())
|
||||
.map_err(|_| ConnError::ConnectionError(ConnectionError::BadConnection(String::from("PRAGMA foreign_keys = on failed"))))
|
||||
.map_err(|_| {
|
||||
ConnError::ConnectionError(ConnectionError::BadConnection(String::from(
|
||||
"PRAGMA foreign_keys = on failed",
|
||||
)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+47
-62
@@ -14,7 +14,7 @@ use plume_common::activity_pub::{
|
||||
};
|
||||
use schema::follows;
|
||||
use users::User;
|
||||
use {ap_url, Connection, BASE_URL, Error, Result};
|
||||
use {ap_url, Connection, Error, Result, BASE_URL};
|
||||
|
||||
#[derive(Clone, Queryable, Identifiable, Associations, AsChangeset)]
|
||||
#[belongs_to(User, foreign_key = "following_id")]
|
||||
@@ -62,12 +62,9 @@ impl Follow {
|
||||
.set_actor_link::<Id>(user.clone().into_id())?;
|
||||
act.follow_props
|
||||
.set_object_link::<Id>(target.clone().into_id())?;
|
||||
act.object_props
|
||||
.set_id_string(self.ap_url.clone())?;
|
||||
act.object_props
|
||||
.set_to_link(target.into_id())?;
|
||||
act.object_props
|
||||
.set_cc_link_vec::<Id>(vec![])?;
|
||||
act.object_props.set_id_string(self.ap_url.clone())?;
|
||||
act.object_props.set_to_link(target.into_id())?;
|
||||
act.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||
Ok(act)
|
||||
}
|
||||
|
||||
@@ -92,21 +89,13 @@ impl Follow {
|
||||
|
||||
let mut accept = Accept::default();
|
||||
let accept_id = ap_url(&format!("{}/follow/{}/accept", BASE_URL.as_str(), &res.id));
|
||||
accept
|
||||
.object_props
|
||||
.set_id_string(accept_id)?;
|
||||
accept
|
||||
.object_props
|
||||
.set_to_link(from.clone().into_id())?;
|
||||
accept
|
||||
.object_props
|
||||
.set_cc_link_vec::<Id>(vec![])?;
|
||||
accept.object_props.set_id_string(accept_id)?;
|
||||
accept.object_props.set_to_link(from.clone().into_id())?;
|
||||
accept.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||
accept
|
||||
.accept_props
|
||||
.set_actor_link::<Id>(target.clone().into_id())?;
|
||||
accept
|
||||
.accept_props
|
||||
.set_object_object(follow)?;
|
||||
accept.accept_props.set_object_object(follow)?;
|
||||
broadcast(&*target, accept, vec![from.clone()]);
|
||||
Ok(res)
|
||||
}
|
||||
@@ -120,29 +109,18 @@ impl FromActivity<FollowAct, Connection> for Follow {
|
||||
.follow_props
|
||||
.actor_link::<Id>()
|
||||
.map(|l| l.into())
|
||||
.or_else(|_| Ok(follow
|
||||
.follow_props
|
||||
.actor_object::<Person>()?
|
||||
.object_props
|
||||
.id_string()?) as Result<String>)?;
|
||||
let from =
|
||||
User::from_url(conn, &from_id)?;
|
||||
match User::from_url(
|
||||
conn,
|
||||
follow
|
||||
.follow_props
|
||||
.object
|
||||
.as_str()?,
|
||||
) {
|
||||
.or_else(|_| {
|
||||
Ok(follow
|
||||
.follow_props
|
||||
.actor_object::<Person>()?
|
||||
.object_props
|
||||
.id_string()?) as Result<String>
|
||||
})?;
|
||||
let from = User::from_url(conn, &from_id)?;
|
||||
match User::from_url(conn, follow.follow_props.object.as_str()?) {
|
||||
Ok(user) => Follow::accept_follow(conn, &from, &user, follow, from.id, user.id),
|
||||
Err(_) => {
|
||||
let blog = Blog::from_url(
|
||||
conn,
|
||||
follow
|
||||
.follow_props
|
||||
.object
|
||||
.as_str()?,
|
||||
)?;
|
||||
let blog = Blog::from_url(conn, follow.follow_props.object.as_str()?)?;
|
||||
Follow::accept_follow(conn, &from, &blog, follow, from.id, blog.id)
|
||||
}
|
||||
}
|
||||
@@ -160,7 +138,8 @@ impl Notify<Connection> for Follow {
|
||||
object_id: self.id,
|
||||
user_id: self.following_id,
|
||||
},
|
||||
).map(|_| ())
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,21 +147,16 @@ impl Deletable<Connection, Undo> for Follow {
|
||||
type Error = Error;
|
||||
|
||||
fn delete(&self, conn: &Connection) -> Result<Undo> {
|
||||
diesel::delete(self)
|
||||
.execute(conn)?;
|
||||
diesel::delete(self).execute(conn)?;
|
||||
|
||||
// delete associated notification if any
|
||||
if let Ok(notif) = Notification::find(conn, notification_kind::FOLLOW, self.id) {
|
||||
diesel::delete(¬if)
|
||||
.execute(conn)?;
|
||||
diesel::delete(¬if).execute(conn)?;
|
||||
}
|
||||
|
||||
let mut undo = Undo::default();
|
||||
undo.undo_props
|
||||
.set_actor_link(
|
||||
User::get(conn, self.follower_id)?
|
||||
.into_id(),
|
||||
)?;
|
||||
.set_actor_link(User::get(conn, self.follower_id)?.into_id())?;
|
||||
undo.object_props
|
||||
.set_id_string(format!("{}/undo", self.ap_url))?;
|
||||
undo.undo_props
|
||||
@@ -209,8 +183,8 @@ impl IntoId for Follow {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use diesel::Connection;
|
||||
use super::*;
|
||||
use diesel::Connection;
|
||||
use tests::db;
|
||||
use users::tests as user_tests;
|
||||
|
||||
@@ -219,20 +193,31 @@ mod tests {
|
||||
let conn = db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let users = user_tests::fill_database(&conn);
|
||||
let follow = Follow::insert(&conn, NewFollow {
|
||||
follower_id: users[0].id,
|
||||
following_id: users[1].id,
|
||||
ap_url: String::new(),
|
||||
}).expect("Couldn't insert new follow");
|
||||
assert_eq!(follow.ap_url, format!("https://{}/follows/{}", *BASE_URL, follow.id));
|
||||
let follow = Follow::insert(
|
||||
&conn,
|
||||
NewFollow {
|
||||
follower_id: users[0].id,
|
||||
following_id: users[1].id,
|
||||
ap_url: String::new(),
|
||||
},
|
||||
)
|
||||
.expect("Couldn't insert new follow");
|
||||
assert_eq!(
|
||||
follow.ap_url,
|
||||
format!("https://{}/follows/{}", *BASE_URL, follow.id)
|
||||
);
|
||||
|
||||
let follow = Follow::insert(&conn, NewFollow {
|
||||
follower_id: users[1].id,
|
||||
following_id: users[0].id,
|
||||
ap_url: String::from("https://some.url/"),
|
||||
}).expect("Couldn't insert new follow");
|
||||
let follow = Follow::insert(
|
||||
&conn,
|
||||
NewFollow {
|
||||
follower_id: users[1].id,
|
||||
following_id: users[0].id,
|
||||
ap_url: String::from("https://some.url/"),
|
||||
},
|
||||
)
|
||||
.expect("Couldn't insert new follow");
|
||||
assert_eq!(follow.ap_url, String::from("https://some.url/"));
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ pub struct Instance {
|
||||
pub open_registrations: bool,
|
||||
pub short_description: SafeString,
|
||||
pub long_description: SafeString,
|
||||
pub default_license : String,
|
||||
pub default_license: String,
|
||||
pub long_description_html: SafeString,
|
||||
pub short_description_html: SafeString,
|
||||
}
|
||||
@@ -46,7 +46,8 @@ impl Instance {
|
||||
.limit(1)
|
||||
.load::<Instance>(conn)?
|
||||
.into_iter()
|
||||
.nth(0).ok_or(Error::NotFound)
|
||||
.nth(0)
|
||||
.ok_or(Error::NotFound)
|
||||
}
|
||||
|
||||
pub fn get_remotes(conn: &Connection) -> Result<Vec<Instance>> {
|
||||
@@ -109,12 +110,7 @@ impl Instance {
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn compute_box(
|
||||
&self,
|
||||
prefix: &str,
|
||||
name: &str,
|
||||
box_name: &str,
|
||||
) -> String {
|
||||
pub fn compute_box(&self, prefix: &str, name: &str, box_name: &str) -> String {
|
||||
ap_url(&format!(
|
||||
"{instance}/{prefix}/{name}/{box_name}",
|
||||
instance = self.public_domain,
|
||||
@@ -209,15 +205,16 @@ pub(crate) mod tests {
|
||||
open_registrations: true,
|
||||
public_domain: "3plu.me".to_string(),
|
||||
},
|
||||
].into_iter()
|
||||
.map(|inst| {
|
||||
(
|
||||
inst.clone(),
|
||||
Instance::find_by_domain(conn, &inst.public_domain)
|
||||
.unwrap_or_else(|_| Instance::insert(conn, inst).unwrap()),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
]
|
||||
.into_iter()
|
||||
.map(|inst| {
|
||||
(
|
||||
inst.clone(),
|
||||
Instance::find_by_domain(conn, &inst.public_domain)
|
||||
.unwrap_or_else(|_| Instance::insert(conn, inst).unwrap()),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -244,8 +241,14 @@ pub(crate) mod tests {
|
||||
public_domain
|
||||
]
|
||||
);
|
||||
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.long_description_html.get(),
|
||||
&inserted.long_description_html
|
||||
);
|
||||
assert_eq!(
|
||||
res.short_description_html.get(),
|
||||
&inserted.short_description_html
|
||||
);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@@ -282,8 +285,14 @@ pub(crate) mod tests {
|
||||
public_domain
|
||||
]
|
||||
);
|
||||
assert_eq!(&newinst.long_description_html, inst.long_description_html.get());
|
||||
assert_eq!(&newinst.short_description_html, inst.short_description_html.get());
|
||||
assert_eq!(
|
||||
&newinst.long_description_html,
|
||||
inst.long_description_html.get()
|
||||
);
|
||||
assert_eq!(
|
||||
&newinst.short_description_html,
|
||||
inst.short_description_html.get()
|
||||
);
|
||||
});
|
||||
|
||||
let page = Instance::page(conn, (0, 2)).unwrap();
|
||||
@@ -292,7 +301,9 @@ pub(crate) mod tests {
|
||||
let page2 = &page[1];
|
||||
assert!(page1.public_domain <= page2.public_domain);
|
||||
|
||||
let mut last_domaine: String = Instance::page(conn, (0, 1)).unwrap()[0].public_domain.clone();
|
||||
let mut last_domaine: String = Instance::page(conn, (0, 1)).unwrap()[0]
|
||||
.public_domain
|
||||
.clone();
|
||||
for i in 1..inserted.len() as i32 {
|
||||
let page = Instance::page(conn, (i, i + 1)).unwrap();
|
||||
assert_eq!(page.len(), 1);
|
||||
@@ -326,11 +337,13 @@ pub(crate) mod tests {
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain)).unwrap(),
|
||||
Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain))
|
||||
.unwrap(),
|
||||
inst.blocked
|
||||
);
|
||||
assert_eq!(
|
||||
Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain)).unwrap(),
|
||||
Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain))
|
||||
.unwrap(),
|
||||
Instance::find_by_domain(conn, &format!("{}a", inst.public_domain))
|
||||
.map(|inst| inst.blocked)
|
||||
.unwrap_or(false)
|
||||
@@ -340,11 +353,13 @@ pub(crate) mod tests {
|
||||
let inst = Instance::get(conn, inst.id).unwrap();
|
||||
assert_eq!(inst.blocked, blocked);
|
||||
assert_eq!(
|
||||
Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain)).unwrap(),
|
||||
Instance::is_blocked(conn, &format!("https://{}/something", inst.public_domain))
|
||||
.unwrap(),
|
||||
inst.blocked
|
||||
);
|
||||
assert_eq!(
|
||||
Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain)).unwrap(),
|
||||
Instance::is_blocked(conn, &format!("https://{}a/something", inst.public_domain))
|
||||
.unwrap(),
|
||||
Instance::find_by_domain(conn, &format!("{}a", inst.public_domain))
|
||||
.map(|inst| inst.blocked)
|
||||
.unwrap_or(false)
|
||||
@@ -375,7 +390,8 @@ pub(crate) mod tests {
|
||||
false,
|
||||
SafeString::new("[short](#link)"),
|
||||
SafeString::new("[long_description](/with_link)"),
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
let inst = Instance::get(conn, inst.id).unwrap();
|
||||
assert_eq!(inst.name, "NewName".to_owned());
|
||||
assert_eq!(inst.open_registrations, false);
|
||||
|
||||
@@ -292,8 +292,8 @@ static DB_NAME: &str = "plume_tests";
|
||||
|
||||
#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
|
||||
lazy_static! {
|
||||
pub static ref DATABASE_URL: String =
|
||||
env::var("DATABASE_URL").unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME));
|
||||
pub static ref DATABASE_URL: String = env::var("DATABASE_URL")
|
||||
.unwrap_or_else(|_| format!("postgres://plume:plume@localhost/{}", DB_NAME));
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
|
||||
@@ -336,7 +336,9 @@ mod tests {
|
||||
Conn::establish(&*DATABASE_URL.as_str()).expect("Couldn't connect to the database");
|
||||
embedded_migrations::run(&conn).expect("Couldn't run migrations");
|
||||
#[cfg(feature = "sqlite")]
|
||||
sql_query("PRAGMA foreign_keys = on;").execute(&conn).expect("PRAGMA foreign_keys fail");
|
||||
sql_query("PRAGMA foreign_keys = on;")
|
||||
.execute(&conn)
|
||||
.expect("PRAGMA foreign_keys fail");
|
||||
conn
|
||||
}
|
||||
}
|
||||
@@ -346,8 +348,8 @@ pub mod api_tokens;
|
||||
pub mod apps;
|
||||
pub mod blog_authors;
|
||||
pub mod blogs;
|
||||
pub mod comments;
|
||||
pub mod comment_seers;
|
||||
pub mod comments;
|
||||
pub mod db_conn;
|
||||
pub mod follows;
|
||||
pub mod headers;
|
||||
@@ -360,7 +362,7 @@ pub mod post_authors;
|
||||
pub mod posts;
|
||||
pub mod reshares;
|
||||
pub mod safe_string;
|
||||
pub mod search;
|
||||
pub mod schema;
|
||||
pub mod search;
|
||||
pub mod tags;
|
||||
pub mod users;
|
||||
|
||||
+12
-34
@@ -38,21 +38,13 @@ impl Like {
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<activity::Like> {
|
||||
let mut act = activity::Like::default();
|
||||
act.like_props
|
||||
.set_actor_link(
|
||||
User::get(conn, self.user_id)?
|
||||
.into_id(),
|
||||
)?;
|
||||
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
||||
act.like_props
|
||||
.set_object_link(
|
||||
Post::get(conn, self.post_id)?
|
||||
.into_id(),
|
||||
)?;
|
||||
.set_object_link(Post::get(conn, self.post_id)?.into_id())?;
|
||||
act.object_props
|
||||
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
|
||||
act.object_props
|
||||
.set_cc_link_vec::<Id>(vec![])?;
|
||||
act.object_props
|
||||
.set_id_string(self.ap_url.clone())?;
|
||||
act.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||
act.object_props.set_id_string(self.ap_url.clone())?;
|
||||
|
||||
Ok(act)
|
||||
}
|
||||
@@ -62,18 +54,8 @@ impl FromActivity<activity::Like, Connection> for Like {
|
||||
type Error = Error;
|
||||
|
||||
fn from_activity(conn: &Connection, like: activity::Like, _actor: Id) -> Result<Like> {
|
||||
let liker = User::from_url(
|
||||
conn,
|
||||
like.like_props
|
||||
.actor
|
||||
.as_str()?,
|
||||
)?;
|
||||
let post = Post::find_by_ap_url(
|
||||
conn,
|
||||
like.like_props
|
||||
.object
|
||||
.as_str()?,
|
||||
)?;
|
||||
let liker = User::from_url(conn, like.like_props.actor.as_str()?)?;
|
||||
let post = Post::find_by_ap_url(conn, like.like_props.object.as_str()?)?;
|
||||
let res = Like::insert(
|
||||
conn,
|
||||
NewLike {
|
||||
@@ -110,26 +92,22 @@ impl Deletable<Connection, activity::Undo> for Like {
|
||||
type Error = Error;
|
||||
|
||||
fn delete(&self, conn: &Connection) -> Result<activity::Undo> {
|
||||
diesel::delete(self)
|
||||
.execute(conn)?;
|
||||
diesel::delete(self).execute(conn)?;
|
||||
|
||||
// delete associated notification if any
|
||||
if let Ok(notif) = Notification::find(conn, notification_kind::LIKE, self.id) {
|
||||
diesel::delete(¬if)
|
||||
.execute(conn)?;
|
||||
diesel::delete(¬if).execute(conn)?;
|
||||
}
|
||||
|
||||
let mut act = activity::Undo::default();
|
||||
act.undo_props
|
||||
.set_actor_link(User::get(conn, self.user_id)?.into_id(),)?;
|
||||
act.undo_props
|
||||
.set_object_object(self.to_activity(conn)?)?;
|
||||
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
||||
act.undo_props.set_object_object(self.to_activity(conn)?)?;
|
||||
act.object_props
|
||||
.set_id_string(format!("{}#delete", self.ap_url))?;
|
||||
act.object_props
|
||||
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
|
||||
act.object_props
|
||||
.set_cc_link_vec::<Id>(vec![])?;
|
||||
act.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||
|
||||
Ok(act)
|
||||
}
|
||||
@@ -151,7 +129,7 @@ impl NewLike {
|
||||
NewLike {
|
||||
post_id: p.id,
|
||||
user_id: u.id,
|
||||
ap_url
|
||||
ap_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+65
-58
@@ -62,12 +62,14 @@ impl Media {
|
||||
list_by!(medias, for_user, owner_id as i32);
|
||||
|
||||
pub fn list_all_medias(conn: &Connection) -> Result<Vec<Media>> {
|
||||
medias::table
|
||||
.load::<Media>(conn)
|
||||
.map_err(Error::from)
|
||||
medias::table.load::<Media>(conn).map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn page_for_user(conn: &Connection, user: &User, (min, max): (i32, i32)) -> Result<Vec<Media>> {
|
||||
pub fn page_for_user(
|
||||
conn: &Connection,
|
||||
user: &User,
|
||||
(min, max): (i32, i32),
|
||||
) -> Result<Vec<Media>> {
|
||||
medias::table
|
||||
.filter(medias::owner_id.eq(user.id))
|
||||
.offset(i64::from(min))
|
||||
@@ -124,7 +126,9 @@ impl Media {
|
||||
pub fn markdown(&self, conn: &Connection) -> Result<SafeString> {
|
||||
let url = self.url(conn)?;
|
||||
Ok(match self.category() {
|
||||
MediaCategory::Image => SafeString::new(&format!("", escape(&self.alt_text), url)),
|
||||
MediaCategory::Image => {
|
||||
SafeString::new(&format!("", escape(&self.alt_text), url))
|
||||
}
|
||||
MediaCategory::Audio | MediaCategory::Video => self.html(conn)?,
|
||||
MediaCategory::Unknown => SafeString::new(""),
|
||||
})
|
||||
@@ -216,7 +220,8 @@ impl Media {
|
||||
.into_iter()
|
||||
.next()?
|
||||
.as_ref(),
|
||||
)?.id,
|
||||
)?
|
||||
.id,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -249,37 +254,41 @@ pub(crate) mod tests {
|
||||
let f2 = "static/media/2.mp3".to_owned();
|
||||
fs::write(f1.clone(), []).unwrap();
|
||||
fs::write(f2.clone(), []).unwrap();
|
||||
(users, vec![
|
||||
NewMedia {
|
||||
file_path: f1,
|
||||
alt_text: "some alt".to_owned(),
|
||||
is_remote: false,
|
||||
remote_url: None,
|
||||
sensitive: false,
|
||||
content_warning: None,
|
||||
owner_id: user_one,
|
||||
},
|
||||
NewMedia {
|
||||
file_path: f2,
|
||||
alt_text: "alt message".to_owned(),
|
||||
is_remote: false,
|
||||
remote_url: None,
|
||||
sensitive: true,
|
||||
content_warning: Some("Content warning".to_owned()),
|
||||
owner_id: user_one,
|
||||
},
|
||||
NewMedia {
|
||||
file_path: "".to_owned(),
|
||||
alt_text: "another alt".to_owned(),
|
||||
is_remote: true,
|
||||
remote_url: Some("https://example.com/".to_owned()),
|
||||
sensitive: false,
|
||||
content_warning: None,
|
||||
owner_id: user_two,
|
||||
},
|
||||
].into_iter()
|
||||
(
|
||||
users,
|
||||
vec![
|
||||
NewMedia {
|
||||
file_path: f1,
|
||||
alt_text: "some alt".to_owned(),
|
||||
is_remote: false,
|
||||
remote_url: None,
|
||||
sensitive: false,
|
||||
content_warning: None,
|
||||
owner_id: user_one,
|
||||
},
|
||||
NewMedia {
|
||||
file_path: f2,
|
||||
alt_text: "alt message".to_owned(),
|
||||
is_remote: false,
|
||||
remote_url: None,
|
||||
sensitive: true,
|
||||
content_warning: Some("Content warning".to_owned()),
|
||||
owner_id: user_one,
|
||||
},
|
||||
NewMedia {
|
||||
file_path: "".to_owned(),
|
||||
alt_text: "another alt".to_owned(),
|
||||
is_remote: true,
|
||||
remote_url: Some("https://example.com/".to_owned()),
|
||||
sensitive: false,
|
||||
content_warning: None,
|
||||
owner_id: user_two,
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.map(|nm| Media::insert(conn, nm).unwrap())
|
||||
.collect())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn clean(conn: &Conn) {
|
||||
@@ -311,7 +320,8 @@ pub(crate) mod tests {
|
||||
content_warning: None,
|
||||
owner_id: user,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(Path::new(&path).exists());
|
||||
media.delete(conn).unwrap();
|
||||
@@ -346,29 +356,26 @@ pub(crate) mod tests {
|
||||
content_warning: None,
|
||||
owner_id: u1.id,
|
||||
},
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
Media::for_user(conn, u1.id).unwrap()
|
||||
.iter()
|
||||
.any(|m| m.id == media.id)
|
||||
);
|
||||
assert!(
|
||||
!Media::for_user(conn, u2.id).unwrap()
|
||||
.iter()
|
||||
.any(|m| m.id == media.id)
|
||||
);
|
||||
assert!(Media::for_user(conn, u1.id)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|m| m.id == media.id));
|
||||
assert!(!Media::for_user(conn, u2.id)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|m| m.id == media.id));
|
||||
media.set_owner(conn, u2).unwrap();
|
||||
assert!(
|
||||
!Media::for_user(conn, u1.id).unwrap()
|
||||
.iter()
|
||||
.any(|m| m.id == media.id)
|
||||
);
|
||||
assert!(
|
||||
Media::for_user(conn, u2.id).unwrap()
|
||||
.iter()
|
||||
.any(|m| m.id == media.id)
|
||||
);
|
||||
assert!(!Media::for_user(conn, u1.id)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|m| m.id == media.id));
|
||||
assert!(Media::for_user(conn, u2.id)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|m| m.id == media.id));
|
||||
|
||||
clean(conn);
|
||||
|
||||
|
||||
@@ -37,11 +37,15 @@ impl Mention {
|
||||
}
|
||||
|
||||
pub fn get_post(&self, conn: &Connection) -> Result<Post> {
|
||||
self.post_id.ok_or(Error::NotFound).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) -> Result<Comment> {
|
||||
self.comment_id.ok_or(Error::NotFound).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) -> Result<User> {
|
||||
@@ -54,21 +58,15 @@ impl Mention {
|
||||
pub fn build_activity(conn: &Connection, ment: &str) -> Result<link::Mention> {
|
||||
let user = User::find_by_fqn(conn, ment)?;
|
||||
let mut mention = link::Mention::default();
|
||||
mention
|
||||
.link_props
|
||||
.set_href_string(user.ap_url)?;
|
||||
mention
|
||||
.link_props
|
||||
.set_name_string(format!("@{}", ment))?;
|
||||
mention.link_props.set_href_string(user.ap_url)?;
|
||||
mention.link_props.set_name_string(format!("@{}", ment))?;
|
||||
Ok(mention)
|
||||
}
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<link::Mention> {
|
||||
let user = self.get_mentioned(conn)?;
|
||||
let mut mention = link::Mention::default();
|
||||
mention
|
||||
.link_props
|
||||
.set_href_string(user.ap_url.clone())?;
|
||||
mention.link_props.set_href_string(user.ap_url.clone())?;
|
||||
mention
|
||||
.link_props
|
||||
.set_name_string(format!("@{}", user.fqn))?;
|
||||
@@ -141,6 +139,7 @@ impl Notify<Connection> for Mention {
|
||||
object_id: self.id,
|
||||
user_id: m.id,
|
||||
},
|
||||
).map(|_| ())
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,24 +80,40 @@ impl Notification {
|
||||
|
||||
pub fn get_url(&self, conn: &Connection) -> Option<String> {
|
||||
match self.kind.as_ref() {
|
||||
notification_kind::COMMENT => self.get_post(conn).and_then(|p| Some(format!("{}#comment-{}", p.url(conn).ok()?, 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).ok()?.fqn)),
|
||||
notification_kind::MENTION => Mention::get(conn, self.object_id).and_then(|mention|
|
||||
mention.get_post(conn).and_then(|p| p.url(conn))
|
||||
.or_else(|_| {
|
||||
let comment = mention.get_comment(conn)?;
|
||||
Ok(format!("{}#comment-{}", comment.get_post(conn)?.url(conn)?, comment.id))
|
||||
})
|
||||
).ok(),
|
||||
notification_kind::MENTION => Mention::get(conn, self.object_id)
|
||||
.and_then(|mention| {
|
||||
mention
|
||||
.get_post(conn)
|
||||
.and_then(|p| p.url(conn))
|
||||
.or_else(|_| {
|
||||
let comment = mention.get_comment(conn)?;
|
||||
Ok(format!(
|
||||
"{}#comment-{}",
|
||||
comment.get_post(conn)?.url(conn)?,
|
||||
comment.id
|
||||
))
|
||||
})
|
||||
})
|
||||
.ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_post(&self, conn: &Connection) -> Option<Post> {
|
||||
match self.kind.as_ref() {
|
||||
notification_kind::COMMENT => Comment::get(conn, self.object_id).and_then(|comment| comment.get_post(conn)).ok(),
|
||||
notification_kind::LIKE => Like::get(conn, self.object_id).and_then(|like| Post::get(conn, like.post_id)).ok(),
|
||||
notification_kind::RESHARE => Reshare::get(conn, self.object_id).and_then(|reshare| reshare.get_post(conn)).ok(),
|
||||
notification_kind::COMMENT => Comment::get(conn, self.object_id)
|
||||
.and_then(|comment| comment.get_post(conn))
|
||||
.ok(),
|
||||
notification_kind::LIKE => Like::get(conn, self.object_id)
|
||||
.and_then(|like| Post::get(conn, like.post_id))
|
||||
.ok(),
|
||||
notification_kind::RESHARE => Reshare::get(conn, self.object_id)
|
||||
.and_then(|reshare| reshare.get_post(conn))
|
||||
.ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -105,7 +121,9 @@ impl Notification {
|
||||
pub fn get_actor(&self, conn: &Connection) -> Result<User> {
|
||||
Ok(match self.kind.as_ref() {
|
||||
notification_kind::COMMENT => Comment::get(conn, self.object_id)?.get_author(conn)?,
|
||||
notification_kind::FOLLOW => User::get(conn, Follow::get(conn, self.object_id)?.follower_id)?,
|
||||
notification_kind::FOLLOW => {
|
||||
User::get(conn, Follow::get(conn, self.object_id)?.follower_id)?
|
||||
}
|
||||
notification_kind::LIKE => User::get(conn, Like::get(conn, self.object_id)?.user_id)?,
|
||||
notification_kind::MENTION => Mention::get(conn, self.object_id)?.get_user(conn)?,
|
||||
notification_kind::RESHARE => Reshare::get(conn, self.object_id)?.get_user(conn)?,
|
||||
|
||||
+248
-190
@@ -1,8 +1,8 @@
|
||||
use activitypub::{
|
||||
CustomObject,
|
||||
activity::{Create, Delete, Update},
|
||||
link,
|
||||
object::{Article, Image, Tombstone},
|
||||
CustomObject,
|
||||
};
|
||||
use canapi::{Error as ApiError, Provider};
|
||||
use chrono::{NaiveDateTime, TimeZone, Utc};
|
||||
@@ -12,25 +12,26 @@ use scheduled_thread_pool::ScheduledThreadPool as Worker;
|
||||
use serde_json;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use plume_api::posts::PostEndpoint;
|
||||
use plume_common::{
|
||||
activity_pub::{
|
||||
inbox::{Deletable, FromActivity},
|
||||
broadcast, Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILTY,
|
||||
},
|
||||
utils::md_to_html,
|
||||
};
|
||||
use blogs::Blog;
|
||||
use instance::Instance;
|
||||
use medias::Media;
|
||||
use mentions::Mention;
|
||||
use plume_api::posts::PostEndpoint;
|
||||
use plume_common::{
|
||||
activity_pub::{
|
||||
broadcast,
|
||||
inbox::{Deletable, FromActivity},
|
||||
Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILTY,
|
||||
},
|
||||
utils::md_to_html,
|
||||
};
|
||||
use post_authors::*;
|
||||
use safe_string::SafeString;
|
||||
use search::Searcher;
|
||||
use schema::posts;
|
||||
use search::Searcher;
|
||||
use tags::*;
|
||||
use users::User;
|
||||
use {ap_url, Connection, BASE_URL, Error, Result, ApiResult};
|
||||
use {ap_url, ApiResult, Connection, Error, Result, BASE_URL};
|
||||
|
||||
pub type LicensedArticle = CustomObject<Licensed, Article>;
|
||||
|
||||
@@ -75,7 +76,11 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
|
||||
id: i32,
|
||||
) -> ApiResult<PostEndpoint> {
|
||||
if let Ok(post) = Post::get(conn, id) {
|
||||
if !post.published && !user_id.map(|u| post.is_author(conn, u).unwrap_or(false)).unwrap_or(false) {
|
||||
if !post.published
|
||||
&& !user_id
|
||||
.map(|u| post.is_author(conn, u).unwrap_or(false))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(ApiError::Authorization(
|
||||
"You are not authorized to access this post yet.".to_string(),
|
||||
));
|
||||
@@ -86,12 +91,23 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
|
||||
subtitle: Some(post.subtitle.clone()),
|
||||
content: Some(post.content.get().clone()),
|
||||
source: Some(post.source.clone()),
|
||||
author: Some(post.get_authors(conn).map_err(|_| ApiError::NotFound("Authors not found".into()))?[0].username.clone()),
|
||||
author: Some(
|
||||
post.get_authors(conn)
|
||||
.map_err(|_| ApiError::NotFound("Authors not found".into()))?[0]
|
||||
.username
|
||||
.clone(),
|
||||
),
|
||||
blog_id: Some(post.blog_id),
|
||||
published: Some(post.published),
|
||||
creation_date: Some(post.creation_date.format("%Y-%m-%d").to_string()),
|
||||
license: Some(post.license.clone()),
|
||||
tags: Some(Tag::for_post(conn, post.id).map_err(|_| ApiError::NotFound("Tags not found".into()))?.into_iter().map(|t| t.tag).collect()),
|
||||
tags: Some(
|
||||
Tag::for_post(conn, post.id)
|
||||
.map_err(|_| ApiError::NotFound("Tags not found".into()))?
|
||||
.into_iter()
|
||||
.map(|t| t.tag)
|
||||
.collect(),
|
||||
),
|
||||
cover_id: post.cover_id,
|
||||
})
|
||||
} else {
|
||||
@@ -114,24 +130,39 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
|
||||
query = query.filter(posts::content.eq(content));
|
||||
}
|
||||
|
||||
query.get_results::<Post>(*conn).map(|ps| ps.into_iter()
|
||||
.filter(|p| p.published || user_id.map(|u| p.is_author(conn, u).unwrap_or(false)).unwrap_or(false))
|
||||
.map(|p| PostEndpoint {
|
||||
id: Some(p.id),
|
||||
title: Some(p.title.clone()),
|
||||
subtitle: Some(p.subtitle.clone()),
|
||||
content: Some(p.content.get().clone()),
|
||||
source: Some(p.source.clone()),
|
||||
author: Some(p.get_authors(conn).unwrap_or_default()[0].username.clone()),
|
||||
blog_id: Some(p.blog_id),
|
||||
published: Some(p.published),
|
||||
creation_date: Some(p.creation_date.format("%Y-%m-%d").to_string()),
|
||||
license: Some(p.license.clone()),
|
||||
tags: Some(Tag::for_post(conn, p.id).unwrap_or_else(|_| vec![]).into_iter().map(|t| t.tag).collect()),
|
||||
cover_id: p.cover_id,
|
||||
query
|
||||
.get_results::<Post>(*conn)
|
||||
.map(|ps| {
|
||||
ps.into_iter()
|
||||
.filter(|p| {
|
||||
p.published
|
||||
|| user_id
|
||||
.map(|u| p.is_author(conn, u).unwrap_or(false))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.map(|p| PostEndpoint {
|
||||
id: Some(p.id),
|
||||
title: Some(p.title.clone()),
|
||||
subtitle: Some(p.subtitle.clone()),
|
||||
content: Some(p.content.get().clone()),
|
||||
source: Some(p.source.clone()),
|
||||
author: Some(p.get_authors(conn).unwrap_or_default()[0].username.clone()),
|
||||
blog_id: Some(p.blog_id),
|
||||
published: Some(p.published),
|
||||
creation_date: Some(p.creation_date.format("%Y-%m-%d").to_string()),
|
||||
license: Some(p.license.clone()),
|
||||
tags: Some(
|
||||
Tag::for_post(conn, p.id)
|
||||
.unwrap_or_else(|_| vec![])
|
||||
.into_iter()
|
||||
.map(|t| t.tag)
|
||||
.collect(),
|
||||
),
|
||||
cover_id: p.cover_id,
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
).unwrap_or_else(|_| vec![])
|
||||
.unwrap_or_else(|_| vec![])
|
||||
}
|
||||
|
||||
fn update(
|
||||
@@ -142,11 +173,15 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn delete((conn, _worker, search, user_id): &(&Connection, &Worker, &Searcher, Option<i32>), id: i32) {
|
||||
fn delete(
|
||||
(conn, _worker, search, user_id): &(&Connection, &Worker, &Searcher, Option<i32>),
|
||||
id: i32,
|
||||
) {
|
||||
let user_id = user_id.expect("Post as Provider::delete: not authenticated");
|
||||
if let Ok(post) = Post::get(conn, id) {
|
||||
if post.is_author(conn, user_id).unwrap_or(false) {
|
||||
post.delete(&(conn, search)).expect("Post as Provider::delete: delete error");
|
||||
post.delete(&(conn, search))
|
||||
.expect("Post as Provider::delete: delete error");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,84 +191,124 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
|
||||
query: PostEndpoint,
|
||||
) -> ApiResult<PostEndpoint> {
|
||||
if user_id.is_none() {
|
||||
return Err(ApiError::Authorization("You are not authorized to create new articles.".to_string()));
|
||||
return Err(ApiError::Authorization(
|
||||
"You are not authorized to create new articles.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let title = query.title.clone().expect("No title for new post in API");
|
||||
let slug = query.title.unwrap().to_kebab_case();
|
||||
|
||||
let date = query.creation_date.clone()
|
||||
.and_then(|d| NaiveDateTime::parse_from_str(format!("{} 00:00:00", d).as_ref(), "%Y-%m-%d %H:%M:%S").ok());
|
||||
let date = query.creation_date.clone().and_then(|d| {
|
||||
NaiveDateTime::parse_from_str(format!("{} 00:00:00", d).as_ref(), "%Y-%m-%d %H:%M:%S")
|
||||
.ok()
|
||||
});
|
||||
|
||||
let domain = &Instance::get_local(&conn)
|
||||
.map_err(|_| ApiError::NotFound("posts::update: Error getting local instance".into()))?
|
||||
.public_domain;
|
||||
let (content, mentions, hashtags) = md_to_html(query.source.clone().unwrap_or_default().clone().as_ref(), domain);
|
||||
let (content, mentions, hashtags) = md_to_html(
|
||||
query.source.clone().unwrap_or_default().clone().as_ref(),
|
||||
domain,
|
||||
);
|
||||
|
||||
let author = User::get(conn, user_id.expect("<Post as Provider>::create: no user_id error"))
|
||||
.map_err(|_| ApiError::NotFound("Author not found".into()))?;
|
||||
let author = User::get(
|
||||
conn,
|
||||
user_id.expect("<Post as Provider>::create: no user_id error"),
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Author not found".into()))?;
|
||||
let blog = match query.blog_id {
|
||||
Some(x) => x,
|
||||
None => Blog::find_for_author(conn, &author).map_err(|_| ApiError::NotFound("No default blog".into()))?[0].id
|
||||
None => {
|
||||
Blog::find_for_author(conn, &author)
|
||||
.map_err(|_| ApiError::NotFound("No default blog".into()))?[0]
|
||||
.id
|
||||
}
|
||||
};
|
||||
|
||||
if Post::find_by_slug(conn, &slug, blog).is_ok() {
|
||||
// Not an actual authorization problem, but we have nothing better for now…
|
||||
// TODO: add another error variant to canapi and add it there
|
||||
return Err(ApiError::Authorization("A post with the same slug already exists".to_string()));
|
||||
return Err(ApiError::Authorization(
|
||||
"A post with the same slug already exists".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let post = Post::insert(conn, NewPost {
|
||||
blog_id: blog,
|
||||
slug,
|
||||
title,
|
||||
content: SafeString::new(content.as_ref()),
|
||||
published: query.published.unwrap_or(true),
|
||||
license: query.license.unwrap_or_else(|| Instance::get_local(conn)
|
||||
.map(|i| i.default_license)
|
||||
.unwrap_or_else(|_| String::from("CC-BY-SA"))),
|
||||
creation_date: date,
|
||||
ap_url: String::new(),
|
||||
subtitle: query.subtitle.unwrap_or_default(),
|
||||
source: query.source.expect("Post API::create: no source error"),
|
||||
cover_id: query.cover_id,
|
||||
}, search).map_err(|_| ApiError::NotFound("Creation error".into()))?;
|
||||
let post = Post::insert(
|
||||
conn,
|
||||
NewPost {
|
||||
blog_id: blog,
|
||||
slug,
|
||||
title,
|
||||
content: SafeString::new(content.as_ref()),
|
||||
published: query.published.unwrap_or(true),
|
||||
license: query.license.unwrap_or_else(|| {
|
||||
Instance::get_local(conn)
|
||||
.map(|i| i.default_license)
|
||||
.unwrap_or_else(|_| String::from("CC-BY-SA"))
|
||||
}),
|
||||
creation_date: date,
|
||||
ap_url: String::new(),
|
||||
subtitle: query.subtitle.unwrap_or_default(),
|
||||
source: query.source.expect("Post API::create: no source error"),
|
||||
cover_id: query.cover_id,
|
||||
},
|
||||
search,
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Creation error".into()))?;
|
||||
|
||||
PostAuthor::insert(conn, NewPostAuthor {
|
||||
author_id: author.id,
|
||||
post_id: post.id
|
||||
}).map_err(|_| ApiError::NotFound("Error saving authors".into()))?;
|
||||
PostAuthor::insert(
|
||||
conn,
|
||||
NewPostAuthor {
|
||||
author_id: author.id,
|
||||
post_id: post.id,
|
||||
},
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Error saving authors".into()))?;
|
||||
|
||||
if let Some(tags) = query.tags {
|
||||
for tag in tags {
|
||||
Tag::insert(conn, NewTag {
|
||||
tag,
|
||||
is_hashtag: false,
|
||||
post_id: post.id
|
||||
}).map_err(|_| ApiError::NotFound("Error saving tags".into()))?;
|
||||
Tag::insert(
|
||||
conn,
|
||||
NewTag {
|
||||
tag,
|
||||
is_hashtag: false,
|
||||
post_id: post.id,
|
||||
},
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Error saving tags".into()))?;
|
||||
}
|
||||
}
|
||||
for hashtag in hashtags {
|
||||
Tag::insert(conn, NewTag {
|
||||
tag: hashtag.to_camel_case(),
|
||||
is_hashtag: true,
|
||||
post_id: post.id
|
||||
}).map_err(|_| ApiError::NotFound("Error saving hashtags".into()))?;
|
||||
Tag::insert(
|
||||
conn,
|
||||
NewTag {
|
||||
tag: hashtag.to_camel_case(),
|
||||
is_hashtag: true,
|
||||
post_id: post.id,
|
||||
},
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Error saving hashtags".into()))?;
|
||||
}
|
||||
|
||||
if post.published {
|
||||
for m in mentions.into_iter() {
|
||||
Mention::from_activity(
|
||||
&*conn,
|
||||
&Mention::build_activity(&*conn, &m).map_err(|_| ApiError::NotFound("Couldn't build mentions".into()))?,
|
||||
&Mention::build_activity(&*conn, &m)
|
||||
.map_err(|_| ApiError::NotFound("Couldn't build mentions".into()))?,
|
||||
post.id,
|
||||
true,
|
||||
true
|
||||
).map_err(|_| ApiError::NotFound("Error saving mentions".into()))?;
|
||||
true,
|
||||
)
|
||||
.map_err(|_| ApiError::NotFound("Error saving mentions".into()))?;
|
||||
}
|
||||
|
||||
let act = post.create_activity(&*conn).map_err(|_| ApiError::NotFound("Couldn't create activity".into()))?;
|
||||
let dest = User::one_by_instance(&*conn).map_err(|_| ApiError::NotFound("Couldn't list remote instances".into()))?;
|
||||
let act = post
|
||||
.create_activity(&*conn)
|
||||
.map_err(|_| ApiError::NotFound("Couldn't create activity".into()))?;
|
||||
let dest = User::one_by_instance(&*conn)
|
||||
.map_err(|_| ApiError::NotFound("Couldn't list remote instances".into()))?;
|
||||
worker.execute(move || broadcast(&author, act, dest));
|
||||
}
|
||||
|
||||
@@ -243,12 +318,23 @@ impl<'a> Provider<(&'a Connection, &'a Worker, &'a Searcher, Option<i32>)> for P
|
||||
subtitle: Some(post.subtitle.clone()),
|
||||
content: Some(post.content.get().clone()),
|
||||
source: Some(post.source.clone()),
|
||||
author: Some(post.get_authors(conn).map_err(|_| ApiError::NotFound("No authors".into()))?[0].username.clone()),
|
||||
author: Some(
|
||||
post.get_authors(conn)
|
||||
.map_err(|_| ApiError::NotFound("No authors".into()))?[0]
|
||||
.username
|
||||
.clone(),
|
||||
),
|
||||
blog_id: Some(post.blog_id),
|
||||
published: Some(post.published),
|
||||
creation_date: Some(post.creation_date.format("%Y-%m-%d").to_string()),
|
||||
license: Some(post.license.clone()),
|
||||
tags: Some(Tag::for_post(conn, post.id).map_err(|_| ApiError::NotFound("Tags not found".into()))?.into_iter().map(|t| t.tag).collect()),
|
||||
tags: Some(
|
||||
Tag::for_post(conn, post.id)
|
||||
.map_err(|_| ApiError::NotFound("Tags not found".into()))?
|
||||
.into_iter()
|
||||
.map(|t| t.tag)
|
||||
.collect(),
|
||||
),
|
||||
cover_id: post.cover_id,
|
||||
})
|
||||
}
|
||||
@@ -279,15 +365,17 @@ impl Post {
|
||||
Ok(post)
|
||||
}
|
||||
pub fn update(&self, conn: &Connection, searcher: &Searcher) -> Result<Self> {
|
||||
diesel::update(self)
|
||||
.set(self)
|
||||
.execute(conn)?;
|
||||
diesel::update(self).set(self).execute(conn)?;
|
||||
let post = Self::get(conn, self.id)?;
|
||||
searcher.update_document(conn, &post)?;
|
||||
Ok(post)
|
||||
}
|
||||
|
||||
pub fn list_by_tag(conn: &Connection, tag: String, (min, max): (i32, i32)) -> Result<Vec<Post>> {
|
||||
pub fn list_by_tag(
|
||||
conn: &Connection,
|
||||
tag: String,
|
||||
(min, max): (i32, i32),
|
||||
) -> Result<Vec<Post>> {
|
||||
use schema::tags;
|
||||
|
||||
let ids = tags::table.filter(tags::tag.eq(tag)).select(tags::post_id);
|
||||
@@ -349,7 +437,11 @@ impl Post {
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn get_recents_for_author(conn: &Connection, author: &User, limit: i64) -> Result<Vec<Post>> {
|
||||
pub fn get_recents_for_author(
|
||||
conn: &Connection,
|
||||
author: &User,
|
||||
limit: i64,
|
||||
) -> Result<Vec<Post>> {
|
||||
use schema::post_authors;
|
||||
|
||||
let posts = PostAuthor::belonging_to(author).select(post_authors::post_id);
|
||||
@@ -481,7 +573,8 @@ impl Post {
|
||||
Ok(PostAuthor::belonging_to(self)
|
||||
.filter(post_authors::author_id.eq(author_id))
|
||||
.count()
|
||||
.get_result::<i64>(conn)? > 0)
|
||||
.get_result::<i64>(conn)?
|
||||
> 0)
|
||||
}
|
||||
|
||||
pub fn get_blog(&self, conn: &Connection) -> Result<Blog> {
|
||||
@@ -529,7 +622,7 @@ impl Post {
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<LicensedArticle> {
|
||||
let cc = self.get_receivers_urls(conn)?;
|
||||
let to = vec![PUBLIC_VISIBILTY.to_string()];
|
||||
let to = vec![PUBLIC_VISIBILTY.to_string()];
|
||||
|
||||
let mut mentions_json = Mention::list_for_post(conn, self.id)?
|
||||
.into_iter()
|
||||
@@ -542,12 +635,8 @@ impl Post {
|
||||
mentions_json.append(&mut tags_json);
|
||||
|
||||
let mut article = Article::default();
|
||||
article
|
||||
.object_props
|
||||
.set_name_string(self.title.clone())?;
|
||||
article
|
||||
.object_props
|
||||
.set_id_string(self.ap_url.clone())?;
|
||||
article.object_props.set_name_string(self.title.clone())?;
|
||||
article.object_props.set_id_string(self.ap_url.clone())?;
|
||||
|
||||
let mut authors = self
|
||||
.get_authors(conn)?
|
||||
@@ -561,12 +650,10 @@ impl Post {
|
||||
article
|
||||
.object_props
|
||||
.set_content_string(self.content.get().clone())?;
|
||||
article
|
||||
.ap_object_props
|
||||
.set_source_object(Source {
|
||||
content: self.source.clone(),
|
||||
media_type: String::from("text/markdown"),
|
||||
})?;
|
||||
article.ap_object_props.set_source_object(Source {
|
||||
content: self.source.clone(),
|
||||
media_type: String::from("text/markdown"),
|
||||
})?;
|
||||
article
|
||||
.object_props
|
||||
.set_published_utctime(Utc.from_utc_datetime(&self.creation_date))?;
|
||||
@@ -578,31 +665,20 @@ impl Post {
|
||||
if let Some(media_id) = self.cover_id {
|
||||
let media = Media::get(conn, media_id)?;
|
||||
let mut cover = Image::default();
|
||||
cover
|
||||
.object_props
|
||||
.set_url_string(media.url(conn)?)?;
|
||||
cover.object_props.set_url_string(media.url(conn)?)?;
|
||||
if media.sensitive {
|
||||
cover
|
||||
.object_props
|
||||
.set_summary_string(media.content_warning.unwrap_or_default())?;
|
||||
}
|
||||
cover.object_props.set_content_string(media.alt_text)?;
|
||||
cover
|
||||
.object_props
|
||||
.set_content_string(media.alt_text)?;
|
||||
cover
|
||||
.object_props
|
||||
.set_attributed_to_link_vec(vec![
|
||||
User::get(conn, media.owner_id)?
|
||||
.into_id(),
|
||||
])?;
|
||||
article
|
||||
.object_props
|
||||
.set_icon_object(cover)?;
|
||||
.set_attributed_to_link_vec(vec![User::get(conn, media.owner_id)?.into_id()])?;
|
||||
article.object_props.set_icon_object(cover)?;
|
||||
}
|
||||
|
||||
article
|
||||
.object_props
|
||||
.set_url_string(self.ap_url.clone())?;
|
||||
article.object_props.set_url_string(self.ap_url.clone())?;
|
||||
article
|
||||
.object_props
|
||||
.set_to_link_vec::<Id>(to.into_iter().map(Id::new).collect())?;
|
||||
@@ -620,52 +696,39 @@ impl Post {
|
||||
act.object_props
|
||||
.set_id_string(format!("{}activity", self.ap_url))?;
|
||||
act.object_props
|
||||
.set_to_link_vec::<Id>(
|
||||
article.object
|
||||
.object_props
|
||||
.to_link_vec()?,
|
||||
)?;
|
||||
.set_to_link_vec::<Id>(article.object.object_props.to_link_vec()?)?;
|
||||
act.object_props
|
||||
.set_cc_link_vec::<Id>(
|
||||
article.object
|
||||
.object_props
|
||||
.cc_link_vec()?,
|
||||
)?;
|
||||
.set_cc_link_vec::<Id>(article.object.object_props.cc_link_vec()?)?;
|
||||
act.create_props
|
||||
.set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?;
|
||||
act.create_props
|
||||
.set_object_object(article)?;
|
||||
act.create_props.set_object_object(article)?;
|
||||
Ok(act)
|
||||
}
|
||||
|
||||
pub fn update_activity(&self, conn: &Connection) -> Result<Update> {
|
||||
let article = self.to_activity(conn)?;
|
||||
let mut act = Update::default();
|
||||
act.object_props.set_id_string(format!(
|
||||
"{}/update-{}",
|
||||
self.ap_url,
|
||||
Utc::now().timestamp()
|
||||
))?;
|
||||
act.object_props
|
||||
.set_id_string(format!("{}/update-{}", self.ap_url, Utc::now().timestamp()))?;
|
||||
.set_to_link_vec::<Id>(article.object.object_props.to_link_vec()?)?;
|
||||
act.object_props
|
||||
.set_to_link_vec::<Id>(
|
||||
article.object
|
||||
.object_props
|
||||
.to_link_vec()?,
|
||||
)?;
|
||||
act.object_props
|
||||
.set_cc_link_vec::<Id>(
|
||||
article.object
|
||||
.object_props
|
||||
.cc_link_vec()?,
|
||||
)?;
|
||||
.set_cc_link_vec::<Id>(article.object.object_props.cc_link_vec()?)?;
|
||||
act.update_props
|
||||
.set_actor_link(Id::new(self.get_authors(conn)?[0].clone().ap_url))?;
|
||||
act.update_props
|
||||
.set_object_object(article)?;
|
||||
act.update_props.set_object_object(article)?;
|
||||
Ok(act)
|
||||
}
|
||||
|
||||
pub fn handle_update(conn: &Connection, updated: &LicensedArticle, searcher: &Searcher) -> Result<()> {
|
||||
let id = updated.object
|
||||
.object_props
|
||||
.id_string()?;
|
||||
pub fn handle_update(
|
||||
conn: &Connection,
|
||||
updated: &LicensedArticle,
|
||||
searcher: &Searcher,
|
||||
) -> Result<()> {
|
||||
let id = updated.object.object_props.id_string()?;
|
||||
let mut post = Post::find_by_ap_url(conn, &id)?;
|
||||
|
||||
if let Ok(title) = updated.object.object_props.name_string() {
|
||||
@@ -698,7 +761,9 @@ impl Post {
|
||||
.into_iter()
|
||||
.map(|s| s.to_camel_case())
|
||||
.collect::<HashSet<_>>();
|
||||
if let Some(serde_json::Value::Array(mention_tags)) = updated.object.object_props.tag.clone() {
|
||||
if let Some(serde_json::Value::Array(mention_tags)) =
|
||||
updated.object.object_props.tag.clone()
|
||||
{
|
||||
let mut mentions = vec![];
|
||||
let mut tags = vec![];
|
||||
let mut hashtags = vec![];
|
||||
@@ -710,8 +775,7 @@ impl Post {
|
||||
serde_json::from_value::<Hashtag>(tag.clone())
|
||||
.map_err(Error::from)
|
||||
.and_then(|t| {
|
||||
let tag_name = t
|
||||
.name_string()?;
|
||||
let tag_name = t.name_string()?;
|
||||
if txt_hashtags.remove(&tag_name) {
|
||||
hashtags.push(t);
|
||||
} else {
|
||||
@@ -854,20 +918,25 @@ impl Post {
|
||||
}
|
||||
|
||||
pub fn cover_url(&self, conn: &Connection) -> Option<String> {
|
||||
self.cover_id.and_then(|i| Media::get(conn, i).ok()).and_then(|c| c.url(conn).ok())
|
||||
self.cover_id
|
||||
.and_then(|i| Media::get(conn, i).ok())
|
||||
.and_then(|c| c.url(conn).ok())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post {
|
||||
type Error = Error;
|
||||
|
||||
fn from_activity((conn, searcher): &(&'a Connection, &'a Searcher), article: LicensedArticle, _actor: Id) -> Result<Post> {
|
||||
fn from_activity(
|
||||
(conn, searcher): &(&'a Connection, &'a Searcher),
|
||||
article: LicensedArticle,
|
||||
_actor: Id,
|
||||
) -> Result<Post> {
|
||||
let license = article.custom_props.license_string().unwrap_or_default();
|
||||
let article = article.object;
|
||||
if let Ok(post) = Post::find_by_ap_url(
|
||||
conn,
|
||||
&article.object_props.id_string().unwrap_or_default(),
|
||||
) {
|
||||
if let Ok(post) =
|
||||
Post::find_by_ap_url(conn, &article.object_props.id_string().unwrap_or_default())
|
||||
{
|
||||
Ok(post)
|
||||
} else {
|
||||
let (blog, authors) = article
|
||||
@@ -880,10 +949,8 @@ impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post
|
||||
Ok(u) => {
|
||||
authors.push(u);
|
||||
(blog, authors)
|
||||
},
|
||||
Err(_) => {
|
||||
(blog.or_else(|| Blog::from_url(conn, &url).ok()), authors)
|
||||
},
|
||||
}
|
||||
Err(_) => (blog.or_else(|| Blog::from_url(conn, &url).ok()), authors),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -893,41 +960,24 @@ impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post
|
||||
.ok()
|
||||
.and_then(|img| Media::from_activity(conn, &img).ok().map(|m| m.id));
|
||||
|
||||
let title = article
|
||||
.object_props
|
||||
.name_string()?;
|
||||
let title = article.object_props.name_string()?;
|
||||
let post = Post::insert(
|
||||
conn,
|
||||
NewPost {
|
||||
blog_id: blog?.id,
|
||||
slug: title.to_kebab_case(),
|
||||
title,
|
||||
content: SafeString::new(
|
||||
&article
|
||||
.object_props
|
||||
.content_string()?,
|
||||
),
|
||||
content: SafeString::new(&article.object_props.content_string()?),
|
||||
published: true,
|
||||
license,
|
||||
// FIXME: This is wrong: with this logic, we may use the display URL as the AP ID. We need two different fields
|
||||
ap_url: article.object_props.url_string().or_else(|_|
|
||||
article
|
||||
.object_props
|
||||
.id_string()
|
||||
)?,
|
||||
creation_date: Some(
|
||||
article
|
||||
.object_props
|
||||
.published_utctime()?
|
||||
.naive_utc(),
|
||||
),
|
||||
subtitle: article
|
||||
ap_url: article
|
||||
.object_props
|
||||
.summary_string()?,
|
||||
source: article
|
||||
.ap_object_props
|
||||
.source_object::<Source>()?
|
||||
.content,
|
||||
.url_string()
|
||||
.or_else(|_| article.object_props.id_string())?,
|
||||
creation_date: Some(article.object_props.published_utctime()?.naive_utc()),
|
||||
subtitle: article.object_props.summary_string()?,
|
||||
source: article.ap_object_props.source_object::<Source>()?.content,
|
||||
cover_id: cover,
|
||||
},
|
||||
searcher,
|
||||
@@ -959,7 +1009,12 @@ impl<'a> FromActivity<LicensedArticle, (&'a Connection, &'a Searcher)> for Post
|
||||
.map_err(Error::from)
|
||||
.and_then(|t| {
|
||||
let tag_name = t.name_string()?;
|
||||
Ok(Tag::from_activity(conn, &t, post.id, hashtags.remove(&tag_name)))
|
||||
Ok(Tag::from_activity(
|
||||
conn,
|
||||
&t,
|
||||
post.id,
|
||||
hashtags.remove(&tag_name),
|
||||
))
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -978,11 +1033,8 @@ impl<'a> Deletable<(&'a Connection, &'a Searcher), Delete> for Post {
|
||||
.set_actor_link(self.get_authors(conn)?[0].clone().into_id())?;
|
||||
|
||||
let mut tombstone = Tombstone::default();
|
||||
tombstone
|
||||
.object_props
|
||||
.set_id_string(self.ap_url.clone())?;
|
||||
act.delete_props
|
||||
.set_object_object(tombstone)?;
|
||||
tombstone.object_props.set_id_string(self.ap_url.clone())?;
|
||||
act.delete_props.set_object_object(tombstone)?;
|
||||
|
||||
act.object_props
|
||||
.set_id_string(format!("{}#delete", self.ap_url))?;
|
||||
@@ -992,16 +1044,22 @@ impl<'a> Deletable<(&'a Connection, &'a Searcher), Delete> for Post {
|
||||
for m in Mention::list_for_post(&conn, self.id)? {
|
||||
m.delete(conn)?;
|
||||
}
|
||||
diesel::delete(self)
|
||||
.execute(*conn)?;
|
||||
diesel::delete(self).execute(*conn)?;
|
||||
searcher.delete_document(self);
|
||||
Ok(act)
|
||||
}
|
||||
|
||||
fn delete_id(id: &str, actor_id: &str, (conn, searcher): &(&Connection, &Searcher)) -> Result<Delete> {
|
||||
fn delete_id(
|
||||
id: &str,
|
||||
actor_id: &str,
|
||||
(conn, searcher): &(&Connection, &Searcher),
|
||||
) -> Result<Delete> {
|
||||
let actor = User::find_by_ap_url(conn, actor_id)?;
|
||||
let post = Post::find_by_ap_url(conn, id)?;
|
||||
let can_delete = post.get_authors(conn)?.into_iter().any(|a| actor.id == a.id);
|
||||
let can_delete = post
|
||||
.get_authors(conn)?
|
||||
.into_iter()
|
||||
.any(|a| actor.id == a.id);
|
||||
if can_delete {
|
||||
post.delete(&(conn, searcher))
|
||||
} else {
|
||||
|
||||
@@ -40,7 +40,11 @@ impl Reshare {
|
||||
post_id as i32
|
||||
);
|
||||
|
||||
pub fn get_recents_for_author(conn: &Connection, user: &User, limit: i64) -> Result<Vec<Reshare>> {
|
||||
pub fn get_recents_for_author(
|
||||
conn: &Connection,
|
||||
user: &User,
|
||||
limit: i64,
|
||||
) -> Result<Vec<Reshare>> {
|
||||
reshares::table
|
||||
.filter(reshares::user_id.eq(user.id))
|
||||
.order(reshares::creation_date.desc())
|
||||
@@ -63,12 +67,10 @@ impl Reshare {
|
||||
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
||||
act.announce_props
|
||||
.set_object_link(Post::get(conn, self.post_id)?.into_id())?;
|
||||
act.object_props
|
||||
.set_id_string(self.ap_url.clone())?;
|
||||
act.object_props.set_id_string(self.ap_url.clone())?;
|
||||
act.object_props
|
||||
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
|
||||
act.object_props
|
||||
.set_cc_link_vec::<Id>(vec![])?;
|
||||
act.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||
|
||||
Ok(act)
|
||||
}
|
||||
@@ -78,29 +80,15 @@ impl FromActivity<Announce, Connection> for Reshare {
|
||||
type Error = Error;
|
||||
|
||||
fn from_activity(conn: &Connection, announce: Announce, _actor: Id) -> Result<Reshare> {
|
||||
let user = User::from_url(
|
||||
conn,
|
||||
announce
|
||||
.announce_props
|
||||
.actor_link::<Id>()?
|
||||
.as_ref(),
|
||||
)?;
|
||||
let post = Post::find_by_ap_url(
|
||||
conn,
|
||||
announce
|
||||
.announce_props
|
||||
.object_link::<Id>()?
|
||||
.as_ref(),
|
||||
)?;
|
||||
let user = User::from_url(conn, announce.announce_props.actor_link::<Id>()?.as_ref())?;
|
||||
let post =
|
||||
Post::find_by_ap_url(conn, announce.announce_props.object_link::<Id>()?.as_ref())?;
|
||||
let reshare = Reshare::insert(
|
||||
conn,
|
||||
NewReshare {
|
||||
post_id: post.id,
|
||||
user_id: user.id,
|
||||
ap_url: announce
|
||||
.object_props
|
||||
.id_string()
|
||||
.unwrap_or_default(),
|
||||
ap_url: announce.object_props.id_string().unwrap_or_default(),
|
||||
},
|
||||
)?;
|
||||
reshare.notify(conn)?;
|
||||
@@ -131,26 +119,22 @@ impl Deletable<Connection, Undo> for Reshare {
|
||||
type Error = Error;
|
||||
|
||||
fn delete(&self, conn: &Connection) -> Result<Undo> {
|
||||
diesel::delete(self)
|
||||
.execute(conn)?;
|
||||
diesel::delete(self).execute(conn)?;
|
||||
|
||||
// delete associated notification if any
|
||||
if let Ok(notif) = Notification::find(conn, notification_kind::RESHARE, self.id) {
|
||||
diesel::delete(¬if)
|
||||
.execute(conn)?;
|
||||
diesel::delete(¬if).execute(conn)?;
|
||||
}
|
||||
|
||||
let mut act = Undo::default();
|
||||
act.undo_props
|
||||
.set_actor_link(User::get(conn, self.user_id)?.into_id())?;
|
||||
act.undo_props
|
||||
.set_object_object(self.to_activity(conn)?)?;
|
||||
act.undo_props.set_object_object(self.to_activity(conn)?)?;
|
||||
act.object_props
|
||||
.set_id_string(format!("{}#delete", self.ap_url))?;
|
||||
act.object_props
|
||||
.set_to_link(Id::new(PUBLIC_VISIBILTY.to_string()))?;
|
||||
act.object_props
|
||||
.set_cc_link_vec::<Id>(vec![])?;
|
||||
act.object_props.set_cc_link_vec::<Id>(vec![])?;
|
||||
|
||||
Ok(act)
|
||||
}
|
||||
@@ -172,7 +156,7 @@ impl NewReshare {
|
||||
NewReshare {
|
||||
post_id: p.id,
|
||||
user_id: u.id,
|
||||
ap_url
|
||||
ap_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,21 +19,15 @@ lazy_static! {
|
||||
static ref CLEAN: Builder<'static> = {
|
||||
let mut b = Builder::new();
|
||||
b.add_generic_attributes(iter::once("id"))
|
||||
.add_tags(&[ "iframe", "video", "audio" ])
|
||||
.add_tags(&["iframe", "video", "audio"])
|
||||
.id_prefix(Some("postcontent-"))
|
||||
.url_relative(UrlRelative::Custom(Box::new(url_add_prefix)))
|
||||
.add_tag_attributes(
|
||||
"iframe",
|
||||
[ "width", "height", "src", "frameborder" ].iter().cloned(),
|
||||
["width", "height", "src", "frameborder"].iter().cloned(),
|
||||
)
|
||||
.add_tag_attributes(
|
||||
"video",
|
||||
[ "src", "title", "controls" ].iter(),
|
||||
)
|
||||
.add_tag_attributes(
|
||||
"audio",
|
||||
[ "src", "title", "controls" ].iter(),
|
||||
);
|
||||
.add_tag_attributes("video", ["src", "title", "controls"].iter())
|
||||
.add_tag_attributes("audio", ["src", "title", "controls"].iter());
|
||||
b
|
||||
};
|
||||
}
|
||||
@@ -69,7 +63,7 @@ impl SafeString {
|
||||
/// Prefer `SafeString::new` as much as possible.
|
||||
pub fn trusted(value: impl AsRef<str>) -> Self {
|
||||
SafeString {
|
||||
value: value.as_ref().to_string()
|
||||
value: value.as_ref().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
mod searcher;
|
||||
mod query;
|
||||
mod searcher;
|
||||
mod tokenizer;
|
||||
pub use self::searcher::*;
|
||||
pub use self::query::PlumeQuery as Query;
|
||||
|
||||
pub use self::searcher::*;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::{Query, Searcher};
|
||||
use diesel::Connection;
|
||||
use std::env::temp_dir;
|
||||
use std::str::FromStr;
|
||||
use diesel::Connection;
|
||||
|
||||
use blogs::tests::fill_database;
|
||||
use plume_common::activity_pub::inbox::Deletable;
|
||||
use plume_common::utils::random_hex;
|
||||
use blogs::tests::fill_database;
|
||||
use posts::{NewPost, Post};
|
||||
use post_authors::*;
|
||||
use posts::{NewPost, Post};
|
||||
use safe_string::SafeString;
|
||||
use tests::db;
|
||||
|
||||
|
||||
pub(crate) fn get_searcher() -> Searcher {
|
||||
let dir = temp_dir().join("plume-test");
|
||||
if dir.exists() {
|
||||
Searcher::open(&dir)
|
||||
} else {
|
||||
Searcher::create(&dir)
|
||||
}.unwrap()
|
||||
}
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -98,7 +97,9 @@ pub(crate) mod tests {
|
||||
|
||||
#[test]
|
||||
fn open() {
|
||||
{get_searcher()};//make sure $tmp/plume-test-tantivy exist
|
||||
{
|
||||
get_searcher()
|
||||
}; //make sure $tmp/plume-test-tantivy exist
|
||||
|
||||
let dir = temp_dir().join("plume-test");
|
||||
Searcher::open(&dir).unwrap();
|
||||
@@ -109,8 +110,10 @@ pub(crate) mod tests {
|
||||
let dir = temp_dir().join(format!("plume-test-{}", random_hex()));
|
||||
|
||||
assert!(Searcher::open(&dir).is_err());
|
||||
{Searcher::create(&dir).unwrap();}
|
||||
Searcher::open(&dir).unwrap();//verify it's well created
|
||||
{
|
||||
Searcher::create(&dir).unwrap();
|
||||
}
|
||||
Searcher::open(&dir).unwrap(); //verify it's well created
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -123,37 +126,56 @@ pub(crate) mod tests {
|
||||
|
||||
let title = random_hex()[..8].to_owned();
|
||||
|
||||
let mut post = Post::insert(conn, NewPost {
|
||||
blog_id: blog.id,
|
||||
slug: title.clone(),
|
||||
title: title.clone(),
|
||||
content: SafeString::new(""),
|
||||
published: true,
|
||||
license: "CC-BY-SA".to_owned(),
|
||||
ap_url: "".to_owned(),
|
||||
creation_date: None,
|
||||
subtitle: "".to_owned(),
|
||||
source: "".to_owned(),
|
||||
cover_id: None,
|
||||
}, &searcher).unwrap();
|
||||
PostAuthor::insert(conn, NewPostAuthor {
|
||||
post_id: post.id,
|
||||
author_id: author.id,
|
||||
}).unwrap();
|
||||
let mut post = Post::insert(
|
||||
conn,
|
||||
NewPost {
|
||||
blog_id: blog.id,
|
||||
slug: title.clone(),
|
||||
title: title.clone(),
|
||||
content: SafeString::new(""),
|
||||
published: true,
|
||||
license: "CC-BY-SA".to_owned(),
|
||||
ap_url: "".to_owned(),
|
||||
creation_date: None,
|
||||
subtitle: "".to_owned(),
|
||||
source: "".to_owned(),
|
||||
cover_id: None,
|
||||
},
|
||||
&searcher,
|
||||
)
|
||||
.unwrap();
|
||||
PostAuthor::insert(
|
||||
conn,
|
||||
NewPostAuthor {
|
||||
post_id: post.id,
|
||||
author_id: author.id,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
searcher.commit();
|
||||
assert_eq!(searcher.search_document(conn, Query::from_str(&title).unwrap(), (0,1))[0].id, post.id);
|
||||
assert_eq!(
|
||||
searcher.search_document(conn, Query::from_str(&title).unwrap(), (0, 1))[0].id,
|
||||
post.id
|
||||
);
|
||||
|
||||
let newtitle = random_hex()[..8].to_owned();
|
||||
post.title = newtitle.clone();
|
||||
post.update(conn, &searcher).unwrap();
|
||||
searcher.commit();
|
||||
assert_eq!(searcher.search_document(conn, Query::from_str(&newtitle).unwrap(), (0,1))[0].id, post.id);
|
||||
assert!(searcher.search_document(conn, Query::from_str(&title).unwrap(), (0,1)).is_empty());
|
||||
assert_eq!(
|
||||
searcher.search_document(conn, Query::from_str(&newtitle).unwrap(), (0, 1))[0].id,
|
||||
post.id
|
||||
);
|
||||
assert!(searcher
|
||||
.search_document(conn, Query::from_str(&title).unwrap(), (0, 1))
|
||||
.is_empty());
|
||||
|
||||
post.delete(&(conn, &searcher)).unwrap();
|
||||
searcher.commit();
|
||||
assert!(searcher.search_document(conn, Query::from_str(&newtitle).unwrap(), (0,1)).is_empty());
|
||||
assert!(searcher
|
||||
.search_document(conn, Query::from_str(&newtitle).unwrap(), (0, 1))
|
||||
.is_empty());
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use chrono::{Datelike, naive::NaiveDate, offset::Utc};
|
||||
use tantivy::{query::*, schema::*, Term};
|
||||
use std::{cmp,ops::Bound};
|
||||
use chrono::{naive::NaiveDate, offset::Utc, Datelike};
|
||||
use search::searcher::Searcher;
|
||||
|
||||
use std::{cmp, ops::Bound};
|
||||
use tantivy::{query::*, schema::*, Term};
|
||||
|
||||
//Generate functions for advanced search
|
||||
macro_rules! gen_func {
|
||||
@@ -142,13 +141,11 @@ pub struct PlumeQuery {
|
||||
}
|
||||
|
||||
impl PlumeQuery {
|
||||
|
||||
/// Create a new empty Query
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
|
||||
/// Parse a query string into this Query
|
||||
pub fn parse_query(&mut self, query: &str) -> &mut Self {
|
||||
self.from_str_req(&query.trim())
|
||||
@@ -160,9 +157,11 @@ impl PlumeQuery {
|
||||
gen_to_query!(self, result; normal: title, subtitle, content, tag;
|
||||
oneoff: instance, author, blog, lang, license);
|
||||
|
||||
for (occur, token) in self.text { // text entries need to be added as multiple Terms
|
||||
for (occur, token) in self.text {
|
||||
// text entries need to be added as multiple Terms
|
||||
match occur {
|
||||
Occur::Must => { // a Must mean this must be in one of title subtitle or content, not in all 3
|
||||
Occur::Must => {
|
||||
// a Must mean this must be in one of title subtitle or content, not in all 3
|
||||
let subresult = vec![
|
||||
(Occur::Should, Self::token_to_query(&token, "title")),
|
||||
(Occur::Should, Self::token_to_query(&token, "subtitle")),
|
||||
@@ -170,20 +169,26 @@ impl PlumeQuery {
|
||||
];
|
||||
|
||||
result.push((Occur::Must, Box::new(BooleanQuery::from(subresult))));
|
||||
},
|
||||
}
|
||||
occur => {
|
||||
result.push((occur, Self::token_to_query(&token, "title")));
|
||||
result.push((occur, Self::token_to_query(&token, "subtitle")));
|
||||
result.push((occur, Self::token_to_query(&token, "content")));
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.before.is_some() || self.after.is_some() { // if at least one range bound is provided
|
||||
let after = self.after.unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce()));
|
||||
let before = self.before.unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce()));
|
||||
if self.before.is_some() || self.after.is_some() {
|
||||
// if at least one range bound is provided
|
||||
let after = self
|
||||
.after
|
||||
.unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce()));
|
||||
let before = self
|
||||
.before
|
||||
.unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce()));
|
||||
let field = Searcher::schema().get_field("creation_date").unwrap();
|
||||
let range = RangeQuery::new_i64_bounds(field, Bound::Included(after), Bound::Included(before));
|
||||
let range =
|
||||
RangeQuery::new_i64_bounds(field, Bound::Included(after), Bound::Included(before));
|
||||
result.push((Occur::Must, Box::new(range)));
|
||||
}
|
||||
|
||||
@@ -195,14 +200,18 @@ impl PlumeQuery {
|
||||
|
||||
// documents newer than the provided date will be ignored
|
||||
pub fn before<D: Datelike>(&mut self, date: &D) -> &mut Self {
|
||||
let before = self.before.unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce()));
|
||||
let before = self
|
||||
.before
|
||||
.unwrap_or_else(|| i64::from(Utc::today().num_days_from_ce()));
|
||||
self.before = Some(cmp::min(before, i64::from(date.num_days_from_ce())));
|
||||
self
|
||||
}
|
||||
|
||||
// documents older than the provided date will be ignored
|
||||
pub fn after<D: Datelike>(&mut self, date: &D) -> &mut Self {
|
||||
let after = self.after.unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce()));
|
||||
let after = self
|
||||
.after
|
||||
.unwrap_or_else(|| i64::from(NaiveDate::from_ymd(2000, 1, 1).num_days_from_ce()));
|
||||
self.after = Some(cmp::max(after, i64::from(date.num_days_from_ce())));
|
||||
self
|
||||
}
|
||||
@@ -212,18 +221,22 @@ impl PlumeQuery {
|
||||
query = query.trim();
|
||||
if query.is_empty() {
|
||||
("", "")
|
||||
} else if query.get(0..1).map(|v| v=="\"").unwrap_or(false) {
|
||||
if let Some(index) = query[1..].find('"') {
|
||||
query.split_at(index+2)
|
||||
} else {
|
||||
(query, "")
|
||||
}
|
||||
} else if query.get(0..2).map(|v| v=="+\"" || v=="-\"").unwrap_or(false) {
|
||||
if let Some(index) = query[2..].find('"') {
|
||||
query.split_at(index+3)
|
||||
} else {
|
||||
(query, "")
|
||||
}
|
||||
} else if query.get(0..1).map(|v| v == "\"").unwrap_or(false) {
|
||||
if let Some(index) = query[1..].find('"') {
|
||||
query.split_at(index + 2)
|
||||
} else {
|
||||
(query, "")
|
||||
}
|
||||
} else if query
|
||||
.get(0..2)
|
||||
.map(|v| v == "+\"" || v == "-\"")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
if let Some(index) = query[2..].find('"') {
|
||||
query.split_at(index + 3)
|
||||
} else {
|
||||
(query, "")
|
||||
}
|
||||
} else if let Some(index) = query.find(' ') {
|
||||
query.split_at(index)
|
||||
} else {
|
||||
@@ -247,13 +260,13 @@ impl PlumeQuery {
|
||||
fn from_str_req(&mut self, mut query: &str) -> &mut Self {
|
||||
query = query.trim_left();
|
||||
if query.is_empty() {
|
||||
return self
|
||||
return self;
|
||||
}
|
||||
|
||||
let occur = if query.get(0..1).map(|v| v=="+").unwrap_or(false) {
|
||||
let occur = if query.get(0..1).map(|v| v == "+").unwrap_or(false) {
|
||||
query = &query[1..];
|
||||
Occur::Must
|
||||
} else if query.get(0..1).map(|v| v=="-").unwrap_or(false) {
|
||||
} else if query.get(0..1).map(|v| v == "-").unwrap_or(false) {
|
||||
query = &query[1..];
|
||||
Occur::MustNot
|
||||
} else {
|
||||
@@ -270,31 +283,59 @@ impl PlumeQuery {
|
||||
let token = token.to_lowercase();
|
||||
let token = token.as_str();
|
||||
let field = Searcher::schema().get_field(field_name).unwrap();
|
||||
if token.contains('@') && (field_name=="author" || field_name=="blog") {
|
||||
if token.contains('@') && (field_name == "author" || field_name == "blog") {
|
||||
let pos = token.find('@').unwrap();
|
||||
let user_term = Term::from_field_text(field, &token[..pos]);
|
||||
let instance_term = Term::from_field_text(Searcher::schema().get_field("instance").unwrap(), &token[pos+1..]);
|
||||
let instance_term = Term::from_field_text(
|
||||
Searcher::schema().get_field("instance").unwrap(),
|
||||
&token[pos + 1..],
|
||||
);
|
||||
Box::new(BooleanQuery::from(vec![
|
||||
(Occur::Must, Box::new(TermQuery::new(user_term, if field_name=="author" { IndexRecordOption::Basic }
|
||||
else { IndexRecordOption::WithFreqsAndPositions }
|
||||
)) as Box<dyn Query + 'static>),
|
||||
(Occur::Must, Box::new(TermQuery::new(instance_term, IndexRecordOption::Basic))),
|
||||
(
|
||||
Occur::Must,
|
||||
Box::new(TermQuery::new(
|
||||
user_term,
|
||||
if field_name == "author" {
|
||||
IndexRecordOption::Basic
|
||||
} else {
|
||||
IndexRecordOption::WithFreqsAndPositions
|
||||
},
|
||||
)) as Box<dyn Query + 'static>,
|
||||
),
|
||||
(
|
||||
Occur::Must,
|
||||
Box::new(TermQuery::new(instance_term, IndexRecordOption::Basic)),
|
||||
),
|
||||
]))
|
||||
} else if token.contains(' ') { // phrase query
|
||||
} else if token.contains(' ') {
|
||||
// phrase query
|
||||
match field_name {
|
||||
"instance" | "author" | "tag" => // phrase query are not available on these fields, treat it as multiple Term queries
|
||||
Box::new(BooleanQuery::from(token.split_whitespace()
|
||||
.map(|token| {
|
||||
let term = Term::from_field_text(field, token);
|
||||
(Occur::Should, Box::new(TermQuery::new(term, IndexRecordOption::Basic))
|
||||
as Box<dyn Query + 'static>)
|
||||
})
|
||||
.collect::<Vec<_>>())),
|
||||
_ => Box::new(PhraseQuery::new(token.split_whitespace()
|
||||
.map(|token| Term::from_field_text(field, token))
|
||||
.collect()))
|
||||
"instance" | "author" | "tag" =>
|
||||
// phrase query are not available on these fields, treat it as multiple Term queries
|
||||
{
|
||||
Box::new(BooleanQuery::from(
|
||||
token
|
||||
.split_whitespace()
|
||||
.map(|token| {
|
||||
let term = Term::from_field_text(field, token);
|
||||
(
|
||||
Occur::Should,
|
||||
Box::new(TermQuery::new(term, IndexRecordOption::Basic))
|
||||
as Box<dyn Query + 'static>,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
}
|
||||
_ => Box::new(PhraseQuery::new(
|
||||
token
|
||||
.split_whitespace()
|
||||
.map(|token| Term::from_field_text(field, token))
|
||||
.collect(),
|
||||
)),
|
||||
}
|
||||
} else { // Term Query
|
||||
} else {
|
||||
// Term Query
|
||||
let term = Term::from_field_text(field, token);
|
||||
let index_option = match field_name {
|
||||
"instance" | "author" | "tag" => IndexRecordOption::Basic,
|
||||
@@ -306,7 +347,6 @@ impl PlumeQuery {
|
||||
}
|
||||
|
||||
impl std::str::FromStr for PlumeQuery {
|
||||
|
||||
type Err = !;
|
||||
|
||||
/// Create a new Query from &str
|
||||
@@ -340,7 +380,7 @@ impl ToString for PlumeQuery {
|
||||
instance, author, blog, lang, license;
|
||||
date: before, after);
|
||||
|
||||
result.pop();// remove trailing ' '
|
||||
result.pop(); // remove trailing ' '
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,14 @@ use Connection;
|
||||
|
||||
use chrono::Datelike;
|
||||
use itertools::Itertools;
|
||||
use std::{cmp, fs::create_dir_all, path::Path, sync::Mutex};
|
||||
use tantivy::{
|
||||
collector::TopDocs, directory::MmapDirectory,
|
||||
schema::*, tokenizer::*, Index, IndexWriter, Term
|
||||
collector::TopDocs, directory::MmapDirectory, schema::*, tokenizer::*, Index, IndexWriter, Term,
|
||||
};
|
||||
use whatlang::{detect as detect_lang, Lang};
|
||||
use std::{cmp, fs::create_dir_all, path::Path, sync::Mutex};
|
||||
|
||||
use search::query::PlumeQuery;
|
||||
use super::tokenizer;
|
||||
use search::query::PlumeQuery;
|
||||
use Result;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -31,20 +30,23 @@ pub struct Searcher {
|
||||
|
||||
impl Searcher {
|
||||
pub fn schema() -> Schema {
|
||||
let tag_indexing = TextOptions::default()
|
||||
.set_indexing_options(TextFieldIndexing::default()
|
||||
.set_tokenizer("whitespace_tokenizer")
|
||||
.set_index_option(IndexRecordOption::Basic));
|
||||
let tag_indexing = TextOptions::default().set_indexing_options(
|
||||
TextFieldIndexing::default()
|
||||
.set_tokenizer("whitespace_tokenizer")
|
||||
.set_index_option(IndexRecordOption::Basic),
|
||||
);
|
||||
|
||||
let content_indexing = TextOptions::default()
|
||||
.set_indexing_options(TextFieldIndexing::default()
|
||||
.set_tokenizer("content_tokenizer")
|
||||
.set_index_option(IndexRecordOption::WithFreqsAndPositions));
|
||||
let content_indexing = TextOptions::default().set_indexing_options(
|
||||
TextFieldIndexing::default()
|
||||
.set_tokenizer("content_tokenizer")
|
||||
.set_index_option(IndexRecordOption::WithFreqsAndPositions),
|
||||
);
|
||||
|
||||
let property_indexing = TextOptions::default()
|
||||
.set_indexing_options(TextFieldIndexing::default()
|
||||
.set_tokenizer("property_tokenizer")
|
||||
.set_index_option(IndexRecordOption::WithFreqsAndPositions));
|
||||
let property_indexing = TextOptions::default().set_indexing_options(
|
||||
TextFieldIndexing::default()
|
||||
.set_tokenizer("property_tokenizer")
|
||||
.set_index_option(IndexRecordOption::WithFreqsAndPositions),
|
||||
);
|
||||
|
||||
let mut schema_builder = SchemaBuilder::default();
|
||||
|
||||
@@ -66,56 +68,65 @@ impl Searcher {
|
||||
schema_builder.build()
|
||||
}
|
||||
|
||||
|
||||
pub fn create(path: &AsRef<Path>) -> Result<Self> {
|
||||
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer
|
||||
.filter(LowerCaser);
|
||||
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser);
|
||||
|
||||
let content_tokenizer = SimpleTokenizer
|
||||
.filter(RemoveLongFilter::limit(40))
|
||||
.filter(LowerCaser);
|
||||
|
||||
let property_tokenizer = NgramTokenizer::new(2, 8, false)
|
||||
.filter(LowerCaser);
|
||||
let property_tokenizer = NgramTokenizer::new(2, 8, false).filter(LowerCaser);
|
||||
|
||||
let schema = Self::schema();
|
||||
|
||||
create_dir_all(path).map_err(|_| SearcherError::IndexCreationError)?;
|
||||
let index = Index::create(MmapDirectory::open(path).map_err(|_| SearcherError::IndexCreationError)?, schema).map_err(|_| SearcherError::IndexCreationError)?;
|
||||
let index = Index::create(
|
||||
MmapDirectory::open(path).map_err(|_| SearcherError::IndexCreationError)?,
|
||||
schema,
|
||||
)
|
||||
.map_err(|_| SearcherError::IndexCreationError)?;
|
||||
|
||||
{
|
||||
let tokenizer_manager = index.tokenizers();
|
||||
tokenizer_manager.register("whitespace_tokenizer", whitespace_tokenizer);
|
||||
tokenizer_manager.register("content_tokenizer", content_tokenizer);
|
||||
tokenizer_manager.register("property_tokenizer", property_tokenizer);
|
||||
}//to please the borrow checker
|
||||
} //to please the borrow checker
|
||||
Ok(Self {
|
||||
writer: Mutex::new(Some(index.writer(50_000_000).map_err(|_| SearcherError::WriteLockAcquisitionError)?)),
|
||||
index
|
||||
writer: Mutex::new(Some(
|
||||
index
|
||||
.writer(50_000_000)
|
||||
.map_err(|_| SearcherError::WriteLockAcquisitionError)?,
|
||||
)),
|
||||
index,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open(path: &AsRef<Path>) -> Result<Self> {
|
||||
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer
|
||||
.filter(LowerCaser);
|
||||
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser);
|
||||
|
||||
let content_tokenizer = SimpleTokenizer
|
||||
.filter(RemoveLongFilter::limit(40))
|
||||
.filter(LowerCaser);
|
||||
|
||||
let property_tokenizer = NgramTokenizer::new(2, 8, false)
|
||||
.filter(LowerCaser);
|
||||
let property_tokenizer = NgramTokenizer::new(2, 8, false).filter(LowerCaser);
|
||||
|
||||
let index = Index::open(MmapDirectory::open(path).map_err(|_| SearcherError::IndexOpeningError)?).map_err(|_| SearcherError::IndexOpeningError)?;
|
||||
let index =
|
||||
Index::open(MmapDirectory::open(path).map_err(|_| SearcherError::IndexOpeningError)?)
|
||||
.map_err(|_| SearcherError::IndexOpeningError)?;
|
||||
|
||||
{
|
||||
let tokenizer_manager = index.tokenizers();
|
||||
tokenizer_manager.register("whitespace_tokenizer", whitespace_tokenizer);
|
||||
tokenizer_manager.register("content_tokenizer", content_tokenizer);
|
||||
tokenizer_manager.register("property_tokenizer", property_tokenizer);
|
||||
}//to please the borrow checker
|
||||
let mut writer = index.writer(50_000_000).map_err(|_| SearcherError::WriteLockAcquisitionError)?;
|
||||
writer.garbage_collect_files().map_err(|_| SearcherError::IndexEditionError)?;
|
||||
} //to please the borrow checker
|
||||
let mut writer = index
|
||||
.writer(50_000_000)
|
||||
.map_err(|_| SearcherError::WriteLockAcquisitionError)?;
|
||||
writer
|
||||
.garbage_collect_files()
|
||||
.map_err(|_| SearcherError::IndexEditionError)?;
|
||||
Ok(Self {
|
||||
writer: Mutex::new(Some(writer)),
|
||||
index,
|
||||
@@ -173,18 +184,24 @@ impl Searcher {
|
||||
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> {
|
||||
let schema = self.index.schema();
|
||||
let post_id = schema.get_field("post_id").unwrap();
|
||||
|
||||
let collector = TopDocs::with_limit(cmp::max(1,max) as usize);
|
||||
let collector = TopDocs::with_limit(cmp::max(1, max) as usize);
|
||||
|
||||
let searcher = self.index.searcher();
|
||||
let res = searcher.search(&query.into_query(), &collector).unwrap();
|
||||
|
||||
res.get(min as usize..).unwrap_or(&[])
|
||||
res.get(min as usize..)
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.filter_map(|(_,doc_add)| {
|
||||
.filter_map(|(_, doc_add)| {
|
||||
let doc = searcher.doc(*doc_add).ok()?;
|
||||
let id = doc.get_first(post_id)?;
|
||||
Post::get(conn, id.i64_value() as i32).ok()
|
||||
|
||||
@@ -38,7 +38,12 @@ impl Tag {
|
||||
Ok(ht)
|
||||
}
|
||||
|
||||
pub fn from_activity(conn: &Connection, tag: &Hashtag, post: i32, is_hashtag: bool) -> Result<Tag> {
|
||||
pub fn from_activity(
|
||||
conn: &Connection,
|
||||
tag: &Hashtag,
|
||||
post: i32,
|
||||
is_hashtag: bool,
|
||||
) -> Result<Tag> {
|
||||
Tag::insert(
|
||||
conn,
|
||||
NewTag {
|
||||
|
||||
+139
-173
@@ -1,6 +1,5 @@
|
||||
use activitypub::{
|
||||
actor::Person, collection::OrderedCollection, object::Image, Activity, CustomObject,
|
||||
Endpoint,
|
||||
actor::Person, collection::OrderedCollection, object::Image, Activity, CustomObject, Endpoint,
|
||||
};
|
||||
use bcrypt;
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
@@ -27,7 +26,10 @@ use rocket::{
|
||||
request::{self, FromRequest, Request},
|
||||
};
|
||||
use serde_json;
|
||||
use std::{cmp::PartialEq, hash::{Hash, Hasher}};
|
||||
use std::{
|
||||
cmp::PartialEq,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
use url::Url;
|
||||
use webfinger::*;
|
||||
|
||||
@@ -41,7 +43,7 @@ use posts::Post;
|
||||
use safe_string::SafeString;
|
||||
use schema::users;
|
||||
use search::Searcher;
|
||||
use {ap_url, Connection, BASE_URL, USE_HTTPS, Error, Result};
|
||||
use {ap_url, Connection, Error, Result, BASE_URL, USE_HTTPS};
|
||||
|
||||
pub type CustomPerson = CustomObject<ApSignature, Person>;
|
||||
|
||||
@@ -97,42 +99,24 @@ impl User {
|
||||
insert!(users, NewUser, |inserted, conn| {
|
||||
let instance = inserted.get_instance(conn)?;
|
||||
if inserted.outbox_url.is_empty() {
|
||||
inserted.outbox_url = instance.compute_box(
|
||||
USER_PREFIX,
|
||||
&inserted.username,
|
||||
"outbox",
|
||||
);
|
||||
inserted.outbox_url = instance.compute_box(USER_PREFIX, &inserted.username, "outbox");
|
||||
}
|
||||
|
||||
if inserted.inbox_url.is_empty() {
|
||||
inserted.inbox_url = instance.compute_box(
|
||||
USER_PREFIX,
|
||||
&inserted.username,
|
||||
"inbox",
|
||||
);
|
||||
inserted.inbox_url = instance.compute_box(USER_PREFIX, &inserted.username, "inbox");
|
||||
}
|
||||
|
||||
if inserted.ap_url.is_empty() {
|
||||
inserted.ap_url = instance.compute_box(
|
||||
USER_PREFIX,
|
||||
&inserted.username,
|
||||
"",
|
||||
);
|
||||
inserted.ap_url = instance.compute_box(USER_PREFIX, &inserted.username, "");
|
||||
}
|
||||
|
||||
if inserted.shared_inbox_url.is_none() {
|
||||
inserted.shared_inbox_url = Some(ap_url(&format!(
|
||||
"{}/inbox",
|
||||
instance.public_domain
|
||||
)));
|
||||
inserted.shared_inbox_url = Some(ap_url(&format!("{}/inbox", instance.public_domain)));
|
||||
}
|
||||
|
||||
if inserted.followers_endpoint.is_empty() {
|
||||
inserted.followers_endpoint = instance.compute_box(
|
||||
USER_PREFIX,
|
||||
&inserted.username,
|
||||
"followers",
|
||||
);
|
||||
inserted.followers_endpoint =
|
||||
instance.compute_box(USER_PREFIX, &inserted.username, "followers");
|
||||
}
|
||||
|
||||
if inserted.fqn.is_empty() {
|
||||
@@ -162,7 +146,8 @@ impl User {
|
||||
|
||||
for blog in Blog::find_for_author(conn, self)?
|
||||
.iter()
|
||||
.filter(|b| b.count_authors(conn).map(|c| c <= 1).unwrap_or(false)) {
|
||||
.filter(|b| b.count_authors(conn).map(|c| c <= 1).unwrap_or(false))
|
||||
{
|
||||
blog.delete(conn, searcher)?;
|
||||
}
|
||||
// delete the posts if they is the only author
|
||||
@@ -180,10 +165,10 @@ impl User {
|
||||
.count()
|
||||
.load(conn)?
|
||||
.first()
|
||||
.unwrap_or(&0) > &0;
|
||||
.unwrap_or(&0)
|
||||
> &0;
|
||||
if !has_other_authors {
|
||||
Post::get(conn, post_id)?
|
||||
.delete(&(conn, searcher))?;
|
||||
Post::get(conn, post_id)?.delete(&(conn, searcher))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,12 +198,18 @@ impl User {
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn update(&self, conn: &Connection, name: String, email: String, summary: String) -> Result<User> {
|
||||
pub fn update(
|
||||
&self,
|
||||
conn: &Connection,
|
||||
name: String,
|
||||
email: String,
|
||||
summary: String,
|
||||
) -> Result<User> {
|
||||
diesel::update(self)
|
||||
.set((
|
||||
users::display_name.eq(name),
|
||||
users::email.eq(email),
|
||||
users::summary_html.eq(utils::md_to_html(&summary,"").0),
|
||||
users::summary_html.eq(utils::md_to_html(&summary, "").0),
|
||||
users::summary.eq(summary),
|
||||
))
|
||||
.execute(conn)?;
|
||||
@@ -278,16 +269,13 @@ impl User {
|
||||
}
|
||||
|
||||
pub fn fetch_from_url(conn: &Connection, url: &str) -> Result<User> {
|
||||
User::fetch(url).and_then(|json| User::from_activity(
|
||||
conn,
|
||||
&json,
|
||||
Url::parse(url)?.host_str()?,
|
||||
))
|
||||
User::fetch(url)
|
||||
.and_then(|json| User::from_activity(conn, &json, Url::parse(url)?.host_str()?))
|
||||
}
|
||||
|
||||
fn from_activity(conn: &Connection, acct: &CustomPerson, inst: &str) -> Result<User> {
|
||||
let instance = Instance::find_by_domain(conn, inst)
|
||||
.or_else(|_| Instance::insert(
|
||||
let instance = Instance::find_by_domain(conn, inst).or_else(|_| {
|
||||
Instance::insert(
|
||||
conn,
|
||||
NewInstance {
|
||||
name: inst.to_owned(),
|
||||
@@ -301,9 +289,15 @@ impl User {
|
||||
short_description_html: String::new(),
|
||||
long_description_html: String::new(),
|
||||
},
|
||||
))?;
|
||||
)
|
||||
})?;
|
||||
|
||||
if acct.object.ap_actor_props.preferred_username_string()?.contains(&['<', '>', '&', '@', '\'', '"'][..]) {
|
||||
if acct
|
||||
.object
|
||||
.ap_actor_props
|
||||
.preferred_username_string()?
|
||||
.contains(&['<', '>', '&', '@', '\'', '"'][..])
|
||||
{
|
||||
return Err(Error::InvalidValue);
|
||||
}
|
||||
let user = User::insert(
|
||||
@@ -314,20 +308,11 @@ impl User {
|
||||
.ap_actor_props
|
||||
.preferred_username_string()
|
||||
.unwrap(),
|
||||
display_name: acct
|
||||
.object
|
||||
.object_props
|
||||
.name_string()?,
|
||||
outbox_url: acct
|
||||
.object
|
||||
.ap_actor_props
|
||||
.outbox_string()?,
|
||||
inbox_url: acct
|
||||
.object
|
||||
.ap_actor_props
|
||||
.inbox_string()?,
|
||||
display_name: acct.object.object_props.name_string()?,
|
||||
outbox_url: acct.object.ap_actor_props.outbox_string()?,
|
||||
inbox_url: acct.object.ap_actor_props.inbox_string()?,
|
||||
is_admin: false,
|
||||
summary:acct
|
||||
summary: acct
|
||||
.object
|
||||
.object_props
|
||||
.summary_string()
|
||||
@@ -342,10 +327,7 @@ impl User {
|
||||
email: None,
|
||||
hashed_password: None,
|
||||
instance_id: instance.id,
|
||||
ap_url: acct
|
||||
.object
|
||||
.object_props
|
||||
.id_string()?,
|
||||
ap_url: acct.object.object_props.id_string()?,
|
||||
public_key: acct
|
||||
.custom_props
|
||||
.public_key_publickey()?
|
||||
@@ -357,10 +339,7 @@ impl User {
|
||||
.endpoints_endpoint()
|
||||
.and_then(|e| e.shared_inbox_string())
|
||||
.ok(),
|
||||
followers_endpoint: acct
|
||||
.object
|
||||
.ap_actor_props
|
||||
.followers_string()?,
|
||||
followers_endpoint: acct.object.ap_actor_props.followers_string()?,
|
||||
avatar_id: None,
|
||||
},
|
||||
)?;
|
||||
@@ -392,26 +371,15 @@ impl User {
|
||||
.object_props
|
||||
.url_string()?,
|
||||
&self,
|
||||
).ok();
|
||||
)
|
||||
.ok();
|
||||
|
||||
diesel::update(self)
|
||||
.set((
|
||||
users::username.eq(json
|
||||
.object
|
||||
.ap_actor_props
|
||||
.preferred_username_string()?),
|
||||
users::display_name.eq(json
|
||||
.object
|
||||
.object_props
|
||||
.name_string()?),
|
||||
users::outbox_url.eq(json
|
||||
.object
|
||||
.ap_actor_props
|
||||
.outbox_string()?),
|
||||
users::inbox_url.eq(json
|
||||
.object
|
||||
.ap_actor_props
|
||||
.inbox_string()?),
|
||||
users::username.eq(json.object.ap_actor_props.preferred_username_string()?),
|
||||
users::display_name.eq(json.object.object_props.name_string()?),
|
||||
users::outbox_url.eq(json.object.ap_actor_props.outbox_string()?),
|
||||
users::inbox_url.eq(json.object.ap_actor_props.inbox_string()?),
|
||||
users::summary.eq(SafeString::new(
|
||||
&json
|
||||
.object
|
||||
@@ -419,10 +387,7 @@ impl User {
|
||||
.summary_string()
|
||||
.unwrap_or_default(),
|
||||
)),
|
||||
users::followers_endpoint.eq(json
|
||||
.object
|
||||
.ap_actor_props
|
||||
.followers_string()?),
|
||||
users::followers_endpoint.eq(json.object.ap_actor_props.followers_string()?),
|
||||
users::avatar_id.eq(avatar.map(|a| a.id)),
|
||||
users::last_fetched_date.eq(Utc::now().naive_utc()),
|
||||
users::public_key.eq(json
|
||||
@@ -441,7 +406,8 @@ impl User {
|
||||
}
|
||||
|
||||
pub fn auth(&self, pass: &str) -> bool {
|
||||
self.hashed_password.clone()
|
||||
self.hashed_password
|
||||
.clone()
|
||||
.map(|hashed| bcrypt::verify(pass, hashed.as_ref()).unwrap_or(false))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
@@ -468,8 +434,7 @@ impl User {
|
||||
let n_acts = acts.len();
|
||||
let mut coll = OrderedCollection::default();
|
||||
coll.collection_props.items = serde_json::to_value(acts)?;
|
||||
coll.collection_props
|
||||
.set_total_items_u64(n_acts as u64)?;
|
||||
coll.collection_props.set_total_items_u64(n_acts as u64)?;
|
||||
Ok(ActivityStream::new(coll))
|
||||
}
|
||||
|
||||
@@ -483,12 +448,11 @@ impl User {
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
)?
|
||||
)?,
|
||||
)
|
||||
.send()?;
|
||||
let text = &res.text()?;
|
||||
let json: serde_json::Value =
|
||||
serde_json::from_str(text)?;
|
||||
let json: serde_json::Value = serde_json::from_str(text)?;
|
||||
Ok(json["items"]
|
||||
.as_array()
|
||||
.unwrap_or(&vec![])
|
||||
@@ -507,7 +471,7 @@ impl User {
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
)?
|
||||
)?,
|
||||
)
|
||||
.send()?;
|
||||
let text = &res.text()?;
|
||||
@@ -531,7 +495,9 @@ impl User {
|
||||
Ok(posts
|
||||
.into_iter()
|
||||
.filter_map(|p| {
|
||||
p.create_activity(conn).ok().and_then(|a| serde_json::to_value(a).ok())
|
||||
p.create_activity(conn)
|
||||
.ok()
|
||||
.and_then(|a| serde_json::to_value(a).ok())
|
||||
})
|
||||
.collect::<Vec<serde_json::Value>>())
|
||||
}
|
||||
@@ -555,7 +521,11 @@ impl User {
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn get_followers_page(&self, conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<User>> {
|
||||
pub fn get_followers_page(
|
||||
&self,
|
||||
conn: &Connection,
|
||||
(min, max): (i32, i32),
|
||||
) -> Result<Vec<User>> {
|
||||
use schema::follows;
|
||||
let follows = Follow::belonging_to(self).select(follows::follower_id);
|
||||
users::table
|
||||
@@ -584,7 +554,11 @@ impl User {
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn get_followed_page(&self, conn: &Connection, (min, max): (i32, i32)) -> Result<Vec<User>> {
|
||||
pub fn get_followed_page(
|
||||
&self,
|
||||
conn: &Connection,
|
||||
(min, max): (i32, i32),
|
||||
) -> Result<Vec<User>> {
|
||||
use schema::follows;
|
||||
let follows = follows::table
|
||||
.filter(follows::follower_id.eq(self.id))
|
||||
@@ -653,33 +627,32 @@ impl User {
|
||||
}
|
||||
|
||||
pub fn get_keypair(&self) -> Result<PKey<Private>> {
|
||||
PKey::from_rsa(
|
||||
Rsa::private_key_from_pem(
|
||||
self.private_key
|
||||
.clone()?
|
||||
.as_ref(),
|
||||
)?,
|
||||
).map_err(Error::from)
|
||||
PKey::from_rsa(Rsa::private_key_from_pem(
|
||||
self.private_key.clone()?.as_ref(),
|
||||
)?)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn rotate_keypair(&self, conn: &Connection) -> Result<PKey<Private>> {
|
||||
if self.private_key.is_none() {
|
||||
return Err(Error::InvalidValue)
|
||||
return Err(Error::InvalidValue);
|
||||
}
|
||||
if (Utc::now().naive_utc() - self.last_fetched_date).num_minutes() < 10 {
|
||||
//rotated recently
|
||||
self.get_keypair()
|
||||
} else {
|
||||
let (public_key, private_key) = gen_keypair();
|
||||
let public_key = String::from_utf8(public_key).expect("NewUser::new_local: public key error");
|
||||
let private_key = String::from_utf8(private_key).expect("NewUser::new_local: private key error");
|
||||
let res = PKey::from_rsa(
|
||||
Rsa::private_key_from_pem(private_key.as_ref())?
|
||||
)?;
|
||||
let public_key =
|
||||
String::from_utf8(public_key).expect("NewUser::new_local: public key error");
|
||||
let private_key =
|
||||
String::from_utf8(private_key).expect("NewUser::new_local: private key error");
|
||||
let res = PKey::from_rsa(Rsa::private_key_from_pem(private_key.as_ref())?)?;
|
||||
diesel::update(self)
|
||||
.set((users::public_key.eq(public_key),
|
||||
.set((
|
||||
users::public_key.eq(public_key),
|
||||
users::private_key.eq(Some(private_key)),
|
||||
users::last_fetched_date.eq(Utc::now().naive_utc())))
|
||||
users::last_fetched_date.eq(Utc::now().naive_utc()),
|
||||
))
|
||||
.execute(conn)
|
||||
.map_err(Error::from)
|
||||
.map(|_| res)
|
||||
@@ -688,18 +661,14 @@ impl User {
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<CustomPerson> {
|
||||
let mut actor = Person::default();
|
||||
actor
|
||||
.object_props
|
||||
.set_id_string(self.ap_url.clone())?;
|
||||
actor.object_props.set_id_string(self.ap_url.clone())?;
|
||||
actor
|
||||
.object_props
|
||||
.set_name_string(self.display_name.clone())?;
|
||||
actor
|
||||
.object_props
|
||||
.set_summary_string(self.summary_html.get().clone())?;
|
||||
actor
|
||||
.object_props
|
||||
.set_url_string(self.ap_url.clone())?;
|
||||
actor.object_props.set_url_string(self.ap_url.clone())?;
|
||||
actor
|
||||
.ap_actor_props
|
||||
.set_inbox_string(self.inbox_url.clone())?;
|
||||
@@ -714,42 +683,31 @@ impl User {
|
||||
.set_followers_string(self.followers_endpoint.clone())?;
|
||||
|
||||
let mut endpoints = Endpoint::default();
|
||||
endpoints
|
||||
.set_shared_inbox_string(ap_url(&format!("{}/inbox/", BASE_URL.as_str())))?;
|
||||
actor
|
||||
.ap_actor_props
|
||||
.set_endpoints_endpoint(endpoints)?;
|
||||
endpoints.set_shared_inbox_string(ap_url(&format!("{}/inbox/", BASE_URL.as_str())))?;
|
||||
actor.ap_actor_props.set_endpoints_endpoint(endpoints)?;
|
||||
|
||||
let mut public_key = PublicKey::default();
|
||||
public_key
|
||||
.set_id_string(format!("{}#main-key", self.ap_url))?;
|
||||
public_key
|
||||
.set_owner_string(self.ap_url.clone())?;
|
||||
public_key
|
||||
.set_public_key_pem_string(self.public_key.clone())?;
|
||||
public_key.set_id_string(format!("{}#main-key", self.ap_url))?;
|
||||
public_key.set_owner_string(self.ap_url.clone())?;
|
||||
public_key.set_public_key_pem_string(self.public_key.clone())?;
|
||||
let mut ap_signature = ApSignature::default();
|
||||
ap_signature
|
||||
.set_public_key_publickey(public_key)?;
|
||||
ap_signature.set_public_key_publickey(public_key)?;
|
||||
|
||||
let mut avatar = Image::default();
|
||||
avatar
|
||||
.object_props
|
||||
.set_url_string(
|
||||
self.avatar_id
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok())
|
||||
.unwrap_or_default(),
|
||||
)?;
|
||||
actor
|
||||
.object_props
|
||||
.set_icon_object(avatar)?;
|
||||
avatar.object_props.set_url_string(
|
||||
self.avatar_id
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok())
|
||||
.unwrap_or_default(),
|
||||
)?;
|
||||
actor.object_props.set_icon_object(avatar)?;
|
||||
|
||||
Ok(CustomPerson::new(actor, ap_signature))
|
||||
}
|
||||
|
||||
pub fn avatar_url(&self, conn: &Connection) -> String {
|
||||
self.avatar_id.and_then(|id|
|
||||
Media::get(conn, id).and_then(|m| m.url(conn)).ok()
|
||||
).unwrap_or_else(|| "/static/default-avatar.png".to_string())
|
||||
self.avatar_id
|
||||
.and_then(|id| Media::get(conn, id).and_then(|m| m.url(conn)).ok())
|
||||
.unwrap_or_else(|| "/static/default-avatar.png".to_string())
|
||||
}
|
||||
|
||||
pub fn webfinger(&self, conn: &Connection) -> Result<Webfinger> {
|
||||
@@ -866,21 +824,15 @@ impl Signer for User {
|
||||
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
|
||||
let key = self.get_keypair()?;
|
||||
let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?;
|
||||
signer
|
||||
.update(to_sign.as_bytes())?;
|
||||
signer
|
||||
.sign_to_vec()
|
||||
.map_err(Error::from)
|
||||
signer.update(to_sign.as_bytes())?;
|
||||
signer.sign_to_vec().map_err(Error::from)
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
|
||||
let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
|
||||
let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?;
|
||||
verifier
|
||||
.update(data.as_bytes())?;
|
||||
verifier
|
||||
.verify(&signature)
|
||||
.map_err(Error::from)
|
||||
verifier.update(data.as_bytes())?;
|
||||
verifier.verify(&signature).map_err(Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -915,7 +867,7 @@ impl NewUser {
|
||||
display_name,
|
||||
is_admin,
|
||||
summary: summary.to_owned(),
|
||||
summary_html: SafeString::new(&utils::md_to_html(&summary,"").0),
|
||||
summary_html: SafeString::new(&utils::md_to_html(&summary, "").0),
|
||||
email: Some(email),
|
||||
hashed_password: Some(password),
|
||||
instance_id: Instance::get_local(conn)?.id,
|
||||
@@ -947,7 +899,8 @@ pub(crate) mod tests {
|
||||
"Hello there, I'm the admin",
|
||||
"admin@example.com".to_owned(),
|
||||
"invalid_admin_password".to_owned(),
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
let user = NewUser::new_local(
|
||||
conn,
|
||||
"user".to_owned(),
|
||||
@@ -956,7 +909,8 @@ pub(crate) mod tests {
|
||||
"Hello there, I'm no one",
|
||||
"user@example.com".to_owned(),
|
||||
"invalid_user_password".to_owned(),
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
let other = NewUser::new_local(
|
||||
conn,
|
||||
"other".to_owned(),
|
||||
@@ -965,8 +919,9 @@ pub(crate) mod tests {
|
||||
"Hello there, I'm someone else",
|
||||
"other@example.com".to_owned(),
|
||||
"invalid_other_password".to_owned(),
|
||||
).unwrap();
|
||||
vec![ admin, user, other ]
|
||||
)
|
||||
.unwrap();
|
||||
vec![admin, user, other]
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -982,7 +937,8 @@ pub(crate) mod tests {
|
||||
"Hello I'm a test",
|
||||
"test@example.com".to_owned(),
|
||||
User::hash_pass("test_password").unwrap(),
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
test_user.id,
|
||||
@@ -996,9 +952,7 @@ pub(crate) mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
test_user.id,
|
||||
User::find_by_email(conn, "test@example.com")
|
||||
.unwrap()
|
||||
.id
|
||||
User::find_by_email(conn, "test@example.com").unwrap().id
|
||||
);
|
||||
assert_eq!(
|
||||
test_user.id,
|
||||
@@ -1009,8 +963,9 @@ pub(crate) mod tests {
|
||||
Instance::get_local(conn).unwrap().public_domain,
|
||||
"test"
|
||||
)
|
||||
).unwrap()
|
||||
.id
|
||||
)
|
||||
.unwrap()
|
||||
.id
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -1040,7 +995,11 @@ pub(crate) mod tests {
|
||||
let mut i = 0;
|
||||
while local_inst.has_admin(conn).unwrap() {
|
||||
assert!(i < 100); //prevent from looping indefinitelly
|
||||
local_inst.main_admin(conn).unwrap().revoke_admin_rights(conn).unwrap();
|
||||
local_inst
|
||||
.main_admin(conn)
|
||||
.unwrap()
|
||||
.revoke_admin_rights(conn)
|
||||
.unwrap();
|
||||
i += 1;
|
||||
}
|
||||
inserted[0].grant_admin_rights(conn).unwrap();
|
||||
@@ -1055,12 +1014,14 @@ pub(crate) mod tests {
|
||||
let conn = &db();
|
||||
conn.test_transaction::<_, (), _>(|| {
|
||||
let inserted = fill_database(conn);
|
||||
let updated = inserted[0].update(
|
||||
conn,
|
||||
"new name".to_owned(),
|
||||
"em@il".to_owned(),
|
||||
"<p>summary</p><script></script>".to_owned(),
|
||||
).unwrap();
|
||||
let updated = inserted[0]
|
||||
.update(
|
||||
conn,
|
||||
"new name".to_owned(),
|
||||
"em@il".to_owned(),
|
||||
"<p>summary</p><script></script>".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(updated.display_name, "new name");
|
||||
assert_eq!(updated.email.unwrap(), "em@il");
|
||||
assert_eq!(updated.summary_html.get(), "<p>summary</p>");
|
||||
@@ -1082,7 +1043,8 @@ pub(crate) mod tests {
|
||||
"Hello I'm a test",
|
||||
"test@example.com".to_owned(),
|
||||
User::hash_pass("test_password").unwrap(),
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(test_user.auth("test_password"));
|
||||
assert!(!test_user.auth("other_password"));
|
||||
@@ -1101,7 +1063,9 @@ pub(crate) mod tests {
|
||||
assert_eq!(page.len(), 2);
|
||||
assert!(page[0].username <= page[1].username);
|
||||
|
||||
let mut last_username = User::get_local_page(conn, (0, 1)).unwrap()[0].username.clone();
|
||||
let mut last_username = User::get_local_page(conn, (0, 1)).unwrap()[0]
|
||||
.username
|
||||
.clone();
|
||||
for i in 1..User::count_local(conn).unwrap() as i32 {
|
||||
let page = User::get_local_page(conn, (i, i + 1)).unwrap();
|
||||
assert_eq!(page.len(), 1);
|
||||
@@ -1109,7 +1073,9 @@ pub(crate) mod tests {
|
||||
last_username = page[0].username.clone();
|
||||
}
|
||||
assert_eq!(
|
||||
User::get_local_page(conn, (0, User::count_local(conn).unwrap() as i32 + 10)).unwrap().len() as i64,
|
||||
User::get_local_page(conn, (0, User::count_local(conn).unwrap() as i32 + 10))
|
||||
.unwrap()
|
||||
.len() as i64,
|
||||
User::count_local(conn).unwrap()
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user