From 36c11182f4cffa21be113179e2ab3516420355c1 Mon Sep 17 00:00:00 2001 From: Bat Date: Tue, 12 Jun 2018 20:10:08 +0100 Subject: [PATCH] Introduce some traits for handling incoming activities --- src/activity_pub/inbox.rs | 151 ++++++++++++-------------------------- src/activity_pub/mod.rs | 6 ++ src/models/comments.rs | 21 +++++- src/models/follows.rs | 36 +++++++++ src/models/likes.rs | 25 +++++++ src/models/posts.rs | 15 ++++ src/models/reshares.rs | 24 ++++-- 7 files changed, 168 insertions(+), 110 deletions(-) diff --git a/src/activity_pub/inbox.rs b/src/activity_pub/inbox.rs index 789ab130..cbd67f39 100644 --- a/src/activity_pub/inbox.rs +++ b/src/activity_pub/inbox.rs @@ -1,27 +1,21 @@ use activitypub::{ - Actor, - activity::{Accept, Announce, Create, Follow, Like, Undo}, - object::{Article, Note} + Activity, Object, + activity::{Create, Like, Undo} }; use diesel::PgConnection; use failure::Error; use serde_json; use activity_pub::{ - broadcast, Id, IntoId, - actor::Actor as APActor, - sign::* + Id }; use models::{ - blogs::Blog, comments::*, - follows, + follows::Follow, likes, posts::*, - reshares::*, - users::User + reshares::* }; -use safe_string::SafeString; #[derive(Fail, Debug)] enum InboxError { @@ -33,98 +27,69 @@ enum InboxError { CantUndo } +pub trait FromActivity: Sized { + fn from_activity(conn: &PgConnection, obj: T, actor: Id) -> Self; + + fn try_from_activity(conn: &PgConnection, act: Create) -> bool { + if let Ok(obj) = act.create_props.object_object() { + Self::from_activity(conn, obj, act.create_props.actor_link::().unwrap()); + true + } else { + false + } + } +} + +pub trait Notify { + fn notify(conn: &PgConnection, act: T, actor: Id); +} + +pub trait Deletable { + /// true if success + fn delete_activity(conn: &PgConnection, id: Id) -> bool; +} + pub trait Inbox { fn received(&self, conn: &PgConnection, act: serde_json::Value); - fn new_article(&self, conn: &PgConnection, article: Article) -> Result<(), Error> { - Post::insert(conn, NewPost { - blog_id: 0, // TODO - slug: String::from(""), // TODO - title: article.object_props.name_string().unwrap(), - content: SafeString::new(&article.object_props.content_string().unwrap()), - published: true, - license: String::from("CC-0"), - ap_url: article.object_props.url_string()? - }); - Ok(()) - } - - fn new_comment(&self, conn: &PgConnection, note: Note, actor_id: String) -> Result<(), Error> { - let previous_url = note.object_props.in_reply_to.clone().unwrap().as_str().unwrap().to_string(); - let previous_comment = Comment::find_by_ap_url(conn, previous_url.clone()); - Comment::insert(conn, NewComment { - content: SafeString::new(¬e.object_props.content_string().unwrap()), - spoiler_text: note.object_props.summary_string().unwrap_or(String::from("")), - ap_url: note.object_props.id_string().ok(), - in_response_to_id: previous_comment.clone().map(|c| c.id), - post_id: previous_comment - .map(|c| c.post_id) - .unwrap_or_else(|| Post::find_by_ap_url(conn, previous_url).unwrap().id), - author_id: User::from_url(conn, actor_id).unwrap().id, - sensitive: false // "sensitive" is not a standard property, we need to think about how to support it with the activitystreams crate - }); - Ok(()) - } - - fn follow(&self, conn: &PgConnection, follow: Follow) -> Result<(), Error> { - let from = User::from_url(conn, follow.follow_props.actor.as_str().unwrap().to_string()).unwrap(); - match User::from_url(conn, follow.follow_props.object.as_str().unwrap().to_string()) { - Some(u) => self.accept_follow(conn, &from, &u, follow, from.id, u.id), - None => { - let blog = Blog::from_url(conn, follow.follow_props.object.as_str().unwrap().to_string()).unwrap(); - self.accept_follow(conn, &from, &blog, follow, from.id, blog.id) - } - }; - Ok(()) - } - - fn like(&self, conn: &PgConnection, like: Like) -> Result<(), Error> { - let liker = User::from_url(conn, like.like_props.actor.as_str().unwrap().to_string()); - let post = Post::find_by_ap_url(conn, like.like_props.object.as_str().unwrap().to_string()); - likes::Like::insert(conn, likes::NewLike { - post_id: post.unwrap().id, - user_id: liker.unwrap().id, - ap_url: like.object_props.id_string()? - }); - Ok(()) - } - fn unlike(&self, conn: &PgConnection, undo: Undo) -> Result<(), Error> { let like = likes::Like::find_by_ap_url(conn, undo.undo_props.object_object::()?.object_props.id_string()?).unwrap(); like.delete(conn); Ok(()) } - fn announce(&self, conn: &PgConnection, announce: Announce) -> Result<(), Error> { - let user = User::from_url(conn, announce.announce_props.actor.as_str().unwrap().to_string()); - let post = Post::find_by_ap_url(conn, announce.announce_props.object.as_str().unwrap().to_string()); - Reshare::insert(conn, NewReshare { - post_id: post.unwrap().id, - user_id: user.unwrap().id, - ap_url: announce.object_props.id_string()? - }); - Ok(()) - } - fn save(&self, conn: &PgConnection, act: serde_json::Value) -> Result<(), Error> { + let actor_id = Id::new(act["actor"].as_str().unwrap()); match act["type"].as_str() { Some(t) => { match t { - "Announce" => self.announce(conn, serde_json::from_value(act.clone())?), + "Announce" => { + Reshare::from_activity(conn, serde_json::from_value(act.clone())?, actor_id); + Ok(()) + }, "Create" => { let act: Create = serde_json::from_value(act.clone())?; - match act.create_props.object["type"].as_str().unwrap() { - "Article" => self.new_article(conn, act.create_props.object_object()?), - "Note" => self.new_comment(conn, act.create_props.object_object()?, act.create_props.actor_link::()?.0), - _ => Err(InboxError::InvalidType)? + if Post::try_from_activity(conn, act.clone()) || Comment::try_from_activity(conn, act) { + Ok(()) + } else { + Err(InboxError::InvalidType)? } }, - "Follow" => self.follow(conn, serde_json::from_value(act.clone())?), - "Like" => self.like(conn, serde_json::from_value(act.clone())?), + "Follow" => { + Follow::from_activity(conn, serde_json::from_value(act.clone())?, actor_id); + Ok(()) + }, + "Like" => { + likes::Like::from_activity(conn, serde_json::from_value(act.clone())?, actor_id); + Ok(()) + }, "Undo" => { let act: Undo = serde_json::from_value(act.clone())?; match act.undo_props.object["type"].as_str().unwrap() { - "Like" => self.unlike(conn, act), + "Like" => { + likes::Like::delete_activity(conn, Id::new(act.undo_props.object_object::()?.object_props.id_string()?)); + Ok(()) + }, _ => Err(InboxError::CantUndo)? } } @@ -134,26 +99,6 @@ pub trait Inbox { None => Err(InboxError::NoType)? } } - - fn accept_follow( - &self, - conn: &PgConnection, - from: &A, - target: &B, - follow: Follow, - from_id: i32, - target_id: i32 - ) { - follows::Follow::insert(conn, follows::NewFollow { - follower_id: from_id, - following_id: target_id - }); - - let mut accept = Accept::default(); - accept.accept_props.set_actor_link::(from.clone().into_id()).unwrap(); - accept.accept_props.set_object_object(follow).unwrap(); - broadcast(conn, &*from, accept, vec![target.clone()]); - } } pub trait WithInbox { diff --git a/src/activity_pub/mod.rs b/src/activity_pub/mod.rs index febd9591..fc49c75e 100644 --- a/src/activity_pub/mod.rs +++ b/src/activity_pub/mod.rs @@ -111,6 +111,12 @@ impl Id { } } +impl Into for Id { + fn into(self) -> String { + self.0.clone() + } +} + pub trait IntoId { fn into_id(self) -> Id; } diff --git a/src/models/comments.rs b/src/models/comments.rs index e763e906..be66f878 100644 --- a/src/models/comments.rs +++ b/src/models/comments.rs @@ -7,8 +7,9 @@ use diesel::{self, PgConnection, RunQueryDsl, QueryDsl, ExpressionMethods, dsl:: use serde_json; use activity_pub::{ - ap_url, IntoId, PUBLIC_VISIBILTY, + ap_url, Id, IntoId, PUBLIC_VISIBILTY, actor::Actor, + inbox::FromActivity, object::Object }; use models::{ @@ -123,6 +124,24 @@ impl Comment { } } +impl FromActivity for Comment { + fn from_activity(conn: &PgConnection, note: Note, actor: Id) -> Comment { + let previous_url = note.object_props.in_reply_to.clone().unwrap().as_str().unwrap().to_string(); + let previous_comment = Comment::find_by_ap_url(conn, previous_url.clone()); + Comment::insert(conn, NewComment { + content: SafeString::new(¬e.object_props.content_string().unwrap()), + spoiler_text: note.object_props.summary_string().unwrap_or(String::from("")), + ap_url: note.object_props.id_string().ok(), + in_response_to_id: previous_comment.clone().map(|c| c.id), + post_id: previous_comment + .map(|c| c.post_id) + .unwrap_or_else(|| Post::find_by_ap_url(conn, previous_url).unwrap().id), + author_id: User::from_url(conn, actor.into()).unwrap().id, + sensitive: false // "sensitive" is not a standard property, we need to think about how to support it with the activitypub crate + }) + } +} + impl Object for Comment { fn serialize(&self, conn: &PgConnection) -> serde_json::Value { let mut to = self.get_author(conn).get_followers(conn).into_iter().map(|f| f.ap_url).collect::>(); diff --git a/src/models/follows.rs b/src/models/follows.rs index 6debe46e..fa5f66fc 100644 --- a/src/models/follows.rs +++ b/src/models/follows.rs @@ -1,5 +1,8 @@ +use activitypub::{Actor, activity::{Accept, Follow as FollowAct}}; use diesel::{self, PgConnection, ExpressionMethods, QueryDsl, RunQueryDsl}; +use activity_pub::{broadcast, Id, IntoId, actor::Actor as ApActor, inbox::{FromActivity, WithInbox}, sign::Signer}; +use models::blogs::Blog; use models::users::User; use schema::follows; @@ -33,4 +36,37 @@ impl Follow { .expect("Unable to load follow by id") .into_iter().nth(0) } + + pub fn accept_follow( + conn: &PgConnection, + from: &A, + target: &B, + follow: FollowAct, + from_id: i32, + target_id: i32 + ) -> Follow { + let res = Follow::insert(conn, NewFollow { + follower_id: from_id, + following_id: target_id + }); + + let mut accept = Accept::default(); + accept.accept_props.set_actor_link::(from.clone().into_id()).unwrap(); + accept.accept_props.set_object_object(follow).unwrap(); + broadcast(conn, &*from, accept, vec![target.clone()]); + res + } +} + +impl FromActivity for Follow { + fn from_activity(conn: &PgConnection, follow: FollowAct, _actor: Id) -> Follow { + let from = User::from_url(conn, follow.follow_props.actor.as_str().unwrap().to_string()).unwrap(); + match User::from_url(conn, follow.follow_props.object.as_str().unwrap().to_string()) { + Some(u) => Follow::accept_follow(conn, &from, &u, follow, from.id, u.id), + None => { + let blog = Blog::from_url(conn, follow.follow_props.object.as_str().unwrap().to_string()).unwrap(); + Follow::accept_follow(conn, &from, &blog, follow, from.id, blog.id) + } + } + } } diff --git a/src/models/likes.rs b/src/models/likes.rs index 65b24ff8..59bd48bc 100644 --- a/src/models/likes.rs +++ b/src/models/likes.rs @@ -4,8 +4,10 @@ use diesel::{self, PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods}; use serde_json; use activity_pub::{ + Id, IntoId, actor::Actor, + inbox::{FromActivity, Deletable}, object::Object }; use models::{ @@ -94,6 +96,29 @@ impl Like { } } +impl FromActivity for Like { + fn from_activity(conn: &PgConnection, like: activity::Like, _actor: Id) -> Like { + let liker = User::from_url(conn, like.like_props.actor.as_str().unwrap().to_string()); + let post = Post::find_by_ap_url(conn, like.like_props.object.as_str().unwrap().to_string()); + Like::insert(conn, NewLike { + post_id: post.unwrap().id, + user_id: liker.unwrap().id, + ap_url: like.object_props.id_string().unwrap_or(String::from("")) + }) + } +} + +impl Deletable for Like { + fn delete_activity(conn: &PgConnection, id: Id) -> bool { + if let Some(like) = Like::find_by_ap_url(conn, id.into()) { + like.delete(conn); + true + } else { + false + } + } +} + impl Object for Like { fn serialize(&self, conn: &PgConnection) -> serde_json::Value { json!({ diff --git a/src/models/posts.rs b/src/models/posts.rs index be2ce429..6e3f4899 100644 --- a/src/models/posts.rs +++ b/src/models/posts.rs @@ -10,6 +10,7 @@ use BASE_URL; use activity_pub::{ PUBLIC_VISIBILTY, ap_url, Id, IntoId, actor::Actor, + inbox::FromActivity, object::Object }; use models::{ @@ -195,6 +196,20 @@ impl Post { } } +impl FromActivity
for Post { + fn from_activity(conn: &PgConnection, article: Article, _actor: Id) -> Post { + Post::insert(conn, NewPost { + blog_id: 0, // TODO + slug: String::from(""), // TODO + title: article.object_props.name_string().unwrap(), + content: SafeString::new(&article.object_props.content_string().unwrap()), + published: true, + license: String::from("CC-0"), + ap_url: article.object_props.url_string().unwrap_or(String::from("")) + }) + } +} + impl IntoId for Post { fn into_id(self) -> Id { Id::new(self.ap_url.clone()) diff --git a/src/models/reshares.rs b/src/models/reshares.rs index 65c4739e..2e2896d8 100644 --- a/src/models/reshares.rs +++ b/src/models/reshares.rs @@ -1,8 +1,8 @@ -use activitypub::activity; +use activitypub::activity::{Announce, Undo}; use chrono::NaiveDateTime; use diesel::{self, PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods}; -use activity_pub::{IntoId, actor::Actor, object::Object}; +use activity_pub::{Id, IntoId, actor::Actor, inbox::FromActivity, object::Object}; use models::{posts::Post, users::User}; use schema::reshares; @@ -80,17 +80,17 @@ impl Reshare { Post::get(conn, self.post_id) } - pub fn delete(&self, conn: &PgConnection) -> activity::Undo { + pub fn delete(&self, conn: &PgConnection) -> Undo { diesel::delete(self).execute(conn).unwrap(); - let mut act = activity::Undo::default(); + let mut act = Undo::default(); act.undo_props.set_actor_link(User::get(conn, self.user_id).unwrap().into_id()).unwrap(); act.undo_props.set_object_object(self.into_activity(conn)).unwrap(); act } - pub fn into_activity(&self, conn: &PgConnection) -> activity::Announce { - let mut act = activity::Announce::default(); + pub fn into_activity(&self, conn: &PgConnection) -> Announce { + let mut act = Announce::default(); act.announce_props.set_actor_link(User::get(conn, self.user_id).unwrap().into_id()).unwrap(); act.announce_props.set_object_link(Post::get(conn, self.post_id).unwrap().into_id()).unwrap(); act.object_props.set_id_string(self.ap_url.clone()).unwrap(); @@ -98,3 +98,15 @@ impl Reshare { act } } + +impl FromActivity for Reshare { + fn from_activity(conn: &PgConnection, announce: Announce, _actor: Id) -> Reshare { + let user = User::from_url(conn, announce.announce_props.actor.as_str().unwrap().to_string()); + let post = Post::find_by_ap_url(conn, announce.announce_props.object.as_str().unwrap().to_string()); + Reshare::insert(conn, NewReshare { + post_id: post.unwrap().id, + user_id: user.unwrap().id, + ap_url: announce.object_props.id_string().unwrap_or(String::from("")) + }) + } +}