diff --git a/Cargo.lock b/Cargo.lock index 6137ab2c..49f848cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -816,6 +816,7 @@ dependencies = [ "serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3493fcad..44acbf12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,6 @@ version = "0.1.0" [dependencies] base64 = "0.9.1" bcrypt = "0.2" -chrono = { version = "0.4", features = ["serde"] } dotenv = "*" heck = "0.3.0" hex = "0.3" @@ -16,6 +15,11 @@ rocket_codegen = "*" serde = "*" serde_derive = "1.0" serde_json = "1.0" +url = "1.7" + +[dependencies.chrono] +features = ["serde"] +version = "0.4" [dependencies.diesel] features = ["postgres", "r2d2", "chrono"] diff --git a/migrations/2018-05-01-165325_add_ap_url/down.sql b/migrations/2018-05-01-165325_add_ap_url/down.sql new file mode 100644 index 00000000..2f1533f1 --- /dev/null +++ b/migrations/2018-05-01-165325_add_ap_url/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE blogs DROP COLUMN ap_url; +ALTER TABLE users DROP COLUMN ap_url; diff --git a/migrations/2018-05-01-165325_add_ap_url/up.sql b/migrations/2018-05-01-165325_add_ap_url/up.sql new file mode 100644 index 00000000..1f2f8e2d --- /dev/null +++ b/migrations/2018-05-01-165325_add_ap_url/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE blogs ADD COLUMN ap_url TEXT NOT NULL default ''; +ALTER TABLE users ADD COLUMN ap_url TEXT NOT NULL default ''; diff --git a/src/activity_pub/activity.rs b/src/activity_pub/activity.rs index 9c41e54e..a4d178eb 100644 --- a/src/activity_pub/activity.rs +++ b/src/activity_pub/activity.rs @@ -7,35 +7,52 @@ use activity_pub::object::Object; #[derive(Clone)] pub enum Activity { - Create(CreatePayload) + Create(Payload), + Accept(Payload) } impl Activity { pub fn serialize(&self) -> serde_json::Value { + json!({ + "type": self.get_type(), + "actor": self.payload().by, + "object": self.payload().object, + "published": self.payload().date.to_rfc3339() + }) + } + + pub fn get_type(&self) -> String { match self { - Activity::Create(data) => json!({ - "type": "Create", - "actor": data.by, - "object": data.object, - "published": data.date.to_rfc3339() - }) + Activity::Accept(_) => String::from("Accept"), + Activity::Create(_) => String::from("Create") + } + } + + pub fn payload(&self) -> Payload { + match self { + Activity::Accept(p) => p.clone(), + Activity::Create(p) => p.clone() } } pub fn create(by: &U, obj: T, conn: &PgConnection) -> Activity { - Activity::Create(CreatePayload::new(serde_json::Value::String(by.compute_id(conn)), obj.serialize(conn))) + Activity::Create(Payload::new(serde_json::Value::String(by.compute_id(conn)), obj.serialize(conn))) + } + + pub fn accept(by: &A, what: String, conn: &PgConnection) -> Activity { + Activity::Accept(Payload::new(serde_json::Value::String(by.compute_id(conn)), serde_json::Value::String(what))) } } #[derive(Clone)] -pub struct CreatePayload { +pub struct Payload { by: serde_json::Value, object: serde_json::Value, date: chrono::DateTime } -impl CreatePayload { - pub fn new(by: serde_json::Value, obj: serde_json::Value) -> CreatePayload { - CreatePayload { +impl Payload { + pub fn new(by: serde_json::Value, obj: serde_json::Value) -> Payload { + Payload { by: by, object: obj, date: chrono::Utc::now() diff --git a/src/activity_pub/actor.rs b/src/activity_pub/actor.rs index 224eddf5..de520ba9 100644 --- a/src/activity_pub/actor.rs +++ b/src/activity_pub/actor.rs @@ -19,7 +19,7 @@ impl ToString for ActorType { } } -pub trait Actor { +pub trait Actor: Sized { fn get_box_prefix() -> &'static str; fn get_actor_id(&self) -> String; @@ -76,4 +76,6 @@ pub trait Actor { Err(_) => println!("Error while sending to inbox") } } + + fn from_url(conn: &PgConnection, url: String) -> Option; } diff --git a/src/activity_pub/inbox.rs b/src/activity_pub/inbox.rs index 343aa6b4..739ae69e 100644 --- a/src/activity_pub/inbox.rs +++ b/src/activity_pub/inbox.rs @@ -1,9 +1,15 @@ use diesel::PgConnection; +use diesel::associations::Identifiable; use serde_json; +use activity_pub::activity::Activity; +use activity_pub::actor::Actor; +use models::blogs::Blog; +use models::follows::{Follow, NewFollow}; use models::posts::{Post, NewPost}; +use models::users::User; -pub trait Inbox { +pub trait Inbox: Actor + Sized { fn received(&self, conn: &PgConnection, act: serde_json::Value); fn save(&self, conn: &PgConnection, act: serde_json::Value) { @@ -23,7 +29,30 @@ pub trait Inbox { x => println!("Received a new {}, but didn't saved it", x) } }, + "Follow" => { + let follow_id = act["object"].as_str().unwrap().to_string(); + let from = User::from_url(conn, act["actor"].as_str().unwrap().to_string()).unwrap(); + match User::from_url(conn, act["object"].as_str().unwrap().to_string()) { + Some(u) => self.accept_follow(conn, &from, &u, follow_id, from.id, u.id), + None => { + let blog = Blog::from_url(conn, follow_id.clone()).unwrap(); + self.accept_follow(conn, &from, &blog, follow_id, from.id, blog.id) + } + }; + + // TODO: notification + } x => println!("Received unknow activity type: {}", x) } } + + fn accept_follow(&self, conn: &PgConnection, from: &A, target: &B, follow_id: String, from_id: i32, target_id: i32) { + Follow::insert(conn, NewFollow { + follower_id: from_id, + following_id: target_id + }); + + let accept = Activity::accept(target, follow_id, conn); + from.send_to_inbox(conn, accept) + } } diff --git a/src/main.rs b/src/main.rs index 91e81730..db8cf237 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,7 @@ extern crate serde; extern crate serde_derive; #[macro_use] extern crate serde_json; +extern crate url; use diesel::pg::PgConnection; use diesel::r2d2::{ConnectionManager, Pool}; diff --git a/src/models/blogs.rs b/src/models/blogs.rs index f6a03128..06f57e21 100644 --- a/src/models/blogs.rs +++ b/src/models/blogs.rs @@ -18,7 +18,8 @@ pub struct Blog { pub outbox_url: String, pub inbox_url: String, pub instance_id: i32, - pub creation_date: NaiveDateTime + pub creation_date: NaiveDateTime, + pub ap_url: String } #[derive(Insertable)] @@ -29,7 +30,8 @@ pub struct NewBlog { pub summary: String, pub outbox_url: String, pub inbox_url: String, - pub instance_id: i32 + pub instance_id: i32, + pub ap_url: String } impl Blog { @@ -52,7 +54,7 @@ impl Blog { blogs::table.filter(blogs::actor_id.eq(username)) .limit(1) .load::(conn) - .expect("Error loading blog by email") + .expect("Error loading blog by actor_id") .into_iter().nth(0) } @@ -68,6 +70,12 @@ impl Blog { .set(blogs::inbox_url.eq(self.compute_inbox(conn))) .get_result::(conn).expect("Couldn't update inbox URL"); } + + if self.ap_url.len() == 0 { + diesel::update(self) + .set(blogs::ap_url.eq(self.compute_id(conn))) + .get_result::(conn).expect("Couldn't update AP URL"); + } } pub fn outbox(&self, conn: &PgConnection) -> Outbox { @@ -95,6 +103,14 @@ impl Actor for Blog { fn get_actor_type () -> ActorType { ActorType::Blog } + + fn from_url(conn: &PgConnection, url: String) -> Option { + blogs::table.filter(blogs::ap_url.eq(url)) + .limit(1) + .load::(conn) + .expect("Error loading blog from url") + .into_iter().nth(0) + } } impl Webfinger for Blog { @@ -137,7 +153,8 @@ impl NewBlog { summary: summary, outbox_url: String::from(""), inbox_url: String::from(""), - instance_id: instance_id + instance_id: instance_id, + ap_url: String::from("") } } } diff --git a/src/models/users.rs b/src/models/users.rs index 4e95644e..5a4d40a2 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -8,6 +8,7 @@ use reqwest::mime::Mime; use rocket::request::{self, FromRequest, Request}; use rocket::outcome::IntoOutcome; use serde_json; +use url::Url; use activity_pub::activity::Activity; use activity_pub::actor::{ActorType, Actor}; @@ -35,7 +36,8 @@ pub struct User { pub email: Option, pub hashed_password: Option, pub instance_id: i32, - pub creation_date: NaiveDateTime + pub creation_date: NaiveDateTime, + pub ap_url: String } #[derive(Insertable)] @@ -49,7 +51,8 @@ pub struct NewUser { pub summary: String, pub email: Option, pub hashed_password: Option, - pub instance_id: i32 + pub instance_id: i32, + pub ap_url: String } impl User { @@ -83,7 +86,7 @@ impl User { .filter(users::instance_id.eq(instance_id)) .limit(1) .load::(conn) - .expect("Error loading user by email") + .expect("Error loading user by name") .into_iter().nth(0) } @@ -109,19 +112,7 @@ impl User { fn fetch_from_webfinger(conn: &PgConnection, acct: String) -> Option { match resolve(acct.clone()) { - Ok(url) => { - let req = Client::new() - .get(&url[..]) - .header(Accept(vec![qitem("application/activity+json".parse::().unwrap())])) - .send(); - match req { - Ok(mut res) => { - let json: serde_json::Value = serde_json::from_str(&res.text().unwrap()).unwrap(); - Some(User::from_activity(conn, json, acct.split("@").last().unwrap().to_string())) - }, - Err(_) => None - } - }, + Ok(url) => User::fetch_from_url(conn, url), Err(details) => { println!("{}", details); None @@ -129,6 +120,20 @@ impl User { } } + fn fetch_from_url(conn: &PgConnection, url: String) -> Option { + let req = Client::new() + .get(&url[..]) + .header(Accept(vec![qitem("application/activity+json".parse::().unwrap())])) + .send(); + match req { + Ok(mut res) => { + let json: serde_json::Value = serde_json::from_str(&res.text().unwrap()).unwrap(); + Some(User::from_activity(conn, json, Url::parse(url.as_ref()).unwrap().host_str().unwrap().to_string())) + }, + Err(_) => None + } + } + fn from_activity(conn: &PgConnection, acct: serde_json::Value, inst: String) -> User { let instance = match Instance::get_by_domain(conn, inst.clone()) { Some(instance) => instance, @@ -145,7 +150,8 @@ impl User { summary: acct["summary"].as_str().unwrap().to_string(), email: None, hashed_password: None, - instance_id: instance.id + instance_id: instance.id, + ap_url: acct["id"].as_str().unwrap().to_string() }) } @@ -167,7 +173,13 @@ impl User { if self.inbox_url.len() == 0 { diesel::update(self) .set(users::inbox_url.eq(self.compute_inbox(conn))) - .get_result::(conn).expect("Couldn't update outbox URL"); + .get_result::(conn).expect("Couldn't update inbox URL"); + } + + if self.ap_url.len() == 0 { + diesel::update(self) + .set(users::ap_url.eq(self.compute_id(conn))) + .get_result::(conn).expect("Couldn't update AP URL"); } } @@ -225,6 +237,26 @@ impl Actor for User { fn get_actor_type() -> ActorType { ActorType::Person } + + fn from_url(conn: &PgConnection, url: String) -> Option { + let in_db = users::table.filter(users::ap_url.eq(url.clone())) + .limit(1) + .load::(conn) + .expect("Error loading user by AP url") + .into_iter().nth(0); + match in_db { + Some(u) => Some(u), + None => { + // The requested user was not in the DB + // We try to fetch it if it is remote + if Url::parse(url.as_ref()).unwrap().host_str().unwrap() != Instance::get_local(conn).unwrap().public_domain { + Some(User::fetch_from_url(conn, url).unwrap()) + } else { + None + } + } + } + } } impl Inbox for User { @@ -281,7 +313,8 @@ impl NewUser { summary: summary, email: Some(email), hashed_password: Some(password), - instance_id: instance_id + instance_id: instance_id, + ap_url: String::from("") } } } diff --git a/src/schema.rs b/src/schema.rs index 5b6b254d..155d97e9 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -17,6 +17,7 @@ table! { inbox_url -> Varchar, instance_id -> Int4, creation_date -> Timestamp, + ap_url -> Text, } } @@ -74,6 +75,7 @@ table! { hashed_password -> Nullable, instance_id -> Int4, creation_date -> Timestamp, + ap_url -> Text, } }