Introduce some traits for handling incoming activities

This commit is contained in:
Bat 2018-06-12 20:10:08 +01:00
parent 7d17751f50
commit 36c11182f4
7 changed files with 168 additions and 110 deletions

View File

@ -1,27 +1,21 @@
use activitypub::{ use activitypub::{
Actor, Activity, Object,
activity::{Accept, Announce, Create, Follow, Like, Undo}, activity::{Create, Like, Undo}
object::{Article, Note}
}; };
use diesel::PgConnection; use diesel::PgConnection;
use failure::Error; use failure::Error;
use serde_json; use serde_json;
use activity_pub::{ use activity_pub::{
broadcast, Id, IntoId, Id
actor::Actor as APActor,
sign::*
}; };
use models::{ use models::{
blogs::Blog,
comments::*, comments::*,
follows, follows::Follow,
likes, likes,
posts::*, posts::*,
reshares::*, reshares::*
users::User
}; };
use safe_string::SafeString;
#[derive(Fail, Debug)] #[derive(Fail, Debug)]
enum InboxError { enum InboxError {
@ -33,98 +27,69 @@ enum InboxError {
CantUndo CantUndo
} }
pub trait FromActivity<T: Object>: 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::<Id>().unwrap());
true
} else {
false
}
}
}
pub trait Notify<T: Activity> {
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 { pub trait Inbox {
fn received(&self, conn: &PgConnection, act: serde_json::Value); 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(&note.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> { fn unlike(&self, conn: &PgConnection, undo: Undo) -> Result<(), Error> {
let like = likes::Like::find_by_ap_url(conn, undo.undo_props.object_object::<Like>()?.object_props.id_string()?).unwrap(); let like = likes::Like::find_by_ap_url(conn, undo.undo_props.object_object::<Like>()?.object_props.id_string()?).unwrap();
like.delete(conn); like.delete(conn);
Ok(()) 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> { 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() { match act["type"].as_str() {
Some(t) => { Some(t) => {
match 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" => { "Create" => {
let act: Create = serde_json::from_value(act.clone())?; let act: Create = serde_json::from_value(act.clone())?;
match act.create_props.object["type"].as_str().unwrap() { if Post::try_from_activity(conn, act.clone()) || Comment::try_from_activity(conn, act) {
"Article" => self.new_article(conn, act.create_props.object_object()?), Ok(())
"Note" => self.new_comment(conn, act.create_props.object_object()?, act.create_props.actor_link::<Id>()?.0), } else {
_ => Err(InboxError::InvalidType)? Err(InboxError::InvalidType)?
} }
}, },
"Follow" => self.follow(conn, serde_json::from_value(act.clone())?), "Follow" => {
"Like" => self.like(conn, serde_json::from_value(act.clone())?), 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" => { "Undo" => {
let act: Undo = serde_json::from_value(act.clone())?; let act: Undo = serde_json::from_value(act.clone())?;
match act.undo_props.object["type"].as_str().unwrap() { 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::<Like>()?.object_props.id_string()?));
Ok(())
},
_ => Err(InboxError::CantUndo)? _ => Err(InboxError::CantUndo)?
} }
} }
@ -134,26 +99,6 @@ pub trait Inbox {
None => Err(InboxError::NoType)? None => Err(InboxError::NoType)?
} }
} }
fn accept_follow<A: Signer + IntoId + Clone, B: Clone + WithInbox + Actor>(
&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::<Id>(from.clone().into_id()).unwrap();
accept.accept_props.set_object_object(follow).unwrap();
broadcast(conn, &*from, accept, vec![target.clone()]);
}
} }
pub trait WithInbox { pub trait WithInbox {

View File

@ -111,6 +111,12 @@ impl Id {
} }
} }
impl Into<String> for Id {
fn into(self) -> String {
self.0.clone()
}
}
pub trait IntoId { pub trait IntoId {
fn into_id(self) -> Id; fn into_id(self) -> Id;
} }

View File

@ -7,8 +7,9 @@ use diesel::{self, PgConnection, RunQueryDsl, QueryDsl, ExpressionMethods, dsl::
use serde_json; use serde_json;
use activity_pub::{ use activity_pub::{
ap_url, IntoId, PUBLIC_VISIBILTY, ap_url, Id, IntoId, PUBLIC_VISIBILTY,
actor::Actor, actor::Actor,
inbox::FromActivity,
object::Object object::Object
}; };
use models::{ use models::{
@ -123,6 +124,24 @@ impl Comment {
} }
} }
impl FromActivity<Note> 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(&note.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 { impl Object for Comment {
fn serialize(&self, conn: &PgConnection) -> serde_json::Value { 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::<Vec<String>>(); let mut to = self.get_author(conn).get_followers(conn).into_iter().map(|f| f.ap_url).collect::<Vec<String>>();

View File

@ -1,5 +1,8 @@
use activitypub::{Actor, activity::{Accept, Follow as FollowAct}};
use diesel::{self, PgConnection, ExpressionMethods, QueryDsl, RunQueryDsl}; 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 models::users::User;
use schema::follows; use schema::follows;
@ -33,4 +36,37 @@ impl Follow {
.expect("Unable to load follow by id") .expect("Unable to load follow by id")
.into_iter().nth(0) .into_iter().nth(0)
} }
pub fn accept_follow<A: Signer + IntoId + Clone, B: Clone + WithInbox + Actor>(
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::<Id>(from.clone().into_id()).unwrap();
accept.accept_props.set_object_object(follow).unwrap();
broadcast(conn, &*from, accept, vec![target.clone()]);
res
}
}
impl FromActivity<FollowAct> 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)
}
}
}
} }

View File

@ -4,8 +4,10 @@ use diesel::{self, PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods};
use serde_json; use serde_json;
use activity_pub::{ use activity_pub::{
Id,
IntoId, IntoId,
actor::Actor, actor::Actor,
inbox::{FromActivity, Deletable},
object::Object object::Object
}; };
use models::{ use models::{
@ -94,6 +96,29 @@ impl Like {
} }
} }
impl FromActivity<activity::Like> 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 { impl Object for Like {
fn serialize(&self, conn: &PgConnection) -> serde_json::Value { fn serialize(&self, conn: &PgConnection) -> serde_json::Value {
json!({ json!({

View File

@ -10,6 +10,7 @@ use BASE_URL;
use activity_pub::{ use activity_pub::{
PUBLIC_VISIBILTY, ap_url, Id, IntoId, PUBLIC_VISIBILTY, ap_url, Id, IntoId,
actor::Actor, actor::Actor,
inbox::FromActivity,
object::Object object::Object
}; };
use models::{ use models::{
@ -195,6 +196,20 @@ impl Post {
} }
} }
impl FromActivity<Article> 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 { impl IntoId for Post {
fn into_id(self) -> Id { fn into_id(self) -> Id {
Id::new(self.ap_url.clone()) Id::new(self.ap_url.clone())

View File

@ -1,8 +1,8 @@
use activitypub::activity; use activitypub::activity::{Announce, Undo};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods}; 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 models::{posts::Post, users::User};
use schema::reshares; use schema::reshares;
@ -80,17 +80,17 @@ impl Reshare {
Post::get(conn, self.post_id) 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(); 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_actor_link(User::get(conn, self.user_id).unwrap().into_id()).unwrap();
act.undo_props.set_object_object(self.into_activity(conn)).unwrap(); act.undo_props.set_object_object(self.into_activity(conn)).unwrap();
act act
} }
pub fn into_activity(&self, conn: &PgConnection) -> activity::Announce { pub fn into_activity(&self, conn: &PgConnection) -> Announce {
let mut act = activity::Announce::default(); 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_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.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(); act.object_props.set_id_string(self.ap_url.clone()).unwrap();
@ -98,3 +98,15 @@ impl Reshare {
act act
} }
} }
impl FromActivity<Announce> 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(""))
})
}
}