Accept follow requests

This commit is contained in:
Bat 2018-05-01 19:02:29 +01:00
parent 2f1f6a0295
commit 9a4f60cfe3
11 changed files with 150 additions and 38 deletions

1
Cargo.lock generated
View File

@ -816,6 +816,7 @@ dependencies = [
"serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)", "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_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)", "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]] [[package]]

View File

@ -5,7 +5,6 @@ version = "0.1.0"
[dependencies] [dependencies]
base64 = "0.9.1" base64 = "0.9.1"
bcrypt = "0.2" bcrypt = "0.2"
chrono = { version = "0.4", features = ["serde"] }
dotenv = "*" dotenv = "*"
heck = "0.3.0" heck = "0.3.0"
hex = "0.3" hex = "0.3"
@ -16,6 +15,11 @@ rocket_codegen = "*"
serde = "*" serde = "*"
serde_derive = "1.0" serde_derive = "1.0"
serde_json = "1.0" serde_json = "1.0"
url = "1.7"
[dependencies.chrono]
features = ["serde"]
version = "0.4"
[dependencies.diesel] [dependencies.diesel]
features = ["postgres", "r2d2", "chrono"] features = ["postgres", "r2d2", "chrono"]

View File

@ -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;

View File

@ -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 '';

View File

@ -7,35 +7,52 @@ use activity_pub::object::Object;
#[derive(Clone)] #[derive(Clone)]
pub enum Activity { pub enum Activity {
Create(CreatePayload) Create(Payload),
Accept(Payload)
} }
impl Activity { impl Activity {
pub fn serialize(&self) -> serde_json::Value { 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 { match self {
Activity::Create(data) => json!({ Activity::Accept(_) => String::from("Accept"),
"type": "Create", Activity::Create(_) => String::from("Create")
"actor": data.by, }
"object": data.object, }
"published": data.date.to_rfc3339()
}) pub fn payload(&self) -> Payload {
match self {
Activity::Accept(p) => p.clone(),
Activity::Create(p) => p.clone()
} }
} }
pub fn create<T: Object, U: Actor>(by: &U, obj: T, conn: &PgConnection) -> Activity { pub fn create<T: Object, U: Actor>(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<A: Actor>(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)] #[derive(Clone)]
pub struct CreatePayload { pub struct Payload {
by: serde_json::Value, by: serde_json::Value,
object: serde_json::Value, object: serde_json::Value,
date: chrono::DateTime<chrono::Utc> date: chrono::DateTime<chrono::Utc>
} }
impl CreatePayload { impl Payload {
pub fn new(by: serde_json::Value, obj: serde_json::Value) -> CreatePayload { pub fn new(by: serde_json::Value, obj: serde_json::Value) -> Payload {
CreatePayload { Payload {
by: by, by: by,
object: obj, object: obj,
date: chrono::Utc::now() date: chrono::Utc::now()

View File

@ -19,7 +19,7 @@ impl ToString for ActorType {
} }
} }
pub trait Actor { pub trait Actor: Sized {
fn get_box_prefix() -> &'static str; fn get_box_prefix() -> &'static str;
fn get_actor_id(&self) -> String; fn get_actor_id(&self) -> String;
@ -76,4 +76,6 @@ pub trait Actor {
Err(_) => println!("Error while sending to inbox") Err(_) => println!("Error while sending to inbox")
} }
} }
fn from_url(conn: &PgConnection, url: String) -> Option<Self>;
} }

View File

@ -1,9 +1,15 @@
use diesel::PgConnection; use diesel::PgConnection;
use diesel::associations::Identifiable;
use serde_json; 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::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 received(&self, conn: &PgConnection, act: serde_json::Value);
fn save(&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) 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) x => println!("Received unknow activity type: {}", x)
} }
} }
fn accept_follow<A: Actor, B: Actor>(&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)
}
} }

View File

@ -18,6 +18,7 @@ extern crate serde;
extern crate serde_derive; extern crate serde_derive;
#[macro_use] #[macro_use]
extern crate serde_json; extern crate serde_json;
extern crate url;
use diesel::pg::PgConnection; use diesel::pg::PgConnection;
use diesel::r2d2::{ConnectionManager, Pool}; use diesel::r2d2::{ConnectionManager, Pool};

View File

@ -18,7 +18,8 @@ pub struct Blog {
pub outbox_url: String, pub outbox_url: String,
pub inbox_url: String, pub inbox_url: String,
pub instance_id: i32, pub instance_id: i32,
pub creation_date: NaiveDateTime pub creation_date: NaiveDateTime,
pub ap_url: String
} }
#[derive(Insertable)] #[derive(Insertable)]
@ -29,7 +30,8 @@ pub struct NewBlog {
pub summary: String, pub summary: String,
pub outbox_url: String, pub outbox_url: String,
pub inbox_url: String, pub inbox_url: String,
pub instance_id: i32 pub instance_id: i32,
pub ap_url: String
} }
impl Blog { impl Blog {
@ -52,7 +54,7 @@ impl Blog {
blogs::table.filter(blogs::actor_id.eq(username)) blogs::table.filter(blogs::actor_id.eq(username))
.limit(1) .limit(1)
.load::<Blog>(conn) .load::<Blog>(conn)
.expect("Error loading blog by email") .expect("Error loading blog by actor_id")
.into_iter().nth(0) .into_iter().nth(0)
} }
@ -68,6 +70,12 @@ impl Blog {
.set(blogs::inbox_url.eq(self.compute_inbox(conn))) .set(blogs::inbox_url.eq(self.compute_inbox(conn)))
.get_result::<Blog>(conn).expect("Couldn't update inbox URL"); .get_result::<Blog>(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::<Blog>(conn).expect("Couldn't update AP URL");
}
} }
pub fn outbox(&self, conn: &PgConnection) -> Outbox { pub fn outbox(&self, conn: &PgConnection) -> Outbox {
@ -95,6 +103,14 @@ impl Actor for Blog {
fn get_actor_type () -> ActorType { fn get_actor_type () -> ActorType {
ActorType::Blog ActorType::Blog
} }
fn from_url(conn: &PgConnection, url: String) -> Option<Blog> {
blogs::table.filter(blogs::ap_url.eq(url))
.limit(1)
.load::<Blog>(conn)
.expect("Error loading blog from url")
.into_iter().nth(0)
}
} }
impl Webfinger for Blog { impl Webfinger for Blog {
@ -137,7 +153,8 @@ impl NewBlog {
summary: summary, summary: summary,
outbox_url: String::from(""), outbox_url: String::from(""),
inbox_url: String::from(""), inbox_url: String::from(""),
instance_id: instance_id instance_id: instance_id,
ap_url: String::from("")
} }
} }
} }

View File

@ -8,6 +8,7 @@ use reqwest::mime::Mime;
use rocket::request::{self, FromRequest, Request}; use rocket::request::{self, FromRequest, Request};
use rocket::outcome::IntoOutcome; use rocket::outcome::IntoOutcome;
use serde_json; use serde_json;
use url::Url;
use activity_pub::activity::Activity; use activity_pub::activity::Activity;
use activity_pub::actor::{ActorType, Actor}; use activity_pub::actor::{ActorType, Actor};
@ -35,7 +36,8 @@ pub struct User {
pub email: Option<String>, pub email: Option<String>,
pub hashed_password: Option<String>, pub hashed_password: Option<String>,
pub instance_id: i32, pub instance_id: i32,
pub creation_date: NaiveDateTime pub creation_date: NaiveDateTime,
pub ap_url: String
} }
#[derive(Insertable)] #[derive(Insertable)]
@ -49,7 +51,8 @@ pub struct NewUser {
pub summary: String, pub summary: String,
pub email: Option<String>, pub email: Option<String>,
pub hashed_password: Option<String>, pub hashed_password: Option<String>,
pub instance_id: i32 pub instance_id: i32,
pub ap_url: String
} }
impl User { impl User {
@ -83,7 +86,7 @@ impl User {
.filter(users::instance_id.eq(instance_id)) .filter(users::instance_id.eq(instance_id))
.limit(1) .limit(1)
.load::<User>(conn) .load::<User>(conn)
.expect("Error loading user by email") .expect("Error loading user by name")
.into_iter().nth(0) .into_iter().nth(0)
} }
@ -109,19 +112,7 @@ impl User {
fn fetch_from_webfinger(conn: &PgConnection, acct: String) -> Option<User> { fn fetch_from_webfinger(conn: &PgConnection, acct: String) -> Option<User> {
match resolve(acct.clone()) { match resolve(acct.clone()) {
Ok(url) => { Ok(url) => User::fetch_from_url(conn, url),
let req = Client::new()
.get(&url[..])
.header(Accept(vec![qitem("application/activity+json".parse::<Mime>().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
}
},
Err(details) => { Err(details) => {
println!("{}", details); println!("{}", details);
None None
@ -129,6 +120,20 @@ impl User {
} }
} }
fn fetch_from_url(conn: &PgConnection, url: String) -> Option<User> {
let req = Client::new()
.get(&url[..])
.header(Accept(vec![qitem("application/activity+json".parse::<Mime>().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 { fn from_activity(conn: &PgConnection, acct: serde_json::Value, inst: String) -> User {
let instance = match Instance::get_by_domain(conn, inst.clone()) { let instance = match Instance::get_by_domain(conn, inst.clone()) {
Some(instance) => instance, Some(instance) => instance,
@ -145,7 +150,8 @@ impl User {
summary: acct["summary"].as_str().unwrap().to_string(), summary: acct["summary"].as_str().unwrap().to_string(),
email: None, email: None,
hashed_password: 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 { if self.inbox_url.len() == 0 {
diesel::update(self) diesel::update(self)
.set(users::inbox_url.eq(self.compute_inbox(conn))) .set(users::inbox_url.eq(self.compute_inbox(conn)))
.get_result::<User>(conn).expect("Couldn't update outbox URL"); .get_result::<User>(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::<User>(conn).expect("Couldn't update AP URL");
} }
} }
@ -225,6 +237,26 @@ impl Actor for User {
fn get_actor_type() -> ActorType { fn get_actor_type() -> ActorType {
ActorType::Person ActorType::Person
} }
fn from_url(conn: &PgConnection, url: String) -> Option<User> {
let in_db = users::table.filter(users::ap_url.eq(url.clone()))
.limit(1)
.load::<User>(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 { impl Inbox for User {
@ -281,7 +313,8 @@ impl NewUser {
summary: summary, summary: summary,
email: Some(email), email: Some(email),
hashed_password: Some(password), hashed_password: Some(password),
instance_id: instance_id instance_id: instance_id,
ap_url: String::from("")
} }
} }
} }

View File

@ -17,6 +17,7 @@ table! {
inbox_url -> Varchar, inbox_url -> Varchar,
instance_id -> Int4, instance_id -> Int4,
creation_date -> Timestamp, creation_date -> Timestamp,
ap_url -> Text,
} }
} }
@ -74,6 +75,7 @@ table! {
hashed_password -> Nullable<Text>, hashed_password -> Nullable<Text>,
instance_id -> Int4, instance_id -> Int4,
creation_date -> Timestamp, creation_date -> Timestamp,
ap_url -> Text,
} }
} }