diff --git a/Cargo.lock b/Cargo.lock index 1270f350..0d27ef05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,6 +16,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "activitystreams" +version = "0.7.0-alpha.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bcc3fbb392890a1942b1e5cca76cba93c8ed24b5ff50004cc3289afaab3f92c" +dependencies = [ + "activitystreams-kinds", + "chrono", + "mime 0.3.16", + "serde 1.0.133", + "serde_json", + "thiserror", + "url 2.2.2", +] + [[package]] name = "activitystreams-derive" version = "0.1.1" @@ -27,6 +42,16 @@ dependencies = [ "syn 0.13.11", ] +[[package]] +name = "activitystreams-kinds" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0784e99afd032199d3ed70cefb8eb3a8d1aef15f7f2c4e68d033c4e12bb6079e" +dependencies = [ + "serde 1.0.133", + "url 2.2.2", +] + [[package]] name = "activitystreams-traits" version = "0.1.0" @@ -205,6 +230,16 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a1bb320f97e6edf9f756bf015900038e43c7700e059688e5724a928c8f3b8d5" +[[package]] +name = "assert-json-diff" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f1c3703dd33532d7f0ca049168930e9099ecac238e23cf932f3a69c42f06da" +dependencies = [ + "serde 1.0.133", + "serde_json", +] + [[package]] name = "async-trait" version = "0.1.52" @@ -3136,6 +3171,7 @@ name = "plume-common" version = "0.7.1" dependencies = [ "activitypub", + "activitystreams", "activitystreams-derive", "activitystreams-traits", "array_tool", @@ -3190,7 +3226,9 @@ name = "plume-models" version = "0.7.1" dependencies = [ "activitypub", + "activitystreams", "ammonia", + "assert-json-diff", "bcrypt", "chrono", "diesel", @@ -5232,6 +5270,7 @@ dependencies = [ "idna 0.2.3", "matches", "percent-encoding 2.1.0", + "serde 1.0.133", ] [[package]] diff --git a/plume-common/Cargo.toml b/plume-common/Cargo.toml index ecdc2fee..0a1cf12a 100644 --- a/plume-common/Cargo.toml +++ b/plume-common/Cargo.toml @@ -24,6 +24,7 @@ tokio = "0.1.22" regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] } tracing = "0.1.30" askama_escape = "0.10.2" +activitystreams = "0.7.0-alpha.14" [dependencies.chrono] features = ["serde"] diff --git a/plume-common/src/utils.rs b/plume-common/src/utils.rs index cbb4db72..17912a02 100644 --- a/plume-common/src/utils.rs +++ b/plume-common/src/utils.rs @@ -271,7 +271,7 @@ pub fn md_to_html<'a>( media_processor: Option>, ) -> (String, HashSet, HashSet) { let base_url = if let Some(base_url) = base_url { - format!("//{}/", base_url) + format!("https://{}/", base_url) } else { "/".to_owned() }; diff --git a/plume-models/Cargo.toml b/plume-models/Cargo.toml index 960aae8e..ce900298 100644 --- a/plume-models/Cargo.toml +++ b/plume-models/Cargo.toml @@ -35,6 +35,7 @@ riker = "0.4.2" once_cell = "1.5.2" lettre = "0.9.6" native-tls = "0.2.8" +activitystreams = "0.7.0-alpha.14" [dependencies.chrono] features = ["serde"] @@ -54,6 +55,7 @@ path = "../plume-common" path = "../plume-macro" [dev-dependencies] +assert-json-diff = "2.0.1" diesel_migrations = "1.3.0" [features] diff --git a/plume-models/src/comments.rs b/plume-models/src/comments.rs index 09f26a35..1da82a36 100644 --- a/plume-models/src/comments.rs +++ b/plume-models/src/comments.rs @@ -16,7 +16,7 @@ use activitypub::{ link, object::{Note, Tombstone}, }; -use chrono::{self, NaiveDateTime}; +use chrono::{self, NaiveDateTime, TimeZone, Utc}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; use plume_common::{ activity_pub::{ @@ -59,7 +59,7 @@ impl Comment { insert!(comments, NewComment, |inserted, conn| { if inserted.ap_url.is_none() { inserted.ap_url = Some(format!( - "{}comment/{}", + "{}/comment/{}", inserted.get_post(conn)?.ap_url, inserted.id )); @@ -129,7 +129,7 @@ impl Comment { |id| Ok(Comment::get(conn, id)?.ap_url.unwrap_or_default()) as Result, )?))?; note.object_props - .set_published_string(chrono::Utc::now().to_rfc3339())?; + .set_published_utctime(Utc.from_utc_datetime(&self.creation_date))?; note.object_props.set_attributed_to_link(author.into_id())?; note.object_props.set_to_link_vec(to)?; note.object_props.set_tag_link_vec( @@ -402,10 +402,34 @@ impl CommentTree { #[cfg(test)] mod tests { use super::*; + use crate::blogs::Blog; use crate::inbox::{inbox, tests::fill_database, InboxResult}; use crate::safe_string::SafeString; - use crate::tests::db; + use crate::tests::{db, format_datetime}; + use assert_json_diff::assert_json_eq; use diesel::Connection; + use serde_json::{json, to_value}; + + fn prepare_activity(conn: &DbConn) -> (Comment, Vec, Vec, Vec) { + let (posts, users, blogs) = fill_database(&conn); + + let comment = Comment::insert( + conn, + NewComment { + content: SafeString::new("My comment, mentioning to @user"), + in_response_to_id: None, + post_id: posts[0].id, + author_id: users[0].id, + ap_url: None, + sensitive: true, + spoiler_text: "My CW".into(), + public_visibility: true, + }, + ) + .unwrap(); + + (comment, posts, users, blogs) + } // creates a post, get it's Create activity, delete the post, // "send" the Create to the inbox, and check it works @@ -413,30 +437,77 @@ mod tests { fn self_federation() { let conn = &db(); conn.test_transaction::<_, (), _>(|| { - let (posts, users, _) = fill_database(&conn); + let (original_comm, posts, users, _blogs) = prepare_activity(&conn); + let act = original_comm.create_activity(&conn).unwrap(); - let original_comm = Comment::insert( + assert_json_eq!(to_value(&act).unwrap(), json!({ + "actor": "https://plu.me/@/admin/", + "cc": ["https://plu.me/@/admin/followers"], + "id": format!("https://plu.me/~/BlogName/testing/comment/{}/activity", original_comm.id), + "object": { + "attributedTo": "https://plu.me/@/admin/", + "content": r###"

My comment, mentioning to @user

+"###, + "id": format!("https://plu.me/~/BlogName/testing/comment/{}", original_comm.id), + "inReplyTo": "https://plu.me/~/BlogName/testing", + "published": format_datetime(&original_comm.creation_date), + "summary": "My CW", + "tag": [ + { + "href": "https://plu.me/@/user/", + "name": "@user", + "type": "Mention" + } + ], + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Note" + }, + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Create", + })); + + let reply = Comment::insert( conn, NewComment { - content: SafeString::new("My comment"), - in_response_to_id: None, + content: SafeString::new(""), + in_response_to_id: Some(original_comm.id), post_id: posts[0].id, - author_id: users[0].id, + author_id: users[1].id, ap_url: None, - sensitive: true, - spoiler_text: "My CW".into(), + sensitive: false, + spoiler_text: "".into(), public_visibility: true, }, ) .unwrap(); - let act = original_comm.create_activity(&conn).unwrap(); + let reply_act = reply.create_activity(&conn).unwrap(); + + assert_json_eq!(to_value(&reply_act).unwrap(), json!({ + "actor": "https://plu.me/@/user/", + "cc": ["https://plu.me/@/user/followers"], + "id": format!("https://plu.me/~/BlogName/testing/comment/{}/activity", reply.id), + "object": { + "attributedTo": "https://plu.me/@/user/", + "content": "", + "id": format!("https://plu.me/~/BlogName/testing/comment/{}", reply.id), + "inReplyTo": format!("https://plu.me/~/BlogName/testing/comment/{}", original_comm.id), + "published": format_datetime(&reply.creation_date), + "summary": "", + "tag": [], + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Note" + }, + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Create" + })); + inbox( &conn, serde_json::to_value(original_comm.build_delete(&conn).unwrap()).unwrap(), ) .unwrap(); - match inbox(&conn, serde_json::to_value(act).unwrap()).unwrap() { + match inbox(&conn, to_value(act).unwrap()).unwrap() { InboxResult::Commented(c) => { // TODO: one is HTML, the other markdown: assert_eq!(c.content, original_comm.content); assert_eq!(c.in_response_to_id, original_comm.in_response_to_id); @@ -451,4 +522,60 @@ mod tests { Ok(()) }) } + + #[test] + fn to_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (comment, _posts, _users, _blogs) = prepare_activity(&conn); + let act = comment.to_activity(&conn)?; + + let expected = json!({ + "attributedTo": "https://plu.me/@/admin/", + "content": r###"

My comment, mentioning to @user

+"###, + "id": format!("https://plu.me/~/BlogName/testing/comment/{}", comment.id), + "inReplyTo": "https://plu.me/~/BlogName/testing", + "published": format_datetime(&comment.creation_date), + "summary": "My CW", + "tag": [ + { + "href": "https://plu.me/@/user/", + "name": "@user", + "type": "Mention" + } + ], + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Note" + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } + + #[test] + fn build_delete() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (comment, _posts, _users, _blogs) = prepare_activity(&conn); + let act = comment.build_delete(&conn)?; + + let expected = json!({ + "actor": "https://plu.me/@/admin/", + "id": format!("https://plu.me/~/BlogName/testing/comment/{}#delete", comment.id), + "object": { + "id": format!("https://plu.me/~/BlogName/testing/comment/{}", comment.id), + "type": "Tombstone" + }, + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Delete" + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } } diff --git a/plume-models/src/follows.rs b/plume-models/src/follows.rs index 9450b59b..b1e6fdaf 100644 --- a/plume-models/src/follows.rs +++ b/plume-models/src/follows.rs @@ -99,11 +99,27 @@ impl Follow { )?; res.notify(conn)?; + let accept = res.build_accept(from, target, follow)?; + broadcast( + &*target, + accept, + vec![from.clone()], + CONFIG.proxy().cloned(), + ); + Ok(res) + } + + pub fn build_accept + IntoId, T>( + &self, + from: &B, + target: &A, + follow: FollowAct, + ) -> Result { let mut accept = Accept::default(); let accept_id = ap_url(&format!( - "{}/follow/{}/accept", + "{}/follows/{}/accept", CONFIG.base_url.as_str(), - &res.id + self.id )); accept.object_props.set_id_string(accept_id)?; accept @@ -116,13 +132,8 @@ impl Follow { .accept_props .set_actor_link::(target.clone().into_id())?; accept.accept_props.set_object_object(follow)?; - broadcast( - &*target, - accept, - vec![from.clone()], - CONFIG.proxy().cloned(), - ); - Ok(res) + + Ok(accept) } pub fn build_undo(&self, conn: &Connection) -> Result { @@ -219,8 +230,29 @@ impl IntoId for Follow { #[cfg(test)] mod tests { use super::*; - use crate::{tests::db, users::tests as user_tests}; + use crate::{tests::db, users::tests as user_tests, users::tests::fill_database}; + use assert_json_diff::assert_json_eq; use diesel::Connection; + use serde_json::{json, to_value}; + + fn prepare_activity(conn: &DbConn) -> (Follow, User, User, Vec) { + let users = fill_database(conn); + let following = &users[1]; + let follower = &users[2]; + let mut follow = Follow::insert( + conn, + NewFollow { + follower_id: follower.id, + following_id: following.id, + ap_url: "".into(), + }, + ) + .unwrap(); + // following.ap_url = format!("https://plu.me/follows/{}", follow.id); + follow.ap_url = format!("https://plu.me/follows/{}", follow.id); + + (follow, following.to_owned(), follower.to_owned(), users) + } #[test] fn test_id() { @@ -255,4 +287,77 @@ mod tests { Ok(()) }) } + + #[test] + fn to_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (follow, _following, _follower, _users) = prepare_activity(&conn); + let act = follow.to_activity(&conn)?; + + let expected = json!({ + "actor": "https://plu.me/@/other/", + "cc": ["https://www.w3.org/ns/activitystreams#Public"], + "id": format!("https://plu.me/follows/{}", follow.id), + "object": "https://plu.me/@/user/", + "to": ["https://plu.me/@/user/"], + "type": "Follow" + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } + + #[test] + fn build_accept() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (follow, following, follower, _users) = prepare_activity(&conn); + let act = follow.build_accept(&follower, &following, follow.to_activity(&conn)?)?; + + let expected = json!({ + "actor": "https://plu.me/@/user/", + "cc": ["https://www.w3.org/ns/activitystreams#Public"], + "id": format!("https://127.0.0.1:7878/follows/{}/accept", follow.id), + "object": { + "actor": "https://plu.me/@/other/", + "cc": ["https://www.w3.org/ns/activitystreams#Public"], + "id": format!("https://plu.me/follows/{}", follow.id), + "object": "https://plu.me/@/user/", + "to": ["https://plu.me/@/user/"], + "type": "Follow" + }, + "to": ["https://plu.me/@/other/"], + "type": "Accept" + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } + + #[test] + fn build_undo() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (follow, _following, _follower, _users) = prepare_activity(&conn); + let act = follow.build_undo(&conn)?; + + let expected = json!({ + "actor": "https://plu.me/@/other/", + "cc": ["https://www.w3.org/ns/activitystreams#Public"], + "id": format!("https://plu.me/follows/{}/undo", follow.id), + "object": format!("https://plu.me/follows/{}", follow.id), + "to": ["https://plu.me/@/user/"], + "type": "Undo" + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } } diff --git a/plume-models/src/lib.rs b/plume-models/src/lib.rs index fd93a601..546d5997 100644 --- a/plume-models/src/lib.rs +++ b/plume-models/src/lib.rs @@ -334,6 +334,7 @@ impl SmtpNewWithAddr for smtp::SmtpClient { #[macro_use] mod tests { use crate::{db_conn, migrations::IMPORTED_MIGRATIONS, Connection as Conn, CONFIG}; + use chrono::{naive::NaiveDateTime, Datelike, Timelike}; use diesel::r2d2::ConnectionManager; use plume_common::utils::random_hex; use std::env::temp_dir; @@ -366,6 +367,33 @@ mod tests { pool }; } + + #[cfg(feature = "postgres")] + pub(crate) fn format_datetime(dt: &NaiveDateTime) -> String { + format!( + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z", + dt.year(), + dt.month(), + dt.day(), + dt.hour(), + dt.minute(), + dt.second(), + dt.timestamp_subsec_micros() + ) + } + + #[cfg(feature = "sqlite")] + pub(crate) fn format_datetime(dt: &NaiveDateTime) -> String { + format!( + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", + dt.year(), + dt.month(), + dt.day(), + dt.hour(), + dt.minute(), + dt.second() + ) + } } pub mod admin; diff --git a/plume-models/src/likes.rs b/plume-models/src/likes.rs index fa092af0..9c119177 100644 --- a/plume-models/src/likes.rs +++ b/plume-models/src/likes.rs @@ -165,8 +165,7 @@ impl AsObject for Like { impl NewLike { pub fn new(p: &Post, u: &User) -> Self { - // TODO: this URL is not valid - let ap_url = format!("{}/like/{}", u.ap_url, p.ap_url); + let ap_url = format!("{}like/{}", u.ap_url, p.ap_url); NewLike { post_id: p.id, user_id: u.id, @@ -174,3 +173,67 @@ impl NewLike { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::diesel::Connection; + use crate::{inbox::tests::fill_database, tests::db}; + use assert_json_diff::assert_json_eq; + use serde_json::{json, to_value}; + + #[test] + fn to_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (posts, _users, _blogs) = fill_database(&conn); + let post = &posts[0]; + let user = &post.get_authors(&conn)?[0]; + let like = Like::insert(&*conn, NewLike::new(post, user))?; + let act = like.to_activity(&conn).unwrap(); + + let expected = json!({ + "actor": "https://plu.me/@/admin/", + "cc": ["https://plu.me/@/admin/followers"], + "id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing", + "object": "https://plu.me/~/BlogName/testing", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Like", + }); + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } + + #[test] + fn build_undo() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (posts, _users, _blogs) = fill_database(&conn); + let post = &posts[0]; + let user = &post.get_authors(&conn)?[0]; + let like = Like::insert(&*conn, NewLike::new(post, user))?; + let act = like.build_undo(&*conn)?; + + let expected = json!({ + "actor": "https://plu.me/@/admin/", + "cc": ["https://plu.me/@/admin/followers"], + "id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing#delete", + "object": { + "actor": "https://plu.me/@/admin/", + "cc": ["https://plu.me/@/admin/followers"], + "id": "https://plu.me/@/admin/like/https://plu.me/~/BlogName/testing", + "object": "https://plu.me/~/BlogName/testing", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Like", + }, + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Undo", + }); + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } +} diff --git a/plume-models/src/medias.rs b/plume-models/src/medias.rs index f4761461..1af5f3b6 100644 --- a/plume-models/src/medias.rs +++ b/plume-models/src/medias.rs @@ -400,7 +400,15 @@ pub(crate) mod tests { pub(crate) fn clean(conn: &Conn) { //used to remove files generated by tests for media in Media::list_all_medias(conn).unwrap() { - media.delete(conn).unwrap(); + if let Some(err) = media.delete(conn).err() { + match &err { + Error::Io(e) => match e.kind() { + std::io::ErrorKind::NotFound => (), + _ => panic!("{:?}", err), + }, + _ => panic!("{:?}", err), + } + } } } diff --git a/plume-models/src/mentions.rs b/plume-models/src/mentions.rs index d83d48d8..16672781 100644 --- a/plume-models/src/mentions.rs +++ b/plume-models/src/mentions.rs @@ -145,3 +145,62 @@ impl Mention { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{inbox::tests::fill_database, tests::db, Error}; + use assert_json_diff::assert_json_eq; + use diesel::Connection; + use serde_json::{json, to_value}; + + #[test] + fn build_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (_posts, users, _blogs) = fill_database(&conn); + let user = &users[0]; + let name = &user.username; + let act = Mention::build_activity(&conn, name)?; + + let expected = json!({ + "href": "https://plu.me/@/admin/", + "name": "@admin", + "type": "Mention", + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } + + #[test] + fn to_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (posts, users, _blogs) = fill_database(&conn); + let post = &posts[0]; + let user = &users[0]; + let mention = Mention::insert( + &conn, + NewMention { + mentioned_id: user.id, + post_id: Some(post.id), + comment_id: None, + }, + )?; + let act = mention.to_activity(&conn)?; + + let expected = json!({ + "href": "https://plu.me/@/admin/", + "name": "@admin", + "type": "Mention", + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } +} diff --git a/plume-models/src/posts.rs b/plume-models/src/posts.rs index c41c6b2d..65c98735 100644 --- a/plume-models/src/posts.rs +++ b/plume-models/src/posts.rs @@ -413,7 +413,7 @@ impl Post { let article = self.to_activity(conn)?; let mut act = Create::default(); act.object_props - .set_id_string(format!("{}activity", self.ap_url))?; + .set_id_string(format!("{}/activity", self.ap_url))?; act.object_props .set_to_link_vec::(article.object.object_props.to_link_vec()?)?; act.object_props @@ -942,9 +942,28 @@ impl From for Arc { mod tests { use super::*; use crate::inbox::{inbox, tests::fill_database, InboxResult}; + use crate::mentions::{Mention, NewMention}; use crate::safe_string::SafeString; - use crate::tests::db; + use crate::tests::{db, format_datetime}; + use assert_json_diff::assert_json_eq; use diesel::Connection; + use serde_json::{json, to_value}; + + fn prepare_activity(conn: &DbConn) -> (Post, Mention, Vec, Vec, Vec) { + let (posts, users, blogs) = fill_database(conn); + let post = &posts[0]; + let mentioned = &users[1]; + let mention = Mention::insert( + &conn, + NewMention { + mentioned_id: mentioned.id, + post_id: Some(post.id), + comment_id: None, + }, + ) + .unwrap(); + (post.to_owned(), mention.to_owned(), posts, users, blogs) + } // creates a post, get it's Create activity, delete the post, // "send" the Create to the inbox, and check it works @@ -1038,4 +1057,153 @@ mod tests { &article.object.object_props.id_string().unwrap() ); } + + #[test] + fn to_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn); + let act = post.to_activity(&conn)?; + + let expected = json!({ + "attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"], + "cc": [], + "content": "Hello", + "id": "https://plu.me/~/BlogName/testing", + "license": "WTFPL", + "name": "Testing", + "published": format_datetime(&post.creation_date), + "source": { + "content": "", + "mediaType": "text/markdown" + }, + "summary": "", + "tag": [ + { + "href": "https://plu.me/@/user/", + "name": "@user", + "type": "Mention" + } + ], + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Article", + "url": "https://plu.me/~/BlogName/testing" + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } + + #[test] + fn create_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn); + let act = post.create_activity(&conn)?; + + let expected = json!({ + "actor": "https://plu.me/@/admin/", + "cc": [], + "id": "https://plu.me/~/BlogName/testing/activity", + "object": { + "attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"], + "cc": [], + "content": "Hello", + "id": "https://plu.me/~/BlogName/testing", + "license": "WTFPL", + "name": "Testing", + "published": format_datetime(&post.creation_date), + "source": { + "content": "", + "mediaType": "text/markdown" + }, + "summary": "", + "tag": [ + { + "href": "https://plu.me/@/user/", + "name": "@user", + "type": "Mention" + } + ], + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Article", + "url": "https://plu.me/~/BlogName/testing" + }, + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Create" + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } + + #[test] + fn update_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (post, _mention, _posts, _users, _blogs) = prepare_activity(&conn); + let act = post.update_activity(&conn)?; + + let expected = json!({ + "actor": "https://plu.me/@/admin/", + "cc": [], + "id": "https://plu.me/~/BlogName/testing/update-", + "object": { + "attributedTo": ["https://plu.me/@/admin/", "https://plu.me/~/BlogName/"], + "cc": [], + "content": "Hello", + "id": "https://plu.me/~/BlogName/testing", + "license": "WTFPL", + "name": "Testing", + "published": format_datetime(&post.creation_date), + "source": { + "content": "", + "mediaType": "text/markdown" + }, + "summary": "", + "tag": [ + { + "href": "https://plu.me/@/user/", + "name": "@user", + "type": "Mention" + } + ], + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Article", + "url": "https://plu.me/~/BlogName/testing" + }, + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Update" + }); + let actual = to_value(act)?; + + let id = actual["id"].to_string(); + let (id_pre, id_post) = id.rsplit_once("-").unwrap(); + assert_eq!(post.ap_url, "https://plu.me/~/BlogName/testing"); + assert_eq!( + id_pre, + to_value("\"https://plu.me/~/BlogName/testing/update") + .unwrap() + .as_str() + .unwrap() + ); + assert_eq!(id_post.len(), 11); + assert_eq!( + id_post.matches(char::is_numeric).collect::().len(), + 10 + ); + for (key, value) in actual.as_object().unwrap().into_iter() { + if key == "id" { + continue; + } + assert_eq!(value, expected.get(key).unwrap()); + } + + Ok(()) + }); + } } diff --git a/plume-models/src/reshares.rs b/plume-models/src/reshares.rs index 58400196..90ca0cc8 100644 --- a/plume-models/src/reshares.rs +++ b/plume-models/src/reshares.rs @@ -191,7 +191,7 @@ impl AsObject for Reshare { impl NewReshare { pub fn new(p: &Post, u: &User) -> Self { - let ap_url = format!("{}/reshare/{}", u.ap_url, p.ap_url); + let ap_url = format!("{}reshare/{}", u.ap_url, p.ap_url); NewReshare { post_id: p.id, user_id: u.id, @@ -199,3 +199,67 @@ impl NewReshare { } } } + +#[cfg(test)] +mod test { + use super::*; + use crate::diesel::Connection; + use crate::{inbox::tests::fill_database, tests::db}; + use assert_json_diff::assert_json_eq; + use serde_json::{json, to_value}; + + #[test] + fn to_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (posts, _users, _blogs) = fill_database(&conn); + let post = &posts[0]; + let user = &post.get_authors(&conn)?[0]; + let reshare = Reshare::insert(&*conn, NewReshare::new(post, user))?; + let act = reshare.to_activity(&conn).unwrap(); + + let expected = json!({ + "actor": "https://plu.me/@/admin/", + "cc": ["https://plu.me/@/admin/followers"], + "id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing", + "object": "https://plu.me/~/BlogName/testing", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Announce", + }); + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } + + #[test] + fn build_undo() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (posts, _users, _blogs) = fill_database(&conn); + let post = &posts[0]; + let user = &post.get_authors(&conn)?[0]; + let reshare = Reshare::insert(&*conn, NewReshare::new(post, user))?; + let act = reshare.build_undo(&*conn)?; + + let expected = json!({ + "actor": "https://plu.me/@/admin/", + "cc": ["https://plu.me/@/admin/followers"], + "id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing#delete", + "object": { + "actor": "https://plu.me/@/admin/", + "cc": ["https://plu.me/@/admin/followers"], + "id": "https://plu.me/@/admin/reshare/https://plu.me/~/BlogName/testing", + "object": "https://plu.me/~/BlogName/testing", + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Announce" + }, + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Undo", + }); + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } +} diff --git a/plume-models/src/users.rs b/plume-models/src/users.rs index 8244075e..1e27fb19 100644 --- a/plume-models/src/users.rs +++ b/plume-models/src/users.rs @@ -429,6 +429,9 @@ impl User { .map_err(Error::from) } pub fn outbox(&self, conn: &Connection) -> Result> { + Ok(ActivityStream::new(self.outbox_collection(conn)?)) + } + pub fn outbox_collection(&self, conn: &Connection) -> Result { let mut coll = OrderedCollection::default(); let first = &format!("{}?page=1", &self.outbox_url); let last = &format!( @@ -440,13 +443,22 @@ impl User { coll.collection_props.set_last_link(Id::new(last))?; coll.collection_props .set_total_items_u64(self.get_activities_count(conn) as u64)?; - Ok(ActivityStream::new(coll)) + Ok(coll) } pub fn outbox_page( &self, conn: &Connection, (min, max): (i32, i32), ) -> Result> { + Ok(ActivityStream::new( + self.outbox_collection_page(conn, (min, max))?, + )) + } + pub fn outbox_collection_page( + &self, + conn: &Connection, + (min, max): (i32, i32), + ) -> Result { let acts = self.get_activities_page(conn, (min, max))?; let n_acts = self.get_activities_count(conn); let mut coll = OrderedCollectionPage::default(); @@ -467,7 +479,7 @@ impl User { coll.collection_props.items = serde_json::to_value(acts)?; coll.collection_page_props .set_part_of_link(Id::new(&self.outbox_url))?; - Ok(ActivityStream::new(coll)) + Ok(coll) } fn fetch_outbox_page(&self, url: &str) -> Result<(Vec, Option)> { let mut res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?; @@ -763,13 +775,13 @@ impl User { let mut ap_signature = ApSignature::default(); 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()).ok()) - .unwrap_or_default(), - )?; - actor.object_props.set_icon_object(avatar)?; + if let Some(avatar_id) = self.avatar_id { + let mut avatar = Image::default(); + avatar + .object_props + .set_url_string(Media::get(conn, avatar_id)?.url()?)?; + actor.object_props.set_icon_object(avatar)?; + } Ok(CustomPerson::new(actor, ap_signature)) } @@ -1142,10 +1154,13 @@ pub(crate) mod tests { use super::*; use crate::{ instance::{tests as instance_tests, Instance}, + medias::{Media, NewMedia}, tests::db, - Connection as Conn, + Connection as Conn, ITEMS_PER_PAGE, }; - use diesel::Connection; + use assert_json_diff::assert_json_eq; + use diesel::{Connection, SaveChangesDsl}; + use serde_json::to_value; pub(crate) fn fill_database(conn: &Conn) -> Vec { instance_tests::fill_database(conn); @@ -1169,7 +1184,7 @@ pub(crate) mod tests { Some("invalid_user_password".to_owned()), ) .unwrap(); - let other = NewUser::new_local( + let mut other = NewUser::new_local( conn, "other".to_owned(), "Another user".to_owned(), @@ -1179,9 +1194,73 @@ pub(crate) mod tests { Some("invalid_other_password".to_owned()), ) .unwrap(); + let avatar = Media::insert( + conn, + NewMedia { + file_path: "static/media/example.png".into(), + alt_text: "Another user".into(), + is_remote: false, + remote_url: None, + sensitive: false, + content_warning: None, + owner_id: other.id, + }, + ) + .unwrap(); + other.avatar_id = Some(avatar.id); + let other = other.save_changes::(&*conn).unwrap(); + vec![admin, user, other] } + fn fill_pages( + conn: &DbConn, + ) -> ( + Vec, + Vec, + Vec, + ) { + use crate::post_authors::NewPostAuthor; + use crate::posts::NewPost; + + let (mut posts, users, blogs) = crate::inbox::tests::fill_database(conn); + let user = &users[0]; + let blog = &blogs[0]; + + for i in 1..(ITEMS_PER_PAGE * 4 + 3) { + let title = format!("Post {}", i); + let content = format!("Content for post {}.", i); + let post = Post::insert( + conn, + NewPost { + blog_id: blog.id, + slug: title.clone(), + title: title.clone(), + content: SafeString::new(&content), + published: true, + license: "CC-0".into(), + creation_date: None, + ap_url: format!("{}/{}", blog.ap_url, title), + subtitle: "".into(), + source: content, + cover_id: None, + }, + ) + .unwrap(); + PostAuthor::insert( + conn, + NewPostAuthor { + post_id: post.id, + author_id: user.id, + }, + ) + .unwrap(); + posts.push(post); + } + + (posts, users, blogs) + } + #[test] fn find_by() { let conn = db(); @@ -1342,4 +1421,139 @@ pub(crate) mod tests { Ok(()) }); } + + #[test] + fn to_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let users = fill_database(&conn); + let user = &users[0]; + let act = user.to_activity(&conn)?; + + let expected = json!({ + "endpoints": { + "sharedInbox": "https://plu.me/inbox" + }, + "followers": "https://plu.me/@/admin/followers", + "following": null, + "id": "https://plu.me/@/admin/", + "inbox": "https://plu.me/@/admin/inbox", + "liked": null, + "name": "The admin", + "outbox": "https://plu.me/@/admin/outbox", + "preferredUsername": "admin", + "publicKey": { + "id": "https://plu.me/@/admin/#main-key", + "owner": "https://plu.me/@/admin/", + "publicKeyPem": user.public_key, + }, + "summary": "

Hello there, I’m the admin

\n", + "type": "Person", + "url": "https://plu.me/@/admin/" + }); + + assert_json_eq!(to_value(act)?, expected); + + let other = &users[2]; + let other_act = other.to_activity(&conn)?; + let expected_other = json!({ + "endpoints": { + "sharedInbox": "https://plu.me/inbox" + }, + "followers": "https://plu.me/@/other/followers", + "following": null, + "icon": { + "url": "https://plu.me/static/media/example.png", + "type": "Image", + }, + "id": "https://plu.me/@/other/", + "inbox": "https://plu.me/@/other/inbox", + "liked": null, + "name": "Another user", + "outbox": "https://plu.me/@/other/outbox", + "preferredUsername": "other", + "publicKey": { + "id": "https://plu.me/@/other/#main-key", + "owner": "https://plu.me/@/other/", + "publicKeyPem": other.public_key, + }, + "summary": "

Hello there, I’m someone else

\n", + "type": "Person", + "url": "https://plu.me/@/other/" + }); + + assert_json_eq!(to_value(other_act)?, expected_other); + + Ok(()) + }); + } + + #[test] + fn delete_activity() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let users = fill_database(&conn); + let user = &users[1]; + let act = user.delete_activity(&conn)?; + + let expected = json!({ + "actor": "https://plu.me/@/user/", + "cc": [], + "id": "https://plu.me/@/user/#delete", + "object": { + "id": "https://plu.me/@/user/", + "type": "Tombstone", + }, + "to": ["https://www.w3.org/ns/activitystreams#Public"], + "type": "Delete", + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } + + #[test] + fn outbox_collection() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let (_pages, users, _blogs) = fill_pages(&conn); + let user = &users[0]; + let act = user.outbox_collection(&conn)?; + + let expected = json!({ + "first": "https://plu.me/@/admin/outbox?page=1", + "items": null, + "last": "https://plu.me/@/admin/outbox?page=5", + "totalItems": 51, + "type": "OrderedCollection", + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } + + #[test] + fn outbox_collection_page() { + let conn = db(); + conn.test_transaction::<_, Error, _>(|| { + let users = fill_database(&conn); + let user = &users[0]; + let act = user.outbox_collection_page(&conn, (33, 36))?; + + let expected = json!({ + "items": [], + "partOf": "https://plu.me/@/admin/outbox", + "prev": "https://plu.me/@/admin/outbox?page=2", + "type": "OrderedCollectionPage", + }); + + assert_json_eq!(to_value(act)?, expected); + + Ok(()) + }); + } }