diff --git a/Cargo.lock b/Cargo.lock index eab553ad..3c603262 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -869,6 +869,7 @@ name = "plume" version = "0.1.0" dependencies = [ "activitystreams 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "activitystreams-derive 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "activitystreams-traits 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "activitystreams-types 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "array_tool 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 4f5ee8f7..d5b416c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ name = "plume" version = "0.1.0" [dependencies] activitystreams = "0.1" +activitystreams-derive = "0.1" activitystreams-traits = "0.1" activitystreams-types = "0.1" array_tool = "1.0" diff --git a/src/activity_pub/inbox.rs b/src/activity_pub/inbox.rs index 0a4d6c3f..55f22dae 100644 --- a/src/activity_pub/inbox.rs +++ b/src/activity_pub/inbox.rs @@ -8,7 +8,7 @@ use diesel::PgConnection; use failure::Error; use serde_json; -use activity_pub::{broadcast, IntoId}; +use activity_pub::{broadcast, Id, IntoId}; use activity_pub::actor::Actor as APActor; use activity_pub::sign::*; use models::blogs::Blog; @@ -118,7 +118,7 @@ pub trait Inbox { } } - fn accept_follow( + fn accept_follow( &self, conn: &PgConnection, from: &A, @@ -132,8 +132,8 @@ pub trait Inbox { following_id: target_id }); - let mut accept = Accept::default();//new(target, follow, conn); - accept.set_actor_link(from.into()).unwrap(); + let mut accept = Accept::default(); + accept.set_actor_link::(from.clone().into_id()).unwrap(); accept.set_object_object(follow).unwrap(); broadcast(conn, &*from, accept, vec![target.clone()]); } diff --git a/src/activity_pub/mod.rs b/src/activity_pub/mod.rs index 864a5f5a..f129041e 100644 --- a/src/activity_pub/mod.rs +++ b/src/activity_pub/mod.rs @@ -102,7 +102,7 @@ pub fn broadcast Id; + fn into_id(self) -> Id; } impl Link for Id {} diff --git a/src/main.rs b/src/main.rs index c0b18605..4d17a369 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,8 @@ #![plugin(rocket_codegen)] extern crate activitystreams; +#[macro_use] +extern crate activitystreams_derive; extern crate activitystreams_traits; extern crate activitystreams_types; extern crate array_tool; diff --git a/src/models/blogs.rs b/src/models/blogs.rs index 66851f75..f246dda8 100644 --- a/src/models/blogs.rs +++ b/src/models/blogs.rs @@ -12,7 +12,7 @@ use openssl::pkey::{PKey, Private}; use openssl::rsa::Rsa; use openssl::sign::Signer; -use activity_pub::{ActivityStream, Id}; +use activity_pub::{ActivityStream, Id, IntoId}; use activity_pub::actor::{Actor as APActor, ActorType}; use activity_pub::inbox::WithInbox; use activity_pub::sign; @@ -175,8 +175,8 @@ impl Blog { } } -impl Into for Blog { - fn into(self) -> Id { +impl IntoId for Blog { + fn into_id(self) -> Id { Id::new(self.ap_url) } } diff --git a/src/models/comments.rs b/src/models/comments.rs index 43f38e1c..14a73bbf 100644 --- a/src/models/comments.rs +++ b/src/models/comments.rs @@ -1,8 +1,12 @@ +use activitystreams_types::{ + activity::Create, + object::{Note, properties::ObjectProperties} +}; use chrono; use diesel::{self, PgConnection, RunQueryDsl, QueryDsl, ExpressionMethods}; use serde_json; -use activity_pub::{ap_url, PUBLIC_VISIBILTY}; +use activity_pub::{ap_url, IntoId, PUBLIC_VISIBILTY}; use activity_pub::actor::Actor; use activity_pub::object::Object; use models::posts::Post; @@ -71,6 +75,37 @@ impl Comment { pub fn get_post(&self, conn: &PgConnection) -> Post { Post::get(conn, self.post_id).unwrap() } + + pub fn into_activity(&self, conn: &PgConnection) -> Note { + let mut to = self.get_author(conn).get_followers(conn).into_iter().map(|f| f.ap_url).collect::>(); + to.append(&mut self.get_post(conn).get_receivers_urls(conn)); + to.push(PUBLIC_VISIBILTY.to_string()); + + let mut comment = Note::default(); + comment.object_props = ObjectProperties { + id: Some(serde_json::to_value(self.ap_url.clone()).unwrap()), + summary: Some(serde_json::to_value(self.spoiler_text.clone()).unwrap()), + content: Some(serde_json::to_value(self.content.clone()).unwrap()), + in_reply_to: Some(serde_json::to_value(self.in_response_to_id.map_or_else(|| self.get_post(conn).ap_url, |id| { + let comm = Comment::get(conn, id).unwrap(); + comm.ap_url.clone().unwrap_or(comm.compute_id(conn)) + })).unwrap()), + published: Some(serde_json::to_value(self.creation_date).unwrap()), + attributed_to: Some(serde_json::to_value(self.get_author(conn).compute_id(conn)).unwrap()), + to: Some(serde_json::to_value(to).unwrap()), + cc: Some(serde_json::to_value(Vec::::new()).unwrap()), + ..ObjectProperties::default() + }; + comment + } + + pub fn create_activity(&self, conn: &PgConnection) -> Create { + let mut act = Create::default(); + act.set_actor_link(self.get_author(conn).into_id()).unwrap(); + act.set_object_object(self.into_activity(conn)).unwrap(); + act.object_props.set_id_string(format!("{}/activity", self.ap_url.clone().unwrap())).unwrap(); + act + } } impl Object for Comment { diff --git a/src/models/likes.rs b/src/models/likes.rs index 1cf3bef5..31de4f0d 100644 --- a/src/models/likes.rs +++ b/src/models/likes.rs @@ -1,7 +1,9 @@ +use activitystreams_types::activity; use chrono; use diesel::{self, PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods}; use serde_json; +use activity_pub::IntoId; use activity_pub::actor::Actor; use activity_pub::object::Object; use models::posts::Post; @@ -66,8 +68,25 @@ impl Like { .into_iter().nth(0) } - pub fn delete(&self, conn: &PgConnection) { + pub fn delete(&self, conn: &PgConnection) -> activity::Undo { diesel::delete(self).execute(conn).unwrap(); + + let mut act = activity::Undo::default(); + act.set_actor_link(User::get(conn, self.user_id).unwrap().into_id()).unwrap(); + act.set_object_object(self.into_activity(conn)).unwrap(); + act + } + + pub fn into_activity(&self, conn: &PgConnection) -> activity::Like { + let mut act = activity::Like::default(); + act.set_actor_link(User::get(conn, self.user_id).unwrap().into_id()).unwrap(); + act.set_object_link(Post::get(conn, self.post_id).unwrap().into_id()).unwrap(); + act.object_props.set_id_string(format!("{}/like/{}", + User::get(conn, self.user_id).unwrap().ap_url, + Post::get(conn, self.post_id).unwrap().ap_url + )).unwrap(); + + act } } diff --git a/src/models/posts.rs b/src/models/posts.rs index 87f8eeac..743f7ea6 100644 --- a/src/models/posts.rs +++ b/src/models/posts.rs @@ -1,10 +1,14 @@ +use activitystreams_types::{ + activity::Create, + object::{Article, properties::ObjectProperties} +}; use chrono::NaiveDateTime; use diesel::{self, PgConnection, RunQueryDsl, QueryDsl, ExpressionMethods, BelongingToDsl}; use diesel::dsl::any; use serde_json; use BASE_URL; -use activity_pub::{PUBLIC_VISIBILTY, ap_url}; +use activity_pub::{PUBLIC_VISIBILTY, ap_url, Id, IntoId}; use activity_pub::actor::Actor; use activity_pub::object::Object; use models::blogs::Blog; @@ -137,6 +141,40 @@ impl Post { }); to } + + pub fn into_activity(&self, conn: &PgConnection) -> Article { + let mut to = self.get_receivers_urls(conn); + to.push(PUBLIC_VISIBILTY.to_string()); + + let mut article = Article::default(); + article.object_props = ObjectProperties { + name: Some(serde_json::to_value(self.title.clone()).unwrap()), + id: Some(serde_json::to_value(self.ap_url.clone()).unwrap()), + attributed_to: Some(serde_json::to_value(self.get_authors(conn).into_iter().map(|x| x.ap_url).collect::>()).unwrap()), + content: Some(serde_json::to_value(self.content.clone()).unwrap()), + published: Some(serde_json::to_value(self.creation_date).unwrap()), + tag: Some(serde_json::to_value(Vec::::new()).unwrap()), + url: Some(serde_json::to_value(self.compute_id(conn)).unwrap()), + to: Some(serde_json::to_value(to).unwrap()), + cc: Some(serde_json::to_value(Vec::::new()).unwrap()), + ..ObjectProperties::default() + }; + article + } + + pub fn create_activity(&self, conn: &PgConnection) -> Create { + let mut act = Create::default(); + act.object_props.set_id_string(format!("{}/activity", self.ap_url)).unwrap(); + act.set_actor_link(Id::new(self.get_authors(conn)[0].clone().ap_url)).unwrap(); + act.set_object_object(self.into_activity(conn)).unwrap(); + act + } +} + +impl IntoId for Post { + fn into_id(self) -> Id { + Id::new(self.ap_url.clone()) + } } impl Object for Post { diff --git a/src/models/users.rs b/src/models/users.rs index e39dcb14..b4614454 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -1,7 +1,9 @@ -use activitystreams_traits::{Actor, Object}; +use activitystreams_traits::{Actor, Object, Link}; use activitystreams_types::{ - activity::Create, - collection::OrderedCollection + actor::Person, + collection::OrderedCollection, + object::properties::ObjectProperties, + CustomObject }; use bcrypt; use chrono::NaiveDateTime; @@ -227,8 +229,11 @@ impl User { } pub fn outbox(&self, conn: &PgConnection) -> ActivityStream { - let mut coll = OrderedCollection::default(); // TODO - coll.collection_props.items = serde_json::to_value(self.get_activities(conn)).unwrap(); + let acts = self.get_activities(conn); + let n_acts = acts.len(); + let mut coll = OrderedCollection::default(); + coll.collection_props.items = serde_json::to_value(acts).unwrap(); + coll.collection_props.set_total_items_u64(n_acts as u64).unwrap(); ActivityStream::new(coll) } @@ -237,10 +242,8 @@ impl User { use schema::post_authors; let posts_by_self = PostAuthor::belonging_to(self).select(post_authors::post_id); let posts = posts::table.filter(posts::id.eq(any(posts_by_self))).load::(conn).unwrap(); - posts.into_iter().map(|_| { - // TODO Create::new(self, &p, conn) - // TODO: add a method to convert Post -> Create - serde_json::to_value(Create::default()).unwrap() + posts.into_iter().map(|p| { + serde_json::to_value(p.create_activity(conn)).unwrap() }).collect::>() } @@ -278,6 +281,42 @@ impl User { pub fn get_keypair(&self) -> PKey { PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.clone().unwrap().as_ref()).unwrap()).unwrap() } + + pub fn into_activity(&self, conn: &PgConnection) -> CustomObject { + let mut actor = Person::default(); + actor.object_props = ObjectProperties { + id: Some(serde_json::to_value(self.compute_id(conn)).unwrap()), + name: Some(serde_json::to_value(self.get_display_name()).unwrap()), + summary: Some(serde_json::to_value(self.get_summary()).unwrap()), + url: Some(serde_json::to_value(self.compute_id(conn)).unwrap()), + ..ObjectProperties::default() + }; + + CustomObject::new(actor, ApProps { + inbox: Some(serde_json::to_value(self.compute_inbox(conn)).unwrap()), + outbox: Some(serde_json::to_value(self.compute_outbox(conn)).unwrap()), + preferred_username: Some(serde_json::to_value(self.get_actor_id()).unwrap()), + endpoints: Some(json!({ + "sharedInbox": ap_url(format!("{}/inbox", BASE_URL.as_str())) + })) + }) + } +} + +#[derive(Serialize, Deserialize, Default, Properties)] +#[serde(rename_all = "camelCase")] +pub struct ApProps { + #[activitystreams(ab(Object, Link))] + inbox: Option, + + #[activitystreams(ab(Object, Link))] + outbox: Option, + + #[activitystreams(ab(Object, Link))] + preferred_username: Option, + + #[activitystreams(ab(Object))] + endpoints: Option } impl<'a, 'r> FromRequest<'a, 'r> for User { @@ -359,7 +398,7 @@ impl APActor for User { } impl IntoId for User { - fn into(&self) -> Id { + fn into_id(self) -> Id { Id::new(self.ap_url.clone()) } } diff --git a/src/routes/comments.rs b/src/routes/comments.rs index 5dbc586a..4561b731 100644 --- a/src/routes/comments.rs +++ b/src/routes/comments.rs @@ -1,4 +1,3 @@ -use activitystreams_types::activity::Create; use rocket::request::Form; use rocket::response::Redirect; use rocket_contrib::Template; @@ -41,8 +40,8 @@ fn create(blog: String, slug: String, query: CommentQuery, data: Form Redirect { ap_url: "".to_string() }); like.update_ap_url(&*conn); - // TODO: let act = Like::new(&user, &post, &*conn); - // TODO: broadcast(&*conn, &user, act, user.get_followers(&*conn)); + + broadcast(&*conn, &user, like.into_activity(&*conn), user.get_followers(&*conn)); } else { let like = likes::Like::find_by_user_on_post(&*conn, &user, &post).unwrap(); - // TODO: like.delete(&*conn); - // TODO: broadcast(&*conn, &user, Undo::new(&user, &like, &*conn), user.get_followers(&*conn)); + let delete_act = like.delete(&*conn); + broadcast(&*conn, &user, delete_act, user.get_followers(&*conn)); } Redirect::to(format!("/~/{}/{}/", blog, slug).as_ref()) diff --git a/src/routes/posts.rs b/src/routes/posts.rs index 44b5e9d9..3732dcbc 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -1,11 +1,10 @@ -use activitystreams_types::activity::Create; use heck::KebabCase; use rocket::request::Form; use rocket::response::Redirect; use rocket_contrib::Template; use serde_json; -use activity_pub::{broadcast, context, activity_pub, ActivityPub, Id}; +use activity_pub::{broadcast, context, activity_pub, ActivityPub}; use activity_pub::object::Object; use db_conn::DbConn; use models::blogs::*; @@ -38,7 +37,7 @@ fn details(blog: String, slug: String, conn: DbConn, user: Option) -> Temp #[get("/~/<_blog>/", rank = 3, format = "application/activity+json")] fn activity_details(_blog: String, slug: String, conn: DbConn) -> ActivityPub { - // TODO: posts in different blogs may have the same slug + // FIXME: posts in different blogs may have the same slug let post = Post::find_by_slug(&*conn, slug).unwrap(); let mut act = post.serialize(&*conn); @@ -85,12 +84,8 @@ fn create(blog_name: String, data: Form, user: User, conn: DbConn) author_id: user.id }); - // TODO: use Post -> Create conversion - // let act = Create::default(); - // act.object_props.set_id_string(format!("{}/activity", post.compute_id(&*conn))); - // act.set_actor_link(Id::new(user.ap_url)); - // act.set_object_object(); - // broadcast(&*conn, &user, act, user.get_followers(&*conn)); + let act = post.create_activity(&*conn); + broadcast(&*conn, &user, act, user.get_followers(&*conn)); Redirect::to(format!("/~/{}/{}", blog_name, slug).as_str()) } diff --git a/src/routes/user.rs b/src/routes/user.rs index cb562b5b..0cb8be9a 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -7,7 +7,7 @@ use rocket::response::Redirect; use rocket_contrib::Template; use serde_json; -use activity_pub::{activity_pub, ActivityPub, ActivityStream, context, broadcast}; +use activity_pub::{activity_pub, ActivityPub, ActivityStream, context, broadcast, Id, IntoId}; use activity_pub::actor::Actor; use activity_pub::inbox::Inbox; use db_conn::DbConn; @@ -56,8 +56,10 @@ fn follow(name: String, conn: DbConn, user: User) -> Redirect { follower_id: user.id, following_id: target.id }); - let act = Follow::default(); - // TODO + let mut act = Follow::default(); + act.set_actor_link::(user.clone().into_id()).unwrap(); + act.set_object_object(user.into_activity(&*conn)).unwrap(); + act.object_props.set_id_string(format!("{}/follow/{}", user.ap_url, target.ap_url)).unwrap(); broadcast(&*conn, &user, act, vec![target]); Redirect::to(format!("/@/{}", name).as_ref()) }