diff --git a/Cargo.lock b/Cargo.lock index 02988cf..50c8e56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -949,6 +949,8 @@ name = "plume" version = "0.1.0" dependencies = [ "activitypub 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)", "ammonia 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "array_tool 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 957e693..12a6825 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,8 @@ name = "plume" version = "0.1.0" [dependencies] activitypub = "0.1.1" +activitystreams-derive = "0.1.0" +activitystreams-traits = "0.1.0" ammonia = "1.1.0" array_tool = "1.0" base64 = "0.9" @@ -18,7 +20,6 @@ hex = "0.3" hyper = "*" lazy_static = "*" openssl = "0.10.6" -pulldown-cmark = { version = "0.1.2", default-features = false } reqwest = "0.8" rpassword = "2.0" serde = "*" @@ -36,6 +37,10 @@ version = "0.4" features = ["postgres", "r2d2", "chrono"] version = "*" +[dependencies.pulldown-cmark] +default-features = false +version = "0.1.2" + [dependencies.rocket] git = "https://github.com/SergioBenitez/Rocket" rev = "df7111143e466c18d1f56377a8d9530a5a306aba" diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 391f3a7..8b9773b 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -6,6 +6,10 @@ All commands are run in the Mac Terminal or terminal emulator of your choice, such as iTerm2. First, you will need [Git](https://git-scm.com/download/mac), [Homebrew](https://brew.sh/), [Rust](https://www.rust-lang.org/en-US/), and [Postgres](https://www.postgresql.org/). Follow the instructions to install Homebrew before continuing if you don't already have it. +### Linux + +Similar to Mac OSX all commands should be run from a terminal (a.k.a command line). First, you will need [Git](https://git-scm.com/download/mac), [Rust](https://www.rust-lang.org/en-US/), and [Postgres](https://www.postgresql.org/). Step-by-step instructions are also available here: [Installing Prerequisites](/doc/PREREQUISITES.md) + #### Download the Repository Navigate to the directory on your machine where you would like to install the repository, such as in `~/dev` by running `cd dev`. Now, clone the remote repository by running `git clone https://github.com/Plume-org/Plume.git`. This will install the codebase to the `Plume` subdirectory. Navigate into that directory by running `cd Plume`. diff --git a/doc/PREREQUISITES.md b/doc/PREREQUISITES.md new file mode 100644 index 0000000..e159e50 --- /dev/null +++ b/doc/PREREQUISITES.md @@ -0,0 +1,86 @@ +# Installing Software Prerequisites + +These instructions have been adapted from the Aardwolf documentation, and may not be accurate. +As such, this notification should be updated once verified for Plume installs. + +> NOTE: These instructions may help in installing a production version, but are +intended for developers to be able to build and test their changes. If in doubt, +seek out documentation from your distribution package or from [the `doc` folder](doc). + +## Installing Requirements + +### Installing PostgreSQL + +In order to run the Plume backend, you will need to have access to a +[PostgreSQL](https://www.postgresql.org/) database. There are a few options for doing this, but for +this guide we’re going to assume you are running the database on your +development machine. + +#### Linux/OSX Instructions + +If you're on an Ubuntu-like machine, you should be able to install +PostgreSQL like this: + + $ sudo apt-get update + $ sudo apt-get install postgresql postgresql-contrib + +If you see an error like: + + = note: /usr/bin/ld: cannot find -lpq + collect2: error: ld returned 1 exit statusb + +Then you may need to install the libpq (PostgreSQL C-library) package as well : + + $ sudo apt-get install libpq-dev + +If you're on OSX and using `brew`, do + + $ brew update + $ brew install postgres + +For Gentoo (eselect-postgresql is optional), + + # emerge --sync + # emerge -av postgresql eselect-postgresql + +For Fedora/CentOS/RHEL, do + + # dnf install postgresql-server postgresql-contrib + +#### Windows Instructions + +For Windows, just download the installer [here](https://www.enterprisedb.com/downloads/postgres-postgresql-downloads#windows) and run it. After installing, make sure to add the /lib directory to your PATH system variable. + +### Installing rustup + +> Note: Rustup managed installations do appear to co-exist with system + installations on Gentoo, and should work on most other distributions. + If not, please file an issue with the Rust and Rustup teams or your distribution’s + managers. + +Next, you’ll need to have the [Rust](https://rust-lang.org/) toolchain +installed. The best way to do this is to install +[rustup](https://rustup.rs), which is a Rust toolchain manager. + +#### Linux/OSX Instructions + +Open your terminal and run the following command: + + $ curl https://sh.rustup.rs -sSf | sh + +For those who are (understandably) uncomfortable with piping a shell +script from the internet directly into `sh`, you can also +[use an alternate installation method](https://github.com/rust-lang-nursery/rustup.rs/#other-installation-methods). + +#### Windows Instructions + +If you don't already have them, download and install the [Visual C++ 2015 Build Tools](http://landinghub.visualstudio.com/visual-cpp-build-tools). + +Then, download the [rustup installer](https://www.rust-lang.org/en-US/install.html) and run it. That's it! + +### Installing Rust Toolchain + +Once you have `rustup` installed, make sure you have the `nightly` rust +toolchain installed: + + $ rustup toolchain install nightly diff --git a/po/en.po b/po/en.po index 67cd876..80ceeab 100644 --- a/po/en.po +++ b/po/en.po @@ -283,3 +283,6 @@ msgstr "" msgid "{{ data }} mentioned you." msgstr "" + +msgid "Your comment" +msgstr "" diff --git a/po/fr.po b/po/fr.po index 79f89a3..a38c24e 100644 --- a/po/fr.po +++ b/po/fr.po @@ -282,3 +282,7 @@ msgstr "Vous n'êtes pas auteur dans ce blog." msgid "{{ data }} mentioned you." msgstr "" + +#, fuzzy +msgid "Your comment" +msgstr "Envoyer le commentaire" diff --git a/po/pl.po b/po/pl.po index 440fa54..5f0d98e 100644 --- a/po/pl.po +++ b/po/pl.po @@ -289,5 +289,9 @@ msgstr "" msgid "{{ data }} mentioned you." msgstr "{{ data }} skomentował Twój artykuł" +#, fuzzy +msgid "Your comment" +msgstr "Wyślij komentarz" + #~ msgid "Logowanie" #~ msgstr "Zaloguj się" diff --git a/po/plume.pot b/po/plume.pot index 683ee16..64032f4 100644 --- a/po/plume.pot +++ b/po/plume.pot @@ -278,3 +278,6 @@ msgstr "" msgid "{{ data }} mentioned you." msgstr "" + +msgid "Your comment" +msgstr "" diff --git a/src/activity_pub/actor.rs b/src/activity_pub/actor.rs deleted file mode 100644 index ab5f6c5..0000000 --- a/src/activity_pub/actor.rs +++ /dev/null @@ -1,86 +0,0 @@ -use diesel::PgConnection; -use serde_json; - -use BASE_URL; -use activity_pub::{activity_pub, ActivityPub, context, ap_url}; -use models::instance::Instance; - -pub enum ActorType { - Person, - Blog -} - -impl ToString for ActorType { - fn to_string(&self) -> String { - String::from(match self { - ActorType::Person => "Person", - ActorType::Blog => "Blog" - }) - } -} - -pub trait Actor: Sized { - fn get_box_prefix() -> &'static str; - - fn get_actor_id(&self) -> String; - - fn get_display_name(&self) -> String; - - fn get_summary(&self) -> String; - - fn get_instance(&self, conn: &PgConnection) -> Instance; - - fn get_actor_type() -> ActorType; - - fn get_inbox_url(&self) -> String; - - fn get_shared_inbox_url(&self) -> Option; - - fn custom_props(&self, _conn: &PgConnection) -> serde_json::Map { - serde_json::Map::new() - } - - fn as_activity_pub (&self, conn: &PgConnection) -> ActivityPub { - let mut repr = json!({ - "@context": context(), - "id": self.compute_id(conn), - "type": Self::get_actor_type().to_string(), - "inbox": self.compute_inbox(conn), - "outbox": self.compute_outbox(conn), - "preferredUsername": self.get_actor_id(), - "name": self.get_display_name(), - "summary": self.get_summary(), - "url": self.compute_id(conn), - "endpoints": { - "sharedInbox": ap_url(format!("{}/inbox", BASE_URL.as_str())) - } - }); - - self.custom_props(conn).iter().for_each(|p| repr[p.0] = p.1.clone()); - - activity_pub(repr) - } - - fn compute_outbox(&self, conn: &PgConnection) -> String { - self.compute_box(conn, "outbox") - } - - fn compute_inbox(&self, conn: &PgConnection) -> String { - self.compute_box(conn, "inbox") - } - - fn compute_box(&self, conn: &PgConnection, box_name: &str) -> String { - format!("{id}/{name}", id = self.compute_id(conn), name = box_name) - } - - fn compute_id(&self, conn: &PgConnection) -> String { - ap_url(format!( - "{instance}/{prefix}/{user}", - instance = self.get_instance(conn).public_domain, - prefix = Self::get_box_prefix(), - user = self.get_actor_id() - )) - } - - fn from_url(conn: &PgConnection, url: String) -> Option; -} diff --git a/src/activity_pub/inbox.rs b/src/activity_pub/inbox.rs index 0305567..012108e 100644 --- a/src/activity_pub/inbox.rs +++ b/src/activity_pub/inbox.rs @@ -50,15 +50,7 @@ pub trait Deletable { } pub trait Inbox { - fn received(&self, conn: &PgConnection, act: serde_json::Value); - - 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 save(&self, conn: &PgConnection, act: serde_json::Value) -> Result<(), Error> { + fn received(&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) => { diff --git a/src/activity_pub/mod.rs b/src/activity_pub/mod.rs index 4943f58..87fb579 100644 --- a/src/activity_pub/mod.rs +++ b/src/activity_pub/mod.rs @@ -1,24 +1,19 @@ use activitypub::{Activity, Actor, Object, Link}; use array_tool::vec::Uniq; -use diesel::PgConnection; use reqwest::Client; use rocket::{ - http::{ContentType, Status}, - response::{Response, Responder, Content}, + http::Status, + response::{Response, Responder}, request::Request }; -use rocket_contrib::Json; use serde_json; use self::sign::Signable; -pub mod actor; pub mod inbox; pub mod request; pub mod sign; -pub type ActivityPub = Content>; - pub const CONTEXT_URL: &'static str = "https://www.w3.org/ns/activitystreams"; pub const PUBLIC_VISIBILTY: &'static str = "https://www.w3.org/ns/activitystreams#Public"; @@ -56,10 +51,6 @@ pub fn context() -> serde_json::Value { ]) } -pub fn activity_pub(json: serde_json::Value) -> ActivityPub { - Content(ContentType::new("application", "activity+json"), Json(json)) -} - pub struct ActivityStream (T); impl ActivityStream { @@ -70,11 +61,15 @@ impl ActivityStream { impl<'r, O: Object> Responder<'r> for ActivityStream { fn respond_to(self, request: &Request) -> Result, Status> { - serde_json::to_string(&self.0).respond_to(request) + let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?; + json["@context"] = context(); + serde_json::to_string(&json).respond_to(request).map(|r| Response::build_from(r) + .raw_header("Content-Type", "application/activity+json") + .finalize()) } } -pub fn broadcast(conn: &PgConnection, sender: &S, act: A, to: Vec) { +pub fn broadcast(sender: &S, act: A, to: Vec) { let boxes = to.into_iter() .map(|u| u.get_shared_inbox_url().unwrap_or(u.get_inbox_url())) .collect::>() @@ -82,14 +77,14 @@ pub fn broadcast(conn let mut act = serde_json::to_value(act).unwrap(); act["@context"] = context(); - let signed = act.sign(sender, conn); + let signed = act.sign(sender); for inbox in boxes { // TODO: run it in Sidekiq or something like that let res = Client::new() .post(&inbox[..]) .headers(request::headers()) - .header(request::signature(sender, request::headers(), conn)) + .header(request::signature(sender, request::headers())) .header(request::digest(signed.to_string())) .body(signed.to_string()) .send(); @@ -120,3 +115,27 @@ pub trait IntoId { } impl Link for Id {} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)] +#[serde(rename_all = "camelCase")] +pub struct ApSignature { + #[serde(skip_serializing_if = "Option::is_none")] + #[activitystreams(concrete(PublicKey), functional)] + pub public_key: Option +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)] +#[serde(rename_all = "camelCase")] +pub struct PublicKey { + #[serde(skip_serializing_if = "Option::is_none")] + #[activitystreams(concrete(String), functional)] + pub id: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[activitystreams(concrete(String), functional)] + pub owner: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[activitystreams(concrete(String), functional)] + pub public_key_pem: Option +} diff --git a/src/activity_pub/request.rs b/src/activity_pub/request.rs index afde1b8..e0e2517 100644 --- a/src/activity_pub/request.rs +++ b/src/activity_pub/request.rs @@ -1,5 +1,4 @@ use base64; -use diesel::PgConnection; use openssl::hash::{Hasher, MessageDigest}; use reqwest::header::{Date, Headers, UserAgent}; use std::time::SystemTime; @@ -23,7 +22,7 @@ pub fn headers() -> Headers { headers } -pub fn signature(signer: &S, headers: Headers, conn: &PgConnection) -> Signature { +pub fn signature(signer: &S, headers: Headers) -> Signature { let signed_string = headers.iter().map(|h| format!("{}: {}", h.name().to_lowercase(), h.value_string())).collect::>().join("\n"); let signed_headers = headers.iter().map(|h| h.name().to_string()).collect::>().join(" ").to_lowercase(); @@ -32,7 +31,7 @@ pub fn signature(signer: &S, headers: Headers, conn: &PgConnection) - Signature(format!( "keyId=\"{key_id}\",algorithm=\"rsa-sha256\",headers=\"{signed_headers}\",signature=\"{signature}\"", - key_id = signer.get_key_id(conn), + key_id = signer.get_key_id(), signed_headers = signed_headers, signature = sign )) diff --git a/src/activity_pub/sign.rs b/src/activity_pub/sign.rs index cdc4c74..d71e6db 100644 --- a/src/activity_pub/sign.rs +++ b/src/activity_pub/sign.rs @@ -1,6 +1,5 @@ use base64; use chrono::Utc; -use diesel::PgConnection; use hex; use openssl::{ pkey::PKey, @@ -17,14 +16,14 @@ pub fn gen_keypair() -> (Vec, Vec) { } pub trait Signer { - fn get_key_id(&self, conn: &PgConnection) -> String; + fn get_key_id(&self) -> String; /// Sign some data with the signer keypair fn sign(&self, to_sign: String) -> Vec; } pub trait Signable { - fn sign(&mut self, creator: &T, conn: &PgConnection) -> &mut Self where T: Signer; + fn sign(&mut self, creator: &T) -> &mut Self where T: Signer; fn hash(data: String) -> String { let bytes = data.into_bytes(); @@ -33,11 +32,11 @@ pub trait Signable { } impl Signable for serde_json::Value { - fn sign(&mut self, creator: &T, conn: &PgConnection) -> &mut serde_json::Value { + fn sign(&mut self, creator: &T) -> &mut serde_json::Value { let creation_date = Utc::now().to_rfc3339(); let mut options = json!({ "type": "RsaSignature2017", - "creator": creator.get_key_id(conn), + "creator": creator.get_key_id(), "created": creation_date }); diff --git a/src/main.rs b/src/main.rs index 793376a..85b4584 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,9 @@ #![plugin(rocket_codegen)] extern crate activitypub; +#[macro_use] +extern crate activitystreams_derive; +extern crate activitystreams_traits; extern crate ammonia; extern crate array_tool; extern crate base64; @@ -68,9 +71,8 @@ fn main() { routes::blogs::new_auth, routes::blogs::create, - routes::comments::new, - routes::comments::new_auth, routes::comments::create, + routes::comments::create_response, routes::instance::index, routes::instance::shared_inbox, @@ -83,6 +85,7 @@ fn main() { routes::notifications::notifications_auth, routes::posts::details, + routes::posts::details_response, routes::posts::activity_details, routes::posts::new, routes::posts::new_auth, diff --git a/src/models/blogs.rs b/src/models/blogs.rs index dc779f6..fd5ec9b 100644 --- a/src/models/blogs.rs +++ b/src/models/blogs.rs @@ -1,4 +1,4 @@ -use activitypub::{Actor, Object, collection::OrderedCollection}; +use activitypub::{Actor, Object, CustomObject, actor::Group, collection::OrderedCollection}; use reqwest::{ Client, header::{Accept, qitem}, @@ -16,15 +16,16 @@ use openssl::{ }; use webfinger::*; +use BASE_URL; use activity_pub::{ - ActivityStream, Id, IntoId, - actor::{Actor as APActor, ActorType}, + ApSignature, ActivityStream, Id, IntoId, PublicKey, inbox::WithInbox, sign }; use models::instance::*; use schema::blogs; +pub type CustomGroup = CustomObject; #[derive(Queryable, Identifiable, Serialize, Deserialize, Clone)] pub struct Blog { @@ -55,9 +56,17 @@ pub struct NewBlog { pub public_key: String } +const BLOG_PREFIX: &'static str = "~"; + impl Blog { insert!(blogs, NewBlog); get!(blogs); + find_by!(blogs, find_by_ap_url, ap_url as String); + find_by!(blogs, find_by_name, actor_id as String, instance_id as i32); + + pub fn get_instance(&self, conn: &PgConnection) -> Instance { + Instance::get(conn, self.instance_id).expect("Couldn't find instance") + } pub fn find_for_author(conn: &PgConnection, author_id: i32) -> Vec { use schema::blog_authors; @@ -67,8 +76,6 @@ impl Blog { .expect("Couldn't load blogs ") } - find_by!(blogs, find_by_name, actor_id as String, instance_id as i32); - pub fn find_local(conn: &PgConnection, name: String) -> Option { Blog::find_by_name(conn, name, Instance::local_id(conn)) } @@ -106,14 +113,14 @@ impl Blog { .send(); match req { Ok(mut res) => { - let json: serde_json::Value = serde_json::from_str(&res.text().unwrap()).unwrap(); + let json = serde_json::from_str(&res.text().unwrap()).unwrap(); Some(Blog::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) -> Blog { + fn from_activity(conn: &PgConnection, acct: CustomGroup, inst: String) -> Blog { let instance = match Instance::find_by_domain(conn, inst.clone()) { Some(instance) => instance, None => { @@ -125,34 +132,55 @@ impl Blog { } }; Blog::insert(conn, NewBlog { - actor_id: acct["preferredUsername"].as_str().unwrap().to_string(), - title: acct["name"].as_str().unwrap().to_string(), - outbox_url: acct["outbox"].as_str().unwrap().to_string(), - inbox_url: acct["inbox"].as_str().unwrap().to_string(), - summary: acct["summary"].as_str().unwrap().to_string(), + actor_id: acct.object.ap_actor_props.preferred_username_string().expect("Blog::from_activity: preferredUsername error"), + title: acct.object.object_props.name_string().expect("Blog::from_activity: name error"), + outbox_url: acct.object.ap_actor_props.outbox_string().expect("Blog::from_activity: outbox error"), + inbox_url: acct.object.ap_actor_props.inbox_string().expect("Blog::from_activity: inbox error"), + summary: acct.object.object_props.summary_string().expect("Blog::from_activity: summary error"), instance_id: instance.id, - ap_url: acct["id"].as_str().unwrap().to_string(), - public_key: acct["publicKey"]["publicKeyPem"].as_str().unwrap_or("").to_string(), + ap_url: acct.object.object_props.id_string().expect("Blog::from_activity: id error"), + public_key: acct.custom_props.public_key_publickey().expect("Blog::from_activity: publicKey error") + .public_key_pem_string().expect("Blog::from_activity: publicKey.publicKeyPem error"), private_key: None }) } + pub fn into_activity(&self, _conn: &PgConnection) -> CustomGroup { + let mut blog = Group::default(); + blog.ap_actor_props.set_preferred_username_string(self.actor_id.clone()).expect("Blog::into_activity: preferredUsername error"); + blog.object_props.set_name_string(self.title.clone()).expect("Blog::into_activity: name error"); + blog.ap_actor_props.set_outbox_string(self.outbox_url.clone()).expect("Blog::into_activity: outbox error"); + blog.ap_actor_props.set_inbox_string(self.inbox_url.clone()).expect("Blog::into_activity: inbox error"); + blog.object_props.set_summary_string(self.summary.clone()).expect("Blog::into_activity: summary error"); + blog.object_props.set_id_string(self.ap_url.clone()).expect("Blog::into_activity: id error"); + + let mut public_key = PublicKey::default(); + public_key.set_id_string(format!("{}#main-key", self.ap_url)).expect("Blog::into_activity: publicKey.id error"); + public_key.set_owner_string(self.ap_url.clone()).expect("Blog::into_activity: publicKey.owner error"); + public_key.set_public_key_pem_string(self.public_key.clone()).expect("Blog::into_activity: publicKey.publicKeyPem error"); + let mut ap_signature = ApSignature::default(); + ap_signature.set_public_key_publickey(public_key).expect("Blog::into_activity: publicKey error"); + + CustomGroup::new(blog, ap_signature) + } + pub fn update_boxes(&self, conn: &PgConnection) { + let instance = self.get_instance(conn); if self.outbox_url.len() == 0 { diesel::update(self) - .set(blogs::outbox_url.eq(self.compute_outbox(conn))) + .set(blogs::outbox_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), "outbox"))) .get_result::(conn).expect("Couldn't update outbox URL"); } if self.inbox_url.len() == 0 { diesel::update(self) - .set(blogs::inbox_url.eq(self.compute_inbox(conn))) + .set(blogs::inbox_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), "inbox"))) .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))) + .set(blogs::ap_url.eq(instance.compute_box(BLOG_PREFIX, self.actor_id.clone(), ""))) .get_result::(conn).expect("Couldn't update AP URL"); } } @@ -175,26 +203,38 @@ impl Blog { pub fn webfinger(&self, conn: &PgConnection) -> Webfinger { Webfinger { subject: format!("acct:{}@{}", self.actor_id, self.get_instance(conn).public_domain), - aliases: vec![self.compute_id(conn)], + aliases: vec![self.ap_url.clone()], links: vec![ Link { rel: String::from("http://webfinger.net/rel/profile-page"), mime_type: None, - href: self.compute_id(conn) + href: self.ap_url.clone() }, Link { rel: String::from("http://schemas.google.com/g/2010#updates-from"), mime_type: Some(String::from("application/atom+xml")), - href: self.compute_box(conn, "feed.atom") + href: self.get_instance(conn).compute_box(BLOG_PREFIX, self.actor_id.clone(), "feed.atom") }, Link { rel: String::from("self"), mime_type: Some(String::from("application/activity+json")), - href: self.compute_id(conn) + href: self.ap_url.clone() } ] } } + + pub fn from_url(conn: &PgConnection, url: String) -> Option { + Blog::find_by_ap_url(conn, url.clone()).or_else(|| { + // 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() != BASE_URL.as_str() { + Some(Blog::fetch_from_url(conn, url).unwrap()) + } else { + None + } + }) + } } impl IntoId for Blog { @@ -216,51 +256,9 @@ impl WithInbox for Blog { } } -impl APActor for Blog { - fn get_box_prefix() -> &'static str { - "~" - } - - fn get_actor_id(&self) -> String { - self.actor_id.to_string() - } - - fn get_display_name(&self) -> String { - self.title.clone() - } - - fn get_summary(&self) -> String { - self.summary.clone() - } - - fn get_instance(&self, conn: &PgConnection) -> Instance { - Instance::get(conn, self.instance_id).unwrap() - } - - fn get_actor_type () -> ActorType { - ActorType::Blog - } - - fn get_inbox_url(&self) -> String { - self.inbox_url.clone() - } - - fn get_shared_inbox_url(&self) -> Option { - None - } - - 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 sign::Signer for Blog { - fn get_key_id(&self, conn: &PgConnection) -> String { - format!("{}#main-key", self.compute_id(conn)) + fn get_key_id(&self) -> String { + format!("{}#main-key", self.ap_url) } fn sign(&self, to_sign: String) -> Vec { diff --git a/src/models/comments.rs b/src/models/comments.rs index f943ddb..a6578f2 100644 --- a/src/models/comments.rs +++ b/src/models/comments.rs @@ -1,7 +1,7 @@ use activitypub::{ activity::Create, link, - object::{Note, properties::ObjectProperties} + object::{Note} }; use chrono; use diesel::{self, PgConnection, RunQueryDsl, QueryDsl, ExpressionMethods, dsl::any}; @@ -9,10 +9,10 @@ use serde_json; use activity_pub::{ ap_url, Id, IntoId, PUBLIC_VISIBILTY, - actor::Actor, inbox::{FromActivity, Notify} }; use models::{ + get_next_id, instance::Instance, mentions::Mention, notifications::*, @@ -21,6 +21,7 @@ use models::{ }; use schema::comments; use safe_string::SafeString; +use utils; #[derive(Queryable, Identifiable, Serialize, Clone)] pub struct Comment { @@ -35,7 +36,7 @@ pub struct Comment { pub spoiler_text: String } -#[derive(Insertable)] +#[derive(Insertable, Default)] #[table_name = "comments"] pub struct NewComment { pub content: SafeString, @@ -50,7 +51,7 @@ pub struct NewComment { impl Comment { insert!(comments, NewComment); get!(comments); - find_by!(comments, find_by_post, post_id as i32); + list_by!(comments, list_by_post, post_id as i32); find_by!(comments, find_by_ap_url, ap_url as String); pub fn get_author(&self, conn: &PgConnection) -> User { @@ -61,37 +62,6 @@ impl Comment { 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.create_props.set_actor_link(self.get_author(conn).into_id()).unwrap(); - act.create_props.set_object_object(self.into_activity(conn)).unwrap(); - act.object_props.set_id_string(format!("{}/activity", self.ap_url.clone().unwrap())).unwrap(); - act - } - pub fn count_local(conn: &PgConnection) -> usize { use schema::users; let local_authors = users::table.filter(users::instance_id.eq(Instance::local_id(conn))).select(users::id); @@ -104,11 +74,16 @@ impl Comment { pub fn to_json(&self, conn: &PgConnection) -> serde_json::Value { let mut json = serde_json::to_value(self).unwrap(); json["author"] = self.get_author(conn).to_json(conn); + let mentions = Mention::list_for_comment(conn, self.id).into_iter() + .map(|m| m.get_mentioned(conn).map(|u| u.get_fqn(conn)).unwrap_or(String::new())) + .collect::>(); + println!("{:?}", mentions); + json["mentions"] = serde_json::to_value(mentions).unwrap(); json } pub fn compute_id(&self, conn: &PgConnection) -> String { - ap_url(format!("{}#comment-{}", self.get_post(conn).compute_id(conn), self.id)) + ap_url(format!("{}#comment-{}", self.get_post(conn).ap_url, self.id)) } } @@ -117,15 +92,6 @@ impl FromActivity for 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()); - // save mentions - if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() { - for tag in tags.into_iter() { - serde_json::from_value::(tag) - .map(|m| Mention::from_activity(conn, m, Id::new(note.clone().object_props.clone().url_string().unwrap_or(String::from(""))))) - .ok(); - } - } - let comm = Comment::insert(conn, NewComment { content: SafeString::new(¬e.object_props.content_string().unwrap()), spoiler_text: note.object_props.summary_string().unwrap_or(String::from("")), @@ -137,6 +103,16 @@ impl FromActivity for Comment { author_id: User::from_url(conn, actor.clone().into()).unwrap().id, sensitive: false // "sensitive" is not a standard property, we need to think about how to support it with the activitypub crate }); + + // save mentions + if let Some(serde_json::Value::Array(tags)) = note.object_props.tag.clone() { + for tag in tags.into_iter() { + serde_json::from_value::(tag) + .map(|m| Mention::from_activity(conn, m, comm.id, false)) + .ok(); + } + } + comm.notify(conn); comm } @@ -155,3 +131,70 @@ impl Notify for Comment { } } } + +impl NewComment { + pub fn build() -> Self { + NewComment::default() + } + + pub fn content>(mut self, val: T) -> Self { + self.content = SafeString::new(val.as_ref()); + self + } + + pub fn in_response_to_id(mut self, val: Option) -> Self { + self.in_response_to_id = val; + self + } + + pub fn post(mut self, post: Post) -> Self { + self.post_id = post.id; + self + } + + pub fn author(mut self, author: User) -> Self { + self.author_id = author.id; + self + } + + pub fn create(mut self, conn: &PgConnection) -> (Create, i32) { + let post = Post::get(conn, self.post_id).unwrap(); + // We have to manually compute it since the new comment haven't been inserted yet, and it needs the activity we are building to be created + let next_id = get_next_id(conn, "comments_id_seq"); + self.ap_url = Some(format!("{}#comment-{}", post.ap_url, next_id)); + self.sensitive = false; + self.spoiler_text = String::new(); + + let (html, mentions) = utils::md_to_html(self.content.get().as_ref()); + + let author = User::get(conn, self.author_id).unwrap(); + let mut note = Note::default(); + let mut to = author.get_followers(conn).into_iter().map(User::into_id).collect::>(); + to.append(&mut post + .get_authors(conn) + .into_iter() + .flat_map(|a| a.get_followers(conn)) + .map(User::into_id) + .collect::>()); + to.push(Id::new(PUBLIC_VISIBILTY.to_string())); + + note.object_props.set_id_string(self.ap_url.clone().unwrap_or(String::new())).expect("NewComment::create: note.id error"); + note.object_props.set_summary_string(self.spoiler_text.clone()).expect("NewComment::create: note.summary error"); + note.object_props.set_content_string(html).expect("NewComment::create: note.content error"); + note.object_props.set_in_reply_to_link(Id::new(self.in_response_to_id.map_or_else(|| Post::get(conn, self.post_id).unwrap().ap_url, |id| { + let comm = Comment::get(conn, id).unwrap(); + comm.ap_url.clone().unwrap_or(comm.compute_id(conn)) + }))).expect("NewComment::create: note.in_reply_to error"); + note.object_props.set_published_string(chrono::Utc::now().to_rfc3339()).expect("NewComment::create: note.published error"); + note.object_props.set_attributed_to_link(author.clone().into_id()).expect("NewComment::create: note.attributed_to error"); + note.object_props.set_to_link_vec(to).expect("NewComment::create: note.to error"); + note.object_props.set_tag_link_vec(mentions.into_iter().map(|m| Mention::build_activity(conn, m)).collect::>()) + .expect("NewComment::create: note.tag error"); + + let mut act = Create::default(); + act.create_props.set_actor_link(author.into_id()).expect("NewComment::create: actor error"); + act.create_props.set_object_object(note).expect("NewComment::create: object error"); + act.object_props.set_id_string(format!("{}/activity", self.ap_url.clone().unwrap())).expect("NewComment::create: id error"); + (act, next_id) + } +} diff --git a/src/models/follows.rs b/src/models/follows.rs index 3518816..c7bffb5 100644 --- a/src/models/follows.rs +++ b/src/models/follows.rs @@ -1,7 +1,7 @@ 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, Notify, WithInbox}, sign::Signer}; +use activity_pub::{broadcast, Id, IntoId, inbox::{FromActivity, Notify, WithInbox}, sign::Signer}; use models::{ blogs::Blog, notifications::*, @@ -44,7 +44,7 @@ impl Follow { 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()]); + broadcast(&*from, accept, vec![target.clone()]); res } } diff --git a/src/models/instance.rs b/src/models/instance.rs index b7128da..b93a92a 100644 --- a/src/models/instance.rs +++ b/src/models/instance.rs @@ -1,9 +1,8 @@ use chrono::NaiveDateTime; use diesel::{self, QueryDsl, RunQueryDsl, ExpressionMethods, PgConnection}; -use serde_json; use std::iter::Iterator; -use activity_pub::inbox::Inbox; +use activity_pub::{ap_url, inbox::Inbox}; use models::users::User; use schema::{instances, users}; @@ -59,12 +58,16 @@ impl Instance { .expect("Couldn't load admins") .len() > 0 } -} -impl Inbox for Instance { - fn received(&self, conn: &PgConnection, act: serde_json::Value) { - self.save(conn, act.clone()).expect("Shared Inbox: Couldn't save activity"); - - // TODO: add to stream, or whatever needs to be done + pub fn compute_box(&self, prefix: &'static str, name: String, box_name: &'static str) -> String { + ap_url(format!( + "{instance}/{prefix}/{name}/{box_name}", + instance = self.public_domain, + prefix = prefix, + name = name, + box_name = box_name + )) } } + +impl Inbox for Instance {} diff --git a/src/models/likes.rs b/src/models/likes.rs index 38a4a19..f7c6fb9 100644 --- a/src/models/likes.rs +++ b/src/models/likes.rs @@ -5,7 +5,6 @@ use diesel::{self, PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods}; use activity_pub::{ Id, IntoId, - actor::Actor, inbox::{FromActivity, Deletable, Notify} }; use models::{ @@ -70,8 +69,8 @@ impl Like { pub fn compute_id(&self, conn: &PgConnection) -> String { format!( "{}/like/{}", - User::get(conn, self.user_id).unwrap().compute_id(conn), - Post::get(conn, self.post_id).unwrap().compute_id(conn) + User::get(conn, self.user_id).unwrap().ap_url, + Post::get(conn, self.post_id).unwrap().ap_url ) } } diff --git a/src/models/mentions.rs b/src/models/mentions.rs index fe4fe90..45b8abb 100644 --- a/src/models/mentions.rs +++ b/src/models/mentions.rs @@ -1,7 +1,7 @@ use activitypub::link; use diesel::{self, PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods}; -use activity_pub::{Id, inbox::Notify}; +use activity_pub::inbox::Notify; use models::{ comments::Comment, notifications::*, @@ -10,13 +10,13 @@ use models::{ }; use schema::mentions; -#[derive(Queryable, Identifiable)] +#[derive(Queryable, Identifiable, Serialize, Deserialize)] pub struct Mention { pub id: i32, pub mentioned_id: i32, pub post_id: Option, pub comment_id: Option, - pub ap_url: String + pub ap_url: String // TODO: remove, since mentions don't have an AP URL actually, this field was added by mistake } #[derive(Insertable)] @@ -34,6 +34,7 @@ impl Mention { find_by!(mentions, find_by_ap_url, ap_url as String); list_by!(mentions, list_for_user, mentioned_id as i32); list_by!(mentions, list_for_post, post_id as i32); + list_by!(mentions, list_for_comment, comment_id as i32); pub fn get_mentioned(&self, conn: &PgConnection) -> Option { User::get(conn, self.mentioned_id) @@ -44,12 +45,11 @@ impl Mention { } pub fn get_comment(&self, conn: &PgConnection) -> Option { - self.post_id.and_then(|id| Comment::get(conn, id)) + self.comment_id.and_then(|id| Comment::get(conn, id)) } pub fn build_activity(conn: &PgConnection, ment: String) -> link::Mention { let user = User::find_by_fqn(conn, ment.clone()); - println!("building act : {} -> {:?}", ment, user); let mut mention = link::Mention::default(); mention.link_props.set_href_string(user.clone().map(|u| u.ap_url).unwrap_or(String::new())).expect("Error setting mention's href"); mention.link_props.set_name_string(format!("@{}", ment)).expect("Error setting mention's name"); @@ -64,21 +64,23 @@ impl Mention { mention } - pub fn from_activity(conn: &PgConnection, ment: link::Mention, inside: Id) -> Option { + pub fn from_activity(conn: &PgConnection, ment: link::Mention, inside: i32, in_post: bool) -> Option { let ap_url = ment.link_props.href_string().unwrap(); let mentioned = User::find_by_ap_url(conn, ap_url).unwrap(); - if let Some(post) = Post::find_by_ap_url(conn, inside.clone().into()) { - let res = Mention::insert(conn, NewMention { - mentioned_id: mentioned.id, - post_id: Some(post.id), - comment_id: None, - ap_url: ment.link_props.href_string().unwrap_or(String::new()) - }); - res.notify(conn); - Some(res) + if in_post { + Post::get(conn, inside.clone().into()).map(|post| { + let res = Mention::insert(conn, NewMention { + mentioned_id: mentioned.id, + post_id: Some(post.id), + comment_id: None, + ap_url: ment.link_props.href_string().unwrap_or(String::new()) + }); + res.notify(conn); + res + }) } else { - if let Some(comment) = Comment::find_by_ap_url(conn, inside.into()) { + Comment::get(conn, inside.into()).map(|comment| { let res = Mention::insert(conn, NewMention { mentioned_id: mentioned.id, post_id: None, @@ -86,10 +88,8 @@ impl Mention { ap_url: ment.link_props.href_string().unwrap_or(String::new()) }); res.notify(conn); - Some(res) - } else { - None - } + res + }) } } } @@ -98,7 +98,7 @@ impl Notify for Mention { fn notify(&self, conn: &PgConnection) { let author = self.get_comment(conn) .map(|c| c.get_author(conn).display_name.clone()) - .unwrap_or(self.get_post(conn).unwrap().get_authors(conn)[0].display_name.clone()); + .unwrap_or_else(|| self.get_post(conn).unwrap().get_authors(conn)[0].display_name.clone()); self.get_mentioned(conn).map(|m| { Notification::insert(conn, NewNotification { diff --git a/src/models/mod.rs b/src/models/mod.rs index fa4a4c8..170e29a 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,3 +1,5 @@ +use diesel::{PgConnection, RunQueryDsl, select}; + macro_rules! find_by { ($table:ident, $fn:ident, $($col:ident as $type:ident),+) => { /// Try to find a $table with a given $col @@ -47,6 +49,16 @@ macro_rules! insert { }; } +sql_function!(nextval, nextval_t, (seq: ::diesel::sql_types::Text) -> ::diesel::sql_types::BigInt); +sql_function!(setval, setval_t, (seq: ::diesel::sql_types::Text, val: ::diesel::sql_types::BigInt) -> ::diesel::sql_types::BigInt); + +fn get_next_id(conn: &PgConnection, seq: &str) -> i32 { + // We cant' use currval because it may fail if nextval have never been called before + let next = select(nextval(seq)).get_result::(conn).expect("Next ID fail"); + select(setval(seq, next - 1)).get_result::(conn).expect("Reset ID fail"); + next as i32 +} + pub mod blog_authors; pub mod blogs; pub mod comments; diff --git a/src/models/posts.rs b/src/models/posts.rs index 368d283..9907ac5 100644 --- a/src/models/posts.rs +++ b/src/models/posts.rs @@ -155,7 +155,7 @@ impl Post { 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(mentions).unwrap()), - url: Some(serde_json::to_value(self.compute_id(conn)).unwrap()), + url: Some(serde_json::to_value(self.ap_url.clone()).unwrap()), to: Some(serde_json::to_value(to).unwrap()), cc: Some(serde_json::to_value(Vec::::new()).unwrap()), ..ObjectProperties::default() @@ -187,16 +187,7 @@ impl Post { impl FromActivity
for Post { fn from_activity(conn: &PgConnection, article: Article, _actor: Id) -> Post { - // save mentions - if let Some(serde_json::Value::Array(tags)) = article.object_props.tag.clone() { - for tag in tags.into_iter() { - serde_json::from_value::(tag) - .map(|m| Mention::from_activity(conn, m, Id::new(article.clone().object_props.clone().url_string().unwrap_or(String::from(""))))) - .ok(); - } - } - - Post::insert(conn, NewPost { + let post = Post::insert(conn, NewPost { blog_id: 0, // TODO slug: String::from(""), // TODO title: article.object_props.name_string().unwrap(), @@ -204,7 +195,17 @@ impl FromActivity
for Post { published: true, license: String::from("CC-0"), ap_url: article.object_props.url_string().unwrap_or(String::from("")) - }) + }); + + // save mentions + if let Some(serde_json::Value::Array(tags)) = article.object_props.tag.clone() { + for tag in tags.into_iter() { + serde_json::from_value::(tag) + .map(|m| Mention::from_activity(conn, m, post.id, true)) + .ok(); + } + } + post } } diff --git a/src/models/reshares.rs b/src/models/reshares.rs index 40a210d..69e4191 100644 --- a/src/models/reshares.rs +++ b/src/models/reshares.rs @@ -2,7 +2,7 @@ use activitypub::activity::{Announce, Undo}; use chrono::NaiveDateTime; use diesel::{self, PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods}; -use activity_pub::{Id, IntoId, actor::Actor, inbox::{FromActivity, Notify, Deletable}}; +use activity_pub::{Id, IntoId, inbox::{FromActivity, Notify, Deletable}}; use models::{notifications::*, posts::Post, users::User}; use schema::reshares; @@ -34,8 +34,8 @@ impl Reshare { diesel::update(self) .set(reshares::ap_url.eq(format!( "{}/reshare/{}", - User::get(conn, self.user_id).unwrap().compute_id(conn), - Post::get(conn, self.post_id).unwrap().compute_id(conn) + User::get(conn, self.user_id).unwrap().ap_url, + Post::get(conn, self.post_id).unwrap().ap_url ))) .get_result::(conn).expect("Couldn't update AP URL"); } diff --git a/src/models/users.rs b/src/models/users.rs index ee7639c..9a3f776 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -1,8 +1,7 @@ use activitypub::{ - Actor, Object, - actor::{Person, properties::ApActorProperties}, - collection::OrderedCollection, - object::properties::ObjectProperties + Actor, Object, Endpoint, CustomObject, + actor::Person, + collection::OrderedCollection }; use bcrypt; use chrono::NaiveDateTime; @@ -28,8 +27,7 @@ use webfinger::*; use BASE_URL; use activity_pub::{ - ap_url, ActivityStream, Id, IntoId, - actor::{ActorType, Actor as APActor}, + ap_url, ActivityStream, Id, IntoId, ApSignature, PublicKey, inbox::{Inbox, WithInbox}, sign::{Signer, gen_keypair} }; @@ -47,6 +45,8 @@ use safe_string::SafeString; pub const AUTH_COOKIE: &'static str = "user_id"; +pub type CustomPerson = CustomObject; + #[derive(Queryable, Identifiable, Serialize, Deserialize, Clone, Debug)] pub struct User { pub id: i32, @@ -84,6 +84,8 @@ pub struct NewUser { pub shared_inbox_url: Option } +const USER_PREFIX: &'static str = "@"; + impl User { insert!(users, NewUser); get!(users); @@ -91,6 +93,10 @@ impl User { find_by!(users, find_by_name, username as String, instance_id as i32); find_by!(users, find_by_ap_url, ap_url as String); + pub fn get_instance(&self, conn: &PgConnection) -> Instance { + Instance::get(conn, self.instance_id).expect("Couldn't find instance") + } + pub fn grant_admin_rights(&self, conn: &PgConnection) { diesel::update(self) .set(users::is_admin.eq(true)) @@ -153,14 +159,14 @@ impl User { .send(); match req { Ok(mut res) => { - let json: serde_json::Value = serde_json::from_str(&res.text().unwrap()).unwrap(); + let json: CustomPerson = 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: CustomPerson, inst: String) -> User { let instance = match Instance::find_by_domain(conn, inst.clone()) { Some(instance) => instance, None => { @@ -172,19 +178,21 @@ impl User { } }; User::insert(conn, NewUser { - username: acct["preferredUsername"].as_str().unwrap().to_string(), - display_name: acct["name"].as_str().unwrap().to_string(), - outbox_url: acct["outbox"].as_str().unwrap().to_string(), - inbox_url: acct["inbox"].as_str().unwrap().to_string(), + username: acct.object.ap_actor_props.preferred_username_string().expect("User::from_activity: preferredUsername error"), + display_name: acct.object.object_props.name_string().expect("User::from_activity: name error"), + outbox_url: acct.object.ap_actor_props.outbox_string().expect("User::from_activity: outbox error"), + inbox_url: acct.object.ap_actor_props.inbox_string().expect("User::from_activity: inbox error"), is_admin: false, - summary: SafeString::new(&acct["summary"].as_str().unwrap().to_string()), + summary: SafeString::new(&acct.object.object_props.summary_string().expect("User::from_activity: summary error")), email: None, hashed_password: None, instance_id: instance.id, - ap_url: acct["id"].as_str().unwrap().to_string(), - public_key: acct["publicKey"]["publicKeyPem"].as_str().unwrap().to_string(), + ap_url: acct.object.object_props.id_string().expect("User::from_activity: id error"), + public_key: acct.custom_props.public_key_publickey().expect("User::from_activity: publicKey error") + .public_key_pem_string().expect("User::from_activity: publicKey.publicKeyPem error"), private_key: None, - shared_inbox_url: acct["endpoints"]["sharedInbox"].as_str().map(|s| s.to_string()) + shared_inbox_url: acct.object.ap_actor_props.endpoints_endpoint() + .and_then(|e| e.shared_inbox_string()).ok() }) } @@ -197,21 +205,22 @@ impl User { } pub fn update_boxes(&self, conn: &PgConnection) { + let instance = self.get_instance(conn); if self.outbox_url.len() == 0 { diesel::update(self) - .set(users::outbox_url.eq(self.compute_outbox(conn))) - .get_result::(conn).expect("Couldn't update outbox URL"); + .set(users::outbox_url.eq(instance.compute_box(USER_PREFIX, self.username.clone(), "outbox"))) + .get_result::(conn).expect("Couldn't update outbox URL"); } if self.inbox_url.len() == 0 { diesel::update(self) - .set(users::inbox_url.eq(self.compute_inbox(conn))) + .set(users::inbox_url.eq(instance.compute_box(USER_PREFIX, self.username.clone(), "inbox"))) .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))) + .set(users::ap_url.eq(instance.compute_box(USER_PREFIX, self.username.clone(), ""))) .get_result::(conn).expect("Couldn't update AP URL"); } @@ -306,28 +315,28 @@ impl User { PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.clone().unwrap().as_ref()).unwrap()).unwrap() } - pub fn into_activity(&self, conn: &PgConnection) -> Person { + pub fn into_activity(&self, _conn: &PgConnection) -> CustomPerson { 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() - }; - actor.ap_actor_props = ApActorProperties { - inbox: serde_json::to_value(self.compute_inbox(conn)).unwrap(), - outbox: 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())) - })), - followers: None, - following: None, - liked: None, - streams: None - }; - actor + actor.object_props.set_id_string(self.ap_url.clone()).expect("User::into_activity: id error"); + actor.object_props.set_name_string(self.display_name.clone()).expect("User::into_activity: name error"); + actor.object_props.set_summary_string(self.summary.get().clone()).expect("User::into_activity: summary error"); + actor.object_props.set_url_string(self.ap_url.clone()).expect("User::into_activity: url error"); + actor.ap_actor_props.set_inbox_string(self.inbox_url.clone()).expect("User::into_activity: inbox error"); + actor.ap_actor_props.set_outbox_string(self.outbox_url.clone()).expect("User::into_activity: outbox error"); + actor.ap_actor_props.set_preferred_username_string(self.username.clone()).expect("User::into_activity: preferredUsername error"); + + let mut endpoints = Endpoint::default(); + endpoints.set_shared_inbox_string(ap_url(format!("{}/inbox/", BASE_URL.as_str()))).expect("User::into_activity: endpoints.sharedInbox error"); + actor.ap_actor_props.set_endpoints_endpoint(endpoints).expect("User::into_activity: endpoints error"); + + let mut public_key = PublicKey::default(); + public_key.set_id_string(format!("{}#main-key", self.ap_url)).expect("Blog::into_activity: publicKey.id error"); + public_key.set_owner_string(self.ap_url.clone()).expect("Blog::into_activity: publicKey.owner error"); + public_key.set_public_key_pem_string(self.public_key.clone()).expect("Blog::into_activity: publicKey.publicKeyPem error"); + let mut ap_signature = ApSignature::default(); + ap_signature.set_public_key_publickey(public_key).expect("Blog::into_activity: publicKey error"); + + CustomPerson::new(actor, ap_signature) } pub fn to_json(&self, conn: &PgConnection) -> serde_json::Value { @@ -339,26 +348,38 @@ impl User { pub fn webfinger(&self, conn: &PgConnection) -> Webfinger { Webfinger { subject: format!("acct:{}@{}", self.username, self.get_instance(conn).public_domain), - aliases: vec![self.compute_id(conn)], + aliases: vec![self.ap_url.clone()], links: vec![ Link { rel: String::from("http://webfinger.net/rel/profile-page"), mime_type: None, - href: self.compute_id(conn) + href: self.ap_url.clone() }, Link { rel: String::from("http://schemas.google.com/g/2010#updates-from"), mime_type: Some(String::from("application/atom+xml")), - href: self.compute_box(conn, "feed.atom") + href: self.get_instance(conn).compute_box(USER_PREFIX, self.username.clone(), "feed.atom") }, Link { rel: String::from("self"), mime_type: Some(String::from("application/activity+json")), - href: self.compute_id(conn) + href: self.ap_url.clone() } ] } } + + pub fn from_url(conn: &PgConnection, url: String) -> Option { + User::find_by_ap_url(conn, url.clone()).or_else(|| { + // 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() != BASE_URL.as_str() { + Some(User::fetch_from_url(conn, url).unwrap()) + } else { + None + } + }) + } } impl<'a, 'r> FromRequest<'a, 'r> for User { @@ -374,63 +395,6 @@ impl<'a, 'r> FromRequest<'a, 'r> for User { } } -impl APActor for User { - fn get_box_prefix() -> &'static str { - "@" - } - - fn get_actor_id(&self) -> String { - self.username.to_string() - } - - fn get_display_name(&self) -> String { - self.display_name.clone() - } - - fn get_summary(&self) -> String { - self.summary.get().clone() - } - - fn get_instance(&self, conn: &PgConnection) -> Instance { - Instance::get(conn, self.instance_id).unwrap() - } - - fn get_actor_type() -> ActorType { - ActorType::Person - } - - fn get_inbox_url(&self) -> String { - self.inbox_url.clone() - } - - fn get_shared_inbox_url(&self) -> Option { - self.shared_inbox_url.clone() - } - - fn custom_props(&self, conn: &PgConnection) -> serde_json::Map { - let mut res = serde_json::Map::new(); - res.insert("publicKey".to_string(), json!({ - "id": self.get_key_id(conn), - "owner": self.compute_id(conn), - "publicKeyPem": self.public_key - })); - res.insert("followers".to_string(), serde_json::Value::String(self.compute_box(conn, "followers"))); - res - } - - fn from_url(conn: &PgConnection, url: String) -> Option { - User::find_by_ap_url(conn, url.clone()).or_else(|| { - // 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() != BASE_URL.as_str() { - Some(User::fetch_from_url(conn, url).unwrap()) - } else { - None - } - }) - } -} - impl IntoId for User { fn into_id(self) -> Id { Id::new(self.ap_url.clone()) @@ -450,19 +414,11 @@ impl WithInbox for User { } } -impl Inbox for User { - fn received(&self, conn: &PgConnection, act: serde_json::Value) { - if let Err(err) = self.save(conn, act.clone()) { - println!("Inbox error:\n{}\n{}\n\nActivity was: {}", err.cause(), err.backtrace(), act.to_string()); - } - - // TODO: add to stream, or whatever needs to be done - } -} +impl Inbox for User {} impl Signer for User { - fn get_key_id(&self, conn: &PgConnection) -> String { - format!("{}#main-key", self.compute_id(conn)) + fn get_key_id(&self) -> String { + format!("{}#main-key", self.ap_url) } fn sign(&self, to_sign: String) -> Vec { diff --git a/src/routes/blogs.rs b/src/routes/blogs.rs index ce53a64..c843968 100644 --- a/src/routes/blogs.rs +++ b/src/routes/blogs.rs @@ -6,7 +6,7 @@ use rocket::{ use rocket_contrib::Template; use serde_json; -use activity_pub::{ActivityStream, ActivityPub, actor::Actor}; +use activity_pub::ActivityStream; use db_conn::DbConn; use models::{ blog_authors::*, @@ -32,9 +32,9 @@ fn details(name: String, conn: DbConn, user: Option) -> Template { } #[get("/~/", format = "application/activity+json", rank = 1)] -fn activity_details(name: String, conn: DbConn) -> ActivityPub { +fn activity_details(name: String, conn: DbConn) -> ActivityStream { let blog = Blog::find_local(&*conn, name).unwrap(); - blog.as_activity_pub(&*conn) + ActivityStream::new(blog.into_activity(&*conn)) } #[get("/blogs/new")] diff --git a/src/routes/comments.rs b/src/routes/comments.rs index e7fe466..aa9997f 100644 --- a/src/routes/comments.rs +++ b/src/routes/comments.rs @@ -1,41 +1,22 @@ use rocket::{ request::Form, - response::{Redirect, Flash} + response::Redirect }; -use rocket_contrib::Template; +use serde_json; -use activity_pub::{broadcast, inbox::Notify}; +use activity_pub::{broadcast, inbox::Inbox}; use db_conn::DbConn; use models::{ blogs::Blog, comments::*, + instance::Instance, posts::Post, users::User }; -use utils; -use safe_string::SafeString; - -#[get("/~///comment")] -fn new(blog: String, slug: String, user: User, conn: DbConn) -> Template { - may_fail!(Blog::find_by_fqn(&*conn, blog), "Couldn't find this blog", |blog| { - may_fail!(Post::find_by_slug(&*conn, slug, blog.id), "Couldn't find this post", |post| { - Template::render("comments/new", json!({ - "post": post, - "account": user - })) - }) - }) -} - -#[get("/~///comment", rank=2)] -fn new_auth(blog: String, slug: String) -> Flash{ - utils::requires_login("You need to be logged in order to post a comment", uri!(new: blog = blog, slug = slug)) -} - #[derive(FromForm)] -struct CommentQuery { - responding_to: Option +pub struct CommentQuery { + pub responding_to: Option } #[derive(FromForm)] @@ -43,23 +24,29 @@ struct NewCommentForm { pub content: String } +// See: https://github.com/SergioBenitez/Rocket/pull/454 +#[post("/~///comment", data = "")] +fn create(blog_name: String, slug: String, data: Form, user: User, conn: DbConn) -> Redirect { + create_response(blog_name, slug, None, data, user, conn) +} + #[post("/~///comment?", data = "")] -fn create(blog_name: String, slug: String, query: CommentQuery, data: Form, user: User, conn: DbConn) -> Redirect { +fn create_response(blog_name: String, slug: String, query: Option, data: Form, user: User, conn: DbConn) -> Redirect { let blog = Blog::find_by_fqn(&*conn, blog_name.clone()).unwrap(); let post = Post::find_by_slug(&*conn, slug.clone(), blog.id).unwrap(); let form = data.get(); - let comment = Comment::insert(&*conn, NewComment { - content: SafeString::new(&form.content.clone()), - in_response_to_id: query.responding_to, - post_id: post.id, - author_id: user.id, - ap_url: None, // TODO: set it - sensitive: false, - spoiler_text: "".to_string() - }); - comment.notify(&*conn); - broadcast(&*conn, &user, comment.create_activity(&*conn), user.get_followers(&*conn)); + let (new_comment, id) = NewComment::build() + .content(form.content.clone()) + .in_response_to_id(query.and_then(|q| q.responding_to)) + .post(post) + .author(user.clone()) + .create(&*conn); - Redirect::to(format!("/~/{}/{}/#comment-{}", blog_name, slug, comment.id)) + let instance = Instance::get_local(&*conn).unwrap(); + instance.received(&*conn, serde_json::to_value(new_comment.clone()).expect("JSON serialization error")) + .expect("We are not compatible with ourselve: local broadcast failed (new comment)"); + broadcast(&user, new_comment, user.get_followers(&*conn)); + + Redirect::to(format!("/~/{}/{}/#comment-{}", blog_name, slug, id)) } diff --git a/src/routes/instance.rs b/src/routes/instance.rs index 9463511..4c7816f 100644 --- a/src/routes/instance.rs +++ b/src/routes/instance.rs @@ -35,8 +35,13 @@ fn index(conn: DbConn, user: Option) -> Template { fn shared_inbox(conn: DbConn, data: String) -> String { let act: serde_json::Value = serde_json::from_str(&data[..]).unwrap(); let instance = Instance::get_local(&*conn).unwrap(); - instance.received(&*conn, act); - String::from("") + match instance.received(&*conn, act) { + Ok(_) => String::new(), + Err(e) => { + println!("Shared inbox error: {}\n{}", e.cause(), e.backtrace()); + format!("Error: {}", e.cause()) + } + } } #[get("/nodeinfo")] diff --git a/src/routes/likes.rs b/src/routes/likes.rs index 216a990..c6b0a5c 100644 --- a/src/routes/likes.rs +++ b/src/routes/likes.rs @@ -25,11 +25,11 @@ fn create(blog: String, slug: String, user: User, conn: DbConn) -> Redirect { like.update_ap_url(&*conn); like.notify(&*conn); - broadcast(&*conn, &user, like.into_activity(&*conn), user.get_followers(&*conn)); + broadcast(&user, like.into_activity(&*conn), user.get_followers(&*conn)); } else { let like = likes::Like::find_by_user_on_post(&*conn, user.id, post.id).unwrap(); let delete_act = like.delete(&*conn); - broadcast(&*conn, &user, delete_act, user.get_followers(&*conn)); + broadcast(&user, delete_act, user.get_followers(&*conn)); } Redirect::to(uri!(super::posts::details: blog = blog, slug = slug)) diff --git a/src/routes/posts.rs b/src/routes/posts.rs index fe56629..79e0a8f 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -1,10 +1,11 @@ +use activitypub::object::Article; use heck::KebabCase; use rocket::request::Form; use rocket::response::{Redirect, Flash}; use rocket_contrib::Template; use serde_json; -use activity_pub::{broadcast, context, activity_pub, ActivityPub, Id}; +use activity_pub::{broadcast, ActivityStream}; use db_conn::DbConn; use models::{ blogs::*, @@ -14,14 +15,21 @@ use models::{ posts::*, users::User }; +use routes::comments::CommentQuery; use safe_string::SafeString; use utils; +// See: https://github.com/SergioBenitez/Rocket/pull/454 #[get("/~//", rank = 4)] fn details(blog: String, slug: String, conn: DbConn, user: Option) -> Template { + details_response(blog, slug, conn, user, None) +} + +#[get("/~//?")] +fn details_response(blog: String, slug: String, conn: DbConn, user: Option, query: Option) -> Template { may_fail!(Blog::find_by_fqn(&*conn, blog), "Couldn't find this blog", |blog| { may_fail!(Post::find_by_slug(&*conn, slug, blog.id), "Couldn't find this post", |post| { - let comments = Comment::find_by_post(&*conn, post.id); + let comments = Comment::list_by_post(&*conn, post.id); Template::render("posts/details", json!({ "author": post.get_authors(&*conn)[0].to_json(&*conn), @@ -33,20 +41,20 @@ fn details(blog: String, slug: String, conn: DbConn, user: Option) -> Temp "n_reshares": post.get_reshares(&*conn).len(), "has_reshared": user.clone().map(|u| u.has_reshared(&*conn, &post)).unwrap_or(false), "account": user, - "date": &post.creation_date.timestamp() + "date": &post.creation_date.timestamp(), + "previous": query.and_then(|q| q.responding_to.map(|r| Comment::get(&*conn, r).expect("Error retrieving previous comment").to_json(&*conn))), + "user_fqn": user.map(|u| u.get_fqn(&*conn)).unwrap_or(String::new()) })) }) }) } #[get("/~//", rank = 3, format = "application/activity+json")] -fn activity_details(blog: String, slug: String, conn: DbConn) -> ActivityPub { +fn activity_details(blog: String, slug: String, conn: DbConn) -> ActivityStream
{ let blog = Blog::find_by_fqn(&*conn, blog).unwrap(); let post = Post::find_by_slug(&*conn, slug, blog.id).unwrap(); - let mut act = serde_json::to_value(post.into_activity(&*conn)).unwrap(); - act["@context"] = context(); - activity_pub(act) + ActivityStream::new(post.into_activity(&*conn)) } #[get("/~//new", rank = 2)] @@ -106,11 +114,11 @@ fn create(blog_name: String, data: Form, user: User, conn: DbConn) }); for m in mentions.into_iter() { - Mention::from_activity(&*conn, Mention::build_activity(&*conn, m), Id::new(post.compute_id(&*conn))); + Mention::from_activity(&*conn, Mention::build_activity(&*conn, m), post.id, true); } let act = post.create_activity(&*conn); - broadcast(&*conn, &user, act, user.get_followers(&*conn)); + broadcast(&user, act, user.get_followers(&*conn)); Redirect::to(uri!(details: blog = blog_name, slug = slug)) } diff --git a/src/routes/reshares.rs b/src/routes/reshares.rs index 5212faa..4166f26 100644 --- a/src/routes/reshares.rs +++ b/src/routes/reshares.rs @@ -25,11 +25,11 @@ fn create(blog: String, slug: String, user: User, conn: DbConn) -> Redirect { reshare.update_ap_url(&*conn); reshare.notify(&*conn); - broadcast(&*conn, &user, reshare.into_activity(&*conn), user.get_followers(&*conn)); + broadcast(&user, reshare.into_activity(&*conn), user.get_followers(&*conn)); } else { let reshare = Reshare::find_by_user_on_post(&*conn, user.id, post.id).unwrap(); let delete_act = reshare.delete(&*conn); - broadcast(&*conn, &user, delete_act, user.get_followers(&*conn)); + broadcast(&user, delete_act, user.get_followers(&*conn)); } Redirect::to(uri!(super::posts::details: blog = blog, slug = slug)) diff --git a/src/routes/user.rs b/src/routes/user.rs index da78cf6..a2bbfc0 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -9,9 +9,8 @@ use rocket_contrib::Template; use serde_json; use activity_pub::{ - activity_pub, ActivityPub, ActivityStream, context, broadcast, Id, IntoId, - inbox::{Inbox, Notify}, - actor::Actor + ActivityStream, broadcast, Id, IntoId, + inbox::{Inbox, Notify} }; use db_conn::DbConn; use models::{ @@ -82,7 +81,7 @@ fn follow(name: String, conn: DbConn, user: User) -> Redirect { act.follow_props.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]); + broadcast(&user, act, vec![target]); Redirect::to(uri!(details: name = name)) } @@ -110,9 +109,9 @@ fn followers(name: String, conn: DbConn, account: Option) -> Template { } #[get("/@/", format = "application/activity+json", rank = 1)] -fn activity_details(name: String, conn: DbConn) -> ActivityPub { +fn activity_details(name: String, conn: DbConn) -> ActivityStream { let user = User::find_local(&*conn, name).unwrap(); - user.as_activity_pub(&*conn) + ActivityStream::new(user.into_activity(&*conn)) } #[get("/users/new")] @@ -199,21 +198,23 @@ fn outbox(name: String, conn: DbConn) -> ActivityStream { fn inbox(name: String, conn: DbConn, data: String) -> String { let user = User::find_local(&*conn, name).unwrap(); let act: serde_json::Value = serde_json::from_str(&data[..]).unwrap(); - user.received(&*conn, act); - String::from("") + match user.received(&*conn, act) { + Ok(_) => String::new(), + Err(e) => { + println!("User inbox error: {}\n{}", e.cause(), e.backtrace()); + format!("Error: {}", e.cause()) + } + } } #[get("/@//followers", format = "application/activity+json")] -fn ap_followers(name: String, conn: DbConn) -> ActivityPub { +fn ap_followers(name: String, conn: DbConn) -> ActivityStream { let user = User::find_local(&*conn, name).unwrap(); - let followers = user.get_followers(&*conn).into_iter().map(|f| f.compute_id(&*conn)).collect::>(); - - let json = json!({ - "@context": context(), - "id": user.compute_box(&*conn, "followers"), - "type": "OrderedCollection", - "totalItems": followers.len(), - "orderedItems": followers - }); - activity_pub(json) + let followers = user.get_followers(&*conn).into_iter().map(|f| Id::new(f.ap_url)).collect::>(); + + let mut coll = OrderedCollection::default(); + coll.object_props.set_id_string(format!("{}/followers", user.ap_url)).expect("Follower collection: id error"); + coll.collection_props.set_total_items_u64(followers.len() as u64).expect("Follower collection: totalItems error"); + coll.collection_props.set_items_link_vec(followers).expect("Follower collection: items error"); + ActivityStream::new(coll) } diff --git a/src/safe_string.rs b/src/safe_string.rs index 3b17a6e..9889743 100644 --- a/src/safe_string.rs +++ b/src/safe_string.rs @@ -9,7 +9,7 @@ use diesel::{self, deserialize::Queryable, sql_types::Text, serialize::{self, Output}}; -#[derive(Debug,Clone,AsExpression,FromSqlRow)] +#[derive(Debug, Clone, AsExpression, FromSqlRow, Default)] #[sql_type = "Text"] pub struct SafeString{ value: String, diff --git a/static/main.css b/static/main.css index b141505..2f033c3 100644 --- a/static/main.css +++ b/static/main.css @@ -257,7 +257,7 @@ input { transition: all 0.1s ease-in; display: block; width: 100%; - margin: auto; + margin: auto auto 5em; padding: 0.5em; box-sizing: border-box; @@ -266,7 +266,7 @@ input { border: none; border-bottom: solid #DADADA 2px; } -input[type="submit"] { margin: 2em auto; } +form input[type="submit"] { margin: 2em auto; } input:focus { background: #FAFAFA; border-bottom-color: #7765E3; diff --git a/templates/comments/new.html.tera b/templates/comments/new.html.tera deleted file mode 100644 index a02de4a..0000000 --- a/templates/comments/new.html.tera +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "base" %} - -{% block title %} -{{ 'Comment "{{ post }}"' | _(post=post.title) }} -{% endblock title %} - -{% block content %} -

{{ 'Comment "{{ post }}"' | _(post=post.title) }}

-
- - - -
-{% endblock content %} - diff --git a/templates/posts/details.html.tera b/templates/posts/details.html.tera index 097ff3a..5f4be6c 100644 --- a/templates/posts/details.html.tera +++ b/templates/posts/details.html.tera @@ -60,7 +60,16 @@

{{ "Comments" | _ }}

- {{ "Comment" | _ }} + + {% if account %} +
+ + {# Ugly, but we don't have the choice if we don't want weird paddings #} + + +
+ {% endif %} +
{% for comment in comments %} {% if comment.author.display_name %} @@ -75,7 +84,7 @@ @{{ comment.author.username }}
{{ comment.content | safe }}
- {{ "Respond" | _ }} + {{ "Respond" | _ }}
{% endfor %}