diff --git a/Cargo.lock b/Cargo.lock index c6875108..1708ea54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1014,12 +1014,13 @@ dependencies = [ "rocket 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)", "rocket_codegen 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)", "rocket_contrib 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)", - "rocket_i18n 0.1.1 (git+https://github.com/BaptisteGelez/rocket_i18n?rev=457b88c59ec31905a9193df43df58bee55b4b83d)", + "rocket_i18n 0.1.1 (git+https://github.com/BaptisteGelez/rocket_i18n?rev=5b4225d5bed5769482dc926a7e6d6b79f1217be6)", "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)", "tera 0.11.7 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "webfinger 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1273,7 +1274,7 @@ dependencies = [ [[package]] name = "rocket_i18n" version = "0.1.1" -source = "git+https://github.com/BaptisteGelez/rocket_i18n?rev=457b88c59ec31905a9193df43df58bee55b4b83d#457b88c59ec31905a9193df43df58bee55b4b83d" +source = "git+https://github.com/BaptisteGelez/rocket_i18n?rev=5b4225d5bed5769482dc926a7e6d6b79f1217be6#5b4225d5bed5769482dc926a7e6d6b79f1217be6" dependencies = [ "gettext-rs 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "rocket 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)", @@ -1913,6 +1914,17 @@ name = "void" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "webfinger" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "reqwest 0.8.5 (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_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "winapi" version = "0.2.8" @@ -2097,7 +2109,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum rocket_codegen_next 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)" = "" "checksum rocket_contrib 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)" = "" "checksum rocket_http 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)" = "" -"checksum rocket_i18n 0.1.1 (git+https://github.com/BaptisteGelez/rocket_i18n?rev=457b88c59ec31905a9193df43df58bee55b4b83d)" = "" +"checksum rocket_i18n 0.1.1 (git+https://github.com/BaptisteGelez/rocket_i18n?rev=5b4225d5bed5769482dc926a7e6d6b79f1217be6)" = "" "checksum rustc-demangle 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11fb43a206a04116ffd7cfcf9bcb941f8eb6cc7ff667272246b0a1c74259a3cb" "checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" "checksum schannel 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "85fd9df495640643ad2d00443b3d78aae69802ad488debab4f1dd52fc1806ade" @@ -2176,6 +2188,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" "checksum version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6b772017e347561807c1aa192438c5fd74242a670a6cffacc40f2defd1dc069d" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +"checksum webfinger 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "27a4e6d1de7050af8beb026c02bcef5340ec1f3af6d4a02248b7990908baa3ff" "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" "checksum winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "04e3bd221fcbe8a271359c04f21a76db7d0c6028862d1bb5512d85e1e2eb5bb3" "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" diff --git a/Cargo.toml b/Cargo.toml index b1753932..87c2e879 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ serde_derive = "1.0" serde_json = "1.0" tera = "0.11" url = "1.7" +webfinger = "0.1" [dependencies.chrono] features = ["serde"] @@ -48,4 +49,4 @@ rev = "df7111143e466c18d1f56377a8d9530a5a306aba" [dependencies.rocket_i18n] git = "https://github.com/BaptisteGelez/rocket_i18n" -rev = "457b88c59ec31905a9193df43df58bee55b4b83d" +rev = "5b4225d5bed5769482dc926a7e6d6b79f1217be6" diff --git a/po/en.po b/po/en.po index 2634ce4e..31d93560 100644 --- a/po/en.po +++ b/po/en.po @@ -253,3 +253,24 @@ msgstr "" msgid "You need to be logged in order to edit your profile" msgstr "" + +msgid "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name }}{{ link_4 }}" +msgstr "" + +msgid "{{ data }} reshared your article" +msgstr "" + +msgid "{{ data }} started following you" +msgstr "" + +msgid "{{ data }} liked your article" +msgstr "" + +msgid "{{ data }} commented your article" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" diff --git a/po/fr.po b/po/fr.po index 5c182ad7..3fa3bb67 100644 --- a/po/fr.po +++ b/po/fr.po @@ -252,3 +252,25 @@ msgstr "Vous devez vous connecter pour suivre quelqu'un" msgid "You need to be logged in order to edit your profile" msgstr "Vous devez vous connecter pour modifier votre profil" + +#, fuzzy +msgid "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name }}{{ link_4 }}" +msgstr "Écrit par {{ link_1 }}{{ url }}{{ link_2 }}{{ name }}{{ link_3 }}" + +msgid "{{ data }} reshared your article" +msgstr "" + +msgid "{{ data }} started following you" +msgstr "" + +msgid "{{ data }} liked your article" +msgstr "" + +msgid "{{ data }} commented your article" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" diff --git a/po/pl.po b/po/pl.po index 9340090a..a319828f 100644 --- a/po/pl.po +++ b/po/pl.po @@ -10,7 +10,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "|| n%100>=20) ? 1 : 2);\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2);\n" msgid "Latest articles" msgstr "Najnowsze artykuły" @@ -46,7 +47,8 @@ msgid "Something broke on our side." msgstr "Coś poszło nie tak." msgid "Sorry about that. If you think this is a bug, please report it." -msgstr "Przepraszamy. Jeżeli uważasz że wystąpił błąd, prosimy o zgłoszenie go." +msgstr "" +"Przepraszamy. Jeżeli uważasz że wystąpił błąd, prosimy o zgłoszenie go." msgid "Configuration" msgstr "Konfiguracja" @@ -114,8 +116,8 @@ msgstr "Utwórz wpis" msgid "Publish" msgstr "Opublikuj" -msgid "Logowanie" -msgstr "Zaloguj się" +msgid "Login" +msgstr "" msgid "Username or email" msgstr "Nazwa użytkownika lub adres e-mail" @@ -133,7 +135,9 @@ msgid "Your Blogs" msgstr "Twoje blogi" msgid "You don't have any blog yet. Create your own, or ask to join one." -msgstr "Nie posiadasz żadnego bloga. Utwórz własny, lub poproś o dołączanie do istniejącego." +msgstr "" +"Nie posiadasz żadnego bloga. Utwórz własny, lub poproś o dołączanie do " +"istniejącego." msgid "Start a new blog" msgstr "Utwórz nowy blog" @@ -252,3 +256,27 @@ msgstr "Musisz się zalogować, aby zacząć obserwować innych" msgid "You need to be logged in order to edit your profile" msgstr "Musisz się zalogować , aby móc edytować swój profil" +#, fuzzy +msgid "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name }}{{ link_4 }}" +msgstr "Napisano przez {{ link_1 }}{{ url }}{{ link_2 }}{{ name }}{{ link_3 }}" + +msgid "{{ data }} reshared your article" +msgstr "" + +msgid "{{ data }} started following you" +msgstr "" + +msgid "{{ data }} liked your article" +msgstr "" + +msgid "{{ data }} commented your article" +msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" + +#~ msgid "Logowanie" +#~ msgstr "Zaloguj się" diff --git a/po/plume.pot b/po/plume.pot index 1d36fc36..6eaebfb9 100644 --- a/po/plume.pot +++ b/po/plume.pot @@ -263,3 +263,9 @@ msgstr "" msgid "{{ data }} commented your article" msgstr "" + +msgid "We couldn't find this page." +msgstr "" + +msgid "The link that led you here may be broken." +msgstr "" diff --git a/src/activity_pub/inbox.rs b/src/activity_pub/inbox.rs index 1d8fe0a6..a20af1ea 100644 --- a/src/activity_pub/inbox.rs +++ b/src/activity_pub/inbox.rs @@ -1,6 +1,6 @@ use activitypub::{ Object, - activity::{Create, Like, Undo} + activity::{Announce, Create, Like, Undo} }; use diesel::PgConnection; use failure::Error; @@ -90,6 +90,10 @@ pub trait Inbox { likes::Like::delete_activity(conn, Id::new(act.undo_props.object_object::()?.object_props.id_string()?)); Ok(()) }, + "Announce" => { + Reshare::delete_activity(conn, Id::new(act.undo_props.object_object::()?.object_props.id_string()?)); + Ok(()) + } _ => Err(InboxError::CantUndo)? } } diff --git a/src/activity_pub/mod.rs b/src/activity_pub/mod.rs index af544aca..f4c4a06a 100644 --- a/src/activity_pub/mod.rs +++ b/src/activity_pub/mod.rs @@ -17,7 +17,6 @@ pub mod inbox; pub mod object; pub mod request; pub mod sign; -pub mod webfinger; pub type ActivityPub = Content>; @@ -76,18 +75,18 @@ impl<'r, O: Object> Responder<'r> for ActivityStream { } } -pub fn broadcast(conn: &PgConnection, sender: &S, act: A, to: Vec) { +pub fn broadcast(conn: &PgConnection, 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::>() .unique(); + + let mut act = serde_json::to_value(act).unwrap(); + act["@context"] = context(); + let signed = act.sign(sender, conn); + for inbox in boxes { // TODO: run it in Sidekiq or something like that - - let mut act = serde_json::to_value(act.clone()).unwrap(); - act["@context"] = context(); - let signed = act.sign(sender, conn); - let res = Client::new() .post(&inbox[..]) .headers(request::headers()) diff --git a/src/activity_pub/webfinger.rs b/src/activity_pub/webfinger.rs deleted file mode 100644 index 7803fa2e..00000000 --- a/src/activity_pub/webfinger.rs +++ /dev/null @@ -1,52 +0,0 @@ -use diesel::PgConnection; -use reqwest::Client; -use reqwest::{ - header::{Accept, qitem}, - mime::Mime -}; -use serde_json; - -use activity_pub::ap_url; - -pub trait Webfinger { - fn webfinger_subject(&self, conn: &PgConnection) -> String; - fn webfinger_aliases(&self, conn: &PgConnection) -> Vec; - fn webfinger_links(&self, conn: &PgConnection) -> Vec>; - - fn webfinger(&self, conn: &PgConnection) -> String { - json!({ - "subject": self.webfinger_subject(conn), - "aliases": self.webfinger_aliases(conn), - "links": self.webfinger_links(conn).into_iter().map(|link| { - let mut link_obj = serde_json::Map::new(); - for (k, v) in link { - link_obj.insert(k, serde_json::Value::String(v)); - } - serde_json::Value::Object(link_obj) - }).collect::>() - }).to_string() - } -} - -pub fn resolve(acct: String) -> Result { - let instance = acct.split("@").last().unwrap(); - let url = ap_url(format!("{}/.well-known/webfinger?resource=acct:{}", instance, acct)); - Client::new() - .get(&url[..]) - .header(Accept(vec![qitem("application/jrd+json".parse::().unwrap())])) - .send() - .map(|mut r| { - let res = r.text().unwrap(); - let json: serde_json::Value = serde_json::from_str(&res[..]).unwrap(); - json["links"].as_array().unwrap() - .into_iter() - .find_map(|link| { - if link["rel"].as_str().unwrap() == "self" && link["type"].as_str().unwrap() == "application/activity+json" { - Some(String::from(link["href"].as_str().unwrap())) - } else { - None - } - }).unwrap() - }) - .map_err(|e| format!("Error while fetchin WebFinger resource ({})", e)) -} diff --git a/src/main.rs b/src/main.rs index 69df18e8..5455d854 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,6 +33,7 @@ extern crate serde_derive; extern crate serde_json; extern crate tera; extern crate url; +extern crate webfinger; use diesel::{pg::PgConnection, r2d2::{ConnectionManager, Pool}}; use dotenv::dotenv; @@ -128,6 +129,10 @@ fn main() { routes::well_known::nodeinfo, routes::well_known::webfinger ]) + .catch(catchers![ + routes::errors::not_found, + routes::errors::server_error + ]) .manage(init_pool()) .attach(Template::custom(|engines| { rocket_i18n::tera(&mut engines.tera); diff --git a/src/models/blog_authors.rs b/src/models/blog_authors.rs index 28fd3f46..106832a8 100644 --- a/src/models/blog_authors.rs +++ b/src/models/blog_authors.rs @@ -19,18 +19,6 @@ pub struct NewBlogAuthor { } impl BlogAuthor { - pub fn insert (conn: &PgConnection, new: NewBlogAuthor) -> BlogAuthor { - diesel::insert_into(blog_authors::table) - .values(new) - .get_result(conn) - .expect("Error saving new blog author") - } - - pub fn get(conn: &PgConnection, id: i32) -> Option { - blog_authors::table.filter(blog_authors::id.eq(id)) - .limit(1) - .load::(conn) - .expect("Error loading blog author by id") - .into_iter().nth(0) - } + insert!(blog_authors, NewBlogAuthor); + get!(blog_authors); } diff --git a/src/models/blogs.rs b/src/models/blogs.rs index cf8bd070..dc779f6e 100644 --- a/src/models/blogs.rs +++ b/src/models/blogs.rs @@ -14,15 +14,15 @@ use openssl::{ rsa::Rsa, sign::Signer }; +use webfinger::*; use activity_pub::{ ActivityStream, Id, IntoId, actor::{Actor as APActor, ActorType}, inbox::WithInbox, - sign, - webfinger::* + sign }; -use models::instance::Instance; +use models::instance::*; use schema::blogs; @@ -56,20 +56,8 @@ pub struct NewBlog { } impl Blog { - pub fn insert (conn: &PgConnection, new: NewBlog) -> Blog { - diesel::insert_into(blogs::table) - .values(new) - .get_result(conn) - .expect("Error saving new blog") - } - - pub fn get(conn: &PgConnection, id: i32) -> Option { - blogs::table.filter(blogs::id.eq(id)) - .limit(1) - .load::(conn) - .expect("Error loading blog by id") - .into_iter().nth(0) - } + insert!(blogs, NewBlog); + get!(blogs); pub fn find_for_author(conn: &PgConnection, author_id: i32) -> Vec { use schema::blog_authors; @@ -79,14 +67,7 @@ impl Blog { .expect("Couldn't load blogs ") } - pub fn find_by_name(conn: &PgConnection, name: String, instance_id: i32) -> Option { - blogs::table.filter(blogs::actor_id.eq(name)) - .filter(blogs::instance_id.eq(instance_id)) - .limit(1) - .load::(conn) - .expect("Error loading blog by name") - .into_iter().nth(0) - } + 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)) @@ -110,9 +91,9 @@ impl Blog { fn fetch_from_webfinger(conn: &PgConnection, acct: String) -> Option { match resolve(acct.clone()) { - Ok(url) => Blog::fetch_from_url(conn, url), + Ok(wf) => wf.links.into_iter().find(|l| l.mime_type == Some(String::from("application/activity+json"))).and_then(|l| Blog::fetch_from_url(conn, l.href)), Err(details) => { - println!("{}", details); + println!("{:?}", details); None } } @@ -136,7 +117,11 @@ impl Blog { let instance = match Instance::find_by_domain(conn, inst.clone()) { Some(instance) => instance, None => { - Instance::insert(conn, inst.clone(), inst.clone(), false) + Instance::insert(conn, NewInstance { + public_domain: inst.clone(), + name: inst.clone(), + local: false + }) } }; Blog::insert(conn, NewBlog { @@ -186,6 +171,30 @@ impl Blog { pub fn get_keypair(&self) -> PKey { PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.clone().unwrap().as_ref()).unwrap()).unwrap() } + + 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)], + links: vec![ + Link { + rel: String::from("http://webfinger.net/rel/profile-page"), + mime_type: None, + href: self.compute_id(conn) + }, + 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") + }, + Link { + rel: String::from("self"), + mime_type: Some(String::from("application/activity+json")), + href: self.compute_id(conn) + } + ] + } + } } impl IntoId for Blog { @@ -249,33 +258,6 @@ impl APActor for Blog { } } -impl Webfinger for Blog { - fn webfinger_subject(&self, conn: &PgConnection) -> String { - format!("acct:{}@{}", self.actor_id, self.get_instance(conn).public_domain) - } - fn webfinger_aliases(&self, conn: &PgConnection) -> Vec { - vec![self.compute_id(conn)] - } - fn webfinger_links(&self, conn: &PgConnection) -> Vec> { - vec![ - vec![ - (String::from("rel"), String::from("http://webfinger.net/rel/profile-page")), - (String::from("href"), self.compute_id(conn)) - ], - vec![ - (String::from("rel"), String::from("http://schemas.google.com/g/2010#updates-from")), - (String::from("type"), String::from("application/atom+xml")), - (String::from("href"), self.compute_box(conn, "feed.atom")) - ], - vec![ - (String::from("rel"), String::from("self")), - (String::from("type"), String::from("application/activity+json")), - (String::from("href"), self.compute_id(conn)) - ] - ] - } -} - impl sign::Signer for Blog { fn get_key_id(&self, conn: &PgConnection) -> String { format!("{}#main-key", self.compute_id(conn)) diff --git a/src/models/comments.rs b/src/models/comments.rs index 75df9df0..79848332 100644 --- a/src/models/comments.rs +++ b/src/models/comments.rs @@ -47,34 +47,10 @@ pub struct NewComment { } impl Comment { - pub fn insert (conn: &PgConnection, new: NewComment) -> Comment { - diesel::insert_into(comments::table) - .values(new) - .get_result(conn) - .expect("Error saving new comment") - } - - pub fn get(conn: &PgConnection, id: i32) -> Option { - comments::table.filter(comments::id.eq(id)) - .limit(1) - .load::(conn) - .expect("Error loading comment by id") - .into_iter().nth(0) - } - - pub fn find_by_post(conn: &PgConnection, post_id: i32) -> Vec { - comments::table.filter(comments::post_id.eq(post_id)) - .load::(conn) - .expect("Error loading comment by post id") - } - - pub fn find_by_ap_url(conn: &PgConnection, ap_url: String) -> Option { - comments::table.filter(comments::ap_url.eq(ap_url)) - .limit(1) - .load::(conn) - .expect("Error loading comment by AP URL") - .into_iter().nth(0) - } + insert!(comments, NewComment); + get!(comments); + find_by!(comments, find_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 { User::get(conn, self.author_id).unwrap() @@ -123,6 +99,12 @@ impl Comment { .expect("Couldn't load local comments") .len() } + + 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); + json + } } impl FromActivity for Comment { diff --git a/src/models/follows.rs b/src/models/follows.rs index 6ba9385f..06548625 100644 --- a/src/models/follows.rs +++ b/src/models/follows.rs @@ -25,20 +25,8 @@ pub struct NewFollow { } impl Follow { - pub fn insert(conn: &PgConnection, new: NewFollow) -> Follow { - diesel::insert_into(follows::table) - .values(new) - .get_result(conn) - .expect("Unable to insert new follow") - } - - pub fn get(conn: &PgConnection, id: i32) -> Option { - follows::table.filter(follows::id.eq(id)) - .limit(1) - .load::(conn) - .expect("Unable to load follow by id") - .into_iter().nth(0) - } + insert!(follows, NewFollow); + get!(follows); pub fn accept_follow( conn: &PgConnection, diff --git a/src/models/instance.rs b/src/models/instance.rs index c3967092..b7128da6 100644 --- a/src/models/instance.rs +++ b/src/models/instance.rs @@ -44,32 +44,9 @@ impl Instance { Instance::get_local(conn).unwrap().id } - pub fn insert<'a>(conn: &PgConnection, pub_dom: String, name: String, local: bool) -> Instance { - diesel::insert_into(instances::table) - .values(NewInstance { - public_domain: pub_dom, - name: name, - local: local - }) - .get_result(conn) - .expect("Error saving new instance") - } - - pub fn get(conn: &PgConnection, id: i32) -> Option { - instances::table.filter(instances::id.eq(id)) - .limit(1) - .load::(conn) - .expect("Error loading local instance infos") - .into_iter().nth(0) - } - - pub fn find_by_domain(conn: &PgConnection, domain: String) -> Option { - instances::table.filter(instances::public_domain.eq(domain)) - .limit(1) - .load::(conn) - .expect("Couldn't load instance by domain") - .into_iter().nth(0) - } + insert!(instances, NewInstance); + get!(instances); + find_by!(instances, find_by_domain, public_domain as String); pub fn block(&self) { unimplemented!() @@ -86,7 +63,7 @@ impl Instance { impl Inbox for Instance { fn received(&self, conn: &PgConnection, act: serde_json::Value) { - self.save(conn, act.clone()).unwrap(); + self.save(conn, act.clone()).expect("Shared Inbox: Couldn't save activity"); // TODO: add to stream, or whatever needs to be done } diff --git a/src/models/likes.rs b/src/models/likes.rs index 2462118a..58aba083 100644 --- a/src/models/likes.rs +++ b/src/models/likes.rs @@ -35,12 +35,10 @@ pub struct NewLike { } impl Like { - pub fn insert(conn: &PgConnection, new: NewLike) -> Like { - diesel::insert_into(likes::table) - .values(new) - .get_result(conn) - .expect("Unable to insert new like") - } + insert!(likes, NewLike); + get!(likes); + find_by!(likes, find_by_ap_url, ap_url as String); + find_by!(likes, find_by_user_on_post, user_id as i32, post_id as i32); pub fn update_ap_url(&self, conn: &PgConnection) { if self.ap_url.len() == 0 { @@ -50,31 +48,6 @@ impl Like { } } - pub fn get(conn: &PgConnection, id: i32) -> Option { - likes::table.filter(likes::id.eq(id)) - .limit(1) - .load::(conn) - .expect("Error loading like by ID") - .into_iter().nth(0) - } - - pub fn find_by_ap_url(conn: &PgConnection, ap_url: String) -> Option { - likes::table.filter(likes::ap_url.eq(ap_url)) - .limit(1) - .load::(conn) - .expect("Error loading like by AP URL") - .into_iter().nth(0) - } - - pub fn find_by_user_on_post(conn: &PgConnection, user: &User, post: &Post) -> Option { - likes::table.filter(likes::post_id.eq(post.id)) - .filter(likes::user_id.eq(user.id)) - .limit(1) - .load::(conn) - .expect("Error loading like for user and post") - .into_iter().nth(0) - } - pub fn delete(&self, conn: &PgConnection) -> activity::Undo { diesel::delete(self).execute(conn).unwrap(); diff --git a/src/models/mod.rs b/src/models/mod.rs index a7b1b87b..d0f01f3c 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,3 +1,40 @@ +macro_rules! find_by { + ($table:ident, $fn:ident, $($col:ident as $type:ident),+) => { + /// Try to find a $table with a given $col + pub fn $fn(conn: &PgConnection, $($col: $type),+) -> Option { + $table::table + $(.filter($table::$col.eq($col)))+ + .limit(1) + .load::(conn) + .expect("Error loading $table by $col") + .into_iter().nth(0) + } + }; +} + +macro_rules! get { + ($table:ident) => { + pub fn get(conn: &PgConnection, id: i32) -> Option { + $table::table.filter($table::id.eq(id)) + .limit(1) + .load::(conn) + .expect("Error loading $table by id") + .into_iter().nth(0) + } + }; +} + +macro_rules! insert { + ($table:ident, $from:ident) => { + pub fn insert(conn: &PgConnection, new: $from) -> Self { + diesel::insert_into($table::table) + .values(new) + .get_result(conn) + .expect("Error saving new $table") + } + }; +} + pub mod blog_authors; pub mod blogs; pub mod comments; diff --git a/src/models/notifications.rs b/src/models/notifications.rs index e0a9acc9..c5b7d599 100644 --- a/src/models/notifications.rs +++ b/src/models/notifications.rs @@ -26,20 +26,8 @@ pub struct NewNotification { } impl Notification { - pub fn insert(conn: &PgConnection, new: NewNotification) -> Notification { - diesel::insert_into(notifications::table) - .values(new) - .get_result(conn) - .expect("Couldn't save notification") - } - - pub fn get(conn: &PgConnection, id: i32) -> Option { - notifications::table.filter(notifications::id.eq(id)) - .limit(1) - .load::(conn) - .expect("Couldn't load notification by ID") - .into_iter().nth(0) - } + insert!(notifications, NewNotification); + get!(notifications); pub fn find_for_user(conn: &PgConnection, user: &User) -> Vec { notifications::table.filter(notifications::user_id.eq(user.id)) diff --git a/src/models/post_authors.rs b/src/models/post_authors.rs index e5849c65..174507c7 100644 --- a/src/models/post_authors.rs +++ b/src/models/post_authors.rs @@ -23,18 +23,6 @@ pub struct NewPostAuthor { } impl PostAuthor { - pub fn insert (conn: &PgConnection, new: NewPostAuthor) -> PostAuthor { - diesel::insert_into(post_authors::table) - .values(new) - .get_result(conn) - .expect("Error saving new blog author") - } - - pub fn get(conn: &PgConnection, id: i32) -> Option { - post_authors::table.filter(post_authors::id.eq(id)) - .limit(1) - .load::(conn) - .expect("Error loading blog author by id") - .into_iter().nth(0) - } + insert!(post_authors, NewPostAuthor); + get!(post_authors); } diff --git a/src/models/posts.rs b/src/models/posts.rs index b17a2340..8d12bc8f 100644 --- a/src/models/posts.rs +++ b/src/models/posts.rs @@ -50,20 +50,10 @@ pub struct NewPost { } impl Post { - pub fn insert(conn: &PgConnection, new: NewPost) -> Post { - diesel::insert_into(posts::table) - .values(new) - .get_result(conn) - .expect("Error saving new post") - } - - pub fn get(conn: &PgConnection, id: i32) -> Option { - posts::table.filter(posts::id.eq(id)) - .limit(1) - .load::(conn) - .expect("Error loading post by id") - .into_iter().nth(0) - } + insert!(posts, NewPost); + get!(posts); + find_by!(posts, find_by_slug, slug as String); + find_by!(posts, find_by_ap_url, ap_url as String); pub fn count_local(conn: &PgConnection) -> usize { use schema::post_authors; @@ -76,22 +66,6 @@ impl Post { .len() } - pub fn find_by_slug(conn: &PgConnection, slug: String) -> Option { - posts::table.filter(posts::slug.eq(slug)) - .limit(1) - .load::(conn) - .expect("Error loading post by slug") - .into_iter().nth(0) - } - - pub fn find_by_ap_url(conn: &PgConnection, ap_url: String) -> Option { - posts::table.filter(posts::ap_url.eq(ap_url)) - .limit(1) - .load::(conn) - .expect("Error loading post by AP URL") - .into_iter().nth(0) - } - pub fn get_recents(conn: &PgConnection, limit: i64) -> Vec { posts::table.order(posts::creation_date.desc()) .limit(limit) @@ -194,6 +168,15 @@ impl Post { act.create_props.set_object_object(self.into_activity(conn)).unwrap(); act } + + pub fn to_json(&self, conn: &PgConnection) -> serde_json::Value { + json!({ + "post": self, + "author": self.get_authors(conn)[0].to_json(conn), + "url": format!("/~/{}/{}/", self.get_blog(conn).actor_id, self.slug), + "date": self.creation_date.timestamp() + }) + } } impl FromActivity
for Post { diff --git a/src/models/reshares.rs b/src/models/reshares.rs index 75537672..09e75bfa 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}, object::Object}; +use activity_pub::{Id, IntoId, actor::Actor, inbox::{FromActivity, Notify, Deletable}, object::Object}; use models::{notifications::*, posts::Post, users::User}; use schema::reshares; @@ -24,20 +24,10 @@ pub struct NewReshare { } impl Reshare { - pub fn insert(conn: &PgConnection, new: NewReshare) -> Reshare { - diesel::insert_into(reshares::table) - .values(new) - .get_result(conn) - .expect("Couldn't save reshare") - } - - pub fn get(conn: &PgConnection, id: i32) -> Option { - reshares::table.filter(reshares::id.eq(id)) - .limit(1) - .load::(conn) - .expect("Could'nt load reshare") - .into_iter().nth(0) - } + insert!(reshares, NewReshare); + get!(reshares); + find_by!(reshares, find_by_ap_url, ap_url as String); + find_by!(reshares, find_by_user_on_post, user_id as i32, post_id as i32); pub fn update_ap_url(&self, conn: &PgConnection) { if self.ap_url.len() == 0 { @@ -51,23 +41,6 @@ impl Reshare { } } - pub fn find_by_ap_url(conn: &PgConnection, ap_url: String) -> Option { - reshares::table.filter(reshares::ap_url.eq(ap_url)) - .limit(1) - .load::(conn) - .expect("Error loading reshare by AP URL") - .into_iter().nth(0) - } - - pub fn find_by_user_on_post(conn: &PgConnection, user: &User, post: &Post) -> Option { - reshares::table.filter(reshares::post_id.eq(post.id)) - .filter(reshares::user_id.eq(user.id)) - .limit(1) - .load::(conn) - .expect("Error loading reshare for user and post") - .into_iter().nth(0) - } - pub fn get_recents_for_author(conn: &PgConnection, user: &User, limit: i64) -> Vec { reshares::table.filter(reshares::user_id.eq(user.id)) .order(reshares::creation_date.desc()) @@ -129,3 +102,14 @@ impl Notify for Reshare { } } } + +impl Deletable for Reshare { + fn delete_activity(conn: &PgConnection, id: Id) -> bool { + if let Some(reshare) = Reshare::find_by_ap_url(conn, id.into()) { + reshare.delete(conn); + true + } else { + false + } + } +} diff --git a/src/models/users.rs b/src/models/users.rs index 78846ac8..5b5b1c10 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -24,21 +24,21 @@ use rocket::{ }; use serde_json; use url::Url; +use webfinger::*; use BASE_URL; use activity_pub::{ ap_url, ActivityStream, Id, IntoId, actor::{ActorType, Actor as APActor}, inbox::{Inbox, WithInbox}, - sign::{Signer, gen_keypair}, - webfinger::{Webfinger, resolve} + sign::{Signer, gen_keypair} }; use db_conn::DbConn; use models::{ blogs::Blog, blog_authors::BlogAuthor, follows::Follow, - instance::Instance, + instance::*, post_authors::PostAuthor, posts::Post }; @@ -85,6 +85,8 @@ pub struct NewUser { } impl User { + insert!(users, NewUser); + pub fn grant_admin_rights(&self, conn: &PgConnection) { diesel::update(self) .set(users::is_admin.eq(true)) @@ -92,13 +94,6 @@ impl User { .expect("Couldn't grant admin rights"); } - pub fn insert(conn: &PgConnection, new: NewUser) -> User { - diesel::insert_into(users::table) - .values(new) - .get_result(conn) - .expect("Error saving new user") - } - pub fn update(&self, conn: &PgConnection, name: String, email: String, summary: String) -> User { diesel::update(self) .set(( @@ -110,13 +105,7 @@ impl User { .into_iter().nth(0).unwrap() } - pub fn get(conn: &PgConnection, id: i32) -> Option { - users::table.filter(users::id.eq(id)) - .limit(1) - .load::(conn) - .expect("Error loading user by id") - .into_iter().nth(0) - } + get!(users); pub fn count_local(conn: &PgConnection) -> usize { users::table.filter(users::instance_id.eq(Instance::local_id(conn))) @@ -125,22 +114,8 @@ impl User { .len() } - pub fn find_by_email(conn: &PgConnection, email: String) -> Option { - users::table.filter(users::email.eq(email)) - .limit(1) - .load::(conn) - .expect("Error loading user by email") - .into_iter().nth(0) - } - - pub fn find_by_name(conn: &PgConnection, username: String, instance_id: i32) -> Option { - users::table.filter(users::username.eq(username)) - .filter(users::instance_id.eq(instance_id)) - .limit(1) - .load::(conn) - .expect("Error loading user by name") - .into_iter().nth(0) - } + find_by!(users, find_by_email, email as String); + find_by!(users, find_by_name, username as String, instance_id as i32); pub fn find_local(conn: &PgConnection, username: String) -> Option { User::find_by_name(conn, username, Instance::local_id(conn)) @@ -164,9 +139,9 @@ impl User { fn fetch_from_webfinger(conn: &PgConnection, acct: String) -> Option { match resolve(acct.clone()) { - Ok(url) => User::fetch_from_url(conn, url), + Ok(wf) => wf.links.into_iter().find(|l| l.mime_type == Some(String::from("application/activity+json"))).and_then(|l| User::fetch_from_url(conn, l.href)), Err(details) => { - println!("{}", details); + println!("{:?}", details); None } } @@ -190,7 +165,11 @@ impl User { let instance = match Instance::find_by_domain(conn, inst.clone()) { Some(instance) => instance, None => { - Instance::insert(conn, inst.clone(), inst.clone(), false) + Instance::insert(conn, NewInstance { + name: inst.clone(), + public_domain: inst.clone(), + local: false + }) } }; User::insert(conn, NewUser { @@ -351,6 +330,36 @@ impl User { }; actor } + + pub fn to_json(&self, conn: &PgConnection) -> serde_json::Value { + let mut json = serde_json::to_value(self).unwrap(); + json["fqn"] = serde_json::Value::String(self.get_fqn(conn)); + json + } + + 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)], + links: vec![ + Link { + rel: String::from("http://webfinger.net/rel/profile-page"), + mime_type: None, + href: self.compute_id(conn) + }, + 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") + }, + Link { + rel: String::from("self"), + mime_type: Some(String::from("application/activity+json")), + href: self.compute_id(conn) + } + ] + } + } } impl<'a, 'r> FromRequest<'a, 'r> for User { @@ -460,33 +469,6 @@ impl Inbox for User { } } -impl Webfinger for User { - fn webfinger_subject(&self, conn: &PgConnection) -> String { - format!("acct:{}@{}", self.username, self.get_instance(conn).public_domain) - } - fn webfinger_aliases(&self, conn: &PgConnection) -> Vec { - vec![self.compute_id(conn)] - } - fn webfinger_links(&self, conn: &PgConnection) -> Vec> { - vec![ - vec![ - (String::from("rel"), String::from("http://webfinger.net/rel/profile-page")), - (String::from("href"), self.compute_id(conn)) - ], - vec![ - (String::from("rel"), String::from("http://schemas.google.com/g/2010#updates-from")), - (String::from("type"), String::from("application/atom+xml")), - (String::from("href"), self.compute_box(conn, "feed.atom")) - ], - vec![ - (String::from("rel"), String::from("self")), - (String::from("type"), String::from("application/activity+json")), - (String::from("href"), self.compute_id(conn)) - ] - ] - } -} - impl Signer for User { fn get_key_id(&self, conn: &PgConnection) -> String { format!("{}#main-key", self.compute_id(conn)) diff --git a/src/routes/blogs.rs b/src/routes/blogs.rs index 0a64e9e6..eecc07fc 100644 --- a/src/routes/blogs.rs +++ b/src/routes/blogs.rs @@ -19,26 +19,16 @@ use utils; #[get("/~/", rank = 2)] fn details(name: String, conn: DbConn, user: Option) -> Template { - let blog = Blog::find_by_fqn(&*conn, name).unwrap(); - let recents = Post::get_recents_for_blog(&*conn, &blog, 5); - Template::render("blogs/details", json!({ - "blog": blog, - "account": user, - "is_author": user.map(|x| x.is_author_in(&*conn, blog)), - "recents": recents.into_iter().map(|p| { - json!({ - "post": p, - "author": ({ - let author = &p.get_authors(&*conn)[0]; - let mut json = serde_json::to_value(author).unwrap(); - json["fqn"] = serde_json::Value::String(author.get_fqn(&*conn)); - json - }), - "url": format!("/~/{}/{}/", p.get_blog(&*conn).actor_id, p.slug), - "date": p.creation_date.timestamp() - }) - }).collect::>() - })) + may_fail!(Blog::find_by_fqn(&*conn, name), "Requested blog couldn't be found", |blog| { + let recents = Post::get_recents_for_blog(&*conn, &blog, 5); + + Template::render("blogs/details", json!({ + "blog": blog, + "account": user, + "is_author": user.map(|x| x.is_author_in(&*conn, blog)), + "recents": recents.into_iter().map(|p| p.to_json(&*conn)).collect::>() + })) + }) } #[get("/~/", format = "application/activity+json", rank = 1)] diff --git a/src/routes/comments.rs b/src/routes/comments.rs index 7c039ee7..289dee28 100644 --- a/src/routes/comments.rs +++ b/src/routes/comments.rs @@ -4,7 +4,7 @@ use rocket::{ }; use rocket_contrib::Template; -use activity_pub::broadcast; +use activity_pub::{broadcast, IntoId, inbox::Notify}; use db_conn::DbConn; use models::{ comments::*, @@ -17,11 +17,12 @@ use safe_string::SafeString; #[get("/~/<_blog>//comment")] fn new(_blog: String, slug: String, user: User, conn: DbConn) -> Template { - let post = Post::find_by_slug(&*conn, slug).unwrap(); - Template::render("comments/new", json!({ - "post": post, - "account": user - })) + may_fail!(Post::find_by_slug(&*conn, slug), "Couldn't find this post", |post| { + Template::render("comments/new", json!({ + "post": post, + "account": user + })) + }) } #[get("/~///comment", rank=2)] @@ -53,6 +54,7 @@ fn create(blog: String, slug: String, query: CommentQuery, data: Form Template { + Template::render("errors/404", json!({ + "error_message": "Page not found" + })) +} + +#[catch(500)] +fn server_error() -> Template { + Template::render("errors/500", json!({ + "error_message": "Server error" + })) +} diff --git a/src/routes/instance.rs b/src/routes/instance.rs index f366a398..be1deb42 100644 --- a/src/routes/instance.rs +++ b/src/routes/instance.rs @@ -22,19 +22,7 @@ fn index(conn: DbConn, user: Option) -> Template { Template::render("instance/index", json!({ "instance": inst, "account": user, - "recents": recents.into_iter().map(|p| { - json!({ - "post": p, - "author": ({ - let author = &p.get_authors(&*conn)[0]; - let mut json = serde_json::to_value(author).unwrap(); - json["fqn"] = serde_json::Value::String(author.get_fqn(&*conn)); - json - }), - "url": format!("/~/{}/{}/", p.get_blog(&*conn).actor_id, p.slug), - "date": p.creation_date.timestamp() - }) - }).collect::>() + "recents": recents.into_iter().map(|p| p.to_json(&*conn)).collect::>() })) } None => { @@ -58,11 +46,11 @@ struct NewInstanceForm { #[post("/configure", data = "")] fn post_config(conn: DbConn, data: Form) -> Redirect { let form = data.get(); - let inst = Instance::insert( - &*conn, - BASE_URL.as_str().to_string(), - form.name.to_string(), - true); + let inst = Instance::insert(&*conn, NewInstance { + public_domain: BASE_URL.as_str().to_string(), + name: form.name.to_string(), + local: true + }); if inst.has_admin(&*conn) { Redirect::to("/") } else { diff --git a/src/routes/likes.rs b/src/routes/likes.rs index f99782ec..57bae6f6 100644 --- a/src/routes/likes.rs +++ b/src/routes/likes.rs @@ -1,6 +1,6 @@ use rocket::response::{Redirect, Flash}; -use activity_pub::broadcast; +use activity_pub::{broadcast, IntoId, inbox::Notify}; use db_conn::DbConn; use models::{ likes, @@ -16,15 +16,16 @@ fn create(blog: String, slug: String, user: User, conn: DbConn) -> Redirect { if !user.has_liked(&*conn, &post) { let like = likes::Like::insert(&*conn, likes::NewLike { - post_id: post.id, - user_id: user.id, - ap_url: "".to_string() + post_id: post.id, + user_id: user.id, + ap_url: "".to_string() }); like.update_ap_url(&*conn); + likes::Like::notify(&*conn, like.into_activity(&*conn), user.clone().into_id()); broadcast(&*conn, &user, like.into_activity(&*conn), user.get_followers(&*conn)); } else { - let like = likes::Like::find_by_user_on_post(&*conn, &user, &post).unwrap(); + 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)); } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 501c774c..6c33f75b 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,8 +1,35 @@ use rocket::response::NamedFile; use std::path::{Path, PathBuf}; +macro_rules! may_fail { + ($expr:expr, $template:expr, $msg:expr, | $res:ident | $block:block) => { + { + let res = $expr; + if res.is_some() { + let $res = res.unwrap(); + $block + } else { + Template::render(concat!("errors/", $template), json!({ + "error_message": $msg + })) + } + } + }; + ($expr:expr, $msg:expr, | $res:ident | $block:block) => { + may_fail!($expr, "404", $msg, |$res| { + $block + }) + }; + ($expr:expr, | $res:ident | $block:block) => { + may_fail!($expr, "", |$res| { + $block + }) + }; +} + pub mod blogs; pub mod comments; +pub mod errors; pub mod instance; pub mod likes; pub mod notifications; diff --git a/src/routes/posts.rs b/src/routes/posts.rs index fef302ab..4fd02666 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -19,38 +19,24 @@ use safe_string::SafeString; #[get("/~//", rank = 4)] fn details(blog: String, slug: String, conn: DbConn, user: Option) -> Template { - let blog = Blog::find_by_fqn(&*conn, blog).unwrap(); - let post = Post::find_by_slug(&*conn, slug).unwrap(); - let comments = Comment::find_by_post(&*conn, post.id); + may_fail!(Blog::find_by_fqn(&*conn, blog), "Couldn't find this blog", |blog| { + may_fail!(Post::find_by_slug(&*conn, slug), "Couldn't find this post", |post| { + let comments = Comment::find_by_post(&*conn, post.id); - Template::render("posts/details", json!({ - "author": ({ - let author = &post.get_authors(&*conn)[0]; - let mut json = serde_json::to_value(author).unwrap(); - json["fqn"] = serde_json::Value::String(author.get_fqn(&*conn)); - json - }), - "post": post, - "blog": blog, - "comments": comments.into_iter().map(|c| { - json!({ - "id": c.id, - "content": c.content, - "author": ({ - let author = &c.get_author(&*conn); - let mut json = serde_json::to_value(author).unwrap(); - json["fqn"] = serde_json::Value::String(author.get_fqn(&*conn)); - json - }) - }) - }).collect::>(), - "n_likes": post.get_likes(&*conn).len(), - "has_liked": user.clone().map(|u| u.has_liked(&*conn, &post)).unwrap_or(false), - "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() - })) + Template::render("posts/details", json!({ + "author": post.get_authors(&*conn)[0].to_json(&*conn), + "post": post, + "blog": blog, + "comments": comments.into_iter().map(|c| c.to_json(&*conn)).collect::>(), + "n_likes": post.get_likes(&*conn).len(), + "has_liked": user.clone().map(|u| u.has_liked(&*conn, &post)).unwrap_or(false), + "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() + })) + }) + }) } #[get("/~/<_blog>/", rank = 3, format = "application/activity+json")] diff --git a/src/routes/reshares.rs b/src/routes/reshares.rs index 50698645..5365af48 100644 --- a/src/routes/reshares.rs +++ b/src/routes/reshares.rs @@ -1,6 +1,6 @@ use rocket::response::{Redirect, Flash}; -use activity_pub::broadcast; +use activity_pub::{broadcast, IntoId, inbox::Notify}; use db_conn::DbConn; use models::{ posts::Post, @@ -22,9 +22,10 @@ fn create(blog: String, slug: String, user: User, conn: DbConn) -> Redirect { }); reshare.update_ap_url(&*conn); + Reshare::notify(&*conn, reshare.into_activity(&*conn), user.clone().into_id()); broadcast(&*conn, &user, reshare.into_activity(&*conn), user.get_followers(&*conn)); } else { - let reshare = Reshare::find_by_user_on_post(&*conn, &user, &post).unwrap(); + 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)); } diff --git a/src/routes/user.rs b/src/routes/user.rs index 2bc25ec6..6b925c87 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -10,7 +10,7 @@ use serde_json; use activity_pub::{ activity_pub, ActivityPub, ActivityStream, context, broadcast, Id, IntoId, - inbox::Inbox, + inbox::{Inbox, Notify}, actor::Actor }; use db_conn::DbConn; @@ -25,7 +25,7 @@ use models::{ use utils; #[get("/me")] -fn me(user: Option) -> Result> { +fn me(user: Option) -> Result> { match user { Some(user) => Ok(Redirect::to(format!("/@/{}/", user.username))), None => Err(utils::requires_login("", "/me")) @@ -34,48 +34,24 @@ fn me(user: Option) -> Result> { #[get("/@/", rank = 2)] fn details(name: String, conn: DbConn, account: Option) -> Template { - let user = User::find_by_fqn(&*conn, name).unwrap(); - let recents = Post::get_recents_for_author(&*conn, &user, 6); - let reshares = Reshare::get_recents_for_author(&*conn, &user, 6); - let user_id = user.id.clone(); - let n_followers = user.get_followers(&*conn).len(); + may_fail!(User::find_by_fqn(&*conn, name), "Couldn't find requested user", |user| { + let recents = Post::get_recents_for_author(&*conn, &user, 6); + let reshares = Reshare::get_recents_for_author(&*conn, &user, 6); + let user_id = user.id.clone(); + let n_followers = user.get_followers(&*conn).len(); - Template::render("users/details", json!({ - "user": serde_json::to_value(user.clone()).unwrap(), - "instance_url": user.get_instance(&*conn).public_domain, - "is_remote": user.instance_id != Instance::local_id(&*conn), - "follows": account.clone().map(|x| x.is_following(&*conn, user.id)).unwrap_or(false), - "account": account, - "recents": recents.into_iter().map(|p| { - json!({ - "post": p, - "author": ({ - let author = &p.get_authors(&*conn)[0]; - let mut json = serde_json::to_value(author).unwrap(); - json["fqn"] = serde_json::Value::String(author.get_fqn(&*conn)); - json - }), - "url": format!("/~/{}/{}/", p.get_blog(&*conn).actor_id, p.slug), - "date": p.creation_date.timestamp() - }) - }).collect::>(), - "reshares": reshares.into_iter().map(|r| { - let p = r.get_post(&*conn).unwrap(); - json!({ - "post": p, - "author": ({ - let author = &p.get_authors(&*conn)[0]; - let mut json = serde_json::to_value(author).unwrap(); - json["fqn"] = serde_json::Value::String(author.get_fqn(&*conn)); - json - }), - "url": format!("/~/{}/{}/", p.get_blog(&*conn).actor_id, p.slug), - "date": p.creation_date.timestamp() - }) - }).collect::>(), - "is_self": account.map(|a| a.id == user_id).unwrap_or(false), - "n_followers": n_followers - })) + Template::render("users/details", json!({ + "user": serde_json::to_value(user.clone()).unwrap(), + "instance_url": user.get_instance(&*conn).public_domain, + "is_remote": user.instance_id != Instance::local_id(&*conn), + "follows": account.clone().map(|x| x.is_following(&*conn, user.id)).unwrap_or(false), + "account": account, + "recents": recents.into_iter().map(|p| p.to_json(&*conn)).collect::>(), + "reshares": reshares.into_iter().map(|r| r.get_post(&*conn).unwrap().to_json(&*conn)).collect::>(), + "is_self": account.map(|a| a.id == user_id).unwrap_or(false), + "n_followers": n_followers + })) + }) } #[get("/dashboard")] @@ -103,6 +79,8 @@ fn follow(name: String, conn: DbConn, user: User) -> Redirect { act.follow_props.set_actor_link::(user.clone().into_id()).unwrap(); 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(); + + follows::Follow::notify(&*conn, act.clone(), user.clone().into_id()); broadcast(&*conn, &user, act, vec![target]); Redirect::to(format!("/@/{}/", name)) } @@ -114,24 +92,20 @@ fn follow_auth(name: String) -> Flash { #[get("/@//followers", rank = 2)] fn followers(name: String, conn: DbConn, account: Option) -> Template { - let user = User::find_by_fqn(&*conn, name.clone()).unwrap(); - let user_id = user.id.clone(); + may_fail!(User::find_by_fqn(&*conn, name.clone()), "Couldn't find requested user", |user| { + let user_id = user.id.clone(); - Template::render("users/followers", json!({ - "user": serde_json::to_value(user.clone()).unwrap(), - "instance_url": user.get_instance(&*conn).public_domain, - "is_remote": user.instance_id != Instance::local_id(&*conn), - "follows": account.clone().map(|x| x.is_following(&*conn, user.id)).unwrap_or(false), - "followers": user.get_followers(&*conn).into_iter().map(|f| { - let fqn = f.get_fqn(&*conn); - let mut json = serde_json::to_value(f).unwrap(); - json["fqn"] = serde_json::Value::String(fqn); - json - }).collect::>(), - "account": account, - "is_self": account.map(|a| a.id == user_id).unwrap_or(false), - "n_followers": user.get_followers(&*conn).len() - })) + Template::render("users/followers", json!({ + "user": serde_json::to_value(user.clone()).unwrap(), + "instance_url": user.get_instance(&*conn).public_domain, + "is_remote": user.instance_id != Instance::local_id(&*conn), + "follows": account.clone().map(|x| x.is_following(&*conn, user.id)).unwrap_or(false), + "followers": user.get_followers(&*conn).into_iter().map(|f| f.to_json(&*conn)).collect::>(), + "account": account, + "is_self": account.map(|a| a.id == user_id).unwrap_or(false), + "n_followers": user.get_followers(&*conn).len() + })) + }) } #[get("/@/", format = "application/activity+json", rank = 1)] diff --git a/src/routes/well_known.rs b/src/routes/well_known.rs index 15b3ad2a..ec9710a2 100644 --- a/src/routes/well_known.rs +++ b/src/routes/well_known.rs @@ -1,8 +1,10 @@ use rocket::http::ContentType; use rocket::response::Content; +use serde_json; +use webfinger::*; use BASE_URL; -use activity_pub::{ap_url, webfinger::Webfinger}; +use activity_pub::ap_url; use db_conn::DbConn; use models::{blogs::Blog, users::User}; @@ -33,29 +35,32 @@ struct WebfingerQuery { resource: String } -#[get("/.well-known/webfinger?")] -fn webfinger(query: WebfingerQuery, conn: DbConn) -> Content> { - let mut parsed_query = query.resource.splitn(2, ":"); - let res_type = parsed_query.next().unwrap(); - let res = parsed_query.next().unwrap(); - if res_type == "acct" { - let mut parsed_res = res.split("@"); - let user = parsed_res.next().unwrap(); - let res_dom = parsed_res.next().unwrap(); +struct WebfingerResolver; - if res_dom == BASE_URL.as_str() { - let res = match User::find_local(&*conn, String::from(user)) { - Some(usr) => Ok(usr.webfinger(&*conn)), - None => match Blog::find_local(&*conn, String::from(user)) { - Some(blog) => Ok(blog.webfinger(&*conn)), - None => Err("Requested actor not found") - } - }; - Content(ContentType::new("application", "jrd+json"), res) - } else { - Content(ContentType::new("text", "plain"), Err("Invalid instance")) +impl Resolver for WebfingerResolver { + fn instance_domain<'a>() -> &'a str { + BASE_URL.as_str() + } + + fn find(acct: String, conn: DbConn) -> Result { + match User::find_local(&*conn, acct.clone()) { + Some(usr) => Ok(usr.webfinger(&*conn)), + None => match Blog::find_local(&*conn, acct) { + Some(blog) => Ok(blog.webfinger(&*conn)), + None => Err(ResolverError::NotFound) + } } - } else { - Content(ContentType::new("text", "plain"), Err("Invalid resource type. Only acct is supported")) + } +} + +#[get("/.well-known/webfinger?")] +fn webfinger(query: WebfingerQuery, conn: DbConn) -> Content { + match WebfingerResolver::endpoint(query.resource, conn).and_then(|wf| serde_json::to_string(&wf).map_err(|_| ResolverError::NotFound)) { + Ok(wf) => Content(ContentType::new("application", "jrd+json"), wf), + Err(err) => Content(ContentType::new("text", "plain"), String::from(match err { + ResolverError::InvalidResource => "Invalid resource. Make sure to request an acct: URI", + ResolverError::NotFound => "Requested resource was not found", + ResolverError::WrongInstance => "This is not the instance of the requested resource" + })) } } diff --git a/templates/errors/404.html.tera b/templates/errors/404.html.tera new file mode 100644 index 00000000..413374a3 --- /dev/null +++ b/templates/errors/404.html.tera @@ -0,0 +1,6 @@ +{% extends "errors/base" %} + +{% block error %} +

{{ "We couldn't find this page." | _ }}

+

{{ "The link that led you here may be broken." | _ }}

+{% endblock error %} diff --git a/templates/macros.html.tera b/templates/macros.html.tera index a8606881..4d50fa97 100644 --- a/templates/macros.html.tera +++ b/templates/macros.html.tera @@ -7,7 +7,7 @@

{{ article.post.title }}

{{ article.post.content | striptags | truncate(length=200) }}

+

{{ article.post.content | safe | striptags | truncate(length=200) }}

{{ "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name }}{{ link_4 }}" | _(