Merge branch 'master' of https://github.com/Plume-org/Plume
This commit is contained in:
commit
2dfe8fad22
19
Cargo.lock
generated
19
Cargo.lock
generated
@ -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"
|
||||||
|
@ -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"
|
||||||
|
21
po/en.po
21
po/en.po
@ -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 ""
|
||||||
|
22
po/fr.po
22
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"
|
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 ""
|
||||||
|
38
po/pl.po
38
po/pl.po
@ -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ę"
|
||||||
|
@ -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 ""
|
||||||
|
@ -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)?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
|
@ -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))
|
|
||||||
}
|
|
@ -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);
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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))
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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))
|
||||||
|
@ -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)]
|
||||||
|
@ -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
15
src/routes/errors.rs
Normal 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"
|
||||||
|
}))
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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")]
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
@ -25,7 +25,7 @@ use models::{
|
|||||||
use utils;
|
use utils;
|
||||||
|
|
||||||
#[get("/me")]
|
#[get("/me")]
|
||||||
fn me(user: Option<User>) -> Result<Redirect,Flash<Redirect>> {
|
fn me(user: Option<User>) -> Result<Redirect, Flash<Redirect>> {
|
||||||
match user {
|
match user {
|
||||||
Some(user) => Ok(Redirect::to(format!("/@/{}/", user.username))),
|
Some(user) => Ok(Redirect::to(format!("/@/{}/", user.username))),
|
||||||
None => Err(utils::requires_login("", "/me"))
|
None => Err(utils::requires_login("", "/me"))
|
||||||
@ -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)]
|
||||||
|
@ -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"))
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
Content(ContentType::new("text", "plain"), Err("Invalid resource type. Only acct is supported"))
|
}
|
||||||
|
|
||||||
|
#[get("/.well-known/webfinger?<query>")]
|
||||||
|
fn webfinger(query: WebfingerQuery, conn: DbConn) -> Content<String> {
|
||||||
|
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"
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
6
templates/errors/404.html.tera
Normal file
6
templates/errors/404.html.tera
Normal 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 %}
|
@ -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 }}" | _(
|
||||||
|
Loading…
Reference in New Issue
Block a user