Run 'cargo fmt' to format code (#489)

This commit is contained in:
Atul Bhosale
2019-03-20 22:26:17 +05:30
committed by Baptiste Gelez
parent 732f514da7
commit b945d1f602
58 changed files with 3160 additions and 2195 deletions
+11 -5
View File
@@ -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);
}
+3 -2
View File
@@ -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
View File
@@ -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());
+2 -1
View File
@@ -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)
+95 -93
View File
@@ -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(&note.object_props.to) ||
is_public(&note.object_props.bto) ||
is_public(&note.object_props.cc) ||
is_public(&note.object_props.bcc);
let public_visibility = is_public(&note.object_props.to)
|| is_public(&note.object_props.bto)
|| is_public(&note.object_props.cc)
|| is_public(&note.object_props.bcc);
let comm = Comment::insert(
conn,
NewComment {
content: SafeString::new(
&note
.object_props
.content_string()?
),
spoiler_text: note
.object_props
.summary_string()
.unwrap_or_default(),
content: SafeString::new(&note.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)
}
+10 -3
View File
@@ -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
View File
@@ -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(&notif)
.execute(conn)?;
diesel::delete(&notif).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(())
});
}
}
}
+43 -27
View File
@@ -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);
+7 -5
View File
@@ -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
View File
@@ -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(&notif)
.execute(conn)?;
diesel::delete(&notif).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
View File
@@ -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);
+11 -12
View File
@@ -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(|_| ())
}
}
+30 -12
View File
@@ -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
View File
@@ -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 {
+16 -32
View File
@@ -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(&notif)
.execute(conn)?;
diesel::delete(&notif).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,
}
}
}
+5 -11
View File
@@ -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(),
}
}
+54 -32
View File
@@ -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(())
});
+92 -52
View File
@@ -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
}
}
+54 -37
View File
@@ -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()
+6 -1
View File
@@ -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
View File
@@ -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()
);