This commit is contained in:
Matthieu 2018-06-19 13:22:26 +02:00
commit 2dfe8fad22
34 changed files with 451 additions and 557 deletions

19
Cargo.lock generated
View File

@ -1014,12 +1014,13 @@ dependencies = [
"rocket 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)", "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_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_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 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)",
"tera 0.11.7 (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)", "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]] [[package]]
@ -1273,7 +1274,7 @@ dependencies = [
[[package]] [[package]]
name = "rocket_i18n" name = "rocket_i18n"
version = "0.1.1" 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 = [ dependencies = [
"gettext-rs 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "rocket 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)",
@ -1913,6 +1914,17 @@ name = "void"
version = "1.0.2" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "winapi" name = "winapi"
version = "0.2.8" 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)" = "<none>" "checksum rocket_codegen_next 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)" = "<none>"
"checksum rocket_contrib 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)" = "<none>" "checksum rocket_contrib 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)" = "<none>"
"checksum rocket_http 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)" = "<none>" "checksum rocket_http 0.4.0-dev (git+https://github.com/SergioBenitez/Rocket?rev=df7111143e466c18d1f56377a8d9530a5a306aba)" = "<none>"
"checksum rocket_i18n 0.1.1 (git+https://github.com/BaptisteGelez/rocket_i18n?rev=457b88c59ec31905a9193df43df58bee55b4b83d)" = "<none>" "checksum rocket_i18n 0.1.1 (git+https://github.com/BaptisteGelez/rocket_i18n?rev=5b4225d5bed5769482dc926a7e6d6b79f1217be6)" = "<none>"
"checksum rustc-demangle 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11fb43a206a04116ffd7cfcf9bcb941f8eb6cc7ff667272246b0a1c74259a3cb" "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 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" "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 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 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 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.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 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" "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"

View File

@ -24,6 +24,7 @@ serde_derive = "1.0"
serde_json = "1.0" serde_json = "1.0"
tera = "0.11" tera = "0.11"
url = "1.7" url = "1.7"
webfinger = "0.1"
[dependencies.chrono] [dependencies.chrono]
features = ["serde"] features = ["serde"]
@ -48,4 +49,4 @@ rev = "df7111143e466c18d1f56377a8d9530a5a306aba"
[dependencies.rocket_i18n] [dependencies.rocket_i18n]
git = "https://github.com/BaptisteGelez/rocket_i18n" git = "https://github.com/BaptisteGelez/rocket_i18n"
rev = "457b88c59ec31905a9193df43df58bee55b4b83d" rev = "5b4225d5bed5769482dc926a7e6d6b79f1217be6"

View File

@ -253,3 +253,24 @@ msgstr ""
msgid "You need to be logged in order to edit your profile" msgid "You need to be logged in order to edit your profile"
msgstr "" 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 ""

View File

@ -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" msgid "You need to be logged in order to edit your profile"
msgstr "Vous devez vous connecter pour modifier votre profil" 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 ""

View File

@ -10,7 +10,8 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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" msgid "Latest articles"
msgstr "Najnowsze artykuły" msgstr "Najnowsze artykuły"
@ -46,7 +47,8 @@ msgid "Something broke on our side."
msgstr "Coś poszło nie tak." msgstr "Coś poszło nie tak."
msgid "Sorry about that. If you think this is a bug, please report it." 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" msgid "Configuration"
msgstr "Konfiguracja" msgstr "Konfiguracja"
@ -114,8 +116,8 @@ msgstr "Utwórz wpis"
msgid "Publish" msgid "Publish"
msgstr "Opublikuj" msgstr "Opublikuj"
msgid "Logowanie" msgid "Login"
msgstr "Zaloguj się" msgstr ""
msgid "Username or email" msgid "Username or email"
msgstr "Nazwa użytkownika lub adres e-mail" msgstr "Nazwa użytkownika lub adres e-mail"
@ -133,7 +135,9 @@ msgid "Your Blogs"
msgstr "Twoje blogi" msgstr "Twoje blogi"
msgid "You don't have any blog yet. Create your own, or ask to join one." 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" msgid "Start a new blog"
msgstr "Utwórz nowy 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" msgid "You need to be logged in order to edit your profile"
msgstr "Musisz się zalogować , aby móc edytować swój profil" 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ę"

View File

@ -263,3 +263,9 @@ msgstr ""
msgid "{{ data }} commented your article" msgid "{{ data }} commented your article"
msgstr "" msgstr ""
msgid "We couldn't find this page."
msgstr ""
msgid "The link that led you here may be broken."
msgstr ""

View File

@ -1,6 +1,6 @@
use activitypub::{ use activitypub::{
Object, Object,
activity::{Create, Like, Undo} activity::{Announce, Create, Like, Undo}
}; };
use diesel::PgConnection; use diesel::PgConnection;
use failure::Error; use failure::Error;
@ -90,6 +90,10 @@ pub trait Inbox {
likes::Like::delete_activity(conn, Id::new(act.undo_props.object_object::<Like>()?.object_props.id_string()?)); likes::Like::delete_activity(conn, Id::new(act.undo_props.object_object::<Like>()?.object_props.id_string()?));
Ok(()) Ok(())
}, },
"Announce" => {
Reshare::delete_activity(conn, Id::new(act.undo_props.object_object::<Announce>()?.object_props.id_string()?));
Ok(())
}
_ => Err(InboxError::CantUndo)? _ => Err(InboxError::CantUndo)?
} }
} }

View File

@ -17,7 +17,6 @@ pub mod inbox;
pub mod object; pub mod object;
pub mod request; pub mod request;
pub mod sign; pub mod sign;
pub mod webfinger;
pub type ActivityPub = Content<Json<serde_json::Value>>; pub type ActivityPub = Content<Json<serde_json::Value>>;
@ -76,18 +75,18 @@ impl<'r, O: Object> Responder<'r> for ActivityStream<O> {
} }
} }
pub fn broadcast<A: Activity + Clone, S: sign::Signer, T: inbox::WithInbox + Actor>(conn: &PgConnection, sender: &S, act: A, to: Vec<T>) { pub fn broadcast<A: Activity, S: sign::Signer, T: inbox::WithInbox + Actor>(conn: &PgConnection, sender: &S, act: A, to: Vec<T>) {
let boxes = to.into_iter() let boxes = to.into_iter()
.map(|u| u.get_shared_inbox_url().unwrap_or(u.get_inbox_url())) .map(|u| u.get_shared_inbox_url().unwrap_or(u.get_inbox_url()))
.collect::<Vec<String>>() .collect::<Vec<String>>()
.unique(); .unique();
for inbox in boxes {
// TODO: run it in Sidekiq or something like that
let mut act = serde_json::to_value(act.clone()).unwrap(); let mut act = serde_json::to_value(act).unwrap();
act["@context"] = context(); act["@context"] = context();
let signed = act.sign(sender, conn); let signed = act.sign(sender, conn);
for inbox in boxes {
// TODO: run it in Sidekiq or something like that
let res = Client::new() let res = Client::new()
.post(&inbox[..]) .post(&inbox[..])
.headers(request::headers()) .headers(request::headers())

View File

@ -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<String>;
fn webfinger_links(&self, conn: &PgConnection) -> Vec<Vec<(String, String)>>;
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::<Vec<serde_json::Value>>()
}).to_string()
}
}
pub fn resolve(acct: String) -> Result<String, String> {
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::<Mime>().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))
}

View File

@ -33,6 +33,7 @@ extern crate serde_derive;
extern crate serde_json; extern crate serde_json;
extern crate tera; extern crate tera;
extern crate url; extern crate url;
extern crate webfinger;
use diesel::{pg::PgConnection, r2d2::{ConnectionManager, Pool}}; use diesel::{pg::PgConnection, r2d2::{ConnectionManager, Pool}};
use dotenv::dotenv; use dotenv::dotenv;
@ -128,6 +129,10 @@ fn main() {
routes::well_known::nodeinfo, routes::well_known::nodeinfo,
routes::well_known::webfinger routes::well_known::webfinger
]) ])
.catch(catchers![
routes::errors::not_found,
routes::errors::server_error
])
.manage(init_pool()) .manage(init_pool())
.attach(Template::custom(|engines| { .attach(Template::custom(|engines| {
rocket_i18n::tera(&mut engines.tera); rocket_i18n::tera(&mut engines.tera);

View File

@ -19,18 +19,6 @@ pub struct NewBlogAuthor {
} }
impl BlogAuthor { impl BlogAuthor {
pub fn insert (conn: &PgConnection, new: NewBlogAuthor) -> BlogAuthor { insert!(blog_authors, NewBlogAuthor);
diesel::insert_into(blog_authors::table) get!(blog_authors);
.values(new)
.get_result(conn)
.expect("Error saving new blog author")
}
pub fn get(conn: &PgConnection, id: i32) -> Option<BlogAuthor> {
blog_authors::table.filter(blog_authors::id.eq(id))
.limit(1)
.load::<BlogAuthor>(conn)
.expect("Error loading blog author by id")
.into_iter().nth(0)
}
} }

View File

@ -14,15 +14,15 @@ use openssl::{
rsa::Rsa, rsa::Rsa,
sign::Signer sign::Signer
}; };
use webfinger::*;
use activity_pub::{ use activity_pub::{
ActivityStream, Id, IntoId, ActivityStream, Id, IntoId,
actor::{Actor as APActor, ActorType}, actor::{Actor as APActor, ActorType},
inbox::WithInbox, inbox::WithInbox,
sign, sign
webfinger::*
}; };
use models::instance::Instance; use models::instance::*;
use schema::blogs; use schema::blogs;
@ -56,20 +56,8 @@ pub struct NewBlog {
} }
impl Blog { impl Blog {
pub fn insert (conn: &PgConnection, new: NewBlog) -> Blog { insert!(blogs, NewBlog);
diesel::insert_into(blogs::table) get!(blogs);
.values(new)
.get_result(conn)
.expect("Error saving new blog")
}
pub fn get(conn: &PgConnection, id: i32) -> Option<Blog> {
blogs::table.filter(blogs::id.eq(id))
.limit(1)
.load::<Blog>(conn)
.expect("Error loading blog by id")
.into_iter().nth(0)
}
pub fn find_for_author(conn: &PgConnection, author_id: i32) -> Vec<Blog> { pub fn find_for_author(conn: &PgConnection, author_id: i32) -> Vec<Blog> {
use schema::blog_authors; use schema::blog_authors;
@ -79,14 +67,7 @@ impl Blog {
.expect("Couldn't load blogs ") .expect("Couldn't load blogs ")
} }
pub fn find_by_name(conn: &PgConnection, name: String, instance_id: i32) -> Option<Blog> { find_by!(blogs, find_by_name, actor_id as String, instance_id as i32);
blogs::table.filter(blogs::actor_id.eq(name))
.filter(blogs::instance_id.eq(instance_id))
.limit(1)
.load::<Blog>(conn)
.expect("Error loading blog by name")
.into_iter().nth(0)
}
pub fn find_local(conn: &PgConnection, name: String) -> Option<Blog> { pub fn find_local(conn: &PgConnection, name: String) -> Option<Blog> {
Blog::find_by_name(conn, name, Instance::local_id(conn)) 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<Blog> { fn fetch_from_webfinger(conn: &PgConnection, acct: String) -> Option<Blog> {
match resolve(acct.clone()) { 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) => { Err(details) => {
println!("{}", details); println!("{:?}", details);
None None
} }
} }
@ -136,7 +117,11 @@ impl Blog {
let instance = match Instance::find_by_domain(conn, inst.clone()) { let instance = match Instance::find_by_domain(conn, inst.clone()) {
Some(instance) => instance, Some(instance) => instance,
None => { 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 { Blog::insert(conn, NewBlog {
@ -186,6 +171,30 @@ impl Blog {
pub fn get_keypair(&self) -> PKey<Private> { pub fn get_keypair(&self) -> PKey<Private> {
PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.clone().unwrap().as_ref()).unwrap()).unwrap() 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 { 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<String> {
vec![self.compute_id(conn)]
}
fn webfinger_links(&self, conn: &PgConnection) -> Vec<Vec<(String, String)>> {
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 { impl sign::Signer for Blog {
fn get_key_id(&self, conn: &PgConnection) -> String { fn get_key_id(&self, conn: &PgConnection) -> String {
format!("{}#main-key", self.compute_id(conn)) format!("{}#main-key", self.compute_id(conn))

View File

@ -47,34 +47,10 @@ pub struct NewComment {
} }
impl Comment { impl Comment {
pub fn insert (conn: &PgConnection, new: NewComment) -> Comment { insert!(comments, NewComment);
diesel::insert_into(comments::table) get!(comments);
.values(new) find_by!(comments, find_by_post, post_id as i32);
.get_result(conn) find_by!(comments, find_by_ap_url, ap_url as String);
.expect("Error saving new comment")
}
pub fn get(conn: &PgConnection, id: i32) -> Option<Comment> {
comments::table.filter(comments::id.eq(id))
.limit(1)
.load::<Comment>(conn)
.expect("Error loading comment by id")
.into_iter().nth(0)
}
pub fn find_by_post(conn: &PgConnection, post_id: i32) -> Vec<Comment> {
comments::table.filter(comments::post_id.eq(post_id))
.load::<Comment>(conn)
.expect("Error loading comment by post id")
}
pub fn find_by_ap_url(conn: &PgConnection, ap_url: String) -> Option<Comment> {
comments::table.filter(comments::ap_url.eq(ap_url))
.limit(1)
.load::<Comment>(conn)
.expect("Error loading comment by AP URL")
.into_iter().nth(0)
}
pub fn get_author(&self, conn: &PgConnection) -> User { pub fn get_author(&self, conn: &PgConnection) -> User {
User::get(conn, self.author_id).unwrap() User::get(conn, self.author_id).unwrap()
@ -123,6 +99,12 @@ impl Comment {
.expect("Couldn't load local comments") .expect("Couldn't load local comments")
.len() .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<Note> for Comment { impl FromActivity<Note> for Comment {

View File

@ -25,20 +25,8 @@ pub struct NewFollow {
} }
impl Follow { impl Follow {
pub fn insert(conn: &PgConnection, new: NewFollow) -> Follow { insert!(follows, NewFollow);
diesel::insert_into(follows::table) get!(follows);
.values(new)
.get_result(conn)
.expect("Unable to insert new follow")
}
pub fn get(conn: &PgConnection, id: i32) -> Option<Follow> {
follows::table.filter(follows::id.eq(id))
.limit(1)
.load::<Follow>(conn)
.expect("Unable to load follow by id")
.into_iter().nth(0)
}
pub fn accept_follow<A: Signer + IntoId + Clone, B: Clone + WithInbox + Actor>( pub fn accept_follow<A: Signer + IntoId + Clone, B: Clone + WithInbox + Actor>(
conn: &PgConnection, conn: &PgConnection,

View File

@ -44,32 +44,9 @@ impl Instance {
Instance::get_local(conn).unwrap().id Instance::get_local(conn).unwrap().id
} }
pub fn insert<'a>(conn: &PgConnection, pub_dom: String, name: String, local: bool) -> Instance { insert!(instances, NewInstance);
diesel::insert_into(instances::table) get!(instances);
.values(NewInstance { find_by!(instances, find_by_domain, public_domain as String);
public_domain: pub_dom,
name: name,
local: local
})
.get_result(conn)
.expect("Error saving new instance")
}
pub fn get(conn: &PgConnection, id: i32) -> Option<Instance> {
instances::table.filter(instances::id.eq(id))
.limit(1)
.load::<Instance>(conn)
.expect("Error loading local instance infos")
.into_iter().nth(0)
}
pub fn find_by_domain(conn: &PgConnection, domain: String) -> Option<Instance> {
instances::table.filter(instances::public_domain.eq(domain))
.limit(1)
.load::<Instance>(conn)
.expect("Couldn't load instance by domain")
.into_iter().nth(0)
}
pub fn block(&self) { pub fn block(&self) {
unimplemented!() unimplemented!()
@ -86,7 +63,7 @@ impl Instance {
impl Inbox for Instance { impl Inbox for Instance {
fn received(&self, conn: &PgConnection, act: serde_json::Value) { 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 // TODO: add to stream, or whatever needs to be done
} }

View File

@ -35,12 +35,10 @@ pub struct NewLike {
} }
impl Like { impl Like {
pub fn insert(conn: &PgConnection, new: NewLike) -> Like { insert!(likes, NewLike);
diesel::insert_into(likes::table) get!(likes);
.values(new) find_by!(likes, find_by_ap_url, ap_url as String);
.get_result(conn) find_by!(likes, find_by_user_on_post, user_id as i32, post_id as i32);
.expect("Unable to insert new like")
}
pub fn update_ap_url(&self, conn: &PgConnection) { pub fn update_ap_url(&self, conn: &PgConnection) {
if self.ap_url.len() == 0 { if self.ap_url.len() == 0 {
@ -50,31 +48,6 @@ impl Like {
} }
} }
pub fn get(conn: &PgConnection, id: i32) -> Option<Like> {
likes::table.filter(likes::id.eq(id))
.limit(1)
.load::<Like>(conn)
.expect("Error loading like by ID")
.into_iter().nth(0)
}
pub fn find_by_ap_url(conn: &PgConnection, ap_url: String) -> Option<Like> {
likes::table.filter(likes::ap_url.eq(ap_url))
.limit(1)
.load::<Like>(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<Like> {
likes::table.filter(likes::post_id.eq(post.id))
.filter(likes::user_id.eq(user.id))
.limit(1)
.load::<Like>(conn)
.expect("Error loading like for user and post")
.into_iter().nth(0)
}
pub fn delete(&self, conn: &PgConnection) -> activity::Undo { pub fn delete(&self, conn: &PgConnection) -> activity::Undo {
diesel::delete(self).execute(conn).unwrap(); diesel::delete(self).execute(conn).unwrap();

View File

@ -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<Self> {
$table::table
$(.filter($table::$col.eq($col)))+
.limit(1)
.load::<Self>(conn)
.expect("Error loading $table by $col")
.into_iter().nth(0)
}
};
}
macro_rules! get {
($table:ident) => {
pub fn get(conn: &PgConnection, id: i32) -> Option<Self> {
$table::table.filter($table::id.eq(id))
.limit(1)
.load::<Self>(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 blog_authors;
pub mod blogs; pub mod blogs;
pub mod comments; pub mod comments;

View File

@ -26,20 +26,8 @@ pub struct NewNotification {
} }
impl Notification { impl Notification {
pub fn insert(conn: &PgConnection, new: NewNotification) -> Notification { insert!(notifications, NewNotification);
diesel::insert_into(notifications::table) get!(notifications);
.values(new)
.get_result(conn)
.expect("Couldn't save notification")
}
pub fn get(conn: &PgConnection, id: i32) -> Option<Notification> {
notifications::table.filter(notifications::id.eq(id))
.limit(1)
.load::<Notification>(conn)
.expect("Couldn't load notification by ID")
.into_iter().nth(0)
}
pub fn find_for_user(conn: &PgConnection, user: &User) -> Vec<Notification> { pub fn find_for_user(conn: &PgConnection, user: &User) -> Vec<Notification> {
notifications::table.filter(notifications::user_id.eq(user.id)) notifications::table.filter(notifications::user_id.eq(user.id))

View File

@ -23,18 +23,6 @@ pub struct NewPostAuthor {
} }
impl PostAuthor { impl PostAuthor {
pub fn insert (conn: &PgConnection, new: NewPostAuthor) -> PostAuthor { insert!(post_authors, NewPostAuthor);
diesel::insert_into(post_authors::table) get!(post_authors);
.values(new)
.get_result(conn)
.expect("Error saving new blog author")
}
pub fn get(conn: &PgConnection, id: i32) -> Option<PostAuthor> {
post_authors::table.filter(post_authors::id.eq(id))
.limit(1)
.load::<PostAuthor>(conn)
.expect("Error loading blog author by id")
.into_iter().nth(0)
}
} }

View File

@ -50,20 +50,10 @@ pub struct NewPost {
} }
impl Post { impl Post {
pub fn insert(conn: &PgConnection, new: NewPost) -> Post { insert!(posts, NewPost);
diesel::insert_into(posts::table) get!(posts);
.values(new) find_by!(posts, find_by_slug, slug as String);
.get_result(conn) find_by!(posts, find_by_ap_url, ap_url as String);
.expect("Error saving new post")
}
pub fn get(conn: &PgConnection, id: i32) -> Option<Post> {
posts::table.filter(posts::id.eq(id))
.limit(1)
.load::<Post>(conn)
.expect("Error loading post by id")
.into_iter().nth(0)
}
pub fn count_local(conn: &PgConnection) -> usize { pub fn count_local(conn: &PgConnection) -> usize {
use schema::post_authors; use schema::post_authors;
@ -76,22 +66,6 @@ impl Post {
.len() .len()
} }
pub fn find_by_slug(conn: &PgConnection, slug: String) -> Option<Post> {
posts::table.filter(posts::slug.eq(slug))
.limit(1)
.load::<Post>(conn)
.expect("Error loading post by slug")
.into_iter().nth(0)
}
pub fn find_by_ap_url(conn: &PgConnection, ap_url: String) -> Option<Post> {
posts::table.filter(posts::ap_url.eq(ap_url))
.limit(1)
.load::<Post>(conn)
.expect("Error loading post by AP URL")
.into_iter().nth(0)
}
pub fn get_recents(conn: &PgConnection, limit: i64) -> Vec<Post> { pub fn get_recents(conn: &PgConnection, limit: i64) -> Vec<Post> {
posts::table.order(posts::creation_date.desc()) posts::table.order(posts::creation_date.desc())
.limit(limit) .limit(limit)
@ -194,6 +168,15 @@ impl Post {
act.create_props.set_object_object(self.into_activity(conn)).unwrap(); act.create_props.set_object_object(self.into_activity(conn)).unwrap();
act 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<Article> for Post { impl FromActivity<Article> for Post {

View File

@ -2,7 +2,7 @@ use activitypub::activity::{Announce, Undo};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods}; use diesel::{self, PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods};
use activity_pub::{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 models::{notifications::*, posts::Post, users::User};
use schema::reshares; use schema::reshares;
@ -24,20 +24,10 @@ pub struct NewReshare {
} }
impl Reshare { impl Reshare {
pub fn insert(conn: &PgConnection, new: NewReshare) -> Reshare { insert!(reshares, NewReshare);
diesel::insert_into(reshares::table) get!(reshares);
.values(new) find_by!(reshares, find_by_ap_url, ap_url as String);
.get_result(conn) find_by!(reshares, find_by_user_on_post, user_id as i32, post_id as i32);
.expect("Couldn't save reshare")
}
pub fn get(conn: &PgConnection, id: i32) -> Option<Reshare> {
reshares::table.filter(reshares::id.eq(id))
.limit(1)
.load::<Reshare>(conn)
.expect("Could'nt load reshare")
.into_iter().nth(0)
}
pub fn update_ap_url(&self, conn: &PgConnection) { pub fn update_ap_url(&self, conn: &PgConnection) {
if self.ap_url.len() == 0 { if self.ap_url.len() == 0 {
@ -51,23 +41,6 @@ impl Reshare {
} }
} }
pub fn find_by_ap_url(conn: &PgConnection, ap_url: String) -> Option<Reshare> {
reshares::table.filter(reshares::ap_url.eq(ap_url))
.limit(1)
.load::<Reshare>(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<Reshare> {
reshares::table.filter(reshares::post_id.eq(post.id))
.filter(reshares::user_id.eq(user.id))
.limit(1)
.load::<Reshare>(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<Reshare> { pub fn get_recents_for_author(conn: &PgConnection, user: &User, limit: i64) -> Vec<Reshare> {
reshares::table.filter(reshares::user_id.eq(user.id)) reshares::table.filter(reshares::user_id.eq(user.id))
.order(reshares::creation_date.desc()) .order(reshares::creation_date.desc())
@ -129,3 +102,14 @@ impl Notify<Announce> 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
}
}
}

View File

@ -24,21 +24,21 @@ use rocket::{
}; };
use serde_json; use serde_json;
use url::Url; use url::Url;
use webfinger::*;
use BASE_URL; use BASE_URL;
use activity_pub::{ use activity_pub::{
ap_url, ActivityStream, Id, IntoId, ap_url, ActivityStream, Id, IntoId,
actor::{ActorType, Actor as APActor}, actor::{ActorType, Actor as APActor},
inbox::{Inbox, WithInbox}, inbox::{Inbox, WithInbox},
sign::{Signer, gen_keypair}, sign::{Signer, gen_keypair}
webfinger::{Webfinger, resolve}
}; };
use db_conn::DbConn; use db_conn::DbConn;
use models::{ use models::{
blogs::Blog, blogs::Blog,
blog_authors::BlogAuthor, blog_authors::BlogAuthor,
follows::Follow, follows::Follow,
instance::Instance, instance::*,
post_authors::PostAuthor, post_authors::PostAuthor,
posts::Post posts::Post
}; };
@ -85,6 +85,8 @@ pub struct NewUser {
} }
impl User { impl User {
insert!(users, NewUser);
pub fn grant_admin_rights(&self, conn: &PgConnection) { pub fn grant_admin_rights(&self, conn: &PgConnection) {
diesel::update(self) diesel::update(self)
.set(users::is_admin.eq(true)) .set(users::is_admin.eq(true))
@ -92,13 +94,6 @@ impl User {
.expect("Couldn't grant admin rights"); .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 { pub fn update(&self, conn: &PgConnection, name: String, email: String, summary: String) -> User {
diesel::update(self) diesel::update(self)
.set(( .set((
@ -110,13 +105,7 @@ impl User {
.into_iter().nth(0).unwrap() .into_iter().nth(0).unwrap()
} }
pub fn get(conn: &PgConnection, id: i32) -> Option<User> { get!(users);
users::table.filter(users::id.eq(id))
.limit(1)
.load::<User>(conn)
.expect("Error loading user by id")
.into_iter().nth(0)
}
pub fn count_local(conn: &PgConnection) -> usize { pub fn count_local(conn: &PgConnection) -> usize {
users::table.filter(users::instance_id.eq(Instance::local_id(conn))) users::table.filter(users::instance_id.eq(Instance::local_id(conn)))
@ -125,22 +114,8 @@ impl User {
.len() .len()
} }
pub fn find_by_email(conn: &PgConnection, email: String) -> Option<User> { find_by!(users, find_by_email, email as String);
users::table.filter(users::email.eq(email)) find_by!(users, find_by_name, username as String, instance_id as i32);
.limit(1)
.load::<User>(conn)
.expect("Error loading user by email")
.into_iter().nth(0)
}
pub fn find_by_name(conn: &PgConnection, username: String, instance_id: i32) -> Option<User> {
users::table.filter(users::username.eq(username))
.filter(users::instance_id.eq(instance_id))
.limit(1)
.load::<User>(conn)
.expect("Error loading user by name")
.into_iter().nth(0)
}
pub fn find_local(conn: &PgConnection, username: String) -> Option<User> { pub fn find_local(conn: &PgConnection, username: String) -> Option<User> {
User::find_by_name(conn, username, Instance::local_id(conn)) 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<User> { fn fetch_from_webfinger(conn: &PgConnection, acct: String) -> Option<User> {
match resolve(acct.clone()) { 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) => { Err(details) => {
println!("{}", details); println!("{:?}", details);
None None
} }
} }
@ -190,7 +165,11 @@ impl User {
let instance = match Instance::find_by_domain(conn, inst.clone()) { let instance = match Instance::find_by_domain(conn, inst.clone()) {
Some(instance) => instance, Some(instance) => instance,
None => { 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 { User::insert(conn, NewUser {
@ -351,6 +330,36 @@ impl User {
}; };
actor 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 { 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<String> {
vec![self.compute_id(conn)]
}
fn webfinger_links(&self, conn: &PgConnection) -> Vec<Vec<(String, String)>> {
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 { impl Signer for User {
fn get_key_id(&self, conn: &PgConnection) -> String { fn get_key_id(&self, conn: &PgConnection) -> String {
format!("{}#main-key", self.compute_id(conn)) format!("{}#main-key", self.compute_id(conn))

View File

@ -19,26 +19,16 @@ use utils;
#[get("/~/<name>", rank = 2)] #[get("/~/<name>", rank = 2)]
fn details(name: String, conn: DbConn, user: Option<User>) -> Template { fn details(name: String, conn: DbConn, user: Option<User>) -> Template {
let blog = Blog::find_by_fqn(&*conn, name).unwrap(); 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); let recents = Post::get_recents_for_blog(&*conn, &blog, 5);
Template::render("blogs/details", json!({ Template::render("blogs/details", json!({
"blog": blog, "blog": blog,
"account": user, "account": user,
"is_author": user.map(|x| x.is_author_in(&*conn, blog)), "is_author": user.map(|x| x.is_author_in(&*conn, blog)),
"recents": recents.into_iter().map(|p| { "recents": recents.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>()
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::<Vec<serde_json::Value>>()
})) }))
})
} }
#[get("/~/<name>", format = "application/activity+json", rank = 1)] #[get("/~/<name>", format = "application/activity+json", rank = 1)]

View File

@ -4,7 +4,7 @@ use rocket::{
}; };
use rocket_contrib::Template; use rocket_contrib::Template;
use activity_pub::broadcast; use activity_pub::{broadcast, IntoId, inbox::Notify};
use db_conn::DbConn; use db_conn::DbConn;
use models::{ use models::{
comments::*, comments::*,
@ -17,11 +17,12 @@ use safe_string::SafeString;
#[get("/~/<_blog>/<slug>/comment")] #[get("/~/<_blog>/<slug>/comment")]
fn new(_blog: String, slug: String, user: User, conn: DbConn) -> Template { fn new(_blog: String, slug: String, user: User, conn: DbConn) -> Template {
let post = Post::find_by_slug(&*conn, slug).unwrap(); may_fail!(Post::find_by_slug(&*conn, slug), "Couldn't find this post", |post| {
Template::render("comments/new", json!({ Template::render("comments/new", json!({
"post": post, "post": post,
"account": user "account": user
})) }))
})
} }
#[get("/~/<blog>/<slug>/comment", rank=2)] #[get("/~/<blog>/<slug>/comment", rank=2)]
@ -53,6 +54,7 @@ fn create(blog: String, slug: String, query: CommentQuery, data: Form<NewComment
spoiler_text: "".to_string() spoiler_text: "".to_string()
}); });
Comment::notify(&*conn, comment.into_activity(&*conn), user.clone().into_id());
broadcast(&*conn, &user, comment.create_activity(&*conn), user.get_followers(&*conn)); broadcast(&*conn, &user, comment.create_activity(&*conn), user.get_followers(&*conn));
Redirect::to(format!("/~/{}/{}/#comment-{}", blog, slug, comment.id)) Redirect::to(format!("/~/{}/{}/#comment-{}", blog, slug, comment.id))

15
src/routes/errors.rs Normal file
View File

@ -0,0 +1,15 @@
use rocket_contrib::Template;
#[catch(404)]
fn not_found() -> 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"
}))
}

View File

@ -22,19 +22,7 @@ fn index(conn: DbConn, user: Option<User>) -> Template {
Template::render("instance/index", json!({ Template::render("instance/index", json!({
"instance": inst, "instance": inst,
"account": user, "account": user,
"recents": recents.into_iter().map(|p| { "recents": recents.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>()
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::<Vec<serde_json::Value>>()
})) }))
} }
None => { None => {
@ -58,11 +46,11 @@ struct NewInstanceForm {
#[post("/configure", data = "<data>")] #[post("/configure", data = "<data>")]
fn post_config(conn: DbConn, data: Form<NewInstanceForm>) -> Redirect { fn post_config(conn: DbConn, data: Form<NewInstanceForm>) -> Redirect {
let form = data.get(); let form = data.get();
let inst = Instance::insert( let inst = Instance::insert(&*conn, NewInstance {
&*conn, public_domain: BASE_URL.as_str().to_string(),
BASE_URL.as_str().to_string(), name: form.name.to_string(),
form.name.to_string(), local: true
true); });
if inst.has_admin(&*conn) { if inst.has_admin(&*conn) {
Redirect::to("/") Redirect::to("/")
} else { } else {

View File

@ -1,6 +1,6 @@
use rocket::response::{Redirect, Flash}; use rocket::response::{Redirect, Flash};
use activity_pub::broadcast; use activity_pub::{broadcast, IntoId, inbox::Notify};
use db_conn::DbConn; use db_conn::DbConn;
use models::{ use models::{
likes, likes,
@ -22,9 +22,10 @@ fn create(blog: String, slug: String, user: User, conn: DbConn) -> Redirect {
}); });
like.update_ap_url(&*conn); 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)); broadcast(&*conn, &user, like.into_activity(&*conn), user.get_followers(&*conn));
} else { } 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); let delete_act = like.delete(&*conn);
broadcast(&*conn, &user, delete_act, user.get_followers(&*conn)); broadcast(&*conn, &user, delete_act, user.get_followers(&*conn));
} }

View File

@ -1,8 +1,35 @@
use rocket::response::NamedFile; use rocket::response::NamedFile;
use std::path::{Path, PathBuf}; 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 blogs;
pub mod comments; pub mod comments;
pub mod errors;
pub mod instance; pub mod instance;
pub mod likes; pub mod likes;
pub mod notifications; pub mod notifications;

View File

@ -19,31 +19,15 @@ use safe_string::SafeString;
#[get("/~/<blog>/<slug>", rank = 4)] #[get("/~/<blog>/<slug>", rank = 4)]
fn details(blog: String, slug: String, conn: DbConn, user: Option<User>) -> Template { fn details(blog: String, slug: String, conn: DbConn, user: Option<User>) -> Template {
let blog = Blog::find_by_fqn(&*conn, blog).unwrap(); may_fail!(Blog::find_by_fqn(&*conn, blog), "Couldn't find this blog", |blog| {
let post = Post::find_by_slug(&*conn, slug).unwrap(); may_fail!(Post::find_by_slug(&*conn, slug), "Couldn't find this post", |post| {
let comments = Comment::find_by_post(&*conn, post.id); let comments = Comment::find_by_post(&*conn, post.id);
Template::render("posts/details", json!({ Template::render("posts/details", json!({
"author": ({ "author": post.get_authors(&*conn)[0].to_json(&*conn),
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, "post": post,
"blog": blog, "blog": blog,
"comments": comments.into_iter().map(|c| { "comments": comments.into_iter().map(|c| c.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
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::<Vec<serde_json::Value>>(),
"n_likes": post.get_likes(&*conn).len(), "n_likes": post.get_likes(&*conn).len(),
"has_liked": user.clone().map(|u| u.has_liked(&*conn, &post)).unwrap_or(false), "has_liked": user.clone().map(|u| u.has_liked(&*conn, &post)).unwrap_or(false),
"n_reshares": post.get_reshares(&*conn).len(), "n_reshares": post.get_reshares(&*conn).len(),
@ -51,6 +35,8 @@ fn details(blog: String, slug: String, conn: DbConn, user: Option<User>) -> Temp
"account": user, "account": user,
"date": &post.creation_date.timestamp() "date": &post.creation_date.timestamp()
})) }))
})
})
} }
#[get("/~/<_blog>/<slug>", rank = 3, format = "application/activity+json")] #[get("/~/<_blog>/<slug>", rank = 3, format = "application/activity+json")]

View File

@ -1,6 +1,6 @@
use rocket::response::{Redirect, Flash}; use rocket::response::{Redirect, Flash};
use activity_pub::broadcast; use activity_pub::{broadcast, IntoId, inbox::Notify};
use db_conn::DbConn; use db_conn::DbConn;
use models::{ use models::{
posts::Post, posts::Post,
@ -22,9 +22,10 @@ fn create(blog: String, slug: String, user: User, conn: DbConn) -> Redirect {
}); });
reshare.update_ap_url(&*conn); 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)); broadcast(&*conn, &user, reshare.into_activity(&*conn), user.get_followers(&*conn));
} else { } 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); let delete_act = reshare.delete(&*conn);
broadcast(&*conn, &user, delete_act, user.get_followers(&*conn)); broadcast(&*conn, &user, delete_act, user.get_followers(&*conn));
} }

View File

@ -10,7 +10,7 @@ use serde_json;
use activity_pub::{ use activity_pub::{
activity_pub, ActivityPub, ActivityStream, context, broadcast, Id, IntoId, activity_pub, ActivityPub, ActivityStream, context, broadcast, Id, IntoId,
inbox::Inbox, inbox::{Inbox, Notify},
actor::Actor actor::Actor
}; };
use db_conn::DbConn; use db_conn::DbConn;
@ -34,7 +34,7 @@ fn me(user: Option<User>) -> Result<Redirect,Flash<Redirect>> {
#[get("/@/<name>", rank = 2)] #[get("/@/<name>", rank = 2)]
fn details(name: String, conn: DbConn, account: Option<User>) -> Template { fn details(name: String, conn: DbConn, account: Option<User>) -> Template {
let user = User::find_by_fqn(&*conn, name).unwrap(); 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 recents = Post::get_recents_for_author(&*conn, &user, 6);
let reshares = Reshare::get_recents_for_author(&*conn, &user, 6); let reshares = Reshare::get_recents_for_author(&*conn, &user, 6);
let user_id = user.id.clone(); let user_id = user.id.clone();
@ -46,36 +46,12 @@ fn details(name: String, conn: DbConn, account: Option<User>) -> Template {
"is_remote": user.instance_id != Instance::local_id(&*conn), "is_remote": user.instance_id != Instance::local_id(&*conn),
"follows": account.clone().map(|x| x.is_following(&*conn, user.id)).unwrap_or(false), "follows": account.clone().map(|x| x.is_following(&*conn, user.id)).unwrap_or(false),
"account": account, "account": account,
"recents": recents.into_iter().map(|p| { "recents": recents.into_iter().map(|p| p.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
json!({ "reshares": reshares.into_iter().map(|r| r.get_post(&*conn).unwrap().to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
"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::<Vec<serde_json::Value>>(),
"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::<Vec<serde_json::Value>>(),
"is_self": account.map(|a| a.id == user_id).unwrap_or(false), "is_self": account.map(|a| a.id == user_id).unwrap_or(false),
"n_followers": n_followers "n_followers": n_followers
})) }))
})
} }
#[get("/dashboard")] #[get("/dashboard")]
@ -103,6 +79,8 @@ fn follow(name: String, conn: DbConn, user: User) -> Redirect {
act.follow_props.set_actor_link::<Id>(user.clone().into_id()).unwrap(); act.follow_props.set_actor_link::<Id>(user.clone().into_id()).unwrap();
act.follow_props.set_object_object(user.into_activity(&*conn)).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(); 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]); broadcast(&*conn, &user, act, vec![target]);
Redirect::to(format!("/@/{}/", name)) Redirect::to(format!("/@/{}/", name))
} }
@ -114,7 +92,7 @@ fn follow_auth(name: String) -> Flash<Redirect> {
#[get("/@/<name>/followers", rank = 2)] #[get("/@/<name>/followers", rank = 2)]
fn followers(name: String, conn: DbConn, account: Option<User>) -> Template { fn followers(name: String, conn: DbConn, account: Option<User>) -> Template {
let user = User::find_by_fqn(&*conn, name.clone()).unwrap(); may_fail!(User::find_by_fqn(&*conn, name.clone()), "Couldn't find requested user", |user| {
let user_id = user.id.clone(); let user_id = user.id.clone();
Template::render("users/followers", json!({ Template::render("users/followers", json!({
@ -122,16 +100,12 @@ fn followers(name: String, conn: DbConn, account: Option<User>) -> Template {
"instance_url": user.get_instance(&*conn).public_domain, "instance_url": user.get_instance(&*conn).public_domain,
"is_remote": user.instance_id != Instance::local_id(&*conn), "is_remote": user.instance_id != Instance::local_id(&*conn),
"follows": account.clone().map(|x| x.is_following(&*conn, user.id)).unwrap_or(false), "follows": account.clone().map(|x| x.is_following(&*conn, user.id)).unwrap_or(false),
"followers": user.get_followers(&*conn).into_iter().map(|f| { "followers": user.get_followers(&*conn).into_iter().map(|f| f.to_json(&*conn)).collect::<Vec<serde_json::Value>>(),
let fqn = f.get_fqn(&*conn);
let mut json = serde_json::to_value(f).unwrap();
json["fqn"] = serde_json::Value::String(fqn);
json
}).collect::<Vec<serde_json::Value>>(),
"account": account, "account": account,
"is_self": account.map(|a| a.id == user_id).unwrap_or(false), "is_self": account.map(|a| a.id == user_id).unwrap_or(false),
"n_followers": user.get_followers(&*conn).len() "n_followers": user.get_followers(&*conn).len()
})) }))
})
} }
#[get("/@/<name>", format = "application/activity+json", rank = 1)] #[get("/@/<name>", format = "application/activity+json", rank = 1)]

View File

@ -1,8 +1,10 @@
use rocket::http::ContentType; use rocket::http::ContentType;
use rocket::response::Content; use rocket::response::Content;
use serde_json;
use webfinger::*;
use BASE_URL; use BASE_URL;
use activity_pub::{ap_url, webfinger::Webfinger}; use activity_pub::ap_url;
use db_conn::DbConn; use db_conn::DbConn;
use models::{blogs::Blog, users::User}; use models::{blogs::Blog, users::User};
@ -33,29 +35,32 @@ struct WebfingerQuery {
resource: String resource: String
} }
#[get("/.well-known/webfinger?<query>")] struct WebfingerResolver;
fn webfinger(query: WebfingerQuery, conn: DbConn) -> Content<Result<String, &'static str>> {
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();
if res_dom == BASE_URL.as_str() { impl Resolver<DbConn> for WebfingerResolver {
let res = match User::find_local(&*conn, String::from(user)) { fn instance_domain<'a>() -> &'a str {
BASE_URL.as_str()
}
fn find(acct: String, conn: DbConn) -> Result<Webfinger, ResolverError> {
match User::find_local(&*conn, acct.clone()) {
Some(usr) => Ok(usr.webfinger(&*conn)), Some(usr) => Ok(usr.webfinger(&*conn)),
None => match Blog::find_local(&*conn, String::from(user)) { None => match Blog::find_local(&*conn, acct) {
Some(blog) => Ok(blog.webfinger(&*conn)), Some(blog) => Ok(blog.webfinger(&*conn)),
None => Err("Requested actor not found") None => Err(ResolverError::NotFound)
} }
}; }
Content(ContentType::new("application", "jrd+json"), res) }
} else { }
Content(ContentType::new("text", "plain"), Err("Invalid instance"))
} #[get("/.well-known/webfinger?<query>")]
} else { fn webfinger(query: WebfingerQuery, conn: DbConn) -> Content<String> {
Content(ContentType::new("text", "plain"), Err("Invalid resource type. Only acct is supported")) 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"
}))
} }
} }

View File

@ -0,0 +1,6 @@
{% extends "errors/base" %}
{% block error %}
<h1>{{ "We couldn't find this page." | _ }}</h1>
<h2>{{ "The link that led you here may be broken." | _ }}</h2>
{% endblock error %}

View File

@ -7,7 +7,7 @@
<div class="card"> <div class="card">
<h3><a href="{{ article.url }}">{{ article.post.title }}</a></h3> <h3><a href="{{ article.url }}">{{ article.post.title }}</a></h3>
<main <main
<p>{{ article.post.content | striptags | truncate(length=200) }}</p> <p>{{ article.post.content | safe | striptags | truncate(length=200) }}</p>
</main> </main>
<p class="author"> <p class="author">
{{ "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name }}{{ link_4 }}" | _( {{ "By {{ link_1 }}{{ link_2 }}{{ link_3 }}{{ name }}{{ link_4 }}" | _(