Compare commits

...

46 Commits

Author SHA1 Message Date
Mina Galić
b596e77f03
remove redundant use statements 2020-05-25 20:00:57 +02:00
Mina Galić
41f97b01f0
unroll filter_map() to easier .await 2020-05-25 20:00:28 +02:00
Mina Galić
a508a4150c
remove redundant use statements 2020-05-25 19:18:24 +02:00
Mina Galić
25c40adf20
yet another dubious clippy warnings fix re returns 2020-05-25 19:17:32 +02:00
Mina Galić
7490567a21
fix warnings about unused doc comments
we do this by making the macro parse and generate doc comments
2020-05-25 15:44:32 +02:00
Mina Galić
492bbb1ba6
make clippy happy with a weird quirk wrt return
not happy about this lint or rust behaviour where the return statement
must not have a `;`
2020-05-25 15:23:55 +02:00
Mina Galić
cf3708e1c6
make clippy happy by removing unused imports 2020-05-25 13:40:35 +02:00
Mina Galić
df442002c2
replace .map().map_err() with a match
so we can .await without worries.
plus, it changes nothing about the code, other than making the intent
clearer at first sight.
2020-05-24 22:16:42 +02:00
Mina Galić
07036b5fad
upgrade validator: it now uses types! in macros!! 2020-05-24 22:03:26 +02:00
Mina Galić
0726375859
add another async (and correctly convert followers_count) 2020-05-24 21:03:59 +02:00
Mina Galić
cb1c260692
remove an experiment of disabling Send… it makes no sense 2020-05-24 21:03:02 +02:00
Mina Galić
de6bfca084
removed a few unused imports 2020-05-24 21:01:16 +02:00
Mina Galić
7aabb9661e
upgrade webfinger everywhere, and implement async 2020-05-24 20:27:39 +02:00
Mina Galić
18bb413011
add async/.await until all our errors are the same:
that our Connection is not Send-safe.
2020-05-24 19:40:02 +02:00
Mina Galić
d2881ee3f7
add async/.await until all our errors are the same: that our Connection is not Send-safe. 2020-05-23 23:32:10 +02:00
Mina Galić
850b3c1337
add async/.await until all our errors are the same:
that our Connection is not Send-safe.
once we get there, we can start thinking about restructing the way we
pass along our connection, or consider using #[database].
2020-05-22 21:41:33 +02:00
Igor Galić
44ebce516c
fix some, break some compiling by adding async/await in front of it
i forgot that we can't `Send` diesel connections over threads, so this
is going to require some rethinking.
2020-05-16 12:06:58 +02:00
Igor Galić
3c830ab0ce
move towards using #[rocket::async_trait]
this also upgrades some dependencies
some of that fixes stuff, others breaks stuff.
2020-05-15 22:38:21 +02:00
Igor Galić
097d0ea9ce
make plume-models async (again) 2020-05-06 22:33:10 +02:00
Igor Galić
6fe16c9f84
upgrade and use futures… then block_on .await in a trait? 2020-05-06 22:32:59 +02:00
Igor Galić
43cb9f700c
update webfinger
as requested in https://github.com/Plume-org/webfinger/issues/8
and provided in https://github.com/Plume-org/webfinger/pull/9

```
meena@76ix ~/s/a/plume (go/async) [101]> cargo update --package webfinger
    Updating crates.io index
    Updating git repository `https://github.com/Plume-org/webfinger`
    Removing bytes v0.4.12
    Removing cookie v0.12.0
    Removing cookie_store v0.7.0
    Removing crossbeam-deque v0.7.2
    Removing crossbeam-epoch v0.8.0
    Removing crossbeam-queue v0.1.2
    Removing crossbeam-utils v0.7.0
    Removing h2 v0.1.26
    Removing http v0.1.21
    Removing http-body v0.1.0
    Removing hyper v0.12.35
    Removing hyper-tls v0.3.2
    Removing publicsuffix v1.5.4
    Removing reqwest v0.9.24
    Removing serde_urlencoded v0.5.5
    Removing string v0.2.1
    Removing tokio v0.1.22
    Removing tokio-buf v0.1.1
    Removing tokio-current-thread v0.1.6
    Removing tokio-executor v0.1.9
    Removing tokio-io v0.1.12
    Removing tokio-reactor v0.1.11
    Removing tokio-sync v0.1.7
    Removing tokio-tcp v0.1.3
    Removing tokio-threadpool v0.1.17
    Removing tokio-timer v0.2.12
    Removing try_from v0.3.2
    Removing want v0.2.0
      Adding webfinger v0.5.0 (https://github.com/Plume-org/webfinger?rev=update-deps#cdaab95e)
    Removing webfinger v0.5.0
meena@76ix ~/s/a/plume (go/async)>
```
2020-05-06 22:30:33 +02:00
Igor Galić
2c285b9aca
start async-ifying routes
template utils and error routes
2020-05-06 22:29:36 +02:00
Igor Galić
e4bb73d22e
cargo clippy 2020-05-06 22:29:36 +02:00
Igor Galić
e9c7259ffb
cargo fmt 2020-05-06 22:29:35 +02:00
Igor Galić
be8c67ee9a
move reqwest client out of thread spawning
this way, we only spawn one, and reuse that.
2020-05-06 22:29:34 +02:00
Igor Galić
65b2c38c29
.await? result from read_to_string() 2020-05-06 22:29:33 +02:00
Jeb Rosen
8aa99cea35
move signature outside the spawning
this allows us to actually move stuff into the async block
and we can drop the 'static life-time.
2020-05-06 22:29:32 +02:00
Igor Galić
a010025074
asyncify reqwest calls (again?) 2020-05-06 22:29:32 +02:00
Igor Galić
82088596a8
asyncify from_activity calls (i.e.: block_on()) 2020-05-06 22:29:31 +02:00
Igor Galić
87ce3a7b51
asyncify plume-models: media upload is now async
including the use of tokio!
2020-05-06 22:29:27 +02:00
Igor Galić
3472a58299
move ClientBuilder into thread, since we cannot Copy it 2020-05-06 22:26:37 +02:00
Igor Galić
a3f165f9f4
Use blocking reqwest API in defer
defer, or, trait functions such as it in general(?) cannot be async (yet)
2020-05-06 22:26:32 +02:00
Igor Galić
25c5da1a7c
add tokio (0.2) as dependency to further async-ify our FromData code
i'm using this opportunity to also update reqwest (0.10), but it's
turning out to be a little trickier, as it requires more modern async
setup, and that appears to need a lot of thinking…
2020-05-06 22:23:35 +02:00
Igor Galić
022e037eea
when using macros!() we need to import the things that they use 2020-05-06 22:20:49 +02:00
Igor Galić
45c335e17b
"manually" create ETag and Cache-Control headers 2020-05-06 22:20:48 +02:00
Igor Galić
b51551973a
start fixing tests in plume-models 2020-05-06 22:20:47 +02:00
Igor Galić
59e5c49aa8
convert plume-models to all async
where sensible! note that State has no asynchronous work to do, so
wrapping it up in async makes no sense.
2020-05-06 22:20:47 +02:00
Igor Galić
ce119ffe50
start making PlumeRocket async 2020-05-06 22:20:46 +02:00
Igor Galić
944f8c42fa
plume-models: convert api-tokens. use DbConn::from_request() directly
there doesn't seem to be a request.guard_async (yet?)
2020-05-06 22:17:25 +02:00
Igor Galić
909f677bdd
plume-models: convert admin & api-tokens to async
n.b.: I do *not* like the error handling in api_tokens 😒
2020-05-06 22:17:24 +02:00
Igor Galić
fd9764ff17
plume-common: also make requests async 2020-05-06 22:17:24 +02:00
Igor Galić
75722abc9e
rocket does not need decl_macro anymore 2020-05-06 22:17:23 +02:00
Igor Galić
ec9b699c6e
convert plume-common to rocket async
it only took 3 hours of @jebrosen's patient help.
2020-05-06 22:17:22 +02:00
Igor Galić
bb5c2b69a7
update rocket* everywhere!
and run cargo update
2020-05-06 22:17:17 +02:00
Igor Galić
e52944e477
update rocket*; which gets us stuck in dependency conflicts 2020-05-06 21:01:25 +02:00
Igor Galić
928470610e
remove csrf for now, so we can update the rest 2020-05-06 21:01:18 +02:00
48 changed files with 2492 additions and 2361 deletions

1
.gitignore vendored
View File

@ -18,3 +18,4 @@ tags.*
search_index search_index
.buildconfig .buildconfig
__pycache__ __pycache__
.vscode/

3685
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@ edition = "2018"
[dependencies] [dependencies]
activitypub = "0.1.3" activitypub = "0.1.3"
askama_escape = "0.1" askama_escape = "0.1"
async-trait = "*"
atom_syndication = "0.6" atom_syndication = "0.6"
clap = "2.33" clap = "2.33"
colored = "1.8" colored = "1.8"
@ -20,20 +21,19 @@ heck = "0.3.0"
lettre = "0.9.2" lettre = "0.9.2"
lettre_email = "0.9.2" lettre_email = "0.9.2"
num_cpus = "1.10" num_cpus = "1.10"
rocket = "0.4.2" rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "async" }
rocket_contrib = { version = "0.4.2", features = ["json"] } rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", rev = "async" , features = ["json"] }
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
rpassword = "4.0" rpassword = "4.0"
runtime-fmt = "0.4.0"
scheduled-thread-pool = "0.2.2" scheduled-thread-pool = "0.2.2"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
serde_qs = "0.5" serde_qs = "0.5"
shrinkwraprs = "0.2.1" shrinkwraprs = "0.2.1"
syntect = "3.3" syntect = "3.3"
validator = "0.8" tokio = "0.2"
validator_derive = "0.8" validator = "0.10"
webfinger = "0.4.1" validator_derive = "0.10"
webfinger = { git = "https://github.com/Plume-org/webfinger", rev = "4e8f12810c4a7ba7a07bbcb722cd265fdff512b6", features = ["async"] }
[[bin]] [[bin]]
name = "plume" name = "plume"
@ -65,9 +65,11 @@ path = "plume-common"
[dependencies.plume-models] [dependencies.plume-models]
path = "plume-models" path = "plume-models"
[dependencies.rocket_csrf] [dependencies.rocket_i18n]
git = "https://github.com/fdb-hiroshima/rocket_csrf" git = "https://github.com/Plume-org/rocket_i18n"
rev = "29910f2829e7e590a540da3804336577b48c7b31" branch = "go-async"
default-features = false
features = ["rocket"]
[build-dependencies] [build-dependencies]
ructe = "0.9.0" ructe = "0.9.0"

View File

@ -1,5 +1,3 @@
use rsass;
use ructe::Ructe; use ructe::Ructe;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::{ffi::OsStr, fs::*, io::Write, path::*}; use std::{ffi::OsStr, fs::*, io::Write, path::*};

View File

@ -6,22 +6,22 @@ edition = "2018"
[dependencies] [dependencies]
activitypub = "0.1.1" activitypub = "0.1.1"
activitystreams-derive = "0.1.1" activitystreams-derive = "0.2"
activitystreams-traits = "0.1.0" activitystreams-traits = "0.1.0"
array_tool = "1.0" array_tool = "1.0"
base64 = "0.10" base64 = "0.10"
futures-util = "*"
heck = "0.3.0" heck = "0.3.0"
hex = "0.3" hex = "0.3"
hyper = "0.12.33" hyper = "0.13"
openssl = "0.10.22" openssl = "0.10.22"
rocket = "0.4.0" rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "async" }
reqwest = "0.9"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
serde_json = "1.0" serde_json = "1.0"
shrinkwraprs = "0.2.1" shrinkwraprs = "0.2.1"
syntect = "3.3" syntect = "3.3"
tokio = "0.1.22" tokio = "0.2"
regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] } regex-syntax = { version = "0.6.17", default-features = false, features = ["unicode-perl"] }
[dependencies.chrono] [dependencies.chrono]
@ -31,3 +31,7 @@ version = "0.4"
[dependencies.pulldown-cmark] [dependencies.pulldown-cmark]
default-features = false default-features = false
version = "0.2.0" version = "0.2.0"
[dependencies.reqwest]
features = ["json", "blocking"]
version = "0.10"

View File

@ -279,8 +279,9 @@ pub trait FromId<C>: Sized {
/// Dereferences an ID /// Dereferences an ID
fn deref(id: &str) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> { fn deref(id: &str) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
reqwest::ClientBuilder::new() // Use blocking reqwest API here, since defer cannot be async (yet)
.connect_timeout(Some(std::time::Duration::from_secs(5))) reqwest::blocking::Client::builder()
.connect_timeout(std::time::Duration::from_secs(5))
.build() .build()
.map_err(|_| (None, InboxError::DerefError.into()))? .map_err(|_| (None, InboxError::DerefError.into()))?
.get(id) .get(id)
@ -296,7 +297,7 @@ pub trait FromId<C>: Sized {
) )
.send() .send()
.map_err(|_| (None, InboxError::DerefError)) .map_err(|_| (None, InboxError::DerefError))
.and_then(|mut r| { .and_then(|r| {
let json: serde_json::Value = r let json: serde_json::Value = r
.json() .json()
.map_err(|_| (None, InboxError::InvalidObject(None)))?; .map_err(|_| (None, InboxError::InvalidObject(None)))?;

View File

@ -1,14 +1,12 @@
use activitypub::{Activity, Link, Object}; use activitypub::{Activity, Link, Object};
use array_tool::vec::Uniq; use array_tool::vec::Uniq;
use reqwest::r#async::ClientBuilder; use reqwest::ClientBuilder;
use rocket::{ use rocket::{
http::Status, http::Status,
request::{FromRequest, Request}, request::{FromRequest, Request},
response::{Responder, Response}, response::{Responder, Response, Result},
Outcome, Outcome,
}; };
use serde_json;
use tokio::prelude::*;
use self::sign::Signable; use self::sign::Signable;
@ -62,39 +60,45 @@ impl<T> ActivityStream<T> {
ActivityStream(t) ActivityStream(t)
} }
} }
#[rocket::async_trait]
impl<'r, O: Object> Responder<'r> for ActivityStream<O> { impl<'r, O: Object + Send + 'r> Responder<'r> for ActivityStream<O> {
fn respond_to(self, request: &Request<'_>) -> Result<Response<'r>, Status> { async fn respond_to(self, request: &'r Request<'_>) -> Result<'r> {
let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?; let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?;
json["@context"] = context(); json["@context"] = context();
serde_json::to_string(&json).respond_to(request).map(|r| { let result = serde_json::to_string(&json).map_err(rocket::response::Debug);
Response::build_from(r) match result.respond_to(request).await {
Ok(r) => Response::build_from(r)
.raw_header("Content-Type", "application/activity+json") .raw_header("Content-Type", "application/activity+json")
.finalize() .ok(),
}) Err(e) => Err(e),
}
} }
} }
#[derive(Clone)] #[derive(Clone)]
pub struct ApRequest; pub struct ApRequest;
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for ApRequest { impl<'a, 'r> FromRequest<'a, 'r> for ApRequest {
type Error = (); type Error = ();
fn from_request(request: &'a Request<'r>) -> Outcome<Self, (Status, Self::Error), ()> { async fn from_request(request: &'a Request<'r>) -> Outcome<Self, (Status, Self::Error), ()> {
request request
.headers() .headers()
.get_one("Accept") .get_one("Accept")
.map(|header| { .map(|header| {
header header
.split(',') .split(',')
.map(|ct| match ct.trim() { .map(|ct| {
// bool for Forward: true if found a valid Content-Type for Plume first (HTML), false otherwise match ct.trim() {
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\"" // bool for Forward: true if found a valid Content-Type for Plume first (HTML),
| "application/ld+json;profile=\"https://w3.org/ns/activitystreams\"" // false otherwise
| "application/activity+json" "application/ld+json; profile=\"https://w3.org/ns/activitystreams\""
| "application/ld+json" => Outcome::Success(ApRequest), | "application/ld+json;profile=\"https://w3.org/ns/activitystreams\""
"text/html" => Outcome::Forward(true), | "application/activity+json"
_ => Outcome::Forward(false), | "application/ld+json" => Outcome::Success(ApRequest),
"text/html" => Outcome::Forward(true),
_ => Outcome::Forward(false),
}
}) })
.fold(Outcome::Forward(false), |out, ct| { .fold(Outcome::Forward(false), |out, ct| {
if out.clone().forwarded().unwrap_or_else(|| out.is_success()) { if out.clone().forwarded().unwrap_or_else(|| out.is_success()) {
@ -130,36 +134,38 @@ where
.sign(sender) .sign(sender)
.expect("activity_pub::broadcast: signature error"); .expect("activity_pub::broadcast: signature error");
let mut rt = tokio::runtime::current_thread::Runtime::new() let rt = tokio::runtime::Builder::new()
.expect("Error while initializing tokio runtime for federation"); .threaded_scheduler()
let client = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5))
.build() .build()
.expect("Can't build client"); .expect("Error while initializing tokio runtime for federation");
for inbox in boxes { for inbox in boxes {
let body = signed.to_string(); let body = signed.to_string();
let mut headers = request::headers(); let mut headers = request::headers();
headers.insert("Digest", request::Digest::digest(&body)); headers.insert("Digest", request::Digest::digest(&body));
rt.spawn( let sig = request::signature(sender, &headers)
.expect("activity_pub::broadcast: request signature error");
let client = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5))
.build()
.expect("Can't build client");
rt.spawn(async move {
client client
.post(&inbox) .post(&inbox)
.headers(headers.clone()) .headers(headers.clone())
.header( .header("Signature", sig)
"Signature",
request::signature(sender, &headers)
.expect("activity_pub::broadcast: request signature error"),
)
.body(body) .body(body)
.send() .send()
.and_then(|r| r.into_body().concat2()) .await
.unwrap()
.text()
.await
.map(move |response| { .map(move |response| {
println!("Successfully sent activity to inbox ({})", inbox); println!("Successfully sent activity to inbox ({})", inbox);
println!("Response: \"{:?}\"\n", response) println!("Response: \"{:?}\"\n", response)
}) })
.map_err(|e| println!("Error while sending to inbox ({:?})", e)), .map_err(|e| println!("Error while sending to inbox ({:?})", e))
); });
} }
rt.run().unwrap();
} }
#[derive(Shrinkwrap, Clone, Serialize, Deserialize)] #[derive(Shrinkwrap, Clone, Serialize, Deserialize)]
@ -203,8 +209,7 @@ pub struct PublicKey {
pub public_key_pem: Option<serde_json::Value>, pub public_key_pem: Option<serde_json::Value>,
} }
#[derive(Clone, Debug, Default, UnitString)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[activitystreams(Hashtag)]
pub struct HashtagType; pub struct HashtagType;
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)] #[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]

View File

@ -1,4 +1,3 @@
use base64;
use chrono::{offset::Utc, DateTime}; use chrono::{offset::Utc, DateTime};
use openssl::hash::{Hasher, MessageDigest}; use openssl::hash::{Hasher, MessageDigest};
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_AGENT}; use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_AGENT};

View File

@ -1,10 +1,7 @@
use super::request; use super::request;
use base64;
use chrono::{naive::NaiveDateTime, DateTime, Duration, Utc}; use chrono::{naive::NaiveDateTime, DateTime, Duration, Utc};
use hex;
use openssl::{pkey::PKey, rsa::Rsa, sha::sha256}; use openssl::{pkey::PKey, rsa::Rsa, sha::sha256};
use rocket::http::HeaderMap; use rocket::http::HeaderMap;
use serde_json;
/// Returns (public key, private key) /// Returns (public key, private key)
pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) { pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {

View File

@ -2,9 +2,7 @@
#[macro_use] #[macro_use]
extern crate activitystreams_derive; extern crate activitystreams_derive;
use activitystreams_traits;
use serde;
#[macro_use] #[macro_use]
extern crate shrinkwraprs; extern crate shrinkwraprs;
#[macro_use] #[macro_use]

View File

@ -10,24 +10,24 @@ ammonia = "2.1.1"
askama_escape = "0.1" askama_escape = "0.1"
bcrypt = "0.5" bcrypt = "0.5"
guid-create = "0.1" guid-create = "0.1"
futures = "0.3"
heck = "0.3.0" heck = "0.3.0"
itertools = "0.8.0" itertools = "0.8.0"
lazy_static = "1.0" lazy_static = "1.0"
migrations_internals= "1.4.0" migrations_internals= "1.4.0"
openssl = "0.10.22" openssl = "0.10.22"
rocket = "0.4.0" rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "async" }
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
reqwest = "0.9"
scheduled-thread-pool = "0.2.2" scheduled-thread-pool = "0.2.2"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
serde_json = "1.0" serde_json = "1.0"
tantivy = "0.10.1" tantivy = "0.10.1"
tokio = "0.2"
url = "2.1" url = "2.1"
walkdir = "2.2" walkdir = "2.2"
webfinger = "0.4.1" webfinger = { git = "https://github.com/Plume-org/webfinger", rev = "4e8f12810c4a7ba7a07bbcb722cd265fdff512b6", features = ["async"] }
whatlang = "0.7.1" whatlang = "0.7.1"
shrinkwraprs = "0.2.1" shrinkwraprs = "0.3"
diesel-derive-newtype = "0.1.2" diesel-derive-newtype = "0.1.2"
glob = "0.3.0" glob = "0.3.0"
@ -48,8 +48,19 @@ path = "../plume-common"
[dependencies.plume-macro] [dependencies.plume-macro]
path = "../plume-macro" path = "../plume-macro"
[dependencies.reqwest]
features = ["json", "blocking"]
version = "0.10"
[dependencies.rocket_i18n]
git = "https://github.com/Plume-org/rocket_i18n"
branch = "go-async"
default-features = false
features = ["rocket"]
[dev-dependencies] [dev-dependencies]
diesel_migrations = "1.3.0" diesel_migrations = "1.4.0"
[features] [features]
postgres = ["diesel/postgres", "plume-macro/postgres" ] postgres = ["diesel/postgres", "plume-macro/postgres" ]

View File

@ -8,11 +8,12 @@ use rocket::{
/// Wrapper around User to use as a request guard on pages reserved to admins. /// Wrapper around User to use as a request guard on pages reserved to admins.
pub struct Admin(pub User); pub struct Admin(pub User);
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for Admin { impl<'a, 'r> FromRequest<'a, 'r> for Admin {
type Error = (); type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<Admin, ()> { async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let user = request.guard::<User>()?; let user = try_outcome!(User::from_request(request).await);
if user.is_admin() { if user.is_admin() {
Outcome::Success(Admin(user)) Outcome::Success(Admin(user))
} else { } else {
@ -24,11 +25,12 @@ impl<'a, 'r> FromRequest<'a, 'r> for Admin {
/// Same as `Admin` but for moderators. /// Same as `Admin` but for moderators.
pub struct Moderator(pub User); pub struct Moderator(pub User);
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for Moderator { impl<'a, 'r> FromRequest<'a, 'r> for Moderator {
type Error = (); type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<Moderator, ()> { async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let user = request.guard::<User>()?; let user = try_outcome!(User::from_request(request).await);
if user.is_moderator() { if user.is_moderator() {
Outcome::Success(Moderator(user)) Outcome::Success(Moderator(user))
} else { } else {

View File

@ -76,32 +76,36 @@ pub enum TokenError {
DbError, DbError,
} }
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for ApiToken { impl<'a, 'r> FromRequest<'a, 'r> for ApiToken {
type Error = TokenError; type Error = TokenError;
fn from_request(request: &'a Request<'r>) -> request::Outcome<ApiToken, TokenError> { async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let headers: Vec<_> = request.headers().get("Authorization").collect(); let headers: Vec<_> = request.headers().get("Authorization").collect();
if headers.len() != 1 { if headers.len() != 1 {
return Outcome::Failure((Status::BadRequest, TokenError::NoHeader)); return Outcome::Failure((Status::BadRequest, TokenError::NoHeader));
} }
let mut parsed_header = headers[0].split(' '); let mut parsed_header = headers[0].split(' ');
let auth_type = parsed_header.next().map_or_else( if let Some(auth_type) = parsed_header.next() {
|| Outcome::Failure((Status::BadRequest, TokenError::NoType)), if let Some(val) = parsed_header.next() {
Outcome::Success, if auth_type == "Bearer" {
)?; if let Outcome::Success(conn) = DbConn::from_request(request).await {
let val = parsed_header.next().map_or_else( if let Ok(token) = ApiToken::find_by_value(&*conn, val) {
|| Outcome::Failure((Status::BadRequest, TokenError::NoValue)), return Outcome::Success(token);
Outcome::Success, }
)?; } else {
return Outcome::Failure((
if auth_type == "Bearer" { Status::InternalServerError,
let conn = request TokenError::DbError,
.guard::<DbConn>() ));
.map_failure(|_| (Status::InternalServerError, TokenError::DbError))?; }
if let Ok(token) = ApiToken::find_by_value(&*conn, val) { }
return Outcome::Success(token); } else {
return Outcome::Failure((Status::BadRequest, TokenError::NoValue));
} }
} else {
return Outcome::Failure((Status::BadRequest, TokenError::NoType));
} }
Outcome::Forward(()) Outcome::Forward(())

View File

@ -20,7 +20,6 @@ use plume_common::activity_pub::{
inbox::{AsActor, FromId}, inbox::{AsActor, FromId},
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source, sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
}; };
use serde_json;
use url::Url; use url::Url;
use webfinger::*; use webfinger::*;
@ -71,7 +70,8 @@ impl Blog {
insert!(blogs, NewBlog, |inserted, conn| { insert!(blogs, NewBlog, |inserted, conn| {
let instance = inserted.get_instance(conn)?; let instance = inserted.get_instance(conn)?;
if inserted.outbox_url.is_empty() { if inserted.outbox_url.is_empty() {
inserted.outbox_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "outbox"); inserted.outbox_url =
instance.compute_box(BLOG_PREFIX, &inserted.actor_id, r#"outbox"#);
} }
if inserted.inbox_url.is_empty() { if inserted.inbox_url.is_empty() {
@ -132,7 +132,7 @@ impl Blog {
.map_err(Error::from) .map_err(Error::from)
} }
pub fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<Blog> { pub async fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<Blog> {
let from_db = blogs::table let from_db = blogs::table
.filter(blogs::fqn.eq(fqn)) .filter(blogs::fqn.eq(fqn))
.first(&*c.conn) .first(&*c.conn)
@ -140,12 +140,13 @@ impl Blog {
if let Some(from_db) = from_db { if let Some(from_db) = from_db {
Ok(from_db) Ok(from_db)
} else { } else {
Blog::fetch_from_webfinger(c, fqn) Blog::fetch_from_webfinger(c, fqn).await
} }
} }
fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<Blog> { async fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<Blog> {
resolve_with_prefix(Prefix::Group, acct.to_owned(), true)? resolve_with_prefix(Prefix::Group, acct.to_owned(), true)
.await?
.links .links
.into_iter() .into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json"))) .find(|l| l.mime_type == Some(String::from("application/activity+json")))

View File

@ -17,6 +17,7 @@ use activitypub::{
}; };
use chrono::{self, NaiveDateTime}; use chrono::{self, NaiveDateTime};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl};
use futures::stream::{self, StreamExt};
use plume_common::{ use plume_common::{
activity_pub::{ activity_pub::{
inbox::{AsActor, AsObject, FromId}, inbox::{AsActor, AsObject, FromId},
@ -24,7 +25,6 @@ use plume_common::{
}, },
utils, utils,
}; };
use serde_json;
use std::collections::HashSet; use std::collections::HashSet;
#[derive(Queryable, Identifiable, Clone, AsChangeset)] #[derive(Queryable, Identifiable, Clone, AsChangeset)]
@ -105,7 +105,7 @@ impl Comment {
.unwrap_or(false) .unwrap_or(false)
} }
pub fn to_activity(&self, c: &PlumeRocket) -> Result<Note> { pub async fn to_activity(&self, c: &PlumeRocket) -> Result<Note> {
let author = User::get(&c.conn, self.author_id)?; let author = User::get(&c.conn, self.author_id)?;
let (html, mentions, _hashtags) = utils::md_to_html( let (html, mentions, _hashtags) = utils::md_to_html(
self.content.get().as_ref(), self.content.get().as_ref(),
@ -132,18 +132,18 @@ impl Comment {
note.object_props.set_attributed_to_link(author.into_id())?; note.object_props.set_attributed_to_link(author.into_id())?;
note.object_props.set_to_link_vec(to)?; note.object_props.set_to_link_vec(to)?;
note.object_props.set_tag_link_vec( note.object_props.set_tag_link_vec(
mentions stream::iter(mentions)
.into_iter() .filter_map(|m| async move { Mention::build_activity(c, &m).await.ok() })
.filter_map(|m| Mention::build_activity(c, &m).ok()) .collect::<Vec<link::Mention>>()
.collect::<Vec<link::Mention>>(), .await,
)?; )?;
Ok(note) Ok(note)
} }
pub fn create_activity(&self, c: &PlumeRocket) -> Result<Create> { pub async fn create_activity(&self, c: &PlumeRocket) -> Result<Create> {
let author = User::get(&c.conn, self.author_id)?; let author = User::get(&c.conn, self.author_id)?;
let note = self.to_activity(c)?; let note = self.to_activity(c).await?;
let mut act = Create::default(); let mut act = Create::default();
act.create_props.set_actor_link(author.into_id())?; act.create_props.set_actor_link(author.into_id())?;
act.create_props.set_object_object(note.clone())?; act.create_props.set_object_object(note.clone())?;

View File

@ -7,7 +7,7 @@ use diesel::{dsl::sql_query, ConnectionError, RunQueryDsl};
use rocket::{ use rocket::{
http::Status, http::Status,
request::{self, FromRequest}, request::{self, FromRequest},
Outcome, Request, State, Outcome, Request,
}; };
use std::ops::Deref; use std::ops::Deref;
@ -21,14 +21,14 @@ pub struct DbConn(pub PooledConnection<ConnectionManager<Connection>>);
/// Attempts to retrieve a single connection from the managed database pool. If /// Attempts to retrieve a single connection from the managed database pool. If
/// no pool is currently managed, fails with an `InternalServerError` status. If /// no pool is currently managed, fails with an `InternalServerError` status. If
/// no connections are available, fails with a `ServiceUnavailable` status. /// no connections are available, fails with a `ServiceUnavailable` status.
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for DbConn { impl<'a, 'r> FromRequest<'a, 'r> for DbConn {
type Error = (); type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let pool = request.guard::<State<'_, DbPool>>()?; match DbConn::from_request(request).await {
match pool.get() { Outcome::Success(a) => Outcome::Success(a),
Ok(conn) => Outcome::Success(DbConn(conn)), _ => Outcome::Failure((Status::ServiceUnavailable, ())),
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())),
} }
} }
} }

View File

@ -6,10 +6,11 @@ use rocket::{
pub struct Headers<'r>(pub HeaderMap<'r>); pub struct Headers<'r>(pub HeaderMap<'r>);
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for Headers<'r> { impl<'a, 'r> FromRequest<'a, 'r> for Headers<'r> {
type Error = (); type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, ()> { async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, ()> {
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
for header in request.headers().clone().into_iter() { for header in request.headers().clone().into_iter() {
headers.add(header); headers.add(header);

View File

@ -1,5 +1,4 @@
use activitypub::activity::*; use activitypub::activity::*;
use serde_json;
use crate::{ use crate::{
comments::Comment, comments::Comment,

View File

@ -4,6 +4,7 @@
#[macro_use] #[macro_use]
extern crate diesel; extern crate diesel;
extern crate futures;
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
#[macro_use] #[macro_use]
@ -75,6 +76,12 @@ impl From<std::option::NoneError> for Error {
} }
} }
impl From<Error> for std::option::NoneError {
fn from(_: Error) -> Self {
std::option::NoneError
}
}
impl From<url::ParseError> for Error { impl From<url::ParseError> for Error {
fn from(_: url::ParseError) -> Self { fn from(_: url::ParseError) -> Self {
Error::Url Error::Url

View File

@ -7,8 +7,8 @@ use crate::{
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
use std::convert::{TryFrom, TryInto}; use std::convert::{TryFrom, TryInto};
/// Represent what a list is supposed to store. Represented in database as an integer
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
/// Represent what a list is supposed to store. Represented in database as an integer
pub enum ListType { pub enum ListType {
User, User,
Blog, Blog,
@ -58,7 +58,11 @@ struct NewList<'a> {
} }
macro_rules! func { macro_rules! func {
(@elem User $id:expr, $value:expr) => { (
$(#[$outer:meta])*
@elem User $id:expr, $value:expr
) => {
$(#[$outer])*
NewListElem { NewListElem {
list_id: $id, list_id: $id,
user_id: Some(*$value), user_id: Some(*$value),
@ -66,7 +70,11 @@ macro_rules! func {
word: None, word: None,
} }
}; };
(@elem Blog $id:expr, $value:expr) => { (
$(#[$outer:meta])*
@elem Blog $id:expr, $value:expr
) => {
$(#[$outer])*
NewListElem { NewListElem {
list_id: $id, list_id: $id,
user_id: None, user_id: None,
@ -74,7 +82,11 @@ macro_rules! func {
word: None, word: None,
} }
}; };
(@elem Word $id:expr, $value:expr) => { (
$(#[$outer:meta])*
@elem Word $id:expr, $value:expr
) => {
$(#[$outer])*
NewListElem { NewListElem {
list_id: $id, list_id: $id,
user_id: None, user_id: None,
@ -82,7 +94,11 @@ macro_rules! func {
word: Some($value), word: Some($value),
} }
}; };
(@elem Prefix $id:expr, $value:expr) => { (
$(#[$outer:meta])*
@elem Prefix $id:expr, $value:expr
) => {
$(#[$outer])*
NewListElem { NewListElem {
list_id: $id, list_id: $id,
user_id: None, user_id: None,
@ -99,7 +115,11 @@ macro_rules! func {
(@out_type Word) => { String }; (@out_type Word) => { String };
(@out_type Prefix) => { String }; (@out_type Prefix) => { String };
(add: $fn:ident, $kind:ident) => { (
$(#[$outer:meta])*
add: $fn:ident, $kind:ident
) => {
$(#[$outer])*
pub fn $fn(&self, conn: &Connection, vals: &[func!(@in_type $kind)]) -> Result<()> { pub fn $fn(&self, conn: &Connection, vals: &[func!(@in_type $kind)]) -> Result<()> {
if self.kind() != ListType::$kind { if self.kind() != ListType::$kind {
return Err(Error::InvalidValue); return Err(Error::InvalidValue);
@ -116,7 +136,11 @@ macro_rules! func {
} }
}; };
(list: $fn:ident, $kind:ident, $table:ident) => { (
$(#[$outer:meta])*
list: $fn:ident, $kind:ident, $table:ident
) => {
$(#[$outer])*
pub fn $fn(&self, conn: &Connection) -> Result<Vec<func!(@out_type $kind)>> { pub fn $fn(&self, conn: &Connection) -> Result<Vec<func!(@out_type $kind)>> {
if self.kind() != ListType::$kind { if self.kind() != ListType::$kind {
return Err(Error::InvalidValue); return Err(Error::InvalidValue);
@ -132,7 +156,11 @@ macro_rules! func {
(set: $fn:ident, $kind:ident, $add:ident) => { (
$(#[$outer:meta])*
set: $fn:ident, $kind:ident, $add:ident
) => {
$(#[$outer])*
pub fn $fn(&self, conn: &Connection, val: &[func!(@in_type $kind)]) -> Result<()> { pub fn $fn(&self, conn: &Connection, val: &[func!(@in_type $kind)]) -> Result<()> {
if self.kind() != ListType::$kind { if self.kind() != ListType::$kind {
return Err(Error::InvalidValue); return Err(Error::InvalidValue);
@ -246,23 +274,35 @@ impl List {
private::ListElem::prefix_in_list(conn, self, word) private::ListElem::prefix_in_list(conn, self, word)
} }
/// Insert new users in a list func! {
func! {add: add_users, User} /// Insert new users in a list
add: add_users, User
}
/// Insert new blogs in a list func! {
func! {add: add_blogs, Blog} /// Insert new blogs in a list
add: add_blogs, Blog
}
/// Insert new words in a list func! {
func! {add: add_words, Word} /// Insert new words in a list
add: add_words, Word
}
/// Insert new prefixes in a list func! {
func! {add: add_prefixes, Prefix} /// Insert new prefixes in a list
add: add_prefixes, Prefix
}
/// Get all users in the list func! {
func! {list: list_users, User, users} /// Get all users in the list
list: list_users, User, users
}
/// Get all blogs in the list func! {
func! {list: list_blogs, Blog, blogs} /// Get all blogs in the list
list: list_blogs, Blog, blogs
}
/// Get all words in the list /// Get all words in the list
pub fn list_words(&self, conn: &Connection) -> Result<Vec<String>> { pub fn list_words(&self, conn: &Connection) -> Result<Vec<String>> {

View File

@ -10,8 +10,8 @@ use plume_common::{
activity_pub::{inbox::FromId, Id}, activity_pub::{inbox::FromId, Id},
utils::MediaProcessor, utils::MediaProcessor,
}; };
use reqwest;
use std::{fs, path::Path}; use std::{fs, path::Path};
use tokio::prelude::*;
#[derive(Clone, Identifiable, Queryable)] #[derive(Clone, Identifiable, Queryable)]
pub struct Media { pub struct Media {
@ -197,7 +197,7 @@ impl Media {
} }
// TODO: merge with save_remote? // TODO: merge with save_remote?
pub fn from_activity(c: &PlumeRocket, image: &Image) -> Result<Media> { pub async fn from_activity(c: &PlumeRocket, image: &Image) -> Result<Media> {
let conn = &*c.conn; let conn = &*c.conn;
let remote_url = image.object_props.url_string().ok()?; let remote_url = image.object_props.url_string().ok()?;
let ext = remote_url let ext = remote_url
@ -211,11 +211,9 @@ impl Media {
ext ext
)); ));
let mut dest = fs::File::create(path.clone()).ok()?; let mut dest = tokio::fs::File::create(path.clone()).await?;
reqwest::get(remote_url.as_str()) let contents = reqwest::get(remote_url.as_str()).await?.bytes().await?;
.ok()? dest.write_all(&contents).await?;
.copy_to(&mut dest)
.ok()?;
Media::insert( Media::insert(
conn, conn,

View File

@ -52,8 +52,8 @@ impl Mention {
} }
} }
pub fn build_activity(c: &PlumeRocket, ment: &str) -> Result<link::Mention> { pub async fn build_activity(c: &PlumeRocket, ment: &str) -> Result<link::Mention> {
let user = User::find_by_fqn(c, ment)?; let user = User::find_by_fqn(c, ment).await?;
let mut mention = link::Mention::default(); let mut mention = link::Mention::default();
mention.link_props.set_href_string(user.ap_url)?; mention.link_props.set_href_string(user.ap_url)?;
mention.link_props.set_name_string(format!("@{}", ment))?; mention.link_props.set_name_string(format!("@{}", ment))?;

View File

@ -20,20 +20,35 @@ mod module {
pub flash_msg: Option<(String, String)>, pub flash_msg: Option<(String, String)>,
} }
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for PlumeRocket { impl<'a, 'r> FromRequest<'a, 'r> for PlumeRocket {
type Error = (); type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<PlumeRocket, ()> { async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let conn = request.guard::<DbConn>()?; let conn = DbConn::from_request(request).await.succeeded().unwrap();
let intl = request.guard::<rocket_i18n::I18n>()?; let intl = rocket_i18n::I18n::from_request(request)
let user = request.guard::<users::User>().succeeded(); .await
let worker = request.guard::<'_, State<'_, Arc<ScheduledThreadPool>>>()?; .succeeded()
let searcher = request.guard::<'_, State<'_, Arc<search::Searcher>>>()?; .unwrap();
let flash_msg = request.guard::<FlashMessage<'_, '_>>().succeeded(); let user = users::User::from_request(request)
.await
.succeeded()
.unwrap();
let worker = request
.guard::<State<'_, Arc<ScheduledThreadPool>>>()
.await
.succeeded()
.unwrap();
let searcher = request
.guard::<State<'_, Arc<search::Searcher>>>()
.await
.succeeded()
.unwrap();
let flash_msg = request.guard::<FlashMessage<'_, '_>>().await.succeeded();
Outcome::Success(PlumeRocket { Outcome::Success(PlumeRocket {
conn, conn,
intl, intl,
user, user: Some(user),
flash_msg: flash_msg.map(|f| (f.name().into(), f.msg().into())), flash_msg: flash_msg.map(|f| (f.name().into(), f.msg().into())),
worker: worker.clone(), worker: worker.clone(),
searcher: searcher.clone(), searcher: searcher.clone(),
@ -60,17 +75,18 @@ mod module {
pub worker: Arc<ScheduledThreadPool>, pub worker: Arc<ScheduledThreadPool>,
} }
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for PlumeRocket { impl<'a, 'r> FromRequest<'a, 'r> for PlumeRocket {
type Error = (); type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<PlumeRocket, ()> { async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let conn = request.guard::<DbConn>()?; let conn = try_outcome!(DbConn::from_request(request).await);
let user = request.guard::<users::User>().succeeded(); let user = try_outcome!(users::User::from_request(request).await);
let worker = request.guard::<'_, State<'_, Arc<ScheduledThreadPool>>>()?; let worker = try_outcome!(request.guard::<'_, State<'_, Arc<ScheduledThreadPool>>>());
let searcher = request.guard::<'_, State<'_, Arc<search::Searcher>>>()?; let searcher = try_outcome!(request.guard::<'_, State<'_, Arc<search::Searcher>>>());
Outcome::Success(PlumeRocket { Outcome::Success(PlumeRocket {
conn, conn,
user, user: Some(user),
worker: worker.clone(), worker: worker.clone(),
searcher: searcher.clone(), searcher: searcher.clone(),
}) })

View File

@ -19,8 +19,8 @@ use plume_common::{
}, },
utils::md_to_html, utils::md_to_html,
}; };
use serde_json;
use std::collections::HashSet; use std::collections::HashSet;
use tokio::runtime::Runtime;
pub type LicensedArticle = CustomObject<Licensed, Article>; pub type LicensedArticle = CustomObject<Licensed, Article>;
@ -579,11 +579,11 @@ impl FromId<PlumeRocket> for Post {
} }
}); });
let cover = article let image = article.object_props.icon_object::<Image>().ok().unwrap();
.object_props
.icon_object::<Image>() let mut r = Runtime::new().unwrap();
.ok() let cover =
.and_then(|img| Media::from_activity(&c, &img).ok().map(|m| m.id)); Some(r.block_on(async { Media::from_activity(&c, &image).await.ok().unwrap().id }));
let title = article.object_props.name_string()?; let title = article.object_props.name_string()?;
let post = Post::insert( let post = Post::insert(
@ -699,17 +699,22 @@ impl FromId<PlumeRocket> for PostUpdate {
} }
fn from_activity(c: &PlumeRocket, updated: LicensedArticle) -> Result<Self> { fn from_activity(c: &PlumeRocket, updated: LicensedArticle) -> Result<Self> {
let image = updated
.object
.object_props
.icon_object::<Image>()
.ok()
.unwrap();
let mut r = Runtime::new().unwrap();
let cover =
Some(r.block_on(async { Media::from_activity(&c, &image).await.ok().unwrap().id }));
Ok(PostUpdate { Ok(PostUpdate {
ap_url: updated.object.object_props.id_string()?, ap_url: updated.object.object_props.id_string()?,
title: updated.object.object_props.name_string().ok(), title: updated.object.object_props.name_string().ok(),
subtitle: updated.object.object_props.summary_string().ok(), subtitle: updated.object.object_props.summary_string().ok(),
content: updated.object.object_props.content_string().ok(), content: updated.object.object_props.content_string().ok(),
cover: updated cover,
.object
.object_props
.icon_object::<Image>()
.ok()
.and_then(|img| Media::from_activity(&c, &img).ok().map(|m| m.id)),
source: updated source: updated
.object .object
.ap_object_props .ap_object_props

View File

@ -7,7 +7,9 @@ use crate::{
users::User, users::User,
PlumeRocket, Result, PlumeRocket, Result,
}; };
use futures::stream::{self, StreamExt};
use plume_common::activity_pub::inbox::AsActor; use plume_common::activity_pub::inbox::AsActor;
use tokio::runtime::Runtime;
use whatlang::{self, Lang}; use whatlang::{self, Lang};
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@ -295,15 +297,33 @@ impl WithList {
} }
} }
List::Array(list) => match self { List::Array(list) => match self {
WithList::Blog => Ok(list WithList::Blog => {
.iter() let mut rt = Runtime::new().unwrap();
.filter_map(|b| Blog::find_by_fqn(rocket, b).ok()) rt.block_on(async move {
.any(|b| b.id == post.blog_id)), Ok(stream::iter(list)
.filter_map(|b| async move {
Some(Blog::find_by_fqn(rocket, b).await.ok().unwrap())
})
.collect::<Vec<_>>()
.await
.into_iter()
.any(|b| b.id == post.blog_id))
})
}
WithList::Author { boosts, likes } => match kind { WithList::Author { boosts, likes } => match kind {
Kind::Original => Ok(list Kind::Original => {
.iter() let mut rt = Runtime::new().unwrap();
.filter_map(|a| User::find_by_fqn(rocket, a).ok()) rt.block_on(async move {
.any(|a| post.is_author(&rocket.conn, a.id).unwrap_or(false))), Ok(stream::iter(list)
.filter_map(|a| async move {
Some(User::find_by_fqn(rocket, a).await.ok().unwrap())
})
.collect::<Vec<_>>()
.await
.into_iter()
.any(|a| post.is_author(&rocket.conn, a.id).unwrap_or(false)))
})
}
Kind::Reshare(u) => { Kind::Reshare(u) => {
if *boosts { if *boosts {
Ok(list.iter().any(|user| &u.fqn == user)) Ok(list.iter().any(|user| &u.fqn == user))

View File

@ -11,7 +11,6 @@ use activitypub::{
object::{Image, Tombstone}, object::{Image, Tombstone},
Activity, CustomObject, Endpoint, Activity, CustomObject, Endpoint,
}; };
use bcrypt;
use chrono::{NaiveDateTime, Utc}; use chrono::{NaiveDateTime, Utc};
use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl}; use diesel::{self, BelongingToDsl, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl};
use openssl::{ use openssl::{
@ -37,7 +36,6 @@ use rocket::{
outcome::IntoOutcome, outcome::IntoOutcome,
request::{self, FromRequest, Request}, request::{self, FromRequest, Request},
}; };
use serde_json;
use std::{ use std::{
cmp::PartialEq, cmp::PartialEq,
hash::{Hash, Hasher}, hash::{Hash, Hasher},
@ -191,7 +189,7 @@ impl User {
.map_err(Error::from) .map_err(Error::from)
} }
pub fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<User> { pub async fn find_by_fqn(c: &PlumeRocket, fqn: &str) -> Result<User> {
let from_db = users::table let from_db = users::table
.filter(users::fqn.eq(fqn)) .filter(users::fqn.eq(fqn))
.first(&*c.conn) .first(&*c.conn)
@ -199,12 +197,13 @@ impl User {
if let Some(from_db) = from_db { if let Some(from_db) = from_db {
Ok(from_db) Ok(from_db)
} else { } else {
User::fetch_from_webfinger(c, fqn) User::fetch_from_webfinger(c, fqn).await
} }
} }
fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<User> { async fn fetch_from_webfinger(c: &PlumeRocket, acct: &str) -> Result<User> {
let link = resolve(acct.to_owned(), true)? let link = resolve(acct.to_owned(), true)
.await?
.links .links
.into_iter() .into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json"))) .find(|l| l.mime_type == Some(String::from("application/activity+json")))
@ -212,8 +211,9 @@ impl User {
User::from_id(c, link.href.as_ref()?, None).map_err(|(_, e)| e) User::from_id(c, link.href.as_ref()?, None).map_err(|(_, e)| e)
} }
pub fn fetch_remote_interact_uri(acct: &str) -> Result<String> { pub async fn fetch_remote_interact_uri(acct: &str) -> Result<String> {
resolve(acct.to_owned(), true)? resolve(acct.to_owned(), true)
.await?
.links .links
.into_iter() .into_iter()
.find(|l| l.rel == "http://ostatus.org/schema/1.0/subscribe") .find(|l| l.rel == "http://ostatus.org/schema/1.0/subscribe")
@ -221,9 +221,9 @@ impl User {
.ok_or(Error::Webfinger) .ok_or(Error::Webfinger)
} }
fn fetch(url: &str) -> Result<CustomPerson> { async fn fetch(url: &str) -> Result<CustomPerson> {
let mut res = ClientBuilder::new() let res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5))) .connect_timeout(std::time::Duration::from_secs(5))
.build()? .build()?
.get(url) .get(url)
.header( .header(
@ -235,8 +235,9 @@ impl User {
.join(", "), .join(", "),
)?, )?,
) )
.send()?; .send()
let text = &res.text()?; .await?;
let text = &res.text().await?;
// without this workaround, publicKey is not correctly deserialized // without this workaround, publicKey is not correctly deserialized
let ap_sign = serde_json::from_str::<ApSignature>(text)?; let ap_sign = serde_json::from_str::<ApSignature>(text)?;
let mut json = serde_json::from_str::<CustomPerson>(text)?; let mut json = serde_json::from_str::<CustomPerson>(text)?;
@ -244,48 +245,48 @@ impl User {
Ok(json) Ok(json)
} }
pub fn fetch_from_url(c: &PlumeRocket, url: &str) -> Result<User> { pub async fn fetch_from_url(c: &PlumeRocket, url: &str) -> Result<User> {
User::fetch(url).and_then(|json| User::from_activity(c, json)) let json = User::fetch(url).await?;
User::from_activity(c, json)
} }
pub fn refetch(&self, conn: &Connection) -> Result<()> { pub async fn refetch(&self, conn: &Connection) -> Result<()> {
User::fetch(&self.ap_url.clone()).and_then(|json| { let json = User::fetch(&self.ap_url.clone()).await?;
let avatar = Media::save_remote( let avatar = Media::save_remote(
conn, conn,
json.object json.object
.object_props .object_props
.icon_image()? .icon_image()?
.object_props .object_props
.url_string()?, .url_string()?,
&self, &self,
) )
.ok(); .ok();
diesel::update(self) diesel::update(self)
.set(( .set((
users::username.eq(json.object.ap_actor_props.preferred_username_string()?), users::username.eq(json.object.ap_actor_props.preferred_username_string()?),
users::display_name.eq(json.object.object_props.name_string()?), users::display_name.eq(json.object.object_props.name_string()?),
users::outbox_url.eq(json.object.ap_actor_props.outbox_string()?), users::outbox_url.eq(json.object.ap_actor_props.outbox_string()?),
users::inbox_url.eq(json.object.ap_actor_props.inbox_string()?), users::inbox_url.eq(json.object.ap_actor_props.inbox_string()?),
users::summary.eq(SafeString::new( users::summary.eq(SafeString::new(
&json &json
.object .object
.object_props .object_props
.summary_string() .summary_string()
.unwrap_or_default(), .unwrap_or_default(),
)), )),
users::followers_endpoint.eq(json.object.ap_actor_props.followers_string()?), users::followers_endpoint.eq(json.object.ap_actor_props.followers_string()?),
users::avatar_id.eq(avatar.map(|a| a.id)), users::avatar_id.eq(avatar.map(|a| a.id)),
users::last_fetched_date.eq(Utc::now().naive_utc()), users::last_fetched_date.eq(Utc::now().naive_utc()),
users::public_key.eq(json users::public_key.eq(json
.custom_props .custom_props
.public_key_publickey()? .public_key_publickey()?
.public_key_pem_string()?), .public_key_pem_string()?),
)) ))
.execute(conn) .execute(conn)
.map(|_| ()) .map(|_| ())
.map_err(Error::from) .map_err(Error::from)
})
} }
pub fn hash_pass(pass: &str) -> Result<String> { pub fn hash_pass(pass: &str) -> Result<String> {
@ -356,9 +357,10 @@ impl User {
.set_part_of_link(Id::new(&self.outbox_url))?; .set_part_of_link(Id::new(&self.outbox_url))?;
Ok(ActivityStream::new(coll)) Ok(ActivityStream::new(coll))
} }
fn fetch_outbox_page<T: Activity>(&self, url: &str) -> Result<(Vec<T>, Option<String>)> {
let mut res = ClientBuilder::new() async fn fetch_outbox_page<T: Activity>(&self, url: &str) -> Result<(Vec<T>, Option<String>)> {
.connect_timeout(Some(std::time::Duration::from_secs(5))) let res = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5))
.build()? .build()?
.get(url) .get(url)
.header( .header(
@ -370,8 +372,9 @@ impl User {
.join(", "), .join(", "),
)?, )?,
) )
.send()?; .send()
let text = &res.text()?; .await?;
let text = &res.text().await?;
let json: serde_json::Value = serde_json::from_str(text)?; let json: serde_json::Value = serde_json::from_str(text)?;
let items = json["items"] let items = json["items"]
.as_array() .as_array()
@ -386,9 +389,9 @@ impl User {
}; };
Ok((items, next)) Ok((items, next))
} }
pub fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> { pub async fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> {
let mut res = ClientBuilder::new() let res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5))) .connect_timeout(std::time::Duration::from_secs(5))
.build()? .build()?
.get(&self.outbox_url[..]) .get(&self.outbox_url[..])
.header( .header(
@ -400,13 +403,14 @@ impl User {
.join(", "), .join(", "),
)?, )?,
) )
.send()?; .send()
let text = &res.text()?; .await?;
let text = &res.text().await?;
let json: serde_json::Value = serde_json::from_str(text)?; let json: serde_json::Value = serde_json::from_str(text)?;
if let Some(first) = json.get("first") { if let Some(first) = json.get("first") {
let mut items: Vec<T> = Vec::new(); let mut items: Vec<T> = Vec::new();
let mut next = first.as_str().unwrap().to_owned(); let mut next = first.as_str().unwrap().to_owned();
while let Ok((mut page, nxt)) = self.fetch_outbox_page(&next) { while let Ok((mut page, nxt)) = self.fetch_outbox_page(&next).await {
if page.is_empty() { if page.is_empty() {
break; break;
} }
@ -431,9 +435,9 @@ impl User {
} }
} }
pub fn fetch_followers_ids(&self) -> Result<Vec<String>> { pub async fn fetch_followers_ids(&self) -> Result<Vec<String>> {
let mut res = ClientBuilder::new() let res = ClientBuilder::new()
.connect_timeout(Some(std::time::Duration::from_secs(5))) .connect_timeout(std::time::Duration::from_secs(5))
.build()? .build()?
.get(&self.followers_endpoint[..]) .get(&self.followers_endpoint[..])
.header( .header(
@ -445,8 +449,9 @@ impl User {
.join(", "), .join(", "),
)?, )?,
) )
.send()?; .send()
let text = &res.text()?; .await?;
let text = &res.text().await?;
let json: serde_json::Value = serde_json::from_str(text)?; let json: serde_json::Value = serde_json::from_str(text)?;
Ok(json["items"] Ok(json["items"]
.as_array() .as_array()
@ -789,11 +794,12 @@ impl User {
} }
} }
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for User { impl<'a, 'r> FromRequest<'a, 'r> for User {
type Error = (); type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<User, ()> { async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let conn = request.guard::<DbConn>()?; let conn = try_outcome!(DbConn::from_request(request).await);
request request
.cookies() .cookies()
.get_private(AUTH_COOKIE) .get_private(AUTH_COOKIE)

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=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n" "Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
"X-Crowdin-Project: plume\n" "X-Crowdin-Project: plume\n"
"X-Crowdin-Language: ar\n" "X-Crowdin-Language: ar\n"
"X-Crowdin-File: /master/po/plume/plume.pot\n" "X-Crowdin-File: /master/po/plume/plume.pot\n"

View File

@ -1 +1 @@
nightly-2020-01-15 nightly-2020-05-05

View File

@ -35,6 +35,7 @@ impl Scope for plume_models::posts::Post {
pub struct Authorization<A, S>(pub ApiToken, PhantomData<(A, S)>); pub struct Authorization<A, S>(pub ApiToken, PhantomData<(A, S)>);
#[rocket::async_trait]
impl<'a, 'r, A, S> FromRequest<'a, 'r> for Authorization<A, S> impl<'a, 'r, A, S> FromRequest<'a, 'r> for Authorization<A, S>
where where
A: Action, A: Action,
@ -42,9 +43,10 @@ where
{ {
type Error = (); type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<Authorization<A, S>, ()> { async fn from_request(request: &'a Request<'r>) -> request::Outcome<Authorization<A, S>, ()> {
request request
.guard::<ApiToken>() .guard::<ApiToken>()
.await
.map_failure(|_| (Status::Unauthorized, ())) .map_failure(|_| (Status::Unauthorized, ()))
.and_then(|token| { .and_then(|token| {
if token.can(A::to_str(), S::to_str()) { if token.can(A::to_str(), S::to_str()) {

View File

@ -4,7 +4,6 @@ use rocket::{
response::{self, Responder}, response::{self, Responder},
}; };
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use serde_json;
use plume_common::utils::random_hex; use plume_common::utils::random_hex;
use plume_models::{api_tokens::*, apps::App, users::User, Error, PlumeRocket}; use plume_models::{api_tokens::*, apps::App, users::User, Error, PlumeRocket};
@ -26,21 +25,31 @@ impl From<std::option::NoneError> for ApiError {
} }
} }
#[rocket::async_trait]
impl<'r> Responder<'r> for ApiError { impl<'r> Responder<'r> for ApiError {
fn respond_to(self, req: &Request<'_>) -> response::Result<'r> { async fn respond_to(self, req: &'r Request<'_>) -> response::Result<'r> {
match self.0 { match self.0 {
Error::NotFound => Json(json!({ Error::NotFound => {
"error": "Not found" Json(json!({
})) "error": "Not found"
.respond_to(req), }))
Error::Unauthorized => Json(json!({ .respond_to(req)
"error": "You are not authorized to access this resource" .await
})) }
.respond_to(req), Error::Unauthorized => {
_ => Json(json!({ Json(json!({
"error": "Server error" "error": "You are not authorized to access this resource"
})) }))
.respond_to(req), .respond_to(req)
.await
}
_ => {
Json(json!({
"error": "Server error"
}))
.respond_to(req)
.await
}
} }
} }
} }
@ -55,14 +64,14 @@ pub struct OAuthRequest {
} }
#[get("/oauth2?<query..>")] #[get("/oauth2?<query..>")]
pub fn oauth( pub async fn oauth(
query: Form<OAuthRequest>, query: Form<OAuthRequest>,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<Json<serde_json::Value>, ApiError> { ) -> Result<Json<serde_json::Value>, ApiError> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let app = App::find_by_client_id(conn, &query.client_id)?; let app = App::find_by_client_id(conn, &query.client_id)?;
if app.client_secret == query.client_secret { if app.client_secret == query.client_secret {
if let Ok(user) = User::find_by_fqn(&rockets, &query.username) { if let Ok(user) = User::find_by_fqn(&rockets, &query.username).await {
if user.auth(&query.password) { if user.auth(&query.password) {
let token = ApiToken::insert( let token = ApiToken::insert(
conn, conn,

View File

@ -98,7 +98,7 @@ pub fn list(
} }
#[post("/posts", data = "<payload>")] #[post("/posts", data = "<payload>")]
pub fn create( pub async fn create(
auth: Authorization<Write, Post>, auth: Authorization<Write, Post>,
payload: Json<NewPostData>, payload: Json<NewPostData>,
rockets: PlumeRocket, rockets: PlumeRocket,
@ -192,7 +192,7 @@ pub fn create(
for m in mentions.into_iter() { for m in mentions.into_iter() {
Mention::from_activity( Mention::from_activity(
&*conn, &*conn,
&Mention::build_activity(&rockets, &m)?, &Mention::build_activity(&rockets, &m).await?,
post.id, post.id,
true, true,
true, true,

View File

@ -9,9 +9,9 @@ use plume_models::{
use rocket::{data::*, http::Status, response::status, Outcome::*, Request}; use rocket::{data::*, http::Status, response::status, Outcome::*, Request};
use rocket_contrib::json::*; use rocket_contrib::json::*;
use serde::Deserialize; use serde::Deserialize;
use std::io::Read; use tokio::io::AsyncReadExt;
pub fn handle_incoming( pub async fn handle_incoming(
rockets: PlumeRocket, rockets: PlumeRocket,
data: SignedJson<serde_json::Value>, data: SignedJson<serde_json::Value>,
headers: Headers<'_>, headers: Headers<'_>,
@ -32,6 +32,7 @@ pub fn handle_incoming(
// maybe we just know an old key? // maybe we just know an old key?
actor actor
.refetch(conn) .refetch(conn)
.await
.and_then(|_| User::get(conn, actor.id)) .and_then(|_| User::get(conn, actor.id))
.and_then(|u| { .and_then(|u| {
if verify_http_headers(&u, &headers.0, &sig).is_secure() || act.clone().verify(&u) { if verify_http_headers(&u, &headers.0, &sig).is_secure() || act.clone().verify(&u) {
@ -73,32 +74,31 @@ impl<'a, T: Deserialize<'a>> FromData<'a> for SignedJson<T> {
type Owned = String; type Owned = String;
type Borrowed = str; type Borrowed = str;
fn transform( fn transform<'r>(r: &'r Request, d: Data) -> TransformFuture<'r, Self::Owned, Self::Error> {
r: &Request<'_>, Box::pin(async move {
d: Data, let size_limit = r.limits().get("json").unwrap_or(JSON_LIMIT);
) -> Transform<rocket::data::Outcome<Self::Owned, Self::Error>> { let mut s = String::with_capacity(512);
let size_limit = r.limits().get("json").unwrap_or(JSON_LIMIT); let outcome = match d.open().take(size_limit).read_to_string(&mut s).await {
let mut s = String::with_capacity(512); Ok(_) => Success(s),
match d.open().take(size_limit).read_to_string(&mut s) { Err(e) => Failure((Status::BadRequest, JsonError::Io(e))),
Ok(_) => Transform::Borrowed(Success(s)), };
Err(e) => Transform::Borrowed(Failure((Status::BadRequest, JsonError::Io(e)))), Transform::Borrowed(outcome)
} })
} }
fn from_data( fn from_data(
_: &Request<'_>, _: &Request<'_>,
o: Transformed<'a, Self>, o: Transformed<'a, Self>,
) -> rocket::data::Outcome<Self, Self::Error> { ) -> FromDataFuture<'a, Self, Self::Error> {
let string = o.borrowed()?; Box::pin(async move {
match serde_json::from_str(&string) { let string = try_outcome!(o.borrowed());
Ok(v) => Success(SignedJson(Digest::from_body(&string), Json(v))), match serde_json::from_str(&string) {
Err(e) => { Ok(v) => Success(SignedJson(Digest::from_body(&string), Json(v))),
if e.is_data() { Err(e) if e.is_data() => {
Failure((Status::UnprocessableEntity, JsonError::Parse(string, e))) return Failure((Status::UnprocessableEntity, JsonError::Parse(string, e)))
} else {
Failure((Status::BadRequest, JsonError::Parse(string, e)))
} }
Err(e) => Failure((Status::BadRequest, JsonError::Parse(string, e))),
} }
} })
} }
} }

View File

@ -1,16 +1,15 @@
#![allow(clippy::too_many_arguments)] #![allow(clippy::too_many_arguments)]
#![feature(decl_macro, proc_macro_hygiene, try_trait)] #![feature(proc_macro_hygiene, try_trait)]
#[macro_use] #[macro_use]
extern crate gettext_macros; extern crate gettext_macros;
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;
#[macro_use] #[macro_use]
extern crate runtime_fmt;
#[macro_use]
extern crate serde_json; extern crate serde_json;
#[macro_use] #[macro_use]
extern crate validator_derive; extern crate validator_derive;
extern crate validator;
use clap::App; use clap::App;
use diesel::r2d2::ConnectionManager; use diesel::r2d2::ConnectionManager;
@ -21,7 +20,6 @@ use plume_models::{
search::{Searcher as UnmanagedSearcher, SearcherError}, search::{Searcher as UnmanagedSearcher, SearcherError},
Connection, Error, CONFIG, Connection, Error, CONFIG,
}; };
use rocket_csrf::CsrfFairingBuilder;
use scheduled_thread_pool::ScheduledThreadPool; use scheduled_thread_pool::ScheduledThreadPool;
use std::process::exit; use std::process::exit;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@ -275,25 +273,7 @@ Then try to restart Plume
.manage(dbpool) .manage(dbpool)
.manage(Arc::new(workpool)) .manage(Arc::new(workpool))
.manage(searcher) .manage(searcher)
.manage(include_i18n!()) .manage(include_i18n!());
.attach(
CsrfFairingBuilder::new()
.set_default_target(
"/csrf-violation?target=<uri>".to_owned(),
rocket::http::Method::Post,
)
.add_exceptions(vec![
("/inbox".to_owned(), "/inbox".to_owned(), None),
(
"/@/<name>/inbox".to_owned(),
"/@/<name>/inbox".to_owned(),
None,
),
("/api/<path..>".to_owned(), "/api/<path..>".to_owned(), None),
])
.finalize()
.expect("main: csrf fairing creation error"),
);
#[cfg(feature = "test")] #[cfg(feature = "test")]
let rocket = rocket.mount("/test", routes![test_routes::health,]); let rocket = rocket.mount("/test", routes![test_routes::health,]);

View File

@ -19,10 +19,14 @@ use plume_models::{
}; };
#[get("/~/<name>?<page>", rank = 2)] #[get("/~/<name>?<page>", rank = 2)]
pub fn details(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> { pub async fn details(
name: String,
page: Option<Page>,
rockets: PlumeRocket,
) -> Result<Ructe, ErrorPage> {
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
let conn = &*rockets.conn; let conn = &*rockets.conn;
let blog = Blog::find_by_fqn(&rockets, &name)?; let blog = Blog::find_by_fqn(&rockets, &name).await?;
let posts = Post::blog_page(conn, &blog, page.limits())?; let posts = Post::blog_page(conn, &blog, page.limits())?;
let articles_count = Post::count_for_blog(conn, &blog)?; let articles_count = Post::count_for_blog(conn, &blog)?;
let authors = &blog.list_authors(conn)?; let authors = &blog.list_authors(conn)?;
@ -38,12 +42,12 @@ pub fn details(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result
} }
#[get("/~/<name>", rank = 1)] #[get("/~/<name>", rank = 1)]
pub fn activity_details( pub async fn activity_details(
name: String, name: String,
rockets: PlumeRocket, rockets: PlumeRocket,
_ap: ApRequest, _ap: ApRequest,
) -> Option<ActivityStream<CustomGroup>> { ) -> Option<ActivityStream<CustomGroup>> {
let blog = Blog::find_by_fqn(&rockets, &name).ok()?; let blog = Blog::find_by_fqn(&rockets, &name).await?;
Some(ActivityStream::new(blog.to_activity(&*rockets.conn).ok()?)) Some(ActivityStream::new(blog.to_activity(&*rockets.conn).ok()?))
} }
@ -83,7 +87,7 @@ fn valid_slug(title: &str) -> Result<(), ValidationError> {
} }
#[post("/blogs/new", data = "<form>")] #[post("/blogs/new", data = "<form>")]
pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> RespondOrRedirect { pub async fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> RespondOrRedirect {
let slug = utils::make_actor_id(&form.title); let slug = utils::make_actor_id(&form.title);
let conn = &*rockets.conn; let conn = &*rockets.conn;
let intl = &rockets.intl.catalog; let intl = &rockets.intl.catalog;
@ -93,7 +97,7 @@ pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> RespondOr
Ok(_) => ValidationErrors::new(), Ok(_) => ValidationErrors::new(),
Err(e) => e, Err(e) => e,
}; };
if Blog::find_by_fqn(&rockets, &slug).is_ok() { if Blog::find_by_fqn(&rockets, &slug).await.is_ok() {
errors.add( errors.add(
"title", "title",
ValidationError { ValidationError {
@ -143,9 +147,11 @@ pub fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> RespondOr
} }
#[post("/~/<name>/delete")] #[post("/~/<name>/delete")]
pub fn delete(name: String, rockets: PlumeRocket) -> RespondOrRedirect { pub async fn delete(name: String, rockets: PlumeRocket) -> RespondOrRedirect {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let blog = Blog::find_by_fqn(&rockets, &name).expect("blog::delete: blog not found"); let blog = Blog::find_by_fqn(&rockets, &name)
.await
.expect("blog::delete: blog not found");
if rockets if rockets
.user .user
@ -184,9 +190,9 @@ pub struct EditForm {
} }
#[get("/~/<name>/edit")] #[get("/~/<name>/edit")]
pub fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> { pub async fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let blog = Blog::find_by_fqn(&rockets, &name)?; let blog = Blog::find_by_fqn(&rockets, &name).await?;
if rockets if rockets
.user .user
.clone() .clone()
@ -233,14 +239,16 @@ fn check_media(conn: &Connection, id: i32, user: &User) -> bool {
} }
#[put("/~/<name>/edit", data = "<form>")] #[put("/~/<name>/edit", data = "<form>")]
pub fn update( pub async fn update(
name: String, name: String,
form: LenientForm<EditForm>, form: LenientForm<EditForm>,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> RespondOrRedirect { ) -> RespondOrRedirect {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let intl = &rockets.intl.catalog; let intl = &rockets.intl.catalog;
let mut blog = Blog::find_by_fqn(&rockets, &name).expect("blog::update: blog not found"); let mut blog = Blog::find_by_fqn(&rockets, &name)
.await
.expect("blog::update: blog not found");
if !rockets if !rockets
.user .user
.clone() .clone()
@ -342,23 +350,28 @@ pub fn update(
} }
#[get("/~/<name>/outbox")] #[get("/~/<name>/outbox")]
pub fn outbox(name: String, rockets: PlumeRocket) -> Option<ActivityStream<OrderedCollection>> { pub async fn outbox(
let blog = Blog::find_by_fqn(&rockets, &name).ok()?; name: String,
rockets: PlumeRocket,
) -> Option<ActivityStream<OrderedCollection>> {
let blog = Blog::find_by_fqn(&rockets, &name).await?;
Some(blog.outbox(&*rockets.conn).ok()?) Some(blog.outbox(&*rockets.conn).ok()?)
} }
#[allow(unused_variables)] #[allow(unused_variables)]
#[get("/~/<name>/outbox?<page>")] #[get("/~/<name>/outbox?<page>")]
pub fn outbox_page( pub async fn outbox_page(
name: String, name: String,
page: Page, page: Page,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Option<ActivityStream<OrderedCollectionPage>> { ) -> Option<ActivityStream<OrderedCollectionPage>> {
let blog = Blog::find_by_fqn(&rockets, &name).ok()?; let blog = Blog::find_by_fqn(&rockets, &name).await?;
Some(blog.outbox_page(&*rockets.conn, page.limits()).ok()?) Some(blog.outbox_page(&*rockets.conn, page.limits()).ok()?)
} }
#[get("/~/<name>/atom.xml")] #[get("/~/<name>/atom.xml")]
pub fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>> { pub async fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>> {
let blog = Blog::find_by_fqn(&rockets, &name).ok()?; let blog = Blog::find_by_fqn(&rockets, &name).await?;
let conn = &*rockets.conn; let conn = &*rockets.conn;
let entries = Post::get_recents_for_blog(&*conn, &blog, 15).ok()?; let entries = Post::get_recents_for_blog(&*conn, &blog, 15).ok()?;
let uri = Instance::get_local() let uri = Instance::get_local()

View File

@ -22,13 +22,13 @@ use plume_models::{
#[derive(Default, FromForm, Debug, Validate)] #[derive(Default, FromForm, Debug, Validate)]
pub struct NewCommentForm { pub struct NewCommentForm {
pub responding_to: Option<i32>, pub responding_to: Option<i32>,
#[validate(length(min = "1", message = "Your comment can't be empty"))] #[validate(length(min = 1, message = "Your comment can't be empty"))]
pub content: String, pub content: String,
pub warning: String, pub warning: String,
} }
#[post("/~/<blog_name>/<slug>/comment", data = "<form>")] #[post("/~/<blog_name>/<slug>/comment", data = "<form>")]
pub fn create( pub async fn create(
blog_name: String, blog_name: String,
slug: String, slug: String,
form: LenientForm<NewCommentForm>, form: LenientForm<NewCommentForm>,
@ -36,10 +36,12 @@ pub fn create(
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<Flash<Redirect>, Ructe> { ) -> Result<Flash<Redirect>, Ructe> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let blog = Blog::find_by_fqn(&rockets, &blog_name).expect("comments::create: blog error"); let blog = Blog::find_by_fqn(&rockets, &blog_name)
.await
.expect("comments::create: blog error");
let post = Post::find_by_slug(&*conn, &slug, blog.id).expect("comments::create: post error"); let post = Post::find_by_slug(&*conn, &slug, blog.id).expect("comments::create: post error");
form.validate() match form.validate() {
.map(|_| { Ok(_ok) => {
let (html, mentions, _hashtags) = utils::md_to_html( let (html, mentions, _hashtags) = utils::md_to_html(
form.content.as_ref(), form.content.as_ref(),
Some( Some(
@ -66,6 +68,7 @@ pub fn create(
.expect("comments::create: insert error"); .expect("comments::create: insert error");
let new_comment = comm let new_comment = comm
.create_activity(&rockets) .create_activity(&rockets)
.await
.expect("comments::create: activity error"); .expect("comments::create: activity error");
// save mentions // save mentions
@ -73,6 +76,7 @@ pub fn create(
Mention::from_activity( Mention::from_activity(
&*conn, &*conn,
&Mention::build_activity(&rockets, &ment) &Mention::build_activity(&rockets, &ment)
.await
.expect("comments::create: build mention error"), .expect("comments::create: build mention error"),
comm.id, comm.id,
false, false,
@ -90,14 +94,14 @@ pub fn create(
.worker .worker
.execute(move || broadcast(&user_clone, new_comment, dest)); .execute(move || broadcast(&user_clone, new_comment, dest));
Flash::success( Ok(Flash::success(
Redirect::to( Redirect::to(
uri!(super::posts::details: blog = blog_name, slug = slug, responding_to = _), uri!(super::posts::details: blog = blog_name, slug = slug, responding_to = _),
), ),
i18n!(&rockets.intl.catalog, "Your comment has been posted."), i18n!(&rockets.intl.catalog, "Your comment has been posted."),
) ))
}) }
.map_err(|errors| { Err(errors) => {
// TODO: de-duplicate this code // TODO: de-duplicate this code
let comments = CommentTree::from_post(&*conn, &post, Some(&user)) let comments = CommentTree::from_post(&*conn, &post, Some(&user))
.expect("comments::create: comments error"); .expect("comments::create: comments error");
@ -106,7 +110,7 @@ pub fn create(
.responding_to .responding_to
.and_then(|r| Comment::get(&*conn, r).ok()); .and_then(|r| Comment::get(&*conn, r).ok());
render!(posts::details( Err(render!(posts::details(
&rockets.to_context(), &rockets.to_context(),
post.clone(), post.clone(),
blog, blog,
@ -133,8 +137,9 @@ pub fn create(
post.get_authors(&*conn) post.get_authors(&*conn)
.expect("comments::create: authors error")[0] .expect("comments::create: authors error")[0]
.clone() .clone()
)) )))
}) }
}
} }
#[post("/~/<blog>/<slug>/comment/<id>/delete")] #[post("/~/<blog>/<slug>/comment/<id>/delete")]
@ -174,15 +179,16 @@ pub fn delete(
} }
#[get("/~/<_blog>/<_slug>/comment/<id>")] #[get("/~/<_blog>/<_slug>/comment/<id>")]
pub fn activity_pub( pub async fn activity_pub(
_blog: String, _blog: String,
_slug: String, _slug: String,
id: i32, id: i32,
_ap: ApRequest, _ap: ApRequest,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Option<ActivityStream<Note>> { ) -> Option<ActivityStream<Note>> {
Comment::get(&*rockets.conn, id) let c = match Comment::get(&*rockets.conn, id) {
.and_then(|c| c.to_activity(&rockets)) Ok(c) => c.to_activity(&rockets).await.ok(),
.ok() Err(_) => None,
.map(ActivityStream::new) };
c.map(ActivityStream::new)
} }

View File

@ -1,6 +1,7 @@
use crate::template_utils::{IntoContext, Ructe}; use crate::template_utils::{IntoContext, Ructe};
use plume_models::{Error, PlumeRocket}; use plume_models::{Error, PlumeRocket};
use rocket::{ use rocket::{
request::FromRequest,
response::{self, Responder}, response::{self, Responder},
Request, Request,
}; };
@ -14,35 +15,46 @@ impl From<Error> for ErrorPage {
} }
} }
#[rocket::async_trait]
impl<'r> Responder<'r> for ErrorPage { impl<'r> Responder<'r> for ErrorPage {
fn respond_to(self, req: &Request<'_>) -> response::Result<'r> { async fn respond_to(self, req: &'r Request<'_>) -> response::Result<'r> {
let rockets = req.guard::<PlumeRocket>().unwrap(); let rockets = PlumeRocket::from_request(req).await.unwrap();
match self.0 { match self.0 {
Error::NotFound => render!(errors::not_found(&rockets.to_context())).respond_to(req), Error::NotFound => {
Error::Unauthorized => { render!(errors::not_found(&rockets.to_context()))
render!(errors::not_found(&rockets.to_context())).respond_to(req) .respond_to(req)
.await
}
Error::Unauthorized => {
render!(errors::not_found(&rockets.to_context()))
.respond_to(req)
.await
}
_ => {
render!(errors::not_found(&rockets.to_context()))
.respond_to(req)
.await
} }
_ => render!(errors::not_found(&rockets.to_context())).respond_to(req),
} }
} }
} }
#[catch(404)] #[catch(404)]
pub fn not_found(req: &Request<'_>) -> Ructe { pub async fn not_found(req: &Request<'_>) -> Ructe {
let rockets = req.guard::<PlumeRocket>().unwrap(); let rockets = req.guard::<PlumeRocket>().await.unwrap();
render!(errors::not_found(&rockets.to_context())) render!(errors::not_found(&rockets.to_context()))
} }
#[catch(422)] #[catch(422)]
pub fn unprocessable_entity(req: &Request<'_>) -> Ructe { pub async fn unprocessable_entity(req: &Request<'_>) -> Ructe {
let rockets = req.guard::<PlumeRocket>().unwrap(); let rockets = req.guard::<PlumeRocket>().await.unwrap();
render!(errors::unprocessable_entity(&rockets.to_context())) render!(errors::unprocessable_entity(&rockets.to_context()))
} }
#[catch(500)] #[catch(500)]
pub fn server_error(req: &Request<'_>) -> Ructe { pub async fn server_error(req: &Request<'_>) -> Ructe {
let rockets = req.guard::<PlumeRocket>().unwrap(); let rockets = req.guard::<PlumeRocket>().await.unwrap();
render!(errors::server_error(&rockets.to_context())) render!(errors::server_error(&rockets.to_context()))
} }

View File

@ -5,7 +5,7 @@ use rocket::{
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use rocket_i18n::I18n; use rocket_i18n::I18n;
use scheduled_thread_pool::ScheduledThreadPool; use scheduled_thread_pool::ScheduledThreadPool;
use serde_json; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use validator::{Validate, ValidationErrors}; use validator::{Validate, ValidationErrors};
@ -76,12 +76,12 @@ pub fn admin_mod(_mod: Moderator, rockets: PlumeRocket) -> Ructe {
#[derive(Clone, FromForm, Validate)] #[derive(Clone, FromForm, Validate)]
pub struct InstanceSettingsForm { pub struct InstanceSettingsForm {
#[validate(length(min = "1"))] #[validate(length(min = 1))]
pub name: String, pub name: String,
pub open_registrations: bool, pub open_registrations: bool,
pub short_description: SafeString, pub short_description: SafeString,
pub long_description: SafeString, pub long_description: SafeString,
#[validate(length(min = "1"))] #[validate(length(min = 1))]
pub default_license: String, pub default_license: String,
} }
@ -386,17 +386,21 @@ fn ban(
} }
#[post("/inbox", data = "<data>")] #[post("/inbox", data = "<data>")]
pub fn shared_inbox( pub async fn shared_inbox(
rockets: PlumeRocket, rockets: PlumeRocket,
data: inbox::SignedJson<serde_json::Value>, data: inbox::SignedJson<serde_json::Value>,
headers: Headers<'_>, headers: Headers<'_>,
) -> Result<String, status::BadRequest<&'static str>> { ) -> Result<String, status::BadRequest<&'static str>> {
inbox::handle_incoming(rockets, data, headers) inbox::handle_incoming(rockets, data, headers).await
} }
#[get("/remote_interact?<target>")] #[get("/remote_interact?<target>")]
pub fn interact(rockets: PlumeRocket, user: Option<User>, target: String) -> Option<Redirect> { pub async fn interact(
if User::find_by_fqn(&rockets, &target).is_ok() { rockets: PlumeRocket,
user: Option<User>,
target: String,
) -> Option<Redirect> {
if User::find_by_fqn(&rockets, &target).await.is_ok() {
return Some(Redirect::to(uri!(super::user::details: name = target))); return Some(Redirect::to(uri!(super::user::details: name = target)));
} }

View File

@ -7,9 +7,8 @@ use chrono::naive::NaiveDateTime;
use plume_models::{posts::Post, Connection, CONFIG, ITEMS_PER_PAGE}; use plume_models::{posts::Post, Connection, CONFIG, ITEMS_PER_PAGE};
use rocket::{ use rocket::{
http::{ http::{
hyper::header::{CacheControl, CacheDirective, ETag, EntityTag},
uri::{FromUriParam, Query}, uri::{FromUriParam, Query},
RawStr, Status, Header, RawStr, Status,
}, },
request::{self, FromFormValue, FromRequest, Request}, request::{self, FromFormValue, FromRequest, Request},
response::{self, Flash, NamedFile, Redirect, Responder, Response}, response::{self, Flash, NamedFile, Redirect, Responder, Response},
@ -95,10 +94,11 @@ impl Page {
#[derive(Shrinkwrap)] #[derive(Shrinkwrap)]
pub struct ContentLen(pub u64); pub struct ContentLen(pub u64);
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for ContentLen { impl<'a, 'r> FromRequest<'a, 'r> for ContentLen {
type Error = (); type Error = ();
fn from_request(r: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { async fn from_request(r: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
match r.limits().get("forms") { match r.limits().get("forms") {
Some(l) => Outcome::Success(ContentLen(l)), Some(l) => Outcome::Success(ContentLen(l)),
None => Outcome::Failure((Status::InternalServerError, ())), None => Outcome::Failure((Status::InternalServerError, ())),
@ -208,14 +208,15 @@ pub mod well_known;
#[response()] #[response()]
pub struct CachedFile { pub struct CachedFile {
inner: NamedFile, inner: NamedFile,
cache_control: CacheControl, cache_control: Header<'static>,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct ThemeFile(NamedFile); pub struct ThemeFile(NamedFile);
#[rocket::async_trait]
impl<'r> Responder<'r> for ThemeFile { impl<'r> Responder<'r> for ThemeFile {
fn respond_to(self, r: &Request<'_>) -> response::Result<'r> { async fn respond_to(self, r: &'r Request<'_>) -> response::Result<'r> {
let contents = std::fs::read(self.0.path()).map_err(|_| Status::InternalServerError)?; let contents = std::fs::read(self.0.path()).map_err(|_| Status::InternalServerError)?;
let mut hasher = DefaultHasher::new(); let mut hasher = DefaultHasher::new();
@ -228,12 +229,12 @@ impl<'r> Responder<'r> for ThemeFile {
{ {
Response::build() Response::build()
.status(Status::NotModified) .status(Status::NotModified)
.header(ETag(EntityTag::strong(etag))) .header(Header::new("ETag", etag))
.ok() .ok()
} else { } else {
Response::build() Response::build()
.merge(self.0.respond_to(r)?) .merge(self.0.respond_to(r).await.ok().unwrap())
.header(ETag(EntityTag::strong(etag))) .header(Header::new("ETag", etag))
.ok() .ok()
} }
} }
@ -256,7 +257,7 @@ pub fn plume_media_files(file: PathBuf) -> Option<CachedFile> {
.ok() .ok()
.map(|f| CachedFile { .map(|f| CachedFile {
inner: f, inner: f,
cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]), cache_control: Header::new("Cache-Control", format!("max-age={}", 60 * 60 * 24 * 30)),
}) })
} }
#[get("/static/<file..>", rank = 3)] #[get("/static/<file..>", rank = 3)]
@ -265,6 +266,6 @@ pub fn static_files(file: PathBuf) -> Option<CachedFile> {
.ok() .ok()
.map(|f| CachedFile { .map(|f| CachedFile {
inner: f, inner: f,
cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]), cache_control: Header::new("Cache-Control", format!("max-age={}", 60 * 60 * 24 * 30)),
}) })
} }

View File

@ -1,3 +1,4 @@
use activitypub::link;
use chrono::Utc; use chrono::Utc;
use heck::{CamelCase, KebabCase}; use heck::{CamelCase, KebabCase};
use rocket::request::LenientForm; use rocket::request::LenientForm;
@ -11,7 +12,7 @@ use std::{
use validator::{Validate, ValidationError, ValidationErrors}; use validator::{Validate, ValidationError, ValidationErrors};
use crate::routes::{ use crate::routes::{
comments::NewCommentForm, errors::ErrorPage, ContentLen, RemoteForm, RespondOrRedirect, comments::NewCommentForm, errors::ErrorPage, ContentLen, Page, RemoteForm, RespondOrRedirect,
}; };
use crate::template_utils::{IntoContext, Ructe}; use crate::template_utils::{IntoContext, Ructe};
use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest}; use plume_common::activity_pub::{broadcast, ActivityStream, ApRequest};
@ -33,7 +34,7 @@ use plume_models::{
}; };
#[get("/~/<blog>/<slug>?<responding_to>", rank = 4)] #[get("/~/<blog>/<slug>?<responding_to>", rank = 4)]
pub fn details( pub async fn details(
blog: String, blog: String,
slug: String, slug: String,
responding_to: Option<i32>, responding_to: Option<i32>,
@ -41,7 +42,7 @@ pub fn details(
) -> Result<Ructe, ErrorPage> { ) -> Result<Ructe, ErrorPage> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let user = rockets.user.clone(); let user = rockets.user.clone();
let blog = Blog::find_by_fqn(&rockets, &blog)?; let blog = Blog::find_by_fqn(&rockets, &blog).await?;
let post = Post::find_by_slug(&*conn, &slug, blog.id)?; let post = Post::find_by_slug(&*conn, &slug, blog.id)?;
if !(post.published if !(post.published
|| post || post
@ -99,14 +100,14 @@ pub fn details(
} }
#[get("/~/<blog>/<slug>", rank = 3)] #[get("/~/<blog>/<slug>", rank = 3)]
pub fn activity_details( pub async fn activity_details(
blog: String, blog: String,
slug: String, slug: String,
_ap: ApRequest, _ap: ApRequest,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<ActivityStream<LicensedArticle>, Option<String>> { ) -> Result<ActivityStream<LicensedArticle>, Option<String>> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let blog = Blog::find_by_fqn(&rockets, &blog).map_err(|_| None)?; let blog = Blog::find_by_fqn(&rockets, &blog).await.map_err(|_| None)?;
let post = Post::find_by_slug(&*conn, &slug, blog.id).map_err(|_| None)?; let post = Post::find_by_slug(&*conn, &slug, blog.id).map_err(|_| None)?;
if post.published { if post.published {
Ok(ActivityStream::new( Ok(ActivityStream::new(
@ -130,9 +131,9 @@ pub fn new_auth(blog: String, i18n: I18n) -> Flash<Redirect> {
} }
#[get("/~/<blog>/new", rank = 1)] #[get("/~/<blog>/new", rank = 1)]
pub fn new(blog: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> { pub async fn new(blog: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let b = Blog::find_by_fqn(&rockets, &blog)?; let b = Blog::find_by_fqn(&rockets, &blog).await?;
let user = rockets.user.clone().unwrap(); let user = rockets.user.clone().unwrap();
if !user.is_author_in(&*conn, &b)? { if !user.is_author_in(&*conn, &b)? {
@ -162,7 +163,7 @@ pub fn new(blog: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe,
} }
#[get("/~/<blog>/<slug>/edit")] #[get("/~/<blog>/<slug>/edit")]
pub fn edit( pub async fn edit(
blog: String, blog: String,
slug: String, slug: String,
cl: ContentLen, cl: ContentLen,
@ -170,7 +171,7 @@ pub fn edit(
) -> Result<Ructe, ErrorPage> { ) -> Result<Ructe, ErrorPage> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let intl = &rockets.intl.catalog; let intl = &rockets.intl.catalog;
let b = Blog::find_by_fqn(&rockets, &blog)?; let b = Blog::find_by_fqn(&rockets, &blog).await?;
let post = Post::find_by_slug(&*conn, &slug, b.id)?; let post = Post::find_by_slug(&*conn, &slug, b.id)?;
let user = rockets.user.clone().unwrap(); let user = rockets.user.clone().unwrap();
@ -216,7 +217,7 @@ pub fn edit(
} }
#[post("/~/<blog>/<slug>/edit", data = "<form>")] #[post("/~/<blog>/<slug>/edit", data = "<form>")]
pub fn update( pub async fn update(
blog: String, blog: String,
slug: String, slug: String,
cl: ContentLen, cl: ContentLen,
@ -224,7 +225,9 @@ pub fn update(
rockets: PlumeRocket, rockets: PlumeRocket,
) -> RespondOrRedirect { ) -> RespondOrRedirect {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let b = Blog::find_by_fqn(&rockets, &blog).expect("post::update: blog error"); let b = Blog::find_by_fqn(&rockets, &blog)
.await
.expect("post::update: blog error");
let mut post = let mut post =
Post::find_by_slug(&*conn, &slug, b.id).expect("post::update: find by slug error"); Post::find_by_slug(&*conn, &slug, b.id).expect("post::update: find by slug error");
let user = rockets.user.clone().unwrap(); let user = rockets.user.clone().unwrap();
@ -301,14 +304,16 @@ pub fn update(
.expect("post::update: update error"); .expect("post::update: update error");
if post.published { if post.published {
post.update_mentions( // NOTE: here we unroll a filter_map(), so we can use .await painlessly
&conn, let mut filtered_mentions: Vec<link::Mention> = vec![];
mentions for m in mentions.into_iter() {
.into_iter() match Mention::build_activity(&rockets, &m).await {
.filter_map(|m| Mention::build_activity(&rockets, &m).ok()) Ok(m) => filtered_mentions.push(m),
.collect(), Err(_) => {}
) }
.expect("post::update: mentions error"); }
post.update_mentions(&conn, filtered_mentions)
.expect("post::update: mentions error");
} }
let tags = form let tags = form
@ -321,7 +326,7 @@ pub fn update(
.filter_map(|t| Tag::build_activity(t).ok()) .filter_map(|t| Tag::build_activity(t).ok())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
post.update_tags(&conn, tags) post.update_tags(&conn, tags)
.expect("post::update: tags error"); .expect(r#"post::update: tags error"#);
let hashtags = hashtags let hashtags = hashtags
.into_iter() .into_iter()
@ -399,14 +404,16 @@ pub fn valid_slug(title: &str) -> Result<(), ValidationError> {
} }
#[post("/~/<blog_name>/new", data = "<form>")] #[post("/~/<blog_name>/new", data = "<form>")]
pub fn create( pub async fn create(
blog_name: String, blog_name: String,
form: LenientForm<NewPostForm>, form: LenientForm<NewPostForm>,
cl: ContentLen, cl: ContentLen,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<RespondOrRedirect, ErrorPage> { ) -> Result<RespondOrRedirect, ErrorPage> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let blog = Blog::find_by_fqn(&rockets, &blog_name).expect("post::create: blog error"); let blog = Blog::find_by_fqn(&rockets, &blog_name)
.await
.expect("post::create: blog error");
let slug = form.title.to_string().to_kebab_case(); let slug = form.title.to_string().to_kebab_case();
let user = rockets.user.clone().unwrap(); let user = rockets.user.clone().unwrap();
@ -520,6 +527,7 @@ pub fn create(
Mention::from_activity( Mention::from_activity(
&*conn, &*conn,
&Mention::build_activity(&rockets, &m) &Mention::build_activity(&rockets, &m)
.await
.expect("post::create: mention build error"), .expect("post::create: mention build error"),
post.id, post.id,
true, true,
@ -562,7 +570,7 @@ pub fn create(
} }
#[post("/~/<blog_name>/<slug>/delete")] #[post("/~/<blog_name>/<slug>/delete")]
pub fn delete( pub async fn delete(
blog_name: String, blog_name: String,
slug: String, slug: String,
rockets: PlumeRocket, rockets: PlumeRocket,
@ -570,6 +578,7 @@ pub fn delete(
) -> Result<Flash<Redirect>, ErrorPage> { ) -> Result<Flash<Redirect>, ErrorPage> {
let user = rockets.user.clone().unwrap(); let user = rockets.user.clone().unwrap();
let post = Blog::find_by_fqn(&rockets, &blog_name) let post = Blog::find_by_fqn(&rockets, &blog_name)
.await
.and_then(|blog| Post::find_by_slug(&*rockets.conn, &slug, blog.id)); .and_then(|blog| Post::find_by_slug(&*rockets.conn, &slug, blog.id));
if let Ok(post) = post { if let Ok(post) = post {
@ -615,12 +624,13 @@ pub fn delete(
} }
#[get("/~/<blog_name>/<slug>/remote_interact")] #[get("/~/<blog_name>/<slug>/remote_interact")]
pub fn remote_interact( pub async fn remote_interact(
rockets: PlumeRocket, rockets: PlumeRocket,
blog_name: String, blog_name: String,
slug: String, slug: String,
) -> Result<Ructe, ErrorPage> { ) -> Result<Ructe, ErrorPage> {
let target = Blog::find_by_fqn(&rockets, &blog_name) let target = Blog::find_by_fqn(&rockets, &blog_name)
.await
.and_then(|blog| Post::find_by_slug(&rockets.conn, &slug, blog.id))?; .and_then(|blog| Post::find_by_slug(&rockets.conn, &slug, blog.id))?;
Ok(render!(posts::remote_interact( Ok(render!(posts::remote_interact(
&rockets.to_context(), &rockets.to_context(),
@ -633,17 +643,19 @@ pub fn remote_interact(
} }
#[post("/~/<blog_name>/<slug>/remote_interact", data = "<remote>")] #[post("/~/<blog_name>/<slug>/remote_interact", data = "<remote>")]
pub fn remote_interact_post( pub async fn remote_interact_post(
rockets: PlumeRocket, rockets: PlumeRocket,
blog_name: String, blog_name: String,
slug: String, slug: String,
remote: LenientForm<RemoteForm>, remote: LenientForm<RemoteForm>,
) -> Result<RespondOrRedirect, ErrorPage> { ) -> Result<RespondOrRedirect, ErrorPage> {
let target = Blog::find_by_fqn(&rockets, &blog_name) let target = Blog::find_by_fqn(&rockets, &blog_name)
.await
.and_then(|blog| Post::find_by_slug(&rockets.conn, &slug, blog.id))?; .and_then(|blog| Post::find_by_slug(&rockets.conn, &slug, blog.id))?;
if let Some(uri) = User::fetch_remote_interact_uri(&remote.remote) if let uri = User::fetch_remote_interact_uri(&remote.remote)
.ok() .await
.and_then(|uri| rt_format!(uri, uri = target.ap_url).ok()) .map(|uri| uri.replace("{uri}", &format!("{}", target.ap_url)))
.unwrap()
{ {
Ok(Redirect::to(uri).into()) Ok(Redirect::to(uri).into())
} else { } else {

View File

@ -10,14 +10,14 @@ use plume_models::{
}; };
#[post("/~/<blog>/<slug>/reshare")] #[post("/~/<blog>/<slug>/reshare")]
pub fn create( pub async fn create(
blog: String, blog: String,
slug: String, slug: String,
user: User, user: User,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<Redirect, ErrorPage> { ) -> Result<Redirect, ErrorPage> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let b = Blog::find_by_fqn(&rockets, &blog)?; let b = Blog::find_by_fqn(&rockets, &blog).await?;
let post = Post::find_by_slug(&*conn, &slug, b.id)?; let post = Post::find_by_slug(&*conn, &slug, b.id)?;
if !user.has_reshared(&*conn, &post)? { if !user.has_reshared(&*conn, &post)? {

View File

@ -35,21 +35,23 @@ pub fn new(m: Option<String>, rockets: PlumeRocket) -> Ructe {
#[derive(Default, FromForm, Validate)] #[derive(Default, FromForm, Validate)]
pub struct LoginForm { pub struct LoginForm {
#[validate(length(min = "1", message = "We need an email, or a username to identify you"))] #[validate(length(min = 1, message = "We need an email, or a username to identify you"))]
pub email_or_name: String, pub email_or_name: String,
#[validate(length(min = "1", message = "Your password can't be empty"))] #[validate(length(min = 1, message = "Your password can't be empty"))]
pub password: String, pub password: String,
} }
#[post("/login", data = "<form>")] #[post("/login", data = "<form>")]
pub fn create( pub async fn create(
form: LenientForm<LoginForm>, form: LenientForm<LoginForm>,
mut cookies: Cookies<'_>, mut cookies: Cookies<'_>,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> RespondOrRedirect { ) -> RespondOrRedirect {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let user = User::find_by_email(&*conn, &form.email_or_name) let user = match User::find_by_email(&*conn, &form.email_or_name) {
.or_else(|_| User::find_by_fqn(&rockets, &form.email_or_name)); Ok(user) => Ok(user),
Err(_) => User::find_by_fqn(&rockets, &form.email_or_name).await,
};
let mut errors = match form.validate() { let mut errors = match form.validate() {
Ok(_) => ValidationErrors::new(), Ok(_) => ValidationErrors::new(),
Err(e) => e, Err(e) => e,
@ -197,7 +199,7 @@ pub fn password_reset_form(token: String, rockets: PlumeRocket) -> Result<Ructe,
#[derive(FromForm, Default, Validate)] #[derive(FromForm, Default, Validate)]
#[validate(schema( #[validate(schema(
function = "passwords_match", function = "passwords_match",
skip_on_field_errors = "false", skip_on_field_errors = false,
message = "Passwords are not matching" message = "Passwords are not matching"
))] ))]
pub struct NewPasswordForm { pub struct NewPasswordForm {

View File

@ -9,7 +9,6 @@ use rocket::{
response::{status, Content, Flash, Redirect}, response::{status, Content, Flash, Redirect},
}; };
use rocket_i18n::I18n; use rocket_i18n::I18n;
use serde_json;
use std::{borrow::Cow, collections::HashMap}; use std::{borrow::Cow, collections::HashMap};
use validator::{Validate, ValidationError, ValidationErrors}; use validator::{Validate, ValidationError, ValidationErrors};
@ -42,7 +41,7 @@ pub fn me(user: Option<User>) -> RespondOrRedirect {
} }
#[get("/@/<name>", rank = 2)] #[get("/@/<name>", rank = 2)]
pub fn details( pub async fn details(
name: String, name: String,
rockets: PlumeRocket, rockets: PlumeRocket,
fetch_rockets: PlumeRocket, fetch_rockets: PlumeRocket,
@ -50,7 +49,7 @@ pub fn details(
update_conn: DbConn, update_conn: DbConn,
) -> Result<Ructe, ErrorPage> { ) -> Result<Ructe, ErrorPage> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let user = User::find_by_fqn(&rockets, &name)?; let user = User::find_by_fqn(&rockets, &name).await?;
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 worker = &rockets.worker; let worker = &rockets.worker;
@ -61,6 +60,7 @@ pub fn details(
worker.execute(move || { worker.execute(move || {
for create_act in user_clone for create_act in user_clone
.fetch_outbox::<Create>() .fetch_outbox::<Create>()
.await
.expect("Remote user: outbox couldn't be fetched") .expect("Remote user: outbox couldn't be fetched")
{ {
match create_act.create_props.object_object::<LicensedArticle>() { match create_act.create_props.object_object::<LicensedArticle>() {
@ -79,6 +79,7 @@ pub fn details(
worker.execute(move || { worker.execute(move || {
for user_id in user_clone for user_id in user_clone
.fetch_followers_ids() .fetch_followers_ids()
.await
.expect("Remote user: fetching followers error") .expect("Remote user: fetching followers error")
{ {
let follower = User::from_id(&fetch_followers_rockets, &user_id, None) let follower = User::from_id(&fetch_followers_rockets, &user_id, None)
@ -101,6 +102,7 @@ pub fn details(
worker.execute(move || { worker.execute(move || {
user_clone user_clone
.refetch(&*update_conn) .refetch(&*update_conn)
.await
.expect("Couldn't update user info"); .expect("Couldn't update user info");
}); });
} }
@ -146,13 +148,13 @@ pub fn dashboard_auth(i18n: I18n) -> Flash<Redirect> {
} }
#[post("/@/<name>/follow")] #[post("/@/<name>/follow")]
pub fn follow( pub async fn follow(
name: String, name: String,
user: User, user: User,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<Flash<Redirect>, ErrorPage> { ) -> Result<Flash<Redirect>, ErrorPage> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let target = User::find_by_fqn(&rockets, &name)?; let target = User::find_by_fqn(&rockets, &name).await?;
let message = if let Ok(follow) = follows::Follow::find(&*conn, user.id, target.id) { let message = if let Ok(follow) = follows::Follow::find(&*conn, user.id, target.id) {
let delete_act = follow.build_undo(&*conn)?; let delete_act = follow.build_undo(&*conn)?;
local_inbox( local_inbox(
@ -190,26 +192,26 @@ pub fn follow(
} }
#[post("/@/<name>/follow", data = "<remote_form>", rank = 2)] #[post("/@/<name>/follow", data = "<remote_form>", rank = 2)]
pub fn follow_not_connected( pub async fn follow_not_connected(
rockets: PlumeRocket, rockets: PlumeRocket,
name: String, name: String,
remote_form: Option<LenientForm<RemoteForm>>, remote_form: Option<LenientForm<RemoteForm>>,
i18n: I18n, i18n: I18n,
) -> Result<RespondOrRedirect, ErrorPage> { ) -> Result<RespondOrRedirect, ErrorPage> {
let target = User::find_by_fqn(&rockets, &name)?; let target = User::find_by_fqn(&rockets, &name).await?;
if let Some(remote_form) = remote_form { if let Some(remote_form) = remote_form {
if let Some(uri) = User::fetch_remote_interact_uri(&remote_form) if let Some(uri) = User::fetch_remote_interact_uri(&remote_form)
.await
.ok() .ok()
.and_then(|uri| { .and_then(|uri| {
rt_format!( Some(uri.replace(
uri, "{uri}",
uri = format!( &format!(
"{}@{}", "{}@{}",
target.fqn, target.fqn,
target.get_instance(&rockets.conn).ok()?.public_domain target.get_instance(&rockets.conn).ok()?.public_domain
) ),
) ))
.ok()
}) })
{ {
Ok(Redirect::to(uri).into()) Ok(Redirect::to(uri).into())
@ -266,15 +268,18 @@ pub fn follow_auth(name: String, i18n: I18n) -> Flash<Redirect> {
} }
#[get("/@/<name>/followers?<page>", rank = 2)] #[get("/@/<name>/followers?<page>", rank = 2)]
pub fn followers( pub async fn followers(
name: String, name: String,
page: Option<Page>, page: Option<Page>,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<Ructe, ErrorPage> { ) -> Result<Ructe, ErrorPage> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
let user = User::find_by_fqn(&rockets, &name)?; let user: User = User::find_by_fqn(&rockets, &name).await?;
let followers_count = user.count_followers(&*conn)?; let followers_count = match user.count_followers(&conn) {
Ok(num) => num,
Err(_) => 0,
};
Ok(render!(users::followers( Ok(render!(users::followers(
&rockets.to_context(), &rockets.to_context(),
@ -293,14 +298,14 @@ pub fn followers(
} }
#[get("/@/<name>/followed?<page>", rank = 2)] #[get("/@/<name>/followed?<page>", rank = 2)]
pub fn followed( pub async fn followed(
name: String, name: String,
page: Option<Page>, page: Option<Page>,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<Ructe, ErrorPage> { ) -> Result<Ructe, ErrorPage> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let page = page.unwrap_or_default(); let page = page.unwrap_or_default();
let user = User::find_by_fqn(&rockets, &name)?; let user = User::find_by_fqn(&rockets, &name).await?;
let followed_count = user.count_followed(conn)?; let followed_count = user.count_followed(conn)?;
Ok(render!(users::followed( Ok(render!(users::followed(
@ -320,12 +325,12 @@ pub fn followed(
} }
#[get("/@/<name>", rank = 1)] #[get("/@/<name>", rank = 1)]
pub fn activity_details( pub async fn activity_details(
name: String, name: String,
rockets: PlumeRocket, rockets: PlumeRocket,
_ap: ApRequest, _ap: ApRequest,
) -> Option<ActivityStream<CustomPerson>> { ) -> Option<ActivityStream<CustomPerson>> {
let user = User::find_by_fqn(&rockets, &name).ok()?; let user: User = User::find_by_fqn(&rockets, &name).await?;
Some(ActivityStream::new(user.to_activity(&*rockets.conn).ok()?)) Some(ActivityStream::new(user.to_activity(&*rockets.conn).ok()?))
} }
@ -412,50 +417,49 @@ pub fn update(
} }
#[post("/@/<name>/delete")] #[post("/@/<name>/delete")]
pub fn delete( pub async fn delete(
name: String, name: String,
user: User, user: User,
mut cookies: Cookies<'_>, mut cookies: Cookies<'_>,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<Flash<Redirect>, ErrorPage> { ) -> Result<Flash<Redirect>, ErrorPage> {
let account = User::find_by_fqn(&rockets, &name)?; let account = User::find_by_fqn(&rockets, &name).await?;
if user.id == account.id { if user.id != account.id {
account.delete(&*rockets.conn, &rockets.searcher)?; return Ok(Flash::error(
let target = User::one_by_instance(&*rockets.conn)?;
let delete_act = account.delete_activity(&*rockets.conn)?;
rockets
.worker
.execute(move || broadcast(&account, delete_act, target));
if let Some(cookie) = cookies.get_private(AUTH_COOKIE) {
cookies.remove_private(cookie);
}
Ok(Flash::success(
Redirect::to(uri!(super::instance::index)),
i18n!(rockets.intl.catalog, "Your account has been deleted."),
))
} else {
Ok(Flash::error(
Redirect::to(uri!(edit: name = name)), Redirect::to(uri!(edit: name = name)),
i18n!( i18n!(
rockets.intl.catalog, rockets.intl.catalog,
"You can't delete someone else's account." "You can't delete someone else's account."
), ),
)) ));
} }
account.delete(&*rockets.conn, &rockets.searcher)?;
let target = User::one_by_instance(&*rockets.conn)?;
let delete_act = account.delete_activity(&*rockets.conn)?;
rockets
.worker
.execute(move || broadcast(&account, delete_act, target));
if let Some(cookie) = cookies.get_private(AUTH_COOKIE) {
cookies.remove_private(cookie);
}
Ok(Flash::success(
Redirect::to(uri!(super::instance::index)),
i18n!(rockets.intl.catalog, "Your account has been deleted."),
))
} }
#[derive(Default, FromForm, Validate)] #[derive(Default, FromForm, Validate)]
#[validate(schema( #[validate(schema(
function = "passwords_match", function = "passwords_match",
skip_on_field_errors = "false", skip_on_field_errors = false,
message = "Passwords are not matching" message = "Passwords are not matching"
))] ))]
pub struct NewUserForm { pub struct NewUserForm {
#[validate( #[validate(
length(min = "1", message = "Username can't be empty"), length(min = 1, message = "Username can't be empty"),
custom( custom(
function = "validate_username", function = "validate_username",
message = "User name is not allowed to contain any of < > & @ ' or \"" message = "User name is not allowed to contain any of < > & @ ' or \""
@ -464,9 +468,9 @@ pub struct NewUserForm {
pub username: String, pub username: String,
#[validate(email(message = "Invalid email"))] #[validate(email(message = "Invalid email"))]
pub email: String, pub email: String,
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))] #[validate(length(min = 8, message = "Password should be at least 8 characters long"))]
pub password: String, pub password: String,
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))] #[validate(length(min = 8, message = "Password should be at least 8 characters long"))]
pub password_confirmation: String, pub password_confirmation: String,
} }
@ -565,37 +569,44 @@ pub fn create(
} }
#[get("/@/<name>/outbox")] #[get("/@/<name>/outbox")]
pub fn outbox(name: String, rockets: PlumeRocket) -> Option<ActivityStream<OrderedCollection>> { pub async fn outbox(
let user = User::find_by_fqn(&rockets, &name).ok()?; name: String,
rockets: PlumeRocket,
) -> Option<ActivityStream<OrderedCollection>> {
let user = User::find_by_fqn(&rockets, &name).await.ok()?;
user.outbox(&*rockets.conn).ok() user.outbox(&*rockets.conn).ok()
} }
#[get("/@/<name>/outbox?<page>")] #[get("/@/<name>/outbox?<page>")]
pub fn outbox_page( pub async fn outbox_page(
name: String, name: String,
page: Page, page: Page,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Option<ActivityStream<OrderedCollectionPage>> { ) -> Option<ActivityStream<OrderedCollectionPage>> {
let user = User::find_by_fqn(&rockets, &name).ok()?; let user = User::find_by_fqn(&rockets, &name).await.ok()?;
user.outbox_page(&*rockets.conn, page.limits()).ok() user.outbox_page(&*rockets.conn, page.limits()).ok()
} }
#[post("/@/<name>/inbox", data = "<data>")] #[post("/@/<name>/inbox", data = "<data>")]
pub fn inbox( pub async fn inbox(
name: String, name: String,
data: inbox::SignedJson<serde_json::Value>, data: inbox::SignedJson<serde_json::Value>,
headers: Headers<'_>, headers: Headers<'_>,
rockets: PlumeRocket, rockets: PlumeRocket,
) -> Result<String, status::BadRequest<&'static str>> { ) -> Result<String, status::BadRequest<&'static str>> {
User::find_by_fqn(&rockets, &name).map_err(|_| status::BadRequest(Some("User not found")))?; User::find_by_fqn(&rockets, &name)
inbox::handle_incoming(rockets, data, headers) .await
.map_err(|_| status::BadRequest(Some("User not found")))?;
inbox::handle_incoming(rockets, data, headers).await
} }
#[get("/@/<name>/followers", rank = 1)] #[get("/@/<name>/followers", rank = 1)]
pub fn ap_followers( pub async fn ap_followers(
name: String, name: String,
rockets: PlumeRocket, rockets: PlumeRocket,
_ap: ApRequest, _ap: ApRequest,
) -> Option<ActivityStream<OrderedCollection>> { ) -> Option<ActivityStream<OrderedCollection>> {
let user = User::find_by_fqn(&rockets, &name).ok()?; let user = User::find_by_fqn(&rockets, &name).await?;
let followers = user let followers = user
.get_followers(&*rockets.conn) .get_followers(&*rockets.conn)
.ok()? .ok()?
@ -615,9 +626,9 @@ pub fn ap_followers(
} }
#[get("/@/<name>/atom.xml")] #[get("/@/<name>/atom.xml")]
pub fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>> { pub async fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>> {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let author = User::find_by_fqn(&rockets, &name).ok()?; let author = User::find_by_fqn(&rockets, &name).await?;
let entries = Post::get_recents_for_author(conn, &author, 15).ok()?; let entries = Post::get_recents_for_author(conn, &author, 15).ok()?;
let uri = Instance::get_local() let uri = Instance::get_local()
.ok()? .ok()?

View File

@ -1,6 +1,5 @@
use rocket::http::ContentType; use rocket::http::ContentType;
use rocket::response::Content; use rocket::response::Content;
use serde_json;
use webfinger::*; use webfinger::*;
use plume_models::{ap_url, blogs::Blog, users::User, PlumeRocket, CONFIG}; use plume_models::{ap_url, blogs::Blog, users::User, PlumeRocket, CONFIG};
@ -43,27 +42,58 @@ pub fn host_meta() -> String {
struct WebfingerResolver; struct WebfingerResolver;
impl Resolver<PlumeRocket> for WebfingerResolver { #[async_trait::async_trait]
fn instance_domain<'a>() -> &'a str { impl AsyncResolver for WebfingerResolver {
type Repo = PlumeRocket;
async fn instance_domain<'a>(&self) -> &'a str {
CONFIG.base_url.as_str() CONFIG.base_url.as_str()
} }
fn find(prefix: Prefix, acct: String, ctx: PlumeRocket) -> Result<Webfinger, ResolverError> { async fn find(
&self,
prefix: Prefix,
acct: String,
ctx: PlumeRocket,
) -> Result<Webfinger, ResolverError> {
match prefix { match prefix {
Prefix::Acct => User::find_by_fqn(&ctx, &acct) Prefix::Acct => User::find_by_fqn(&ctx, &acct)
.await
.and_then(|usr| usr.webfinger(&*ctx.conn)) .and_then(|usr| usr.webfinger(&*ctx.conn))
.or(Err(ResolverError::NotFound)), .or(Err(ResolverError::NotFound)),
Prefix::Group => Blog::find_by_fqn(&ctx, &acct) Prefix::Group => Blog::find_by_fqn(&ctx, &acct)
.await
.and_then(|blog| blog.webfinger(&*ctx.conn)) .and_then(|blog| blog.webfinger(&*ctx.conn))
.or(Err(ResolverError::NotFound)), .or(Err(ResolverError::NotFound)),
Prefix::Custom(_) => Err(ResolverError::NotFound), Prefix::Custom(_) => Err(ResolverError::NotFound),
} }
} }
async fn endpoint<R: Into<String> + Send>(
&self,
resource: R,
resource_repo: PlumeRocket,
) -> Result<Webfinger, ResolverError> {
let resource = resource.into();
let mut parsed_query = resource.splitn(2, ':');
let res_prefix = Prefix::from(parsed_query.next().ok_or(ResolverError::InvalidResource)?);
let res = parsed_query.next().ok_or(ResolverError::InvalidResource)?;
let mut parsed_res = res.splitn(2, '@');
let user = parsed_res.next().ok_or(ResolverError::InvalidResource)?;
let domain = parsed_res.next().ok_or(ResolverError::InvalidResource)?;
if domain == self.instance_domain().await {
self.find(res_prefix, user.to_string(), resource_repo).await
} else {
Err(ResolverError::WrongDomain)
}
}
} }
#[get("/.well-known/webfinger?<resource>")] #[get("/.well-known/webfinger?<resource>")]
pub fn webfinger(resource: String, rockets: PlumeRocket) -> Content<String> { pub async fn webfinger(resource: String, rockets: PlumeRocket) -> Content<String> {
match WebfingerResolver::endpoint(resource, rockets) let wf_resolver = WebfingerResolver;
match wf_resolver
.endpoint(resource, rockets)
.await
.and_then(|wf| serde_json::to_string(&wf).map_err(|_| ResolverError::NotFound)) .and_then(|wf| serde_json::to_string(&wf).map_err(|_| ResolverError::NotFound))
{ {
Ok(wf) => Content(ContentType::new("application", "jrd+json"), wf), Ok(wf) => Content(ContentType::new("application", "jrd+json"), wf),

View File

@ -1,8 +1,7 @@
use plume_models::{notifications::*, users::User, Connection, PlumeRocket}; use plume_models::{notifications::*, users::User, Connection, PlumeRocket};
use crate::templates::Html; use crate::templates::Html;
use rocket::http::hyper::header::{ETag, EntityTag}; use rocket::http::{Header, Method, Status};
use rocket::http::{Method, Status};
use rocket::request::Request; use rocket::request::Request;
use rocket::response::{self, content::Html as HtmlCt, Responder, Response}; use rocket::response::{self, content::Html as HtmlCt, Responder, Response};
use rocket_i18n::Catalog; use rocket_i18n::Catalog;
@ -52,11 +51,12 @@ impl IntoContext for PlumeRocket {
#[derive(Debug)] #[derive(Debug)]
pub struct Ructe(pub Vec<u8>); pub struct Ructe(pub Vec<u8>);
#[rocket::async_trait]
impl<'r> Responder<'r> for Ructe { impl<'r> Responder<'r> for Ructe {
fn respond_to(self, r: &Request<'_>) -> response::Result<'r> { async fn respond_to(self, r: &'r Request<'_>) -> response::Result<'r> {
//if method is not Get or page contain a form, no caching //if method is not Get or page contain a form, no caching
if r.method() != Method::Get || self.0.windows(6).any(|w| w == b"<form ") { if r.method() != Method::Get || self.0.windows(6).any(|w| w == b"<form ") {
return HtmlCt(self.0).respond_to(r); return HtmlCt(self.0).respond_to(r).await;
} }
let mut hasher = DefaultHasher::new(); let mut hasher = DefaultHasher::new();
hasher.write(&self.0); hasher.write(&self.0);
@ -67,12 +67,12 @@ impl<'r> Responder<'r> for Ructe {
{ {
Response::build() Response::build()
.status(Status::NotModified) .status(Status::NotModified)
.header(ETag(EntityTag::strong(etag))) .header(Header::new("ETag", etag))
.ok() .ok()
} else { } else {
Response::build() Response::build()
.merge(HtmlCt(self.0).respond_to(r)?) .merge(HtmlCt(self.0).respond_to(r).await.ok().unwrap())
.header(ETag(EntityTag::strong(etag))) .header(Header::new("ETag", etag))
.ok() .ok()
} }
} }

View File

@ -1,8 +1,12 @@
@use plume_models::CONFIG; @use plume_models::CONFIG;
@use plume_models::instance::Instance; @use plume_models::instance::Instance;
@use rocket::http::RawStr;
@use rocket::request::Form;
@use std::path::Path; @use std::path::Path;
@use std::path::PathBuf;
@use crate::template_utils::*; @use crate::template_utils::*;
@use crate::routes::*; @use crate::routes::*;
@use crate::routes::search::SearchQuery;
@(ctx: BaseContext, title: String, head: Content, header: Content, content: Content) @(ctx: BaseContext, title: String, head: Content, header: Content, content: Content)

View File

@ -2,7 +2,9 @@
@use plume_models::instance::Instance; @use plume_models::instance::Instance;
@use plume_models::posts::Post; @use plume_models::posts::Post;
@use plume_models::users::User; @use plume_models::users::User;
@use rocket::http::RawStr;
@use std::path::Path; @use std::path::Path;
@use std::path::PathBuf;
@use crate::templates::{base, partials::post_card}; @use crate::templates::{base, partials::post_card};
@use crate::template_utils::*; @use crate::template_utils::*;
@use crate::routes::*; @use crate::routes::*;

View File

@ -5,10 +5,12 @@
@use crate::template_utils::*; @use crate::template_utils::*;
@use crate::templates::base; @use crate::templates::base;
@use crate::templates::partials::image_select; @use crate::templates::partials::image_select;
@use crate::routes::Page;
@use crate::routes::blogs; @use crate::routes::blogs;
@use crate::routes::blogs::EditForm; @use crate::routes::blogs::EditForm;
@use crate::routes::medias; @use crate::routes::medias;
@(ctx: BaseContext, blog: &Blog, medias: Vec<Media>, form: &EditForm, errors: ValidationErrors) @(ctx: BaseContext, blog: &Blog, medias: Vec<Media>, form: &EditForm, errors: ValidationErrors)
@:base(ctx, i18n!(ctx.1, "Edit \"{}\""; &blog.title), {}, { @:base(ctx, i18n!(ctx.1, "Edit \"{}\""; &blog.title), {}, {

View File

@ -3,7 +3,9 @@
@use plume_models::posts::Post; @use plume_models::posts::Post;
@use plume_models::tags::Tag; @use plume_models::tags::Tag;
@use plume_models::users::User; @use plume_models::users::User;
@use rocket::http::RawStr;
@use std::path::Path; @use std::path::Path;
@use std::path::PathBuf;
@use validator::ValidationErrors; @use validator::ValidationErrors;
@use crate::templates::{base, partials::comment}; @use crate::templates::{base, partials::comment};
@use crate::template_utils::*; @use crate::template_utils::*;