Compare commits

..

7 Commits

Author SHA1 Message Date
Ebrahim 6405ba57ee add test comment 😅 2020-06-23 08:51:31 +04:30
Gelez d99b42582d Add Persian to the list of languages (#782) 2020-06-17 18:33:58 +02:00
KITAITI Makoto 92a386277b Switchable tokenizer (#776)
* [REFACTORING]Rename whitespace_tokenizer to tag_tokenizer for
registration

Name representing its purpose is preferred.

* Add lindera-tantivy to plume-model's dependencies

* Install lindera-tantivy

* Add SearchTokenizerConfig struct

* Add search tokenizers to config option

* Use CONFIG for tokenizers

* Use enum to hold tokenizer config instead of initializing on config phase

* Use guard instead of duplicate default values

* Use as_deref() instead of guard

* Move SearchTokenizer from plume-models to plume-models::search::tokenizer

* Rename SearchTokenizer to TokenizerKind

* Define SearchTokenierConfig::determine_tokenizer()

* Use determine_tokenizer in SearchTokenizerConfig::init()

* Pass tokenizer config to Searcher methods

* Add LowerCase filter to Lindera tokenizer

* Add test for Lindera tokenizer

* Define SEARCH_LANG env to specify tokenizers set

* Run cargo fmt

* Make Lindera tokenizer optional

* Fix typos
2020-06-17 16:57:28 +02:00
Gelez 297d9fcf40 Don't show boosts and likes for "all" and "local" in timelines (#781)
Fixes #711
2020-06-15 19:50:28 +02:00
KITAITI Makoto ef70cb93e6 Upgrade Tantivy to v0.12.0 (#771)
* Upgrade Tantivy to 0.12.0

* Follow Tantivy Tokenizer's new type definition

* Wrap tokenizers with TextAnalyzer to use filter methods

* Replace async IndexWriter::garbage_collect_files with sync functions

* Update Cargo.toml
2020-05-20 13:31:45 +02:00
Daniel Watkins efb76a3c17 remove dependency on runtime-fmt (#773)
Per the issue, "runtime-fmt uses perma-unstable rust APIs and is
therefore susceptible to breakage".

This replaces the two calls to rt_format! with .replace() and drops the
dependency.

Fixes #769
2020-05-18 20:18:07 +02:00
KITAITI Makoto 197f0d7ecd Add test for hash including ZWSP (#772)
* Add test for hash including ZWSP

* Run cargo fmt
2020-05-17 13:53:31 +02:00
60 changed files with 3016 additions and 2595 deletions
-1
View File
@@ -18,4 +18,3 @@ tags.*
search_index search_index
.buildconfig .buildconfig
__pycache__ __pycache__
.vscode/
Generated
+2239 -1758
View File
File diff suppressed because it is too large Load Diff
+10 -12
View File
@@ -8,7 +8,6 @@ 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"
@@ -21,8 +20,9 @@ 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 = { git = "https://github.com/SergioBenitez/Rocket", rev = "async" } rocket = "0.4.2"
rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", rev = "async" , features = ["json"] } rocket_contrib = { version = "0.4.2", features = ["json"] }
rocket_i18n = { git = "https://github.com/Plume-org/rocket_i18n", rev = "e922afa7c366038b3433278c03b1456b346074f2" }
rpassword = "4.0" rpassword = "4.0"
scheduled-thread-pool = "0.2.2" scheduled-thread-pool = "0.2.2"
serde = "1.0" serde = "1.0"
@@ -30,10 +30,9 @@ 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"
tokio = "0.2" validator = "0.8"
validator = "0.10" validator_derive = "0.8"
validator_derive = "0.10" webfinger = "0.4.1"
webfinger = { git = "https://github.com/Plume-org/webfinger", rev = "4e8f12810c4a7ba7a07bbcb722cd265fdff512b6", features = ["async"] }
[[bin]] [[bin]]
name = "plume" name = "plume"
@@ -65,11 +64,9 @@ path = "plume-common"
[dependencies.plume-models] [dependencies.plume-models]
path = "plume-models" path = "plume-models"
[dependencies.rocket_i18n] [dependencies.rocket_csrf]
git = "https://github.com/Plume-org/rocket_i18n" git = "https://github.com/fdb-hiroshima/rocket_csrf"
branch = "go-async" rev = "29910f2829e7e590a540da3804336577b48c7b31"
default-features = false
features = ["rocket"]
[build-dependencies] [build-dependencies]
ructe = "0.9.0" ructe = "0.9.0"
@@ -81,6 +78,7 @@ postgres = ["plume-models/postgres", "diesel/postgres"]
sqlite = ["plume-models/sqlite", "diesel/sqlite"] sqlite = ["plume-models/sqlite", "diesel/sqlite"]
debug-mailer = [] debug-mailer = []
test = [] test = []
search-lindera = ["plume-models/search-lindera"]
[workspace] [workspace]
members = ["plume-api", "plume-cli", "plume-models", "plume-common", "plume-front", "plume-macro"] members = ["plume-api", "plume-cli", "plume-models", "plume-common", "plume-front", "plume-macro"]
+2
View File
@@ -1,3 +1,5 @@
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::*};
+1
View File
@@ -23,3 +23,4 @@ path = "../plume-models"
[features] [features]
postgres = ["plume-models/postgres", "diesel/postgres"] postgres = ["plume-models/postgres", "diesel/postgres"]
sqlite = ["plume-models/sqlite", "diesel/sqlite"] sqlite = ["plume-models/sqlite", "diesel/sqlite"]
search-lindera = ["plume-models/search-lindera"]
+3 -2
View File
@@ -82,7 +82,7 @@ fn init<'a>(args: &ArgMatches<'a>, conn: &Connection) {
} }
}; };
if can_do || force { if can_do || force {
let searcher = Searcher::create(&path).unwrap(); let searcher = Searcher::create(&path, &CONFIG.search_tokenizers).unwrap();
refill(args, conn, Some(searcher)); refill(args, conn, Some(searcher));
} else { } else {
eprintln!( eprintln!(
@@ -98,7 +98,8 @@ fn refill<'a>(args: &ArgMatches<'a>, conn: &Connection, searcher: Option<Searche
Some(path) => Path::new(path).join("search_index"), Some(path) => Path::new(path).join("search_index"),
None => Path::new(&CONFIG.search_index).to_path_buf(), None => Path::new(&CONFIG.search_index).to_path_buf(),
}; };
let searcher = searcher.unwrap_or_else(|| Searcher::open(&path).unwrap()); let searcher =
searcher.unwrap_or_else(|| Searcher::open(&path, &CONFIG.search_tokenizers).unwrap());
searcher.fill(conn).expect("Couldn't import post"); searcher.fill(conn).expect("Couldn't import post");
println!("Commiting result"); println!("Commiting result");
+5 -9
View File
@@ -6,22 +6,22 @@ edition = "2018"
[dependencies] [dependencies]
activitypub = "0.1.1" activitypub = "0.1.1"
activitystreams-derive = "0.2" activitystreams-derive = "0.1.1"
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.13" hyper = "0.12.33"
openssl = "0.10.22" openssl = "0.10.22"
rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "async" } rocket = "0.4.0"
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.2" tokio = "0.1.22"
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,7 +31,3 @@ 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"
+10 -11
View File
@@ -86,7 +86,7 @@ where
/// - the context to be passed to each handler. /// - the context to be passed to each handler.
/// - the activity /// - the activity
/// - the reason it has not been handled yet /// - the reason it has not been handled yet
NotHandled(&'a mut C, serde_json::Value, InboxError<E>), NotHandled(&'a C, serde_json::Value, InboxError<E>),
/// A matching handler have been found but failed /// A matching handler have been found but failed
/// ///
@@ -139,16 +139,16 @@ where
/// ///
/// - `ctx`: the context to pass to each handler /// - `ctx`: the context to pass to each handler
/// - `json`: the JSON representation of the incoming activity /// - `json`: the JSON representation of the incoming activity
pub fn handle(ctx: &'a mut C, json: serde_json::Value) -> Inbox<'a, C, E, R> { pub fn handle(ctx: &'a C, json: serde_json::Value) -> Inbox<'a, C, E, R> {
Inbox::NotHandled(ctx, json, InboxError::NoMatch) Inbox::NotHandled(ctx, json, InboxError::NoMatch)
} }
/// Registers an handler on this Inbox. /// Registers an handler on this Inbox.
pub fn with<A, V, M>(self) -> Inbox<'a, C, E, R> pub fn with<A, V, M>(self) -> Inbox<'a, C, E, R>
where where
A: AsActor<&'a mut C> + FromId<C, Error = E>, A: AsActor<&'a C> + FromId<C, Error = E>,
V: activitypub::Activity, V: activitypub::Activity,
M: AsObject<A, V, &'a mut C, Error = E> + FromId<C, Error = E>, M: AsObject<A, V, &'a C, Error = E> + FromId<C, Error = E>,
M::Output: Into<R>, M::Output: Into<R>,
{ {
if let Inbox::NotHandled(ctx, mut act, e) = self { if let Inbox::NotHandled(ctx, mut act, e) = self {
@@ -264,7 +264,7 @@ pub trait FromId<C>: Sized {
/// - `object`: optional object that will be used if the object was not found in the database /// - `object`: optional object that will be used if the object was not found in the database
/// If absent, the ID will be dereferenced. /// If absent, the ID will be dereferenced.
fn from_id( fn from_id(
ctx: &mut C, ctx: &C,
id: &str, id: &str,
object: Option<Self::Object>, object: Option<Self::Object>,
) -> Result<Self, (Option<serde_json::Value>, Self::Error)> { ) -> Result<Self, (Option<serde_json::Value>, Self::Error)> {
@@ -279,9 +279,8 @@ 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)> {
// Use blocking reqwest API here, since defer cannot be async (yet) reqwest::ClientBuilder::new()
reqwest::blocking::Client::builder() .connect_timeout(Some(std::time::Duration::from_secs(5)))
.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)
@@ -297,7 +296,7 @@ pub trait FromId<C>: Sized {
) )
.send() .send()
.map_err(|_| (None, InboxError::DerefError)) .map_err(|_| (None, InboxError::DerefError))
.and_then(|r| { .and_then(|mut 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)))?;
@@ -308,10 +307,10 @@ pub trait FromId<C>: Sized {
} }
/// Builds a `Self` from its ActivityPub representation /// Builds a `Self` from its ActivityPub representation
fn from_activity(ctx: &mut C, activity: Self::Object) -> Result<Self, Self::Error>; fn from_activity(ctx: &C, activity: Self::Object) -> Result<Self, Self::Error>;
/// Tries to find a `Self` with a given ID (`id`), using `ctx` (a database) /// Tries to find a `Self` with a given ID (`id`), using `ctx` (a database)
fn from_db(ctx: &mut C, id: &str) -> Result<Self, Self::Error>; fn from_db(ctx: &C, id: &str) -> Result<Self, Self::Error>;
} }
/// Should be implemented by anything representing an ActivityPub actor. /// Should be implemented by anything representing an ActivityPub actor.
+37 -42
View File
@@ -1,12 +1,14 @@
use activitypub::{Activity, Link, Object}; use activitypub::{Activity, Link, Object};
use array_tool::vec::Uniq; use array_tool::vec::Uniq;
use reqwest::ClientBuilder; use reqwest::r#async::ClientBuilder;
use rocket::{ use rocket::{
http::Status, http::Status,
request::{FromRequest, Request}, request::{FromRequest, Request},
response::{Responder, Response, Result}, response::{Responder, Response},
Outcome, Outcome,
}; };
use serde_json;
use tokio::prelude::*;
use self::sign::Signable; use self::sign::Signable;
@@ -60,45 +62,39 @@ impl<T> ActivityStream<T> {
ActivityStream(t) ActivityStream(t)
} }
} }
#[rocket::async_trait]
impl<'r, O: Object + Send + 'r> Responder<'r> for ActivityStream<O> { impl<'r, O: Object> Responder<'r> for ActivityStream<O> {
async fn respond_to(self, request: &'r Request<'_>) -> Result<'r> { fn respond_to(self, request: &Request<'_>) -> Result<Response<'r>, Status> {
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();
let result = serde_json::to_string(&json).map_err(rocket::response::Debug); serde_json::to_string(&json).respond_to(request).map(|r| {
match result.respond_to(request).await { Response::build_from(r)
Ok(r) => Response::build_from(r)
.raw_header("Content-Type", "application/activity+json") .raw_header("Content-Type", "application/activity+json")
.ok(), .finalize()
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 = ();
async fn from_request(request: &'a Request<'r>) -> Outcome<Self, (Status, Self::Error), ()> { 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| { .map(|ct| match ct.trim() {
match ct.trim() { // bool for Forward: true if found a valid Content-Type for Plume first (HTML), false otherwise
// 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/ld+json;profile=\"https://w3.org/ns/activitystreams\""
"application/ld+json; profile=\"https://w3.org/ns/activitystreams\"" | "application/activity+json"
| "application/ld+json;profile=\"https://w3.org/ns/activitystreams\"" | "application/ld+json" => Outcome::Success(ApRequest),
| "application/activity+json" "text/html" => Outcome::Forward(true),
| "application/ld+json" => Outcome::Success(ApRequest), _ => Outcome::Forward(false),
"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()) {
@@ -134,38 +130,36 @@ where
.sign(sender) .sign(sender)
.expect("activity_pub::broadcast: signature error"); .expect("activity_pub::broadcast: signature error");
let rt = tokio::runtime::Builder::new() let mut rt = tokio::runtime::current_thread::Runtime::new()
.threaded_scheduler()
.build()
.expect("Error while initializing tokio runtime for federation"); .expect("Error while initializing tokio runtime for federation");
let client = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5))
.build()
.expect("Can't build client");
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));
let sig = request::signature(sender, &headers) rt.spawn(
.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("Signature", sig) .header(
"Signature",
request::signature(sender, &headers)
.expect("activity_pub::broadcast: request signature error"),
)
.body(body) .body(body)
.send() .send()
.await .and_then(|r| r.into_body().concat2())
.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)]
@@ -209,7 +203,8 @@ pub struct PublicKey {
pub public_key_pem: Option<serde_json::Value>, pub public_key_pem: Option<serde_json::Value>,
} }
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, UnitString)]
#[activitystreams(Hashtag)]
pub struct HashtagType; pub struct HashtagType;
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)] #[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
+1
View File
@@ -1,3 +1,4 @@
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};
+3
View File
@@ -1,7 +1,10 @@
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>) {
+2
View File
@@ -2,7 +2,9 @@
#[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]
+1
View File
@@ -442,6 +442,7 @@ mod tests {
("not_a#hashtag", vec![]), ("not_a#hashtag", vec![]),
("#نرم‌افزار_آزاد", vec!["نرم‌افزار_آزاد"]), ("#نرم‌افزار_آزاد", vec!["نرم‌افزار_آزاد"]),
("[#hash in link](https://example.org/)", vec![]), ("[#hash in link](https://example.org/)", vec![]),
("#zwsp\u{200b}inhash", vec!["zwsp"]),
]; ];
for (md, mentions) in tests { for (md, mentions) in tests {
+4
View File
@@ -19,6 +19,7 @@ init_i18n!(
en, en,
eo, eo,
es, es,
fa,
fr, fr,
gl, gl,
hi, hi,
@@ -45,6 +46,9 @@ lazy_static! {
let lang = js! { return navigator.language }.into_string().unwrap(); let lang = js! { return navigator.language }.into_string().unwrap();
let lang = lang.splitn(2, '-').next().unwrap_or("en"); let lang = lang.splitn(2, '-').next().unwrap_or("en");
// Force a language (Add .env setting)
//let lang = "fa"; ???
let english_position = catalogs let english_position = catalogs
.iter() .iter()
.position(|(language_code, _)| *language_code == "en") .position(|(language_code, _)| *language_code == "en")
+9 -18
View File
@@ -10,26 +10,27 @@ 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 = { git = "https://github.com/SergioBenitez/Rocket", rev = "async" } rocket = "0.4.0"
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.12.0"
tokio = "0.2"
url = "2.1" url = "2.1"
walkdir = "2.2" walkdir = "2.2"
webfinger = { git = "https://github.com/Plume-org/webfinger", rev = "4e8f12810c4a7ba7a07bbcb722cd265fdff512b6", features = ["async"] } webfinger = "0.4.1"
whatlang = "0.7.1" whatlang = "0.7.1"
shrinkwraprs = "0.3" shrinkwraprs = "0.2.1"
diesel-derive-newtype = "0.1.2" diesel-derive-newtype = "0.1.2"
glob = "0.3.0" glob = "0.3.0"
lindera-tantivy = { version = "0.1.2", optional = true }
[dependencies.chrono] [dependencies.chrono]
features = ["serde"] features = ["serde"]
@@ -48,20 +49,10 @@ 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.4.0" diesel_migrations = "1.3.0"
[features] [features]
postgres = ["diesel/postgres", "plume-macro/postgres" ] postgres = ["diesel/postgres", "plume-macro/postgres" ]
sqlite = ["diesel/sqlite", "plume-macro/sqlite" ] sqlite = ["diesel/sqlite", "plume-macro/sqlite" ]
search-lindera = ["lindera-tantivy"]
+4 -6
View File
@@ -8,12 +8,11 @@ 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 = ();
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> request::Outcome<Admin, ()> {
let user = try_outcome!(User::from_request(request).await); let user = request.guard::<User>()?;
if user.is_admin() { if user.is_admin() {
Outcome::Success(Admin(user)) Outcome::Success(Admin(user))
} else { } else {
@@ -25,12 +24,11 @@ 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 = ();
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> request::Outcome<Moderator, ()> {
let user = try_outcome!(User::from_request(request).await); let user = request.guard::<User>()?;
if user.is_moderator() { if user.is_moderator() {
Outcome::Success(Moderator(user)) Outcome::Success(Moderator(user))
} else { } else {
+16 -20
View File
@@ -76,36 +76,32 @@ 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;
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> request::Outcome<ApiToken, TokenError> {
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(' ');
if let Some(auth_type) = parsed_header.next() { let auth_type = parsed_header.next().map_or_else(
if let Some(val) = parsed_header.next() { || Outcome::Failure((Status::BadRequest, TokenError::NoType)),
if auth_type == "Bearer" { Outcome::Success,
if let Outcome::Success(conn) = DbConn::from_request(request).await { )?;
if let Ok(token) = ApiToken::find_by_value(&*conn, val) { let val = parsed_header.next().map_or_else(
return Outcome::Success(token); || Outcome::Failure((Status::BadRequest, TokenError::NoValue)),
} Outcome::Success,
} else { )?;
return Outcome::Failure((
Status::InternalServerError, if auth_type == "Bearer" {
TokenError::DbError, let conn = request
)); .guard::<DbConn>()
} .map_failure(|_| (Status::InternalServerError, TokenError::DbError))?;
} if let Ok(token) = ApiToken::find_by_value(&*conn, val) {
} else { return Outcome::Success(token);
return Outcome::Failure((Status::BadRequest, TokenError::NoValue));
} }
} else {
return Outcome::Failure((Status::BadRequest, TokenError::NoType));
} }
Outcome::Forward(()) Outcome::Forward(())
+14 -12
View File
@@ -20,6 +20,7 @@ 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::*;
@@ -70,8 +71,7 @@ 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 = inserted.outbox_url = instance.compute_box(BLOG_PREFIX, &inserted.actor_id, "outbox");
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 async fn find_by_fqn(c: &mut PlumeRocket, fqn: &str) -> Result<Blog> { pub 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,13 +140,12 @@ 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).await Blog::fetch_from_webfinger(c, fqn)
} }
} }
async fn fetch_from_webfinger(c: &mut PlumeRocket, acct: &str) -> Result<Blog> { 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")))
@@ -340,11 +339,11 @@ impl FromId<PlumeRocket> for Blog {
type Error = Error; type Error = Error;
type Object = CustomGroup; type Object = CustomGroup;
fn from_db(c: &mut PlumeRocket, id: &str) -> Result<Self> { fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Self::find_by_ap_url(&c.conn, id) Self::find_by_ap_url(&c.conn, id)
} }
fn from_activity(c: &mut PlumeRocket, acct: CustomGroup) -> Result<Self> { fn from_activity(c: &PlumeRocket, acct: CustomGroup) -> Result<Self> {
let url = Url::parse(&acct.object.object_props.id_string()?)?; let url = Url::parse(&acct.object.object_props.id_string()?)?;
let inst = url.host_str()?; let inst = url.host_str()?;
let instance = Instance::find_by_domain(&c.conn, inst).or_else(|_| { let instance = Instance::find_by_domain(&c.conn, inst).or_else(|_| {
@@ -436,7 +435,7 @@ impl FromId<PlumeRocket> for Blog {
} }
} }
impl AsActor<&mut PlumeRocket> for Blog { impl AsActor<&PlumeRocket> for Blog {
fn get_inbox_url(&self) -> String { fn get_inbox_url(&self) -> String {
self.inbox_url.clone() self.inbox_url.clone()
} }
@@ -499,6 +498,7 @@ pub(crate) mod tests {
use super::*; use super::*;
use crate::{ use crate::{
blog_authors::*, blog_authors::*,
config::CONFIG,
instance::tests as instance_tests, instance::tests as instance_tests,
medias::NewMedia, medias::NewMedia,
search::tests::get_searcher, search::tests::get_searcher,
@@ -768,7 +768,9 @@ pub(crate) mod tests {
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let (_, blogs) = fill_database(conn); let (_, blogs) = fill_database(conn);
blogs[0].delete(conn, &get_searcher()).unwrap(); blogs[0]
.delete(conn, &get_searcher(&CONFIG.search_tokenizers))
.unwrap();
assert!(Blog::get(conn, blogs[0].id).is_err()); assert!(Blog::get(conn, blogs[0].id).is_err());
Ok(()) Ok(())
}) })
@@ -778,7 +780,7 @@ pub(crate) mod tests {
fn delete_via_user() { fn delete_via_user() {
let conn = &db(); let conn = &db();
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let searcher = get_searcher(); let searcher = get_searcher(&CONFIG.search_tokenizers);
let (user, _) = fill_database(conn); let (user, _) = fill_database(conn);
let b1 = Blog::insert( let b1 = Blog::insert(
+16 -19
View File
@@ -24,6 +24,7 @@ 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)]
@@ -104,7 +105,7 @@ impl Comment {
.unwrap_or(false) .unwrap_or(false)
} }
pub async fn to_activity(&self, c: &mut PlumeRocket) -> Result<Note> { pub 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(),
@@ -130,23 +131,19 @@ impl Comment {
.set_published_string(chrono::Utc::now().to_rfc3339())?; .set_published_string(chrono::Utc::now().to_rfc3339())?;
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(
let mut tag_link_vec = vec![]; mentions
let mut iter = mentions.into_iter(); .into_iter()
while let Some(m) = iter.next() { .filter_map(|m| Mention::build_activity(c, &m).ok())
if let Ok(a) = Mention::build_activity(c, &m).await { .collect::<Vec<link::Mention>>(),
tag_link_vec.push(a); )?;
}
}
note.object_props.set_tag_link_vec(tag_link_vec)?;
Ok(note) Ok(note)
} }
pub async fn create_activity(&self, c: &mut PlumeRocket) -> Result<Create> { pub 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).await?; let note = self.to_activity(c)?;
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())?;
@@ -201,11 +198,11 @@ impl FromId<PlumeRocket> for Comment {
type Error = Error; type Error = Error;
type Object = Note; type Object = Note;
fn from_db(c: &mut PlumeRocket, id: &str) -> Result<Self> { fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Self::find_by_ap_url(&c.conn, id) Self::find_by_ap_url(&c.conn, id)
} }
fn from_activity(c: &mut PlumeRocket, note: Note) -> Result<Self> { fn from_activity(c: &PlumeRocket, note: Note) -> Result<Self> {
let conn = &*c.conn; let conn = &*c.conn;
let comm = { let comm = {
let previous_url = note.object_props.in_reply_to.as_ref()?.as_str()?; let previous_url = note.object_props.in_reply_to.as_ref()?.as_str()?;
@@ -325,21 +322,21 @@ impl FromId<PlumeRocket> for Comment {
} }
} }
impl AsObject<User, Create, &mut PlumeRocket> for Comment { impl AsObject<User, Create, &PlumeRocket> for Comment {
type Error = Error; type Error = Error;
type Output = Self; type Output = Self;
fn activity(self, _c: &mut PlumeRocket, _actor: User, _id: &str) -> Result<Self> { fn activity(self, _c: &PlumeRocket, _actor: User, _id: &str) -> Result<Self> {
// The actual creation takes place in the FromId impl // The actual creation takes place in the FromId impl
Ok(self) Ok(self)
} }
} }
impl AsObject<User, Delete, &mut PlumeRocket> for Comment { impl AsObject<User, Delete, &PlumeRocket> for Comment {
type Error = Error; type Error = Error;
type Output = (); type Output = ();
fn activity(self, c: &mut PlumeRocket, actor: User, _id: &str) -> Result<()> { fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
if self.author_id != actor.id { if self.author_id != actor.id {
return Err(Error::Unauthorized); return Err(Error::Unauthorized);
} }
+53
View File
@@ -1,3 +1,4 @@
use crate::search::TokenizerKind as SearchTokenizer;
use rocket::config::Limits; use rocket::config::Limits;
use rocket::Config as RocketConfig; use rocket::Config as RocketConfig;
use std::env::{self, var}; use std::env::{self, var};
@@ -14,6 +15,7 @@ pub struct Config {
pub db_max_size: Option<u32>, pub db_max_size: Option<u32>,
pub db_min_idle: Option<u32>, pub db_min_idle: Option<u32>,
pub search_index: String, pub search_index: String,
pub search_tokenizers: SearchTokenizerConfig,
pub rocket: Result<RocketConfig, RocketError>, pub rocket: Result<RocketConfig, RocketError>,
pub logo: LogoConfig, pub logo: LogoConfig,
pub default_theme: String, pub default_theme: String,
@@ -188,6 +190,56 @@ impl Default for LogoConfig {
} }
} }
pub struct SearchTokenizerConfig {
pub tag_tokenizer: SearchTokenizer,
pub content_tokenizer: SearchTokenizer,
pub property_tokenizer: SearchTokenizer,
}
impl SearchTokenizerConfig {
pub fn init() -> Self {
use SearchTokenizer::*;
match var("SEARCH_LANG").ok().as_deref() {
Some("ja") => {
#[cfg(not(feature = "search-lindera"))]
panic!("You need build Plume with search-lindera feature, or execute it with SEARCH_TAG_TOKENIZER=ngram and SEARCH_CONTENT_TOKENIZER=ngram to enable Japanese search feature");
#[cfg(feature = "search-lindera")]
Self {
tag_tokenizer: Self::determine_tokenizer("SEARCH_TAG_TOKENIZER", Lindera),
content_tokenizer: Self::determine_tokenizer(
"SEARCH_CONTENT_TOKENIZER",
Lindera,
),
property_tokenizer: Ngram,
}
}
_ => Self {
tag_tokenizer: Self::determine_tokenizer("SEARCH_TAG_TOKENIZER", Whitespace),
content_tokenizer: Self::determine_tokenizer("SEARCH_CONTENT_TOKENIZER", Simple),
property_tokenizer: Ngram,
},
}
}
fn determine_tokenizer(var_name: &str, default: SearchTokenizer) -> SearchTokenizer {
use SearchTokenizer::*;
match var(var_name).ok().as_deref() {
Some("simple") => Simple,
Some("ngram") => Ngram,
Some("whitespace") => Whitespace,
Some("lindera") => {
#[cfg(not(feature = "search-lindera"))]
panic!("You need build Plume with search-lindera feature to use Lindera tokenizer");
#[cfg(feature = "search-lindera")]
Lindera
}
_ => default,
}
}
}
lazy_static! { lazy_static! {
pub static ref CONFIG: Config = Config { pub static ref CONFIG: Config = Config {
base_url: var("BASE_URL").unwrap_or_else(|_| format!( base_url: var("BASE_URL").unwrap_or_else(|_| format!(
@@ -209,6 +261,7 @@ lazy_static! {
#[cfg(feature = "sqlite")] #[cfg(feature = "sqlite")]
database_url: var("DATABASE_URL").unwrap_or_else(|_| format!("{}.sqlite", DB_NAME)), database_url: var("DATABASE_URL").unwrap_or_else(|_| format!("{}.sqlite", DB_NAME)),
search_index: var("SEARCH_INDEX").unwrap_or_else(|_| "search_index".to_owned()), search_index: var("SEARCH_INDEX").unwrap_or_else(|_| "search_index".to_owned()),
search_tokenizers: SearchTokenizerConfig::init(),
rocket: get_rocket_config(), rocket: get_rocket_config(),
logo: LogoConfig::default(), logo: LogoConfig::default(),
default_theme: var("DEFAULT_THEME").unwrap_or_else(|_| "default-light".to_owned()), default_theme: var("DEFAULT_THEME").unwrap_or_else(|_| "default-light".to_owned()),
+6 -6
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, Outcome, Request, State,
}; };
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 = ();
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
match DbConn::from_request(request).await { let pool = request.guard::<State<'_, DbPool>>()?;
Outcome::Success(a) => Outcome::Success(a), match pool.get() {
_ => Outcome::Failure((Status::ServiceUnavailable, ())), Ok(conn) => Outcome::Success(DbConn(conn)),
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())),
} }
} }
} }
+6 -6
View File
@@ -136,11 +136,11 @@ impl Follow {
} }
} }
impl AsObject<User, FollowAct, &mut PlumeRocket> for User { impl AsObject<User, FollowAct, &PlumeRocket> for User {
type Error = Error; type Error = Error;
type Output = Follow; type Output = Follow;
fn activity(self, c: &mut PlumeRocket, actor: User, id: &str) -> Result<Follow> { fn activity(self, c: &PlumeRocket, actor: User, id: &str) -> Result<Follow> {
// Mastodon (at least) requires the full Follow object when accepting it, // Mastodon (at least) requires the full Follow object when accepting it,
// so we rebuilt it here // so we rebuilt it here
let mut follow = FollowAct::default(); let mut follow = FollowAct::default();
@@ -156,11 +156,11 @@ impl FromId<PlumeRocket> for Follow {
type Error = Error; type Error = Error;
type Object = FollowAct; type Object = FollowAct;
fn from_db(c: &mut PlumeRocket, id: &str) -> Result<Self> { fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Follow::find_by_ap_url(&c.conn, id) Follow::find_by_ap_url(&c.conn, id)
} }
fn from_activity(c: &mut PlumeRocket, follow: FollowAct) -> Result<Self> { fn from_activity(c: &PlumeRocket, follow: FollowAct) -> Result<Self> {
let actor = let actor =
User::from_id(c, &follow.follow_props.actor_link::<Id>()?, None).map_err(|(_, e)| e)?; User::from_id(c, &follow.follow_props.actor_link::<Id>()?, None).map_err(|(_, e)| e)?;
@@ -170,11 +170,11 @@ impl FromId<PlumeRocket> for Follow {
} }
} }
impl AsObject<User, Undo, &mut PlumeRocket> for Follow { impl AsObject<User, Undo, &PlumeRocket> for Follow {
type Error = Error; type Error = Error;
type Output = (); type Output = ();
fn activity(self, c: &mut PlumeRocket, actor: User, _id: &str) -> Result<()> { fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
let conn = &*c.conn; let conn = &*c.conn;
if self.follower_id == actor.id { if self.follower_id == actor.id {
diesel::delete(&self).execute(conn)?; diesel::delete(&self).execute(conn)?;
+1 -2
View File
@@ -6,11 +6,10 @@ 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 = ();
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, ()> { 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);
+3 -2
View File
@@ -1,4 +1,5 @@
use activitypub::activity::*; use activitypub::activity::*;
use serde_json;
use crate::{ use crate::{
comments::Comment, comments::Comment,
@@ -45,7 +46,7 @@ impl_into_inbox_result! {
Reshare => Reshared Reshare => Reshared
} }
pub fn inbox(ctx: &mut PlumeRocket, act: serde_json::Value) -> Result<InboxResult, Error> { pub fn inbox(ctx: &PlumeRocket, act: serde_json::Value) -> Result<InboxResult, Error> {
Inbox::handle(ctx, act) Inbox::handle(ctx, act)
.with::<User, Announce, Post>() .with::<User, Announce, Post>()
.with::<User, Create, Comment>() .with::<User, Create, Comment>()
@@ -72,7 +73,7 @@ pub(crate) mod tests {
use diesel::Connection; use diesel::Connection;
pub fn fill_database( pub fn fill_database(
rockets: &mut PlumeRocket, rockets: &PlumeRocket,
) -> ( ) -> (
Vec<crate::posts::Post>, Vec<crate::posts::Post>,
Vec<crate::users::User>, Vec<crate::users::User>,
+1 -8
View File
@@ -4,7 +4,6 @@
#[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]
@@ -76,12 +75,6 @@ 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
@@ -327,7 +320,7 @@ mod tests {
pub fn rockets() -> super::PlumeRocket { pub fn rockets() -> super::PlumeRocket {
super::PlumeRocket { super::PlumeRocket {
conn: db_conn::DbConn((*DB_POOL).get().unwrap()), conn: db_conn::DbConn((*DB_POOL).get().unwrap()),
searcher: Arc::new(search::tests::get_searcher()), searcher: Arc::new(search::tests::get_searcher(&CONFIG.search_tokenizers)),
worker: Arc::new(ScheduledThreadPool::new(2)), worker: Arc::new(ScheduledThreadPool::new(2)),
user: None, user: None,
} }
+6 -6
View File
@@ -83,11 +83,11 @@ impl Like {
} }
} }
impl AsObject<User, activity::Like, &mut PlumeRocket> for Post { impl AsObject<User, activity::Like, &PlumeRocket> for Post {
type Error = Error; type Error = Error;
type Output = Like; type Output = Like;
fn activity(self, c: &mut PlumeRocket, actor: User, id: &str) -> Result<Like> { fn activity(self, c: &PlumeRocket, actor: User, id: &str) -> Result<Like> {
let res = Like::insert( let res = Like::insert(
&c.conn, &c.conn,
NewLike { NewLike {
@@ -107,11 +107,11 @@ impl FromId<PlumeRocket> for Like {
type Error = Error; type Error = Error;
type Object = activity::Like; type Object = activity::Like;
fn from_db(c: &mut PlumeRocket, id: &str) -> Result<Self> { fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Like::find_by_ap_url(&c.conn, id) Like::find_by_ap_url(&c.conn, id)
} }
fn from_activity(c: &mut PlumeRocket, act: activity::Like) -> Result<Self> { fn from_activity(c: &PlumeRocket, act: activity::Like) -> Result<Self> {
let res = Like::insert( let res = Like::insert(
&c.conn, &c.conn,
NewLike { NewLike {
@@ -129,11 +129,11 @@ impl FromId<PlumeRocket> for Like {
} }
} }
impl AsObject<User, activity::Undo, &mut PlumeRocket> for Like { impl AsObject<User, activity::Undo, &PlumeRocket> for Like {
type Error = Error; type Error = Error;
type Output = (); type Output = ();
fn activity(self, c: &mut PlumeRocket, actor: User, _id: &str) -> Result<()> { fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
let conn = &*c.conn; let conn = &*c.conn;
if actor.id == self.user_id { if actor.id == self.user_id {
diesel::delete(&self).execute(conn)?; diesel::delete(&self).execute(conn)?;
+20 -60
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};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
/// Represent what a list is supposed to store. Represented in database as an integer /// Represent what a list is supposed to store. Represented in database as an integer
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ListType { pub enum ListType {
User, User,
Blog, Blog,
@@ -58,11 +58,7 @@ 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),
@@ -70,11 +66,7 @@ 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,
@@ -82,11 +74,7 @@ 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,
@@ -94,11 +82,7 @@ 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,
@@ -115,11 +99,7 @@ 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);
@@ -136,11 +116,7 @@ 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);
@@ -156,11 +132,7 @@ 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);
@@ -274,35 +246,23 @@ impl List {
private::ListElem::prefix_in_list(conn, self, word) private::ListElem::prefix_in_list(conn, self, word)
} }
func! { /// Insert new users in a list
/// Insert new users in a list func! {add: add_users, User}
add: add_users, User
}
func! { /// Insert new blogs in a list
/// Insert new blogs in a list func! {add: add_blogs, Blog}
add: add_blogs, Blog
}
func! { /// Insert new words in a list
/// Insert new words in a list func! {add: add_words, Word}
add: add_words, Word
}
func! { /// Insert new prefixes in a list
/// Insert new prefixes in a list func! {add: add_prefixes, Prefix}
add: add_prefixes, Prefix
}
func! { /// Get all users in the list
/// Get all users in the list func! {list: list_users, User, users}
list: list_users, User, users
}
func! { /// Get all blogs in the list
/// Get all blogs in the list func! {list: list_blogs, Blog, blogs}
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>> {
+22 -21
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,8 @@ impl Media {
} }
// TODO: merge with save_remote? // TODO: merge with save_remote?
pub async fn from_activity(c: &mut PlumeRocket, image: &Image) -> Result<Media> { pub fn from_activity(c: &PlumeRocket, image: &Image) -> Result<Media> {
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
.rsplit('.') .rsplit('.')
@@ -210,26 +211,14 @@ impl Media {
ext ext
)); ));
let mut dest = tokio::fs::File::create(path.clone()).await?; let mut dest = fs::File::create(path.clone()).ok()?;
let contents = reqwest::get(remote_url.as_str()).await?.bytes().await?; reqwest::get(remote_url.as_str())
dest.write_all(&contents).await?; .ok()?
.copy_to(&mut dest)
let owner_id = User::from_id( .ok()?;
c,
image
.object_props
.attributed_to_link_vec::<Id>()
.ok()?
.into_iter()
.next()?
.as_ref(),
None,
)
.map_err(|(_, e)| e)?
.id;
Media::insert( Media::insert(
&mut c.conn, conn,
NewMedia { NewMedia {
file_path: path.to_str()?.to_string(), file_path: path.to_str()?.to_string(),
alt_text: image.object_props.content_string().ok()?, alt_text: image.object_props.content_string().ok()?,
@@ -237,7 +226,19 @@ impl Media {
remote_url: None, remote_url: None,
sensitive: image.object_props.summary_string().is_ok(), sensitive: image.object_props.summary_string().is_ok(),
content_warning: image.object_props.summary_string().ok(), content_warning: image.object_props.summary_string().ok(),
owner_id owner_id: User::from_id(
c,
image
.object_props
.attributed_to_link_vec::<Id>()
.ok()?
.into_iter()
.next()?
.as_ref(),
None,
)
.map_err(|(_, e)| e)?
.id,
}, },
) )
} }
+2 -2
View File
@@ -52,8 +52,8 @@ impl Mention {
} }
} }
pub async fn build_activity(c: &mut PlumeRocket, ment: &str) -> Result<link::Mention> { pub fn build_activity(c: &PlumeRocket, ment: &str) -> Result<link::Mention> {
let user = User::find_by_fqn(c, ment).await?; let user = User::find_by_fqn(c, ment)?;
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))?;
+14 -30
View File
@@ -20,35 +20,20 @@ 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 = ();
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> request::Outcome<PlumeRocket, ()> {
let conn = DbConn::from_request(request).await.succeeded().unwrap(); let conn = request.guard::<DbConn>()?;
let intl = rocket_i18n::I18n::from_request(request) let intl = request.guard::<rocket_i18n::I18n>()?;
.await let user = request.guard::<users::User>().succeeded();
.succeeded() let worker = request.guard::<'_, State<'_, Arc<ScheduledThreadPool>>>()?;
.unwrap(); let searcher = request.guard::<'_, State<'_, Arc<search::Searcher>>>()?;
let user = users::User::from_request(request) let flash_msg = request.guard::<FlashMessage<'_, '_>>().succeeded();
.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: Some(user), 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(),
@@ -75,18 +60,17 @@ 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 = ();
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> request::Outcome<PlumeRocket, ()> {
let conn = try_outcome!(DbConn::from_request(request).await); let conn = request.guard::<DbConn>()?;
let user = try_outcome!(users::User::from_request(request).await); let user = request.guard::<users::User>().succeeded();
let worker = try_outcome!(request.guard::<'_, State<'_, Arc<ScheduledThreadPool>>>()); let worker = request.guard::<'_, State<'_, Arc<ScheduledThreadPool>>>()?;
let searcher = try_outcome!(request.guard::<'_, State<'_, Arc<search::Searcher>>>()); let searcher = request.guard::<'_, State<'_, Arc<search::Searcher>>>()?;
Outcome::Success(PlumeRocket { Outcome::Success(PlumeRocket {
conn, conn,
user: Some(user), user,
worker: worker.clone(), worker: worker.clone(),
searcher: searcher.clone(), searcher: searcher.clone(),
}) })
+29 -33
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>;
@@ -554,11 +554,12 @@ impl FromId<PlumeRocket> for Post {
type Error = Error; type Error = Error;
type Object = LicensedArticle; type Object = LicensedArticle;
fn from_db(c: &mut PlumeRocket, id: &str) -> Result<Self> { fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Self::find_by_ap_url(&c.conn, id) Self::find_by_ap_url(&c.conn, id)
} }
fn from_activity(c: &mut PlumeRocket, article: LicensedArticle) -> Result<Self> { fn from_activity(c: &PlumeRocket, article: LicensedArticle) -> Result<Self> {
let conn = &*c.conn;
let searcher = &c.searcher; let searcher = &c.searcher;
let license = article.custom_props.license_string().unwrap_or_default(); let license = article.custom_props.license_string().unwrap_or_default();
let article = article.object; let article = article.object;
@@ -569,24 +570,24 @@ impl FromId<PlumeRocket> for Post {
.into_iter() .into_iter()
.fold((None, vec![]), |(blog, mut authors), link| { .fold((None, vec![]), |(blog, mut authors), link| {
let url = link; let url = link;
match User::from_id(&mut c, &url, None) { match User::from_id(&c, &url, None) {
Ok(u) => { Ok(u) => {
authors.push(u); authors.push(u);
(blog, authors) (blog, authors)
} }
Err(_) => (blog.or_else(|| Blog::from_id(&mut c, &url, None).ok()), authors), Err(_) => (blog.or_else(|| Blog::from_id(&c, &url, None).ok()), authors),
} }
}); });
let image = article.object_props.icon_object::<Image>().ok().unwrap(); let cover = article
.object_props
let mut r = Runtime::new().unwrap(); .icon_object::<Image>()
let cover = .ok()
Some(r.block_on(async { Media::from_activity(&mut c, &image).await.ok().unwrap().id })); .and_then(|img| Media::from_activity(&c, &img).ok().map(|m| m.id));
let title = article.object_props.name_string()?; let title = article.object_props.name_string()?;
let post = Post::insert( let post = Post::insert(
&mut c.conn, conn,
NewPost { NewPost {
blog_id: blog?.id, blog_id: blog?.id,
slug: title.to_kebab_case(), slug: title.to_kebab_case(),
@@ -609,7 +610,7 @@ impl FromId<PlumeRocket> for Post {
for author in authors { for author in authors {
PostAuthor::insert( PostAuthor::insert(
&mut c.conn, conn,
NewPostAuthor { NewPostAuthor {
post_id: post.id, post_id: post.id,
author_id: author.id, author_id: author.id,
@@ -626,7 +627,7 @@ impl FromId<PlumeRocket> for Post {
if let Some(serde_json::Value::Array(tags)) = article.object_props.tag { if let Some(serde_json::Value::Array(tags)) = article.object_props.tag {
for tag in tags { for tag in tags {
serde_json::from_value::<link::Mention>(tag.clone()) serde_json::from_value::<link::Mention>(tag.clone())
.map(|m| Mention::from_activity(&mut c.conn, &m, post.id, true, true)) .map(|m| Mention::from_activity(conn, &m, post.id, true, true))
.ok(); .ok();
serde_json::from_value::<Hashtag>(tag.clone()) serde_json::from_value::<Hashtag>(tag.clone())
@@ -634,7 +635,7 @@ impl FromId<PlumeRocket> for Post {
.and_then(|t| { .and_then(|t| {
let tag_name = t.name_string()?; let tag_name = t.name_string()?;
Ok(Tag::from_activity( Ok(Tag::from_activity(
&mut c.conn, conn,
&t, &t,
post.id, post.id,
hashtags.remove(&tag_name), hashtags.remove(&tag_name),
@@ -650,21 +651,21 @@ impl FromId<PlumeRocket> for Post {
} }
} }
impl AsObject<User, Create, &mut PlumeRocket> for Post { impl AsObject<User, Create, &PlumeRocket> for Post {
type Error = Error; type Error = Error;
type Output = Post; type Output = Post;
fn activity(self, _c: &mut PlumeRocket, _actor: User, _id: &str) -> Result<Post> { fn activity(self, _c: &PlumeRocket, _actor: User, _id: &str) -> Result<Post> {
// TODO: check that _actor is actually one of the author? // TODO: check that _actor is actually one of the author?
Ok(self) Ok(self)
} }
} }
impl AsObject<User, Delete, &mut PlumeRocket> for Post { impl AsObject<User, Delete, &PlumeRocket> for Post {
type Error = Error; type Error = Error;
type Output = (); type Output = ();
fn activity(self, c: &mut PlumeRocket, actor: User, _id: &str) -> Result<()> { fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
let can_delete = self let can_delete = self
.get_authors(&c.conn)? .get_authors(&c.conn)?
.into_iter() .into_iter()
@@ -692,28 +693,23 @@ impl FromId<PlumeRocket> for PostUpdate {
type Error = Error; type Error = Error;
type Object = LicensedArticle; type Object = LicensedArticle;
fn from_db(_: &mut PlumeRocket, _: &str) -> Result<Self> { fn from_db(_: &PlumeRocket, _: &str) -> Result<Self> {
// Always fail because we always want to deserialize the AP object // Always fail because we always want to deserialize the AP object
Err(Error::NotFound) Err(Error::NotFound)
} }
fn from_activity(c: &mut 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(&mut 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, cover: updated
.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
@@ -726,11 +722,11 @@ impl FromId<PlumeRocket> for PostUpdate {
} }
} }
impl AsObject<User, Update, &mut PlumeRocket> for PostUpdate { impl AsObject<User, Update, &PlumeRocket> for PostUpdate {
type Error = Error; type Error = Error;
type Output = (); type Output = ();
fn activity(self, c: &mut PlumeRocket, actor: User, _id: &str) -> Result<()> { fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
let conn = &*c.conn; let conn = &*c.conn;
let searcher = &c.searcher; let searcher = &c.searcher;
let mut post = Post::from_id(c, &self.ap_url, None).map_err(|(_, e)| e)?; let mut post = Post::from_id(c, &self.ap_url, None).map_err(|(_, e)| e)?;
+6 -6
View File
@@ -107,11 +107,11 @@ impl Reshare {
} }
} }
impl AsObject<User, Announce, &mut PlumeRocket> for Post { impl AsObject<User, Announce, &PlumeRocket> for Post {
type Error = Error; type Error = Error;
type Output = Reshare; type Output = Reshare;
fn activity(self, c: &mut PlumeRocket, actor: User, id: &str) -> Result<Reshare> { fn activity(self, c: &PlumeRocket, actor: User, id: &str) -> Result<Reshare> {
let conn = &*c.conn; let conn = &*c.conn;
let reshare = Reshare::insert( let reshare = Reshare::insert(
conn, conn,
@@ -132,11 +132,11 @@ impl FromId<PlumeRocket> for Reshare {
type Error = Error; type Error = Error;
type Object = Announce; type Object = Announce;
fn from_db(c: &mut PlumeRocket, id: &str) -> Result<Self> { fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Reshare::find_by_ap_url(&c.conn, id) Reshare::find_by_ap_url(&c.conn, id)
} }
fn from_activity(c: &mut PlumeRocket, act: Announce) -> Result<Self> { fn from_activity(c: &PlumeRocket, act: Announce) -> Result<Self> {
let res = Reshare::insert( let res = Reshare::insert(
&c.conn, &c.conn,
NewReshare { NewReshare {
@@ -154,11 +154,11 @@ impl FromId<PlumeRocket> for Reshare {
} }
} }
impl AsObject<User, Undo, &mut PlumeRocket> for Reshare { impl AsObject<User, Undo, &PlumeRocket> for Reshare {
type Error = Error; type Error = Error;
type Output = (); type Output = ();
fn activity(self, c: &mut PlumeRocket, actor: User, _id: &str) -> Result<()> { fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
let conn = &*c.conn; let conn = &*c.conn;
if actor.id == self.user_id { if actor.id == self.user_id {
diesel::delete(&self).execute(conn)?; diesel::delete(&self).execute(conn)?;
+74 -12
View File
@@ -3,10 +3,11 @@ mod searcher;
mod tokenizer; mod tokenizer;
pub use self::query::PlumeQuery as Query; pub use self::query::PlumeQuery as Query;
pub use self::searcher::*; pub use self::searcher::*;
pub use self::tokenizer::TokenizerKind;
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use super::{Query, Searcher}; use super::{Query, Searcher, TokenizerKind};
use diesel::Connection; use diesel::Connection;
use plume_common::utils::random_hex; use plume_common::utils::random_hex;
use std::env::temp_dir; use std::env::temp_dir;
@@ -14,18 +15,20 @@ pub(crate) mod tests {
use crate::{ use crate::{
blogs::tests::fill_database, blogs::tests::fill_database,
config::SearchTokenizerConfig,
post_authors::*, post_authors::*,
posts::{NewPost, Post}, posts::{NewPost, Post},
safe_string::SafeString, safe_string::SafeString,
tests::db, tests::db,
CONFIG,
}; };
pub(crate) fn get_searcher() -> Searcher { pub(crate) fn get_searcher(tokenizers: &SearchTokenizerConfig) -> Searcher {
let dir = temp_dir().join(&format!("plume-test-{}", random_hex())); let dir = temp_dir().join(&format!("plume-test-{}", random_hex()));
if dir.exists() { if dir.exists() {
Searcher::open(&dir) Searcher::open(&dir, tokenizers)
} else { } else {
Searcher::create(&dir) Searcher::create(&dir, tokenizers)
} }
.unwrap() .unwrap()
} }
@@ -100,27 +103,27 @@ pub(crate) mod tests {
fn open() { fn open() {
let dir = temp_dir().join(format!("plume-test-{}", random_hex())); let dir = temp_dir().join(format!("plume-test-{}", random_hex()));
{ {
Searcher::create(&dir).unwrap(); Searcher::create(&dir, &CONFIG.search_tokenizers).unwrap();
} }
Searcher::open(&dir).unwrap(); Searcher::open(&dir, &CONFIG.search_tokenizers).unwrap();
} }
#[test] #[test]
fn create() { fn create() {
let dir = temp_dir().join(format!("plume-test-{}", random_hex())); let dir = temp_dir().join(format!("plume-test-{}", random_hex()));
assert!(Searcher::open(&dir).is_err()); assert!(Searcher::open(&dir, &CONFIG.search_tokenizers).is_err());
{ {
Searcher::create(&dir).unwrap(); Searcher::create(&dir, &CONFIG.search_tokenizers).unwrap();
} }
Searcher::open(&dir).unwrap(); //verify it's well created Searcher::open(&dir, &CONFIG.search_tokenizers).unwrap(); //verify it's well created
} }
#[test] #[test]
fn search() { fn search() {
let conn = &db(); let conn = &db();
conn.test_transaction::<_, (), _>(|| { conn.test_transaction::<_, (), _>(|| {
let searcher = get_searcher(); let searcher = get_searcher(&CONFIG.search_tokenizers);
let blog = &fill_database(conn).1[0]; let blog = &fill_database(conn).1[0];
let author = &blog.list_authors(conn).unwrap()[0]; let author = &blog.list_authors(conn).unwrap()[0];
@@ -180,10 +183,69 @@ pub(crate) mod tests {
}); });
} }
#[cfg(feature = "search-lindera")]
#[test]
fn search_japanese() {
let conn = &db();
conn.test_transaction::<_, (), _>(|| {
let tokenizers = SearchTokenizerConfig {
tag_tokenizer: TokenizerKind::Lindera,
content_tokenizer: TokenizerKind::Lindera,
property_tokenizer: TokenizerKind::Ngram,
};
let searcher = get_searcher(&tokenizers);
let blog = &fill_database(conn).1[0];
let title = random_hex()[..8].to_owned();
let post = Post::insert(
conn,
NewPost {
blog_id: blog.id,
slug: title.clone(),
title: title.clone(),
content: SafeString::new("ブログエンジンPlumeです。"),
published: true,
license: "CC-BY-SA".to_owned(),
ap_url: "".to_owned(),
creation_date: None,
subtitle: "".to_owned(),
source: "".to_owned(),
cover_id: None,
},
&searcher,
)
.unwrap();
searcher.commit();
assert_eq!(
searcher.search_document(conn, Query::from_str("ブログエンジン").unwrap(), (0, 1))
[0]
.id,
post.id
);
assert_eq!(
searcher.search_document(conn, Query::from_str("Plume").unwrap(), (0, 1))[0].id,
post.id
);
assert_eq!(
searcher.search_document(conn, Query::from_str("です").unwrap(), (0, 1))[0].id,
post.id
);
assert_eq!(
searcher.search_document(conn, Query::from_str("").unwrap(), (0, 1))[0].id,
post.id
);
Ok(())
});
}
#[test] #[test]
fn drop_writer() { fn drop_writer() {
let searcher = get_searcher(); let searcher = get_searcher(&CONFIG.search_tokenizers);
searcher.drop_writer(); searcher.drop_writer();
get_searcher(); get_searcher(&CONFIG.search_tokenizers);
} }
} }
+32 -37
View File
@@ -1,18 +1,14 @@
use crate::{ use crate::{
instance::Instance, config::SearchTokenizerConfig, instance::Instance, posts::Post, schema::posts,
posts::Post, search::query::PlumeQuery, tags::Tag, Connection, Result,
schema::posts,
search::{query::PlumeQuery, tokenizer},
tags::Tag,
Connection, Result,
}; };
use chrono::Datelike; use chrono::Datelike;
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use itertools::Itertools; use itertools::Itertools;
use std::{cmp, fs::create_dir_all, path::Path, sync::Mutex}; use std::{cmp, fs::create_dir_all, path::Path, sync::Mutex};
use tantivy::{ use tantivy::{
collector::TopDocs, directory::MmapDirectory, schema::*, tokenizer::*, Index, IndexReader, collector::TopDocs, directory::MmapDirectory, schema::*, Index, IndexReader, IndexWriter,
IndexWriter, ReloadPolicy, Term, ReloadPolicy, Term,
}; };
use whatlang::{detect as detect_lang, Lang}; use whatlang::{detect as detect_lang, Lang};
@@ -34,7 +30,7 @@ impl Searcher {
pub fn schema() -> Schema { pub fn schema() -> Schema {
let tag_indexing = TextOptions::default().set_indexing_options( let tag_indexing = TextOptions::default().set_indexing_options(
TextFieldIndexing::default() TextFieldIndexing::default()
.set_tokenizer("whitespace_tokenizer") .set_tokenizer("tag_tokenizer")
.set_index_option(IndexRecordOption::Basic), .set_index_option(IndexRecordOption::Basic),
); );
@@ -70,15 +66,7 @@ impl Searcher {
schema_builder.build() schema_builder.build()
} }
pub fn create(path: &dyn AsRef<Path>) -> Result<Self> { pub fn create(path: &dyn AsRef<Path>, tokenizers: &SearchTokenizerConfig) -> Result<Self> {
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser);
let content_tokenizer = SimpleTokenizer
.filter(RemoveLongFilter::limit(40))
.filter(LowerCaser);
let property_tokenizer = NgramTokenizer::new(2, 8, false).filter(LowerCaser);
let schema = Self::schema(); let schema = Self::schema();
create_dir_all(path).map_err(|_| SearcherError::IndexCreationError)?; create_dir_all(path).map_err(|_| SearcherError::IndexCreationError)?;
@@ -90,9 +78,9 @@ impl Searcher {
{ {
let tokenizer_manager = index.tokenizers(); let tokenizer_manager = index.tokenizers();
tokenizer_manager.register("whitespace_tokenizer", whitespace_tokenizer); tokenizer_manager.register("tag_tokenizer", tokenizers.tag_tokenizer);
tokenizer_manager.register("content_tokenizer", content_tokenizer); tokenizer_manager.register("content_tokenizer", tokenizers.content_tokenizer);
tokenizer_manager.register("property_tokenizer", property_tokenizer); tokenizer_manager.register("property_tokenizer", tokenizers.property_tokenizer);
} //to please the borrow checker } //to please the borrow checker
Ok(Self { Ok(Self {
writer: Mutex::new(Some( writer: Mutex::new(Some(
@@ -109,31 +97,38 @@ impl Searcher {
}) })
} }
pub fn open(path: &dyn AsRef<Path>) -> Result<Self> { pub fn open(path: &dyn AsRef<Path>, tokenizers: &SearchTokenizerConfig) -> Result<Self> {
let whitespace_tokenizer = tokenizer::WhitespaceTokenizer.filter(LowerCaser); let mut index =
let content_tokenizer = SimpleTokenizer
.filter(RemoveLongFilter::limit(40))
.filter(LowerCaser);
let property_tokenizer = NgramTokenizer::new(2, 8, false).filter(LowerCaser);
let index =
Index::open(MmapDirectory::open(path).map_err(|_| SearcherError::IndexOpeningError)?) Index::open(MmapDirectory::open(path).map_err(|_| SearcherError::IndexOpeningError)?)
.map_err(|_| SearcherError::IndexOpeningError)?; .map_err(|_| SearcherError::IndexOpeningError)?;
{ {
let tokenizer_manager = index.tokenizers(); let tokenizer_manager = index.tokenizers();
tokenizer_manager.register("whitespace_tokenizer", whitespace_tokenizer); tokenizer_manager.register("tag_tokenizer", tokenizers.tag_tokenizer);
tokenizer_manager.register("content_tokenizer", content_tokenizer); tokenizer_manager.register("content_tokenizer", tokenizers.content_tokenizer);
tokenizer_manager.register("property_tokenizer", property_tokenizer); tokenizer_manager.register("property_tokenizer", tokenizers.property_tokenizer);
} //to please the borrow checker } //to please the borrow checker
let mut writer = index let writer = index
.writer(50_000_000) .writer(50_000_000)
.map_err(|_| SearcherError::WriteLockAcquisitionError)?; .map_err(|_| SearcherError::WriteLockAcquisitionError)?;
writer
.garbage_collect_files() // Since Tantivy v0.12.0, IndexWriter::garbage_collect_files() returns Future.
// To avoid conflict with Plume async project, we don't introduce async now.
// After async is introduced to Plume, we can use garbage_collect_files() again.
// Algorithm stolen from Tantivy's SegmentUpdater::list_files()
use std::collections::HashSet;
use std::path::PathBuf;
let mut files: HashSet<PathBuf> = index
.list_all_segment_metas()
.into_iter()
.flat_map(|segment_meta| segment_meta.list_files())
.collect();
files.insert(Path::new("meta.json").to_path_buf());
index
.directory_mut()
.garbage_collect(|| files)
.map_err(|_| SearcherError::IndexEditionError)?; .map_err(|_| SearcherError::IndexEditionError)?;
Ok(Self { Ok(Self {
writer: Mutex::new(Some(writer)), writer: Mutex::new(Some(writer)),
reader: index reader: index
+34 -7
View File
@@ -1,5 +1,34 @@
#[cfg(feature = "search-lindera")]
use lindera_tantivy::tokenizer::LinderaTokenizer;
use std::str::CharIndices; use std::str::CharIndices;
use tantivy::tokenizer::{Token, TokenStream, Tokenizer}; use tantivy::tokenizer::*;
#[derive(Clone, Copy)]
pub enum TokenizerKind {
Simple,
Ngram,
Whitespace,
#[cfg(feature = "search-lindera")]
Lindera,
}
impl From<TokenizerKind> for TextAnalyzer {
fn from(tokenizer: TokenizerKind) -> TextAnalyzer {
use TokenizerKind::*;
match tokenizer {
Simple => TextAnalyzer::from(SimpleTokenizer)
.filter(RemoveLongFilter::limit(40))
.filter(LowerCaser),
Ngram => TextAnalyzer::from(NgramTokenizer::new(2, 8, false)).filter(LowerCaser),
Whitespace => TextAnalyzer::from(WhitespaceTokenizer).filter(LowerCaser),
#[cfg(feature = "search-lindera")]
Lindera => {
TextAnalyzer::from(LinderaTokenizer::new("decompose", "")).filter(LowerCaser)
}
}
}
}
/// Tokenize the text by splitting on whitespaces. Pretty much a copy of Tantivy's SimpleTokenizer, /// Tokenize the text by splitting on whitespaces. Pretty much a copy of Tantivy's SimpleTokenizer,
/// but not splitting on punctuation /// but not splitting on punctuation
@@ -12,15 +41,13 @@ pub struct WhitespaceTokenStream<'a> {
token: Token, token: Token,
} }
impl<'a> Tokenizer<'a> for WhitespaceTokenizer { impl Tokenizer for WhitespaceTokenizer {
type TokenStreamImpl = WhitespaceTokenStream<'a>; fn token_stream<'a>(&self, text: &'a str) -> BoxTokenStream<'a> {
BoxTokenStream::from(WhitespaceTokenStream {
fn token_stream(&self, text: &'a str) -> Self::TokenStreamImpl {
WhitespaceTokenStream {
text, text,
chars: text.char_indices(), chars: text.char_indices(),
token: Token::default(), token: Token::default(),
} })
} }
} }
impl<'a> WhitespaceTokenStream<'a> { impl<'a> WhitespaceTokenStream<'a> {
+2 -2
View File
@@ -208,7 +208,7 @@ impl Timeline {
.map_err(Error::from) .map_err(Error::from)
} }
pub fn add_to_all_timelines(rocket: &mut PlumeRocket, post: &Post, kind: Kind<'_>) -> Result<()> { pub fn add_to_all_timelines(rocket: &PlumeRocket, post: &Post, kind: Kind<'_>) -> Result<()> {
let timelines = timeline_definition::table let timelines = timeline_definition::table
.load::<Self>(rocket.conn.deref()) .load::<Self>(rocket.conn.deref())
.map_err(Error::from)?; .map_err(Error::from)?;
@@ -231,7 +231,7 @@ impl Timeline {
Ok(()) Ok(())
} }
pub fn matches(&self, rocket: &mut PlumeRocket, post: &Post, kind: Kind<'_>) -> Result<bool> { pub fn matches(&self, rocket: &PlumeRocket, post: &Post, kind: Kind<'_>) -> Result<bool> {
let query = TimelineQuery::parse(&self.query)?; let query = TimelineQuery::parse(&self.query)?;
query.matches(rocket, self, post, kind) query.matches(rocket, self, post, kind)
} }
+15 -35
View File
@@ -7,9 +7,7 @@ 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)]
@@ -162,7 +160,7 @@ enum TQ<'a> {
impl<'a> TQ<'a> { impl<'a> TQ<'a> {
fn matches( fn matches(
&self, &self,
rocket: &mut PlumeRocket, rocket: &PlumeRocket,
timeline: &Timeline, timeline: &Timeline,
post: &Post, post: &Post,
kind: Kind<'_>, kind: Kind<'_>,
@@ -207,7 +205,7 @@ enum Arg<'a> {
impl<'a> Arg<'a> { impl<'a> Arg<'a> {
pub fn matches( pub fn matches(
&self, &self,
rocket: &mut PlumeRocket, rocket: &PlumeRocket,
timeline: &Timeline, timeline: &Timeline,
post: &Post, post: &Post,
kind: Kind<'_>, kind: Kind<'_>,
@@ -232,7 +230,7 @@ enum WithList {
impl WithList { impl WithList {
pub fn matches( pub fn matches(
&self, &self,
rocket: &mut PlumeRocket, rocket: &PlumeRocket,
timeline: &Timeline, timeline: &Timeline,
post: &Post, post: &Post,
list: &List<'_>, list: &List<'_>,
@@ -297,33 +295,15 @@ impl WithList {
} }
} }
List::Array(list) => match self { List::Array(list) => match self {
WithList::Blog => { WithList::Blog => Ok(list
let mut rt = Runtime::new().unwrap(); .iter()
rt.block_on(async move { .filter_map(|b| Blog::find_by_fqn(rocket, b).ok())
Ok(stream::iter(list) .any(|b| b.id == post.blog_id)),
.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 => { Kind::Original => Ok(list
let mut rt = Runtime::new().unwrap(); .iter()
rt.block_on(async move { .filter_map(|a| User::find_by_fqn(rocket, a).ok())
Ok(stream::iter(list) .any(|a| post.is_author(&rocket.conn, a.id).unwrap_or(false))),
.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))
@@ -391,7 +371,7 @@ enum Bool {
impl Bool { impl Bool {
pub fn matches( pub fn matches(
&self, &self,
rocket: &mut PlumeRocket, rocket: &PlumeRocket,
timeline: &Timeline, timeline: &Timeline,
post: &Post, post: &Post,
kind: Kind<'_>, kind: Kind<'_>,
@@ -426,8 +406,8 @@ impl Bool {
} }
} }
Bool::HasCover => Ok(post.cover_id.is_some()), Bool::HasCover => Ok(post.cover_id.is_some()),
Bool::Local => Ok(post.get_blog(&rocket.conn)?.is_local()), Bool::Local => Ok(post.get_blog(&rocket.conn)?.is_local() && kind == Kind::Original),
Bool::All => Ok(true), Bool::All => Ok(kind == Kind::Original),
} }
} }
} }
@@ -662,7 +642,7 @@ impl<'a> TimelineQuery<'a> {
pub fn matches( pub fn matches(
&self, &self,
rocket: &mut PlumeRocket, rocket: &PlumeRocket,
timeline: &Timeline, timeline: &Timeline,
post: &Post, post: &Post,
kind: Kind<'_>, kind: Kind<'_>,
+79 -82
View File
@@ -11,6 +11,7 @@ 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::{
@@ -36,6 +37,7 @@ 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},
@@ -189,7 +191,7 @@ impl User {
.map_err(Error::from) .map_err(Error::from)
} }
pub async fn find_by_fqn(c: &mut PlumeRocket, fqn: &str) -> Result<User> { pub 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)
@@ -197,13 +199,12 @@ 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).await User::fetch_from_webfinger(c, fqn)
} }
} }
async fn fetch_from_webfinger(c: &mut PlumeRocket, acct: &str) -> Result<User> { 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")))
@@ -211,9 +212,8 @@ 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 async fn fetch_remote_interact_uri(acct: &str) -> Result<String> { pub 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)
} }
async fn fetch(url: &str) -> Result<CustomPerson> { fn fetch(url: &str) -> Result<CustomPerson> {
let res = ClientBuilder::new() let mut res = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5)) .connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()? .build()?
.get(url) .get(url)
.header( .header(
@@ -235,9 +235,8 @@ impl User {
.join(", "), .join(", "),
)?, )?,
) )
.send() .send()?;
.await?; let text = &res.text()?;
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)?;
@@ -245,48 +244,48 @@ impl User {
Ok(json) Ok(json)
} }
pub async fn fetch_from_url(c: &mut PlumeRocket, url: &str) -> Result<User> { pub fn fetch_from_url(c: &PlumeRocket, url: &str) -> Result<User> {
let json = User::fetch(url).await?; User::fetch(url).and_then(|json| User::from_activity(c, json))
User::from_activity(c, json)
} }
pub async fn refetch(&self, conn: &Connection) -> Result<()> { pub fn refetch(&self, conn: &Connection) -> Result<()> {
let json = User::fetch(&self.ap_url.clone()).await?; User::fetch(&self.ap_url.clone()).and_then(|json| {
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> {
@@ -357,10 +356,9 @@ 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>)> {
async fn fetch_outbox_page<T: Activity>(&self, url: &str) -> Result<(Vec<T>, Option<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(url) .get(url)
.header( .header(
@@ -372,9 +370,8 @@ impl User {
.join(", "), .join(", "),
)?, )?,
) )
.send() .send()?;
.await?; let text = &res.text()?;
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()
@@ -389,9 +386,9 @@ impl User {
}; };
Ok((items, next)) Ok((items, next))
} }
pub async fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> { pub fn fetch_outbox<T: Activity>(&self) -> Result<Vec<T>> {
let res = ClientBuilder::new() let mut res = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5)) .connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()? .build()?
.get(&self.outbox_url[..]) .get(&self.outbox_url[..])
.header( .header(
@@ -403,14 +400,13 @@ impl User {
.join(", "), .join(", "),
)?, )?,
) )
.send() .send()?;
.await?; let text = &res.text()?;
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).await { while let Ok((mut page, nxt)) = self.fetch_outbox_page(&next) {
if page.is_empty() { if page.is_empty() {
break; break;
} }
@@ -435,9 +431,9 @@ impl User {
} }
} }
pub async fn fetch_followers_ids(&self) -> Result<Vec<String>> { pub fn fetch_followers_ids(&self) -> Result<Vec<String>> {
let res = ClientBuilder::new() let mut res = ClientBuilder::new()
.connect_timeout(std::time::Duration::from_secs(5)) .connect_timeout(Some(std::time::Duration::from_secs(5)))
.build()? .build()?
.get(&self.followers_endpoint[..]) .get(&self.followers_endpoint[..])
.header( .header(
@@ -449,9 +445,8 @@ impl User {
.join(", "), .join(", "),
)?, )?,
) )
.send() .send()?;
.await?; let text = &res.text()?;
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()
@@ -794,12 +789,11 @@ 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 = ();
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> request::Outcome<User, ()> {
let conn = try_outcome!(DbConn::from_request(request).await); let conn = request.guard::<DbConn>()?;
request request
.cookies() .cookies()
.get_private(AUTH_COOKIE) .get_private(AUTH_COOKIE)
@@ -821,11 +815,11 @@ impl FromId<PlumeRocket> for User {
type Error = Error; type Error = Error;
type Object = CustomPerson; type Object = CustomPerson;
fn from_db(c: &mut PlumeRocket, id: &str) -> Result<Self> { fn from_db(c: &PlumeRocket, id: &str) -> Result<Self> {
Self::find_by_ap_url(&c.conn, id) Self::find_by_ap_url(&c.conn, id)
} }
fn from_activity(c: &mut PlumeRocket, acct: CustomPerson) -> Result<Self> { fn from_activity(c: &PlumeRocket, acct: CustomPerson) -> Result<Self> {
let url = Url::parse(&acct.object.object_props.id_string()?)?; let url = Url::parse(&acct.object.object_props.id_string()?)?;
let inst = url.host_str()?; let inst = url.host_str()?;
let instance = Instance::find_by_domain(&c.conn, inst).or_else(|_| { let instance = Instance::find_by_domain(&c.conn, inst).or_else(|_| {
@@ -917,7 +911,7 @@ impl FromId<PlumeRocket> for User {
} }
} }
impl AsActor<&mut PlumeRocket> for User { impl AsActor<&PlumeRocket> for User {
fn get_inbox_url(&self) -> String { fn get_inbox_url(&self) -> String {
self.inbox_url.clone() self.inbox_url.clone()
} }
@@ -933,11 +927,11 @@ impl AsActor<&mut PlumeRocket> for User {
} }
} }
impl AsObject<User, Delete, &mut PlumeRocket> for User { impl AsObject<User, Delete, &PlumeRocket> for User {
type Error = Error; type Error = Error;
type Output = (); type Output = ();
fn activity(self, c: &mut PlumeRocket, actor: User, _id: &str) -> Result<()> { fn activity(self, c: &PlumeRocket, actor: User, _id: &str) -> Result<()> {
if self.id == actor.id { if self.id == actor.id {
self.delete(&c.conn, &c.searcher).map(|_| ()) self.delete(&c.conn, &c.searcher).map(|_| ())
} else { } else {
@@ -1032,6 +1026,7 @@ impl NewUser {
pub(crate) mod tests { pub(crate) mod tests {
use super::*; use super::*;
use crate::{ use crate::{
config::CONFIG,
instance::{tests as instance_tests, Instance}, instance::{tests as instance_tests, Instance},
search::tests::get_searcher, search::tests::get_searcher,
tests::{db, rockets}, tests::{db, rockets},
@@ -1128,7 +1123,9 @@ pub(crate) mod tests {
let inserted = fill_database(conn); let inserted = fill_database(conn);
assert!(User::get(conn, inserted[0].id).is_ok()); assert!(User::get(conn, inserted[0].id).is_ok());
inserted[0].delete(conn, &get_searcher()).unwrap(); inserted[0]
.delete(conn, &get_searcher(&CONFIG.search_tokenizers))
.unwrap();
assert!(User::get(conn, inserted[0].id).is_err()); assert!(User::get(conn, inserted[0].id).is_err());
Ok(()) Ok(())
}); });
+1 -2
View File
@@ -10,8 +10,7 @@ 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 " "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"
"&& 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"
+1 -1
View File
@@ -1 +1 @@
nightly-2020-05-05 nightly-2020-01-15
+1 -3
View File
@@ -35,7 +35,6 @@ 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,
@@ -43,10 +42,9 @@ where
{ {
type Error = (); type Error = ();
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Authorization<A, S>, ()> { 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()) {
+20 -28
View File
@@ -4,6 +4,7 @@ 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};
@@ -25,31 +26,21 @@ impl From<std::option::NoneError> for ApiError {
} }
} }
#[rocket::async_trait]
impl<'r> Responder<'r> for ApiError { impl<'r> Responder<'r> for ApiError {
async fn respond_to(self, req: &'r Request<'_>) -> response::Result<'r> { fn respond_to(self, req: &Request<'_>) -> response::Result<'r> {
match self.0 { match self.0 {
Error::NotFound => { Error::NotFound => Json(json!({
Json(json!({ "error": "Not found"
"error": "Not found" }))
})) .respond_to(req),
.respond_to(req) Error::Unauthorized => Json(json!({
.await "error": "You are not authorized to access this resource"
} }))
Error::Unauthorized => { .respond_to(req),
Json(json!({ _ => Json(json!({
"error": "You are not authorized to access this resource" "error": "Server error"
})) }))
.respond_to(req) .respond_to(req),
.await
}
_ => {
Json(json!({
"error": "Server error"
}))
.respond_to(req)
.await
}
} }
} }
} }
@@ -64,16 +55,17 @@ pub struct OAuthRequest {
} }
#[get("/oauth2?<query..>")] #[get("/oauth2?<query..>")]
pub async fn oauth( pub 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 app = App::find_by_client_id(&rockets.conn, &query.client_id)?; let conn = &*rockets.conn;
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(&mut rockets, &query.username).await { if let Ok(user) = User::find_by_fqn(&rockets, &query.username) {
if user.auth(&query.password) { if user.auth(&query.password) {
let token = ApiToken::insert( let token = ApiToken::insert(
&rockets.conn, conn,
NewApiToken { NewApiToken {
app_id: app.id, app_id: app.id,
user_id: user.id, user_id: user.id,
@@ -93,7 +85,7 @@ pub async fn oauth(
// Making fake password verification to avoid different // Making fake password verification to avoid different
// response times that would make it possible to know // response times that would make it possible to know
// if a username is registered or not. // if a username is registered or not.
User::get(&rockets.conn, 1)?.auth(&query.password); User::get(conn, 1)?.auth(&query.password);
Ok(Json(json!({ Ok(Json(json!({
"error": "Invalid credentials" "error": "Invalid credentials"
}))) })))
+2 -2
View File
@@ -98,7 +98,7 @@ pub fn list(
} }
#[post("/posts", data = "<payload>")] #[post("/posts", data = "<payload>")]
pub async fn create( pub 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 async 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).await?, &Mention::build_activity(&rockets, &m)?,
post.id, post.id,
true, true,
true, true,
+22 -22
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 tokio::io::AsyncReadExt; use std::io::Read;
pub async fn handle_incoming( pub fn handle_incoming(
rockets: PlumeRocket, rockets: PlumeRocket,
data: SignedJson<serde_json::Value>, data: SignedJson<serde_json::Value>,
headers: Headers<'_>, headers: Headers<'_>,
@@ -32,7 +32,6 @@ pub async 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) {
@@ -74,31 +73,32 @@ impl<'a, T: Deserialize<'a>> FromData<'a> for SignedJson<T> {
type Owned = String; type Owned = String;
type Borrowed = str; type Borrowed = str;
fn transform<'r>(r: &'r Request, d: Data) -> TransformFuture<'r, Self::Owned, Self::Error> { fn transform(
Box::pin(async move { r: &Request<'_>,
let size_limit = r.limits().get("json").unwrap_or(JSON_LIMIT); d: Data,
let mut s = String::with_capacity(512); ) -> Transform<rocket::data::Outcome<Self::Owned, Self::Error>> {
let outcome = match d.open().take(size_limit).read_to_string(&mut s).await { let size_limit = r.limits().get("json").unwrap_or(JSON_LIMIT);
Ok(_) => Success(s), let mut s = String::with_capacity(512);
Err(e) => Failure((Status::BadRequest, JsonError::Io(e))), match d.open().take(size_limit).read_to_string(&mut s) {
}; Ok(_) => Transform::Borrowed(Success(s)),
Transform::Borrowed(outcome) Err(e) => Transform::Borrowed(Failure((Status::BadRequest, JsonError::Io(e)))),
}) }
} }
fn from_data( fn from_data(
_: &Request<'_>, _: &Request<'_>,
o: Transformed<'a, Self>, o: Transformed<'a, Self>,
) -> FromDataFuture<'a, Self, Self::Error> { ) -> rocket::data::Outcome<Self, Self::Error> {
Box::pin(async move { let string = o.borrowed()?;
let string = try_outcome!(o.borrowed()); match serde_json::from_str(&string) {
match serde_json::from_str(&string) { Ok(v) => Success(SignedJson(Digest::from_body(&string), Json(v))),
Ok(v) => Success(SignedJson(Digest::from_body(&string), Json(v))), Err(e) => {
Err(e) if e.is_data() => { if e.is_data() {
return Failure((Status::UnprocessableEntity, JsonError::Parse(string, e))) Failure((Status::UnprocessableEntity, JsonError::Parse(string, e)))
} else {
Failure((Status::BadRequest, JsonError::Parse(string, e)))
} }
Err(e) => Failure((Status::BadRequest, JsonError::Parse(string, e))),
} }
}) }
} }
} }
+24 -5
View File
@@ -1,5 +1,5 @@
#![allow(clippy::too_many_arguments)] #![allow(clippy::too_many_arguments)]
#![feature(proc_macro_hygiene, try_trait)] #![feature(decl_macro, proc_macro_hygiene, try_trait)]
#[macro_use] #[macro_use]
extern crate gettext_macros; extern crate gettext_macros;
@@ -9,7 +9,6 @@ extern crate rocket;
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;
@@ -20,13 +19,15 @@ 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};
use std::time::Duration; use std::time::Duration;
init_i18n!( init_i18n!(
"plume", ar, bg, ca, cs, de, en, eo, es, fr, gl, hi, hr, it, ja, nb, pl, pt, ro, ru, sr, sk, sv "plume", ar, bg, ca, cs, de, en, eo, es, fa, fr, gl, hi, hr, it, ja, nb, pl, pt, ro, ru, sr,
sk, sv
); );
mod api; mod api;
@@ -98,7 +99,7 @@ Then try to restart Plume.
let workpool = ScheduledThreadPool::with_name("worker {}", num_cpus::get()); let workpool = ScheduledThreadPool::with_name("worker {}", num_cpus::get());
// we want a fast exit here, so // we want a fast exit here, so
#[allow(clippy::match_wild_err_arm)] #[allow(clippy::match_wild_err_arm)]
let searcher = match UnmanagedSearcher::open(&CONFIG.search_index) { let searcher = match UnmanagedSearcher::open(&CONFIG.search_index, &CONFIG.search_tokenizers) {
Err(Error::Search(e)) => match e { Err(Error::Search(e)) => match e {
SearcherError::WriteLockAcquisitionError => panic!( SearcherError::WriteLockAcquisitionError => panic!(
r#" r#"
@@ -273,7 +274,25 @@ 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,]);
+18 -31
View File
@@ -19,14 +19,10 @@ use plume_models::{
}; };
#[get("/~/<name>?<page>", rank = 2)] #[get("/~/<name>?<page>", rank = 2)]
pub async fn details( pub fn details(name: String, page: Option<Page>, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
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).await?; let blog = Blog::find_by_fqn(&rockets, &name)?;
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)?;
@@ -42,12 +38,12 @@ pub async fn details(
} }
#[get("/~/<name>", rank = 1)] #[get("/~/<name>", rank = 1)]
pub async fn activity_details( pub 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).await?; let blog = Blog::find_by_fqn(&rockets, &name).ok()?;
Some(ActivityStream::new(blog.to_activity(&*rockets.conn).ok()?)) Some(ActivityStream::new(blog.to_activity(&*rockets.conn).ok()?))
} }
@@ -87,7 +83,7 @@ fn valid_slug(title: &str) -> Result<(), ValidationError> {
} }
#[post("/blogs/new", data = "<form>")] #[post("/blogs/new", data = "<form>")]
pub async fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> RespondOrRedirect { pub 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;
@@ -97,7 +93,7 @@ pub async fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> Res
Ok(_) => ValidationErrors::new(), Ok(_) => ValidationErrors::new(),
Err(e) => e, Err(e) => e,
}; };
if Blog::find_by_fqn(&rockets, &slug).await.is_ok() { if Blog::find_by_fqn(&rockets, &slug).is_ok() {
errors.add( errors.add(
"title", "title",
ValidationError { ValidationError {
@@ -147,11 +143,9 @@ pub async fn create(form: LenientForm<NewBlogForm>, rockets: PlumeRocket) -> Res
} }
#[post("/~/<name>/delete")] #[post("/~/<name>/delete")]
pub async fn delete(name: String, rockets: PlumeRocket) -> RespondOrRedirect { pub fn delete(name: String, rockets: PlumeRocket) -> RespondOrRedirect {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let blog = Blog::find_by_fqn(&rockets, &name) let blog = Blog::find_by_fqn(&rockets, &name).expect("blog::delete: blog not found");
.await
.expect("blog::delete: blog not found");
if rockets if rockets
.user .user
@@ -190,9 +184,9 @@ pub struct EditForm {
} }
#[get("/~/<name>/edit")] #[get("/~/<name>/edit")]
pub async fn edit(name: String, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> { pub 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).await?; let blog = Blog::find_by_fqn(&rockets, &name)?;
if rockets if rockets
.user .user
.clone() .clone()
@@ -239,16 +233,14 @@ fn check_media(conn: &Connection, id: i32, user: &User) -> bool {
} }
#[put("/~/<name>/edit", data = "<form>")] #[put("/~/<name>/edit", data = "<form>")]
pub async fn update( pub 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) let mut blog = Blog::find_by_fqn(&rockets, &name).expect("blog::update: blog not found");
.await
.expect("blog::update: blog not found");
if !rockets if !rockets
.user .user
.clone() .clone()
@@ -350,28 +342,23 @@ pub async fn update(
} }
#[get("/~/<name>/outbox")] #[get("/~/<name>/outbox")]
pub async fn outbox( pub fn outbox(name: String, rockets: PlumeRocket) -> Option<ActivityStream<OrderedCollection>> {
name: String, let blog = Blog::find_by_fqn(&rockets, &name).ok()?;
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 async fn outbox_page( pub 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).await?; let blog = Blog::find_by_fqn(&rockets, &name).ok()?;
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 async fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>> { pub fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>> {
let blog = Blog::find_by_fqn(&rockets, &name).await?; let blog = Blog::find_by_fqn(&rockets, &name).ok()?;
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()
+17 -23
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 async fn create( pub fn create(
blog_name: String, blog_name: String,
slug: String, slug: String,
form: LenientForm<NewCommentForm>, form: LenientForm<NewCommentForm>,
@@ -36,12 +36,10 @@ pub async 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) let blog = Blog::find_by_fqn(&rockets, &blog_name).expect("comments::create: blog error");
.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");
match form.validate() { form.validate()
Ok(_ok) => { .map(|_| {
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(
@@ -68,7 +66,6 @@ pub async 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
@@ -76,7 +73,6 @@ pub async 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,
@@ -94,14 +90,14 @@ pub async fn create(
.worker .worker
.execute(move || broadcast(&user_clone, new_comment, dest)); .execute(move || broadcast(&user_clone, new_comment, dest));
Ok(Flash::success( 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."),
)) )
} })
Err(errors) => { .map_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");
@@ -110,7 +106,7 @@ pub async fn create(
.responding_to .responding_to
.and_then(|r| Comment::get(&*conn, r).ok()); .and_then(|r| Comment::get(&*conn, r).ok());
Err(render!(posts::details( render!(posts::details(
&rockets.to_context(), &rockets.to_context(),
post.clone(), post.clone(),
blog, blog,
@@ -137,9 +133,8 @@ pub async 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")]
@@ -179,16 +174,15 @@ pub fn delete(
} }
#[get("/~/<_blog>/<_slug>/comment/<id>")] #[get("/~/<_blog>/<_slug>/comment/<id>")]
pub async fn activity_pub( pub 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>> {
let c = match Comment::get(&*rockets.conn, id) { Comment::get(&*rockets.conn, id)
Ok(c) => c.to_activity(&rockets).await.ok(), .and_then(|c| c.to_activity(&rockets))
Err(_) => None, .ok()
}; .map(ActivityStream::new)
c.map(ActivityStream::new)
} }
+11 -23
View File
@@ -1,7 +1,6 @@
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,
}; };
@@ -15,46 +14,35 @@ impl From<Error> for ErrorPage {
} }
} }
#[rocket::async_trait]
impl<'r> Responder<'r> for ErrorPage { impl<'r> Responder<'r> for ErrorPage {
async fn respond_to(self, req: &'r Request<'_>) -> response::Result<'r> { fn respond_to(self, req: &Request<'_>) -> response::Result<'r> {
let rockets = PlumeRocket::from_request(req).await.unwrap(); let rockets = req.guard::<PlumeRocket>().unwrap();
match self.0 { match self.0 {
Error::NotFound => { Error::NotFound => render!(errors::not_found(&rockets.to_context())).respond_to(req),
render!(errors::not_found(&rockets.to_context()))
.respond_to(req)
.await
}
Error::Unauthorized => { Error::Unauthorized => {
render!(errors::not_found(&rockets.to_context())) render!(errors::not_found(&rockets.to_context())).respond_to(req)
.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 async fn not_found(req: &Request<'_>) -> Ructe { pub fn not_found(req: &Request<'_>) -> Ructe {
let rockets = req.guard::<PlumeRocket>().await.unwrap(); let rockets = req.guard::<PlumeRocket>().unwrap();
render!(errors::not_found(&rockets.to_context())) render!(errors::not_found(&rockets.to_context()))
} }
#[catch(422)] #[catch(422)]
pub async fn unprocessable_entity(req: &Request<'_>) -> Ructe { pub fn unprocessable_entity(req: &Request<'_>) -> Ructe {
let rockets = req.guard::<PlumeRocket>().await.unwrap(); let rockets = req.guard::<PlumeRocket>().unwrap();
render!(errors::unprocessable_entity(&rockets.to_context())) render!(errors::unprocessable_entity(&rockets.to_context()))
} }
#[catch(500)] #[catch(500)]
pub async fn server_error(req: &Request<'_>) -> Ructe { pub fn server_error(req: &Request<'_>) -> Ructe {
let rockets = req.guard::<PlumeRocket>().await.unwrap(); let rockets = req.guard::<PlumeRocket>().unwrap();
render!(errors::server_error(&rockets.to_context())) render!(errors::server_error(&rockets.to_context()))
} }
+7 -11
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 std::path::PathBuf; use serde_json;
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,21 +386,17 @@ fn ban(
} }
#[post("/inbox", data = "<data>")] #[post("/inbox", data = "<data>")]
pub async fn shared_inbox( pub 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).await inbox::handle_incoming(rockets, data, headers)
} }
#[get("/remote_interact?<target>")] #[get("/remote_interact?<target>")]
pub async fn interact( pub fn interact(rockets: PlumeRocket, user: Option<User>, target: String) -> Option<Redirect> {
rockets: PlumeRocket, if User::find_by_fqn(&rockets, &target).is_ok() {
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)));
} }
+10 -11
View File
@@ -7,8 +7,9 @@ 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},
Header, RawStr, Status, 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},
@@ -94,11 +95,10 @@ 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 = ();
async fn from_request(r: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { 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,15 +208,14 @@ pub mod well_known;
#[response()] #[response()]
pub struct CachedFile { pub struct CachedFile {
inner: NamedFile, inner: NamedFile,
cache_control: Header<'static>, cache_control: CacheControl,
} }
#[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 {
async fn respond_to(self, r: &'r Request<'_>) -> response::Result<'r> { fn respond_to(self, 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();
@@ -229,12 +228,12 @@ impl<'r> Responder<'r> for ThemeFile {
{ {
Response::build() Response::build()
.status(Status::NotModified) .status(Status::NotModified)
.header(Header::new("ETag", etag)) .header(ETag(EntityTag::strong(etag)))
.ok() .ok()
} else { } else {
Response::build() Response::build()
.merge(self.0.respond_to(r).await.ok().unwrap()) .merge(self.0.respond_to(r)?)
.header(Header::new("ETag", etag)) .header(ETag(EntityTag::strong(etag)))
.ok() .ok()
} }
} }
@@ -257,7 +256,7 @@ pub fn plume_media_files(file: PathBuf) -> Option<CachedFile> {
.ok() .ok()
.map(|f| CachedFile { .map(|f| CachedFile {
inner: f, inner: f,
cache_control: Header::new("Cache-Control", format!("max-age={}", 60 * 60 * 24 * 30)), cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
}) })
} }
#[get("/static/<file..>", rank = 3)] #[get("/static/<file..>", rank = 3)]
@@ -266,6 +265,6 @@ pub fn static_files(file: PathBuf) -> Option<CachedFile> {
.ok() .ok()
.map(|f| CachedFile { .map(|f| CachedFile {
inner: f, inner: f,
cache_control: Header::new("Cache-Control", format!("max-age={}", 60 * 60 * 24 * 30)), cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]),
}) })
} }
+28 -40
View File
@@ -1,4 +1,3 @@
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;
@@ -12,7 +11,7 @@ use std::{
use validator::{Validate, ValidationError, ValidationErrors}; use validator::{Validate, ValidationError, ValidationErrors};
use crate::routes::{ use crate::routes::{
comments::NewCommentForm, errors::ErrorPage, ContentLen, Page, RemoteForm, RespondOrRedirect, comments::NewCommentForm, errors::ErrorPage, ContentLen, 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};
@@ -34,7 +33,7 @@ use plume_models::{
}; };
#[get("/~/<blog>/<slug>?<responding_to>", rank = 4)] #[get("/~/<blog>/<slug>?<responding_to>", rank = 4)]
pub async fn details( pub fn details(
blog: String, blog: String,
slug: String, slug: String,
responding_to: Option<i32>, responding_to: Option<i32>,
@@ -42,7 +41,7 @@ pub async 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).await?; let blog = Blog::find_by_fqn(&rockets, &blog)?;
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
@@ -100,14 +99,14 @@ pub async fn details(
} }
#[get("/~/<blog>/<slug>", rank = 3)] #[get("/~/<blog>/<slug>", rank = 3)]
pub async fn activity_details( pub 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).await.map_err(|_| None)?; let blog = Blog::find_by_fqn(&rockets, &blog).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(
@@ -131,9 +130,9 @@ pub fn new_auth(blog: String, i18n: I18n) -> Flash<Redirect> {
} }
#[get("/~/<blog>/new", rank = 1)] #[get("/~/<blog>/new", rank = 1)]
pub async fn new(blog: String, cl: ContentLen, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> { pub 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).await?; let b = Blog::find_by_fqn(&rockets, &blog)?;
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)? {
@@ -163,7 +162,7 @@ pub async fn new(blog: String, cl: ContentLen, rockets: PlumeRocket) -> Result<R
} }
#[get("/~/<blog>/<slug>/edit")] #[get("/~/<blog>/<slug>/edit")]
pub async fn edit( pub fn edit(
blog: String, blog: String,
slug: String, slug: String,
cl: ContentLen, cl: ContentLen,
@@ -171,7 +170,7 @@ pub async 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).await?; let b = Blog::find_by_fqn(&rockets, &blog)?;
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();
@@ -217,7 +216,7 @@ pub async fn edit(
} }
#[post("/~/<blog>/<slug>/edit", data = "<form>")] #[post("/~/<blog>/<slug>/edit", data = "<form>")]
pub async fn update( pub fn update(
blog: String, blog: String,
slug: String, slug: String,
cl: ContentLen, cl: ContentLen,
@@ -225,9 +224,7 @@ pub async fn update(
rockets: PlumeRocket, rockets: PlumeRocket,
) -> RespondOrRedirect { ) -> RespondOrRedirect {
let conn = &*rockets.conn; let conn = &*rockets.conn;
let b = Blog::find_by_fqn(&rockets, &blog) let b = Blog::find_by_fqn(&rockets, &blog).expect("post::update: blog error");
.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();
@@ -304,16 +301,14 @@ pub async fn update(
.expect("post::update: update error"); .expect("post::update: update error");
if post.published { if post.published {
// NOTE: here we unroll a filter_map(), so we can use .await painlessly post.update_mentions(
let mut filtered_mentions: Vec<link::Mention> = vec![]; &conn,
for m in mentions.into_iter() { mentions
match Mention::build_activity(&rockets, &m).await { .into_iter()
Ok(m) => filtered_mentions.push(m), .filter_map(|m| Mention::build_activity(&rockets, &m).ok())
Err(_) => {} .collect(),
} )
} .expect("post::update: mentions error");
post.update_mentions(&conn, filtered_mentions)
.expect("post::update: mentions error");
} }
let tags = form let tags = form
@@ -326,7 +321,7 @@ pub async 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(r#"post::update: tags error"#); .expect("post::update: tags error");
let hashtags = hashtags let hashtags = hashtags
.into_iter() .into_iter()
@@ -404,16 +399,14 @@ pub fn valid_slug(title: &str) -> Result<(), ValidationError> {
} }
#[post("/~/<blog_name>/new", data = "<form>")] #[post("/~/<blog_name>/new", data = "<form>")]
pub async fn create( pub 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) let blog = Blog::find_by_fqn(&rockets, &blog_name).expect("post::create: blog error");
.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();
@@ -527,7 +520,6 @@ pub async 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,
@@ -570,7 +562,7 @@ pub async fn create(
} }
#[post("/~/<blog_name>/<slug>/delete")] #[post("/~/<blog_name>/<slug>/delete")]
pub async fn delete( pub fn delete(
blog_name: String, blog_name: String,
slug: String, slug: String,
rockets: PlumeRocket, rockets: PlumeRocket,
@@ -578,7 +570,6 @@ pub async 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 {
@@ -624,13 +615,12 @@ pub async fn delete(
} }
#[get("/~/<blog_name>/<slug>/remote_interact")] #[get("/~/<blog_name>/<slug>/remote_interact")]
pub async fn remote_interact( pub 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(),
@@ -643,19 +633,17 @@ pub async fn remote_interact(
} }
#[post("/~/<blog_name>/<slug>/remote_interact", data = "<remote>")] #[post("/~/<blog_name>/<slug>/remote_interact", data = "<remote>")]
pub async fn remote_interact_post( pub 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 uri = User::fetch_remote_interact_uri(&remote.remote) if let Some(uri) = User::fetch_remote_interact_uri(&remote.remote)
.await .ok()
.map(|uri| uri.replace("{uri}", &format!("{}", target.ap_url))) .map(|uri| uri.replace("{uri}", &target.ap_url))
.unwrap()
{ {
Ok(Redirect::to(uri).into()) Ok(Redirect::to(uri).into())
} else { } else {
+2 -2
View File
@@ -10,14 +10,14 @@ use plume_models::{
}; };
#[post("/~/<blog>/<slug>/reshare")] #[post("/~/<blog>/<slug>/reshare")]
pub async fn create( pub 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).await?; let b = Blog::find_by_fqn(&rockets, &blog)?;
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)? {
+6 -8
View File
@@ -35,23 +35,21 @@ 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 async fn create( pub 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 = match User::find_by_email(&*conn, &form.email_or_name) { let user = User::find_by_email(&*conn, &form.email_or_name)
Ok(user) => Ok(user), .or_else(|_| User::find_by_fqn(&rockets, &form.email_or_name));
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,
@@ -199,7 +197,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 {
+51 -63
View File
@@ -9,6 +9,7 @@ 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};
@@ -41,7 +42,7 @@ pub fn me(user: Option<User>) -> RespondOrRedirect {
} }
#[get("/@/<name>", rank = 2)] #[get("/@/<name>", rank = 2)]
pub async fn details( pub fn details(
name: String, name: String,
rockets: PlumeRocket, rockets: PlumeRocket,
fetch_rockets: PlumeRocket, fetch_rockets: PlumeRocket,
@@ -49,7 +50,7 @@ pub async 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).await?; let user = User::find_by_fqn(&rockets, &name)?;
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;
@@ -60,7 +61,6 @@ pub async 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,7 +79,6 @@ pub async 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)
@@ -102,7 +101,6 @@ pub async 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");
}); });
} }
@@ -148,13 +146,13 @@ pub fn dashboard_auth(i18n: I18n) -> Flash<Redirect> {
} }
#[post("/@/<name>/follow")] #[post("/@/<name>/follow")]
pub async fn follow( pub 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).await?; let target = User::find_by_fqn(&rockets, &name)?;
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(
@@ -192,16 +190,15 @@ pub async fn follow(
} }
#[post("/@/<name>/follow", data = "<remote_form>", rank = 2)] #[post("/@/<name>/follow", data = "<remote_form>", rank = 2)]
pub async fn follow_not_connected( pub 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).await?; let target = User::find_by_fqn(&rockets, &name)?;
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| {
Some(uri.replace( Some(uri.replace(
@@ -268,18 +265,15 @@ pub fn follow_auth(name: String, i18n: I18n) -> Flash<Redirect> {
} }
#[get("/@/<name>/followers?<page>", rank = 2)] #[get("/@/<name>/followers?<page>", rank = 2)]
pub async fn followers( pub 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 = User::find_by_fqn(&rockets, &name).await?; let user = User::find_by_fqn(&rockets, &name)?;
let followers_count = match user.count_followers(&conn) { let followers_count = user.count_followers(&*conn)?;
Ok(num) => num,
Err(_) => 0,
};
Ok(render!(users::followers( Ok(render!(users::followers(
&rockets.to_context(), &rockets.to_context(),
@@ -298,14 +292,14 @@ pub async fn followers(
} }
#[get("/@/<name>/followed?<page>", rank = 2)] #[get("/@/<name>/followed?<page>", rank = 2)]
pub async fn followed( pub 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).await?; let user = User::find_by_fqn(&rockets, &name)?;
let followed_count = user.count_followed(conn)?; let followed_count = user.count_followed(conn)?;
Ok(render!(users::followed( Ok(render!(users::followed(
@@ -325,12 +319,12 @@ pub async fn followed(
} }
#[get("/@/<name>", rank = 1)] #[get("/@/<name>", rank = 1)]
pub async fn activity_details( pub fn activity_details(
name: String, name: String,
rockets: PlumeRocket, rockets: PlumeRocket,
_ap: ApRequest, _ap: ApRequest,
) -> Option<ActivityStream<CustomPerson>> { ) -> Option<ActivityStream<CustomPerson>> {
let user: User = User::find_by_fqn(&rockets, &name).await?; let user = User::find_by_fqn(&rockets, &name).ok()?;
Some(ActivityStream::new(user.to_activity(&*rockets.conn).ok()?)) Some(ActivityStream::new(user.to_activity(&*rockets.conn).ok()?))
} }
@@ -417,49 +411,50 @@ pub fn update(
} }
#[post("/@/<name>/delete")] #[post("/@/<name>/delete")]
pub async fn delete( pub 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).await?; let account = User::find_by_fqn(&rockets, &name)?;
if user.id != account.id { if user.id == account.id {
return Ok(Flash::error( 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."),
))
} 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 \""
@@ -468,9 +463,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,
} }
@@ -569,44 +564,37 @@ pub fn create(
} }
#[get("/@/<name>/outbox")] #[get("/@/<name>/outbox")]
pub async fn outbox( pub fn outbox(name: String, rockets: PlumeRocket) -> Option<ActivityStream<OrderedCollection>> {
name: String, let user = User::find_by_fqn(&rockets, &name).ok()?;
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 async fn outbox_page( pub 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).await.ok()?; let user = User::find_by_fqn(&rockets, &name).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 async fn inbox( pub 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) User::find_by_fqn(&rockets, &name).map_err(|_| status::BadRequest(Some("User not found")))?;
.await inbox::handle_incoming(rockets, data, headers)
.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 async fn ap_followers( pub 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).await?; let user = User::find_by_fqn(&rockets, &name).ok()?;
let followers = user let followers = user
.get_followers(&*rockets.conn) .get_followers(&*rockets.conn)
.ok()? .ok()?
@@ -626,9 +614,9 @@ pub async fn ap_followers(
} }
#[get("/@/<name>/atom.xml")] #[get("/@/<name>/atom.xml")]
pub async fn atom_feed(name: String, rockets: PlumeRocket) -> Option<Content<String>> { pub 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).await?; let author = User::find_by_fqn(&rockets, &name).ok()?;
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()?
+6 -36
View File
@@ -1,5 +1,6 @@
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};
@@ -42,58 +43,27 @@ pub fn host_meta() -> String {
struct WebfingerResolver; struct WebfingerResolver;
#[async_trait::async_trait] impl Resolver<PlumeRocket> for WebfingerResolver {
impl AsyncResolver for WebfingerResolver { fn instance_domain<'a>() -> &'a str {
type Repo = PlumeRocket;
async fn instance_domain<'a>(&self) -> &'a str {
CONFIG.base_url.as_str() CONFIG.base_url.as_str()
} }
async fn find( fn find(prefix: Prefix, acct: String, ctx: PlumeRocket) -> Result<Webfinger, ResolverError> {
&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 async fn webfinger(resource: String, rockets: PlumeRocket) -> Content<String> { pub fn webfinger(resource: String, rockets: PlumeRocket) -> Content<String> {
let wf_resolver = WebfingerResolver; match WebfingerResolver::endpoint(resource, rockets)
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),
+7 -7
View File
@@ -1,7 +1,8 @@
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::{Header, Method, Status}; use rocket::http::hyper::header::{ETag, EntityTag};
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;
@@ -51,12 +52,11 @@ 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 {
async fn respond_to(self, r: &'r Request<'_>) -> response::Result<'r> { fn respond_to(self, 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).await; return HtmlCt(self.0).respond_to(r);
} }
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(Header::new("ETag", etag)) .header(ETag(EntityTag::strong(etag)))
.ok() .ok()
} else { } else {
Response::build() Response::build()
.merge(HtmlCt(self.0).respond_to(r).await.ok().unwrap()) .merge(HtmlCt(self.0).respond_to(r)?)
.header(Header::new("ETag", etag)) .header(ETag(EntityTag::strong(etag)))
.ok() .ok()
} }
} }
-4
View File
@@ -1,12 +1,8 @@
@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)
-2
View File
@@ -2,9 +2,7 @@
@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::*;
-2
View File
@@ -5,12 +5,10 @@
@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), {}, {
-2
View File
@@ -3,9 +3,7 @@
@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::*;